diff --git a/Jint.Tests/Runtime/EqualityLaneTests.cs b/Jint.Tests/Runtime/EqualityLaneTests.cs
index aa0a55c1b..f8619cb85 100644
--- a/Jint.Tests/Runtime/EqualityLaneTests.cs
+++ b/Jint.Tests/Runtime/EqualityLaneTests.cs
@@ -119,6 +119,116 @@ public void GlobalLaneDeclinesWhenGlobalRedefinedAsAccessor()
Assert.Equal("4:2", result);
}
+ [Fact]
+ public void FusedModuloEqualityMatchesGenericPathAcrossSigns()
+ {
+ var engine = new Engine();
+ // negative dividends produce negative (or -0) remainders; the equality consumer
+ // erases the ±0 distinction, so the fused raw-double form must agree everywhere
+ var result = engine.Evaluate("""
+ (function () {
+ var r = [];
+ for (var i = -3; i <= 3; i++) {
+ r.push(i % 2 == 0, i % 2 === 0, i % 2 != 0, i % 2 !== 0, i % 2 == -1, i % -2 == 1);
+ }
+ return r.join(',');
+ })()
+ """).AsString();
+
+ var expected = string.Join(",",
+ "false,false,true,true,true,false", // i = -3 → -1
+ "true,true,false,false,false,false", // i = -2 → -0
+ "false,false,true,true,true,false", // i = -1 → -1
+ "true,true,false,false,false,false", // i = 0 → 0
+ "false,false,true,true,false,true", // i = 1 → 1
+ "true,true,false,false,false,false", // i = 2 → 0
+ "false,false,true,true,false,true"); // i = 3 → 1
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void FusedModuloHandlesSpecialNumbersAndDeclines()
+ {
+ var engine = new Engine();
+ var result = engine.Evaluate("""
+ (function () {
+ var nan = NaN, five = 5, str = '4';
+ return [
+ nan % 2 == 0, nan % 2 != 0,
+ five % 0 == 0, five % 0 != 0,
+ five % Infinity == 5,
+ str % 2 == 0
+ ].join(',');
+ })()
+ """).AsString();
+
+ Assert.Equal("false,true,false,true,true,true", result);
+ }
+
+ [Fact]
+ public void FusedModuloWorksOnGlobals()
+ {
+ var engine = new Engine();
+ // the stopwatch.js if-chain shape at script top level (globals, not slots)
+ var result = engine.Evaluate("""
+ var hits = 0;
+ for (var x = 0; x < 8; x++) {
+ var z = x ^ 1;
+ if (z % 2 == 0) { hits++; }
+ else if (z % 3 == 0) { hits += 10; }
+ }
+ hits;
+ """).AsNumber();
+
+ Assert.Equal(14, result);
+ }
+
+ [Fact]
+ public void ConstBindingsTakeTheLaneShapes()
+ {
+ var engine = new Engine();
+ // the stopwatch-modern shape: const bindings inside the loop body flow through the
+ // relational, equality and fused-modulo lanes (immutable bindings are readable)
+ var result = engine.Evaluate("""
+ (function () {
+ let hits = 0;
+ for (let x = 0; x < 8; x++) {
+ const z = x ^ 1;
+ if (z % 2 == 0) { hits++; }
+ else if (z % 3 === 0) { hits += 10; }
+ const lim = 4;
+ if (z < lim) { hits += 100; }
+ }
+ return hits;
+ })()
+ """).AsNumber();
+
+ Assert.Equal(414, result); // 4 even-z hits + one z=3 (+10) + four z<4 (+400)
+ }
+
+ [Fact]
+ public void TdzReadStillThrowsThroughLaneShapes()
+ {
+ var engine = new Engine();
+ // an uninitialized const slot has neither an unboxed number nor a reference value,
+ // so the lane declines and the generic path raises the ReferenceError
+ var result = engine.Evaluate("""
+ (function () {
+ try {
+ for (let i = 0; i < 2; i++) {
+ if (z === 0) { }
+ const z = i;
+ }
+ return 'no-throw';
+ } catch (e) {
+ return e instanceof ReferenceError ? 'ReferenceError' : 'other';
+ }
+ })()
+ """).AsString();
+
+ Assert.Equal("ReferenceError", result);
+ }
+
[Fact]
public void GlobalLaneDeclinesWhenValueTypeChangesInPlace()
{
diff --git a/Jint/Runtime/Environments/DeclarativeEnvironment.cs b/Jint/Runtime/Environments/DeclarativeEnvironment.cs
index 92aa3d5df..e17dba465 100644
--- a/Jint/Runtime/Environments/DeclarativeEnvironment.cs
+++ b/Jint/Runtime/Environments/DeclarativeEnvironment.cs
@@ -90,6 +90,38 @@ internal bool TryGetNumberSlot(int slotIndex, out double value)
return false;
}
+ ///
+ /// Read-only sibling of for the comparison lanes: admits
+ /// immutable bindings too (a `const` number is the canonical loop-body operand). Uninitialized
+ /// (TDZ) bindings have neither an unboxed number nor a reference value, so they decline here
+ /// and the generic path raises the ReferenceError. Never pair with .
+ ///
+ internal bool TryGetNumberSlotForRead(int slotIndex, out double value)
+ {
+ var slots = _slots;
+ if (slots is null || (uint) slotIndex >= (uint) slots.Length)
+ {
+ value = 0;
+ return false;
+ }
+
+ ref var binding = ref slots[slotIndex];
+ if (binding.IsUnboxedNumber)
+ {
+ value = binding.UnboxedNumber;
+ return true;
+ }
+
+ if (binding.HasReferenceValue && binding.Value is JsNumber number)
+ {
+ value = number._value;
+ return true;
+ }
+
+ value = 0;
+ return false;
+ }
+
///
/// Stores a raw number into a slot previously validated by
/// without materializing a JsNumber.
diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
index c054fa98f..4479a9e63 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
@@ -88,7 +88,7 @@ public bool TryGetOperands(EvaluationContext context, out double left, out doubl
var env = engine.ExecutionContext.LexicalEnvironment;
if (!_slotCache.TryResolve(engine, env, identifier.Identifier, out var environment, out var slotIndex)
- || !environment.TryGetNumberSlot(slotIndex, out left))
+ || !environment.TryGetNumberSlotForRead(slotIndex, out left))
{
// top-level vars are global-object properties, never slots: fall back to the
// validated global-descriptor cache (the stopwatch-shape loop test). The null
@@ -106,31 +106,91 @@ public bool TryGetOperands(EvaluationContext context, out double left, out doubl
}
if (_rightSlotCache.TryResolve(engine, env, rightIdentifier.Identifier, out var rightEnvironment, out var rightSlotIndex)
- && rightEnvironment.TryGetNumberSlot(rightSlotIndex, out right))
+ && rightEnvironment.TryGetNumberSlotForRead(rightSlotIndex, out right))
{
return true;
}
return rightIdentifier._cachedGlobalEnv is not null && TryReadGlobalNumber(rightIdentifier, engine, env, out right);
}
+ }
- ///
- /// Global-binding arm of the lane operand read: validated-cache probe plus a field read,
- /// both pure — so a decline (or a left read followed by a right-side decline) has no
- /// observable effect. The value type is re-checked on every read because in-place value
- /// mutations bump no version. Out of line so lane misses don't grow the inlined probe.
- ///
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static bool TryReadGlobalNumber(JintIdentifierExpression identifier, Engine engine, Environments.Environment env, out double value)
+ ///
+ /// Global-binding arm of the lane operand reads: validated-cache probe plus a field read,
+ /// both pure — so a decline (or a left read followed by a right-side decline) has no
+ /// observable effect. The value type is re-checked on every read because in-place value
+ /// mutations bump no version. Out of line so lane misses don't grow the inlined probes.
+ /// Callers pre-check for null.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private protected static bool TryReadGlobalNumber(JintIdentifierExpression identifier, Engine engine, Environments.Environment env, out double value)
+ {
+ if (identifier.TryGetValidatedGlobalDescriptor(engine, env) is { _value: JsNumber number })
{
- if (identifier.TryGetValidatedGlobalDescriptor(engine, env) is { _value: JsNumber number })
+ value = number._value;
+ return true;
+ }
+
+ value = 0;
+ return false;
+ }
+
+ ///
+ /// Fused lane for the `identifier % numericConstant == numericConstant` test — the
+ /// stopwatch.js if-chain shape. Reads the identifier through the same slot/global arms as
+ /// 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.
+ ///
+ private protected struct ModuloEqualityLane
+ {
+ private JintIdentifierExpression? _identifier;
+ private double _divisor;
+ private double _expected;
+ private SlotLocationCache _slotCache;
+
+ public void Initialize(JintExpression left, JintExpression right)
+ {
+ if (left is ModuloBinaryExpression { _left: JintIdentifierExpression identifier, _right: JintConstantExpression { Value: JsNumber divisor } }
+ && right is JintConstantExpression { Value: JsNumber expected })
{
- value = number._value;
- return true;
+ _identifier = identifier;
+ _divisor = divisor._value;
+ _expected = expected._value;
}
+ }
- value = 0;
- return false;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryEvaluate(EvaluationContext context, out bool equal)
+ {
+ equal = false;
+
+ var identifier = _identifier;
+ if (identifier is null || context.OperatorOverloadingAllowed)
+ {
+ return false;
+ }
+
+ var engine = context.Engine;
+ if (engine.ExecutionContext.Suspendable is not null)
+ {
+ return false;
+ }
+
+ var env = engine.ExecutionContext.LexicalEnvironment;
+ if (!_slotCache.TryResolve(engine, env, identifier.Identifier, out var environment, out var slotIndex)
+ || !environment.TryGetNumberSlotForRead(slotIndex, out var value))
+ {
+ if (identifier._cachedGlobalEnv is null || !TryReadGlobalNumber(identifier, engine, env, out value))
+ {
+ return false;
+ }
+ }
+
+ equal = value % _divisor == _expected;
+ return true;
}
}
@@ -398,10 +458,12 @@ internal static void ValidateBigIntPowSize(Realm realm, BigInteger baseValue, in
private sealed class StrictlyEqualBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;
+ private ModuloEqualityLane _moduloLane;
public StrictlyEqualBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
+ _moduloLane.Initialize(_left, _right);
}
protected override object EvaluateInternal(EvaluationContext context)
@@ -411,6 +473,11 @@ protected override object EvaluateInternal(EvaluationContext context)
return unboxedLeft == unboxedRight ? JsBoolean.True : JsBoolean.False;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return moduloEqual ? JsBoolean.True : JsBoolean.False;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
@@ -427,6 +494,11 @@ public override bool GetBooleanValue(EvaluationContext context)
return unboxedLeft == unboxedRight;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return moduloEqual;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
@@ -439,10 +511,12 @@ public override bool GetBooleanValue(EvaluationContext context)
private sealed class StrictlyNotEqualBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;
+ private ModuloEqualityLane _moduloLane;
public StrictlyNotEqualBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
+ _moduloLane.Initialize(_left, _right);
}
protected override object EvaluateInternal(EvaluationContext context)
@@ -452,6 +526,11 @@ protected override object EvaluateInternal(EvaluationContext context)
return unboxedLeft == unboxedRight ? JsBoolean.False : JsBoolean.True;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return moduloEqual ? JsBoolean.False : JsBoolean.True;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
@@ -467,6 +546,11 @@ public override bool GetBooleanValue(EvaluationContext context)
return unboxedLeft != unboxedRight;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return !moduloEqual;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
@@ -848,11 +932,13 @@ private sealed class EqualBinaryExpression : JintBinaryExpression
{
private readonly bool _invert;
private NumericConstantComparisonLane _numericLane;
+ private ModuloEqualityLane _moduloLane;
public EqualBinaryExpression(NonLogicalBinaryExpression expression, bool invert = false) : base(expression)
{
_invert = invert;
_numericLane.Initialize(_left, _right);
+ _moduloLane.Initialize(_left, _right);
}
protected override object EvaluateInternal(EvaluationContext context)
@@ -862,6 +948,11 @@ protected override object EvaluateInternal(EvaluationContext context)
return (unboxedLeft == unboxedRight) == !_invert ? JsBoolean.True : JsBoolean.False;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return moduloEqual == !_invert ? JsBoolean.True : JsBoolean.False;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
@@ -889,6 +980,11 @@ public override bool GetBooleanValue(EvaluationContext context)
return _invert ? !equal : equal;
}
+ if (_moduloLane.TryEvaluate(context, out var moduloEqual))
+ {
+ return _invert ? !moduloEqual : moduloEqual;
+ }
+
if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;