Q. ECMAScript で使われる疑似コードの進化

ES1 から始まり、ECMA-262 は番号の付いたステップからなる疑似コードで書かれたアルゴリズムで規定されてきた。例えば条件演算子の意味論は ES1 [Steele 1997] で次のように規定される:

生成規則 ConditionalExpression: LogicalORExpresion ? AssignmentExpression : AssignmentExpresion は次のように評価される:

  1. LogicalORExpression を評価する。
  2. GetValue(Result(1)) を呼ぶ。
  3. ToBoolean(Result(2)) を呼ぶ。
  4. もし Result(3) が false なら、ステップ 8 に行く。
  5. 一つ目の AssignmentExpression を評価する。
  6. GetValue(Result(5)) を呼ぶ。
  7. Result(6) を返す。
  8. 二つ目の AssignmentExpression を評価する。
  9. GetValueResult(8) を呼ぶ。
  10. Result(9) を返す。
ES1–ES3 の疑似コード

この形式では疑似コードを説明するヘッダーの後に番号付きのステップが並ぶ。これは機械語スタイルの疑似コードと特徴付けることができ、機械語でのコーディングと同様の利便性の問題を持つ。goto に似た制御スタイルと中間的な結果に対する数値ラベルの利用はコードの理解と保守を困難にする。ES1 では大部分のアルゴリズムが上記の例より短かったので、この形式でも問題は起こらなかった。しかし ES3 では込み入った制御フローを持つ長いアルゴリズムを必要とする複雑なライブラリ関数がいくつか追加されたので、問題が発生した。

二度の ES4 の試みは ES1–ES3 の疑似コードを捨て、新しい形式化を採用した。Waldemar Horwat は ES41 のために以前より手の込んだ仕様言語を開発した。彼の仕様言語で書かれた ConditionalExpression の意味論 [Horwat 2003a] を次に示す:

ES41 の疑似コード
ES41 の疑似コード

ES42 は生成規則 ConditionalExpression の意味論を規定するまで至らなかった。しかし David Herman と Cormac Flanagan [2007] は、ECMAScript 仕様で SML をどう利用していくつもりかを説明するときに ConditionalExpression を利用した。彼らは次の例を示した:

fun evalCondExpr (regs:REGS)
                 (cond:EXPR)
                 (thn:EXPR)
                 (els:EXPR)
  : VAL =
  let
    val v = evalExpr regs cond
    val b = toBoolean v
  in
    if b
    then evalExpr regs thn
    else evalExpr regs els
end
ES42 の ML コード

ES5 ワーキンググループは ES1–ES3 で使われた記法と慣習をベースとしながらインクリメンタルに改善して仕様を作成することを望んだ。しかし ES5 では新しく追加された複雑なライブラリ関数をいくつか規定する必要があった。また以前の版には不完全あるいは不明瞭な散文を使って不適切に記述されていた部分がいくつかあったので、そういった部分は相互運用性を高めるためにアルゴリズムを書き直す必要があった。数個のドラフトの後、ワーキンググループは Allen Wirfs-Brocks が「構造化プログラミング」と呼んだスタイルの疑似コードを開発した。最初の大きな変更は中間的な計算結果に名前を付ける「let 文」の導入である。この慣習は実際には ES1 でも二つのアルゴリズムで使われており、ES3 でも正規表現マッチングのアルゴリズムで使われていたのだが、疑似コードの慣習として正式には定義されず、仕様のそれ以外の部分では使われていなかった。Mark Miller は二つ目の大きな変更を導入した。それは構造化された制御フロー文と、入れ子になった制御フローを強調する視覚的なインデントである。ES5 で ConditionalExpression の意味論は次のように表される:

生成規則 ConditionalExpression: LogicalORExpresion ? AssignmentExpression : AssignmentExpresion は次のように評価される:

  1. lrefLogicalORExpression の評価結果とする。
  2. もし ToBoolean(GetValue(lref)) が true なら
    1. trueRef を一つ目の AssignmentExpression の評価結果とする。
    2. GetValue(trueRef) を返す。
  3. そうでないなら
    1. falseRef を二つ目の AssignmentExpression の評価結果とする。
    2. GetValue(falseRef) を返す。
ES5 の疑似コード

