diff --git a/common/types/compare.go b/common/types/compare.go index e1968261..3cc750d2 100644 --- a/common/types/compare.go +++ b/common/types/compare.go @@ -16,10 +16,16 @@ package types import ( "math" + "math/big" "github.com/google/cel-go/common/types/ref" ) +// compareDoubleInt compares a CEL double and int value for ordering purposes. +// +// Direct conversion of Int to float64 loses precision for integer values +// outside the safe integer range of float64 (i.e., abs(i) > 2^53). Using +// math/big.Float ensures an exact comparison without rounding errors. func compareDoubleInt(d Double, i Int) Int { if d < math.MinInt64 { return IntNegOne @@ -27,13 +33,20 @@ func compareDoubleInt(d Double, i Int) Int { if d > math.MaxInt64 { return IntOne } - return compareDouble(d, Double(i)) + bf := new(big.Float).SetFloat64(float64(d)) + bi := new(big.Float).SetInt64(int64(i)) + return Int(bf.Cmp(bi)) } func compareIntDouble(i Int, d Double) Int { return -compareDoubleInt(d, i) } +// compareDoubleUint compares a CEL double and uint value for ordering purposes. +// +// Direct conversion of Uint to float64 loses precision for values outside +// the safe integer range of float64 (i.e., u > 2^53). Using math/big.Float +// ensures an exact comparison without rounding errors. func compareDoubleUint(d Double, u Uint) Int { if d < 0 { return IntNegOne @@ -41,7 +54,9 @@ func compareDoubleUint(d Double, u Uint) Int { if d > math.MaxUint64 { return IntOne } - return compareDouble(d, Double(u)) + bf := new(big.Float).SetFloat64(float64(d)) + bu := new(big.Float).SetUint64(uint64(u)) + return Int(bf.Cmp(bu)) } func compareUintDouble(u Uint, d Double) Int { diff --git a/common/types/compare_test.go b/common/types/compare_test.go new file mode 100644 index 00000000..287213df --- /dev/null +++ b/common/types/compare_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "testing" + +// TestCompareDoubleIntPrecision verifies that compareDoubleInt and +// compareDoubleUint correctly distinguish integer values that are adjacent +// to float64 representable boundaries (> 2^53), where naive float64 casting +// loses precision and can incorrectly equate distinct integers. +func TestCompareDoubleIntPrecision(t *testing.T) { + tests := []struct { + d Double + i Int + want Int + desc string + }{ + // Values within safe float64 range — must still work correctly. + {Double(1.0), Int(1), IntZero, "1.0 == 1"}, + {Double(1.5), Int(1), IntOne, "1.5 > 1"}, + {Double(0.5), Int(1), IntNegOne, "0.5 < 1"}, + {Double(9007199254740992), Int(9007199254740992), IntZero, "2^53 == 2^53"}, + + // Precision-loss zone (> 2^53): distinct integers must NOT compare equal. + // Bug: Double(i) cast loses low bits, collapsing adjacent ints to same float64. + {Double(9007199254740992), Int(9007199254740993), IntNegOne, "2^53 < 2^53+1"}, + {Double(9007199254740992), Int(9007199254740994), IntNegOne, "2^53 < 2^53+2"}, + {Double(1e17), Int(100000000000000001), IntNegOne, "1e17 < 1e17+1"}, + {Double(1e18), Int(1000000000000000001), IntNegOne, "1e18 < 1e18+1"}, + + // Symmetric: int smaller than double. + {Double(9007199254740993), Int(9007199254740992), IntOne, "2^53+1 > 2^53 (as Double)"}, + } + for _, tc := range tests { + got := compareDoubleInt(tc.d, tc.i) + if got != tc.want { + t.Errorf("compareDoubleInt(%v, %v): got %v, want %v — %s", + tc.d, tc.i, got, tc.want, tc.desc) + } + } +} + +func TestCompareDoubleUintPrecision(t *testing.T) { + tests := []struct { + d Double + u Uint + want Int + desc string + }{ + {Double(0), Uint(0), IntZero, "0.0 == 0"}, + {Double(1.0), Uint(1), IntZero, "1.0 == 1"}, + {Double(9007199254740992), Uint(9007199254740992), IntZero, "2^53 == 2^53"}, + {Double(9007199254740992), Uint(9007199254740993), IntNegOne, "2^53 < 2^53+1 (uint)"}, + {Double(1e17), Uint(100000000000000001), IntNegOne, "1e17 < 1e17+1 (uint)"}, + } + for _, tc := range tests { + got := compareDoubleUint(tc.d, tc.u) + if got != tc.want { + t.Errorf("compareDoubleUint(%v, %v): got %v, want %v — %s", + tc.d, tc.u, got, tc.want, tc.desc) + } + } +}