んだ日記

ndaDayoの技術日記です

Writing A Compiler In Go を読んでいく Chapter 3 Infix Expressions

こんにちわ、んだです。

今回も『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が理解できるコードに置き換えた上で処理を実行していますね。

どのくらいの違いが出ているのか知りたいところですが、今はコンパイラ本の完走を目標に時間を使っていきたいので、完走したら違いを比べてみようと思います。

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