ステップのラベルは解説で個々のステップを参照しやすくするためだけにある。仕様内でアルゴリズムの慣習を説明する部分は新しいスタイルを要求するよう改訂された。新しいアルゴリズムは新しいスタイルで書かれ、プロジェクトを通して ES 編集者は既存のアルゴリズムを新しい慣習に沿って全て書き直した。

ES5 の疑似コードが使った慣習は以降の版でも基礎として使われた。ES6 ではアルゴリズムを説明するヘッダーが単純になり、例外伝播の意味論が明示的になった。ES6 における ConditionalExpression の意味論では新しいステップ (ステップ 2) が一つ追加される:

実行時意味論: 評価

ConditionalExpression: LogicalORExpresion ? AssignmentExpression : AssignmentExpresion

  1. lrefLogicalORExpression の評価結果とする。
  2. ReturnIfAbrupt(lref)
  3. もし ToBoolean(GetValue(lref)) が true なら
    1. trueRef を一つ目の AssignmentExpression の評価結果とする。
    2. GetValue(trueRef) を返す。
  4. そうでないなら
    1. falseRef を二つ目の AssignmentExpression の評価結果とする。
    2. GetValue(falseRef) を返す。
ES2015/ES6 の疑似コード

ECMAScript 仕様の目標は、言語を明確に規定することで同じ ECMAScript プログラムを仕様に準拠する異なる実装で実行したときに観測可能な振る舞いの違いが生まれないようにすることである。観測可能な違いの発生源になりがちだったが例外の伝播で、観測可能な副作用を持つ可能性のある他の意味論的操作との相対的な実行順序が曖昧になることがあった

ES3 以降の仕様で、例外の伝播は完了レコード (Completion Record) という抽象化を使ってモデル化される。完了レコードは仕様に含まれるアルゴリズムのほとんどで利用され、値を持った通常完了 (Normal Completion) あるいは例外の送出などの中途完了 (Abrupt Completion) のいずれかを表す。ES2015 より前の仕様では異なる完了レコードの生成と区別が疑似コードで明示的に示されておらず、アルゴリズムに「この値を返す」とあれば、その値を持った通常完了レコードが暗黙に作成されるものとされた。しかしアルゴリズムを呼んで中途完了が返ったときに何が起こるかは定義されていなかった。中途完了は即時に伝播され、呼び出し側のアルゴリズムは実行を止めるのだろうか、それとも中途完了レコードが実際に使われるまで呼び出し側のアルゴリズムは実行を許されるのだろうか? ECMAScript の異なる実装は異なる選択をしていた。しかしアルゴリズムが実行を続けて観測可能な副作用を持つ操作を行うと、完了レコードの扱いの違いが JavaScript プログラムから観測可能な違いを生んでしまう。

この点は ES2015 仕様で上記の例におけるステップ 2 のようなステップを追加することで明確化された。この例でステップ 2 の ReturnIfAbrupt はステップ 1 で行われた LogicalORExpression の評価が中途完了レコードを生成したかどうかを明示的に確認し、もし生成していたらその中途完了レコードを ConditionalExpression の評価結果として返す。もし評価結果が通常完了なら、その値がアンラップされ lref に代入される。ReturnIfAbrupt による判定がステップ 3.a と 4.a で必要とならないのは、GetValue に中途完了を渡すとそれがそのまま GetValue の完了レコードとして返るためである。

ES2016 は呼び出しの最初にクエスチョンマークを付ける記法を一貫して使うことで ReturnIfAbrupt の行を取り払った。このクエスチョンマークは疑似コードにおける演算子で、ReturnIfAbrupt に似た意味論を持つ。つまり中途完了を伝播させるか、通常完了の値をアンラップしたものを返す:

実行時意味論: 評価

ConditionalExpression: LogicalORExpresion ? AssignmentExpression : AssignmentExpresion

  1. lrefLogicalORExpression の評価結果とする。
  2. もし ?ToBoolean(GetValue(lref)) が true なら
    1. trueRef を一つ目の AssignmentExpression の評価結果とする。
    2. ?GetValue(trueRef) を返す。
  3. そうでないなら
    1. falseRef を二つ目の AssignmentExpression の評価結果とする。
    2. ?GetValue(falseRef) を返す。
ES2016 の疑似コード
関連書籍 (Amazon アソシエイト)
プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで
JavaScript Primer 迷わないための入門書
JavaScript 第7版
「ものづくり」の科学史 世界を変えた《標準革命》