0
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 8 配列と文脈リダイレクション

Last updated at Posted at 2020-11-17

初回

←前回

今回の目標

今回は概ね以下のような動作をする、次の演算子を作ります。

トークンの種類 get文脈ドメインでの挙動
空括弧[] 空配列初期化子[]
括弧[ ] 配列初期化子[values]
前置$# 長さ演算子$#array
後置括弧[ ] 配列要素アクセサarray[index]
疑似fl8ソースコード
empty_array :   []          ;
array       :   [1; 2; 3]   ;
length      : $#[1; 2; 3]   ;
item        :   [1; 2; 3][1];

疑似JavaScript中間コード
let empty_attay  = []              ;
let attay        = [1, 2, 3]       ;
let length       = [1, 2, 3].length;
let item         = [1, 2, 3][1]    ;

長さの取得はPerlやbashのように前置$#で行います。最大の添え字ではなく要素数が得られ、空配列のときに0になります。これは、通常「最大の添え字+1」と考えて問題ありません。

配列要素アクセサarray[index]は、indexが0のときに先頭の要素を返します。すなわち添え字は0から始まります。

式の区切りはセミコロン;

fluorite-8では、引数やオブジェクト初期化子のような式を区切る表現は原則としてセミコロン;で行います。

トップレベルや( )の中で;を使うと命令の区切りになり、[ ]の中で;を使うと項の区切りになり、{ }の中で;を使うと宣言項目の区切りになるという、文脈によって意味の変わるセミコロン;がこの言語の大きな特徴です。

配列初期化子[values]のパターン

配列初期化子[values]には次の2種類の現れ方があります。

  • [1]valuesが1個の要素であるパターン
  • [1; 2]valuesがセミコロン;で区切られた複数の要素であるパターン

いずれもうまく動くように定義しなければなりません。

空配列初期化子[]の追加

とりあえず一番簡単そうな空配列初期化子[]から追加します。

実装

空括弧[]の文法を追加します。括弧類のルールは新たな種類の括弧を1行で追加できる形にはなっていないので、追加できるように変形します。

改変前
Brackets = "(" _ main:Formula _ ")" {
             return token("round", [main], location());
           }

改変後
Brackets = main:(
             "(" _ main:Formula _ ")" { return ["round", [main], location()]; }
           / "[" _ "]" { return ["empty_square", [], location()]; }
           ) {
             return token(main[0], main[1], main[2]);
           }

なんか冗長に見えますが、他のルールと並べて見たときに規則的になるように書かれています。


空括弧[]get時の挙動を定義します。

customizeEnvironment関数内に追加
    env.registerOperatorHandler("get", "empty_square", (env, token) => {
      const uid = env.getNextUid();
      return toOperation(
        "const v_" + uid + " = [];\n",
        "(v_" + uid + ")"                 // [] !== [] であるため一旦変数に入れて渡す
      );
    });

テスト

空配列が作れるようになりました。

image.png

長さ演算子$#arrayの追加

空でない配列初期化子の実装の話は複雑なので、その前に一旦長さ演算子と配列要素アクセサの実装をしておきます。

実装

前置$#トークンのルールを作り、get時の挙動を定義します。

