Skip to content

Commit 52ce23a

Browse files
fix: reduce safe integer range to exclude +/- 2^53 and match JavaScript safe-integer bounds
1 parent bc7425e commit 52ce23a

8 files changed

+70
-70
lines changed

core/vm.cpp

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,40 @@ class Interpreter {
873873
}
874874
}
875875

876+
/** Safely converts a double to an int64_t, with range and validity checks.
877+
*
878+
* This function is used primarily for bitwise operations which require integer operands.
879+
* It performs two safety checks:
880+
* 1. Verifies the value is finite (not NaN or Infinity)
881+
* 2. Ensures the value is within the safe integer range [-2^53, 2^53]
882+
*
883+
* The safe integer range limitation is necessary because IEEE 754 double precision
884+
* floating point numbers can only precisely represent integers in the range [-2^53, 2^53].
885+
* Beyond this range, precision is lost, which would lead to unpredictable results
886+
* in bitwise operations that depend on exact bit patterns.
887+
*
888+
* \param value The double value to convert
889+
* \param loc The location in source code (for error reporting)
890+
* \throws RuntimeError if value is not finite or outside the safe integer range
891+
* \returns The value converted to int64_t
892+
*/
893+
int64_t safeDoubleToInt64(double value, const internal::LocationRange& loc) {
894+
if (std::isnan(value) || std::isinf(value)) {
895+
throw internal::StaticError(loc, "numeric value is not finite");
896+
}
897+
898+
// Constants for safe double-to-int conversion
899+
// Jsonnet uses IEEE 754 doubles, which precisely represent integers in the range [-2^53 + 1, 2^53 - 1].
900+
constexpr int64_t DOUBLE_MAX_SAFE_INTEGER = (1LL << 53) - 1;
901+
constexpr int64_t DOUBLE_MIN_SAFE_INTEGER = -((1LL << 53) - 1);
902+
903+
// Check if the value is within the safe integer range
904+
if (value < DOUBLE_MIN_SAFE_INTEGER || value > DOUBLE_MAX_SAFE_INTEGER) {
905+
throw makeError(loc, "numeric value outside safe integer range for bitwise operation.");
906+
}
907+
return static_cast<int64_t>(value);
908+
}
909+
876910
public:
877911
/** Create a new interpreter.
878912
*
@@ -3415,21 +3449,4 @@ std::vector<std::string> jsonnet_vm_execute_stream(Allocator *alloc, const AST *
34153449
return vm.manifestStream(string_output);
34163450
}
34173451

3418-
inline int64_t safeDoubleToInt64(double value, const internal::LocationRange& loc) {
3419-
if (std::isnan(value) || std::isinf(value)) {
3420-
throw internal::StaticError(loc, "numeric value is not finite");
3421-
}
3422-
3423-
// Constants for safe double-to-int conversion
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);
3427-
3428-
// Check if the value is within the safe integer range
3429-
if (value < DOUBLE_MIN_SAFE_INTEGER || value > DOUBLE_MAX_SAFE_INTEGER) {
3430-
throw internal::StaticError(loc, "numeric value outside safe integer range for bitwise operation.");
3431-
}
3432-
return static_cast<int64_t>(value);
3433-
}
3434-
34353452
} // namespace jsonnet::internal

core/vm.h

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,25 +130,6 @@ std::vector<std::string> jsonnet_vm_execute_stream(
130130
double gc_min_objects, double gc_growth_trigger, const VmNativeCallbackMap &natives,
131131
JsonnetImportCallback *import_callback, void *import_callback_ctx, bool string_output);
132132

133-
/** Safely converts a double to an int64_t, with range and validity checks.
134-
*
135-
* This function is used primarily for bitwise operations which require integer operands.
136-
* It performs two safety checks:
137-
* 1. Verifies the value is finite (not NaN or Infinity)
138-
* 2. Ensures the value is within the safe integer range [-2^53, 2^53]
139-
*
140-
* The safe integer range limitation is necessary because IEEE 754 double precision
141-
* floating point numbers can only precisely represent integers in the range [-2^53, 2^53].
142-
* Beyond this range, precision is lost, which would lead to unpredictable results
143-
* in bitwise operations that depend on exact bit patterns.
144-
*
145-
* \param value The double value to convert
146-
* \param loc The location in source code (for error reporting)
147-
* \throws StaticError if value is not finite or outside the safe integer range
148-
* \returns The value converted to int64_t
149-
*/
150-
int64_t safeDoubleToInt64(double value, const LocationRange& loc);
151-
152133
} // namespace jsonnet::internal
153134

154135
#endif

doc/ref/language.html.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Strings can be constructed as literals, slices of existing strings, concatenatio
133133

134134
Jsonnet numbers are 64-bit floating point numbers as defined in IEEE754 excluding `nan` and `inf` values. Operations resulting in infinity or not a number are errors.
135135

