Skip to content

Commit

Permalink
implement type float (#43)
Browse files Browse the repository at this point in the history
* implement type float

* object/object: extend CompareObjects by FLOAT_OBJ

* evaluator: make evalInfixExpression for numbers more predictable

* object: add more tests for number types

* add float docs

Signed-off-by: Flipez <code@brauser.io>

* catch division by zero

Signed-off-by: Flipez <code@brauser.io>

Co-authored-by: Markus Freitag <fmarkus@mailbox.org>
  • Loading branch information
Flipez and MarkusFreitag authored Jan 16, 2022
1 parent 520d817 commit 119d400
Show file tree
Hide file tree
Showing 15 changed files with 547 additions and 4 deletions.
9 changes: 9 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions docs/content/en/docs/literals/float.md
Original file line number Diff line number Diff line change
@@ -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()
```
17 changes: 17 additions & 0 deletions docs/content/en/docs/literals/integer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 4 additions & 0 deletions docs/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
55 changes: 53 additions & 2 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
}

Expand Down
57 changes: 57 additions & 0 deletions object/float.go
Original file line number Diff line number Diff line change
@@ -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)
}
54 changes: 54 additions & 0 deletions object/float_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
20 changes: 20 additions & 0 deletions object/integer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
}
4 changes: 3 additions & 1 deletion object/integer_test.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"},
Expand Down
Loading

1 comment on commit 119d400

@vercel
Copy link

@vercel vercel bot commented on 119d400 Jan 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.