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;