136-
Integers can be precisely represented as a Jsonnet number in the range [-2^53,2^53]. This is [a direct consequence of IEEE754 spec](https://en.wikipedia.org/wiki/Double-precision_floating-point_format).
136+
Integers can be safely represented as a Jsonnet number in the range [-2^53 + 1,2^53 - 1]. This is [a direct consequence of IEEE754 spec](https://en.wikipedia.org/wiki/Double-precision_floating-point_format), with the requirements that a safe integer is representable exactly, and cannot be produced by rounding any other integer to fit the IEEE-754 representation. See also, the JavaScript [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER), [`Number.MIN_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER), and [`Number.isSafeInteger`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger) definitions.
137137

138138
### Function
139139

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
// Value just beyond MAX_SAFE_INTEGER (2^53)
2-
local beyond_max = 9007199254740994; // 2^53 + 1
1+
// Value just beyond MAX_SAFE_INTEGER (2^53 - 1)
2+
local beyond_max = 9007199254740992; // 2^53
33
beyond_max << 1 // Should throw error "numeric value outside safe integer range for bitwise operation"
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
STATIC ERROR: error.integer_conversion.jsonnet:3:1-16: numeric value outside safe integer range for bitwise operation.
1+
RUNTIME ERROR: numeric value outside safe integer range for bitwise operation.
2+
error.integer_conversion.jsonnet:3:1-16
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
STATIC ERROR: error.integer_left_shift.jsonnet:3:1-17: numeric value outside safe integer range for bitwise operation.
1+
RUNTIME ERROR: numeric value outside safe integer range for bitwise operation.
2+
error.integer_left_shift.jsonnet:3:1-17

test_suite/safe_integer_conversion.jsonnet

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Test values at boundary of safe integer range
2-
local max_safe = 9007199254740992; // 2^53
3-
local min_safe = -9007199254740992; // -2^53
2+
local max_safe = 9007199254740991; // 2^53 - 1
3+
local min_safe = -9007199254740991; // -(2^53 - 1)
44

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
5+
std.assertEqual(max_safe & 1, 1) && // Check 2^53
6+
std.assertEqual(min_safe & 1, 1) && // Check -2^53
7+
std.assertEqual((max_safe - 1) & 1, 0) && // Check 2^53 - 1
8+
std.assertEqual((min_safe + 1) & 1, 0) && // Check -2^53
99

1010
std.assertEqual(~(max_safe - 1), min_safe) && // ~(2^53 - 1) == -2^53
1111
std.assertEqual(~(min_safe + 1), max_safe - 2) && // ~(-2^53 + 1) == 2^53 - 2
@@ -17,18 +17,18 @@ std.assertEqual(~(-1), 0) &&
1717

1818
// Test shift operations with large values at safe boundary
1919
// (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
20+
std.assertEqual(max_safe >> 4, 562949953421311) &&
21+
// MAX_SAFE_INTEGER (2^53 - 1) right shift by 1 bit is (2^52 - 1)
22+
std.assertEqual(max_safe >> 1, 4503599627370495) && // 2^52
23+
// MIN_SAFE_INTEGER -(2^53 - 1) right shift by 1 bit is (-2^52)
24+
std.assertEqual(min_safe >> 1, -4503599627370496) && // -2^52
2525

2626
// Cannot left shift 2^53 without potential overflow/loss of precision issues
2727
// 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) &&
28+
// (2^52-1) left shift by 1 bit (result is 2^53-2)
29+
std.assertEqual(4503599627370495 << 1, max_safe - 1) &&
30+
// (-(2^52-1)) left shift by 1 bit (result is -(2^53-2))
31+
std.assertEqual((-4503599627370495) << 1, min_safe + 1) &&
3232

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

test_suite/safe_integer_conversion.jsonnet.fmt.golden

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Test values at boundary of safe integer range
2-
local max_safe = 9007199254740992; // 2^53
3-
local min_safe = -9007199254740992; // -2^53
2+
local max_safe = 9007199254740991; // 2^53 - 1
3+
local min_safe = -9007199254740991; // -(2^53 - 1)
44

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
5+
std.assertEqual(max_safe & 1, 1) && // Check 2^53
6+
std.assertEqual(min_safe & 1, 1) && // Check -2^53
7+
std.assertEqual((max_safe - 1) & 1, 0) && // Check 2^53 - 1
8+
std.assertEqual((min_safe + 1) & 1, 0) && // Check -2^53
99

1010
std.assertEqual(~(max_safe - 1), min_safe) && // ~(2^53 - 1) == -2^53
1111
std.assertEqual(~(min_safe + 1), max_safe - 2) && // ~(-2^53 + 1) == 2^53 - 2
@@ -17,18 +17,18 @@ std.assertEqual(~(-1), 0) &&
1717

1818
// Test shift operations with large values at safe boundary
1919
// (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
20+
std.assertEqual(max_safe >> 4, 562949953421311) &&
21+
// MAX_SAFE_INTEGER (2^53 - 1) right shift by 1 bit is (2^52 - 1)
22+
std.assertEqual(max_safe >> 1, 4503599627370495) && // 2^52
23+
// MIN_SAFE_INTEGER -(2^53 - 1) right shift by 1 bit is (-2^52)
2424
std.assertEqual(min_safe >> 1, -4503599627370496) && // -2^52
2525

2626
// Cannot left shift 2^53 without potential overflow/loss of precision issues
2727
// 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) &&
28+
// (2^52-1) left shift by 1 bit (result is 2^53-2)
29+
std.assertEqual(4503599627370495 << 1, max_safe - 1) &&
30+
// (-(2^52-1)) left shift by 1 bit (result is -(2^53-2))
31+
std.assertEqual((-4503599627370495) << 1, min_safe + 1) &&
3232

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

0 commit comments

Comments
 (0)