pkg/math: fix issue 418
Fixes #418
Change-Id: I9f13a0d8e35c9d6f1a8c50fdc0d4c3884a336a8d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6921
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/core/convert/go.go b/internal/core/convert/go.go
index aea75da..067321d 100644
--- a/internal/core/convert/go.go
+++ b/internal/core/convert/go.go
@@ -275,8 +275,16 @@
return n
case *apd.Decimal:
- // TODO: set num or float based on value of number.
- n := &adt.Num{Src: ctx.Source(), K: adt.NumKind}
+ // TODO: should we allow an "int" bit to be set here? It is a bit
+ // tricky, as we would also need to pass down the result of rounding.
+ // So more likely an API must return explicitly whether a value is
+ // a float or an int after all.
+ // The code to autodetect whether something is an integer can be done
+ // with this:
+ // var d apd.Decimal
+ // res, _ := apd.BaseContext.RoundToIntegralExact(&d, v)
+ // integer := !res.Inexact()
+ n := &adt.Num{Src: ctx.Source(), K: adt.FloatKind}
n.X = *v
return n
diff --git a/pkg/list/testdata/gen.txtar b/pkg/list/testdata/gen.txtar
index 2061db7..06f6daa 100644
--- a/pkg/list/testdata/gen.txtar
+++ b/pkg/list/testdata/gen.txtar
@@ -93,7 +93,7 @@
Result:
(_|_){
// [eval]
- t1: (number){ 2.5 }
+ t1: (float){ 2.5 }
t2: (_|_){
// [eval] error in call to list.Avg: empty list
}
@@ -161,7 +161,7 @@
// [eval] t14: cannot use "foo" (type string) as int in argument 2 to list.FlattenN:
// ./in.cue:16:24
}
- t15: (number){ 4 }
+ t15: (float){ 4 }
t16: (_|_){
// [eval] error in call to list.Max: empty list
}
@@ -169,7 +169,7 @@
// [eval] t17: cannot use "foo" (type string) as list in argument 1 to list.Max:
// ./in.cue:19:15
}
- t18: (number){ 1 }
+ t18: (float){ 1 }
t19: (_|_){
// [eval] error in call to list.Min: empty list
}
@@ -177,8 +177,8 @@
// [eval] t20: cannot use "foo" (type string) as list in argument 1 to list.Min:
// ./in.cue:22:15
}
- t21: (number){ 24 }
- t22: (number){ 1 }
+ t21: (float){ 24 }
+ t22: (float){ 1 }
t23: (_|_){
// [eval] t23: cannot use "foo" (type string) as list in argument 1 to list.Product:
// ./in.cue:25:19
@@ -193,38 +193,38 @@
// [eval] error in call to list.Range: end must be less than start when step is negative
}
t27: (#list){
- 0: (number){ 0 }
- 1: (number){ 1 }
- 2: (number){ 2 }
- 3: (number){ 3 }
- 4: (number){ 4 }
+ 0: (float){ 0 }
+ 1: (float){ 1 }
+ 2: (float){ 2 }
+ 3: (float){ 3 }
+ 4: (float){ 4 }
}
t28: (#list){
- 0: (number){ 0 }
+ 0: (float){ 0 }
}
t29: (#list){
- 0: (number){ 0 }
- 1: (number){ 2 }
- 2: (number){ 4 }
+ 0: (float){ 0 }
+ 1: (float){ 2 }
+ 2: (float){ 4 }
}
t30: (#list){
- 0: (number){ 5 }
- 1: (number){ 4 }
- 2: (number){ 3 }
- 3: (number){ 2 }
- 4: (number){ 1 }
+ 0: (float){ 5 }
+ 1: (float){ 4 }
+ 2: (float){ 3 }
+ 3: (float){ 2 }
+ 4: (float){ 1 }
}
t31: (#list){
- 0: (number){ 0 }
- 1: (number){ 0.5 }
- 2: (number){ 1.0 }
- 3: (number){ 1.5 }
- 4: (number){ 2.0 }
- 5: (number){ 2.5 }
- 6: (number){ 3.0 }
- 7: (number){ 3.5 }
- 8: (number){ 4.0 }
- 9: (number){ 4.5 }
+ 0: (float){ 0 }
+ 1: (float){ 0.5 }
+ 2: (float){ 1.0 }
+ 3: (float){ 1.5 }
+ 4: (float){ 2.0 }
+ 5: (float){ 2.5 }
+ 6: (float){ 3.0 }
+ 7: (float){ 3.5 }
+ 8: (float){ 4.0 }
+ 9: (float){ 4.5 }
}
t32: (#list){
0: (int){ 2 }
@@ -274,8 +274,8 @@
t42: (_|_){
// [eval] 0: error in call to list.SortStrings: element 0 of list argument 0: cannot use value 1 (type int) as string
}
- t43: (number){ 10 }
- t44: (number){ 0 }
+ t43: (float){ 10 }
+ t44: (float){ 0 }
t45: (_|_){
// [eval] t45: cannot use "foo" (type string) as list in argument 1 to list.Sum:
// ./in.cue:51:15
diff --git a/pkg/math/manual.go b/pkg/math/manual.go
index 80e3cf7..dd77ddd 100644
--- a/pkg/math/manual.go
+++ b/pkg/math/manual.go
@@ -15,21 +15,42 @@
package math
import (
+ "math/big"
+
"github.com/cockroachdb/apd/v2"
"cuelang.org/go/internal"
)
+func roundContext(rounder string) *apd.Context {
+ c := *apdContext
+ c.Rounding = rounder
+ return &c
+}
+
+// TODO: for now we convert Decimals to int. This allows the desired type to be
+// conveyed. This has the disadvantage tht a number like 1E10000 will need to be
+// expanded. Eventually it would be better to to unify number types and allow
+// anything that results in an integer to pose as an integer type.
+func toInt(d *internal.Decimal) *big.Int {
+ i := &d.Coeff
+ if d.Negative {
+ i.Neg(i)
+ }
+ return i
+}
+
// Floor returns the greatest integer value less than or equal to x.
//
// Special cases are:
// Floor(±0) = ±0
// Floor(±Inf) = ±Inf
// Floor(NaN) = NaN
-func Floor(x *internal.Decimal) (*internal.Decimal, error) {
+func Floor(x *internal.Decimal) (*big.Int, error) {
var d internal.Decimal
_, err := apdContext.Floor(&d, x)
- return &d, err
+ _, _ = apdContext.Quantize(&d, &d, 0)
+ return toInt(&d), err
}
// Ceil returns the least integer value greater than or equal to x.
@@ -38,13 +59,14 @@
// Ceil(±0) = ±0
// Ceil(±Inf) = ±Inf
// Ceil(NaN) = NaN
-func Ceil(x *internal.Decimal) (*internal.Decimal, error) {
+func Ceil(x *internal.Decimal) (*big.Int, error) {
var d internal.Decimal
_, err := apdContext.Ceil(&d, x)
- return &d, err
+ _, _ = apdContext.Quantize(&d, &d, 0)
+ return toInt(&d), err
}
-var roundTruncContext = apd.Context{Rounding: apd.RoundDown}
+var roundTruncContext = roundContext(apd.RoundDown)
// Trunc returns the integer value of x.
//
@@ -52,13 +74,13 @@
// Trunc(±0) = ±0
// Trunc(±Inf) = ±Inf
// Trunc(NaN) = NaN
-func Trunc(x *internal.Decimal) (*internal.Decimal, error) {
+func Trunc(x *internal.Decimal) (*big.Int, error) {
var d internal.Decimal
_, err := roundTruncContext.RoundToIntegralExact(&d, x)
- return &d, err
+ return toInt(&d), err
}
-var roundUpContext = apd.Context{Rounding: apd.RoundHalfUp}
+var roundUpContext = roundContext(apd.RoundHalfUp)
// Round returns the nearest integer, rounding half away from zero.
//
@@ -66,13 +88,13 @@
// Round(±0) = ±0
// Round(±Inf) = ±Inf
// Round(NaN) = NaN
-func Round(x *internal.Decimal) (*internal.Decimal, error) {
+func Round(x *internal.Decimal) (*big.Int, error) {
var d internal.Decimal
_, err := roundUpContext.RoundToIntegralExact(&d, x)
- return &d, err
+ return toInt(&d), err
}
-var roundEvenContext = apd.Context{Rounding: apd.RoundHalfEven}
+var roundEvenContext = roundContext(apd.RoundHalfEven)
// RoundToEven returns the nearest integer, rounding ties to even.
//
@@ -80,10 +102,10 @@
// RoundToEven(±0) = ±0
// RoundToEven(±Inf) = ±Inf
// RoundToEven(NaN) = NaN
-func RoundToEven(x *internal.Decimal) (*internal.Decimal, error) {
+func RoundToEven(x *internal.Decimal) (*big.Int, error) {
var d internal.Decimal
_, err := roundEvenContext.RoundToIntegralExact(&d, x)
- return &d, err
+ return toInt(&d), err
}
var mulContext = apd.BaseContext.WithPrecision(1)
diff --git a/pkg/math/pkg.go b/pkg/math/pkg.go
index c079314..4c2c086 100644
--- a/pkg/math/pkg.go
+++ b/pkg/math/pkg.go
@@ -69,7 +69,7 @@
}, {
Name: "Floor",
Params: []adt.Kind{adt.NumKind},
- Result: adt.NumKind,
+ Result: adt.IntKind,
Func: func(c *internal.CallCtxt) {
x := c.Decimal(0)
if c.Do() {
@@ -79,7 +79,7 @@
}, {
Name: "Ceil",
Params: []adt.Kind{adt.NumKind},
- Result: adt.NumKind,
+ Result: adt.IntKind,
Func: func(c *internal.CallCtxt) {
x := c.Decimal(0)
if c.Do() {
@@ -89,7 +89,7 @@
}, {
Name: "Trunc",
Params: []adt.Kind{adt.NumKind},
- Result: adt.NumKind,
+ Result: adt.IntKind,
Func: func(c *internal.CallCtxt) {
x := c.Decimal(0)
if c.Do() {
@@ -99,7 +99,7 @@
}, {
Name: "Round",
Params: []adt.Kind{adt.NumKind},
- Result: adt.NumKind,
+ Result: adt.IntKind,
Func: func(c *internal.CallCtxt) {
x := c.Decimal(0)
if c.Do() {
@@ -109,7 +109,7 @@
}, {
Name: "RoundToEven",
Params: []adt.Kind{adt.NumKind},
- Result: adt.NumKind,
+ Result: adt.IntKind,
Func: func(c *internal.CallCtxt) {
x := c.Decimal(0)
if c.Do() {
diff --git a/pkg/math/testdata/gen.txtar b/pkg/math/testdata/gen.txtar
index aa29913..6f449bf 100644
--- a/pkg/math/testdata/gen.txtar
+++ b/pkg/math/testdata/gen.txtar
@@ -6,25 +6,12 @@
t1: math.Pi
t2: math.Floor(math.Pi)
t3: math.Pi(3)
-t4: math.Floor(3, 5)
-t5: math.Floor("foo")
t6: math.Jacobi(1000, 2000)
t7: math.Jacobi(1000, 201)
t8: math.Asin(2.0e400)
-t9: math.MultipleOf(4, 2)
-t10: math.MultipleOf(5, 2)
-t11: math.MultipleOf(5, 0)
-t12: math.MultipleOf(100, 1.00001)
-t13: math.MultipleOf(1, 1)
-t14: math.MultipleOf(5, 2.5)
-t15: math.MultipleOf(100e100, 10)
t16: math.Pow(8, 4)
t17: math.Pow10(4)
t18: math.Signbit(-4)
-t19: math.Round(2.5)
-t20: math.Round(-2.5)
-t21: math.RoundToEven(2.5)
-t22: math.RoundToEven(-2.5)
t23: math.Abs(2.5)
t24: math.Abs(-2.2)
t25: math.Cbrt(2)
@@ -36,81 +23,43 @@
t31: math.Log2(5)
t32: math.Dim(3, 2.5)
t33: math.Dim(5, 7.2)
-t34: math.Ceil(2.5)
-t35: math.Ceil(-2.2)
-t36: math.Floor(2.9)
-t37: math.Floor(-2.2)
-t38: math.Trunc(2.5)
-t39: math.Trunc(-2.9)
-- out/math --
Errors:
error in call to math.Jacobi: big: invalid 2nd argument to Int.Jacobi: need odd integer but got 2000
-error in call to math.MultipleOf: division by zero
t3: cannot call non-function math.Pi (type float):
./in.cue:5:5
-t4: too many arguments in call to math.Floor (have 2, want 1):
- ./in.cue:6:20
-t5: cannot use "foo" (type string) as number in argument 1 to math.Floor:
- ./in.cue:7:16
cannot use 2.0E+400 (type float) as float64 in argument 0 to math.Asin: value was rounded up:
- ./in.cue:10:5
+ ./in.cue:8:5
Result:
(_|_){
// [eval]
t1: (float){ 3.14159265358979323846264338327950288419716939937510582097494459 }
- t2: (number){ 3 }
+ t2: (int){ 3 }
t3: (_|_){
// [eval] t3: cannot call non-function math.Pi (type float):
// ./in.cue:5:5
}
- t4: (_|_){
- // [eval] t4: too many arguments in call to math.Floor (have 2, want 1):
- // ./in.cue:6:20
- }
- t5: (_|_){
- // [eval] t5: cannot use "foo" (type string) as number in argument 1 to math.Floor:
- // ./in.cue:7:16
- }
t6: (_|_){
// [eval] error in call to math.Jacobi: big: invalid 2nd argument to Int.Jacobi: need odd integer but got 2000
}
t7: (int){ 1 }
t8: (_|_){
// [eval] cannot use 2.0E+400 (type float) as float64 in argument 0 to math.Asin: value was rounded up:
- // ./in.cue:10:5
+ // ./in.cue:8:5
}
- t9: (bool){ true }
- t10: (bool){ false }
- t11: (_|_){
- // [eval] error in call to math.MultipleOf: division by zero
- }
- t12: (bool){ false }
- t13: (bool){ true }
- t14: (bool){ true }
- t15: (bool){ true }
- t16: (number){ 4096 }
- t17: (number){ 1E+4 }
+ t16: (float){ 4096 }
+ t17: (float){ 1E+4 }
t18: (bool){ true }
- t19: (number){ 3 }
- t20: (number){ -3 }
- t21: (number){ 2 }
- t22: (number){ -2 }
- t23: (number){ 2.5 }
- t24: (number){ 2.2 }
- t25: (number){ 1.25992104989487316476721 }
- t26: (number){ -5 }
- t27: (number){ 20.0855369231876677409285 }
- t28: (number){ 11.3137084989847603904135 }
- t29: (number){ 1.38629436111989061883446 }
- t30: (number){ 0.602059991327962390427478 }
- t31: (number){ 2.32192809488736234787032 }
- t32: (number){ 0.5 }
- t33: (number){ 0 }
- t34: (number){ 3 }
- t35: (number){ -2 }
- t36: (number){ 2 }
- t37: (number){ -3 }
- t38: (number){ 2 }
- t39: (number){ -2 }
+ t23: (float){ 2.5 }
+ t24: (float){ 2.2 }
+ t25: (float){ 1.25992104989487316476721 }
+ t26: (float){ -5 }
+ t27: (float){ 20.0855369231876677409285 }
+ t28: (float){ 11.3137084989847603904135 }
+ t29: (float){ 1.38629436111989061883446 }
+ t30: (float){ 0.602059991327962390427478 }
+ t31: (float){ 2.32192809488736234787032 }
+ t32: (float){ 0.5 }
+ t33: (float){ 0 }
}
diff --git a/pkg/math/testdata/issue418.txtar b/pkg/math/testdata/issue418.txtar
new file mode 100644
index 0000000..fba9bb9
--- /dev/null
+++ b/pkg/math/testdata/issue418.txtar
@@ -0,0 +1,33 @@
+-- in.cue --
+import "math"
+
+a: {
+ x: 32.45
+ y: int
+ y: math.Round(x)
+}
+
+b: {
+ x: 32.45
+ y: int
+ y: math.Log(x)
+}
+-- out/math --
+Errors:
+b.y: conflicting values int and 3.47970044315009900124277 (mismatched types int and float)
+
+Result:
+(_|_){
+ // [eval]
+ a: (struct){
+ x: (float){ 32.45 }
+ y: (int){ 32 }
+ }
+ b: (_|_){
+ // [eval]
+ x: (float){ 32.45 }
+ y: (_|_){
+ // [eval] b.y: conflicting values int and 3.47970044315009900124277 (mismatched types int and float)
+ }
+ }
+}
diff --git a/pkg/math/testdata/round.txtar b/pkg/math/testdata/round.txtar
new file mode 100644
index 0000000..b1eda54
--- /dev/null
+++ b/pkg/math/testdata/round.txtar
@@ -0,0 +1,76 @@
+# generated from the original tests.
+# Henceforth it may be nicer to group tests into separate files.
+-- in.cue --
+import "math"
+
+mul0: math.MultipleOf(4, 2)
+mul1: math.MultipleOf(5, 2)
+// TODO(errors): ensure path is included for the following error.
+mul2: math.MultipleOf(5, 0)
+mul3: math.MultipleOf(100, 1.00001)
+mul4: math.MultipleOf(1, 1)
+mul5: math.MultipleOf(5, 2.5)
+mul6: math.MultipleOf(100e100, 10)
+
+r0: math.Round(2.5)
+r1: math.Round(-2.5)
+r2: math.RoundToEven(2.5)
+r3: math.RoundToEven(-2.5)
+
+floorE1: math.Floor(3, 5)
+floorE2: math.Floor("foo")
+
+floor0: math.Floor(math.Pi)
+floor1: math.Floor(2.9)
+floor2: math.Floor(-2.2)
+floor3: math.Floor(2900)
+floor4: math.Floor(29E2)
+
+ceil0: math.Ceil(2.5)
+ceil1: math.Ceil(-2.2)
+
+trunc0: math.Trunc(2.5)
+trunc1: math.Trunc(-2.9)
+
+-- out/math --
+Errors:
+error in call to math.MultipleOf: division by zero
+floorE1: too many arguments in call to math.Floor (have 2, want 1):
+ ./in.cue:17:25
+floorE2: cannot use "foo" (type string) as number in argument 1 to math.Floor:
+ ./in.cue:18:21
+
+Result:
+(_|_){
+ // [eval]
+ mul0: (bool){ true }
+ mul1: (bool){ false }
+ mul2: (_|_){
+ // [eval] error in call to math.MultipleOf: division by zero
+ }
+ mul3: (bool){ false }
+ mul4: (bool){ true }
+ mul5: (bool){ true }
+ mul6: (bool){ true }
+ r0: (int){ 3 }
+ r1: (int){ -3 }
+ r2: (int){ 2 }
+ r3: (int){ -2 }
+ floorE1: (_|_){
+ // [eval] floorE1: too many arguments in call to math.Floor (have 2, want 1):
+ // ./in.cue:17:25
+ }
+ floorE2: (_|_){
+ // [eval] floorE2: cannot use "foo" (type string) as number in argument 1 to math.Floor:
+ // ./in.cue:18:21
+ }
+ floor0: (int){ 3 }
+ floor1: (int){ 2 }
+ floor2: (int){ -3 }
+ floor3: (int){ 2900 }
+ floor4: (int){ 2900 }
+ ceil0: (int){ 3 }
+ ceil1: (int){ -2 }
+ trunc0: (int){ 2 }
+ trunc1: (int){ -2 }
+}