こんにちわ、んだです。
今回も『Writing A Compiler In Go』の写経記録を書いていきます。 前回まで Chapter 2 が終わりましたので、今回はChapter 3から進めていきます。 nda-desu.hatenablog.com
Infix Expressions
前回Chapter 2 ではAddまでやりましたが、Chapter 3では Sub,Mul, Divを対応していきます。
Sub,Mul, Divを定義する
まずは、対応するOpcodeを定義します。
// code/code.go const ( OpSub OpMul OpDiv ) var definitions = map[Opcode]*Definition{ OpSub: {"OpSub", []int{}}, OpMul: {"OpMul", []int{}}, OpDiv: {"OpDiv", []int{}}, }
TestCase
続いて、テストケースについても確認しましょう。 Subのケースしか記載しませんが、他もほぼ同じです
// compiler/compiler_test.go { input: "1 - 2", expectedConstants: []interface{}{1, 2}, expectedInstructions: []code.Instructions{ code.Make(code.OpConstant, 0), code.Make(code.OpConstant, 1), code.Make(code.OpSub), code.Make(code.OpPop), }, },
Compiler
Compileは、至ってシンプルです。 Operatorによって、Opcodeをemitに渡しているだけですね。
// compiler/compiler.go func (c *Compiler) Compile(node ast.Node) error { case *ast.InfixExpression: switch node.Operator { case "+": c.emit(code.OpAdd) case "-": c.emit(code.OpSub) case "*": c.emit(code.OpMul) case "/": c.emit(code.OpDiv)
VM
では、いよいよVMです。 VMでは、どのようにSub,Mul, Divを処理するのでしょうか?
TestCase
まずは、テストケースから確認しましょう。 Sub,Mul, Divのテストケースが揃っていますね
// vm/vm_test.go func TestIntegerArithmetic(t *testing.T) { tests := []vmTestCase{ {"1 - 2", -1}, {"1 * 2", 2}, {"4 / 2", 2}, {"50 / 2 * 2 + 10 - 5", 55}, {"5 * (2 + 10)", 60}, {"5 + 5 + 5 + 5 - 10", 10}, {"2 * 2 * 2 * 2 * 2", 32}, {"5 * 2 + 10", 20}, {"5 + 2 * 10", 25}, {"5 * (2 + 10)", 60}, } runVmTests(t, tests) }
Run
まずは、Runから確認しましょう。 Sub,Mul, DivのOpcodeを命令として受け取った時には、executeBinaryOperationを実行しています。
// vm/vm.go func (vm *VM) Run() error { for ip := 0; ip < len(vm.instructions); ip++ { op := code.Opcode(vm.instructions[ip]) switch op { case code.OpAdd, code.OpSub, code.OpMul, code.OpDiv: err := vm.executeBinaryOperation(op) if err != nil { return err }
executeBinaryOperation
では、executeBinaryOperation自体は何をしているのでしょうか?
func (vm *VM) executeBinaryOperation(op code.Opcode) error { right := vm.pop() left := vm.pop() leftType := left.Type() rightType := right.Type() if leftType == object.INTEGER_OBJ && rightType == object.INTEGER_OBJ { return vm.executeBinaryIntegerOperation(op, left, right) } return fmt.Errorf("unsupported types for binary operation: %s %s", leftType, rightType) }
中置式の右と左をPopしてきて、executeBinaryIntegerOperationに渡しています。
executeBinaryIntegerOperation
さて、最後はexecuteBinaryIntegerOperationです。 最終的には、Sub,Mul, Divに応じて計算をGoで実行してスタックにプッシュして完了です。
func (vm *VM) executeBinaryIntegerOperation( op code.Opcode, left, right object.Object, ) error { leftValue := left.(*object.Integer).Value rightValue := right.(*object.Integer).Value var result int64 switch op { case code.OpAdd: result = leftValue + rightValue case code.OpSub: result = leftValue - rightValue case code.OpMul: result = leftValue * rightValue case code.OpDiv: result = leftValue / rightValue default: return fmt.Errorf("unknown integer operator: %d", op) } return vm.push(&object.Integer{Value: result}) }
以上が、Sub,Mul, Divの処理でございました。
まとめ
Infix Expressionsを改めて追ってみました。
インタプリタでは評価はASTをたどって都度評価しましたが、コンパイラ編では一度VMが理解できるコードに置き換えた上で処理を実行していますね。
どのくらいの違いが出ているのか知りたいところですが、今はコンパイラ本の完走を目標に時間を使っていきたいので、完走したら違いを比べてみようと思います。
僕から以上。あったかくして寝ろよ。