改変後
Left     = head:((
             "+" { return ["left_plus", location()]; }
           / "-" { return ["left_minus", location()]; }
           / "$#" { return ["left_dollar_hash", 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;
           }
customizeEnvironment関数内に追加
    // 大まかな流れは符号演算子と同じ
    env.registerOperatorHandler("get", "left_dollar_hash", (env, token) => {
      const o1 = env.compile("get", token.argument[0]);
      const uid = env.getNextUid();
      return toOperation(
        // 符号演算子とはここが違う         ↓              ↓
        o1.head + "const v_" + uid + " = " + o1.body + ".length;\n",
        "(v_" + uid + ")"
      );
    });

テスト

fl8
array : [];

$#array
中間コード
let v_0;
const v_1 = [];
v_0 = (v_1);
const v_2 = (v_0).length;
(v_2)

image.png

ちゃんと配列に対して.lengthの動作を行っています。配列に要素があれば、その要素数が出力されることでしょう。

配列要素アクセサarray[index]の追加

実装

後置括弧[ ]トークンのルールを作り、get時の挙動を定義します。

改変後
Right    = head:Factor tail:(_ (
             "(" _ main:Formula _ ")" { return ["right_round", [main], location()] }
             // ↓ここを追加
           / "[" _ main:Formula _ "]" { return ["right_square", [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;
           }
customizeEnvironment関数内に追加
    env.registerOperatorHandler("get", "right_square", (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(
        //                          right_roundとはここが違うだけ   ↓               ↓
        o1.head + o2.head + "const v_" + uid + " = " + o1.body + "[" + o2.body + "];\n",
        "(v_" + uid + ")"
      );
    });

テスト

fl8
array : [];

array[1]
中間コード
let v_0;
const v_1 = [];
v_0 = (v_1);
const v_2 = (v_0)[(1)];
(v_2)

image.png

まだ空配列しか作れないのでundefinedになってしまいましたが、ちゃんと配列に対して[index]を行っています。

細かい動作はJavaScriptのarray[index]に従います。

多要素の配列初期化子[values]の追加

ここでは次のようにコンパイルされる演算子の実装を目指します。

fl8
[1; 2; 3]

中間コード
const v_0 = [];
v_0[v_0.length] = (1);
v_0[v_0.length] = (2);
v_0[v_0.length] = (3);
(v_0)

array文脈ドメインの追加

配列初期化子[values]valuesに現れたセミコロン;トークンは、通常の変数宣言文などの動作ではなく、区切られた各式の値を配列に追加する動作を行わなければなりません。

セミコロン;トークンの挙動を切り替えるために、array文脈ドメインを新しく導入します。

array文脈ドメインでのコンパイルに必要な要素

array文脈ドメインに登場するトークンには、セミコロン;以外にもいくつかのものが考えられます。

トークンの種類 array文脈ドメインでの挙動
セミコロン; ;で区切られた各式の値を要素に持つ配列を生成する。
中置.. 1 .. 10と書くと1から10までの整数を要素に持つ配列を生成する。
後置空括弧[] array[]と書くとarrayの各要素を要素に持つ配列を生成する。

セミコロン;は各式の値を単に順番に追加していくだけでしたが、トークンによってはもう少し複雑な過程によって要素を追加するかもしれません。

例えば、[1 .. 10]は10個の代入文が並ぶのではなく、ループ構文を用いて次のようにコンパイルされるかもしれません。

JavaScript中間コード
const array = [];
for (let i = 1; i <= 10; i++) {
  const v_0 = i;
  array[array.length] = v_0;
}
array

ここで、上記の中間コードの構成要素が、**配列初期化子[ ]配列要素を与えるコード1 .. 10**のどちらによってもたらされたかを分類すると、次のようになります。

番号 出身fl8コード 中間コード
1 [ ] const array = [];
2 1 .. 10 for (let i = 1; i <= 10; i++) {
3 1 .. 10  const v_0 = i;
4 [ ]  array[array.length] =
5 1 .. 10   v_0
6 [ ]  ;
7 1 .. 10 }
8 [ ] array

見ての通り、中間コードの出身トークンが複雑に入り乱れています。

1と8は配列オブジェクトの生成処理の前後を囲うものです。
2と7は配列要素の列挙処理の前後を囲うものです。
3と5は配列要素の生成処理の前文と本文です。
4と6は配列要素の受領処理を行うものです。
また、3~6は複数回現れる可能性があります。


これをまとめると、array挙動オブジェクトには次の性質が必要です。

  1. 「『配列要素の生成処理を**get挙動オブジェクトとして渡すと配列要素の受領処理を付加してrun挙動オブジェクトとして返却する配列要素受領用set挙動オブジェクト**』を受け取ると、それを使用して配列要素の列挙処理を行う**run挙動オブジェクトを返すgenerateメソッド**」を持っている。

generateメソッドを使用する側は、「任意の変数に組み立て用の空配列をセットする」中間コードをまず書き、「その配列への追加処理を配列要素の受領処理とする配列要素受領用set挙動オブジェクト」を作り、generateメソッドに与え、JavaScript文を得ます。そして、ここまでの一連の処理を前文、組み立て用の変数を本文として、配列オブジェクトの生成処理を行う**get挙動オブジェクト**を作ります。

image.png

壮絶に分かりづらいですが、要するに配列の生成処理は[ ]1 .. 10という2個のトークンがメッセージをやり取りしながら4階層の構造を成すことで成立しているということです。

array文脈ドメインの実装

挙動オブジェクトを生成する関数を作ります。

customizeEnvironment関数内の改変後
    // 型を表すコメントを追加

    function toOperation(head/* string */, body/* string */) {
      return {head, body};
    }
    function toOperationSet(accept/* operationGet => operationRun */, suggestedName = undefined/* string */) {
      return {accept, suggestedName};
    }
    // これ↓を追加した
    function toOperationArray(generate/* operationSet => operationRun */) {
      return {generate};
    }
    function toOperationRun(head/* string */) {
      return {head};
    }

これでarray文脈ドメインの定義ができました。

多要素の配列初期化子[values]の実装

上の方で空括弧[]を追加されたBracketsルールに、今度は括弧[ ]を追加します。

改変後
Brackets = main:(
             "(" _ main:Formula _ ")" { return ["round", [main], location()]; }
             // ↓ここが増えた
           / "[" _ main:Formula _ "]" { return ["square", [main], location()]; }
           / "[" _ "]" { return ["empty_square", [], location()]; }
           ) {
             return token(main[0], main[1], main[2]);
           }

空括弧[]get挙動、セミコロン;array挙動を定義します。

追加
    env.registerOperatorHandler("get", "square", (env, token) => {
      const o1 = env.compile("array", token.argument[0]);  // 中身をarray文脈でコンパイル
      const uid = env.getNextUid();                        // 配列を格納する
      // array挙動オブジェクトのgenerateに対して、配列追加セット挙動オブジェクトを渡す
      const o2 = o1.generate(toOperationSet(o => toOperationRun(
        o.head +
        "v_" + uid + "[v_" + uid + ".length] = " + o.body + ";\n"
      )));
      return toOperation(
        "const v_" + uid + " = [];\n" +                    // 空配列の生成
        o2.head,                                           // 配列の組み立て
        "(v_" + uid + ")"
      );
    });
    env.registerOperatorHandler("array", "semicolons", (env, token) => {
      // 各要素をgetした結果を結合してarray挙動として返す
      return toOperationArray(oSet => toOperationRun(
        token.argument.map(token2 => oSet.accept(env.compile("get", token2)).head).join("")
      ));
    });

テスト

これで複数の要素を持つ配列が作れるようになりました。

fl8
[1; 2; (a : 3; a)]

image.png

要素数1の配列問題

さて、ここで問題が生じました。

配列初期化子[values]は、valuesに対してarray文脈ドメインを要求します。要素数が複数であればvalueはセミコロン;トークンだったので、上で定義したセミコロン;array文脈ドメインの挙動が使われます。

しかし、要素数が1の配列は、valuesにセミコロン;以外のトークンが現れます。

fl8
[1 + 2]

image.png

image.png

そのような状況に対応するには、array文脈ドメインでの挙動が定義されていないトークンに対してはget文脈ドメインでのコンパイルを試みるというリダイレクション機能を実装する必要があります。

対策案A

この問題は、Environmentが持つcompileメソッドに「文脈ドメインがarrayだった場合は、コンパイル失敗時にget文脈ドメインでのコンパイルを試み、結果を整形して返す」という仕様を加えると解決できます。

改変前
    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;
    }

    env.registerOperatorHandler("array", "semicolons", (env, token) => {
      return toOperationArray(oSet => toOperationRun(
        token.argument.map(token2 => oSet.accept(env.compile("get", token2)).head).join("")
      ));
    });

対策案A適用後
    // コンパイルを試みるメソッド
    tryCompile(domain, token, options) {
      const handlerTable = this._operatorRegistry[domain];
      if (handlerTable === undefined) return null; // 失敗時にnullを返す
      const handler = handlerTable[token.type];
      if (handler === undefined) return null;

      const suggestedName = this._suggestedName;
      this._suggestedName = options.suggestedName;
      const operation = handler(this, token);
      this._suggestedName = suggestedName;

      return operation;
    }
    compile(domain, token, options = {}) {
      let operation;
      operation = this.tryCompile(domain, token, options);  // コンパイル試行
      if (operation !== null) return operation;             // 成功時はそれを返す
      if (domain === "array") {                             // array限定処理
        // スコープの関係上ここにも設置
        function toOperationArray(generate/* operationSet => operationRun */) {
          return {generate};
        }
        operation = this.tryCompile("get", token, options); // コンパイル試行
        // 成功時は整形して返す
        if (operation !== null) return toOperationArray(oSet => oSet.accept(operation));
      }
      throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, this, token);
    }

    env.registerOperatorHandler("array", "semicolons", (env, token) => {
      // 「;」で区切られた各要素に対してもarrayでのコンパイルを行う
      return toOperationArray(oSet => toOperationRun(
        token.argument.map(token2 => env.compile("array", token2).generate(oSet).head).join("")
      ));
    });

先ほどの図は次のように変わります。

image.png

これで今回の目標の演算子はすべて意図通り動くように実装されました。ひとまずゴールです。

EnvironmentcustomizeEnvironmentの分離問題

対策案Aでは、ソースコードに論理構造上の問題が発生します。対策案A適用後のソースコード全体の大雑把な構成図を示します。

{
  loc関数
  エラークラス

  class Environment {
    諸機能
    tryCompile(domain, token, options) {
      ~~
    }
    compile(domain, token, options = {}) {
      let operation;
      operation = this.tryCompile(domain, token, options);
      if (operation !== null) return operation;
      if (domain === "array") {
        toOperationArray関数 // ■■■■■■■■■■■■■■■■■■■■■■■■
        operation = this.tryCompile("get", token, options);
        if (operation !== null) return toOperationArray(oSet => oSet.accept(operation));
      }
      throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, this, token);
    }
    諸機能
  }

  function customizeEnvironment(env) {
    indent関数
    toOperation系関数 // ■■■■■■■■■■■■■■■■■■■■■■■■
    トークンの挙動定義部
    組み込み定数定義部
  }

  token関数
}
ルール定義部

Environmentのメソッドcompileの中に、getarrayといった具体的な実装上の表現が現れています。

今までEnvironmentは演算子が挙動をもつことや文脈ドメインという概念があることは知っていましたが、具体的にどのようなものが登録されるのかについては無関心で、それらの設計はすべてcustomizeEnvironment関数内で完結していました。

対策案Aはこの関心の切り分けに反しています。


また、JavaScript中間コードの生成に関わる部分もこれまではすべてcustomizeEnvironment内に記述されていましたが、具体的な中間コード生成の処理がcustomizeEnvironmentから漏れ出たために、toOperationArrayが2か所に現れるという奇妙な構造が生じました。

実は中間コードがJavaScriptであるというのは、customizeEnvironment関数内の特有の取り決めです。別のcustomizeEnvironment関数を作ればPerlコードを生成することもできます。しかし中間コード生成機能がcustomizeEnvironmentから漏れ出すと、そこら辺の抽象化が崩れてしまいます。

このように対策案Aは処理内容の切り分けからも逸脱しています。

Environmentにコンパイラを登録できるようにする

コンパイル時にどのような処理を行えばよいかは、文脈ドメインによって異なります。そこで、対策案Aに対してEnvironmentに文脈ドメインごとのコンパイル動作を登録する機能を付け加え、それをcustomizeEnvironment内から登録することで、「具体的な実装」をcustomizeEnvironment内に収容します。

改変後
  class Environment {
    constructor() {
      ~~
      this._compilerRegistry = Object.create(null);        // コンパイラ管理オブジェクト
      ~~
    }
    ~~
    registerCompilerHandler(domain, handler) {             // コンパイラの登録メソッド
      this._compilerRegistry[domain] = handler;
    }
    tryCompile(domain, token, options) {                   // もはやcompileから直接呼ばれない
      const handlerTable = this._operatorRegistry[domain];
      if (handlerTable === undefined) return null;
      const handler = handlerTable[token.type];
      if (handler === undefined) return null;

      const suggestedName = this._suggestedName;
      this._suggestedName = options.suggestedName;
      const operation = handler(this, token);
      this._suggestedName = suggestedName;

      return operation;
    }
    compile(domain, token, options = {}) {
      const handler = this._compilerRegistry[domain];      // コンパイラを検索
      if (handler === undefined) throw new Fluorite8CompileError("Unknown compiler: " + domain, this, token); 
      return handler(this, token, options);                // コンパイル処理は委譲する
    }
    ~~
  }

これで、env.registerCompilerHandlerによって文脈ドメインごとにコンパイラを登録できるようになりました。


customizeEnvironment内でコンパイラを登録します。ここからであればtoOperationArray関数はそのまま見えます。

改変後
  function customizeEnvironment(env) {
    ~~

    // リダイレクションを行わない普通のコンパイラ
    function registerDefaultCompilerHandler(domain) {
      env.registerCompilerHandler(domain, (env, token, options) => {
        let operation;
        operation = env.tryCompile(domain, token, options);
        if (operation !== null) return operation;
        throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, env, token);
      });
    }

    // コンパイラの登録
    registerDefaultCompilerHandler("get");
    registerDefaultCompilerHandler("set");
    registerDefaultCompilerHandler("run");
    // arrayのコンパイルには特殊な動作を行う
    env.registerCompilerHandler("array", (env, token, options) => {
      let operation;
      operation = env.tryCompile("array", token, options);
      if (operation !== null) return operation;
      operation = env.tryCompile("get", token, options);
      if (operation !== null) return toOperationArray(oSet => oSet.accept(operation));
      throw new Fluorite8CompileError("Unknown operator: array/" + token.type, env, token);
    });

    ~~
  }

これで、具体的な実装をcustomizeEnvironment内に保ったままenv.compileの挙動をカスタマイズすることが出来ました。

個々のコンパイラの実装がcustomizeEnvironment内に移動しただけで、大まかな戦略は実装案Aと同じです。


image.png

ちゃんと要素数が1の配列の生成が出来ています。

テスト

ここまでに追加した文法をすべて使ったテストです。

fl8
[
  [];
  [1];
  [1; 2; 3];
  $#[1; 2; 3];
  [1; 2; 3][1]
]
中間コード
const v_0 = [];
const v_1 = [];
v_0[v_0.length] = (v_1);
const v_2 = [];
v_2[v_2.length] = (1);
v_0[v_0.length] = (v_2);
const v_3 = [];
v_3[v_3.length] = (1);
v_3[v_3.length] = (2);
v_3[v_3.length] = (3);
v_0[v_0.length] = (v_3);
const v_4 = [];
v_4[v_4.length] = (1);
v_4[v_4.length] = (2);
v_4[v_4.length] = (3);
const v_5 = (v_4).length;
v_0[v_0.length] = (v_5);
const v_6 = [];
v_6[v_6.length] = (1);
v_6[v_6.length] = (2);
v_6[v_6.length] = (3);
const v_7 = (v_6)[(1)];
v_0[v_0.length] = (v_7);
(v_0)

image.png

結果
   [
      [],
      [
         1
      ],
      [
         1,
         2,
         3
      ],
      3,
      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._compilerRegistry = 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;
    }
    registerCompilerHandler(domain, handler) {
      this._compilerRegistry[domain] = handler;
    }
    tryCompile(domain, token, options) {
      const handlerTable = this._operatorRegistry[domain];
      if (handlerTable === undefined) return null;
      const handler = handlerTable[token.type];
      if (handler === undefined) return null;

      const suggestedName = this._suggestedName;
      this._suggestedName = options.suggestedName;
      const operation = handler(this, token);
      this._suggestedName = suggestedName;

      return operation;
    }
    compile(domain, token, options = {}) {
      const handler = this._compilerRegistry[domain];
      if (handler === undefined) throw new Fluorite8CompileError("Unknown compiler: " + domain, this, token); 
      return handler(this, token, options);
    }

    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/* string */, body/* string */) {
      return {head, body};
    }
    function toOperationSet(accept/* operationGet => operationRun */, suggestedName = undefined/* string */) {
      return {accept, suggestedName};
    }
    function toOperationArray(generate/* operationSet => operationRun */) {
      return {generate};
    }
    function toOperationRun(head/* string */) {
      return {head};
    }
    function registerDefaultCompilerHandler(domain) {
      env.registerCompilerHandler(domain, (env, token, options) => {
        let operation;
        operation = env.tryCompile(domain, token, options);
        if (operation !== null) return operation;
        throw new Fluorite8CompileError("Unknown operator: " + domain + "/" + token.type, env, token);
      });
    }

    registerDefaultCompilerHandler("get");
    registerDefaultCompilerHandler("set");
    registerDefaultCompilerHandler("run");
    env.registerCompilerHandler("array", (env, token, options) => {
      let operation;
      operation = env.tryCompile("array", token, options);
      if (operation !== null) return operation;
      operation = env.tryCompile("get", token, options);
      if (operation !== null) return toOperationArray(oSet => oSet.accept(operation));
      throw new Fluorite8CompileError("Unknown operator: array/" + token.type, env, token);
    });

    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", "square", (env, token) => {
      const o1 = env.compile("array", token.argument[0]);
      const uid = env.getNextUid();
      const o2 = o1.generate(toOperationSet(o => toOperationRun(
        o.head +
        "v_" + uid + "[v_" + uid + ".length] = " + o.body + ";\n"
      )));
      return toOperation(
        "const v_" + uid + " = [];\n" +
        o2.head,
        "(v_" + uid + ")"
      );
    });
    env.registerOperatorHandler("get", "empty_square", (env, token) => {
      const uid = env.getNextUid();
      return toOperation(
        "const v_" + uid + " = [];\n",
        "(v_" + uid + ")"
      );
    });
    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", "left_dollar_hash", (env, token) => {
      const o1 = env.compile("get", token.argument[0]);
      const uid = env.getNextUid();
      return toOperation(
        o1.head + "const v_" + uid + " = " + o1.body + ".length;\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", "right_square", (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("array", "semicolons", (env, token) => {
      return toOperationArray(oSet => toOperationRun(
        token.argument.map(token2 => env.compile("array", token2).generate(oSet).head).join("")
      ));
    });

    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()]; }
           / "$#" { return ["left_dollar_hash", 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()] }
           / "[" _ main:Formula _ "]" { return ["right_square", [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:(
             "(" _ main:Formula _ ")" { return ["round", [main], location()]; }
           / "[" _ main:Formula _ "]" { return ["square", [main], location()]; }
           / "[" _ "]" { return ["empty_square", [], location()]; }
           ) {
             return token(main[0], main[1], main[2]);
           }
_        = [ \t\r\n]*

この時点で次の特徴があります。

  • ソースコードから構文木の生成
  • 構文木から中間コードの生成
  • 中間コードの評価
  • 副作用を持つ演算の適切な順序での実行
  • エラー出力の改善
  • トークンの文脈の管理
  • 識別子の文脈の管理
  • トークンの出現場所の管理
  • ソースファイル名の管理
  • 関数名の推測
  • 文脈ドメインごとのコンパイラの管理 (新規)
  • スペース
  • 識別子
    • 組み込み定数 (ドメイン:get
      • PI
    • 引数 (ドメイン:get
    • 変数 (ドメイン:get set
  • getドメイントークン
    • 整数リテラル123
    • 識別子identifier
    • 丸括弧(formula)
    • 空配列初期化子[] (新規)
    • 配列初期化子[values] (新規)
    • 関数呼び出しfunction(argument)
    • 配列要素アクセサarray[index] (新規)
    • べき乗a ^ b
    • 符号 +a -a
    • 長さ演算子$#array (新規)
    • 加減乗除 a + b a - b a * b a / b
    • 三項演算子cond ? then ? else
    • ラムダ式arg -> formula
    • ;
  • setドメイントークン
    • 識別子 identifier
  • arrayドメイントークン (新規)
    • 配列要素区切り; (新規)
  • runドメイントークン
    • 変数宣言variable : value
    • 代入acceptor = value

文脈リダイレクションは、あらゆる文脈におけるパースを統一された1個のシンタックスとして表現するfluorite-8を象徴する機構です。

次回→

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?