変数の導入
この回では変数について、以下の機能を実装します。
- 変数の宣言
- 変数の参照
代入は行いません。宣言時に1回だけ代入をし、以降変数の内容は変わりません。つまりまだ定数です。
変数に関する文法の設定
変数は、以下のようにして宣言することにします。
variable : value;
これでvariable
という変数が定義され、value
によって与えられる値で初期化されました。
変数を呼び出す際には単にvariable
と書きます。
具体的には、次のように使います。
1 + 2 * (
a : 3 * 4;
a * (a - 1)
) + 5
ソースコード全体もしくは(
)
の内部は、;
で区切られることができます。
;
で区切られた部分のうち、最も後ろ以外の部分は「文」で、今はとりあえず変数宣言文のみが書けます。
変数はそれを取り囲む直近の(
)
内のみからアクセスできます。つまり、(
)
内から出ると識別子は見えなくなります。すなわち、(
)
演算子は固有の識別子フレームを形成した状態で内部の式を評価します。
;
で区切られた部分のうち最も後ろの部分は「式」で、ソースコード全体や(
)
など、文を含みうるコードが最終的に返す値を決定します。
このコードを等価に変換すると、次のようになります。
1 + 2 * (
(3 * 4) * ((3 * 4) - 1)
) + 5
式の値は270です。
丸括弧の中に;
を書くことが出来て文が置けるという文法はあまり一般的ではありませんが、Perlのdo{}
文がこれに近いです。
- Perlの組み込み関数 do の翻訳 - perldoc.jp https://perldoc.jp/func/do
中間コードのヘッダ部
ラムダ式と変数
次のコードはどのようなJavaScriptにコンパイルされるべきでしょうか。
b -> (
a : b();
a * a
)
概ね以下のようなスクリプトであれば要件を満たします。
(function(v_0) {
let v_1 = v_0();
return v_1 * v_1;
})
ここで、中間コード上でのトークンの出現の順番に問題があります。
関数b -> (a : b(); a * a)
の本文となる式は、(a : b(); a * a)
です。通常、本文となる式を(function(v_0) { return
; })
で囲めば関数生成が出来ます。
しかしながら、ここではlet v_1 = v_0();
という本文からはみ出た変数定義の文が、関数生成トークンの付属物であるreturn
の前に来ています。
つまり、(a : b(); a * a)
は、前文let v_1 = v_0();
と本文v_1 * v_1
に分けてコンパイルされなければなりません。
三項演算子と変数
次の式はどうコンパイルされるべきでしょうか。
n ? (
a : f(n - 1);
n * a
) : 1
この式では、n
が0でないときに、関数f
をn - 1
で呼び出し、その結果をn
とかけて返します。この式は階乗の一部分に似ています。
三項演算子a ? b : c
は、a
が偽であった場合はb
式を評価しません。b
が関数だった場合、その関数は呼び出されず、副作用や計算時間が生じることもありません。
この式を次のように変換すると、この式の挙動は変わります。
let v_0 = f(n - 1);
n ? n * v_0 : 1
この中間コードでは、関数f
はn
の値にかかわらず必ず評価されます。評価順序も変わり、三項演算子の左辺よりも中辺にあった式が先に評価されています。
この式が階乗関数の一部であった場合、その関数はスタックオーバーフローのエラーで使えなくなります。
上のfl8コードは、次のように変換されるべきです。
let v_0;
if (n) {
let v_1 = f(n - 1);
v_0 = n * v_1;
} else {
v_0 = 1;
}
v_0
これも、値を返す本文と、それを準備する前文の組で表すことができます。
評価順序の保存のため、三項演算子に限らずすべての演算子は一時変数を経由する形で出力されるべきです。そして、本文は副作用のない単一の変数やリテラルであるべきです。前文head
の方は、"let v_4 = (45);\n"
のような改行付きのJavaScript文であることが求められます。
構文木のコンパイル結果を前文と本文の組で表すようにする
とりあえずトークン挙動定義関数が、単なる中間コードではなく前文head
と本文body
の組を返すように書き換えてみます。
とりあえず整数トークンを改変してみます。
registry.integer = (env, argument) => "(" + parseInt(argument, 10) + ")";
↓
function toOperation(head, body) {
return {head, body};
}
registry.integer = (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")");
整数トークンは中間コード上で副作用のないリテラルであるため、前文による準備は行わず、直接その場に値を出力します。
これを呼び出す側にも変更が必要です。compile
はトークン挙動定義関数の戻り値をそのまま返しているため、compile
の呼び出し側を対応させる必要があります。
Root = _ main:Formula _ {
const token = main;
const env = new Environment();
env.registerAlias("PI", "(" + Math.PI + ")");
const operation = compile(env, main); // コンパイル結果を保持
const code = operation.head + operation.body; // headとbodyを単純に結合して中間コードにする
const result = eval(code);
return [result, code, token];
}
ここでは、コンパイル結果をhead
とbody
を単純に文字列結合することで中間コード化します。一般的には、結合の仕方はコンパイル結果の利用ケースによってさまざまです。
compile
関数はトークン挙動定義関数からも呼び出されます。このとき、演算子の各項の前文head
を項を計算する順序の通りに並べて、演算した結果を一時変数に格納し、その変数を本文body
として返します。
registry.plus = (env, argument) => "(" + compile(env, argument[0]) + " + " + compile(env, argument[1]) + ")";
↓
registry.plus = (env, argument) => {
const o1 = compile(env, argument[0]); // 左辺のコンパイル結果
const o2 = compile(env, argument[1]); // 右辺のコンパイル結果
const uid = env.getNextUid(); // 演算子の結果を保持するための一時変数
return toOperation(
// 左辺の準備、右辺の準備、左辺の本文と右辺の本文の和を一時変数に格納
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " + " + o2.body + ";\n",
// 一時変数を本文として返す
"(v_" + uid + ")"
);
};
ほとんどの一般的な演算子は同じ方式で対応できます。
もう1個だけ例を見てみます。これはラムダ式->
の挙動定義関数です。
registry.minus_greater = (env, argument) => {
const name = argument[0].argument; // 引数名の取得
const uidBody = env.getNextUid(); // 引数の名前につけるUIDを用意
env.pushAliasFrame(); // 関数内部のための識別子フレーム開始
env.registerAlias(argument[0].argument, "(v_" + uidBody + ")"); // 引数を登録
const operationBody = compile(env, argument[1]); // 関数内部をコンパイル
env.popAliasFrame(); // 識別子フレーム終了
const uid = env.getNextUid(); // 生成された関数を格納する一時変数
return toOperation(
// 一時変数に関数を代入する
"const v_" + uid + " = " + "(function(v_" + uidBody + ") {\n" +
operationBody.head + // 関数内のreturnの前に、関数内部の前文を置く
"return " + operationBody.body + ";\n" + // 関数内部の本文をreturnする
"});\n",
"(v_" + uid + ")" // 本文には一時変数を返す
);
};
ここで、operationBody.head
"return "
operationBody.body
という順番で文字列を連結できるのが、トークン挙動定義関数の仕様を前文head
と本文body
に分けた恩恵です。
前文を前に置き、本文を含んだ文を次に置くことで、演算子の挙動を柔軟に定義できます。
出力のインデント
中間コードは構造を持ったJavaScriptソースコードです。そのためインデントが発生します。
次のインデント関数を導入して出力を綺麗にします。
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
この章のまとめ
計算順序に注意してトークン挙動定義関数の仕様を変更すると、次のようなPEG.jsコードになります。
**[開閉]**
{
class Environment {
constructor() {
this.nextUid = 0;
this.aliasFrame = Object.create(null);
}
getNextUid() {
return this.nextUid++;
}
registerAlias(alias, code) {
this.aliasFrame[alias] = code;
}
resolveAlias(alias) {
return this.aliasFrame[alias];
}
pushAliasFrame() {
this.aliasFrame = Object.create(this.aliasFrame);
}
popAliasFrame() {
this.aliasFrame = Object.getPrototypeOf(this.aliasFrame);
}
}
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
const registry = {};
function toOperation(head, body) {
return {head, body};
}
registry.integer = (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")");
registry.identifier = (env, argument) => {
const code = env.resolveAlias(argument);
if (code !== undefined) return toOperation("", code);
throw new Error("Unknown identifier");
};
registry.left_plus = (env, argument) => {
const o1 = compile(env, argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = +" + o1.body + ";\n",
"(v_" + uid + ")"
);
};
registry.left_minus = (env, argument) => {
const o1 = compile(env, argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = -" + o1.body + ";\n",
"(v_" + uid + ")"
);
};
registry.right_round = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + "(" + o2.body + ");\n",
"(v_" + uid + ")"
);
};
registry.plus = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " + " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.minus = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " - " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.asterisk = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " * " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.slash = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " / " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.circumflex = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = Math.pow(" + o1.body + ", " + o2.body + ");\n",
"(v_" + uid + ")"
);
};
registry.ternary_question_colon = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const o3 = compile(env, argument[2]);
const uid = env.getNextUid();
return toOperation(
o1.head +
"let v_" + uid + ";\n" +
"if (" + o1.body + ") {\n" +
indent(
o2.head +
"v_" + uid + " = " + o2.body + ";\n"
) +
"} else {\n" +
indent(
o3.head +
"v_" + uid + " = " + o3.body + ";\n"
) +
"}\n",
"(v_" + uid + ")"
);
};
registry.minus_greater = (env, argument) => {
const name = argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(argument[0].argument, "(v_" + uidBody + ")");
const operationBody = compile(env, argument[1]);
env.popAliasFrame();
const uid = env.getNextUid();
return toOperation(
"const v_" + uid + " = " + "(function(v_" + uidBody + ") {\n" +
indent(
operationBody.head +
"return " + operationBody.body + ";\n"
) +
"});\n",
"(v_" + uid + ")"
);
};
function compile(env, token) {
return registry[token.type](env, token.argument);
}
function token(type, argument) {
return {type, argument};
}
}
Root = _ main:Formula _ {
const token = main;
const env = new Environment();
env.registerAlias("PI", "(" + Math.PI + ")");
const operation = compile(env, main);
const code = operation.head + operation.body;
const result = eval(code);
return [result, code, token];
}
Formula = Lambda
Lambda = head:(If _ (
"->" { return "minus_greater"; }
) _)* tail:If {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][2], [head[i][0], result]);
}
return result;
}
If = head:Add _ "?" _ body:If _ ":" _ tail:If {
return token("ternary_question_colon", [head, body, tail]);
}
/ Add
Add = head:Term tail:(_ (
"+" { return "plus"; }
/ "-" { return "minus"; }
) _ Term)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1], [result, tail[i][3]]);
}
return result;
}
Term = head:Left tail:(_ (
"*" { return "asterisk"; }
/ "/" { return "slash"; }
) _ Left)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1], [result, tail[i][3]]);
}
return result;
}
Left = head:((
"+" { return "left_plus"; }
/ "-" { return "left_minus"; }
) _)* tail:Pow {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][0], [result]);
}
return result;
}
Pow = head:Right _ operator:(
"^" { return "circumflex"; }
) _ tail:Left {
return token(operator, [head, tail]);
}
/ Right
Right = head:Factor tail:(_ (
"(" _ main:Formula _ ")" { return ["right_round", main] }
))* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1][0], [result, tail[i][1][1]]);
}
return result;
}
Factor = Integer
/ Identifier
/ Brackets
Integer = main:$[0-9]+ {
return token("integer", main);
}
Identifier = main:$([a-zA-Z_] [a-zA-Z0-9_]*) {
return token("identifier", main);
}
Brackets = "(" _ main:Formula _ ")" {
return main;
}
_ = [ \t\r\n]*
実行例です。
(+1) + (-2) - 3 ^ 4 / 5 * (0 ? 7 : (a -> a * 10)(5))
const v_0 = +(1);
const v_1 = -(2);
const v_2 = (v_0) + (v_1);
const v_3 = Math.pow((3), (4));
const v_4 = (v_3) / (5);
let v_9;
if ((0)) {
v_9 = (7);
} else {
const v_7 = (function(v_5) {
const v_6 = (v_5) * (10);
return (v_6);
});
const v_8 = (v_7)((5));
v_9 = (v_8);
}
const v_10 = (v_4) * (v_9);
const v_11 = (v_2) - (v_10);
(v_11)
各演算が評価順序に従って上から綺麗に並んでいることが分かります。
エラーメッセージを少し改善
Root
を次のように改変し、意味解析・実行時にエラーが出た際の挙動を少し親切にします。
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
env.registerAlias("PI", "(" + Math.PI + ")");
operation = compile(env, main);
} catch (e) {
return ["Fl8CompileError: " + e, token];
}
const code = operation.head + operation.body;
let result;
try {
result = eval(code);
} catch (e) {
return ["Fl8RuntimeError: " + e, code, token];
}
return [result, code, token];
}
エラーメッセージ自体も少し改善します。
registry.identifier = (env, argument) => {
const code = env.resolveAlias(argument);
if (code !== undefined) return toOperation("", code);
throw new Error("Unknown identifier: " + argument);
};
function compile(env, token) {
const handler = registry[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + token.type);
return handler(env, token.argument);
}
コンパイルエラー時の表示
ランタイムエラー時の表示
変数宣言の追加
目標
以下の仕様を満たす変数宣言の文法を追加します。
-
variable : value;
と書くと、value
で初期化された変数variable
が現在の識別子フレーム上で宣言される。 - 変数宣言は、ソースコード全体もしくは
(
)
内の先頭に、0個以上連続して置かれることができる。 - 宣言された変数は、
value
部分および変数宣言文以降のコードで利用できる。 - 宣言された変数は、宣言時点での識別子フレームが破棄されると見えなくなる。
fl8では中置:
で変数を宣言します。そして、宣言と同時に必ず代入も行います。
戦略
構文解析的にはセミコロン;
列とコロン:
を別の文法として定義しておきます。
セミコロン演算子;
は、セミコロンで区切られたすべてのパートを引数としてsemicolons
演算子を形成することにします。
つまり、中置+
ではa + b + c
がplus(plus(a, b), c)
のように入れ子状に組み立てられていたのを、;
ではa; b; c
をsemicolons(a, b, c)
のように組み立てます。
コロン:
は、代入系演算子の一員として、ラムダ式->
と同じ優先度とします。こうすると次のような式が括弧なしで書けるようになります。
setValue : value -> myValue = value;
中置=
演算子は、将来的に代入を表すことにします。
このコードは、変数myValue
に対して引数value
を代入する関数を作り、その関数をsetValue
として宣言します。つまりセッターメソッド的なものです。
semicolons
演算子は、末尾以外のすべてのパートをコロンであると仮定し、その右辺の式を左辺の名前で変数宣言します。
丸括弧(formula)
演算子におけるformula
と、関数呼び出しfunc(arg)
演算子におけるarg
は、新しい識別子フレームの上でコンパイルされることにします。
実装
変数宣言の文法定義
セミコロン;
ルールと、コロン:
演算子を追加します。
Formula = Semicolons
Semicolons = head:Lambda tail:(_ ";" _ Lambda)* { // 1個のLambdaの後に0個以上の「;Lambda」
if (tail.length == 0) return head; // セミコロンが1個もなかったらセミコロン演算子を形成しない
return token("semicolons", [head, ...tail.map(s => s[3])]);
}
Lambda = head:(If _ (
"->" { return "minus_greater"; }
/ ":" { return "colon"; } // ここが変わった
) _)* tail:If {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][2], [head[i][0], result]);
}
return result;
}
変数宣言の挙動定義
トークンsemicolons
の挙動を定義します。
registry.semicolons = (env, argument) => {
const heads = []; // 前文リスト
for (let i = 0; i < argument.length - 1; i++) { // 末尾以外の各要素について
const name = argument[i].argument[0].argument; // 変数名を取得
const uid = env.getNextUid(); // 変数用のUIDを発行
env.registerAlias(name, "(v_" + uid + ")"); // 現在の識別子フレームに変数を登録
const operation = compile(env, argument[i].argument[1]); // コンパイル
heads.push(
"let v_" + uid + ";\n" + // まず変数を宣言
operation.head + // 変数初期化式の前文を実行
"v_" + uid + " = " + operation.body + ";\n" // 変数を初期化
); // 変数を宣言して初期化する文を前文リストに追加
}
// この時点で末尾以外の部分の処理が完了した
const operation = compile(env, argument[argument.length - 1]); // セミコロン列の本文のコンパイル
return toOperation(
heads.join("") + // さっき作った前文リストを順番に展開
operation.head, // セミコロン列の本文の前文を置く
operation.body // セミコロン列の本文の本文を返す
);
};
括弧演算子(formula)
の改変
丸括弧(formula)
を演算子扱いにします。
Brackets = "(" _ main:Formula _ ")" {
return token("round", [main]); // 元々単にmainを返していた
}
丸括弧(formula)
演算子の挙動を定義します。関数呼び出し演算子func(arg)
側の括弧も同様に識別子フレームを形成することにしておきます。
registry.round = (env, argument) => {
env.pushAliasFrame(); // 識別子フレームを新しく作る
const o1 = compile(env, argument[0]); // 丸括弧内部のコンパイル
env.popAliasFrame(); // この時点で丸括弧内部で登録された識別子は破棄される
return o1;
};
registry.right_round = (env, argument) => {
const o1 = compile(env, argument[0]);
env.pushAliasFrame();
const o2 = compile(env, argument[1]);
env.popAliasFrame();
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + "(" + o2.body + ");\n",
"(v_" + uid + ")"
);
};
これで、括弧内で識別子フレームをあれこれ改造しても括弧を出るとリセットされるようになりました。
テスト
この記事の冒頭で現れたこのコードを入れると、270が得られます。
1 + 2 * (
a : 3 * 4;
a * (a - 1)
) + 5
const v_0 = (3) * (4);
let v_1 = (v_0);
const v_2 = (v_1) - (1);
const v_3 = (v_1) * (v_2);
const v_4 = (2) * (v_3);
const v_5 = (1) + (v_4);
const v_6 = (v_5) + (5);
(v_6)
もう一つ、再帰呼び出しを使った階乗の例を載せます。変数factorial
は中間コード上でv_0
と書かれていますが、変数宣言let v_0;
だけ分離されているため、初期化式n -> n ? n * factorial(n - 1) : 1
の中から変数を参照できるようになっています。JavaScriptの仕様上初期化式の前文の後に変数宣言を置いても動くけど。
factorial : n -> n ? n * factorial(n - 1) : 1;
factorial(9)
let v_0;
const v_6 = (function(v_1) {
let v_5;
if ((v_1)) {
const v_2 = (v_1) - (1);
const v_3 = (v_0)((v_2));
const v_4 = (v_1) * (v_3);
v_5 = (v_4);
} else {
v_5 = (1);
}
return (v_5);
});
v_0 = (v_6);
const v_7 = (v_0)((9));
(v_7)
これで変数の機能が完成しました。
まとめ
ここまでに出来上がったPEG.jsコードです。
**[開閉]**
{
class Environment {
constructor() {
this.nextUid = 0;
this.aliasFrame = Object.create(null);
}
getNextUid() {
return this.nextUid++;
}
registerAlias(alias, code) {
this.aliasFrame[alias] = code;
}
resolveAlias(alias) {
return this.aliasFrame[alias];
}
pushAliasFrame() {
this.aliasFrame = Object.create(this.aliasFrame);
}
popAliasFrame() {
this.aliasFrame = Object.getPrototypeOf(this.aliasFrame);
}
}
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
const registry = {};
function toOperation(head, body) {
return {head, body};
}
registry.integer = (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")");
registry.identifier = (env, argument) => {
const code = env.resolveAlias(argument);
if (code !== undefined) return toOperation("", code);
throw new Error("Unknown identifier: " + argument);
};
registry.round = (env, argument) => {
env.pushAliasFrame();
const o1 = compile(env, argument[0]);
env.popAliasFrame();
return o1;
};
registry.left_plus = (env, argument) => {
const o1 = compile(env, argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = +" + o1.body + ";\n",
"(v_" + uid + ")"
);
};
registry.left_minus = (env, argument) => {
const o1 = compile(env, argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = -" + o1.body + ";\n",
"(v_" + uid + ")"
);
};
registry.right_round = (env, argument) => {
const o1 = compile(env, argument[0]);
env.pushAliasFrame();
const o2 = compile(env, argument[1]);
env.popAliasFrame();
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + "(" + o2.body + ");\n",
"(v_" + uid + ")"
);
};
registry.plus = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " + " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.minus = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " - " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.asterisk = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " * " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.slash = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " / " + o2.body + ";\n",
"(v_" + uid + ")"
);
};
registry.circumflex = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = Math.pow(" + o1.body + ", " + o2.body + ");\n",
"(v_" + uid + ")"
);
};
registry.ternary_question_colon = (env, argument) => {
const o1 = compile(env, argument[0]);
const o2 = compile(env, argument[1]);
const o3 = compile(env, argument[2]);
const uid = env.getNextUid();
return toOperation(
o1.head +
"let v_" + uid + ";\n" +
"if (" + o1.body + ") {\n" +
indent(
o2.head +
"v_" + uid + " = " + o2.body + ";\n"
) +
"} else {\n" +
indent(
o3.head +
"v_" + uid + " = " + o3.body + ";\n"
) +
"}\n",
"(v_" + uid + ")"
);
};
registry.minus_greater = (env, argument) => {
const name = argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(argument[0].argument, "(v_" + uidBody + ")");
const operationBody = compile(env, argument[1]);
env.popAliasFrame();
const uid = env.getNextUid();
return toOperation(
"const v_" + uid + " = " + "(function(v_" + uidBody + ") {\n" +
indent(
operationBody.head +
"return " + operationBody.body + ";\n"
) +
"});\n",
"(v_" + uid + ")"
);
};
registry.semicolons = (env, argument) => {
const heads = [];
for (let i = 0; i < argument.length - 1; i++) {
const name = argument[i].argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, "(v_" + uid + ")");
const operation = compile(env, argument[i].argument[1]);
heads.push(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
}
const operation = compile(env, argument[argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
};
function compile(env, token) {
const handler = registry[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + token.type);
return handler(env, token.argument);
}
function token(type, argument) {
return {type, argument};
}
}
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
env.registerAlias("PI", "(" + Math.PI + ")");
operation = compile(env, main);
} catch (e) {
return ["Fl8CompileError: " + e, token];
}
const code = operation.head + operation.body;
let result;
try {
result = eval(code);
} catch (e) {
return ["Fl8RuntimeError: " + e, code, token];
}
return [result, code, token];
}
Formula = Semicolons
Semicolons = head:Lambda tail:(_ ";" _ Lambda)* {
if (tail.length == 0) return head;
return token("semicolons", [head, ...tail.map(s => s[3])]);
}
Lambda = head:(If _ (
"->" { return "minus_greater"; }
/ ":" { return "colon"; }
) _)* tail:If {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][2], [head[i][0], result]);
}
return result;
}
If = head:Add _ "?" _ body:If _ ":" _ tail:If {
return token("ternary_question_colon", [head, body, tail]);
}
/ Add
Add = head:Term tail:(_ (
"+" { return "plus"; }
/ "-" { return "minus"; }
) _ Term)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1], [result, tail[i][3]]);
}
return result;
}
Term = head:Left tail:(_ (
"*" { return "asterisk"; }
/ "/" { return "slash"; }
) _ Left)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1], [result, tail[i][3]]);
}
return result;
}
Left = head:((
"+" { return "left_plus"; }
/ "-" { return "left_minus"; }
) _)* tail:Pow {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][0], [result]);
}
return result;
}
Pow = head:Right _ operator:(
"^" { return "circumflex"; }
) _ tail:Left {
return token(operator, [head, tail]);
}
/ Right
Right = head:Factor tail:(_ (
"(" _ main:Formula _ ")" { return ["right_round", main] }
))* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1][0], [result, tail[i][1][1]]);
}
return result;
}
Factor = Integer
/ Identifier
/ Brackets
Integer = main:$[0-9]+ {
return token("integer", main);
}
Identifier = main:$([a-zA-Z_] [a-zA-Z0-9_]*) {
return token("identifier", main);
}
Brackets = "(" _ main:Formula _ ")" {
return token("round", [main]);
}
_ = [ \t\r\n]*
この時点で次の特徴があります。
- ソースコードから構文木の生成
- 構文木から中間コードの生成
- 中間コードの評価
- 副作用を持つ演算の適切な順序での実行
- エラー出力の改善 (NEW)
- スペース
- 整数リテラル
- 識別子
- 定数
- PI
- 引数
- 変数 (NEW)
- 定数
- 演算子
- 括弧
- 関数呼び出し
- べき乗
- 符号
- 加減乗除
- 三項演算子
- ラムダ式
fl8実装における最初の難所が終わりました。