前記事では、コードの生成に必要なライブラリlang/runtime.ts
について見ただけで終わってしまいましたので、今回はいよいよコードを生成しているlang/CodeGen.ts
を見て行きます。
lang/CodeGen.ts
import { ValueExpression, NumberLiteral, Identifier, MemberAccess, Call} from "./Expressions";
export function generate(expr:ValueExpression):string {
const g=generate;
if (expr instanceof NumberLiteral) {
return `Num(${expr.value})`;
} else if (expr instanceof Identifier) {
return expr.text;
} else if (expr instanceof MemberAccess) {
return `${g(expr.left)}.${expr.name.text}`;
} else {// if (expr instanceof Call) {
const a=expr.args.map(g);
return `${g(expr.left)}(${a.join(",")})`;
}
}
今のところ、generate
という関数1つだけです。
generate(expr:ValueExpression):string
と宣言されていることから、ValueExpression
オブジェクトから文字列の変換であることがわかりますが、ValueExpression
って何だったかをちゃんと見てなかったですので、定義している`lang/Expresions.tsを見ておきましょう。
lang/Expressions.ts
export class NumberLiteral {
constructor(public value:number){}
}
export class Identifier {
constructor(public text:string){}
toString() {return this.text;}
}
export class MemberAccess {
constructor(public left:ValueExpression, public name:Identifier){}
}
export class Call {
constructor(public left:ValueExpression, public args:ValueExpression[]){}
}
export type ValueExpression=NumberLiteral|Identifier|MemberAccess|Call;
~~~
これらは、「式」として解釈可能なオブジェクトの一覧です。現状定義されているのはこの4つです
- `NumberLiteral`(数値定数)
- `Identifier`(変数)
- `MemberAccess` (`object.x` のようなプロパティへのアクセス)
- `Call` (`f(x)`のような関数呼び出し)
さきほどの`generate`メソッドは、上4つのそれぞれの場合について、対応するJavaScriptのコードを生成します。
##数値定数→コード
~~~ts
if (expr instanceof NumberLiteral) {
return `Num(${expr.value})`;
~~~
数値定数の場合は、[前記事](https://qiita.com/hoge1e3/items/5f1ec29732b130d68ee7)の`lang/runtime.ts`で定義した`Num`関数を使って、例えば`3`に対して`Num(3)`のようにコードを生成します。
## 変数→コード
~~~ts
} else if (expr instanceof Identifier) {
return expr.text;
~~~
変数の場合は、テキストの中身をそのまま生成します……って、これは実はまだ使用されていません。`3.add(2)`とかの`.add`の部分はこれではなく、次の「MemberAccess」で生成しています。
## MemberAccess→コード
~~~ts
} else if (expr instanceof MemberAccess) {
return `${g(expr.left)}.${expr.name.text}`;
~~~
MemberAccessには、`left`と呼んでいる式が含まれます。これは`object.x`の`object`の部分(すなわち、`.`の「左側」)に相当します。ここには別の式が来るので、`generate`を再帰呼び出しします(`g`と省略できるように`generate`の冒頭で宣言しています)
「左側」を生成したら、それに`.`と名前(`object.x`の`x`のほう)をくっつけて生成完了です。
## Call→コード
~~~ts
} else {// if (expr instanceof Call) {
const a=expr.args.map(g);
return `${g(expr.left)}(${a.join(",")})`;
}
~~~
まず、余談から。なんで`// if (expr instanceof Call) {`がコメントになっているかというと、この部分があるとTypeScriptエラーを吐くからです。このコメントを外すと「`expr`が`Call`でなかった場合に`return`がないからダメよ」とおっしゃるのですが、`lang/Expression.ts`の`ValueExpression`の定義を見ればわかる通り、`NumberLiteral|Identifier|MemberAccess|Call`の4つしかないので、それ以外はありえないのに……と思うんです。しかも、このelseの内部、ちゃんと、`expr`が`Call`であることを推論してくれる(前3つのifでそれ以外の型の可能性を排除しているから)のに、なんで`if (expr instanceof Call) {`があるときは気をきかせてくれないのか……[2021/02/27追記] [こんな方法](https://twitter.com/suin/status/1365184105172717574)があるそうです。次回修正版を紹介します。
閑話休題.
この内部は`Call`だった時の処理です。`Call`は`MemberAccess`同様に`left`(`f(x)`の`f`の部分)があって、その後ろに`args`が来ます。`args`は引数ですので、`ValueExpression`の配列になっています。これらの引数すべてに`g`(`generate`)を適用させて、引数部分のコードを生成します(`a`はJavaScriptコードになった文字列の配列)。leftの部分を生成した後、`(`と`a`の中身のカンマ区切り、`)`を生成して生成完了です。
# 例
例えば式`3.add(2)`をJavaScriptに変換する場合、
- 式全体は`Call`で、`left`は`3.add`、`args`は`[2]`
- `left`部分を生成
- `3.add`は`MemberAccess`で、`left`は`3`、`name`は`add`
- `left`部分を生成
- `3` は `NumberLiteral`なので、`Num(3)`を生成
- `Num(3)`に`.`と`add`をくっつけて、`Num(3).add`を生成
- `args`部分を生成
- `args`は要素1個の配列で、0個目は`2`、これは`NumberLiteral`なので、`Num(2)`を生成
- `left`で生成したものと、`(`と、`args`で生成したものと、`)` をくっつける
- `Num(3).add(Num(2))` を生成(完成)
# 実行器
さて、コードは生成されたのでいよいよ実行する部分を見てみます。第1回で見た`index.ts`をもう一度見てみましょう。
~~~index.ts
//前略
function run(src:string) {
const t=MyTokenizer(src);
const tokens=t.tokenize();
//★A
tokens.forEach((token,i)=>console.log(`${i}:[${token.type}] ${token.text}`));
const p=MyParser(tokens);
const tree=p.parse();
//★B
console.log(tree);
const js=`
const {Num}=runtime;
return ${generate(tree)};
`;
//★C
console.log(js);
const func=new Function("runtime", js);
const res=func(runtime);
//★D
console.log(res);
}
~~~
★Aから★Dは、それぞれ次の結果を表示しています。
- //★A 字句解析の結果
- //★B 構文解析の結果
- //★C コード生成の結果
- //★D 実行の結果
//★Cの手前のコード
~~~ts
const js=`
const {Num}=runtime;
return ${generate(tree)};
`;
~~~
で、`generate`が使われています。さきほどの`3.add(2)`という式であれば、
~~~js
const {Num}=runtime
return Num(3).add(Num(2));
~~~
というコードが生成されて、変数`js`に入るはずです。
`runtime`は([前回](https://qiita.com/hoge1e3/items/5f1ec29732b130d68ee7)見た`lang/runtime.ts`)index.tsの冒頭でインポートされています。この中に`Num`の定義があります。
~~~ts
import * as runtime from "./lang/runtime";
~~~
そして、変数`js`の内容から`Function`オブジェクトを生成しています。`Function`オブジェクトは、`new Function(引数名, コード文字列)`という形式で関数を動的に生成できます。
~~~ts
const func=new Function("runtime", js);
const res=func(runtime);
//★D
console.log(res);
~~~
生成された`func`の内容はこんな感じになります
~~~js
const func=function (runtime) {
const {Num}=runtime
return Num(3).add(Num(2));
}
これをfunc(runtime)
で呼び出して、実行を行っているわけです。
次回は、意味解析について見て行きます。
理解度チェック
前回の「理解度チェック」で出て来た次の式をtinyfuncで記述可能な形式に書き換えて、実際に計算させてみましょう。
- 20+30-5 → 実際には
20.add(3).sub(5)
と書かないと動かないです - 50-10-20
- 50-(10-20)
- 204+302
- 20*(4+30)*2
- 100/10/2
- 100/(10/2)