作って理解JavaScript：JOKE開発記その9 - 残余引数と分割代入

# 今回のスコープ

[予告](https://qiita.com/junjis0203/items/7125ded846ee283c4df5#%E3%81%82%E3%81%A8%E3%81%8C%E3%81%8D)通りに以下の機能を実装します。過去二回（オブジェクトとクラス、配列）に比べると細かな機能のためさっと実装できました。

- ステップ13：引数展開と残余引数
- ステップ14：分割代入

# 構文解析器のリファクタリング

追加仕様の説明に入る前に、
[前回](https://qiita.com/junjis0203/items/7125ded846ee283c4df5#%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90%E7%9A%84%E3%81%AA%E8%AA%B2%E9%A1%8C)、「そろそろ汎用的なBNF選択の仕組みを作るべき」と書きましたが、分割代入あたりでまた問題になりそうなのでまずはこの対応を行いました。

まず以下のような`selectBNF`関数を作ります。この関数では候補のBNF（関数）を順番に適用してみて結果が返ってきたもの（受理できなかった場合は`undefined`を返すルール）を関数の戻り値として返します。
また、スキャナを途中まで進めてしまった場合に備えて（forなのかfor-ofなのかは途中まで読まないとわかりません）、スキャナの状態を保存しておき結果が`undefined`ならリストアします。保存とリストアが不要なこともありますが（先頭トークンだけで即returnされた場合）、「汎用にバックトラックを提供する」ためにこのようにしてあります。まあ、JOKEは効率無視なので。

```js:parser.js抜粋
function selectBNF(BNFs, scanner) {
    for (const BNF of BNFs) {
        const state = scanner.saveState();
        const node = BNF(scanner);
        if (node) {
            return node;
        } else {
            scanner.restoreState(state);
        }
    }    
}
```

`selectBNF`関数を使い（それと気軽に例外バンバン投げてたのを全部直して）、前回「// TODO: too ad-hoc」と書かれていたforとfor-ofの分岐がこんなにきれいになりました！（[全コード](https://github.com/junjis0203/joke/blob/aea2fb3d79a47c41782436bd969bf96ca957c2ac/lib/parser.js#L747)）

```js:parser.js抜粋
function IterationStatement(scanner) {
    function WhileStatement(scanner) {
        // 省略
    }

    function ForStatement(scanner) {
        // 省略
    }

    function ForOfStatement(scanner) {
        // 省略
    }

    const BNFs = [
        WhileStatement,
        ForStatement,
        ForOfStatement
    ]
    return selectBNF(BNFs, scanner);
}
```

また、バックトラック汎用化により[アロー関数の引数でめんどくさい処理をしていた](https://qiita.com/junjis0203/items/7125ded846ee283c4df5#%E3%82%B3%E3%83%B3%E3%83%9E%E6%BC%94%E7%AE%97%E3%82%92%E8%A4%87%E6%95%B0%E5%BC%95%E6%95%B0%E3%81%A8%E3%81%97%E3%81%A6%E6%89%B1%E3%81%86)のも非常にシンプルになりました。さらば`CoverParenthesizedExpressionAndArrowParameterList`。

```js:parser.js抜粋
function ArrowParameter(scanner) {
    if (checkCharToken(scanner.token, '(')) {
        scanner.next();

        // Specification says CoverParenthesizedExpressionAndArrowParameterList is recognized as ( StrictFormalParameters ).
        const params = StrictFormalParameters(scanner);
        if (checkAndStepCharToken(scanner, ')')) {
            return {
                params: params.params
            };
        }
    } else {
        // 省略
    }
}
```

# ステップ13：引数展開と残余引数

ステップ13段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0013

## 引数展開

関数呼び出しについては詳しく説明したことはなかったと思いますがステップ12までは以下のようになっていました。引数をスタックに積み、`CALL`で引数の数（スタックから取り出す数）を指定する形です。

```js:ステップ12までの関数呼び出し
[
  { command: 'PUSH', operand: 'func' },
  { command: 'LOOKUP_VARIABLE' },
  { command: 'PUSH', operand: 12 },
  { command: 'PUSH', operand: 34 },
  { command: 'CALL', operand: 2 }
]
```

しかしこれだと展開結果が何個になるかわからない引数展開には対応できないため以下のように修正しました。つまり、前回実装した配列を使って引数の準備を行うように変更しました。引数展開も前回実装した配列リテラルでのスプレッドがそのまま使えます[^1]。

[^1]: [仕様](http://www.ecma-international.org/ecma-262/6.0/#sec-argument-lists-runtime-semantics-argumentlistevaluation)を読んでいたらスプレッドはイテレータを使って値を取り出すと書かれているのですが、配列リテラルでのスプレッドはイテレータ実装する前に作ったので現状配列の展開しか対応していません。

```js:ステップ13以降の関数呼び出し
[
  { command: 'PUSH', operand: 'func' },
  { command: 'LOOKUP_VARIABLE' },
  { command: 'MAKE_ARRAY' },
  { command: 'PUSH', operand: 12 },
  { command: 'ADD_ELEMENT' },
  { command: 'PUSH', operand: 34 },
  { command: 'ADD_ELEMENT' },
  { command: 'CALL' }
]
```

## 残余引数

残余引数については「残っている引数」を配列に詰め込めばいいだけなので特筆すべきことは特にありません。

# ステップ14：分割代入

ステップ14段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0014

[分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)はフルにサポートしようとするとかなりめんどくさいですが実用（自分が使う範囲とも言う）を考慮しとりあえず以下の仕様としました。

- オブジェクトの分割代入のみ
- 変数宣言時と関数引数のみ（代入演算やfor-ofでの使用はサポートしない）
- デフォルト値とか別の変数名に代入とかもサポートしない
- 入れ子もサポートしない

ここまで機能を限定してしまうと作るのはそんなに難しくありません。分割代入を表現したノード`PATTERN_INITIALIZE`に対して以下のような実行コードを生成します。[^2]

[^2]: 初めは一つずつ`GET_PROPERTY`と`SET_VARIABLE`するようにしようと思いましたが将来サポートするかもしれない入れ子のためにできるだけ汎用性を持たせました。

```js:分割代入の処理
{ command: 'PUSH', operand: 'obj' },
{ command: 'LOOKUP_VARIABLE' },
{
  command: 'EXTRACT_PATTERN',
  operand1: [
    { key: 'a', name: 'a' },
    { key: 'b', name: 'b' },
    { key: 'c', name: 'c' }
  ],
  operand2: true,
}
```

淡々と値を取得してセットする[処理](https://github.com/junjis0203/joke/blob/step0014/lib/vm.js#L137)を行います。
TODOと書いてあり、機能追加をしやすい構造にはしましたがデフォルト値（式）や入れ子はサポートしないと思います(笑)

```js:vm.js抜粋
function executeInsn(context, insn) {
    switch (insn.command) {
    case I.EXTRACT_PATTERN:
        {
            const currentScope = scopes[0];
            const obj = stack.pop();
            extractPattern(obj, insn.operand1, currentScope, insn.operand2);
        }
        break;
    }
}

function extractPattern(obj, patterns, scope, initialize) {
    for (const pattern of patterns) {
        // TODO: support default and nest
        if (pattern.name) {
            const value = obj.getProperty(pattern.key);
            scope.setVariable(pattern.name, value, initialize);
        }
    }
}
```

# あとがき

以上、今回は残余引数と分割代入を実装しました。ネタが少ないので後からやるつもりのテンプレート文字列も先に実装しようかと思いましたがまとまりがなくなるのでやめておきました。
というか今回のメインは構文解析器のリファクタリングですね。本当だったらこういう仕組みは初めに考えるべきなのでしょうが「作りながら、ある程度知見がたまったところで見直す」というのもありだと思います。

次回予告、は別にしなくてもいいのですが、次は例外関連の処理を作る予定です。