JavaScript 2.0
根拠
構文
previousupnext

11/19/2002 (Tue)

この節では草案の開発中に考えられていた他の構文を幾つか示す。

セミコロンの挿入

定義

文間のセミコロンを省略してもプログラムが記述できるのは、暗黙のセミコロンの挿入があるためである。JavaScript 1.5 、JavaScript 2.0 はいずれも2種類のセミコロン挿入を持つ:

文法的セミコロンの挿入
閉じ括弧 } やプログラムの終端の前のセミコロンは JavaScript 1.5 、JavaScript 2.0 のいずれでも省略可能である。これに加えて JavaScript 2.0 パーサは if-else 文の else の前と do-while 文の while の前のセミコロンの省略も認めている。
改行におけるセミコロンの挿入
JavaScript プログラムの先頭から n 番目までのトークンは文法的に正しいが、n+1番目のトークンが誤っていて、且つ n 番目のトークンと n+1番目のトークンの間に改行がある場合は、パーサは n 番目のトークンと n+1番目のトークンの間に VirtualSemicolon を挿入してからプログラムをもう1度パースしようとする。

文法的セミコロンの挿入は構文文法の生成規則により直接実装 (実現) され、上記の場合は単純にセミコロンを必要としない。ソースコード中の改行は文法的セミコロンの挿入には無関係である。

改行におけるセミコロンの挿入を構文文法で実装 (実現) するのは容易ではない。この種のセミコロンの挿入は構文的に誤ったプログラムを正しいプログラムに変えてしまい、ソースコードの改行にも依存する。

議論

文法的セミコロンの挿入は無害なものである。一方、改行におけるセミコロンの挿入には以下に示す問題がある:

  1. 改行におけるセミコロンの挿入はプログラムのソースコードにおける改行に依存してしまう
  2. この種のセミコロン挿入の結果はプログラマの考えと一致しない
  3. 新しい構文が導入されると既存のプログラムの挙動は不意に変わってしまうことがある

最初の問題は、XML 属性を扱い、改行を空白に変換するプリプロセッサの構築を困難にする。2番目と3番目の問題はより深刻である。プログラマは

a = b + c
(d + e).print()

というプログラムが以下のようになると考えるが

a = b + c;
(d + e).print();

実際にはこのプログラムは以下のようにパースされる:

a = b + c(d + e).print();

このことが分かるとプログラマは混乱する。

3番目の問題は最も深刻なものである。新しい機能が言語に追加されると誤った構文は正しいものになる。仮に既存のプログラムが改行におけるセミコロンの挿入を発生させるために誤った構文を使っているとしたら、機能が追加された途端、暗黙の内にそのプログラムの挙動は変更されてしまう。例えば (4 "in" のように) 数値リテラルの後ろに文字列リテラルを並べて書くと JavaScript 1.5 では不正だが、JavaScript 2.0 においては単位式として構文的に正しいものである。残念ながらこの構文の拡張は次のような JavaScript 1.5 プログラムの意味を変更してしまう:

a = b + 4
"in".print()

JavaScript 1.5 で以下のようになっていたものが

a = b + 4;
"in".print();

JavaScript 2.0 では次のようになる

a = b + 4"in".print();

JavaScript 2.0 では数値リテラルと文字列リテラルを同じ行に置くことを要求する [no line break] 制限を文法に追加することにより、この不一致を克服している。残念ながらこの互換性は諸刃の剣であり、JavaScript 1.5 との互換性のために JavaScript 2.0 は非常に多くの [no line break] 制限を持たなければならなくなる。その全てを漏らさず設定することは困難であり、1つでも忘れると JavaScript 2.0 プログラムで (先に述べた) 再解釈が起こることになる。また、

internal
  function f(x) {return x*x}

というコードが

internal function f(x) {return x*x}

のようにならず、

internal;
function f(x) {return x*x}

となることでプログラマは驚くだろう (internal; は式文である) 。

