diff --git a/ast/ast.go b/ast/ast.go index 53d1b3c..c69559f 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -172,6 +172,15 @@ func (il *IntegerLiteral) expressionNode() {} func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal } func (il *IntegerLiteral) String() string { return il.Token.Literal } +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 } + type PrefixExpression struct { Token token.Token Operator string diff --git a/docs/content/en/docs/literals/float.md b/docs/content/en/docs/literals/float.md new file mode 100644 index 0000000..de4edd0 --- /dev/null +++ b/docs/content/en/docs/literals/float.md @@ -0,0 +1,59 @@ +--- +title: "Float" +menu: + docs: + parent: "literals" +--- + + + + +## Literal Specific Methods + +### plz_s() +> Returns `STRING` + +Returns a string representation of the float. + + +```js +🚀 > a = 123.456 +=> 123.456 +🚀 > a.plz_s() +=> "123.456" +``` + + + +## Generic Literal Methods + +### methods() +> Returns `ARRAY` + +Returns an array of all supported methods names. + +```js +🚀 > "test".methods() +=> [count, downcase, find, reverse!, split, lines, upcase!, strip!, downcase!, size, plz_i, replace, reverse, strip, upcase] +``` + +### type() +> Returns `STRING` + +Returns the type of the object. + +```js +🚀 > "test".type() +=> "STRING" +``` + +### wat() +> Returns `STRING` + +Returns the supported methods with usage information. + +```js +🚀 > true.wat() +=> BOOLEAN supports the following methods: + plz_s() +``` diff --git a/docs/content/en/docs/literals/integer.md b/docs/content/en/docs/literals/integer.md index a95f8ae..44cec06 100644 --- a/docs/content/en/docs/literals/integer.md +++ b/docs/content/en/docs/literals/integer.md @@ -21,6 +21,23 @@ is_false = 1 == 2; ## Literal Specific Methods +### plz_f() +> Returns `FLOAT` + +Converts the integer into a float. + + +```js +🚀 > a = 456 +=> 456 +🚀 > a.plz_f() +=> 456.0 + +🚀 > 1234.plz_f() +=> 1234.0 +``` + + ### plz_s(INTEGER) > Returns `STRING` diff --git a/docs/generate.go b/docs/generate.go index 97ad6fc..fddedb6 100644 --- a/docs/generate.go +++ b/docs/generate.go @@ -24,6 +24,7 @@ func main() { error_methods := object.ListObjectMethods()[object.ERROR_OBJ] file_methods := object.ListObjectMethods()[object.FILE_OBJ] null_methods := object.ListObjectMethods()[object.NULL_OBJ] + float_methods := object.ListObjectMethods()[object.FLOAT_OBJ] tempData := templateData{ Title: "String", @@ -94,6 +95,9 @@ To cast a negative integer a digit can be prefixed with a - eg. -456.`, DefaultMethods: default_methods} create_doc("docs/templates/literal.md", "docs/content/en/docs/literals/integer.md", tempData) + tempData = templateData{Title: "Float", LiteralMethods: float_methods, DefaultMethods: default_methods} + create_doc("docs/templates/literal.md", "docs/content/en/docs/literals/float.md", tempData) + } func create_doc(path string, target string, data templateData) bool { diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index ae0d233..9512f44 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -42,6 +42,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object { // Expressions case *ast.IntegerLiteral: return &object.Integer{Value: node.Value} + case *ast.FloatLiteral: + return &object.Float{Value: node.Value} case *ast.FunctionLiteral: params := node.Parameters body := node.Body @@ -366,6 +368,9 @@ func evalIntegerInfixExpression(operator string, left, right object.Object) obje case "*": return &object.Integer{Value: leftVal * rightVal} case "/": + if rightVal == 0 { + return newError("devision by zero not allowed") + } return &object.Integer{Value: leftVal / rightVal} case "<": return nativeBoolToBooleanObject(leftVal < rightVal) @@ -376,16 +381,62 @@ func evalIntegerInfixExpression(operator string, left, right object.Object) obje } } +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 "/": + if rightVal == 0 { + return newError("devision by zero not allowed") + } + return &object.Float{Value: leftVal / rightVal} + case "<": + return nativeBoolToBooleanObject(leftVal < rightVal) + case ">": + return nativeBoolToBooleanObject(leftVal > rightVal) + default: + return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) + } +} + func evalInfixExpression(operator string, left, right object.Object) object.Object { switch { case operator == "==": return nativeBoolToBooleanObject(object.CompareObjects(left, right)) case operator == "!=": return nativeBoolToBooleanObject(!object.CompareObjects(left, right)) + case object.IsNumber(left) && object.IsNumber(right): + if left.Type() == right.Type() && operator != "/" { + if left.Type() == object.INTEGER_OBJ { + return evalIntegerInfixExpression(operator, left, right) + } else if left.Type() == object.FLOAT_OBJ { + return evalFloatInfixExpression(operator, left, right) + } + } + + leftOrig, rightOrig := left, right + if left.Type() == object.INTEGER_OBJ { + left = left.(*object.Integer).ToFloat() + } + if right.Type() == object.INTEGER_OBJ { + right = right.(*object.Integer).ToFloat() + } + + result := evalFloatInfixExpression(operator, left, right) + + if object.IsNumber(result) && leftOrig.Type() == object.INTEGER_OBJ && rightOrig.Type() == object.INTEGER_OBJ { + return result.(*object.Float).TryInteger() + } + return result case left.Type() != right.Type(): return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type()) - case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ: - return evalIntegerInfixExpression(operator, left, right) case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: return evalStringInfixExpression(operator, left, right) case left.Type() == object.ARRAY_OBJ && right.Type() == object.ARRAY_OBJ: diff --git a/lexer/lexer.go b/lexer/lexer.go index 130f77d..ffb71f7 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -102,8 +102,12 @@ func (l *Lexer) NextToken() token.Token { tok.Type = token.LookupIdent(tok.Literal) return tok } else if isDigit(l.ch) { - tok.Type = token.INT tok.Literal = l.readNumber() + if strings.Contains(tok.Literal, ".") { + tok.Type = token.FLOAT + } else { + tok.Type = token.INT + } return tok } else if i := isEmoji(l.ch); i > 0 { out := make([]byte, i) @@ -253,6 +257,14 @@ func (l *Lexer) readNumber() string { for isDigit(l.ch) { l.readChar() } + + if l.ch == '.' && isDigit(l.peekChar()) { + l.readChar() + for isDigit(l.ch) { + l.readChar() + } + } + return l.input[position:l.position] } diff --git a/object/float.go b/object/float.go new file mode 100644 index 0000000..80bc2d9 --- /dev/null +++ b/object/float.go @@ -0,0 +1,57 @@ +package object + +import ( + "fmt" + "hash/fnv" + "strconv" +) + +type Float struct { + Value float64 +} + +func (f *Float) Inspect() string { return f.toString() } +func (f *Float) Type() ObjectType { return FLOAT_OBJ } +func (f *Float) HashKey() HashKey { + h := fnv.New64a() + h.Write([]byte(fmt.Sprintf("%f", f.Value))) + + return HashKey{Type: f.Type(), Value: h.Sum64()} +} + +func init() { + objectMethods[FLOAT_OBJ] = map[string]ObjectMethod{ + "plz_s": ObjectMethod{ + description: "Returns a string representation of the float.", + example: `🚀 > a = 123.456 +=> 123.456 +🚀 > a.plz_s() +=> "123.456"`, + returnPattern: [][]string{ + []string{STRING_OBJ}, + }, + method: func(o Object, args []Object) Object { + f := o.(*Float) + return &String{Value: f.toString()} + }, + }, + } +} + +func (f *Float) InvokeMethod(method string, env Environment, args ...Object) Object { + return objectMethodLookup(f, method, args) +} + +func (f *Float) TryInteger() Object { + if i := int64(f.Value); f.Value == float64(i) { + return &Integer{Value: i} + } + return f +} + +func (f *Float) toString() string { + if f.Value == float64(int64(f.Value)) { + return fmt.Sprintf("%.1f", f.Value) + } + return strconv.FormatFloat(f.Value, 'f', -1, 64) +} diff --git a/object/float_test.go b/object/float_test.go new file mode 100644 index 0000000..b871e75 --- /dev/null +++ b/object/float_test.go @@ -0,0 +1,54 @@ +package object_test + +import ( + "testing" + + "github.com/flipez/rocket-lang/object" +) + +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 +} + +func TestFloatObjectMethods(t *testing.T) { + tests := []inputTestCase{ + {`2.1.plz_s()`, "2.1"}, + {`10.0.type()`, "FLOAT"}, + {`2.2.nope()`, "Failed to invoke method: nope"}, + {`(2.0.wat().lines().size() == 2.0.methods().size() + 1).plz_s()`, "true"}, + } + + testInput(t, tests) +} + +func TestFloatHashKey(t *testing.T) { + float1_1 := &object.Float{Value: 1.0} + float1_2 := &object.Float{Value: 1.0} + float2 := &object.Float{Value: 2.0} + + if float1_1.HashKey() != float1_2.HashKey() { + t.Errorf("float with same content have different hash keys") + } + + if float1_1.HashKey() == float2.HashKey() { + t.Errorf("float with different content have same hash keys") + } +} + +func TestFloatInspect(t *testing.T) { + float1 := &object.Float{Value: 1.0} + + if float1.Inspect() != "1.0" { + t.Errorf("float inspect does not match value") + } +} diff --git a/object/integer.go b/object/integer.go index 0444d99..82d3630 100644 --- a/object/integer.go +++ b/object/integer.go @@ -48,10 +48,30 @@ func init() { return &String{Value: strconv.FormatInt(i.Value, base)} }, }, + "plz_f": ObjectMethod{ + description: "Converts the integer into a float.", + example: `🚀 > a = 456 +=> 456 +🚀 > a.plz_f() +=> 456.0 + +🚀 > 1234.plz_f() +=> 1234.0`, + returnPattern: [][]string{ + []string{FLOAT_OBJ}, + }, + method: func(o Object, _ []Object) Object { + i := o.(*Integer) + return &Float{Value: float64(i.Value)} + }, + }, } } func (i *Integer) InvokeMethod(method string, env Environment, args ...Object) Object { return objectMethodLookup(i, method, args) +} +func (i *Integer) ToFloat() Object { + return &Float{Value: float64(i.Value)} } diff --git a/object/integer_test.go b/object/integer_test.go index 914445c..0f14edf 100644 --- a/object/integer_test.go +++ b/object/integer_test.go @@ -1,8 +1,9 @@ package object_test import ( - "github.com/flipez/rocket-lang/object" "testing" + + "github.com/flipez/rocket-lang/object" ) func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool { @@ -23,6 +24,7 @@ func TestIntegerObjectMethods(t *testing.T) { tests := []inputTestCase{ {`2.plz_s()`, "2"}, {`10.plz_s(2)`, "1010"}, + {`2.plz_f()`, 2.0}, {`10.type()`, "INTEGER"}, {`2.nope()`, "Failed to invoke method: nope"}, {`(2.wat().lines().size() == 2.methods().size() + 1).plz_s()`, "true"}, diff --git a/object/number_test.go b/object/number_test.go new file mode 100644 index 0000000..9e55b31 --- /dev/null +++ b/object/number_test.go @@ -0,0 +1,229 @@ +package object_test + +import "testing" + +func TestNumberObjects(t *testing.T) { + tests := []inputTestCase{ + // integer | integer + {"1 == 1", true}, + {"1 == 2", false}, + + {"1 != 1", false}, + {"1 != 2", true}, + + {"1 < 2", true}, + {"1 < 1", false}, + + {"2 > 1", true}, + {"1 > 2", false}, + + {"0+0", 0}, + {"0+1", 1}, + {"1+2", 3}, + + {"0-0", 0}, + {"0-1", -1}, + {"1-0", 1}, + {"1-2", -1}, + {"2-1", 1}, + + {"1*1", 1}, + {"1*2", 2}, + {"1*0", 0}, + {"2*1", 2}, + {"0*1", 0}, + + {"1/1", 1}, + {"1/2", 0.5}, + {"2/1", 2}, + + // float | float + {"1.0 == 1.0", true}, + {"1.0 == 2.0", false}, + + {"1.0 != 1.0", false}, + {"1.0 != 2.0", true}, + + {"1.0 < 2.0", true}, + {"1.0 < 1.0", false}, + + {"2.0 > 1.0", true}, + {"1.0 > 2.0", false}, + + {"0.0+0.0", 0.0}, + {"0.0+1.0", 1.0}, + {"1.0+2.0", 3.0}, + {"1.2+3.4", 4.6}, + + {"0.0-0.0", 0.0}, + {"0.0-1.0", -1.0}, + {"1.0-0.0", 1.0}, + {"1.0-2.0", -1.0}, + {"2.0-1.0", 1.0}, + {"3.4-1.2", 2.2}, + + {"1.0*1.0", 1.0}, + {"1.0*2.0", 2.0}, + {"1.0*0.0", 0.0}, + {"2.0*1.0", 2.0}, + {"0.0*1.0", 0.0}, + {"1.2*3.4", 4.08}, + + {"1.0/1.0", 1.0}, + {"1.0/2.0", 0.5}, + {"2.0/1.0", 2.0}, + {"6.8/3.4", 2.0}, + + // integer | float + {"1 == 1.0", false}, + {"1 == 2.0", false}, + + {"1 != 1.0", true}, + {"1 != 2.0", true}, + + {"1 < 2.0", true}, + {"1 < 1.0", false}, + + {"2 > 1.0", true}, + {"1 > 2.0", false}, + + {"0+0.0", 0.0}, + {"0+1.0", 1.0}, + {"1+2.0", 3.0}, + {"1+3.4", 4.4}, + + {"0-0.0", 0.0}, + {"0-1.0", -1.0}, + {"1-0.0", 1.0}, + {"1-2.0", -1.0}, + {"2-1.0", 1.0}, + {"3-1.2", 1.8}, + + {"1*1.0", 1.0}, + {"1*2.0", 2.0}, + {"1*0.0", 0.0}, + {"2*1.0", 2.0}, + {"0*1.0", 0.0}, + {"3*1.2", 3.5999999999999996}, + + {"1/1.0", 1.0}, + {"1/2.0", 0.5}, + {"2/1.0", 2.0}, + + // float | integer + {"1.0 == 1", false}, + {"1.0 == 2", false}, + + {"1.0 != 1", true}, + {"1.0 != 2", true}, + + {"1.0 < 2", true}, + {"1.0 < 1", false}, + + {"2.0 > 1", true}, + {"1.0 > 2", false}, + + {"0.0+0", 0.0}, + {"0.0+1", 1.0}, + {"1.0+2", 3.0}, + {"1.2+3", 4.2}, + + {"0.0-0", 0.0}, + {"0.0-1", -1.0}, + {"1.0-0", 1.0}, + {"1.0-2", -1.0}, + {"2.0-1", 1.0}, + {"3.4-1", 2.4}, + + {"1.0*1", 1.0}, + {"1.0*2", 2.0}, + {"1.0*0", 0.0}, + {"2.0*1", 2.0}, + {"0.0*1", 0.0}, + {"1.2*3", 3.5999999999999996}, + + {"1.0/1", 1.0}, + {"1.0/2", 0.5}, + {"2.0/1", 2.0}, + + // float var | integer + {"a = 1.0; a == 1", false}, + {"a = 1.0; a == 2", false}, + + {"a = 1.0; a != 1", true}, + {"a = 1.0; a != 2", true}, + + {"a = 1.0; a < 2", true}, + {"a = 1.0; a < 1", false}, + + {"a = 2.0; a > 1", true}, + {"a = 1.0; a > 2", false}, + + {"a = 0.0; a = a+0; a", 0.0}, + {"a = 0.0; a = a+1; a", 1.0}, + {"a = 1.0; a = a+2; a", 3.0}, + {"a = 1.2; a = a+3; a", 4.2}, + + {"a = 0.0; a = a-0; a", 0.0}, + {"a = 0.0; a = a-1; a", -1.0}, + {"a = 1.0; a = a-0; a", 1.0}, + {"a = 1.0; a = a-2; a", -1.0}, + {"a = 2.0; a = a-1; a", 1.0}, + {"a = 3.4; a = a-1; a", 2.4}, + + {"a = 1.0; a = a*1; a", 1.0}, + {"a = 1.0; a = a*2; a", 2.0}, + {"a = 1.0; a = a*0; a", 0.0}, + {"a = 2.0; a = a*1; a", 2.0}, + {"a = 0.0; a = a*1; a", 0.0}, + {"a = 1.2; a = a*3; a", 3.5999999999999996}, + + {"a = 1.0; a = a/1; a", 1.0}, + {"a = 1.0; a = a/2; a", 0.5}, + {"a = 2.0; a = a/1; a", 2.0}, + + // integer var | float + {"a = 1; a == 1.0", false}, + {"a = 1; a == 2.0", false}, + + {"a = 1; a != 1.0", true}, + {"a = 1; a != 2.0", true}, + + {"a = 1; a < 2.0", true}, + {"a = 1; a < 1.0", false}, + + {"a = 2; a > 1.0", true}, + {"a = 1; a > 2.0", false}, + + {"a = 0; a = a+0.0; a", 0.0}, + {"a = 0; a = a+1.0; a", 1.0}, + {"a = 1; a = a+2.0; a", 3.0}, + {"a = 1; a = a+3.4; a", 4.4}, + + {"a = 0; a = a-0.0; a", 0.0}, + {"a = 0; a = a-1.0; a", -1.0}, + {"a = 1; a = a-0.0; a", 1.0}, + {"a = 1; a = a-2.0; a", -1.0}, + {"a = 2; a = a-1.0; a", 1.0}, + {"a = 3; a = a-1.2; a", 1.8}, + + {"a = 1; a = a*1.0; a", 1.0}, + {"a = 1; a = a*2.0; a", 2.0}, + {"a = 1; a = a*0.0; a", 0.0}, + {"a = 2; a = a*1.0; a", 2.0}, + {"a = 0; a = a*1.0; a", 0.0}, + {"a = 3; a = a*1.2; a", 3.5999999999999996}, + + {"a = 1; a = a/1.0; a", 1.0}, + {"a = 1; a = a/2.0; a", 0.5}, + {"a = 2; a = a/1.0; a", 2.0}, + + // division by zero + {"1.0 / 0", "devision by zero not allowed"}, + {"1.0 / 0.0", "devision by zero not allowed"}, + {"1 / 0", "devision by zero not allowed"}, + {"1 / 0.0", "devision by zero not allowed"}, + } + + testInput(t, tests) +} diff --git a/object/object.go b/object/object.go index 0a25460..e965c47 100644 --- a/object/object.go +++ b/object/object.go @@ -24,6 +24,7 @@ type Hashable interface { const ( INTEGER_OBJ = "INTEGER" + FLOAT_OBJ = "FLOAT" BOOLEAN_OBJ = "BOOLEAN" NULL_OBJ = "NULL" RETURN_VALUE_OBJ = "RETURN_VALUE" @@ -215,6 +216,11 @@ func CompareObjects(ao, bo Object) bool { return ao.(*Integer).Value == b.Value } return false + case FLOAT_OBJ: + if b, ok := bo.(*Float); ok { + return ao.(*Float).Value == b.Value + } + return false case BOOLEAN_OBJ: if b, ok := bo.(*Boolean); ok { return ao.(*Boolean).Value == b.Value @@ -275,3 +281,7 @@ func CompareObjects(ao, bo Object) bool { return false } + +func IsNumber(o Object) bool { + return o.Type() == INTEGER_OBJ || o.Type() == FLOAT_OBJ +} diff --git a/object/object_test.go b/object/object_test.go index 1a141d1..460cddb 100644 --- a/object/object_test.go +++ b/object/object_test.go @@ -31,6 +31,8 @@ func testInput(t *testing.T, tests []inputTestCase) { switch expected := tt.expected.(type) { case int: testIntegerObject(t, evaluated, int64(expected)) + case float64: + testFloatObject(t, evaluated, float64(expected)) case string: arrObj, ok := evaluated.(*object.Array) if ok { diff --git a/parser/parser.go b/parser/parser.go index dd83bcf..1c8c6ab 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -67,6 +67,7 @@ func New(l *lexer.Lexer, imports map[string]struct{}) *Parser { p.prefixParseFns = make(map[token.TokenType]prefixParseFn) p.registerPrefix(token.IDENT, p.parseIdentifier) p.registerPrefix(token.INT, p.parseIntegerLiteral) + p.registerPrefix(token.FLOAT, p.parseFloatLiteral) p.registerPrefix(token.BANG, p.parsePrefixExpression) p.registerPrefix(token.MINUS, p.parsePrefixExpression) p.registerPrefix(token.TRUE, p.parseBoolean) @@ -298,6 +299,21 @@ func (p *Parser) parseIntegerLiteral() ast.Expression { return lit } +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 float", p.curToken.Literal) + p.errors = append(p.errors, msg) + return nil + } + + lit.Value = value + + return lit +} + func (p *Parser) parsePrefixExpression() ast.Expression { expression := &ast.PrefixExpression{ Token: p.curToken, diff --git a/token/token.go b/token/token.go index 2283229..14c3284 100644 --- a/token/token.go +++ b/token/token.go @@ -13,6 +13,7 @@ const ( IDENT = "IDENT" // add, foobar, x, y INT = "INT" // 123456 + FLOAT = "FLOAT" // 123.456 STRING = "STRING" ASSIGN = "="