fluorite-8　実装の軌跡　Part 8　配列と文脈リダイレクション


[初回](https://qiita.com/MirrgieRiana/items/56244fee4cdde1b1c7b0)

[←前回](https://qiita.com/MirrgieRiana/items/1da9faf84e2a77cec299)

# 今回の目標

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

|トークンの種類|`get`文脈ドメインでの挙動|
|:-:|:-:|
|空括弧`[]`|空配列初期化子`[]`|
|括弧`[` `]`|配列初期化子`[values]`|
|前置`$#`|長さ演算子`$#array`|
|後置括弧`[` `]`|配列要素アクセサ`array[index]`|

```perl:疑似fl8ソースコード
empty_array :   []          ;
array       :   [1; 2; 3]   ;
length      : $#[1; 2; 3]   ;
item        :   [1; 2; 3][1];
```

↓

```javascript:疑似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行で追加できる形にはなっていないので、追加できるように変形します。

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

↓

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

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

----

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

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

## テスト

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/4522a00b-fa31-a377-243e-af83ea535ccf.png)

# 長さ演算子`$#array`の追加

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

## 実装

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

```java:改変後
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;
           }
```

```javascript: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 + ")"
      );
    });
```

## テスト

```perl:fl8
array : [];

$#array
```

```javascript:中間コード
let v_0;
const v_1 = [];
v_0 = (v_1);
const v_2 = (v_0).length;
(v_2)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/0d3fdcd1-dbee-b92d-57f2-b9aa22c0cc2c.png)

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

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

## 実装

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

```java:改変後
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;
           }
```

```javascript: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 + ")"
      );
    });
```

## テスト

```perl:fl8
array : [];

array[1]
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/a711f869-e9c6-9583-e1d9-f116a5af038b.png)

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

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

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

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

```perl:fl8
[1; 2; 3]
```

↓

```javascript:中間コード
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: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/2a550911-1131-5da3-1f07-30288c887880.png)

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

### `array`文脈ドメインの実装

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

```javascript: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`ルールに、今度は括弧`[` `]`を追加します。

```java:改変後
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`挙動を定義します。

```javascript:追加
    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("")
      ));
    });
```

## テスト

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/f015c4ce-9ab9-0d2f-5e4e-1848eb7658d3.png)

# 要素数1の配列問題

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

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

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

```perl:fl8
[1 + 2]
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/f7a3d920-9349-91cc-f8d3-6eadbbb01c26.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/f1245cd6-2069-97f2-8893-6f73718b5295.png)

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

## 対策案A

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

```javascript:改変前
    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("")
      ));
    });
```

↓

```javascript:対策案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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/7438a842-ce29-db43-2e83-9cfc10a5530a.png)

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

## `Environment`と`customizeEnvironment`の分離問題

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

```java
{
  ～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`の中に、`get`や`array`といった具体的な実装上の表現が現れています。

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

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

----

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

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

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

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

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

```javascript:改変後
  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`関数はそのまま見えます。

```javascript:改変後
  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/481ab66f-e31d-5b79-4922-184f1d88aee1.png)

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

# テスト

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

```perl:fl8
[
  [];
  [1];
  [1; 2; 3];
  $#[1; 2; 3];
  [1; 2; 3][1]
]
```

```javascript:中間コード
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280627/f306649c-5124-4e2a-b9de-1c5b76f4c362.png)

```javascript:結果
   [
      [],
      [
         1
      ],
      [
         1,
         2,
         3
      ],
      3,
      2
   ]
```

配列の実装が完了しました。

# まとめ

ここまでに出来上がったPEG.jsコードです。

<details><summary>**[開閉]**</summary><div>

```java:
{

  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]*
```

</div></details>

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

- ソースコードから構文木の生成
- 構文木から中間コードの生成
- 中間コードの評価
- 副作用を持つ演算の適切な順序での実行
- エラー出力の改善
- トークンの文脈の管理
- 識別子の文脈の管理
- トークンの出現場所の管理
- ソースファイル名の管理
- 関数名の推測
- 文脈ドメインごとのコンパイラの管理 **(新規)**
- スペース
- 識別子
    - 組み込み定数　（ドメイン：`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を象徴する機構です。

[次回→](https://qiita.com/MirrgieRiana/items/630fefc25aef83a0a401)