diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index 44c2cff0..ff06231f 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -245,7 +245,7 @@ class Evaluator( if (rd == 0) null else { val r = ld / rd; if (r.isInfinite) null else Val.cachedNum(pos, r) } case Expr.BinaryOp.OP_% => - Val.cachedNum(pos, ld % rd) + if (rd == 0) null else Val.cachedNum(pos, ld % rd) case Expr.BinaryOp.OP_+ => val r = ld + rd; if (r.isInfinite) null else Val.cachedNum(pos, r) case Expr.BinaryOp.OP_- => @@ -719,7 +719,9 @@ class Evaluator( val r = ld / rd if (r.isInfinite) Error.fail("overflow", pos) Val.cachedNum(pos, r) - case Expr.BinaryOp.OP_% => Val.cachedNum(pos, ld % rd) + case Expr.BinaryOp.OP_% => + if (rd == 0) Error.fail("Division by zero.", pos) + Val.cachedNum(pos, ld % rd) // Use position-free static singletons for boolean results — this method is only called // from comprehension fast paths where position info on boolean results is unnecessary. // Avoids 1 object allocation per comparison in inner loops (significant for 1M+ iterations). @@ -862,7 +864,10 @@ class Evaluator( val result = l / r if (result.isInfinite) Error.fail("overflow", pos); result case Expr.BinaryOp.OP_% => - visitExprAsDouble(e.lhs) % visitExprAsDouble(e.rhs) + val l = visitExprAsDouble(e.lhs) + val r = visitExprAsDouble(e.rhs) + if (r == 0) Error.fail("Division by zero.", pos) + l % r case Expr.BinaryOp.OP_+ => val r = visitExprAsDouble(e.lhs) + visitExprAsDouble(e.rhs) if (r.isInfinite) Error.fail("overflow", pos); r @@ -1342,8 +1347,10 @@ class Evaluator( l match { case Val.Num(_, ld) => r match { - case Val.Num(_, rd) => Val.cachedNum(pos, ld % rd) - case _ => failBinOp(l, e.op, r, pos) + case Val.Num(_, rd) => + if (rd == 0) Error.fail("Division by zero.", pos) + Val.cachedNum(pos, ld % rd) + case _ => failBinOp(l, e.op, r, pos) } case ls: Val.Str => Format.format(ls.str, r, pos) case _ => failBinOp(l, e.op, r, pos) diff --git a/sjsonnet/src/sjsonnet/StaticOptimizer.scala b/sjsonnet/src/sjsonnet/StaticOptimizer.scala index 1ed07ac3..39a8803b 100644 --- a/sjsonnet/src/sjsonnet/StaticOptimizer.scala +++ b/sjsonnet/src/sjsonnet/StaticOptimizer.scala @@ -187,7 +187,7 @@ class StaticOptimizer( case BinaryOp.OP_/ => if (r == 0) return Double.NaN val res = l / r; if (res.isInfinite) return Double.NaN; res - case BinaryOp.OP_% => l % r + case BinaryOp.OP_% => if (r == 0) return Double.NaN; l % r case BinaryOp.OP_<< => val ll = l.toSafeLong(pos)(ev); val rr = r.toSafeLong(pos)(ev) if (rr < 0) return Double.NaN @@ -480,8 +480,8 @@ class StaticOptimizer( } case BinaryOp.OP_% => (lhs, rhs) match { - case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l % r) - case _ => fallback + case (Val.Num(_, l), Val.Num(_, r)) if r != 0 => Val.Num(pos, l % r) + case _ => fallback } case BinaryOp.OP_< => tryFoldComparison(pos, lhs, BinaryOp.OP_<, rhs, fallback) diff --git a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala index 7f4dfa98..d47dd81b 100644 --- a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala @@ -254,8 +254,10 @@ object MathModule extends AbstractFunctionModule { new Val.Builtin2("mod", "a", "b") { def evalRhs(a: Eval, b: Eval, ev: EvalScope, pos: Position): Val = { (a.value, b.value) match { - case (x: Val.Num, y: Val.Num) => Val.cachedNum(pos, x.asDouble % y.asDouble) - case _ => Format.format(a.value.asString, b.value, pos)(ev) + case (x: Val.Num, y: Val.Num) => + if (y.asDouble == 0) Error.fail("Division by zero.", pos)(ev) + Val.cachedNum(pos, x.asDouble % y.asDouble) + case _ => Format.format(a.value.asString, b.value, pos)(ev) } } } @@ -268,6 +270,7 @@ object MathModule extends AbstractFunctionModule { * Performs modulo arithmetic for numeric values. */ builtin("modulo", "a", "b") { (pos, ev, a: Double, b: Double) => + if (b == 0) Error.fail("Division by zero.", pos)(ev) a % b }, /** diff --git a/sjsonnet/test/resources/go_test_suite/percent_mod_int5.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/percent_mod_int5.jsonnet.golden index 998bf664..b0b01bf7 100644 --- a/sjsonnet/test/resources/go_test_suite/percent_mod_int5.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/percent_mod_int5.jsonnet.golden @@ -1,3 +1,3 @@ -sjsonnet.Error: not a number +sjsonnet.Error: Division by zero. at [].(percent_mod_int5.jsonnet:1:4) diff --git a/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet new file mode 100644 index 00000000..071e3d0a --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet @@ -0,0 +1,2 @@ +// Modulo by zero must report "Division by zero." matching go-jsonnet behavior. +1 % 0 diff --git a/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet.golden new file mode 100644 index 00000000..f83cfdcf --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: Division by zero. + at [].(error.modulo_by_zero.jsonnet:2:3) \ No newline at end of file diff --git a/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet new file mode 100644 index 00000000..22124ea9 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet @@ -0,0 +1,4 @@ +// Runtime modulo by zero (non-constant operands) must also report "Division by zero." +local x = 10; +local y = 0; +x % y diff --git a/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet.golden new file mode 100644 index 00000000..55c01d3c --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.modulo_by_zero_runtime.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: Division by zero. \ No newline at end of file