前記事では、コードの生成に必要なライブラリ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)