diff --git a/compiler/ast/expressions.go b/compiler/ast/expressions.go index 03a9a8807..76e98e855 100644 --- a/compiler/ast/expressions.go +++ b/compiler/ast/expressions.go @@ -6,28 +6,57 @@ import ( "strings" ) +// IntegerLiteral contains the node expression and its value type IntegerLiteral struct { *BaseNode Value int } func (il *IntegerLiteral) expressionNode() {} + +// IntegerLiteral.TokenLiteral gets the Integer type token func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal } + +// IntegerLiteral.String gets the string format of the Integer type token func (il *IntegerLiteral) String() string { return il.Token.Literal } +// FloatLiteral contains the node expression and its value +type FloatLiteral struct { + *BaseNode + Value float64 +} + +func (il *FloatLiteral) expressionNode() {} + +// FloatLiteral.TokenLiteral gets the literal of the Float type token +func (il *FloatLiteral) TokenLiteral() string { + return il.Token.Literal +} + +// FloatLiteral.String gets the string format of the Float type token +func (il *FloatLiteral) String() string { + return il.Token.Literal +} + +// StringLiteral contains the node expression and its value type StringLiteral struct { *BaseNode Value string } +// Define the string literal which contains the node expression and its value func (sl *StringLiteral) expressionNode() {} + +// StringLiteral.TokenLiteral gets the literal of the String type token func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal } + +// StringLiteral.String gets the string format of the String type token func (sl *StringLiteral) String() string { var out bytes.Buffer @@ -37,15 +66,20 @@ func (sl *StringLiteral) String() string { return out.String() } +// ArrayExpression defines the array expression literal which contains the node expression and its value type ArrayExpression struct { *BaseNode Elements []Expression } func (ae *ArrayExpression) expressionNode() {} + +// ArrayExpression gets the literal of the Array type token func (ae *ArrayExpression) TokenLiteral() string { return ae.Token.Literal } + +// ArrayExpression.String gets the string format of the Array type token func (ae *ArrayExpression) String() string { var out bytes.Buffer @@ -90,15 +124,20 @@ func (pe *PairExpression) String() string { return fmt.Sprintf("%s: %s", pe.Key.String(), pe.Value.String()) } +// HashExpression defines the hash expression literal which contains the node expression and its value type HashExpression struct { *BaseNode Data map[string]Expression } func (he *HashExpression) expressionNode() {} + +// HashExpression.TokenLiteral gets the literal of the Hash type token func (he *HashExpression) TokenLiteral() string { return he.Token.Literal } + +// HashExpression.String gets the string format of the Hash type token func (he *HashExpression) String() string { var out bytes.Buffer var pairs []string @@ -190,15 +229,20 @@ func (ae *AssignExpression) String() string { return out.String() } +// BooleanExpression defines the boolean expression literal which contains the node expression and its value type BooleanExpression struct { *BaseNode Value bool } func (b *BooleanExpression) expressionNode() {} + +// BooleanExpression.TokenLiteral gets the literal of the Boolean type token func (b *BooleanExpression) TokenLiteral() string { return b.Token.Literal } + +// BooleanExpression.String gets the string format of the Boolean type token func (b *BooleanExpression) String() string { return b.Token.Literal } @@ -367,6 +411,7 @@ func (ye *YieldExpression) String() string { return out.String() } +// RangeExpression defines the range expression literal which contains the node expression and its start/end value type RangeExpression struct { *BaseNode Start Expression @@ -374,9 +419,13 @@ type RangeExpression struct { } func (re *RangeExpression) expressionNode() {} + +// RangeExpression.TokenLiteral gets the literal of the Range type token func (re *RangeExpression) TokenLiteral() string { return re.Token.Literal } + +// RangeExpression.String gets the string format of the Range type token func (re *RangeExpression) String() string { var out bytes.Buffer diff --git a/compiler/bytecode/expression_generation.go b/compiler/bytecode/expression_generation.go index eec63f3f1..bfee13de0 100644 --- a/compiler/bytecode/expression_generation.go +++ b/compiler/bytecode/expression_generation.go @@ -14,6 +14,8 @@ func (g *Generator) compileExpression(is *InstructionSet, exp ast.Expression, sc is.define(GetInstanceVariable, sourceLine, exp.Value) case *ast.IntegerLiteral: is.define(PutObject, sourceLine, fmt.Sprint(exp.Value)) + case *ast.FloatLiteral: + is.define(PutFloat, sourceLine, fmt.Sprint(exp.Value)) case *ast.StringLiteral: is.define(PutString, sourceLine, exp.Value) case *ast.BooleanExpression: diff --git a/compiler/bytecode/instruction.go b/compiler/bytecode/instruction.go index cf6373e39..763bf5619 100644 --- a/compiler/bytecode/instruction.go +++ b/compiler/bytecode/instruction.go @@ -24,6 +24,7 @@ const ( SetInstanceVariable = "setinstancevariable" PutBoolean = "putboolean" PutString = "putstring" + PutFloat = "putfloat" PutSelf = "putself" PutObject = "putobject" PutNull = "putnil" diff --git a/compiler/parser/data_type_parsing.go b/compiler/parser/data_type_parsing.go index 33f4bd096..35844e4cf 100644 --- a/compiler/parser/data_type_parsing.go +++ b/compiler/parser/data_type_parsing.go @@ -1,6 +1,7 @@ package parser import ( + "fmt" "github.com/goby-lang/goby/compiler/ast" "github.com/goby-lang/goby/compiler/token" "strconv" @@ -20,6 +21,25 @@ func (p *Parser) parseIntegerLiteral() ast.Expression { return lit } +func (p *Parser) parseFloatLiteral(integerPart ast.Expression) ast.Expression { + // Get the fractional part of the token + p.nextToken() + + floatTok := token.Token{ + Type: token.Float, + Literal: fmt.Sprintf("%s.%s", integerPart.String(), p.curToken.Literal), + Line: p.curToken.Line, + } + lit := &ast.FloatLiteral{BaseNode: &ast.BaseNode{Token: floatTok}} + value, err := strconv.ParseFloat(lit.TokenLiteral(), 64) + if err != nil { + p.error = newTypeParsingError(lit.TokenLiteral(), "float", p.curToken.Line) + return nil + } + lit.Value = float64(value) + return lit +} + func (p *Parser) parseStringLiteral() ast.Expression { lit := &ast.StringLiteral{BaseNode: &ast.BaseNode{Token: p.curToken}} lit.Value = p.curToken.Literal diff --git a/compiler/parser/expression_parsing.go b/compiler/parser/expression_parsing.go index d2eb80416..e8088b26d 100644 --- a/compiler/parser/expression_parsing.go +++ b/compiler/parser/expression_parsing.go @@ -388,6 +388,18 @@ func (p *Parser) parseMultiVariables(left ast.Expression) ast.Expression { return result } +func (p *Parser) parseDotExpression(receiver ast.Expression) ast.Expression { + _, ok := receiver.(*ast.IntegerLiteral) + + // When both receiver & caller are integer => Float + if ok && p.peekTokenIs(token.Int) { + return p.parseFloatLiteral(receiver) + } + + // Normal call method expression with receiver + return p.parseCallExpressionWithReceiver(receiver) +} + func (p *Parser) expandAssignmentValue(value ast.Expression) ast.Expression { switch p.curToken.Type { case token.Assign: diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 88e11d727..baf3c7c48 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -171,7 +171,7 @@ func New(l *lexer.Lexer) *Parser { p.registerInfix(token.ResolutionOperator, p.parseInfixExpression) p.registerInfix(token.Assign, p.parseAssignExpression) p.registerInfix(token.Range, p.parseRangeExpression) - p.registerInfix(token.Dot, p.parseCallExpressionWithReceiver) + p.registerInfix(token.Dot, p.parseDotExpression) p.registerInfix(token.LParen, p.parseCallExpressionWithoutReceiver) p.registerInfix(token.LBracket, p.parseIndexExpression) p.registerInfix(token.Colon, p.parsePairExpression) diff --git a/compiler/token/token.go b/compiler/token/token.go index 19281bcf9..bbcce34f7 100644 --- a/compiler/token/token.go +++ b/compiler/token/token.go @@ -19,6 +19,7 @@ const ( Ident = "IDENT" InstanceVariable = "INSTANCE_VAR" Int = "INT" + Float = "FLOAT" String = "STRING" Comment = "COMMENT" diff --git a/vm/float.go b/vm/float.go index 15eeba353..e9b54d5da 100644 --- a/vm/float.go +++ b/vm/float.go @@ -2,10 +2,10 @@ package vm import ( "math" - "strconv" "github.com/goby-lang/goby/vm/classes" "github.com/goby-lang/goby/vm/errors" + "strconv" ) // FloatObject represents an inexact real number using the native architecture's double-precision floating point @@ -438,7 +438,7 @@ func (f *FloatObject) numericComparison(t *thread, rightObject Object, operation // toString returns the object's value as the string format, in non // exponential format (straight number, without exponent `E`). func (f *FloatObject) toString() string { - return strconv.FormatFloat(f.value, 'f', -1, 64) + return strconv.FormatFloat(f.value, 'f', -1, 64) // fmt.Sprintf("%f", f.value) } // toJSON just delegates to toString diff --git a/vm/float_test.go b/vm/float_test.go index 7bd58ea17..e8d4a320f 100644 --- a/vm/float_test.go +++ b/vm/float_test.go @@ -27,12 +27,12 @@ func TestFloatArithmeticOperationWithFloat(t *testing.T) { input string expected interface{} }{ - {`'13.5'.to_f + '3.2'.to_f`, 16.7}, - {`'13.5'.to_f - '3.2'.to_f`, 10.3}, - {`'13.5'.to_f * '3.2'.to_f`, 43.2}, - {`'13.5'.to_f % '3.75'.to_f`, 2.25}, - {`'13.5'.to_f / '3.75'.to_f`, 3.6}, - {`'16.0'.to_f ** '3.5'.to_f`, 16384.0}, + {`13.5 + 3.2`, 16.7}, + {`13.5 - 3.2`, 10.3}, + {`13.5 * 3.2`, 43.2}, + {`13.5 % 3.75`, 2.25}, + {`13.5 / 3.75`, 3.6}, + {`16.0 ** 3.5`, 16384.0}, } for i, tt := range tests { @@ -49,12 +49,12 @@ func TestFloatArithmeticOperationWithInteger(t *testing.T) { input string expected interface{} }{ - {`'13.5'.to_f + 3`, 16.5}, - {`'13.5'.to_f - 3`, 10.5}, - {`'13.5'.to_f * 3`, 40.5}, - {`'13.5'.to_f % 3`, 1.5}, - {`'13.5'.to_f / 3`, 4.5}, - {`'13.5'.to_f ** 3`, 2460.375}, + {`13.5 + 3`, 16.5}, + {`13.5 - 3`, 10.5}, + {`13.5 * 3`, 40.5}, + {`13.5 % 3`, 1.5}, + {`13.5 / 3`, 4.5}, + {`13.5 ** 3`, 2460.375}, } for i, tt := range tests { @@ -68,10 +68,10 @@ func TestFloatArithmeticOperationWithInteger(t *testing.T) { func TestFloatArithmeticOperationFail(t *testing.T) { testsFail := []errorTestCase{ - {`'1'.to_f + "p"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f - "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f ** "p"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f / "t"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 + "p"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 - "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 ** "p"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 / "t"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, } for i, tt := range testsFail { @@ -88,21 +88,21 @@ func TestFloatComparisonWithFloat(t *testing.T) { input string expected interface{} }{ - {`'1.5'.to_f > '2.5'.to_f`, false}, - {`'2.5'.to_f > '1.5'.to_f`, true}, - {`'3.5'.to_f > '3.5'.to_f`, false}, - {`'1.5'.to_f < '2.5'.to_f`, true}, - {`'2.5'.to_f < '1.5'.to_f`, false}, - {`'3.5'.to_f < '3.5'.to_f`, false}, - {`'1.5'.to_f >= '2.5'.to_f`, false}, - {`'2.5'.to_f >= '1.5'.to_f`, true}, - {`'3.5'.to_f >= '3.5'.to_f`, true}, - {`'1.5'.to_f <= '2.5'.to_f`, true}, - {`'2.5'.to_f <= '1.5'.to_f`, false}, - {`'3.5'.to_f <= '3.5'.to_f`, true}, - {`'1.5'.to_f <=> '2.5'.to_f`, -1}, - {`'2.5'.to_f <=> '1.5'.to_f`, 1}, - {`'3.5'.to_f <=> '3.5'.to_f`, 0}, + {`1.5 > 2.5`, false}, + {`2.5 > 1.5`, true}, + {`3.5 > 3.5`, false}, + {`1.5 < 2.5`, true}, + {`2.5 < 1.5`, false}, + {`3.5 < 3.5`, false}, + {`1.5 >= 2.5`, false}, + {`2.5 >= 1.5`, true}, + {`3.5 >= 3.5`, true}, + {`1.5 <= 2.5`, true}, + {`2.5 <= 1.5`, false}, + {`3.5 <= 3.5`, true}, + {`1.5 <=> 2.5`, -1}, + {`2.5 <=> 1.5`, 1}, + {`3.5 <=> 3.5`, 0}, } for i, tt := range tests { @@ -119,21 +119,21 @@ func TestFloatComparisonWithInteger(t *testing.T) { input string expected interface{} }{ - {`'1'.to_f > 2`, false}, - {`'2'.to_f > 1`, true}, - {`'3'.to_f > 3`, false}, - {`'1'.to_f < 2`, true}, - {`'2'.to_f < 1`, false}, - {`'3'.to_f < 3`, false}, - {`'1'.to_f >= 2`, false}, - {`'2'.to_f >= 1`, true}, - {`'3'.to_f >= 3`, true}, - {`'1'.to_f <= 2`, true}, - {`'2'.to_f <= 1`, false}, - {`'3'.to_f <= 3`, true}, - {`'1'.to_f <=> 2`, -1}, - {`'2'.to_f <=> 1`, 1}, - {`'3'.to_f <=> 3`, 0}, + {`1 > 2`, false}, + {`2 > 1`, true}, + {`3 > 3`, false}, + {`1 < 2`, true}, + {`2 < 1`, false}, + {`3 < 3`, false}, + {`1 >= 2`, false}, + {`2 >= 1`, true}, + {`3 >= 3`, true}, + {`1 <= 2`, true}, + {`2 <= 1`, false}, + {`3 <= 3`, true}, + {`1 <=> 2`, -1}, + {`2 <=> 1`, 1}, + {`3 <=> 3`, 0}, } for i, tt := range tests { @@ -147,11 +147,11 @@ func TestFloatComparisonWithInteger(t *testing.T) { func TestFloatComparisonFail(t *testing.T) { testsFail := []errorTestCase{ - {`'1'.to_f > "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f >= "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f < "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f <= "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, - {`'1'.to_f <=> "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 > "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 >= "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 < "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 <= "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, + {`1 <=> "m"`, "TypeError: Expect argument to be Numeric. got: String", 1, 1}, } for i, tt := range testsFail { @@ -168,22 +168,22 @@ func TestFloatEquality(t *testing.T) { input string expected interface{} }{ - {`'123.5'.to_f == '123.5'.to_f`, true}, - {`'123'.to_f == 123`, true}, - {`'123.5'.to_f == '124'.to_f`, false}, - {`'123.5'.to_f == "123.5"`, false}, - {`'123.5'.to_f == (1..3)`, false}, - {`'123.5'.to_f == { a: 1 }`, false}, - {`'123.5'.to_f == [1]`, false}, - {`'123.5'.to_f == Float`, false}, - {`'123.5'.to_f != '123.5'.to_f`, false}, - {`'123.5'.to_f != 123`, true}, - {`'123.5'.to_f != '124'.to_f`, true}, - {`'123.5'.to_f != "123.5"`, true}, - {`'123.5'.to_f != (1..3)`, true}, - {`'123.5'.to_f != { a: 1 }`, true}, - {`'123.5'.to_f != [1]`, true}, - {`'123.5'.to_f != Float`, true}, + {`123.5 == 123.5`, true}, + {`123 == 123`, true}, + {`123.5 == 124`, false}, + {`123.5 == "123.5"`, false}, + {`123.5 == (1..3)`, false}, + {`123.5 == { a: 1 }`, false}, + {`123.5 == [1]`, false}, + {`123.5 == Float`, false}, + {`123.5 != 123.5`, false}, + {`123.5 != 123`, true}, + {`123.5 != 124`, true}, + {`123.5 != "123.5"`, true}, + {`123.5 != (1..3)`, true}, + {`123.5 != { a: 1 }`, true}, + {`123.5 != [1]`, true}, + {`123.5 != Float`, true}, } for i, tt := range tests { @@ -200,15 +200,42 @@ func TestFloatConversions(t *testing.T) { input string expected interface{} }{ - {`'100.3'.to_f.to_i`, 100}, - {`'100.3'.to_f.to_s`, "100.3"}, - {`'100.3'.to_f.to_d.to_s`, "100.3"}, + {`(100.3).to_i`, 100}, + {`(100.3).to_s`, "100.3"}, + {`(100.3).to_d.to_s`, "100.3"}, {` - '3.14159265358979'.to_f.to_d.to_s`, + (3.14159265358979).to_d.to_s`, "3.14159265358979"}, {` - '-273.150000000'.to_f.to_d.to_s`, + (-273.150000000).to_d.to_s`, "-273.15"}, + {`100.3.to_i`, 100}, + {`100.3.to_s`, "100.3"}, + {`100.3.to_d.to_s`, "100.3"}, + {` + 3.14159265358979.to_d.to_s`, + "3.14159265358979"}, + // TODO: Able to parse negative float value and call method without parentheses + //{` + //-273.150000000.to_d.to_s`, + // "-273.15"}, + } + + for i, tt := range tests { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkExpected(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestFloatEdgeCases(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {`(0.1 + 0.2).to_s`, "0.30000000000000004"}, } for i, tt := range tests { diff --git a/vm/instruction.go b/vm/instruction.go index 0a854a9f3..5fad1f78e 100644 --- a/vm/instruction.go +++ b/vm/instruction.go @@ -5,6 +5,7 @@ import ( "github.com/goby-lang/goby/compiler/bytecode" "github.com/goby-lang/goby/vm/classes" "github.com/goby-lang/goby/vm/errors" + "strconv" "strings" ) @@ -338,6 +339,20 @@ var builtinActions = map[operationType]*action{ t.stack.push(&Pointer{Target: object}) }, }, + bytecode.PutFloat: { + name: bytecode.PutFloat, + operation: func(t *thread, sourceLine int, cf *normalCallFrame, args ...interface{}) { + var value float64 + switch argValue := args[0].(type) { + case string: + value, _ = strconv.ParseFloat(argValue, 64) + case int: + value = float64(argValue) + } + object := t.vm.initFloatObject(value) + t.stack.push(&Pointer{Target: object}) + }, + }, bytecode.PutNull: { name: bytecode.PutNull, operation: func(t *thread, sourceLine int, cf *normalCallFrame, args ...interface{}) { @@ -557,7 +572,7 @@ func (vm *VM) initObjectFromGoType(value interface{}) Object { case int32: return vm.initIntegerObject(int(v)) case float64: - return vm.initIntegerObject(int(v)) + return vm.initFloatObject(v) case []uint8: bytes := []byte{} diff --git a/vm/json.go b/vm/json.go index f1d879d13..43f2c669f 100644 --- a/vm/json.go +++ b/vm/json.go @@ -140,6 +140,14 @@ func (v *VM) convertJSONToHashObj(j jsonObj) Object { // Single json object case map[string]interface{}: objectMap[key] = v.convertJSONToHashObj(jsonValue) + case float64: + // TODO: Find a better way to distinguish between Float & Integer because default GO JSON package + // TODO: support only for parsing float out regardless of integer or float type data of JSON value + if jsonValue == float64(int(jsonValue)) { + objectMap[key] = v.initIntegerObject(int(jsonValue)) + } else { + objectMap[key] = v.initFloatObject(jsonValue) + } default: objectMap[key] = v.initObjectFromGoType(jsonValue) }