LoginSignup
1
1

More than 3 years have passed since last update.

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

Posted at

今回のスコープ

予告通りに以下の機能を実装します。過去二回(オブジェクトとクラス、配列)に比べると細かな機能のためさっと実装できました。

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

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

追加仕様の説明に入る前に、
前回、「そろそろ汎用的なBNF選択の仕組みを作るべき」と書きましたが、分割代入あたりでまた問題になりそうなのでまずはこの対応を行いました。

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

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の分岐がこんなにきれいになりました!(全コード

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

    function ForStatement(scanner) {
        // 省略
    }

    function ForOfStatement(scanner) {
        // 省略
    }

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

また、バックトラック汎用化によりアロー関数の引数でめんどくさい処理をしていたのも非常にシンプルになりました。さらばCoverParenthesizedExpressionAndArrowParameterList

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で引数の数(スタックから取り出す数)を指定する形です。

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

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

ステップ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

分割代入はフルにサポートしようとするとかなりめんどくさいですが実用(自分が使う範囲とも言う)を考慮しとりあえず以下の仕様としました。

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

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

分割代入の処理
{ 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,
}

淡々と値を取得してセットする処理を行います。
TODOと書いてあり、機能追加をしやすい構造にはしましたがデフォルト値(式)や入れ子はサポートしないと思います(笑)

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);
        }
    }
}

あとがき

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

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


  1. 仕様を読んでいたらスプレッドはイテレータを使って値を取り出すと書かれているのですが、配列リテラルでのスプレッドはイテレータ実装する前に作ったので現状配列の展開しか対応していません。 

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1