Skip to content

Commit 2a08552

Browse files
committed
fix: correct safe integer range for bitwise ops
IEEE 754 double-precision floating-point numbers can precisely represent all integers in the range [-2^53, 2^53]. The previous implementation incorrectly used the range [-(2^53 - 1), 2^53 - 1] when checking if a double could be safely converted to an int64 for bitwise operations. This commit updates the `safeDoubleToInt64` function and its associated comments and tests to use the correct safe integer range, aligning with the IEEE 754 standard and Jsonnet documentation. Test cases have been adjusted to verify the new boundaries and fix related assertions. Signed-off-by: Ville Vesilehto <[email protected]>
1 parent 3544891 commit 2a08552

File tree

4 files changed

+29
-14
lines changed

4 files changed

+29
-14
lines changed

core/vm.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -3421,9 +3421,9 @@ inline int64_t safeDoubleToInt64(double value, const internal::LocationRange& lo
34213421
}
34223422

34233423
// Constants for safe double-to-int conversion
3424-
// IEEE 754 doubles precisely represent integers up to 2^53, beyond which precision is lost
3425-
constexpr int64_t DOUBLE_MAX_SAFE_INTEGER = (1LL << 53) - 1;
3426-
constexpr int64_t DOUBLE_MIN_SAFE_INTEGER = -((1LL << 53) - 1);
3424+
// Jsonnet uses IEEE 754 doubles, which precisely represent integers in the range [-2^53, 2^53].
3425+
constexpr int64_t DOUBLE_MAX_SAFE_INTEGER = 1LL << 53;
3426+
constexpr int64_t DOUBLE_MIN_SAFE_INTEGER = -(1LL << 53);
34273427

34283428
// Check if the value is within the safe integer range
34293429
if (value < DOUBLE_MIN_SAFE_INTEGER || value > DOUBLE_MAX_SAFE_INTEGER) {

core/vm.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ std::vector<std::string> jsonnet_vm_execute_stream(
134134
* This function is used primarily for bitwise operations which require integer operands.
135135
* It performs two safety checks:
136136
* 1. Verifies the value is finite (not NaN or Infinity)
137-
* 2. Ensures the value is within the safe integer range (±(2^53-1))
137+
* 2. Ensures the value is within the safe integer range [-2^53, 2^53]
138138
*
139139
* The safe integer range limitation is necessary because IEEE 754 double precision
140-
* floating point numbers can only precisely represent integers up to 2^53.
140+
* floating point numbers can only precisely represent integers in the range [-2^53, 2^53].
141141
* Beyond this range, precision is lost, which would lead to unpredictable results
142142
* in bitwise operations that depend on exact bit patterns.
143143
*
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Value just beyond MAX_SAFE_INTEGER (2^53)
2-
local beyond_max = 9007199254740992; // 2^53
2+
local beyond_max = 9007199254740994; // 2^53 + 1
33
beyond_max << 1 // Should throw error "numeric value outside safe integer range for bitwise operation"

test_suite/safe_integer_conversion.jsonnet

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
// Test values at boundary of safe integer range
2-
std.assertEqual(~9007199254740991, -9007199254740992) && // ~MAX_SAFE_INTEGER
3-
std.assertEqual(~(-9007199254740991), 9007199254740990) && // ~MIN_SAFE_INTEGER
2+
local max_safe = 9007199254740992; // 2^53
3+
local min_safe = -9007199254740992; // -2^53
4+
5+
std.assertEqual(max_safe & 1, 0) && // Check 2^53
6+
std.assertEqual(min_safe & 1, 0) && // Check -2^53
7+
std.assertEqual((max_safe - 1) & 1, 1) && // Check 2^53 - 1
8+
std.assertEqual((min_safe + 1) & 1, 1) && // Check -2^53 + 1
9+
10+
std.assertEqual(~(max_safe - 1), min_safe) && // ~(2^53 - 1) == -2^53
11+
std.assertEqual(~(min_safe + 1), max_safe - 2) && // ~(-2^53 + 1) == 2^53 - 2
412

513
// Test basic values
614
std.assertEqual(~0, -1) &&
715
std.assertEqual(~1, -2) &&
816
std.assertEqual(~(-1), 0) &&
917

1018
// Test shift operations with large values at safe boundary
11-
// MAX_SAFE_INTEGER (2^53-1) right shift by 4 bits
12-
std.assertEqual(9007199254740991 >> 4, 562949953421311) &&
13-
// MAX_SAFE_INTEGER (2^53-1) left shift by 1 bit (result is still precisely representable)
14-
std.assertEqual(9007199254740991 << 1, 18014398509481982) &&
15-
// (2^52-1) left shift by 1 bit (result is MAX_SAFE_INTEGER-1)
16-
std.assertEqual(4503599627370495 << 1, 9007199254740990) &&
19+
// (2^53 - 1) right shift by 4 bits
20+
std.assertEqual((max_safe - 1) >> 4, 562949953421311) &&
21+
// MAX_SAFE_INTEGER (2^53) right shift by 1 bit
22+
std.assertEqual(max_safe >> 1, 4503599627370496) && // 2^52
23+
// MIN_SAFE_INTEGER (-2^53) right shift by 1 bit
24+
std.assertEqual(min_safe >> 1, -4503599627370496) && // -2^52
25+
26+
// Cannot left shift 2^53 without potential overflow/loss of precision issues
27+
// depending on the shift amount, but can shift smaller numbers up to it.
28+
// (2^52) left shift by 1 bit (result is 2^53)
29+
std.assertEqual((max_safe >> 1) << 1, max_safe) &&
30+
// (-2^52) left shift by 1 bit (result is -2^53)
31+
std.assertEqual((min_safe >> 1) << 1, min_safe) &&
1732

1833
// Test larger values within safe range
1934
std.assertEqual(~123456789, -123456790) &&

0 commit comments

Comments
 (0)