Monekey言語:FLOATの実装

書籍「Go言語でつくるインタプリタ」で実装しているMonkey言語にfloat型を実装してみる.自分用のメモなので雑に記述しているのは許して欲しい.


Lexer

まずtokenを作れるようにするために,token/token.goのconstに

FLOAT = “FLOAT”

を加える.

次にテストケースを作る.lexer/lexer_test.goのTestNextTokenのinputに

let d = 1.2

を追加し,expectedに

{token.LET, “let”},
{token.IDENT, “d”},
{token.ASSIGN, “=“},
{token.FLOAT, “1.2”},
{token.SEMICOLON, “;”},

を追加する.

次にいよいよlexerをいじる.Lexerでは,小数点も一回だけ拾えるようにNextToken関数を次のように修正する

else if isDigit(l.ch){
            tok.Type = token.INT
            tok.Literal = l.readNumber()
            if l.ch == '.'{
                ch := tok.Literal   //part of int
                literal := string(ch) + string(l.ch)    //add point
                l.readChar()
                literal = literal + l.readNumber()  //add fractional part
                tok = token.Token{Type: token.FLOAT, Literal: literal}
            }
            return tok
}


修正前は,連続している整数を拾っていき,整数以外が来た時に終了するものであったが,修正後は一回途切れてもそれがピリオドだった場合はそれを拾い,もう一回整数以外がくるまで拾い続ける.


Parser

Ast.goでは次のように構造体を作成する.Integerと同様である.

type FloatLiteral struct{
    Token token.Token
    Value float64
}

func (fl *FloatLiteral) expressionNode(){}
func (fl *FloatLiteral) TokenLiteral() string{return fl.Token.Literal}
func (fl *FloatLiteral) String() string{return fl.Token.Literal}


parser_test.goには次のように記述する.

func TestFloatLiteralExpression(t *testing.T){
    input := "1.2;"
    l := lexer.New(input)
    p := New(l)
    program := p.ParseProgram()
    checkParserErrors(t, p)

    if len(program.Statements) != 1{
        t.Fatalf("program has not enough statements. got=%d", len(program.Statements))
    }
    stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
    if !ok {
        t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0])
    }
    literal, ok := stmt.Expression.(*ast.FloatLiteral)
    if !ok{
        t.Fatalf("exp not *ast.IntegerLiteral. got=%T", stmt.Expression)
    }
    if literal.TokenLiteral() != "1.2"{
        t.Errorf("literal.TokenLiteral not %s. got=%s", "1.2", literal.TokenLiteral())
    }
}



Parserには,まず,registerPrefixでの射影の登録と,射影先の関数を作る.射影先の関数は「parseFloatLiteral」とする.

p.registerPrefix(token.FLOAT, p.parseFloatrLiteral)



func (p *Parser) parseFloatLiteral() ast.Expression{
    lit := &ast.FloatLiteral{Token: p.curToken}
    value, err := strconv.ParseFloat(p.curToken.Literal, 64)
    if err != nil{
        msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal)
        p.errors = append(p.errors,msg)
        return nil
    }
    lit.Value = value
    return lit
}


これでテストは通るようになる.


Evaluator

object.goではまずconstに次にようにオブジェクトを追加する,

FLOAT_OBJ = "FLOAT"


structは次のようにする(HashKeyのuint64()で渡すのは違うかもしれない).
一応動いてはいたのですが,違っていたらコメントか何かで教えてくれると助かります.

type Float struct{
    Value float64
}
func (f *Float) Inspect() string{return fmt.Sprintf("%f", f.Value)}
func (f *Float) Type() ObjectType{return FLOAT_OBJ}
func (f *Float) HashKey() HashKey {
    return HashKey{Type: f.Type(), Value: uint64(f.Value)}
}


これらをテストするためにevaluator_test.goには次のテスト関数を追加する.

func testFloatObject(t *testing.T, obj object.Object, expected float64)bool{
    result, ok := obj.(*object.Float)
    if !ok{
        t.Errorf("object is not Float. got=%T (%+v)", obj, obj)
        return false
    }
    if result.Value != expected{
        t.Errorf("object has wrong value. got=%f, want=%f", result.Value, expected)
        return false
    }
    return true
}



evaluator.goにはEval関数の分岐に次のようなcaseを追加すれば良い.

case *ast.FloatLiteral:
        return &object.Float{Value: node.Value}


こうすることでテストは通過する.
現状ではREPLで以下のようになる.

Feel free to type in commands
>> 1.2;
1.200000
>> 1.2 + 2.3;
ERROR: unknown operator: FLOAT + FLOAT
>> let x = 1.2;
>> x;
1.200000


変数に値を束縛することができたり,値を出すことはできるが,まだ演算に対応できてはいない.
Integerの評価に倣って実装していく.まずevaluator_test.goに次のテストを追加する.上記のREPLの結果でfloat64単体での評価はできているはずなので確認作業である.

func TestEvalFloatExpression(t *testing.T){
    tests := []struct{
        input       string
        expected    float64
    }{
        {"1.2", 1.2},
        {"12.34", 12.34},
    }
    for _, tt := range tests{
        evaluated := testEval(tt.input)
        testFloatObject(t, evaluated, tt.expected)
    }
}


このテストを追加しても当然クリアする.
次に,ひとまず「!」前置演算子がfloatにも対応しているかを確かめるために,TestBangOperatorのinputに次のケースを追加する.

{"!1.5", false},
{"!!1.5", true},


これも何も対処せずに通過する.
次は「ー」前置演算子にfloatを対応させる.先程のTestEvalFloatExpressionのinputに次のケースを追加する.

