1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-23

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?