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
>>
この記事が気に入ったらサポートをしてみませんか?