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
33 changes: 33 additions & 0 deletions Jint.Benchmark/LoopDispatchBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class LoopDispatchBenchmarks
private Prepared<Script> _localCopy;
private Prepared<Script> _stringAppend;
private Prepared<Script> _comparisonOnly;
private Prepared<Script> _strictEqualTest;
private Prepared<Script> _looseEqualTest;
private Prepared<Script> _moduloEqualTest;

[GlobalSetup]
public void Setup()
Expand Down Expand Up @@ -60,13 +63,34 @@ public void Setup()
f();
""");

// + one strict-equality test (=== over a slot and a constant)
_strictEqualTest = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { if (i === 50000) { } } return n; }
f();
""");

// + one loose-equality test (== over a slot and a constant)
_looseEqualTest = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { if (i == 50000) { } } return n; }
f();
""");

// + the stopwatch.js if-chain shape: modulo of a slot vs a constant, loosely compared
_moduloEqualTest = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { if (i % 2 == 0) { } } return n; }
f();
""");

_engine = new Engine();
_engine.Evaluate(_emptyLoop);
_engine.Evaluate(_variableBoundLoop);
_engine.Evaluate(_counterAdd);
_engine.Evaluate(_localCopy);
_engine.Evaluate(_stringAppend);
_engine.Evaluate(_comparisonOnly);
_engine.Evaluate(_strictEqualTest);
_engine.Evaluate(_looseEqualTest);
_engine.Evaluate(_moduloEqualTest);
}

[Benchmark(Baseline = true)]
Expand All @@ -86,4 +110,13 @@ public void Setup()

[Benchmark]
public JsValue ComparisonOnly() => _engine.Evaluate(_comparisonOnly);

[Benchmark]
public JsValue StrictEqualTest() => _engine.Evaluate(_strictEqualTest);

[Benchmark]
public JsValue LooseEqualTest() => _engine.Evaluate(_looseEqualTest);

[Benchmark]
public JsValue ModuloEqualTest() => _engine.Evaluate(_moduloEqualTest);
}
80 changes: 80 additions & 0 deletions Jint.Tests/Runtime/EqualityLaneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the semantics of the unboxed slot-number lane on ==/===/!=/!== (JintBinaryExpression):
/// the lane engages for slot-stored numbers compared against numeric constants or other slots,
/// and must reproduce the generic path exactly, declining for any non-number operand.
/// </summary>
public class EqualityLaneTests
{
[Fact]
public void SlotNumberEqualityMatchesSpecForSpecialValues()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var nan = NaN, pz = 0, nz = -0, one = 1;
return [
nan === nan, nan !== nan, nan == nan, nan != nan,
pz === nz, pz == nz, pz !== nz,
one === 1, one == 1, one !== 1, one != 1
].join(',');
})()
""").AsString();

Assert.Equal("false,true,false,true,true,true,false,true,true,false,false", result);
}

[Fact]
public void LaneDeclinesWhenSlotTypeChangesMidLoop()
{
var engine = new Engine();
// x starts as a number (lane engages), then becomes a string mid-loop; the
// per-evaluation slot probe must fall back to the coercing path
var result = engine.Evaluate("""
(function () {
var x = 1, hits = 0;
for (var i = 0; i < 4; i++) {
if (x == 1) { hits++; }
if (i === 1) { x = '1'; }
}
return hits;
})()
""").AsNumber();

Assert.Equal(4, result); // '1' == 1 keeps matching through loose coercion
}

[Fact]
public void BigIntAndMixedOperandsBailToGenericEquality()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var big = 1n, other = 1n, num = 1, str = '1', undef;
return [
big === other, big !== other, big == num, big === num,
str == num, str === num,
undef == null, undef === null
].join(',');
})()
""").AsString();

Assert.Equal("true,false,true,false,true,false,true,false", result);
}

[Fact]
public void EqualityLaneWorksInValuePositions()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = 2, b = 2, c = 3;
var r1 = (a === b), r2 = (a !== c), r3 = (a == b), r4 = (a != c);
return r1 && r2 && r3 && r4;
})()
""").AsBoolean();

Assert.True(result);
}
}
44 changes: 43 additions & 1 deletion Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ private JintBinaryExpression(NonLogicalBinaryExpression expression) : base(expre
/// ping-ponging between its update (stores raw) and its loop test (would materialize).
/// Slot reads are pure, so declining after a partial read has no observable effect.
/// IEEE double comparisons reproduce the abstract relational operator for all four forms,
/// including NaN operands (spec result undefined, coerced to false).
/// including NaN operands (spec result undefined, coerced to false). For the equality
/// operators the operands are runtime-proven Numbers, so ==/===/!=/!== degenerate to the
/// numeric compare (IsLooselyEqual step 1 defers to IsStrictlyEqual for same-type operands):
/// IEEE == gives NaN == NaN false and +0 == -0 true, both spec-exact.
/// </summary>
private protected struct NumericConstantComparisonLane
{
Expand Down Expand Up @@ -364,12 +367,20 @@ internal static void ValidateBigIntPowSize(Realm realm, BigInteger baseValue, in

private sealed class StrictlyEqualBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;

public StrictlyEqualBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft == unboxedRight ? JsBoolean.True : JsBoolean.False;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
Expand All @@ -381,6 +392,11 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft == unboxedRight;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
Expand All @@ -392,12 +408,20 @@ public override bool GetBooleanValue(EvaluationContext context)

private sealed class StrictlyNotEqualBinaryExpression : JintBinaryExpression
{
private NumericConstantComparisonLane _numericLane;

public StrictlyNotEqualBinaryExpression(NonLogicalBinaryExpression expression) : base(expression)
{
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft == unboxedRight ? JsBoolean.False : JsBoolean.True;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
Expand All @@ -408,6 +432,11 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return unboxedLeft != unboxedRight;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
Expand Down Expand Up @@ -788,14 +817,21 @@ protected override object EvaluateInternal(EvaluationContext context)
private sealed class EqualBinaryExpression : JintBinaryExpression
{
private readonly bool _invert;
private NumericConstantComparisonLane _numericLane;

public EqualBinaryExpression(NonLogicalBinaryExpression expression, bool invert = false) : base(expression)
{
_invert = invert;
_numericLane.Initialize(_left, _right);
}

protected override object EvaluateInternal(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
return (unboxedLeft == unboxedRight) == !_invert ? JsBoolean.True : JsBoolean.False;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return JsValue.Undefined;
Expand All @@ -817,6 +853,12 @@ protected override object EvaluateInternal(EvaluationContext context)

public override bool GetBooleanValue(EvaluationContext context)
{
if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight))
{
var equal = unboxedLeft == unboxedRight;
return _invert ? !equal : equal;
}

if (!TryEvaluateOperands(context, out var left, out var right))
{
return false;
Expand Down
Loading