diff --git a/interpreter/fixedpoint_test.go b/interpreter/fixedpoint_test.go index e1093f19b5..2849c3418b 100644 --- a/interpreter/fixedpoint_test.go +++ b/interpreter/fixedpoint_test.go @@ -24,8 +24,10 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/onflow/cadence/ast" "github.com/onflow/cadence/fixedpoint" "github.com/onflow/cadence/interpreter" "github.com/onflow/cadence/sema" @@ -730,3 +732,183 @@ func TestInterpretStringFixedPointConversion(t *testing.T) { } } + +func TestInterpretFixedPointLeastSignificantDecimalHandling(t *testing.T) { + t.Parallel() + + type testValue[T interpreter.Value] struct { + operation ast.Operation + a, b T + result T + } + + test := func(tt *testing.T, typ sema.Type, a, b, expectedResult interpreter.Value, operation ast.Operation) { + testName := fmt.Sprintf("%s (%s%s%s)", + typ, + a, + operation.Symbol(), + b, + ) + + tt.Run(testName, func(ttt *testing.T) { + ttt.Parallel() + + code := fmt.Sprintf(` + fun main(): %[1]s { + return %[2]s %[4]s %[3]s + }`, + typ, + a, + b, + operation.Symbol(), + ) + + invokable := parseCheckAndPrepare(ttt, code) + + result, err := invokable.Invoke("main") + require.NoError(t, err) + + assert.Equal( + ttt, + expectedResult, + result, + ) + }) + } + + t.Run("unsigned", func(t *testing.T) { + t.Parallel() + + testCases := map[sema.Type][]testValue[interpreter.UFix64Value]{ + sema.UFix64Type: { + { + a: interpreter.NewUnmeteredUFix64Value(4560000000), + b: interpreter.NewUnmeteredUFix64Value(1), + operation: ast.OperationMul, + result: interpreter.NewUnmeteredUFix64Value(45), + }, + { + a: interpreter.NewUnmeteredUFix64Value(6), + b: interpreter.NewUnmeteredUFix64Value(1), + operation: ast.OperationMul, + result: interpreter.NewUnmeteredUFix64Value(0), + }, + { + a: interpreter.NewUnmeteredUFix64Value(456), + b: interpreter.NewUnmeteredUFix64Value(1000000000), + operation: ast.OperationDiv, + result: interpreter.NewUnmeteredUFix64Value(45), + }, + { + a: interpreter.NewUnmeteredUFix64Value(6), + b: interpreter.NewUnmeteredUFix64Value(1000000000), + operation: ast.OperationDiv, + result: interpreter.NewUnmeteredUFix64Value(0), + }, + }, + + // TODO: UFix128 + } + + for typ, testValues := range testCases { + typ := typ + + for _, testCase := range testValues { + test( + t, + typ, + testCase.a, + testCase.b, + testCase.result, + testCase.operation, + ) + } + } + }) + + t.Run("signed", func(t *testing.T) { + t.Parallel() + + testCases := map[sema.Type][]testValue[interpreter.Fix64Value]{ + sema.Fix64Type: { + { + a: interpreter.NewUnmeteredFix64Value(4560000000), + b: interpreter.NewUnmeteredFix64Value(1), + operation: ast.OperationMul, + result: interpreter.NewUnmeteredFix64Value(45), + }, + { + a: interpreter.NewUnmeteredFix64Value(6), + b: interpreter.NewUnmeteredFix64Value(1), + operation: ast.OperationMul, + result: interpreter.NewUnmeteredFix64Value(0), + }, + { + a: interpreter.NewUnmeteredFix64Value(456), + b: interpreter.NewUnmeteredFix64Value(1000000000), + operation: ast.OperationDiv, + result: interpreter.NewUnmeteredFix64Value(45), + }, + { + a: interpreter.NewUnmeteredFix64Value(6), + b: interpreter.NewUnmeteredFix64Value(1000000000), + operation: ast.OperationDiv, + result: interpreter.NewUnmeteredFix64Value(0), + }, + }, + + // TODO: Fix128 + } + + for typ, testValues := range testCases { + typ := typ + + for _, testCase := range testValues { + a := testCase.a + b := testCase.b + operation := testCase.operation + result := testCase.result + + // Both `a` and `b` are positive. + test( + t, + typ, + a, + b, + result, + operation, + ) + + // Both `a` and `b` are negative. + test( + t, + typ, + a.Negate(nil, interpreter.EmptyLocationRange), + b.Negate(nil, interpreter.EmptyLocationRange), + result, + operation, + ) + + // `a` is positive and `b` is negative. + test( + t, + typ, + a, + b.Negate(nil, interpreter.EmptyLocationRange), + result.Negate(nil, interpreter.EmptyLocationRange), + operation, + ) + + // `a` is negative and `b` is positive. + test( + t, + typ, + a.Negate(nil, interpreter.EmptyLocationRange), + b, + result.Negate(nil, interpreter.EmptyLocationRange), + operation, + ) + } + } + }) +} diff --git a/interpreter/integers_test.go b/interpreter/integers_test.go index 4c092cc882..9023b100a5 100644 --- a/interpreter/integers_test.go +++ b/interpreter/integers_test.go @@ -995,3 +995,212 @@ func TestInterpretStringIntegerConversion(t *testing.T) { t.Run(typ.String(), func(t *testing.T) { test(t, typ) }) } } + +func TestInterpretIntegerDivisionLeastSignificantDecimalHandling(t *testing.T) { + t.Parallel() + + type testCase struct { + typ sema.Type + + // Accept int8, so that this value is representable in all integer types. + valueConstructor func(int8) interpreter.Value + } + + test := func( + tt *testing.T, + typ sema.Type, + a, b, expectedResult int8, + valueConstructor func(int8) interpreter.Value, + ) { + + testName := fmt.Sprintf("%s (%d/%d=%d)", + typ, + a, + b, + expectedResult, + ) + + tt.Run(testName, func(ttt *testing.T) { + ttt.Parallel() + + code := fmt.Sprintf(` + fun main(): %[1]s { + return %[2]d / %[3]d + }`, + typ, + a, + b, + ) + + invokable := parseCheckAndPrepare(ttt, code) + + result, err := invokable.Invoke("main") + require.NoError(t, err) + + expectedResultValue := valueConstructor(expectedResult) + + assert.Equal( + ttt, + expectedResultValue, + result, + ) + }) + } + + a := int8(7) + b := int8(2) + expectedResult := int8(3) + + t.Run("unsigned", func(t *testing.T) { + t.Parallel() + + testCases := []testCase{ + { + typ: sema.UIntType, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUIntValueFromUint64(uint64(value)) + }, + }, + { + typ: sema.UInt8Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt8Value(uint8(value)) + }, + }, + { + typ: sema.UInt16Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt16Value(uint16(value)) + }, + }, + { + typ: sema.UInt32Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt32Value(uint32(value)) + }, + }, + { + typ: sema.UInt64Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt64Value(uint64(value)) + }, + }, + { + typ: sema.UInt128Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt128ValueFromUint64(uint64(value)) + }, + }, + { + typ: sema.UInt256Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredUInt256ValueFromUint64(uint64(value)) + }, + }, + } + + for _, testCase := range testCases { + + test( + t, + testCase.typ, + a, + b, + expectedResult, + testCase.valueConstructor, + ) + } + }) + + t.Run("signed", func(t *testing.T) { + t.Parallel() + + testCases := []testCase{ + { + typ: sema.IntType, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredIntValueFromInt64(int64(value)) + }, + }, + { + typ: sema.Int8Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt8Value(value) + }, + }, + { + typ: sema.Int16Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt16Value(int16(value)) + }, + }, + { + typ: sema.Int32Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt32Value(int32(value)) + }, + }, + { + typ: sema.Int64Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt64Value(int64(value)) + }, + }, + { + typ: sema.Int128Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt128ValueFromInt64(int64(value)) + }, + }, + { + typ: sema.Int256Type, + valueConstructor: func(value int8) interpreter.Value { + return interpreter.NewUnmeteredInt256ValueFromInt64(int64(value)) + }, + }, + } + + for _, testCase := range testCases { + + // Both `a` and `b` are positive. + test( + t, + testCase.typ, + a, + b, + expectedResult, + testCase.valueConstructor, + ) + + // Both `a` and `b` are negative. + test( + t, + testCase.typ, + -a, + -b, + expectedResult, + testCase.valueConstructor, + ) + + // `a` is positive and `b` is negative. + test( + t, + testCase.typ, + a, + -b, + -expectedResult, + testCase.valueConstructor, + ) + + // `a` is negative and `b` is positive. + test( + t, + testCase.typ, + -a, + b, + -expectedResult, + testCase.valueConstructor, + ) + } + }) +} diff --git a/interpreter/value_fix64.go b/interpreter/value_fix64.go index 8d5702c6ad..725fdc6422 100644 --- a/interpreter/value_fix64.go +++ b/interpreter/value_fix64.go @@ -264,7 +264,7 @@ func (v Fix64Value) Mul(context NumberValueArithmeticContext, other NumberValue, valueGetter := func() int64 { result := new(big.Int).Mul(a, b) - result.Div(result, sema.Fix64FactorBig) + result.Quo(result, sema.Fix64FactorBig) if result.Cmp(minInt64Big) < 0 { panic(&UnderflowError{ @@ -298,7 +298,7 @@ func (v Fix64Value) SaturatingMul(context NumberValueArithmeticContext, other Nu valueGetter := func() int64 { result := new(big.Int).Mul(a, b) - result.Div(result, sema.Fix64FactorBig) + result.Quo(result, sema.Fix64FactorBig) if result.Cmp(minInt64Big) < 0 { return math.MinInt64 @@ -328,7 +328,7 @@ func (v Fix64Value) Div(context NumberValueArithmeticContext, other NumberValue, valueGetter := func() int64 { result := new(big.Int).Mul(a, sema.Fix64FactorBig) - result.Div(result, b) + result.Quo(result, b) if result.Cmp(minInt64Big) < 0 { panic(&UnderflowError{ @@ -362,7 +362,7 @@ func (v Fix64Value) SaturatingDiv(context NumberValueArithmeticContext, other Nu valueGetter := func() int64 { result := new(big.Int).Mul(a, sema.Fix64FactorBig) - result.Div(result, b) + result.Quo(result, b) if result.Cmp(minInt64Big) < 0 { return math.MinInt64 diff --git a/interpreter/value_int128.go b/interpreter/value_int128.go index 3bede23132..8b2300c13c 100644 --- a/interpreter/value_int128.go +++ b/interpreter/value_int128.go @@ -459,7 +459,7 @@ func (v Int128Value) Div(context NumberValueArithmeticContext, other NumberValue LocationRange: locationRange, }) } - res.Div(v.BigInt, o.BigInt) + res.Quo(v.BigInt, o.BigInt) return res } @@ -495,7 +495,7 @@ func (v Int128Value) SaturatingDiv(context NumberValueArithmeticContext, other N if (v.BigInt.Cmp(sema.Int128TypeMinIntBig) == 0) && (o.BigInt.Cmp(res) == 0) { return sema.Int128TypeMaxIntBig } - res.Div(v.BigInt, o.BigInt) + res.Quo(v.BigInt, o.BigInt) return res } diff --git a/interpreter/value_int256.go b/interpreter/value_int256.go index 7797de68b5..695a1f44cf 100644 --- a/interpreter/value_int256.go +++ b/interpreter/value_int256.go @@ -428,7 +428,7 @@ func (v Int256Value) Div(context NumberValueArithmeticContext, other NumberValue LocationRange: locationRange, }) } - res.Div(v.BigInt, o.BigInt) + res.Quo(v.BigInt, o.BigInt) return res } @@ -463,7 +463,7 @@ func (v Int256Value) SaturatingDiv(context NumberValueArithmeticContext, other N if (v.BigInt.Cmp(sema.Int256TypeMinIntBig) == 0) && (o.BigInt.Cmp(res) == 0) { return sema.Int256TypeMaxIntBig } - res.Div(v.BigInt, o.BigInt) + res.Quo(v.BigInt, o.BigInt) return res } diff --git a/interpreter/value_uint.go b/interpreter/value_uint.go index 877313c64b..1d264cbc83 100644 --- a/interpreter/value_uint.go +++ b/interpreter/value_uint.go @@ -359,7 +359,7 @@ func (v UIntValue) Div(context NumberValueArithmeticContext, other NumberValue, LocationRange: locationRange, }) } - return res.Div(v.BigInt, o.BigInt) + return res.Quo(v.BigInt, o.BigInt) }, ) } diff --git a/interpreter/value_uint128.go b/interpreter/value_uint128.go index b081e09490..3b5d26b90a 100644 --- a/interpreter/value_uint128.go +++ b/interpreter/value_uint128.go @@ -376,7 +376,7 @@ func (v UInt128Value) Div(context NumberValueArithmeticContext, other NumberValu LocationRange: locationRange, }) } - return res.Div(v.BigInt, o.BigInt) + return res.Quo(v.BigInt, o.BigInt) }, ) diff --git a/interpreter/value_uint256.go b/interpreter/value_uint256.go index f41eb01fc8..83c59ad9c4 100644 --- a/interpreter/value_uint256.go +++ b/interpreter/value_uint256.go @@ -379,7 +379,7 @@ func (v UInt256Value) Div(context NumberValueArithmeticContext, other NumberValu LocationRange: locationRange, }) } - return res.Div(v.BigInt, o.BigInt) + return res.Quo(v.BigInt, o.BigInt) }, ) } diff --git a/interpreter/value_word128.go b/interpreter/value_word128.go index d4d94a129f..a599d0a550 100644 --- a/interpreter/value_word128.go +++ b/interpreter/value_word128.go @@ -298,7 +298,7 @@ func (v Word128Value) Div(context NumberValueArithmeticContext, other NumberValu LocationRange: locationRange, }) } - return res.Div(v.BigInt, o.BigInt) + return res.Quo(v.BigInt, o.BigInt) }, ) diff --git a/interpreter/value_word256.go b/interpreter/value_word256.go index 6d385cce77..9f421b3442 100644 --- a/interpreter/value_word256.go +++ b/interpreter/value_word256.go @@ -298,7 +298,7 @@ func (v Word256Value) Div(context NumberValueArithmeticContext, other NumberValu LocationRange: locationRange, }) } - return res.Div(v.BigInt, o.BigInt) + return res.Quo(v.BigInt, o.BigInt) }, ) diff --git a/values/value_int.go b/values/value_int.go index 8adeb193ca..e5979f3355 100644 --- a/values/value_int.go +++ b/values/value_int.go @@ -162,7 +162,7 @@ func (v IntValue) Div(gauge common.MemoryGauge, other IntValue) (IntValue, error common.NewDivBigIntMemoryUsage(v.BigInt, other.BigInt), func() *big.Int { res := new(big.Int) - return res.Div(v.BigInt, other.BigInt) + return res.Quo(v.BigInt, other.BigInt) }, ), nil } diff --git a/values/value_ufix64.go b/values/value_ufix64.go index bccfbc09c8..4f6b5ad075 100644 --- a/values/value_ufix64.go +++ b/values/value_ufix64.go @@ -142,7 +142,7 @@ func (v UFix64Value) Mul(gauge common.MemoryGauge, other UFix64Value) (UFix64Val valueGetter := func() (uint64, error) { result := new(big.Int).Mul(a, b) - result.Div(result, sema.Fix64FactorBig) + result.Quo(result, sema.Fix64FactorBig) if !result.IsUint64() { return 0, OverflowError{} @@ -161,7 +161,7 @@ func (v UFix64Value) SaturatingMul(gauge common.MemoryGauge, other UFix64Value) valueGetter := func() (uint64, error) { result := new(big.Int).Mul(a, b) - result.Div(result, sema.Fix64FactorBig) + result.Quo(result, sema.Fix64FactorBig) if !result.IsUint64() { return math.MaxUint64, nil @@ -180,7 +180,7 @@ func (v UFix64Value) Div(gauge common.MemoryGauge, other UFix64Value) (UFix64Val valueGetter := func() (uint64, error) { result := new(big.Int).Mul(a, sema.Fix64FactorBig) - result.Div(result, b) + result.Quo(result, b) if !result.IsUint64() { return 0, OverflowError{}