{"-1.2", -1.2},
{"-12.34", -12.34},


これはこのままテストをすると次のようなエラーが出る.

    evaluator_test.go:491: object is not Float. got=*object.Error (&{Message:unknown operator: -FLOAT})
    evaluator_test.go:491: object is not Float. got=*object.Error (&{Message:unknown operator: -FLOAT


なので「ー」前置演算子の評価関数をいじる必要があるとわかる.


前置演算子"-"の評価

修正前は次のようになっている.

func evalMinusPrefixOperatorExpression(right object.Object) object.Object{
    if right.Type() != object.INTEGER_OBJ{
        return newError("unknown operator: -%s", right.Type())
    }
    value := right.(*object.Integer).Value
    return &object.Integer{Value: -value}
}


このままではINTEGERオブジェクト以外は弾かれてしまうことがわかる.なのでFLOATオブジェクトにも対応できるように場合分けを変更,追加して次のようにする.


func evalMinusPrefixOperatorExpression(right object.Object) object.Object{
    if right.Type() == object.INTEGER_OBJ{
        value := right.(*object.Integer).Value
        return &object.Integer{Value: -value}
    }else if right.Type() == object.FLOAT_OBJ{
        value := right.(*object.Float).Value
        return &object.Float{Value: -value}
    }else {
        return newError("unknown operator: -%s", right.Type())
    }
}


前置演算子はこの二つしかないので,前置の対応はし終わった.次は中置演算の対応をしていく.


中置演算子の評価(四則演算)

まずは「+」「ー」「*」「/」の4つについて対応していく.中置のテストのために先程のTestEvalFloatExpressionのinputに次のケースを追加する.


{"1.5 + 1.5 + 1.5 + 1.5 - 2.5", 3.5},
{"1.1 * 1.1", 1.21},
{"-1.5 + 3.0 + -1.5", 0},
{"1.5 * 2.0 + 10.0", 13.0},
{"1.5 + 2.5 * 10.0", 26.5},
{"1.5 + 2.0 * -0.75", 0},
{"1.5 / 0.75 * 2.0 + 1.2", 5.2},
{"1.5 * (1.5 + 0.5)", 3.0},
{"3.0 * 3.0 * 3.0 + 10.0", 37.0},
{"3.0 * (3.0 * 3.0) + 10.0", 37.0},
{"(5.0 + 10.0 * 2.0 + 15.0 / 3.0) * 2.0 + -10.0", 50.0},


evalInfixExpression関数をいじっていく.修正前は次のようになっている.ここで対応すべき4つの中置演算子に関わっているのは一番上のcaseだけである.

switch {
    case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ:
        return evalIntegerInfixExpression(operator, left, right)
    case operator == "==":
        return nativeBoolToBooleanObject(left == right)
    case operator == "!=":
        return nativeBoolToBooleanObject(left != right)
    case left.Type() != right.Type():
        return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type())
    case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ:
        return evalStringInfixexpression(operator, left, right)
    default:
        return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
    }



Floatに対応させるにはINTEGERを模倣すればよさそうである.なので次のようなcaseを追加すれば良い.

case left.Type() == object.FLOAT_OBJ && right.Type() == object.FLOAT_OBJ:
        return evalFloatInfixExpression(operator, left, right)


返り値である関数もIntegerのものを模倣して次のように作成する.

func evalFloatInfixExpression(operator string, left, right object.Object) object.Object{
    leftVal := left.(*object.Float).Value
    rightVal := right.(*object.Float).Value
    switch operator{
    case "+":
        return &object.Float{Value: leftVal + rightVal}
    case "-":
        return &object.Float{Value: leftVal - rightVal}
    case "*":
        return &object.Float{Value: leftVal * rightVal}
    case "/":
        return &object.Float{Value: leftVal / rightVal}
    default:
        return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
    }
}



これでテストを通過させることができる,
ただし,何故か「*」のときに結果が 1.○○のときはwantedと一致するにも関わらず,expectedと一致しないと返されてしまう.なぜかわからないが,結果自体は同じであるはずなのでひとまず見過ごしておく,

Ex) 
1.1 * 1.1 OUT
1.1 * 1.8 OUT
1.1 * 1.0 OK



中置演算子の評価(bool)

次は,bool 値を返す演算に対応させていく.evaluator_test.goのTestEvalBooleanExpressionにおけるinputに次のものを追加する.

{"1.2 < 2.4", true},
{"2.4 < 1.2", false},
{"2.4 > 1.2", true},
{"1.2 > 2.4", false},
{"1.2 == 1.2", true},
{"1.2 == 2.4", false},
{"1.2 != 2.4", true},
{"1.2 != 1.2", false},


上で追加したevalFloatInfixExpressionの分岐に次のように追加するだけでよい.

case "<":
        return nativeBoolToBooleanObject(leftVal < rightVal)
case ">":
        return nativeBoolToBooleanObject(leftVal > rightVal)
case "==":
        return nativeBoolToBooleanObject(leftVal == rightVal)
case "!=":
        return nativeBoolToBooleanObject(leftVal != rightVal)


これでfloatの実装は完了である.

Feel free to type in commands
>> let x = 1.21;
>> let y = 2.0;
>> x + y;
3.210000
>> x * y;
2.420000
>> x != y;
true
>> let add = fn(a, b){return a + b;};
>> add(1.2, 3.4);
4.600000
>> let array = [1.2, 3.4, 2, "4"];
>> array[2];
2
>> array[5];
3.400000
>> 


この記事が気に入ったらサポートをしてみませんか?