今回の目標
エラー文が分かりにくい問題
突然ですが、次のコードの実行結果を見てみましょう。
f1 : a -> (
f2 : b -> (
f3 : c -> (
b + c(b)
);
f3
);
f2(a)
);
f1(1)(2)
Fl8RuntimeError: TypeError: v_5 is not a function
というエラーが出ました。「どこかの変数v_5
に入っている関数でない何かを呼び出そうとして実行時エラーが出た」ということは分かりますが、ソースコード上のどのあたりで発生したのかは分かりません。
このエラーオブジェクトをコンソールに出力しても、「eval
の7行目にあるv_8
という関数の中」というところまでしかわかりません。
中間コードを見に行ってみると、確かに変数v_8
に格納された関数の中にある7行目でv_5
を実行しようとしています。これはfl8ソースコード上のc(b)
に相当します。
let v_0;
const v_11 = (function(v_1) {
let v_2;
const v_9 = (function(v_3) {
let v_4;
const v_8 = (function(v_5) {
const v_6 = (v_5)((v_3)); // 7行目
const v_7 = (v_3) + (v_6);
return (v_7);
});
v_4 = (v_8);
return (v_4);
});
v_2 = (v_9);
const v_10 = (v_2)((v_1));
return (v_10);
});
v_0 = (v_11);
const v_12 = (v_0)((1));
const v_13 = (v_12)((2));
(v_13)
中間コードを参照すると場所が特定できますが、せめてスタックトレースには元のfl8ソースコードに対応する行番号が表示されて欲しいところです。
目標
実行時エラーにおいて、スタックトレース上に中間コードに対応する行番号ではなく、fl8ソースコードに対応する行番号が表示されるようにします。
TypeError: v_5 is not a function
v_8 https://pegjs.org/js/online.js line 62 > eval line 158 > eval:7
<anonymous> https://pegjs.org/js/online.js line 62 > eval line 158 > eval:20
↓
TypeError: v_5 is not a function
f3(eval,L:4,C:12) https://pegjs.org/js/online.js line 62 > eval line 158 > eval:7
<anonymous> https://pegjs.org/js/online.js line 62 > eval line 158 > eval:20
また、コンパイルエラーにはメッセージ自体にエラーが発生した位置を与えます。
Fl8CompileError: Error: Unknown identifier: a
↓
Fl8CompileError: Error: Unknown identifier: a (eval,L:5,C:1)
(eval,L:5,C:1)
という記述は、eval
というソースコード内の5行目1文字目という意味です。この言語は1行に大量のコードを押し込む傾向があるため、横の位置も含めます。
構文木オブジェクトにトークンの位置を添付
PEG.jsでは、ルールのアクション部分でlocation()
と書くと、そのルールにヒットした部分の位置を得ることができます。
これを構文木に添付すれば意味解析側でトークンの出現位置を特定できます。
実装
出現場所は、リテラルや識別子などはその左端、中置演算子a + b
については左辺の左端ではなく、+
記号のある場所とします。
{
~~
function token(type, argument) {
return {type, argument};
}
}
~~
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;
}
~~
Identifier = main:$([a-zA-Z_] [a-zA-Z0-9_]*) {
return token("identifier", main);
}
~~
↓
{
~~
function token(type, argument, location) {
return {type, argument, location}; // 構文木オブジェクトは出現位置情報を持つ
}
}
~~
Add = head:Term tail:(_ (
"+" { return ["plus", location()]; } // 「+」記号の位置でlocation()
/ "-" { return ["minus", location()]; }
) _ Term)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
// tail[i][1]はトークン名と出現位置の組になった
result = token(tail[i][1][0], [result, tail[i][3]], tail[i][1][1]);
}
return result;
}
~~
Identifier = main:$([a-zA-Z_] [a-zA-Z0-9_]*) {
// 識別子はそれ自体が出現位置
return token("identifier", main, location());
}
~~
その他すべてのtoken
関数呼び出し個所において同様にします。
結果
次のコードの構文木オブジェクトの内容は、次のようになります。構文木オブジェクトは、トークンの種類、項やリテラルの内容の他に、出現位置の情報も持つようになりました。
1 + 2
{
"type": "plus",
"argument": [
{
"type": "integer",
"argument": "1",
"location": {
"start": {
"offset": 0,
"line": 1,
"column": 1
},
"end": {
"offset": 1,
"line": 1,
"column": 2
}
}
},
{
"type": "integer",
"argument": "2",
"location": {
"start": {
"offset": 4,
"line": 1,
"column": 5
},
"end": {
"offset": 5,
"line": 1,
"column": 6
}
}
}
],
"location": {
"start": {
"offset": 2,
"line": 1,
"column": 3
},
"end": {
"offset": 3,
"line": 1,
"column": 4
}
}
}
挙動定義関数がargument
ではなく構文木オブジェクトtoken
を受け取るように変更
ここではただのリファクタリングを行います。
トークン挙動定義関数の変更
次のコードは整数トークンの挙動定義関数ですが、整数トークン自身の文字列表現であるargument
だけを受け取っています。これをtoken
を受け取るようにします。
(env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")")
↓
(env, token) => toOperation("", "(" + parseInt(token.argument, 10) + ")")
また、compile
メソッド側でトークン挙動定義関数に引数ではなく構文木オブジェクト自体を渡すようにします。
compile(domain, token) {
const handlerTable = this._operatorRegistry[domain];
if (handlerTable === undefined) throw new Error("Unknown domain: " + domain);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + domain + "/" + token.type);
return handler(this, token.argument);
}
↓
compile(domain, token) {
const handlerTable = this._operatorRegistry[domain];
if (handlerTable === undefined) throw new Error("Unknown domain: " + domain);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + domain + "/" + token.type);
return handler(this, token); // 引数を変更
}
識別子挙動定義関数の変更
ついでに識別子の方も同様にしておきます。identifier
トークンのget
およびset
の挙動定義関数と、すべての識別子登録個所を変えます。
env.registerOperatorHandler("get", "identifier", (env, token) => {
const handlerTable = env.resolveAlias(token.argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + token.argument);
const handler = handlerTable["get"];
if (handler === undefined) throw new Error("Unreadable identifier: " + token.argument);
return handler(env);
});
env.registerAlias(name, {
get: env => toOperation("", "(v_" + uid + ")"),
set: env => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n")),
});
↓
env.registerOperatorHandler("get", "identifier", (env, token) => {
const handlerTable = env.resolveAlias(token.argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + token.argument);
const handler = handlerTable["get"];
if (handler === undefined) throw new Error("Unreadable identifier: " + token.argument);
return handler(env, token); // トークンを渡す
});
env.registerAlias(name, {
// トークンを受け取る
get: (env, token) => toOperation("", "(v_" + uid + ")"),
set: (env, token) => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n")),
});
コンパイルエラーメッセージの改善
インフラストラクチャーの整備が完了したので、コンパイルエラーメッセージを改善していきます。
Environment
が現在解析中のファイル名を保持できるようにする
Environment
に、「今何という名前のソースコードを解析しているか」を管理する機能を追加します。ファイル名にはeval
などもあり得ます。
class Environment {
constructor() {
~~
this._file = "anonymous"; // ファイル名管理機能を追加
}
~~
setFile(file) { // 追加
this._file = file;
}
getFile() { // 追加
return this._file;
}
}
カスタムエラークラスの追加
メッセージとトークン出現位置が含まれるオブジェクトを与えるとうまいことやってくれるようなエラークラスを作ります。
function loc(env, token) { // 追加
return `(${env.getFile()},L:${token.location.start.line},C:${token.location.start.column})`;
}
class Fluorite8CompileError extends Error { // カスタム例外を追加
constructor(message, env, token) {
super(message + " " + loc(env, token)); // メッセージにトークン位置を付与
this.name = "Fluorite8CompileError";
this.file = env.getFile();
this.token = token;
}
}
コンパイルエラーメッセージでカスタムクラスを利用
すべてのコンパイルエラーメッセージを次のように改善します。
compile(domain, token) {
const handlerTable = this._operatorRegistry[domain];
if (handlerTable === undefined) throw new Error("Unknown domain: " + domain);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + domain + "/" + token.type);
return handler(this, token);
}
↓
compile(domain, token) {
const handlerTable = this._operatorRegistry[domain];
// カスタム例外を使用する
if (handlerTable === undefined) throw new Fluorite8CompileError("Unknown domain: " + domain, this, token);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, this, token);
return handler(this, token);
}
Root
ルール側の改変
折角ソースコード名を設定できるようにしたので、使ってみます。
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
env.setFile("OnlineDemo"); // ソースコード名を指定
customizeEnvironment(env);
operation = env.compile("get", main);
} catch (e) {
console.log(e); // エラーを出力
return ["CompileError: " + e, token]; // Fl8を削除
}
const code = operation.head + operation.body;
let result;
try {
result = eval(code);
} catch (e) {
console.log(e); // エラーを出力
return ["RuntimeError: " + e, code, token]; // Fl8を削除
}
return [result, code, token];
}
結果
コンパイルエラーは次のような出力になりました。
arg -> (
arg = 5;
arg
)
CompileError: Fluorite8CompileError: Readonly identifier: arg (OnlineDemo,L:2,C:3)
「代入できない識別子に代入しようとした」というコンパイルエラーが、オンラインデモの2行目3文字目で発生したことが分かります。
実行時エラーのスタックトレースの改善
分かりにくいスタックトレースをよく見てみると、中間コード上の一時変数の名前が見えています。
const v_8 = (function(v_5) { // ←ここ
const v_6 = (v_5)((v_3));
const v_7 = (v_3) + (v_6);
return (v_7);
});
JavaScriptでは、生成時に何に代入したかによって無名関数の名前が自動的に決まります。
というわけでこれをうまく使って関数名にデバッグのヒントを与えます。
関数オブジェクトが名前に->
の出現位置を持つようにする
目標
現状のラムダ式a -> 式
は概ね次のようにコンパイルなります。
const v_0 = (function(v_1) {
前文;
return 本文;
});
(v_0)
これを次のような中間コードが生成されるようにします。
const v_2 = Symbol("Fl8Lambda(OnlineDemo,L:1,C:3)");
const v_0 = {[v_2]: function(v_1) {
前文;
return 本文;
}}[v_2];
(v_0)
実装
改変後のみ掲載します。
env.registerOperatorHandler("get", "minus_greater", (env, token) => {
const name = token.argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(token.argument[0].argument, {
get: (env, token) => toOperation("", "(v_" + uidBody + ")"),
});
const operationBody = env.compile("get", token.argument[1]);
env.popAliasFrame();
const label = `Fl8Lambda${loc(env, token)}`; // 関数名
const uidSymbol = env.getNextUid(); // 関数名を格納するシンボル用変数
const uid = env.getNextUid();
return toOperation(
// 関数名を格納するシンボルを定義
"const v_" + uidSymbol + " = Symbol(" + JSON.stringify(label) + ");\n" +
// オブジェクトを生成して即参照することで関数名にシンボルの説明文を指定する
"const v_" + uid + " = " + "{[v_" + uidSymbol + "]: function(v_" + uidBody + ") {\n" +
indent(
operationBody.head +
"return " + operationBody.body + ";\n"
) +
"}}[v_" + uidSymbol + "];\n",
"(v_" + uid + ")"
);
});
テスト
次のfl8コードを実行してみます。
(arg -> 1(1))(1)
const v_2 = Symbol("Fl8Lambda(OnlineDemo,L:1,C:6)");
const v_3 = {[v_2]: function(v_0) {
const v_1 = (1)((1));
return (v_1);
}}[v_2];
const v_4 = (v_3)((1));
(v_4)
うまく狙った中間コードが生成されました。
ちゃんとスタックトレースにも発生個所が出力されています。
関数名の出力
これだけでも良いですが、できれば関数名も表示させればより分かりやすいです。そこで、JavaScriptの無名関数の名前推定のような機構を追加します。
Environment
の改変
Environment
に「その場所で関数が生成されたとき、どんな名前になるのか」を管理する機能を追加します。
class Environment {
constructor() {
~~
this._suggestedName = undefined; // 推測名を管理するフィールド
}
~~
// compileメソッドは省略可能なオプション引数を持つ
compile(domain, token, options = {}) {
const handlerTable = this._operatorRegistry[domain];
if (handlerTable === undefined) throw new Fluorite8CompileError("Unknown domain: " + domain, this, token);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, this, token);
// この部分が追加
const suggestedName = this._suggestedName;
this._suggestedName = options.suggestedName; // 大抵はundefined
const operation = handler(this, token); // コンパイル時に推測名を与える
this._suggestedName = suggestedName; // コンパイルが終わったら推測名の設定は復元される
return operation;
}
~~
getSuggestedName() { // 推測名を得るメソッド
// undefinedの場合は「匿名」という名前とする
if (this._suggestedName === undefined) return "anonymous";
return this._suggestedName;
}
}
推測名の設定
関数の推測名を与えうるトークンにその機能を実装します。以下のものが該当します。
- 丸括弧
(formula)
におけるformula
- 推測名は自身に与えられたものを中継する。
- 変数宣言
variable : value
におけるvalue
- 推測名は
variable
と同じ。
- 推測名は
- 代入
acceptor = value
におけるvalue
- 推測名は
acceptor
が何かによる。 -
set
文脈ドメインの挙動オブジェクトに推測名を追加する。
- 推測名は
function toOperationSet(accept, suggestedName) { // 推測名を取るようになった
return {accept, suggestedName};
}
env.registerOperatorHandler("get", "round", (env, token) => {
env.pushAliasFrame();
const o1 = env.compile("get", token.argument[0], {
suggestedName: env.getSuggestedName(), // 推測名を中継
});
env.popAliasFrame();
return o1;
});
env.registerOperatorHandler("run", "colon", (env, token) => {
const name = token.argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, {
get: (env, token) => toOperation("", "(v_" + uid + ")"),
// set挙動が推測名を与えるように
set: (env, token) => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n"), name),
});
const operation = env.compile("get", token.argument[1], {
// 変数への代入時の推測名は、その変数の名前と同じ
suggestedName: name,
});
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
});
env.registerOperatorHandler("run", "equal", (env, token) => {
const operationSetLeft = env.compile("set", token.argument[0]);
const operationGetRight = env.compile("get", token.argument[1], {
suggestedName: operationSetLeft.suggestedName, // 推測名は左辺による
});
return toOperationRun(
operationSetLeft.accept(operationGetRight).head
);
});
推測名の利用
ラムダ式arg -> formula
側で生成する関数の名前に反映させます。
env.registerOperatorHandler("get", "minus_greater", (env, token) => {
const name = token.argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(token.argument[0].argument, {
get: (env, token) => toOperation("", "(v_" + uidBody + ")"),
});
const operationBody = env.compile("get", token.argument[1]);
env.popAliasFrame();
// ここで推測された関数名を使ってJavaScript関数の名前を作る
const label = `${env.getSuggestedName()}${loc(env, token)}`;
const uidSymbol = env.getNextUid();
const uid = env.getNextUid();
return toOperation(
"const v_" + uidSymbol + " = Symbol(" + JSON.stringify(label) + ");\n" +
"const v_" + uid + " = " + "{[v_" + uidSymbol + "]: function(v_" + uidBody + ") {\n" +
indent(
operationBody.head +
"return " + operationBody.body + ";\n"
) +
"}}[v_" + uidSymbol + "];\n",
"(v_" + uid + ")"
);
});
最初のコードの実行
冒頭で現れたこれを実行してみましょう。
f1 : a -> (
f2 : b -> (
f3 : c -> (
b + c(b)
);
f3
);
f2(a)
);
f1(1)(2)
let v_0;
const v_13 = Symbol("f1(OnlineDemo,L:1,C:8)");
const v_14 = {[v_13]: function(v_1) {
let v_2;
const v_10 = Symbol("f2(OnlineDemo,L:2,C:10)");
const v_11 = {[v_10]: function(v_3) {
let v_4;
const v_8 = Symbol("f3(OnlineDemo,L:3,C:12)");
const v_9 = {[v_8]: function(v_5) {
const v_6 = (v_5)((v_3));
const v_7 = (v_3) + (v_6);
return (v_7);
}}[v_8];
v_4 = (v_9);
return (v_4);
}}[v_10];
v_2 = (v_11);
const v_12 = (v_2)((v_1));
return (v_12);
}}[v_13];
v_0 = (v_14);
const v_15 = (v_0)((1));
const v_16 = (v_15)((2));
(v_16)
中間コード上にfl8ソースコードの宣言した変数名が現れるようになりました。
相変わらずメッセージ本文に一時変数の内容が見えていますが、将来的に「呼び出し不可能な値を呼び出そうとした」というJavaScript上のエラーは型チェックにより発生しないようにするので、この一時変数は現れなくなります。
fl8ソースコード上の関数名やトークン出現位置が、ちゃんとスタックトレースにも表れています。
OnlineDemo
の3行目12文字目といえば、↓ここです。厳密に何行目で発生したのかは分かりませんが、発生個所を調べる範囲は高々1個の関数内で済みます。
f1 : a -> (
f2 : b -> (
f3 : c -> ( // ←ここ
b + c(b)
);
f3
);
f2(a)
);
f1(1)(2)
まとめ
ここまでに出来上がったPEG.jsコードです。
**[開閉]**
{
function loc(env, token) {
return `(${env.getFile()},L:${token.location.start.line},C:${token.location.start.column})`;
}
class Fluorite8CompileError extends Error {
constructor(message, env, token) {
super(message + " " + loc(env, token));
this.name = "Fluorite8CompileError";
this.file = env.getFile();
this.token = token;
}
}
class Environment {
constructor() {
this._nextUid = 0;
this._aliasFrame = Object.create(null);
this._operatorRegistry = Object.create(null);
this._file = "anonymous";
this._suggestedName = undefined;
}
getNextUid() {
return this._nextUid++;
}
registerAlias(alias, handlerTable) {
this._aliasFrame[alias] = handlerTable;
}
resolveAlias(alias) {
return this._aliasFrame[alias];
}
pushAliasFrame() {
this._aliasFrame = Object.create(this._aliasFrame);
}
popAliasFrame() {
this._aliasFrame = Object.getPrototypeOf(this._aliasFrame);
}
registerOperatorHandler(domain, type, handler) {
if (this._operatorRegistry[domain] === undefined) this._operatorRegistry[domain] = Object.create(null);
this._operatorRegistry[domain][type] = handler;
}
compile(domain, token, options = {}) {
const handlerTable = this._operatorRegistry[domain];
if (handlerTable === undefined) throw new Fluorite8CompileError("Unknown domain: " + domain, this, token);
const handler = handlerTable[token.type];
if (handler === undefined) throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, this, token);
const suggestedName = this._suggestedName;
this._suggestedName = options.suggestedName;
const operation = handler(this, token);
this._suggestedName = suggestedName;
return operation;
}
setFile(file) {
this._file = file;
}
getFile() {
return this._file;
}
getSuggestedName() {
if (this._suggestedName === undefined) return "anonymous";
return this._suggestedName;
}
}
function customizeEnvironment(env) {
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
function toOperation(head, body) {
return {head, body};
}
function toOperationSet(accept, suggestedName) {
return {accept, suggestedName};
}
function toOperationRun(head) {
return {head};
}
env.registerOperatorHandler("get", "integer", (env, token) => toOperation("", "(" + parseInt(token.argument, 10) + ")"));
env.registerOperatorHandler("get", "identifier", (env, token) => {
const handlerTable = env.resolveAlias(token.argument);
if (handlerTable === undefined) throw new Fluorite8CompileError("Unknown identifier: " + token.argument, env, token);
const handler = handlerTable["get"];
if (handler === undefined) throw new Fluorite8CompileError("Unreadable identifier: " + token.argument, env, token);
return handler(env);
});
env.registerOperatorHandler("get", "round", (env, token) => {
env.pushAliasFrame();
const o1 = env.compile("get", token.argument[0], {
suggestedName: env.getSuggestedName(),
});
env.popAliasFrame();
return o1;
});
env.registerOperatorHandler("get", "left_plus", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = +" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "left_minus", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = -" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "right_round", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
env.pushAliasFrame();
const o2 = env.compile("get", token.argument[1]);
env.popAliasFrame();
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + "(" + o2.body + ");\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "plus", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " + " + o2.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "minus", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " - " + o2.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "asterisk", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " * " + o2.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "slash", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = " + o1.body + " / " + o2.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "circumflex", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const uid = env.getNextUid();
return toOperation(
o1.head + o2.head + "const v_" + uid + " = Math.pow(" + o1.body + ", " + o2.body + ");\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "ternary_question_colon", (env, token) => {
const o1 = env.compile("get", token.argument[0]);
const o2 = env.compile("get", token.argument[1]);
const o3 = env.compile("get", token.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 + ")"
);
});
env.registerOperatorHandler("get", "minus_greater", (env, token) => {
const name = token.argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(token.argument[0].argument, {
get: (env, token) => toOperation("", "(v_" + uidBody + ")"),
});
const operationBody = env.compile("get", token.argument[1]);
env.popAliasFrame();
const label = `${env.getSuggestedName()}${loc(env, token)}`;
const uidSymbol = env.getNextUid();
const uid = env.getNextUid();
return toOperation(
"const v_" + uidSymbol + " = Symbol(" + JSON.stringify(label) + ");\n" +
"const v_" + uid + " = " + "{[v_" + uidSymbol + "]: function(v_" + uidBody + ") {\n" +
indent(
operationBody.head +
"return " + operationBody.body + ";\n"
) +
"}}[v_" + uidSymbol + "];\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "semicolons", (env, token) => {
const heads = [];
for (let i = 0; i < token.argument.length - 1; i++) {
const operation = env.compile("run", token.argument[i]);
heads.push(operation.head);
}
const operation = env.compile("get", token.argument[token.argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
});
env.registerOperatorHandler("set", "identifier", (env, token) => {
const handlerTable = env.resolveAlias(token.argument);
if (handlerTable === undefined) throw new Fluorite8CompileError("Unknown identifier: " + token.argument, env, token);
const handler = handlerTable["set"];
if (handler === undefined) throw new Fluorite8CompileError("Readonly identifier: " + token.argument, env, token);
return handler(env);
});
env.registerOperatorHandler("run", "colon", (env, token) => {
const name = token.argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, {
get: (env, token) => toOperation("", "(v_" + uid + ")"),
set: (env, token) => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n"), name),
});
const operation = env.compile("get", token.argument[1], {
suggestedName: name,
});
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
});
env.registerOperatorHandler("run", "equal", (env, token) => {
const operationSetLeft = env.compile("set", token.argument[0]);
const operationGetRight = env.compile("get", token.argument[1], {
suggestedName: operationSetLeft.suggestedName,
});
return toOperationRun(
operationSetLeft.accept(operationGetRight).head
);
});
env.registerAlias("PI", {
get: (env, token) => toOperation("", "(" + Math.PI + ")"),
});
}
function token(type, argument, location) {
return {type, argument, location};
}
}
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
env.setFile("OnlineDemo");
customizeEnvironment(env);
operation = env.compile("get", main);
} catch (e) {
console.log(e);
return ["CompileError: " + e, token];
}
const code = operation.head + operation.body;
let result;
try {
result = eval(code);
} catch (e) {
console.log(e);
return ["RuntimeError: " + e, code, token];
}
return [result, code, token];
}
Formula = Semicolons
Semicolons = head:Lambda tail:(_ (";" { return location(); }) _ Lambda)* {
if (tail.length == 0) return head;
return token("semicolons", [head, ...tail.map(s => s[3])], tail[0][1]);
}
Lambda = head:(If _ (
"->" { return ["minus_greater", location()]; }
/ ":" { return ["colon", location()]; }
/ "=" { return ["equal", location()]; }
) _)* tail:If {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][2][0], [head[i][0], result], head[i][2][1]);
}
return result;
}
If = head:Add _ operator:("?" { return location(); }) _ body:If _ ":" _ tail:If {
return token("ternary_question_colon", [head, body, tail], operator);
}
/ Add
Add = head:Term tail:(_ (
"+" { return ["plus", location()]; }
/ "-" { return ["minus", location()]; }
) _ Term)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1][0], [result, tail[i][3]], tail[i][1][1]);
}
return result;
}
Term = head:Left tail:(_ (
"*" { return ["asterisk", location()]; }
/ "/" { return ["slash", location()]; }
) _ Left)* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1][0], [result, tail[i][3]], tail[i][1][1]);
}
return result;
}
Left = head:((
"+" { return ["left_plus", location()]; }
/ "-" { return ["left_minus", location()]; }
) _)* tail:Pow {
let result = tail;
for (let i = head.length - 1; i >= 0; i--) {
result = token(head[i][0][0], [result], head[i][0][1]);
}
return result;
}
Pow = head:Right _ operator:(
"^" { return ["circumflex", location()]; }
) _ tail:Left {
return token(operator[0], [head, tail], operator[1]);
}
/ Right
Right = head:Factor tail:(_ (
"(" _ main:Formula _ ")" { return ["right_round", [main], location()] }
))* {
let result = head;
for (let i = 0; i < tail.length; i++) {
result = token(tail[i][1][0], [result, ...tail[i][1][1]], tail[i][1][2]);
}
return result;
}
Factor = Integer
/ Identifier
/ Brackets
Integer = main:$[0-9]+ {
return token("integer", main, location());
}
Identifier = main:$([a-zA-Z_] [a-zA-Z0-9_]*) {
return token("identifier", main, location());
}
Brackets = "(" _ main:Formula _ ")" {
return token("round", [main], location());
}
_ = [ \t\r\n]*
この時点で次の特徴があります。
- ソースコードから構文木の生成 (改善)
- 構文木から中間コードの生成
- 中間コードの評価
- 副作用を持つ演算の適切な順序での実行
- エラー出力の改善 (改善)
- トークンの文脈の管理
- 識別子の文脈の管理
- トークンの出現場所の管理 (新規)
- ソースファイル名の管理 (新規)
- 関数名の推測 (新規)
- スペース
- 識別子
- 組み込み定数 (ドメイン:
get
)PI
- 引数 (ドメイン:
get
) - 変数 (ドメイン:
get
set
)
- 組み込み定数 (ドメイン:
-
get
ドメイントークン- 整数リテラル
123
- 識別子
identifier
- 丸括弧
(formula)
- 関数呼び出し
function(argument)
- べき乗
a ^ b
- 符号
+a
-a
- 加減乗除
a + b
a - b
a * b
a / b
- 三項演算子
cond ? then ? else
- ラムダ式
arg -> formula
- 文
;
- 整数リテラル
-
set
ドメイントークン- 識別子
identifier
- 識別子
-
run
ドメイントークン- 変数宣言
variable : value
- 代入
acceptor = value
- 変数宣言
この機構はあれこれ実装した後で導入しようとすると影響箇所が膨大になり地獄が見えます。