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 }
+}