diff --git a/Jint.Tests/Runtime/NumberTests.cs b/Jint.Tests/Runtime/NumberTests.cs index e6b4c22e7..2b7170ced 100644 --- a/Jint.Tests/Runtime/NumberTests.cs +++ b/Jint.Tests/Runtime/NumberTests.cs @@ -221,4 +221,39 @@ public void CompoundAssignmentMatchesBinaryOperatorSemantics() var tdzUpdate = Assert.Throws(() => engine.Evaluate("(function() { { y++; let y; } })()")); Assert.Equal("ReferenceError", tdzUpdate.Error.AsObject().Get("constructor").AsObject().Get("name").AsString()); } + + [Fact] + public void IntegerMultiplicationPreservesNegativeZero() + { + var engine = new Engine(); + // Number::multiply: a zero product takes a negative sign when the operand signs differ. + // The integer fast paths (binary * and compound *=) cannot represent -0 and must route + // zero products through double arithmetic; test every operand shape the lanes serve. + var result = engine.Evaluate(""" + (function () { + var zero = 0, negFive = -5, five = 5; + var r = []; + r.push(Object.is(zero * negFive, -0)); + r.push(Object.is(negFive * zero, -0)); + r.push(Object.is(zero * five, 0)); + r.push(Object.is(zero * zero, 0)); + r.push(Object.is(0 * -5, -0)); + var arr = [0, -5]; + r.push(Object.is(arr[0] * arr[1], -0)); + var o = { a: 0, b: -5 }; + r.push(Object.is(o.a * o.b, -0)); + function f(x, y) { return x * y; } + r.push(Object.is(f(0, -5), -0)); + var acc = 0; acc *= -5; + r.push(Object.is(acc, -0)); + var acc2 = -5; acc2 *= 0; + r.push(Object.is(acc2, -0)); + var acc3 = 0; acc3 *= 5; + r.push(Object.is(acc3, 0)); + return r.join(','); + })() + """).AsString(); + + Assert.Equal("true,true,true,true,true,true,true,true,true,true,true", result); + } } diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 7774f2d13..449e2ea55 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -281,7 +281,12 @@ private JsValue ComputeCompound(EvaluationContext context, JsValue originalLeftV { if (AreIntegerOperands(originalLeftValue, rval)) { - return JsNumber.Create((long) originalLeftValue.AsInteger() * rval.AsInteger()); + var product = (long) originalLeftValue.AsInteger() * rval.AsInteger(); + // Number::multiply gives -0 for zero products of opposite signs (0 * -5), + // which integer math cannot represent — route through double arithmetic + return product != 0 + ? JsNumber.Create(product) + : JsNumber.Create((double) originalLeftValue.AsInteger() * rval.AsInteger()); } var leftNumeric = TypeConverter.ToNumeric(originalLeftValue); diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs index 1fa877bec..ef5a13c74 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs @@ -1162,7 +1162,12 @@ protected override object EvaluateInternal(EvaluationContext context) } else if (AreIntegerOperands(left, right)) { - result = JsNumber.Create((long) left.AsInteger() * right.AsInteger()); + var product = (long) left.AsInteger() * right.AsInteger(); + // Number::multiply gives -0 for zero products of opposite signs (0 * -5), which + // integer math cannot represent — route zero products through double arithmetic + result = product != 0 + ? JsNumber.Create(product) + : JsNumber.Create((double) left.AsInteger() * right.AsInteger()); } else if (left._type == InternalTypes.Number && right._type == InternalTypes.Number) {