LoginSignup
0
0

More than 3 years have passed since last update.

fluorite-8 実装の軌跡 Part 6 代入文と文脈

Last updated at Posted at 2020-11-08

初回

←前回

今回の目標

今回は次のような代入文を作ります。

fl8
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を受け取り、カスタマイズします。この関数は組み込み定数の定義も行います。

indenttoOperationcustomizeEnvironment内でしか利用しないため、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コード全体の構造は概ね以下のようになりました。customizeEnvironmentEnvironmentを直接参照しないので、ソースコードを分離して別の場所に持っていくこともできます。

全体構造の概要
{

  class Environment {
    ~~
  }

  function customizeEnvironment(env) {
    ~~
  }

  function token(type, argument) {
    ~~
  }

}
Root     = _ main:Formula _ {
             ~~
           }
その他のルール

文脈の管理

代入式の左辺に現れるトークンの挙動は、通常の式とは異なります。

例えば、組み込み定数PIは現在(3.141592653589793)という中間コードにコンパイルされます。これを単純に左辺に持ってきた場合、次のように異常な中間コードが生成されます。

代入文が追加されたfl8
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]*

代入文本体を実装

代入文を実装するために、とりあえず代入文本体を実装します。

目標

以下の要件を満たす代入文を追加します。

  1. acceptor = value;と書くと、acceptorが表す変数等に、valueの値を代入する。

戦略

まず、valueは普通にget文脈ドメインでコンパイルします。一方acceptorset文脈ドメインでコンパイルされる必要があります。

そして、これらをコンパイルして得られた挙動オブジェクトを組み合わせて中間コードを作ります。

実装

代入系演算子がまとまっている場所に中置=を追加します。

改変後
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文脈ドメイン時の挙動を実装

ここでは代入文の左辺に識別子を置けるように、識別子が代入文の左辺に現れた場合の挙動を定義します。

目標

次のようにします。

  1. 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);
    });

これで識別子を左辺に置くことができるようになりましたが、まだ代入可能な識別子が存在しません。

変数を書き換え可能にする

ここでは変数を書き換えられるようにします。

目標

次のようにします。

  1. 変数宣言文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"
      );
    });

テスト

これで変数は再代入が可能になりました。


次のコードを実行してみます。

fl8
a : 5;
a = a * a;
a

image.png

ちゃんと代入出来ました。


左辺に組み込み定数PIがあったら?

image.png

組み込み定数は書き込み時の挙動が設定されていないので書き込めません。同様にラムダ式の引数にも現状書き込めません。


左辺がリテラルだったら?

image.png

リテラルに代入したときの挙動が設定されていないので代入できません。ちゃんと、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 (新規)

今回は手続き型言語へのパラダイムシフトが起こりました。

次回→

0
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
0
0