今回の目標
今回は次のような代入文を作ります。
a : 5;
a = a * a;
a
このソースコードが返す値は25です。
そして、今回はこれをスマートに実現するために「大雑把な文脈」(トークンや識別子がどのような位置に書かれたか)の概念を管理する機構を作る必要があります。
compile
関数をEnvironment
のメソッドに変更
コンパイル関係のリファクタリングです。
Environment
付近の改変
トークン挙動定義関数は今までグローバル的な場所に置かれていたregistry
およびcompile
関数で管理されていましたが、これをEnvironment
の中に入れます。
こうすることでコンパイルに必要なものがEnvironment
内にまとまり、管理がしやすくなります。
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);
}
}
const registry = {};
function compile(env, token) {
const handler = registry[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + token.type);
return handler(env, token.argument);
}
↓
class Environment {
constructor() {
this._nextUid = 0; // 名前が変わった
this._aliasFrame = Object.create(null); // 名前が変わった
this._operatorRegistry = Object.create(null); // 元々グローバル的オブジェクトのregistryだった
}
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);
}
registerOperatorHandler(type, handler) { // 元々registerに直接書き込んでいた
this._operatorRegistry[type] = handler;
}
compile(token) { // 元々グローバル的関数だった
const handler = this._operatorRegistry[token.type];
if (handler === undefined) throw new Error("Unknown operator: " + token.type);
return handler(this, token.argument);
}
}
トークン挙動定義部分の改変
トークンの挙動の定義は、これからはconst env = new Environment();
した後でenv.registerOperatorHandler(type, handler)
によって行います。
customizeEnvironment
関数は空のEnvironment
を受け取り、カスタマイズします。この関数は組み込み定数の定義も行います。
indent
やtoOperation
はcustomizeEnvironment
内でしか利用しないため、customizeEnvironment
関数の中に格納します。
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
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;
};
~その他のトークン挙動定義~
↓
function customizeEnvironment(env) {
// ここでしか使わないので関数内に格納
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
function toOperation(head, body) {
return {head, body};
}
// 登録にメソッドを使うようになった
// 元:
// registry.integer = (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")");
env.registerOperatorHandler("integer", (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")"));
~その他のトークン挙動定義~
env.registerOperatorHandler("round", (env, argument) => {
env.pushAliasFrame();
const o1 = env.compile(argument[0]); // 関数からメソッドになった
env.popAliasFrame();
return o1;
});
~その他のトークン挙動定義~
env.registerAlias("PI", "(" + Math.PI + ")");
}
Root
ルールの改変
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];
}
↓
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment(); // 空の環境を作る
customizeEnvironment(env); // 環境をカスタマイズする
// PIの定義はcustomizeEnvironmentに移動させた
operation = env.compile(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];
}
全体構造
これで、PEG.jsコード全体の構造は概ね以下のようになりました。customizeEnvironment
はEnvironment
を直接参照しないので、ソースコードを分離して別の場所に持っていくこともできます。
{
class Environment {
~~
}
function customizeEnvironment(env) {
~~
}
function token(type, argument) {
~~
}
}
Root = _ main:Formula _ {
~~
}
~その他のルール~
文脈の管理
代入式の左辺に現れるトークンの挙動は、通常の式とは異なります。
例えば、組み込み定数PI
は現在(3.141592653589793)
という中間コードにコンパイルされます。これを単純に左辺に持ってきた場合、次のように異常な中間コードが生成されます。
PI = 500;
(3.1415926535) = 500;
この中間コードを評価しようとしてeval関数に入れると、SyntaxError: invalid assignment left-hand side
という構文エラーによって終了します。
なんでも代入文の左辺式にできるわけではなく、むしろ代入可能なトークンの種類は極めて限定されます。そのため、ここでは**通常の値を返す式という文脈get
**と、**代入文の左辺という文脈set
**を分け、トークン挙動定義関数のテーブルも別に管理します。
実装
トークンが大雑把にどのような場所に現れたかを、ここでは文脈ドメインという言葉で表すことにしておきます。
トークンの挙動は文脈ドメインごとに定義されるという風にEnvironment
・トークン挙動定義部分・Root
の挙動を改変します。
class Environment {
constructor() {
~~
this._operatorRegistry = Object.create(null); // データ構造が2重のマップに変わった
}
~~
registerOperatorHandler(domain, type, handler) { // 登録時にドメインの指定が必須
// ドメインが未登録の場合、新規にテーブルを作る
if (this._operatorRegistry[domain] === undefined) this._operatorRegistry[domain] = Object.create(null);
this._operatorRegistry[domain][type] = handler; // ドメインとトークン種類の順に引く
}
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);
}
}
env.registerOperatorHandler("get", "round", (env, argument) => { // ここが変わった
env.pushAliasFrame();
const o1 = env.compile("get", argument[0]); // ここが変わった
env.popAliasFrame();
return o1;
});
~その他のトークン挙動定義~
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
customizeEnvironment(env);
operation = env.compile("get", 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];
}
これで、代入文の左辺式をコンパイルするときはconst operationSet = env.compile("get", token)
のように書くことができます。operationSet
には、中間コード上で実際に代入の処理を行うJavaScriptコードを組み立てるために必要なオブジェクトが格納されます。
識別子の挙動を文脈ドメインごとに管理する
識別子ごとに代入可能かどうかが異なるため、個々の識別子にもトークンと同様に出現した文脈ドメインごとの挙動を定義できるようにします。
これまで識別子フレームにはget
時に中間コードにくっつけるJavaScriptコード文字列を直接登録していましたが、これからは文脈ドメインから識別子挙動定義関数を引くテーブルが得られるようにします。
Environment
の改変
Environment
では意味的な型が変わります。JavaScriptには型はありませんが、型に合わせて変数名を対応させます。
class Environment {
~~
registerAlias(alias, code) {
this._aliasFrame[alias] = code;
}
~~
}
↓
class Environment {
~~
registerAlias(alias, handlerTable) { // 引数名が変わっただけ
this._aliasFrame[alias] = handlerTable;
}
~~
}
識別子登録部分の改変
識別子の登録は次のように、文字列ではなく識別子挙動定義関数のテーブルを渡します。
env.registerAlias("PI", "(" + Math.PI + ")");
↓
env.registerAlias("PI", {
get: env => toOperation("", "(" + Math.PI + ")"),
});
識別子利用部分の改変
識別子の登録内容が変わったのでenv.resolveAlias
から帰って来る値も変わりました。このメソッドは現在identifier
トークンのget
文脈ドメインの挙動定義関数から呼び出されているので、これを対応させます。
env.registerOperatorHandler("get", "identifier", (env, argument) => {
const code = env.resolveAlias(argument);
if (code !== undefined) return toOperation("", code);
throw new Error("Unknown identifier: " + argument);
});
↓
env.registerOperatorHandler("get", "identifier", (env, argument) => {
const handlerTable = env.resolveAlias(argument); // 挙動定義関数テーブルが帰って来る
if (handlerTable === undefined) throw new Error("Unknown identifier: " + argument);
const handler = handlerTable["get"]; // 挙動定義関数を引く
if (handler === undefined) throw new Error("Unreadable identifier: " + argument);
return handler(env); // 挙動オブジェクトが返って来る
});
文の挙動定義をセミコロン;
演算子から分離
現在のセミコロン;
演算子の挙動定義関数は、それ自体に変数宣言のルーチンが書かれています。このままでは新しい文を追加する度にこの関数が肥大化するので、これを**実行run
**文脈ドメインを経由する形に整理します。
実行run
文脈ドメインの挙動定義関数は(env, argument) => {head: 前文}
という形をしています。実行run
時には副作用のある効果しか行われないので、本文body
を持ちません。
env.registerOperatorHandler("get", "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, {
get: env => toOperation("", "(v_" + uid + ")"),
});
const operation = env.compile("get", argument[i].argument[1]);
heads.push(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
}
const operation = env.compile("get", argument[argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
});
↓
function toOperationRun(head) { // run文脈ドメインの挙動オブジェクトを組み立てる関数
return {head};
}
env.registerOperatorHandler("get", "semicolons", (env, argument) => {
const heads = [];
for (let i = 0; i < argument.length - 1; i++) { // 最後以外の部位について、
const operation = env.compile("run", argument[i]); // run文脈ドメインでコンパイルして、
heads.push(operation.head); // その前文を前文リストに追加
}
const operation = env.compile("get", argument[argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
});
env.registerOperatorHandler("run", "colon", (env, argument) => {
const name = argument[0].argument; // 中置「:」の左辺を変数名として取得
const uid = env.getNextUid(); // 変数用のUIDを発行
env.registerAlias(name, {
get: env => toOperation("", "(v_" + uid + ")"),
}); // 常に変数実体を返す識別子を登録
const operation = env.compile("get", argument[1]); // 右辺をコンパイル
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
// get用の挙動オブジェクトと異なり、本文はない
);
});
これで;
の左側に変数宣言文以外の文を色々登録できるようになりました。
ここまでのまとめ
これで代入文を実装するためのインフラは整いました。ここまでソースコードのリファクタリングを行っただけで、外部から見た動作は変わりません。
ここまでに出来上がったPEG.jsコードです。
**[開閉]**
{
class Environment {
constructor() {
this._nextUid = 0;
this._aliasFrame = Object.create(null);
this._operatorRegistry = Object.create(null);
}
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) {
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);
}
}
function customizeEnvironment(env) {
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
function toOperation(head, body) {
return {head, body};
}
function toOperationRun(head) {
return {head};
}
env.registerOperatorHandler("get", "integer", (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")"));
env.registerOperatorHandler("get", "identifier", (env, argument) => {
const handlerTable = env.resolveAlias(argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + argument);
const handler = handlerTable["get"];
if (handler === undefined) throw new Error("Unreadable identifier: " + argument);
return handler(env);
});
env.registerOperatorHandler("get", "round", (env, argument) => {
env.pushAliasFrame();
const o1 = env.compile("get", argument[0]);
env.popAliasFrame();
return o1;
});
env.registerOperatorHandler("get", "left_plus", (env, argument) => {
const o1 = env.compile("get", argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = +" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "left_minus", (env, argument) => {
const o1 = env.compile("get", argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = -" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "right_round", (env, argument) => {
const o1 = env.compile("get", argument[0]);
env.pushAliasFrame();
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", argument[1]);
const o3 = env.compile("get", 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, argument) => {
const name = argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(argument[0].argument, {
get: env => toOperation("", "(v_" + uidBody + ")"),
});
const operationBody = env.compile("get", 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 + ")"
);
});
env.registerOperatorHandler("get", "semicolons", (env, argument) => {
const heads = [];
for (let i = 0; i < argument.length - 1; i++) {
const operation = env.compile("run", argument[i]);
heads.push(operation.head);
}
const operation = env.compile("get", argument[argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
});
env.registerOperatorHandler("run", "colon", (env, argument) => {
const name = argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, {
get: env => toOperation("", "(v_" + uid + ")"),
});
const operation = env.compile("get", argument[1]);
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
});
env.registerAlias("PI", {
get: env => toOperation("", "(" + Math.PI + ")"),
});
}
function token(type, argument) {
return {type, argument};
}
}
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
customizeEnvironment(env);
operation = env.compile("get", 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]*
代入文本体を実装
代入文を実装するために、とりあえず代入文本体を実装します。
目標
以下の要件を満たす代入文を追加します。
-
acceptor = value;
と書くと、acceptor
が表す変数等に、value
の値を代入する。
戦略
まず、value
は普通にget
文脈ドメインでコンパイルします。一方acceptor
はset
文脈ドメインでコンパイルされる必要があります。
そして、これらをコンパイルして得られた挙動オブジェクトを組み合わせて中間コードを作ります。
実装
代入系演算子がまとまっている場所に中置=
を追加します。
Lambda = head:(If _ (
"->" { return "minus_greater"; }
/ ":" { return "colon"; }
/ "=" { return "equal"; } // ここが追加された
) _)* 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;
}
中置=
のrun
文脈ドメインの挙動を定義します。
env.registerOperatorHandler("run", "equal", (env, argument) => {
const operationSetLeft = env.compile("set", argument[0]); // 左辺をコンパイル
const operationGetRight = env.compile("get", argument[1]); // 右辺をコンパイル
return toOperationRun(
operationSetLeft.accept(operationGetRight).head // 挙動オブジェクトを組み合わせる
);
});
**set
文脈ドメインの挙動オブジェクトoperationSet
は、「get
文脈ドメインの挙動オブジェクトoperationGet
**を受け取り{head: 前文}
というデータ構造を返すaccept
メソッド」を持つことにします。
const operationSet = {
accept: operationGet => {
head: operationGet.head + "variable = " + operationGet.body + ";\n"
},
};
operationSet.accept({
head: "",
body: "(567)",
}).head
これで代入文自体はできましたが、まだ左辺に何も置くことが出来ません。
識別子identiier
トークンのset
文脈ドメイン時の挙動を実装
ここでは代入文の左辺に識別子を置けるように、識別子が代入文の左辺に現れた場合の挙動を定義します。
目標
次のようにします。
-
identifier = value;
と書いたときに、identifier
が代入可能であれば代入を行う。
戦略
上で、識別子を識別子辞書から引っ張ってくると挙動定義関数のテーブルが返るようにしました。なので、識別子がset
という動作が可能であればそれを呼び出すようにします。
実装
識別子を識別子辞書から引いて来て、set
可能であればその挙動オブジェクトを返します。
env.registerOperatorHandler("set", "identifier", (env, argument) => {
const handlerTable = env.resolveAlias(argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + argument);
const handler = handlerTable["set"];
if (handler === undefined) throw new Error("Readonly identifier: " + argument);
return handler(env);
});
これで識別子を左辺に置くことができるようになりましたが、まだ代入可能な識別子が存在しません。
変数を書き換え可能にする
ここでは変数を書き換えられるようにします。
目標
次のようにします。
- 変数宣言文
variable : value;
で宣言した変数は、代入文acceptor = value;
で値を書き換えられる。
戦略
変数宣言文variable : value;
で変数を識別子辞書に登録する際に、set
時の挙動を定義します。
実装
中置:
の挙動定義関数を改変します。これで、中置:
が登録した識別子はset
文脈で現れた際に変数への代入を行う中間コードを生成します。
function toOperationSet(accept) {
return {accept};
}
env.registerOperatorHandler("run", "colon", (env, argument) => {
const name = argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, {
get: env => toOperation("", "(v_" + uid + ")"),
// setできるようになった
set: env => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n")),
});
const operation = env.compile("get", argument[1]);
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
});
テスト
これで変数は再代入が可能になりました。
次のコードを実行してみます。
a : 5;
a = a * a;
a
ちゃんと代入出来ました。
左辺に組み込み定数PI
があったら?
組み込み定数は書き込み時の挙動が設定されていないので書き込めません。同様にラムダ式の引数にも現状書き込めません。
左辺がリテラルだったら?
リテラルに代入したときの挙動が設定されていないので代入できません。ちゃんと、JavaScriptではなくfl8上で管理されたエラーメッセージが出ます。
まとめ
ここまでに出来上がったPEG.jsコードです。
**[開閉]**
{
class Environment {
constructor() {
this._nextUid = 0;
this._aliasFrame = Object.create(null);
this._operatorRegistry = Object.create(null);
}
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) {
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);
}
}
function customizeEnvironment(env) {
function indent(code) {
return " " + code.replace(/\n(?!$)/g, "\n ");
}
function toOperation(head, body) {
return {head, body};
}
function toOperationSet(accept) {
return {accept};
}
function toOperationRun(head) {
return {head};
}
env.registerOperatorHandler("get", "integer", (env, argument) => toOperation("", "(" + parseInt(argument, 10) + ")"));
env.registerOperatorHandler("get", "identifier", (env, argument) => {
const handlerTable = env.resolveAlias(argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + argument);
const handler = handlerTable["get"];
if (handler === undefined) throw new Error("Unreadable identifier: " + argument);
return handler(env);
});
env.registerOperatorHandler("get", "round", (env, argument) => {
env.pushAliasFrame();
const o1 = env.compile("get", argument[0]);
env.popAliasFrame();
return o1;
});
env.registerOperatorHandler("get", "left_plus", (env, argument) => {
const o1 = env.compile("get", argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = +" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "left_minus", (env, argument) => {
const o1 = env.compile("get", argument[0]);
const uid = env.getNextUid();
return toOperation(
o1.head + "const v_" + uid + " = -" + o1.body + ";\n",
"(v_" + uid + ")"
);
});
env.registerOperatorHandler("get", "right_round", (env, argument) => {
const o1 = env.compile("get", argument[0]);
env.pushAliasFrame();
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", 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, argument) => {
const o1 = env.compile("get", argument[0]);
const o2 = env.compile("get", argument[1]);
const o3 = env.compile("get", 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, argument) => {
const name = argument[0].argument;
const uidBody = env.getNextUid();
env.pushAliasFrame();
env.registerAlias(argument[0].argument, {
get: env => toOperation("", "(v_" + uidBody + ")"),
});
const operationBody = env.compile("get", 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 + ")"
);
});
env.registerOperatorHandler("get", "semicolons", (env, argument) => {
const heads = [];
for (let i = 0; i < argument.length - 1; i++) {
const operation = env.compile("run", argument[i]);
heads.push(operation.head);
}
const operation = env.compile("get", argument[argument.length - 1]);
return toOperation(
heads.join("") +
operation.head,
operation.body
);
});
env.registerOperatorHandler("set", "identifier", (env, argument) => {
const handlerTable = env.resolveAlias(argument);
if (handlerTable === undefined) throw new Error("Unknown identifier: " + argument);
const handler = handlerTable["set"];
if (handler === undefined) throw new Error("Readonly identifier: " + argument);
return handler(env);
});
env.registerOperatorHandler("run", "colon", (env, argument) => {
const name = argument[0].argument;
const uid = env.getNextUid();
env.registerAlias(name, {
get: env => toOperation("", "(v_" + uid + ")"),
set: env => toOperationSet(o => toOperationRun(o.head + "v_" + uid + " = " + o.body + ";\n")),
});
const operation = env.compile("get", argument[1]);
return toOperationRun(
"let v_" + uid + ";\n" +
operation.head +
"v_" + uid + " = " + operation.body + ";\n"
);
});
env.registerOperatorHandler("run", "equal", (env, argument) => {
const operationSetLeft = env.compile("set", argument[0]);
const operationGetRight = env.compile("get", argument[1]);
return toOperationRun(
operationSetLeft.accept(operationGetRight).head
);
});
env.registerAlias("PI", {
get: env => toOperation("", "(" + Math.PI + ")"),
});
}
function token(type, argument) {
return {type, argument};
}
}
Root = _ main:Formula _ {
const token = main;
let operation;
try {
const env = new Environment();
customizeEnvironment(env);
operation = env.compile("get", 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"; }
/ "=" { return "equal"; }
) _)* 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]*
この時点で次の特徴があります。
- ソースコードから構文木の生成
- 構文木から中間コードの生成
- 中間コードの評価
- 副作用を持つ演算の適切な順序での実行
- エラー出力の改善
- トークンの文脈の管理 (新規)
- 識別子の文脈の管理 (新規)
- スペース
- 識別子
- 組み込み定数 (ドメイン:
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
(新規)
- 変数宣言
今回は手続き型言語へのパラダイムシフトが起こりました。