前記事では、コードの生成に必要なライブラリlang/runtime.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を見ておきましょう。
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のコードを生成します。
数値定数→コード
if (expr instanceof NumberLiteral) {
return `Num(${expr.value})`;
数値定数の場合は、前記事のlang/runtime.tsで定義したNum関数を使って、例えば3に対してNum(3)のようにコードを生成します。
変数→コード
} else if (expr instanceof Identifier) {
return expr.text;
変数の場合は、テキストの中身をそのまま生成します……って、これは実はまだ使用されていません。3.add(2)とかの.addの部分はこれではなく、次の「MemberAccess」で生成しています。
MemberAccess→コード
} else if (expr instanceof MemberAccess) {
return `${g(expr.left)}.${expr.name.text}`;
MemberAccessには、leftと呼んでいる式が含まれます。これはobject.xのobjectの部分(すなわち、.の「左側」)に相当します。ここには別の式が来るので、generateを再帰呼び出しします(gと省略できるようにgenerateの冒頭で宣言しています)
「左側」を生成したら、それに.と名前(object.xのxのほう)をくっつけて生成完了です。
Call→コード
} 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追記] こんな方法があるそうです。次回修正版を紹介します。
閑話休題.
この内部は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をもう一度見てみましょう。
//前略
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の手前のコード
const js=`
const {Num}=runtime;
return ${generate(tree)};
`;
で、generateが使われています。さきほどの3.add(2)という式であれば、
const {Num}=runtime
return Num(3).add(Num(2));
というコードが生成されて、変数jsに入るはずです。
runtimeは(前回見たlang/runtime.ts)index.tsの冒頭でインポートされています。この中にNumの定義があります。
import * as runtime from "./lang/runtime";
そして、変数jsの内容からFunctionオブジェクトを生成しています。Functionオブジェクトは、new Function(引数名, コード文字列)という形式で関数を動的に生成できます。
const func=new Function("runtime", js);
const res=func(runtime);
//★D
console.log(res);
生成されたfuncの内容はこんな感じになります
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)