Skip to content

Commit 982de09

Browse files
correct safe integer range for bitwise ops (#381)
Fixes #317 #316 #315
1 parent ad5ae74 commit 982de09

File tree

10 files changed

+95
-27
lines changed

10 files changed

+95
-27
lines changed

bench/src/main/scala/sjsonnet/ProfilingEvaluator.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class ProfilingEvaluator(
162162
all.foreach { b =>
163163
val cl = b.expr match {
164164
case _: Val => classOf[Val]
165-
case ee => ee.getClass
165+
case ee => ee.getClass
166166
}
167167
val n = cl.getName.replaceAll("^sjsonnet\\.", "").replace('$', '.')
168168
val eb = m.getOrElseUpdate(n, new ExprTypeBox(n))

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -549,14 +549,21 @@ class Evaluator(
549549

550550
case Expr.BinaryOp.OP_<< =>
551551
(l, r) match {
552-
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong << r.toLong).toDouble)
553-
case _ => fail()
552+
case (l: Val.Num, r: Val.Num) =>
553+
val ll = l.asSafeLong(pos)
554+
val rr = r.asSafeLong(pos)
555+
if (rr >= 1 && ll >= (1L << (63 - rr)))
556+
Error.fail("numeric value outside safe integer range for bitwise operation", pos)
557+
else
558+
Val.Num(pos, (ll << rr).toDouble)
559+
case _ => fail()
554560
}
555561

556562
case Expr.BinaryOp.OP_>> =>
557563
(l, r) match {
558-
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong >> r.toLong).toDouble)
559-
case _ => fail()
564+
case (l: Val.Num, r: Val.Num) =>
565+
Val.Num(pos, (l.asSafeLong(pos) >> r.asSafeLong(pos)).toDouble)
566+
case _ => fail()
560567
}
561568

562569
case Expr.BinaryOp.OP_in =>
@@ -567,20 +574,23 @@ class Evaluator(
567574

568575
case Expr.BinaryOp.OP_& =>
569576
(l, r) match {
570-
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong & r.toLong).toDouble)
571-
case _ => fail()
577+
case (l: Val.Num, r: Val.Num) =>
578+
Val.Num(pos, (l.asSafeLong(pos) & r.asSafeLong(pos)).toDouble)
579+
case _ => fail()
572580
}
573581

574582
case Expr.BinaryOp.OP_^ =>
575583
(l, r) match {
576-
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong ^ r.toLong).toDouble)
577-
case _ => fail()
584+
case (l: Val.Num, r: Val.Num) =>
585+
Val.Num(pos, (l.asSafeLong(pos) ^ r.asSafeLong(pos)).toDouble)
586+
case _ => fail()
578587
}
579588

580589
case Expr.BinaryOp.OP_| =>
581590
(l, r) match {
582-
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong | r.toLong).toDouble)
583-
case _ => fail()
591+
case (l: Val.Num, r: Val.Num) =>
592+
Val.Num(pos, (l.asSafeLong(pos) | r.asSafeLong(pos)).toDouble)
593+
case _ => fail()
584594
}
585595

586596
case _ => fail()

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ object PrettyNamed {
8181
implicit val nullName: PrettyNamed[Val.Null] = new PrettyNamed("null")
8282
}
8383
object Val {
84+
// Constants for safe double-to-int conversion
85+
// IEEE 754 doubles precisely represent integers up to 2^53, beyond which precision is lost
86+
private val DOUBLE_MAX_SAFE_INTEGER = (1L << 53) - 1
87+
private val DOUBLE_MIN_SAFE_INTEGER = -((1L << 53) - 1)
8488

8589
abstract class Literal extends Val with Expr {
8690
final override private[sjsonnet] val _tag = ExprTags.`Val.Literal`
@@ -108,6 +112,17 @@ object Val {
108112
def prettyName = "number"
109113
override def asInt: Int = value.toInt
110114
override def asLong: Long = value.toLong
115+
116+
def asSafeLong(pos: Position)(implicit ev: EvalErrorScope): Long = {
117+
if (value.isInfinite || value.isNaN) {
118+
Error.fail("numeric value is not finite", pos)
119+
}
120+
121+
if (value < DOUBLE_MIN_SAFE_INTEGER || value > DOUBLE_MAX_SAFE_INTEGER) {
122+
Error.fail("numeric value outside safe integer range for bitwise operation", pos)
123+
}
124+
value.toLong
125+
}
111126
override def asDouble: Double = value
112127
}
113128

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Value just beyond MAX_SAFE_INTEGER (2^53 - 1)
2+
local beyond_max = 9007199254740992; // 2^53
3+
beyond_max << 1 // Should throw error "numeric value outside safe integer range for bitwise operation"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Test that left-shifting a value that would exceed int64_t range throws an error
2+
local large_value = 1 << 62; // 2^62
3+
large_value << 2 // Would be 2^64, exceeding signed 64-bit integer range
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
local large_value = 4503599627370496;
2+
large_value << 11
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.
2828
// (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) &&
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) &&
@@ -41,4 +41,4 @@ std.assertEqual(123 ^ 456, 435) &&
4141
std.assertEqual(123 << 2, 492) &&
4242
std.assertEqual(123 >> 2, 30) &&
4343

44-
true
44+
true

sjsonnet/test/src-js/sjsonnet/ErrorTests.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,5 +355,23 @@ object ErrorTests extends TestSuite {
355355
| at [std.manifestTomlEx].(sjsonnet/test/resources/test_suite/error.manifest_toml_null_value.jsonnet:17:19)
356356
|""".stripMargin
357357
)
358+
359+
test("integer_conversion") - check(
360+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
361+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_conversion.jsonnet:3:12)
362+
|""".stripMargin
363+
)
364+
365+
test("integer_left_shift") - check(
366+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
367+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_left_shift.jsonnet:3:13)
368+
|""".stripMargin
369+
)
370+
371+
test("integer_left_shift_runtime") - check(
372+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
373+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_left_shift_runtime.jsonnet:2:13)
374+
|""".stripMargin
375+
)
358376
}
359377
}

sjsonnet/test/src-jvm-native/ErrorTests.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,23 @@ object ErrorTests extends TestSuite {
342342
| at [std.manifestTomlEx].(sjsonnet/test/resources/test_suite/error.manifest_toml_null_value.jsonnet:17:19)
343343
|""".stripMargin
344344
)
345+
346+
test("integer_conversion") - check(
347+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
348+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_conversion.jsonnet:3:12)
349+
|""".stripMargin
350+
)
351+
352+
test("integer_left_shift") - check(
353+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
354+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_left_shift.jsonnet:3:13)
355+
|""".stripMargin
356+
)
357+
358+
test("integer_left_shift_runtime") - check(
359+
"""|sjsonnet.Error: numeric value outside safe integer range for bitwise operation
360+
| at [BinaryOp <<].(sjsonnet/test/resources/test_suite/error.integer_left_shift_runtime.jsonnet:2:13)
361+
|""".stripMargin
362+
)
345363
}
346364
}

sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ object Std0150FunctionsTests extends TestSuite {
158158
ujson.Str("j s o n n e t this is")
159159
eval("std.foldr(function(v, i) i + v + v, 'bcd', 'a')") ==> ujson.Str("addccbb")
160160

161-
162161
eval("""std.foldl(function (acc, it) acc + " " + it, "jsonnet", "this is")""") ==>
163162
ujson.Str("this is j s o n n e t")
164163
}

0 commit comments

Comments
 (0)