1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

fluorite-8 実装の軌跡 Part 7 エラー発生位置の出力

Last updated at Posted at 2020-11-09

初回

←前回

今回の目標

エラー文が分かりにくい問題

突然ですが、次のコードの実行結果を見てみましょう。

fl8
f1 : a -> (
  f2 : b -> (
    f3 : c -> (
      b + c(b)
    );
    f3
  );
  f2(a)
);

f1(1)(2)

image.png

Fl8RuntimeError: TypeError: v_5 is not a functionというエラーが出ました。「どこかの変数v_5に入っている関数でない何かを呼び出そうとして実行時エラーが出た」ということは分かりますが、ソースコード上のどのあたりで発生したのかは分かりません。


このエラーオブジェクトをコンソールに出力しても、「evalの7行目にあるv_8という関数の中」というところまでしかわかりません。

image.png


中間コードを見に行ってみると、確かに変数v_8に格納された関数の中にある7行目でv_5を実行しようとしています。これはfl8ソースコード上のc(b)に相当します。

中間コード:eval
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

また、コンパイルエラーにはメッセージ自体にエラーが発生した位置を与えます。

image.png

現状
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()と書くと、そのルールにヒットした部分の位置を得ることができます。

image.png

これを構文木に添付すれば意味解析側でトークンの出現位置を特定できます。

実装

出現場所は、リテラルや識別子などはその左端、中置演算子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関数呼び出し個所において同様にします。

結果

次のコードの構文木オブジェクトの内容は、次のようになります。構文木オブジェクトは、トークンの種類、項やリテラルの内容の他に、出現位置の情報も持つようになりました。

fl8
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];
           }

結果

コンパイルエラーは次のような出力になりました。

fl8
arg -> (
  arg = 5;
  arg
)

image.png

CompileError: Fluorite8CompileError: Readonly identifier: arg (OnlineDemo,L:2,C:3)

「代入できない識別子に代入しようとした」というコンパイルエラーが、オンラインデモの2行目3文字目で発生したことが分かります。

実行時エラーのスタックトレースの改善

分かりにくいスタックトレースをよく見てみると、中間コード上の一時変数の名前が見えています。

image.png

中間コードの一部分
    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コードを実行してみます。

fl8
(arg -> 1(1))(1)

image.png

中間コード
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)

うまく狙った中間コードが生成されました。


ちゃんとスタックトレースにも発生個所が出力されています。

image.png

関数名の出力

これだけでも良いですが、できれば関数名も表示させればより分かりやすいです。そこで、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 + ")"
      );
    });

最初のコードの実行

冒頭で現れたこれを実行してみましょう。

fl8
f1 : a -> (
  f2 : b -> (
    f3 : c -> (
      b + c(b)
    );
    f3
  );
  f2(a)
);

f1(1)(2)

image.png

中間コード
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上のエラーは型チェックにより発生しないようにするので、この一時変数は現れなくなります。


image.png

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

この機構はあれこれ実装した後で導入しようとすると影響箇所が膨大になり地獄が見えます。

次回→

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?