経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

『Go言語で作るインタプリタ』をTypeScriptで実装する(後編)

前編を書いたのが約1年前なので1年間かかってしまった。

wafuwafu13.hatenadiary.com

github.com


ずっとやってたわけではなく、前回は2章までできたらいいかって感じで終わらせて、1年越しに再開してみた。
Commits · wafuwafu13/Interpreter-made-in-TypeScript · GitHub


前回のブログの、「字句解析器(レキサー)」で述べている通り、基本的にはGoのStructをJavaScriptのclassに置き換えるとうまくいくけれど、詰まったところとかもあるので整理しておく。

まずは、新たに『Monkeyソースコードを評価する際に出てくる値全てをObjectで表現する』(p.121)ために作ったObjectインターフェースの差異からみていく。
これは前回と同様にクラスで構築していけばいい。

const(
    STRING_OBJ = "STRING"
)

...
type String struct {
	Value string
}

func (s *String) Type() ObjectType { return STRING_OBJ }
func (s *String) Inspect() string  { return s.Value }
export const STRING_OBJ = 'STRING';

...

interface StringProps {
  value: string;
}

export class String<T extends StringProps> {
  value: T['value'];

  constructor(value: T['value']) {
    this.value = value;
  }

  inspect(): string {
    return this.value;
  }

  type(): ObjectType {
    return STRING_OBJ;
  }
}

呼び出しの際、例えばevaluatorのevalStringInfixExpression関数では、Goではこうなっているが、

return &object.String{Value: leftVal + rightVal}

今回はコンストラクタを返せばいい。

const evalStringInfixExpression = (
  operator: string,
  left: any,
  right: any,
): any => {
  if (operator != '+') {
    return new Error(
      `unknown operator: ${left.type()} ${operator} ${right.type()}`,
    );
  }
  const leftVal = left.value;
  const rightVal = right.value;

  return new String(leftVal + rightVal);
};

evalatorのEval関数では、Goの場合はType switchesを使ってast.Nodeが保持している型によって処理を分けているが、

func Eval(node ast.Node, env *object.Environment) object.Object {
	switch node := node.(type) {

	case *ast.Program:
		return evalProgram(node, env)

	case *ast.ExpressionStatement:
		return Eval(node.Expression, env)
        ....

ast.tsではそもそもNodeインターフェースを定義していない。
今回の場合はnode.constructor.nameでクラス名を見てやることで解決する。

export const Eval = (node: any, env: Environment): any => {
  switch (node.constructor.name) {
    case 'Program':
      return evalProgram(node, env);
    case 'ExpressionStatement':
      return Eval(node.expression, env);
    ...

次に、evaluatorのevalInfixExpressionでoperatorが"=="の際、Booleanオブジェクトを返す処理があった。

case operator == "==":
    return nativeBoolToBooleanObject(left == right)

TypeScriptの場合は、デバッグ結果が一見同じで、true判定して欲しいにも関わらず、false判定がなされてうまくいかなかった。

if (operator == '==') {
     console.log(left, right); // Boolean { value: false } Boolean { value: false }
     return new Boolean(left == right);
}

Goの場合は、本にも書いてある通り、『ここで真偽値の等価性を確認するためにポインタの比較を使っている。これでうまくいくのは、私たちはオブジェクトを指し示すのに常にポインタを利用していて、かつ真偽値に関してはTRUEとFALSEの2つを使っているからだ...』(p.140)というようにポインタを使っているのでうまく判定される。
一方JavaScriptの場合は、異なるコンストラクタの同値判定はfalseになる。

class BooleanClass {
    constructor(value) {
        this.value = value
    }
}

const a = new BooleanClass(false);
const b = new BooleanClass(false);

console.log(a,b); // BooleanClass { value: false } BooleanClass { value: false }
console.log(a == b); // false


なので、判定の際はvalueまでみにいって判定した。

if (operator == '==') {
    return new Boolean(left.value == right.value);
}

p.171から、『このインタプリタはGoのガベージコレクタ(GC)を使っている』という記述があったが、今回も何も意識せずにJavaScriptGCを使っていることになる。
メモリ管理 - JavaScript | MDN

4.4の配列の実装をしている途中で、何が原因かわからないバグにも遭遇した。

結果的にはparserのparseIndexExpression関数の第一引数にt: Tokenを足したらなおった。
なおった理由もよくわかっていない。


最後に、p.207で配列の独自のビルトインとしてrestを構築する際、新しく割り当てられた配列を返さないといけなかった。

newElements := make([]object.Object, length-1, length-1)
copy(newElements, arr.Elements[1:length])
return &object.Array{Elements: newElements}

Goの場合はcopyで配列のdeep copyはできるけれど、JavaScriptの場合は調べてみると結構やっかいだった。
What is the most efficient way to deep clone an object in JavaScript? - Stack Overflow

結果的にJSON.parse/JSON.stringifyを使えばうまくいった。

const newElements = JSON.parse(JSON.stringify(args[0].elements)).slice(
    1,
    length,
);

return new Array(newElements);

本にはテストコードがなかったけど、ちゃんとかいた。

it('testRest2', () => {
    const input = `let a = [1, 2, 3, 4]; rest(rest(rest(a)));`;
    const evaluated = testEval(input);

    expect(evaluated.elements.length).toBe(1);
    expect(evaluated.elements[0].value).toBe(4);
});


という感じでGoとTypeScriptの差異に躓きながらも最後まで実装を進めていった。
本の最後のハッシュの章がまだバグがあって動かないから、また1年後にでもやるかもしれない。

1年前のコードを読みはじめたとき、よくこんな難解なコードを書いていたなと思ったから、コードを追う能力とかは1年前とさほど変わっていないと実感した。
今回やったのは、前回構築したParserを元に評価する、ということだから前回ほど大きな学びはなかったけど、いい復習になったし、言語の差異の壁を自力で乗り越えて本を1冊やり切ったことは自信にもなった。
ASTがいろんなところに使われていることも認識できるようになってきた。
graphql-validation-complexity から学ぶGraphQLのAST走査 - 経験は何よりも饒舌
が、まだGoやTypeScriptの仕様そのものがどう構築されているのかとか、GCとかまだよくわからないことの方が多いので後々詰めていけばいいと思っている。