Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions Jint.Tests/Runtime/NumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,80 @@ public void IntegerMultiplicationPreservesNegativeZero()
Assert.Equal("true,true,true,true,true,true,true,true,true,true,true,true,true,true", result);
}

[Fact]
public void IntegerRemainderLanesPreserveSignAndSpecialCases()
{
var engine = new Engine();
// Number::remainder: the result takes the dividend's sign, so a zero remainder from a
// negative (or -0) dividend is -0. The unboxed raw-double lanes (compound `x %= y`,
// statement `lhs = a % b`, fused `x % c === c`) compute integral operands with int32
// math and must reproduce this exactly, deferring NaN/fractional/zero-divisor/out-of-range
// operands to fmod. The loop over slot-stored locals keeps the lanes engaged and their
// slot caches hot; every check pushes true.
var result = engine.Evaluate("""
(function () {
var r = [];
for (var i = 0; i < 3; i++) {
// compound `x %= y` shape
var a = -7, b = 2;
a %= b;
r.push(a === -1);
var c = 7, d = -2;
c %= d;
r.push(c === 1);
var negFour = -4, two = 2;
negFour %= two;
r.push(Object.is(negFour, -0));
var four = 4;
four %= two;
r.push(Object.is(four, 0));
var n = 5;
n %= 0;
r.push(Number.isNaN(n));
var f = 5.5;
f %= two;
r.push(f === 1.5);
var min = -2147483648, negOne = -1;
min %= negOne;
r.push(Object.is(min, -0));
var negZero = -0;
negZero %= two;
r.push(Object.is(negZero, -0));

// `lhs = a % b` statement shape
var res = 0;
var negFourB = -4, fourB = 4, sevenB = 7, negTwoB = -2, minB = -2147483648, fracB = 5.5;
res = negFourB % 2;
r.push(Object.is(res, -0));
res = fourB % 2;
r.push(Object.is(res, 0));
res = sevenB % negTwoB;
r.push(res === 1);
res = minB % negOne;
r.push(Object.is(res, -0));
res = fracB % 2;
r.push(res === 1.5);
res = sevenB % 0;
r.push(Number.isNaN(res));

// fused `x % constant === constant` equality shape (a -0 remainder
// compares equal to 0 under IEEE ===)
var e1 = -4, e2 = 4, e3 = -7, e4 = 7, e5 = 5, e6 = 5.5, e7 = -2147483648;
r.push(e1 % 2 === 0);
r.push(e2 % 2 === 0);
r.push(e3 % 2 === -1);
r.push(e4 % -2 === 1);
r.push(e5 % 0 !== 0);
r.push(e6 % 2 === 1.5);
r.push(e7 % -1 === 0);
}
return r.join(',');
})()
""").AsString();

Assert.Equal(string.Join(",", Enumerable.Repeat("true", 63)), result);
}

// The following tests guard the primitive-receiver method resolution that skips allocating a
// Number/Boolean/BigInt wrapper on `primitive.method()`. The wrapper was only ever a lookup
// vehicle (the this-value passed to the callee is the primitive, boxed at call time for sloppy
Expand Down
10 changes: 6 additions & 4 deletions Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,9 @@ private bool TryCompoundUnboxed(EvaluationContext context)
result = left / right;
break;
case Operator.RemainderAssignment:
// IEEE 754 remainder (fmod) matches the ECMAScript algorithm for all special cases
result = left % right;
// int32 lane avoids native fmod for integral operands; the fallback IEEE 754
// remainder (fmod) matches the ECMAScript algorithm for all special cases
result = RemainderUnboxed(left, right);
break;
case Operator.BitwiseAndAssignment:
result = TypeConverter.ToInt32(left) & TypeConverter.ToInt32(right);
Expand Down Expand Up @@ -887,8 +888,9 @@ private bool TryAssignNumeric(EvaluationContext context)
result = left / right;
break;
case Operator.Remainder:
// IEEE 754 remainder (fmod) matches the ECMAScript algorithm for all special cases
result = left % right;
// int32 lane avoids native fmod for integral operands; the fallback IEEE 754
// remainder (fmod) matches the ECMAScript algorithm for all special cases
result = RemainderUnboxed(left, right);
break;
default:
return false;
Expand Down
12 changes: 6 additions & 6 deletions Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,11 +649,11 @@ private static bool TryReadIndex(JintIdentifierExpression? indexIdentifier, ref
/// <summary>
/// Fused lane for the `identifier % numericConstant == numericConstant` test — the
/// stopwatch.js if-chain shape. Reads the identifier through the same slot/global arms as
/// <see cref="NumericConstantComparisonLane"/> and computes the whole test on raw doubles.
/// C# double % implements Number::remainder exactly, and the ways integer and double
/// remainders can disagree (−0 vs +0 results) are erased by the equality consumer
/// (IEEE == treats ±0 as equal), so the fused result is spec-exact for any proven-Number
/// operand: NaN dividends/divisors (and % 0) compare false, v % ±Infinity == v.
/// <see cref="NumericConstantComparisonLane"/> and computes the whole test on raw doubles
/// via <see cref="JintExpression.RemainderUnboxed"/>, which implements Number::remainder
/// exactly (int32 lane for integral operands, IEEE 754 fmod otherwise), so the fused
/// result is spec-exact for any proven-Number operand: NaN dividends/divisors (and % 0)
/// compare false, v % ±Infinity == v, and ±0 results compare equal under IEEE ==.
/// </summary>
private protected struct ModuloEqualityLane
{
Expand Down Expand Up @@ -700,7 +700,7 @@ public bool TryEvaluate(EvaluationContext context, out bool equal)
}
}

equal = value % _divisor == _expected;
equal = RemainderUnboxed(value, _divisor) == _expected;
return true;
}
}
Expand Down
28 changes: 28 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,34 @@ protected static JsValue Remainder(EvaluationContext context, JsValue left, JsVa
return result;
}

/// <summary>
/// Number::remainder over raw doubles with an int32 fast lane — the raw-double lanes
/// otherwise pay a native fmod call even for the dominant integral loop-counter shapes.
/// Anything failing the round-trip gate (NaN, ±Infinity, fractional values, operands
/// beyond int32 range, a zero divisor) falls back to IEEE 754 %, which matches the
/// ECMAScript algorithm for all special cases.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static double RemainderUnboxed(double left, double right)
{
// int32-representable check: NaN/±Infinity/fractional values fail the round-trip
// equality and defer to fmod, as does a zero divisor (fmod gives the spec NaN);
// a -0 dividend round-trips to 0 and is re-signed below
var leftInteger = (int) left;
var rightInteger = (int) right;
if (leftInteger == left && rightInteger == right && rightInteger != 0)
{
// long math: int.MinValue % -1 raises a hardware overflow in 32-bit division
var modulo = (long) leftInteger % rightInteger;
// Number::remainder gives the result the dividend's sign, so a zero remainder
// from a negative (or -0) dividend is -0 — integer math cannot represent it,
// test the sign bit of the original double (left is proven non-NaN by the gate)
return modulo == 0 && BitConverter.DoubleToInt64Bits(left) < 0 ? -0.0 : modulo;
}

return left % right;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static JsValue Divide(EvaluationContext context, JsValue left, JsValue right)
{
Expand Down
Loading