今回のスコープ
予告通りに以下の機能を実装します。過去二回(オブジェクトとクラス、配列)に比べると細かな機能のためさっと実装できました。
- ステップ13:引数展開と残余引数
- ステップ14:分割代入
構文解析器のリファクタリング
追加仕様の説明に入る前に、
前回、「そろそろ汎用的なBNF選択の仕組みを作るべき」と書きましたが、分割代入あたりでまた問題になりそうなのでまずはこの対応を行いました。
まず以下のようなselectBNF
関数を作ります。この関数では候補のBNF(関数)を順番に適用してみて結果が返ってきたもの(受理できなかった場合はundefined
を返すルール)を関数の戻り値として返します。
また、スキャナを途中まで進めてしまった場合に備えて(forなのかfor-ofなのかは途中まで読まないとわかりません)、スキャナの状態を保存しておき結果がundefined
ならリストアします。保存とリストアが不要なこともありますが(先頭トークンだけで即returnされた場合)、「汎用にバックトラックを提供する」ためにこのようにしてあります。まあ、JOKEは効率無視なので。
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の分岐がこんなにきれいになりました!(全コード)
function IterationStatement(scanner) {
function WhileStatement(scanner) {
// 省略
}
function ForStatement(scanner) {
// 省略
}
function ForOfStatement(scanner) {
// 省略
}
const BNFs = [
WhileStatement,
ForStatement,
ForOfStatement
]
return selectBNF(BNFs, scanner);
}
また、バックトラック汎用化によりアロー関数の引数でめんどくさい処理をしていたのも非常にシンプルになりました。さらばCoverParenthesizedExpressionAndArrowParameterList
。
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
で引数の数(スタックから取り出す数)を指定する形です。
[
{ command: 'PUSH', operand: 'func' },
{ command: 'LOOKUP_VARIABLE' },
{ command: 'PUSH', operand: 12 },
{ command: 'PUSH', operand: 34 },
{ command: 'CALL', operand: 2 }
]
しかしこれだと展開結果が何個になるかわからない引数展開には対応できないため以下のように修正しました。つまり、前回実装した配列を使って引数の準備を行うように変更しました。引数展開も前回実装した配列リテラルでのスプレッドがそのまま使えます1。
[
{ 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と書いてあり、機能追加をしやすい構造にはしましたがデフォルト値(式)や入れ子はサポートしないと思います(笑)
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);
}
}
}
あとがき
以上、今回は残余引数と分割代入を実装しました。ネタが少ないので後からやるつもりのテンプレート文字列も先に実装しようかと思いましたがまとまりがなくなるのでやめておきました。
というか今回のメインは構文解析器のリファクタリングですね。本当だったらこういう仕組みは初めに考えるべきなのでしょうが「作りながら、ある程度知見がたまったところで見直す」というのもありだと思います。
次回予告、は別にしなくてもいいのですが、次は例外関連の処理を作る予定です。