JavaScript 2.0 の初期のバージョンでは改行におけるセミコロンの挿入を認めていなかった。現在は非 strict モードにおいてのみ認められており、strict モードでは言語の単純化のために [no line break] 制限は全て取り除かれる。これらの副作用として strict モードと非 strict モードで異なる振る舞いをするプログラム (上に挙げた例のような) を書くことができるが、これは単純さのための代価に過ぎない。

正規表現リテラル

JavaScript 2.0 は正規表現リテラルを検出する規則に JavaScript 1.5 と同じものを採用することで互換性を保持している。この方法ではパースせずに JavaScript プログラムから全てのトークンを見つけることが不可能になるため、構文指向テキストエディタやマシンスキャナといったプログラムの設計が困難になる。

JavaScript 2.0 の字句文法を構文文法から独立させた場合、ツールは JavaScript プログラムを簡単に扱えるようになり、例えば JavaScript 2.0 或いはそれ以降のプログラムを正確に HTML ページに埋め込むために、例えば </ といったものを全て避けることができる。完全なパーサは JavaScript のバージョン毎に変わる。このような困難を理解するには以下の JavaScript 1.5 コード片を比較するとよい:

for (var x = a in foo && "</x>" || mot ? z:/x:3;x<5;y</g/i) {xyz(x++);}
for (var x = a in foo && "</x>" || mot ? z/x:3;x<5;y</g/i) {xyz(x++);}

正規表現の代替構文

JavaScript 2.0 の設計で早期に検討されていた代替案に、正規表現の明確な構文と新しい構文の使用があった。RegularExpression はその開始、終了デリミタに // の代わりに «» を使用することが明確に示されてた。例えば «3*» は0個以上の 3 にマッチする正規表現である。«» とすると空の正規表現となり、空文字列だけにマッチする。一方 // はコメントの開始になる。JavaScript 1.5 で空の正規表現を記述する場合は /(?:)/ としなければならない。

構文の再同期

JavaScript の将来のバージョンの機能で記述されたプログラムの部分をレキシカルアナライザが読み飛ばすためにブロックの終点 (} が相当する) を探さなければならないとき、構文の再同期が起こる。通常これは問題では無いが、正規表現により字句解析が構文解析に依存するようになるので事は複雑になる。正規表現リテラルの解釈に使う規則はプログラムの各部分において変えなければならない。以下の規則は正しく機能するか、或いはその簡易解析は正規表現の位置を決定するための入力に基づいて処理される。これについては更なる作業が必要である。

構文の再同期において JavaScript 2.0 は / が正規表現の開始なのか、除算 (或いは /=) 演算子なのかを前のトークンだけに基づき決定する。

/ の解釈 前のトークン
//=   Identifier   Number   RegularExpression   String
)   ++   --   ]   }
class   false   null   private   protected   public   super   this   true
get   include   set
その他の区切り子
RegularExpression   !   !=   !==   #   %   %=   &   &&   &&=   &=   (   *=   +   +=   ,   -   -=   ->   .   ..   ...   /   /=   :   ::   ;   <   <<   <<=   <=   =   ==   ===   >   >=   >>   >>=   >>>   >>>=   ?   @   [   ^   ^=   ^^   ^^=   {   |   |=   ||   ||=   ~
abstract   break   case   catch   const   continue   debugger   default   delete   do   else   enum   export   extends   final   finally   for   function   goto   if   implements   import   in   instanceof   interface   is   namespace   native   new   package   return   static   switch   synchronized   throw   throws   transient   try   typeof   use   var   volatile   while   with

// は前のトークンに関係無く、コメントの開始と解釈される。

ただ1つ議論の余地のある選択は )} である。)} の後ろの / は除算のシンボル ()} が部分式かオブジェクトリテラルを閉じる場合) と正規表現トークン ()} が前の文、ifwhile 、或いは for 式を閉じる場合) のいずれにもなり得る。(x+y)/2 のような式で /RegularExpression として解釈すると問題があるので、)} の後ろでは除算演算子として解釈する。正規表現リテラルを式文の先頭に置きたい場合は、正規表現を括弧内に記述するのが最善である。幸い、正規表現操作の結果を変数に代入するのが普通なのでこのようなことは一般的ではない。

型宣言

現在の草案では JavaScript 2.0 は宣言中に型を含める場合にパスカルスタイルのコロンを使うようになっている。例えば:

