んだ日記

ndaDayoの技術日記です

Writing A Compiler In Go を読んでいく Chapter 2 Compiler

こんにちわ、んだです。

今回も前回記事に引き続き、『Writing A Compiler In Go』 です。

nda-desu.hatenablog.com

今回はCompilerについて、触れていきます。

compiler_test

まずはテストコードから眺めていきましょう。

 // compiler/compiler_test.go
package compiler

func TestIntegerArithmetic(t *testing.T) {
    tests := []compilerTestCase{
        {
            input:             "1 + 2",
            expectedConstants: []interface{}{1, 2},
            expectedInstructions: []code.Instructions{
                code.Make(code.OpConstant, 0),
                code.Make(code.OpConstant, 1),
                code.Make(code.OpAdd),
            },
        },
    }

    runCompilerTests(t, tests)
}

func runCompilerTests(t *testing.T, tests []compilerTestCase) {
    t.Helper()

    for _, tt := range tests {
        program := parse(tt.input)

        compiler := New()
        err := compiler.Compile(program)
        if err != nil {
            t.Fatalf("compiler error: %s", err)
        }

        bytecode := compiler.Bytecode()

        err = testInstructions(tt.expectedInstructions, bytecode.Instructions)
        if err != nil {
            t.Fatalf("testInstructions failed: %s", err)
        }

        err = testConstants(t, tt.expectedConstants, bytecode.Constants)
        if err != nil {
            t.Fatalf("testConstants failed: %s", err)
        }
    }
}

compilerTestCase

テストケースだけ、詳細をみてみます

tests := []compilerTestCase{
        {
            input:             "1 + 2",
            expectedConstants: []interface{}{1, 2},
            expectedInstructions: []code.Instructions{
                code.Make(code.OpConstant, 0),
                code.Make(code.OpConstant, 1),
                code.Make(code.OpAdd),
            },
        },
    }

1 + 2 が渡され、結果としてbytecodeに変換されて命令として生成されることが期待値として表現されています。

Compilerに渡されるのは、1 + 2ではなく、ASTです。
ASTへの変換は、parse関数で行ってます

 // compiler/compiler_test.go
func parse(input string) *ast.Program {
    l := lexer.New(input)
    p := parser.New(l)
    return p.ParseProgram()
}

compiler

では、compiler本体をみていきましょう

func (c *Compiler) Compile(node ast.Node) error {
    switch node := node.(type) {
    case *ast.Program:
        for _, s := range node.Statements {
            err := c.Compile(s)
            if err != nil {
                return err
            }
        }
    case *ast.ExpressionStatement:
        err := c.Compile(node.Expression)
        if err != nil {
            return err
        }
    case *ast.InfixExpression:
        err := c.Compile(node.Left)
        if err != nil {
            return err
        }

        err = c.Compile(node.Right)
        if err != nil {
            return err
        }

        switch node.Operator {
        case "+":
            c.emit(code.OpAdd)
        default:
            return fmt.Errorf("unknown operator %s", node.Operator)
        }

    case *ast.IntegerLiteral:
        integer := &object.Integer{Value: node.Value}
        c.emit(code.OpConstant, c.addConstant(integer))
    }

    return nil
}

ASTを走査していって再帰的に処理していっています。
前書でパーサを学びましたが、雰囲気はよく似てますね。再帰すばらしい。

さて、再帰的に処理していっていますが、中身は結局のところ何をしているんでしょうか?

emit

結局のところ、emitです。 ASTを走査していき、最終的にはemitが呼び出されます。

では、emitは何をしているんでしょか?

func (c *Compiler) emit(op code.Opcode, operands ...int) int {
    ins := code.Make(op, operands...)
    pos := c.addInstruction(ins)
    return pos
}

func (c *Compiler) addInstruction(ins []byte) int {
    posNewInstruction := len(c.instructions)
    c.instructions = append(c.instructions, ins...)
    return posNewInstruction
}

やっていることは、非常にシンプルです。

まずは、bytecodeに変換する

ins := code.Make(op, operands...)

まずは、内部的にMake関数にopcodeとoperandsを渡して、bytecodeに変換します。

opcodeについては、ASTのトークンタイプによって分岐させているのがわかりますね。

case "+":
    c.emit(code.OpAdd)

case *ast.IntegerLiteral:
    integer := &object.Integer{Value: node.Value}
    c.emit(code.OpConstant, c.addConstant(integer))

続いて、命令のバイトスライスに加える

Make関数によって生成されたbytecodeは、addInstructionに渡されバイトスライスに加えられます。

ようやく、1 + 2 という式がbytecodeとして変換されました。

c.instructions = append(c.instructions, ins...)

まとめ

前回はMake関数について書いていきました。 Make関数は、opcodeとoperandsを受け取り、bytecodeに変換していました。

今回はcompilerでした。 compilerでは、ASTを再帰的に処理して最終的にはMakeを呼び出しbytecodeに変換、さらに変換したbytecodeをinstructionsに加えるところまでやりました。

さて、次はいよいよVMです。 興奮してきますね。

僕から以上。あったかくして寝ろよ。

github.com