Help us understand the problem. What is going on with this article?

JavaScriptで関数型言語を作ろう(4)

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

数値定数→コード

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.xobjectの部分(すなわち、.の「左側」)に相当します。ここには別の式が来るので、generateを再帰呼び出しします(gと省略できるようにgenerateの冒頭で宣言しています)

「左側」を生成したら、それに.と名前(object.xxのほう)をくっつけて生成完了です。

Call→コード

} else {// if (expr instanceof Call) {
        const a=expr.args.map(g);
        return `${g(expr.left)}(${a.join(",")})`;
}

まず、余談から。なんで// if (expr instanceof Call) {がコメントになっているかというと、この部分があるとTypeScriptエラーを吐くからです。このコメントを外すと「exprCallでなかった場合にreturnがないからダメよ」とおっしゃるのですが、lang/Expression.tsValueExpressionの定義を見ればわかる通り、NumberLiteral|Identifier|MemberAccess|Callの4つしかないので、それ以外はありえないのに……と思うんです。しかも、このelseの内部、ちゃんと、exprCallであることを推論してくれる(前3つのifでそれ以外の型の可能性を排除しているから)のに、なんでif (expr instanceof Call) {があるときは気をきかせてくれないのか……

さておき、この内部はCallだった時の処理です。CallMemberAccess同様にleft(f(x)fの部分)があって、その後ろにargsが来ます。argsは引数ですので、ValueExpressionの配列になっています。これらの引数すべてにg(generate)を適用させて、引数部分のコードを生成します(aはJavaScriptコードになった文字列の配列)。leftの部分を生成した後、(aの中身のカンマ区切り、)を生成して生成完了です。

例えば式3.add(2)をJavaScriptに変換する場合、

  • 式全体はCallで、left3.addargs[2]
  • left部分を生成
    • 3.addMemberAccessで、left3nameadd
      • left部分を生成
        • 3NumberLiteralなので、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の手前のコード

    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)
  • 20*4+30*2
  • 20*(4+30)*2
  • 100/10/2
  • 100/(10/2)
hoge1e3
HTML5で動作するゲームエンジン+言語「Tonyu System2」を開発しています.
https://www.tonyu.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away