diff --git a/ast/number.go b/ast/number.go index 9503706..2e7d748 100644 --- a/ast/number.go +++ b/ast/number.go @@ -2,11 +2,12 @@ package ast import ( "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) type Number struct { ExpressionNode - Token token.Token - Value decimal.Decimal + Token token.Token + IntValue int64 + FloatValue float64 + IsFloat bool } diff --git a/evaluator/assign.go b/evaluator/assign.go index 32617aa..2061687 100644 --- a/evaluator/assign.go +++ b/evaluator/assign.go @@ -44,7 +44,7 @@ func evaluateIndexAssignment(node *ast.Index, assignmentValue object.Object, sco switch obj := left.(type) { case *object.List: - idx := int(index.(*object.Number).Value.IntPart()) + idx := int(index.(*object.Number).Int64()) elements := obj.Elements if idx < 0 { diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 1b74a4b..5786a92 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -552,8 +552,8 @@ func isNumberObject(t *testing.T, obj object.Object, expected int64) bool { return false } - if number.Value.IntPart() != expected { - t.Errorf("object has wrong value. got=%d, expected=%d", number.Value.IntPart(), expected) + if number.Int64() != expected { + t.Errorf("object has wrong value. got=%d, expected=%d", number.Int64(), expected) return false } diff --git a/evaluator/for_in.go b/evaluator/for_in.go index 642f2ba..0b44d4b 100644 --- a/evaluator/for_in.go +++ b/evaluator/for_in.go @@ -3,7 +3,6 @@ package evaluator import ( "ghostlang.org/x/ghost/ast" "ghostlang.org/x/ghost/object" - "github.com/shopspring/decimal" ) func evaluateForIn(node *ast.ForIn, scope *object.Scope) object.Object { @@ -33,7 +32,7 @@ func evaluateForIn(node *ast.ForIn, scope *object.Scope) object.Object { switch obj := iterable.(type) { case *object.List: for k, v := range obj.Elements { - scope.Environment.Set(node.Key.Value, &object.Number{Value: decimal.NewFromInt(int64(k))}) + scope.Environment.Set(node.Key.Value, object.NewInt(int64(k))) scope.Environment.Set(node.Value.Value, v) block := Evaluate(node.Block, scope) diff --git a/evaluator/index.go b/evaluator/index.go index c92685c..50df88e 100644 --- a/evaluator/index.go +++ b/evaluator/index.go @@ -33,7 +33,7 @@ func evaluateIndex(node *ast.Index, scope *object.Scope) object.Object { func evaluateListIndex(node *ast.Index, left, index object.Object) object.Object { list := left.(*object.List) - idx := index.(*object.Number).Value.IntPart() + idx := index.(*object.Number).Int64() max := int64(len(list.Elements) - 1) if idx < 0 || idx > max { @@ -63,7 +63,7 @@ func evaluateMapIndex(node *ast.Index, left, index object.Object) object.Object func evaluateStringIndex(node *ast.Index, left, index object.Object) object.Object { str := left.(*object.String) - idx := index.(*object.Number).Value.IntPart() + idx := index.(*object.Number).Int64() max := int64(len(str.Value) - 1) if idx < 0 || idx > max { diff --git a/evaluator/number.go b/evaluator/number.go index 2b00cb1..9a5f26d 100644 --- a/evaluator/number.go +++ b/evaluator/number.go @@ -3,57 +3,54 @@ package evaluator import ( "ghostlang.org/x/ghost/ast" "ghostlang.org/x/ghost/object" - "github.com/shopspring/decimal" ) func evaluateNumber(node *ast.Number, scope *object.Scope) object.Object { - return &object.Number{Value: node.Value} + if node.IsFloat { + return object.NewFloat(node.FloatValue) + } + return object.NewInt(node.IntValue) } func evaluateNumberInfix(node *ast.Infix, left object.Object, right object.Object) object.Object { - leftValue := left.(*object.Number).Value - rightValue := right.(*object.Number).Value + leftNum := left.(*object.Number) + rightNum := right.(*object.Number) switch node.Operator { case "+": - return &object.Number{Value: leftValue.Add(rightValue)} + return leftNum.Add(rightNum) case "-": - return &object.Number{Value: leftValue.Sub(rightValue)} + return leftNum.Sub(rightNum) case "*": - return &object.Number{Value: leftValue.Mul(rightValue)} + return leftNum.Mul(rightNum) case "/": - return &object.Number{Value: leftValue.Div(rightValue)} + return leftNum.Div(rightNum) case "%": - return &object.Number{Value: leftValue.Mod(rightValue)} + return leftNum.Mod(rightNum) case "<": - return toBooleanValue(leftValue.LessThan(rightValue)) + return toBooleanValue(leftNum.LessThan(rightNum)) case "<=": - return toBooleanValue(leftValue.LessThanOrEqual(rightValue)) + return toBooleanValue(leftNum.LessThanOrEqual(rightNum)) case ">": - return toBooleanValue(leftValue.GreaterThan(rightValue)) + return toBooleanValue(leftNum.GreaterThan(rightNum)) case ">=": - return toBooleanValue(leftValue.GreaterThanOrEqual(rightValue)) + return toBooleanValue(leftNum.GreaterThanOrEqual(rightNum)) case "==": - return toBooleanValue(leftValue.Equal(rightValue)) + return toBooleanValue(leftNum.Equal(rightNum)) case "!=": - return toBooleanValue(!leftValue.Equal(rightValue)) + return toBooleanValue(!leftNum.Equal(rightNum)) case "..": - numbers := make([]object.Object, 0) - one := decimal.NewFromInt(1) - number := leftValue + start := leftNum.Int64() + end := rightNum.Int64() - if leftValue.GreaterThan(rightValue) { - return &object.List{Elements: numbers} + if start > end { + return &object.List{Elements: []object.Object{}} } - for { - numbers = append(numbers, &object.Number{Value: number}) - - if number.GreaterThanOrEqual(rightValue) { - break - } + numbers := make([]object.Object, 0, end-start+1) - number = number.Add(one) + for i := start; i <= end; i++ { + numbers = append(numbers, object.NewInt(i)) } return &object.List{Elements: numbers} diff --git a/evaluator/postfix.go b/evaluator/postfix.go index 135bdee..d4f7fff 100644 --- a/evaluator/postfix.go +++ b/evaluator/postfix.go @@ -3,7 +3,6 @@ package evaluator import ( "ghostlang.org/x/ghost/ast" "ghostlang.org/x/ghost/object" - "github.com/shopspring/decimal" ) func evaluatePostfix(node *ast.Postfix, scope *object.Scope) object.Object { @@ -19,11 +18,7 @@ func evaluatePostfix(node *ast.Postfix, scope *object.Scope) object.Object { return newError("%d:%d:%s: runtime error: identifier is not a number: %s", node.Token.Line, node.Token.Column, node.Token.File, node.Token.Lexeme) } - one := decimal.NewFromInt(1) - - newValue := &object.Number{ - Value: value.(*object.Number).Value.Add(one), - } + newValue := value.(*object.Number).Increment() scope.Environment.Set(node.Token.Lexeme, newValue) @@ -39,11 +34,7 @@ func evaluatePostfix(node *ast.Postfix, scope *object.Scope) object.Object { return newError("%d:%d:%s: runtime error: identifier is not a number: %s", node.Token.Line, node.Token.Column, node.Token.File, node.Token.Lexeme) } - one := decimal.NewFromInt(1) - - newValue := &object.Number{ - Value: value.(*object.Number).Value.Sub(one), - } + newValue := value.(*object.Number).Decrement() scope.Environment.Set(node.Token.Lexeme, newValue) diff --git a/evaluator/prefix.go b/evaluator/prefix.go index e2fdbc3..d03ee5e 100644 --- a/evaluator/prefix.go +++ b/evaluator/prefix.go @@ -31,9 +31,7 @@ func evaluatePrefix(node *ast.Prefix, scope *object.Scope) object.Object { return newError("%d:%d:%s: runtime error: unknown operator: -%s", node.Token.Line, node.Token.Column, node.Token.File, right.Type()) } - numberValue := right.(*object.Number).Value.Neg() - - return &object.Number{Value: numberValue} + return right.(*object.Number).Neg() } return newError("%d:%d:%s: runtime error: unknown operator: %s%s", node.Token.Line, node.Token.Column, node.Token.File, node.Operator, right.Type()) diff --git a/go.mod b/go.mod index 36580b8..6a7f75e 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,6 @@ module ghostlang.org/x/ghost go 1.21.1 -require ( - github.com/peterh/liner v1.2.1 - github.com/shopspring/decimal v1.3.1 -) +require github.com/peterh/liner v1.2.1 require github.com/mattn/go-runewidth v0.0.3 // indirect diff --git a/go.sum b/go.sum index 7d9bd0b..83a7dd4 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,3 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/library/modules/json_test.go b/library/modules/json_test.go index 0293c9f..772cbbd 100644 --- a/library/modules/json_test.go +++ b/library/modules/json_test.go @@ -5,7 +5,6 @@ import ( "ghostlang.org/x/ghost/object" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) func TestJsonDecode(t *testing.T) { @@ -13,7 +12,7 @@ func TestJsonDecode(t *testing.T) { expected := &object.Map{Pairs: map[object.MapKey]object.MapPair{ (&object.String{Value: "name"}).MapKey(): {Key: &object.String{Value: "name"}, Value: &object.String{Value: "Kai"}}, - (&object.String{Value: "age"}).MapKey(): {Key: &object.String{Value: "age"}, Value: &object.Number{Value: decimal.NewFromInt(34)}}, + (&object.String{Value: "age"}).MapKey(): {Key: &object.String{Value: "age"}, Value: object.NewInt(34)}, }} result := jsonDecode(nil, token.Token{}, &object.String{Value: input}) @@ -26,7 +25,7 @@ func TestJsonDecode(t *testing.T) { func TestJsonEncode(t *testing.T) { input := &object.Map{Pairs: map[object.MapKey]object.MapPair{ (&object.String{Value: "name"}).MapKey(): {Key: &object.String{Value: "name"}, Value: &object.String{Value: "Kai"}}, - (&object.String{Value: "age"}).MapKey(): {Key: &object.String{Value: "age"}, Value: &object.Number{Value: decimal.NewFromInt(34)}}, + (&object.String{Value: "age"}).MapKey(): {Key: &object.String{Value: "age"}, Value: object.NewInt(34)}, }} expected := `{"age":34,"name":"Kai"}` diff --git a/library/modules/math.go b/library/modules/math.go index 29a52bd..eda9b40 100644 --- a/library/modules/math.go +++ b/library/modules/math.go @@ -1,9 +1,10 @@ package modules import ( + "math" + "ghostlang.org/x/ghost/object" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) var MathMethods = map[string]*object.LibraryFunction{} @@ -38,7 +39,7 @@ func mathAbs(scope *object.Scope, tok token.Token, args ...object.Object) object number := args[0].(*object.Number) - return &object.Number{Value: number.Value.Abs()} + return number.Abs() } // mathCos returns the cosine value of the referenced number. @@ -53,7 +54,7 @@ func mathCos(scope *object.Scope, tok token.Token, args ...object.Object) object number := args[0].(*object.Number) - return &object.Number{Value: number.Value.Cos()} + return number.Cos() } // mathisNegative returns true if the referenced number is negative. @@ -68,7 +69,7 @@ func mathIsNegative(scope *object.Scope, tok token.Token, args ...object.Object) number := args[0].(*object.Number) - return &object.Boolean{Value: number.Value.IsNegative()} + return &object.Boolean{Value: number.IsNeg()} } // mathisPositive returns true if the referenced number is positive. @@ -83,7 +84,7 @@ func mathIsPositive(scope *object.Scope, tok token.Token, args ...object.Object) number := args[0].(*object.Number) - return &object.Boolean{Value: number.Value.IsPositive()} + return &object.Boolean{Value: number.IsPos()} } // mathisZero returns true if the referenced number is zero. @@ -98,7 +99,7 @@ func mathIsZero(scope *object.Scope, tok token.Token, args ...object.Object) obj number := args[0].(*object.Number) - return &object.Boolean{Value: number.Value.IsZero()} + return &object.Boolean{Value: number.IsZero()} } // mathSin returns the sine value of the referenced number. @@ -113,7 +114,7 @@ func mathSin(scope *object.Scope, tok token.Token, args ...object.Object) object number := args[0].(*object.Number) - return &object.Number{Value: number.Value.Sin()} + return number.Sin() } // mathTan returns the tangent value of the referenced number. @@ -128,7 +129,7 @@ func mathTan(scope *object.Scope, tok token.Token, args ...object.Object) object number := args[0].(*object.Number) - return &object.Number{Value: number.Value.Tan()} + return number.Tan() } // mathMax returns the largest number of the referenced numbers. @@ -148,7 +149,7 @@ func mathMax(scope *object.Scope, tok token.Token, args ...object.Object) object number1 := args[0].(*object.Number) number2 := args[1].(*object.Number) - if number1.Value.GreaterThan(number2.Value) { + if number1.GreaterThan(number2) { return number1 } @@ -172,7 +173,7 @@ func mathMin(scope *object.Scope, tok token.Token, args ...object.Object) object number1 := args[0].(*object.Number) number2 := args[1].(*object.Number) - if number1.Value.LessThan(number2.Value) { + if number1.LessThan(number2) { return number1 } @@ -183,31 +184,23 @@ func mathMin(scope *object.Scope, tok token.Token, args ...object.Object) object // mathPi returns the value of π, othewise known as Pi. func mathPi(scope *object.Scope, tok token.Token) object.Object { - pi, _ := decimal.NewFromString("3.141592653589793") - - return &object.Number{Value: pi} + return object.NewFloat(math.Pi) } // mathE returns the value of e, otherwise known as Euler's number. func mathE(scope *object.Scope, tok token.Token) object.Object { - e, _ := decimal.NewFromString("2.718281828459045") - - return &object.Number{Value: e} + return object.NewFloat(math.E) } // mathTau returns the value of τ, otherwise known as Tau. Tau is a circle -// constant equal to 2π, the ratio of a circle’s circumference to its radius. +// constant equal to 2π, the ratio of a circle's circumference to its radius. func mathTau(scope *object.Scope, tok token.Token) object.Object { - tau, _ := decimal.NewFromString("6.283185307179586") - - return &object.Number{Value: tau} + return object.NewFloat(2 * math.Pi) } // mathEpsilon returns the value of ϵ, otherwise known as Epsilon. Epsilon // represents the difference between 1 and the smallest floating point number // greater than 1. func mathEpsilon(scope *object.Scope, tok token.Token) object.Object { - epsilon, _ := decimal.NewFromString("2.2204460492503130808472633361816E-16") - - return &object.Number{Value: epsilon} + return object.NewFloat(math.SmallestNonzeroFloat64) } diff --git a/library/modules/os.go b/library/modules/os.go index 7561f11..fa5cb17 100644 --- a/library/modules/os.go +++ b/library/modules/os.go @@ -8,7 +8,6 @@ import ( "ghostlang.org/x/ghost/object" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) var OsMethods = map[string]*object.LibraryFunction{} @@ -34,9 +33,7 @@ func osArgs(scope *object.Scope, tok token.Token, args ...object.Object) object. } func osClock(scope *object.Scope, tok token.Token, args ...object.Object) object.Object { - seconds := decimal.NewFromInt(time.Now().UnixNano()) - - return &object.Number{Value: seconds} + return object.NewInt(time.Now().UnixNano()) } func osExit(scope *object.Scope, tok token.Token, args ...object.Object) object.Object { @@ -70,7 +67,7 @@ func osExit(scope *object.Scope, tok token.Token, args ...object.Object) object. arg := args[0].(*object.Number) - os.Exit(int(arg.Value.IntPart())) + os.Exit(int(arg.Int64())) return arg } diff --git a/library/modules/random.go b/library/modules/random.go index 80016c3..2253658 100644 --- a/library/modules/random.go +++ b/library/modules/random.go @@ -6,7 +6,6 @@ import ( "ghostlang.org/x/ghost/object" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) var seed int64 @@ -31,11 +30,11 @@ func randomRandom(scope *object.Scope, tok token.Token, args ...object.Object) o max := float64(1) if len(args) > 0 { - max, _ = args[0].(*object.Number).Value.Float64() + max = args[0].(*object.Number).Float64() if len(args) > 1 { min = max - max, _ = args[1].(*object.Number).Value.Float64() + max = args[1].(*object.Number).Float64() } } @@ -47,7 +46,7 @@ func randomRandom(scope *object.Scope, tok token.Token, args ...object.Object) o number = randomizer.Float64() } - return &object.Number{Value: decimal.NewFromFloat(number)} + return object.NewFloat(number) } // randomSeed sets the referenced number as the seed for the pseudo-random @@ -55,7 +54,7 @@ func randomRandom(scope *object.Scope, tok token.Token, args ...object.Object) o // nano timestamp will be used. func randomSeed(scope *object.Scope, tok token.Token, args ...object.Object) object.Object { if len(args) == 1 && args[0].Type() == object.NUMBER { - seed = args[0].(*object.Number).Value.IntPart() + seed = args[0].(*object.Number).Int64() } else { seed = time.Now().UnixNano() } @@ -69,5 +68,5 @@ func randomSeed(scope *object.Scope, tok token.Token, args ...object.Object) obj // randomSeedProperty returns the current seed value used internally. func randomSeedProperty(scope *object.Scope, tok token.Token) object.Object { - return &object.Number{Value: decimal.NewFromInt(seed)} + return object.NewInt(seed) } diff --git a/library/modules/time.go b/library/modules/time.go index fbc4d4d..90ee8fe 100644 --- a/library/modules/time.go +++ b/library/modules/time.go @@ -5,7 +5,6 @@ import ( "ghostlang.org/x/ghost/object" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) var TimeMethods = map[string]*object.LibraryFunction{} @@ -29,90 +28,65 @@ func init() { func timeSleep(scope *object.Scope, tok token.Token, args ...object.Object) object.Object { if len(args) != 1 { - // TODO: error return nil } if args[0].Type() != object.NUMBER { - // TODO: error return nil } ms := args[0].(*object.Number) - time.Sleep(time.Duration(ms.Value.IntPart()) * time.Millisecond) + time.Sleep(time.Duration(ms.Int64()) * time.Millisecond) return nil } func timeNow(scope *object.Scope, tok token.Token, args ...object.Object) object.Object { if len(args) != 0 { - // TODO: error return nil } - unix := decimal.NewFromInt(time.Now().Unix()) - - return &object.Number{Value: unix} + return object.NewInt(time.Now().Unix()) } // properties func timeNanosecond(scope *object.Scope, tok token.Token) object.Object { - nanosecond := decimal.NewFromFloat(0.00001) - - return &object.Number{Value: nanosecond} + return object.NewFloat(0.00001) } func timeMicrosecond(scope *object.Scope, tok token.Token) object.Object { - microsecond := decimal.NewFromFloat(0.0001) - - return &object.Number{Value: microsecond} + return object.NewFloat(0.0001) } func timeMillisecond(scope *object.Scope, tok token.Token) object.Object { - millisecond := decimal.NewFromFloat(0.001) - - return &object.Number{Value: millisecond} + return object.NewFloat(0.001) } func timeSecond(scope *object.Scope, tok token.Token) object.Object { - second := decimal.NewFromInt(1) - - return &object.Number{Value: second} + return object.NewInt(1) } func timeMinute(scope *object.Scope, tok token.Token) object.Object { - minute := decimal.NewFromInt(60) - - return &object.Number{Value: minute} + return object.NewInt(60) } func timeHour(scope *object.Scope, tok token.Token) object.Object { - hour := decimal.NewFromInt(3600) - - return &object.Number{Value: hour} + return object.NewInt(3600) } func timeDay(scope *object.Scope, tok token.Token) object.Object { - day := decimal.NewFromInt(86400) - - return &object.Number{Value: day} + return object.NewInt(86400) } func timeWeek(scope *object.Scope, tok token.Token) object.Object { - week := decimal.NewFromInt(604800) - - return &object.Number{Value: week} + return object.NewInt(604800) } func timeMonth(scope *object.Scope, tok token.Token) object.Object { - month := decimal.NewFromInt(2592000) - - return &object.Number{Value: month} + return object.NewInt(2592000) } func timeYear(scope *object.Scope, tok token.Token) object.Object { - year := decimal.NewFromInt(31536000) - - return &object.Number{Value: year} + return object.NewInt(31536000) } diff --git a/object/list.go b/object/list.go index cd7e681..1c7db78 100644 --- a/object/list.go +++ b/object/list.go @@ -3,8 +3,6 @@ package object import ( "bytes" "strings" - - "github.com/shopspring/decimal" ) const LIST = "LIST" @@ -86,7 +84,7 @@ func (list *List) last(args []Object) (Object, bool) { } func (list *List) length(args []Object) (Object, bool) { - return &Number{Value: decimal.NewFromInt(int64(len(list.Elements)))}, true + return NewInt(int64(len(list.Elements))), true } func (list *List) pop(args []Object) (Object, bool) { @@ -110,7 +108,7 @@ func (list *List) push(args []Object) (Object, bool) { list.Elements = newElements - return &Number{Value: decimal.NewFromInt(int64(newLength))}, true + return NewInt(int64(newLength)), true } func (list *List) tail(args []Object) (Object, bool) { diff --git a/object/map.go b/object/map.go index 1eae1d1..efdce14 100644 --- a/object/map.go +++ b/object/map.go @@ -4,8 +4,6 @@ import ( "bytes" "fmt" "strings" - - "github.com/shopspring/decimal" ) const MAP = "MAP" @@ -61,8 +59,9 @@ func NewMap(values map[string]interface{}) *Map { switch val := value.(type) { case int: + pairValue = NewInt(int64(val)) case int64: - pairValue = &Number{Value: decimal.NewFromInt(int64(val))} + pairValue = NewInt(val) case string: pairValue = &String{Value: val} } diff --git a/object/number.go b/object/number.go index e35b607..2e2c29a 100644 --- a/object/number.go +++ b/object/number.go @@ -1,38 +1,83 @@ package object -import "github.com/shopspring/decimal" +import ( + "fmt" + "math" + "strconv" +) const NUMBER = "NUMBER" -// Number objects consist of a decimal value. +// Number objects represent numeric values using either int64 or float64 internally. +// Integer operations stay as int64 for speed and exactness. +// Float operations use float64. Division always promotes to float. type Number struct { - Value decimal.Decimal + i int64 + f float64 + isFloat bool +} + +// NewInt creates a new integer Number. +func NewInt(i int64) *Number { + return &Number{i: i} +} + +// NewFloat creates a new float Number. +func NewFloat(f float64) *Number { + return &Number{f: f, isFloat: true} +} + +// Int64 returns the integer value. If float, truncates. +func (n *Number) Int64() int64 { + if n.isFloat { + return int64(n.f) + } + return n.i +} + +// Float64 returns the float value. If integer, converts. +func (n *Number) Float64() float64 { + if n.isFloat { + return n.f + } + return float64(n.i) +} + +// IsFloat returns true if the number is a float. +func (n *Number) IsFloat() bool { + return n.isFloat } // String represents the number object's value as a string. -func (number *Number) String() string { - return number.Value.String() +func (n *Number) String() string { + if n.isFloat { + return strconv.FormatFloat(n.f, 'f', -1, 64) + } + return strconv.FormatInt(n.i, 10) } // Type returns the number object type. -func (number *Number) Type() Type { +func (n *Number) Type() Type { return NUMBER } // MapKey defines a unique hash value for use as a map key. -func (number *Number) MapKey() MapKey { - return MapKey{Type: number.Type(), Value: uint64(number.Value.IntPart())} +func (n *Number) MapKey() MapKey { + if n.isFloat { + return MapKey{Type: n.Type(), Value: math.Float64bits(n.f)} + } + return MapKey{Type: n.Type(), Value: uint64(n.i)} } // Method defines the set of methods available on number objects. -func (number *Number) Method(method string, args []Object) (Object, bool) { +func (n *Number) Method(method string, args []Object) (Object, bool) { switch method { case "round": - return number.round(args) + return n.round(args) case "floor": - return number.floor(args) + return n.floor(args) case "toString": - return number.toString(args) + return n.toString(args) } return nil, false @@ -41,24 +86,199 @@ func (number *Number) Method(method string, args []Object) (Object, bool) { // ============================================================================= // Object methods -func (number *Number) toString(args []Object) (Object, bool) { - return &String{Value: number.Value.String()}, true +func (n *Number) toString(args []Object) (Object, bool) { + return &String{Value: n.String()}, true } -func (number *Number) round(args []Object) (Object, bool) { - places := &Number{Value: decimal.NewFromInt(0)} +func (n *Number) round(args []Object) (Object, bool) { + places := int64(0) if len(args) == 1 { if args[0].Type() != NUMBER { return nil, false } - places = args[0].(*Number) + places = args[0].(*Number).Int64() + } + + if !n.isFloat { + return n, true + } + + if places == 0 { + return NewInt(int64(math.Round(n.f))), true + } + + shift := math.Pow(10, float64(places)) + return NewFloat(math.Round(n.f*shift) / shift), true +} + +func (n *Number) floor(args []Object) (Object, bool) { + if !n.isFloat { + return n, true + } + + return NewInt(int64(math.Floor(n.f))), true +} + +// ============================================================================= +// Arithmetic helpers for the evaluator + +// Add returns the sum of two numbers. +func (n *Number) Add(other *Number) *Number { + if n.isFloat || other.isFloat { + return NewFloat(n.Float64() + other.Float64()) + } + return NewInt(n.i + other.i) +} + +// Sub returns the difference of two numbers. +func (n *Number) Sub(other *Number) *Number { + if n.isFloat || other.isFloat { + return NewFloat(n.Float64() - other.Float64()) + } + return NewInt(n.i - other.i) +} + +// Mul returns the product of two numbers. +func (n *Number) Mul(other *Number) *Number { + if n.isFloat || other.isFloat { + return NewFloat(n.Float64() * other.Float64()) + } + return NewInt(n.i * other.i) +} + +// Div always returns a float. +func (n *Number) Div(other *Number) *Number { + return NewFloat(n.Float64() / other.Float64()) +} + +// Mod returns the modulo of two numbers. +func (n *Number) Mod(other *Number) *Number { + if n.isFloat || other.isFloat { + return NewFloat(math.Mod(n.Float64(), other.Float64())) + } + return NewInt(n.i % other.i) +} + +// Neg returns the negation. +func (n *Number) Neg() *Number { + if n.isFloat { + return NewFloat(-n.f) + } + return NewInt(-n.i) +} + +// LessThan compares two numbers. +func (n *Number) LessThan(other *Number) bool { + if n.isFloat || other.isFloat { + return n.Float64() < other.Float64() } + return n.i < other.i +} - return &Number{Value: number.Value.Round(int32(places.Value.IntPart()))}, true +// LessThanOrEqual compares two numbers. +func (n *Number) LessThanOrEqual(other *Number) bool { + if n.isFloat || other.isFloat { + return n.Float64() <= other.Float64() + } + return n.i <= other.i } -func (number *Number) floor(args []Object) (Object, bool) { - return &Number{Value: number.Value.Floor()}, true +// GreaterThan compares two numbers. +func (n *Number) GreaterThan(other *Number) bool { + if n.isFloat || other.isFloat { + return n.Float64() > other.Float64() + } + return n.i > other.i +} + +// GreaterThanOrEqual compares two numbers. +func (n *Number) GreaterThanOrEqual(other *Number) bool { + if n.isFloat || other.isFloat { + return n.Float64() >= other.Float64() + } + return n.i >= other.i +} + +// Equal compares two numbers. +func (n *Number) Equal(other *Number) bool { + if n.isFloat || other.isFloat { + return n.Float64() == other.Float64() + } + return n.i == other.i +} + +// Abs returns the absolute value. +func (n *Number) Abs() *Number { + if n.isFloat { + return NewFloat(math.Abs(n.f)) + } + if n.i < 0 { + return NewInt(-n.i) + } + return n +} + +// IsNegative returns true if the number is negative. +func (n *Number) IsNeg() bool { + if n.isFloat { + return n.f < 0 + } + return n.i < 0 +} + +// IsPositive returns true if the number is positive. +func (n *Number) IsPos() bool { + if n.isFloat { + return n.f > 0 + } + return n.i > 0 +} + +// IsZero returns true if the number is zero. +func (n *Number) IsZero() bool { + if n.isFloat { + return n.f == 0 + } + return n.i == 0 +} + +// Increment returns n + 1, preserving type. +func (n *Number) Increment() *Number { + if n.isFloat { + return NewFloat(n.f + 1) + } + return NewInt(n.i + 1) +} + +// Decrement returns n - 1, preserving type. +func (n *Number) Decrement() *Number { + if n.isFloat { + return NewFloat(n.f - 1) + } + return NewInt(n.i - 1) +} + +// Cos returns the cosine. +func (n *Number) Cos() *Number { + return NewFloat(math.Cos(n.Float64())) +} + +// Sin returns the sine. +func (n *Number) Sin() *Number { + return NewFloat(math.Sin(n.Float64())) +} + +// Tan returns the tangent. +func (n *Number) Tan() *Number { + return NewFloat(math.Tan(n.Float64())) +} + +// Stringer for debugging. +func (n *Number) GoString() string { + if n.isFloat { + return fmt.Sprintf("Number(float:%g)", n.f) + } + return fmt.Sprintf("Number(int:%d)", n.i) } diff --git a/object/object.go b/object/object.go index d554baa..7a494b9 100644 --- a/object/object.go +++ b/object/object.go @@ -3,7 +3,6 @@ package object import ( "ghostlang.org/x/ghost/ast" "ghostlang.org/x/ghost/token" - "github.com/shopspring/decimal" ) var evaluator func(node ast.Node, scope *Scope) Object @@ -50,11 +49,11 @@ func AnyValueToObject(val any) Object { case string: return &String{Value: v} case int: - return &Number{Value: decimal.NewFromInt(int64(v))} + return NewInt(int64(v)) case int64: - return &Number{Value: decimal.NewFromInt(int64(v))} + return NewInt(v) case float64: - return &Number{Value: decimal.NewFromFloat(v)} + return NewFloat(v) case nil: return &Null{} case []any: @@ -91,14 +90,10 @@ func ObjectToAnyValue(val Object) any { case *String: return string(v.Value) case *Number: - // Determine if value is an integer or float. - if v.Value.Exponent() <= 0 { - return int(v.Value.IntPart()) + if v.IsFloat() { + return v.Float64() } - - num, _ := v.Value.Float64() - - return num + return int(v.Int64()) case *Null: return nil case *List: diff --git a/object/string.go b/object/string.go index a36ed16..54f1088 100644 --- a/object/string.go +++ b/object/string.go @@ -4,10 +4,9 @@ import ( "fmt" "hash/fnv" "regexp" + "strconv" "strings" "unicode/utf8" - - "github.com/shopspring/decimal" ) const STRING = "STRING" @@ -120,9 +119,7 @@ func (str *String) endsWith(args []Object) (Object, bool) { } func (str *String) length(args []Object) (Object, bool) { - length := &Number{Value: decimal.NewFromInt(int64(utf8.RuneCountInString(str.Value)))} - - return length, true + return NewInt(int64(utf8.RuneCountInString(str.Value))), true } func (str *String) matches(args []Object) (Object, bool) { @@ -171,9 +168,19 @@ func (str *String) toString(args []Object) (Object, bool) { } func (str *String) toNumber(args []Object) (Object, bool) { - number, _ := decimal.NewFromString(str.Value) + // Try integer first, then float. + if !strings.ContainsAny(str.Value, ".eE") { + if i, err := strconv.ParseInt(str.Value, 10, 64); err == nil { + return NewInt(i), true + } + } + + f, err := strconv.ParseFloat(str.Value, 64) + if err != nil { + return NewInt(0), true + } - return &Number{Value: number}, true + return NewFloat(f), true } func (str *String) trim(args []Object) (Object, bool) { diff --git a/parser/number.go b/parser/number.go index 599ae0e..fe50b23 100644 --- a/parser/number.go +++ b/parser/number.go @@ -1,22 +1,38 @@ package parser import ( + "strconv" + "strings" + "ghostlang.org/x/ghost/ast" "ghostlang.org/x/ghost/log" - "github.com/shopspring/decimal" ) func (parser *Parser) numberLiteral() ast.ExpressionNode { number := &ast.Number{Token: parser.currentToken} + lexeme := parser.currentToken.Lexeme - value, err := decimal.NewFromString(parser.currentToken.Lexeme) + // If the lexeme contains a decimal point or scientific notation, parse as float. + if strings.ContainsAny(lexeme, ".e") { + value, err := strconv.ParseFloat(lexeme, 64) - if err != nil { - log.Error("%d:__: syntax error: could not parse %q as number", parser.currentToken.Line, parser.currentToken.Lexeme) - return nil - } + if err != nil { + log.Error("%d:__: syntax error: could not parse %q as number", parser.currentToken.Line, lexeme) + return nil + } + + number.FloatValue = value + number.IsFloat = true + } else { + value, err := strconv.ParseInt(lexeme, 10, 64) - number.Value = value + if err != nil { + log.Error("%d:__: syntax error: could not parse %q as number", parser.currentToken.Line, lexeme) + return nil + } + + number.IntValue = value + } return number } diff --git a/parser/parser_test.go b/parser/parser_test.go index 6cb9426..2f5accd 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -358,12 +358,14 @@ func TestInfixExpressions(t *testing.T) { func TestNumberLiteral(t *testing.T) { tests := []struct { - input string - expected string + input string + isFloat bool + intValue int64 + floatValue float64 }{ - {"5", "5"}, - {"3.14", "3.14"}, - {"5e10", "50000000000"}, + {"5", false, 5, 0}, + {"3.14", true, 0, 3.14}, + {"5e10", true, 0, 5e10}, } for _, tt := range tests { @@ -389,8 +391,18 @@ func TestNumberLiteral(t *testing.T) { t.Fatalf("statement is not ast.Number. got=%T", statement.Expression) } - if number.Value.String() != tt.expected { - t.Fatalf("number.Value is not '%s'. got=%s", tt.expected, number.Value.String()) + if number.IsFloat != tt.isFloat { + t.Fatalf("number.IsFloat is not '%t'. got=%t", tt.isFloat, number.IsFloat) + } + + if tt.isFloat { + if number.FloatValue != tt.floatValue { + t.Fatalf("number.FloatValue is not '%g'. got=%g", tt.floatValue, number.FloatValue) + } + } else { + if number.IntValue != tt.intValue { + t.Fatalf("number.IntValue is not '%d'. got=%d", tt.intValue, number.IntValue) + } } } } @@ -698,7 +710,7 @@ func TestMapLiteralsWithIntegerKeys(t *testing.T) { t.Errorf("key is not ast.Number. got=%T", key) } - expectedValue := expected[number.Value.IntPart()] + expectedValue := expected[number.IntValue] isNumberLiteral(t, value, expectedValue) } @@ -1016,8 +1028,8 @@ func isNumberLiteral(t *testing.T, expression ast.ExpressionNode, value int64) b t.Errorf("expression is not ast.Number. got=%T", expression) } - if number.Value.IntPart() != value { - t.Errorf("number.Value is not %d. got=%d", value, number.Value.IntPart()) + if number.IntValue != value { + t.Errorf("number.IntValue is not %d. got=%d", value, number.IntValue) } return true