こんにちわ、んだです。
今回も前回記事に引き続き、『Writing A Compiler In Go』 です。
今回は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です。 興奮してきますね。
僕から以上。あったかくして寝ろよ。