var x:Integer = 7;
function square(a:Number):Number {return a*a}

これは ECMA ワーキンググループの決定によるが、Waldemar だけは賛成していない。これには他に2、3の構文が考えられる:

C スタイル

関数の戻り値の型を引数リストの後ろに書く修正 C スタイルが考えられる:

var Integer x = 7;
var Integer y = 8, Integer z = 9;  // Integer 型変数を2つ宣言する
function square(Number a) Number {return a*a}

文法が曖昧になるため、関数の戻り値の型を引数リストの前に置くことはできない。

実際には実装は TypedIdentifierResult 文法規則を以下で置換することにより、パスカルスタイルと修正 C スタイルの両方の宣言を許容できる。最終的な文法は LALR(1) のままである。

TypedIdentifier 
   Identifier
|  Identifier : TypeExpression
|  TypeExpression Identifier
Result 
   «empty»
|  : TypeExpressionallowIn
|  [lookahead{{}] TypeExpressionallowIn

修正 C スタイル構文を使用することの利点は以下の通りである:

属性スタイル

Attribute-Style

属性は簡単な式であるので型として評価することもできるだろう。var 宣言と const 宣言についてはこれらの属性は宣言された変数の型となる。function 宣言についてはそれらの属性は関数の戻り値の型となる。一貫した文体のために引数の型は識別子の前に書く。

Integer var x = 7;
Integer var y = 8, z = 9;        // Integer 型変数を2つ宣言する
Number function square(Number a) {return a*a}

このスタイルは簡潔であり、見た目も幾分自然である。

繰り返しになるが実装はパスカルスタイルと属性スタイルの両方の宣言を許容でき、その最終的な文法は LALR(1) のままである。しかしながら2つ或いは3つのスタイルを使うことによる混乱を起こすよりも、どれか1つに決めてしまった方が良い (この柔軟性は既存のプログラムとの互換性のために使用できるだろうが)。

型式

以下の理由により、JavaScript 2.0 では型式に値式と同じ構文を使用する:

関数の宣言

ゲッタとセッタ

ECMA TC39 モジュラリティ サブコミッティの統一見解により、ゲッタの定義に getter function id (...) ではなく function get id (...) を、セッタの定義に setter function id (...) ではなく function set id (...) を使用することになった。前者の構文を使用するためには FunctionName は以下のように簡単化される:

FunctionName  Identifier

更に2つの属性 gettersetter を追加する必要がある。この決定はいずれの構文も他方より実装が困難になることはないという美学に基づいた。

言語ディレクティブ

初期の草案でプラグマの代替案となっていたものは、構文エラーを、問題となっている文が解析されたときではなく実行されたときに出していた。この方法では単一のプログラムに将来のバージョンの JavaScript で書かれた部分を含むことができた (そのバージョンの JavaScript を解釈できないシステムがその部分を実行しない限りはエラーにならないというわけである)。エラーを含む箇所が実行されない場合、そのエラーがスクリプトを破壊することもない。例えば以下の関数は whizBangFeaturefalse であれば正しく終了する:

function move(x:Integer, y:Integer, d:Integer) {
  x += 10;
  y += 3;
  if (whizBangFeature) { // [訳注: 勢いよく飛んで行ってどこかにぶつかるという意味?]
    simulate{@x and #y} along path
  } else {
    x += d; y += d;
  }
  return [x,y];
}

simulate{@x and #y} along path というコードは構文的には誤りであるが、スクリプトがこのコード片を実行しない限りはエラーによりスクリプトが破壊されることはない。

このアプローチの問題はデバッグが巧くいかないことである。構文エラーは実行時ではなくコンパイル時に発見できなければどうしようもない。


Waldemar Horwat
最終更新: 2002年11月19日 (火)
previousupnext
訳者: exeal <exeal@student.interq.or.jp>
このドキュメントのオリジナルは mozilla.org において英語で公布されています。
この和訳は、利用者の利便のために Mozilla Japan 翻訳部門 によって提供されています。
内容に関してご不明な点がありましたら webmaster までお問い合わせください。