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
110 changes: 110 additions & 0 deletions Jint.Tests/Runtime/EqualityLaneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
32 changes: 32 additions & 0 deletions Jint/Runtime/Environments/DeclarativeEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,38 @@ internal bool TryGetNumberSlot(int slotIndex, out double value)
return false;
}

/// <summary>
/// Read-only sibling of <see cref="TryGetNumberSlot"/> 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 <see cref="SetNumberSlot"/>.
/// </summary>
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;
}

/// <summary>
/// Stores a raw number into a slot previously validated by <see cref="TryGetNumberSlot"/>
/// without materializing a JsNumber.
Expand Down
126 changes: 111 additions & 15 deletions Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TryReadGlobalNumber(JintIdentifierExpression identifier, Engine engine, Environments.Environment env, out double value)
/// <summary>
/// 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 <see cref="JintIdentifierExpression._cachedGlobalEnv"/> for null.
/// </summary>
[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;
}

/// <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.
/// </summary>
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;
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading