JavaScriptで作る!ミニ言語のインタプリタ(5)〜JSONでプログラムを書く〜
はじめに
前回の記事では、組み込み関数の実装を行い、インタプリタが標準出力などを扱えるように拡張しました。今回は前回予告していた通り、JSON形式のプログラムを読み込んで実行できるようにします。
これまではJavaScriptで直に抽象構文木を記述していたため、「自作言語のプログラム」を外部に記述することは(evalなどを使わない限り)できませんでした。しかし、今回の拡張によってJSONとしてプログラムを記述しておき、必要に応じて読み込むことができるようになります。
プログラムの記述例
まず、前回の記事で扱ったプログラムを再掲します。
const program = new Program(
// function print_add(x, y) {
// print(add(mul(x, y), 3)
// }
// print_add(5, 3);
[
new FunDef("print_add", ["x", "y"],
new FunCall("print",
new FunCall("add",
new FunCall("mul", new VarRef("x"), new VarRef("y")),
new Num(3)
)
)
)
],
new FunCall("print_add", new Num(5), new Num(3))
);
これをJSONで記述したものが以下です。JSONで表現するための方法はいくつもありますが、簡潔さを重視して、LispのS式に似た形式で記述しています。
const program = `{
"functions": [
["print_add", ["x", "y"],
["call", "print",
["call", "add",
["call", "mul", ["ref", "x"], ["ref", "y"]],
3]]]
],
"body": ["call", "print_add", 5, 3]
}`;
プログラム全体を見ると、
- プログラム全体が一つのオブジェクトになる
- 関数定義の配列は、キー
functions
で取り出せる - プログラム本体は、キー
body
で取り出せる
であることがわかります。また、プログラム本体は["call", ...]
が関数呼び出しを表し、["ref", ...]
が変数参照を表すといった具合で、配列の最初の要素が構文の名前を表現し、それ以降の要素が構文の実体を表現しています。これはまさにLispのS式と同じ方式です。
JSONを変換する
JSONであらわされたプログラムを読み込んでインタプリタで実行するためには、まずJSONパーザーを呼び出して、それを内部的なデータ構造に変換する必要があります。この役割を担うのが translateProgram
とtraslateExpr
の2つの関数です。それでは、それぞれの動作を説明し、プログラム全体を見ていきましょう。
translateProgram
translateProgram
関数は、JSON形式で記述されたプログラムを内部構造に変換します。以下に関数の定義を示します。
function translateProgram(jsonProgram) {
const defs = jsonProgram['functions'].map(f =>
new FunDef(f[0], f[1], translateExpr(f[2]))
);
const body = translateExpr(jsonProgram['body']);
return new Program(defs, body);
}
この関数では、まずJSONオブジェクトからfunctions
キーで関数定義を取り出し、それぞれを FunDef
クラスのオブジェクトに変換します。プログラムのbody
も同様にtranslateExpr
関数を使って内部表現に変換します。このようにして、JSON形式で記述されたプログラムをインタプリタで評価可能な形式に変換するのです。
translateExpr
translateExpr
関数は、JSONで記述された各式を対応する内部オブジェクトに変換します。この関数は、式が配列形式で表現されているかどうかを確認し、その内容に基づいて適切なオブジェクトを生成します。以下が関数の定義です:
function translateExpr(jsonObject) {
if(Array.isArray(jsonObject)) {
const operator = jsonObject[0];
switch(operator) {
case "+":
return BinExpr("+", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "-":
return BinExpr("-", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "*":
return BinExpr("*", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "/":
return BinExpr("/", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "<":
return BinExpr("<", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case ">":
return BinExpr(">", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "<=":
return BinExpr("<=", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case ">=":
return BinExpr(">=", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "==":
return BinExpr("==", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "!=":
return BinExpr("!=", translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "seq":
return new Seq(...jsonObject.slice(1).map(translateExpr));
case "if":
return new If(translateExpr(jsonObject[1]), translateExpr(jsonObject[2]), translateExpr(jsonObject[3]));
case "while":
return new tWhile(translateExpr(jsonObject[1]), translateExpr(jsonObject[2]));
case "<-":
return new Assignment(jsonObject[1], translateExpr(jsonObject[2]));
case "ref":
return new VarRef(jsonObject[1]);
case "call":
return new FunCall(jsonObject[1], ...jsonObject.slice(2).map(translateExpr));
}
}
switch (typeof jsonObject) {
case "number":
return new Num(jsonObject);
}
throw new Error("Not implemented for: " + JSON.stringify(jsonObject));
}
この関数は、JSONオブジェクトの形式に応じて様々なクラスを使って内部表現に変換します。例えば、二項演算子であればBinExpr
クラスを使用し、条件分岐であればIf
クラスを使用します。
プログラムの実行
以下のevalJsonProgram
関数はJSON文字列を受け取って実行する関数です:
function evalJsonProgram(jsonString) {
const jsonProgram = JSON.parse(jsonString);
const program = translateProgram(jsonProgram);
return evalProgram(program);
}
内部はとても単純です。まず、JSON.parse()
を呼び出してJSON文字列をオブジェクトに変換し、次に、translateProgram
を呼び出すことで内部形式に変換しています。最後は、evalProgram
を呼び出し、内部形式に変換されたプログラムを解釈しています。
evalProgram
は定義済みの関数(インタプリタ)です。つまり、今回のプログラムがやっていることは、JSON文字列を受け取って適切な内部形式に変換する作業だと言えます。
プログラム全体の実行例
それでは、いよいよプログラム全体を見ていきましょう:
const program = `{
"functions": [
["print_add", ["x", "y"],
["call", "print",
["call", "add",
["call", "mul", ["ref", "x"], ["ref", "y"]],
3]]]
],
"body": ["call", "print_add", 5, 3]
}`;
evalJsonProgram(program);
このプログラムは次のように読めます。
-
functions
は関数定義の並びに対応しており、その中ではprint_add
という関数を定義しています。この関数は引数x
とy
を受け取り、x
とy
の積に3を足した結果をprint
関数で出力します。 -
body
はプログラム本体に対応しており、print_add
関数を引数5
と3
で呼び出しています。
evalJsonProgram
が呼び出されると、次の手順で上記のプログラムが評価されます:
- JSON文字列がオブジェクトに変換される(
JSON.parse()
)。 - オブジェクトが内部表現に変換される(
translateProgmram()
)。 - 内部表現が実行され、コンソールに計算結果が出力される(
evalProgram()
)。
この例では、5 * 3 + 3 = 18
が計算され、結果として18
が出力されます。
ファイルからプログラムを読み込む
先のprogram
に格納されているのは単なるJSON文字列ですから、これを外部ファイルに格納しても構いません。その場合、program.json
を以下の内容で作成します:
{
"functions": [
["print_add", ["x", "y"],
["call", "print",
["call", "add",
["call", "mul", ["ref", "x"], ["ref", "y"]],
3]]]
],
"body": ["call", "print_add", 5, 3]
}
次に、先ほどの実行例を次のように修正します:
const fs = require('fs');
try {
const program = fs.readFileSync('program.json', 'utf8');
evalJsonProgram(program);
} catch (err) {
console.error('Error reading the file:', err);
}
同じように18
が出力されていれば成功です。
まとめ
今回の記事では、JSONでプログラムを記述し、インタプリタで読み込んで評価する手法を紹介しました。これにより、これまで内部でのみ記述していたプログラムを外部から読み込み、実行することができるようになりました。次回の記事では、静的型を付け加えるための前準備として、インタプリタを拡張して、数以外のデータ型(真偽値、文字列など)を扱えるようにします。
お楽しみに!
Discussion