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
34 changes: 34 additions & 0 deletions Jint.Benchmark/LoopDispatchBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class LoopDispatchBenchmarks
private Prepared<Script> _strictEqualTest;
private Prepared<Script> _looseEqualTest;
private Prepared<Script> _moduloEqualTest;
private Prepared<Script> _ifChainLoop;
private Prepared<Script> _varDeclBody;
private Prepared<Script> _arrayLengthBound;
private Prepared<Script> _stringLengthBound;

Expand Down Expand Up @@ -83,6 +85,30 @@ public void Setup()
f();
""");

// the full stopwatch.js body shape: var-decl + 4-way modulo else-if chain whose branches
// call tiny closures + two dead member-read var-decls
_ifChainLoop = Engine.PrepareScript("""
function f() {
var c = { n: 0, a: function () { this.n++; }, b: function () { this.n--; } };
for (var i = 0; i < 100000; i++) {
var z = i ^ 3;
if (z % 2 == 0) c.a();
else if (z % 3 == 0) c.b();
else if (z % 5 == 0) c.a();
else if (z % 7 == 0) c.b();
var v = c.n;
}
return c.n;
}
f();
""");

// + one var declaration with an initializer (the `var z = x ^ y` statement alone)
_varDeclBody = Engine.PrepareScript("""
function f() { var n = 0; for (var i = 0; i < 100000; i++) { var z = i ^ 3; } return n; }
f();
""");

// the member-bound loop test: `i < a.length` re-reads the live length every iteration
// (6250 × 16 = 100k inner iterations)
_arrayLengthBound = Engine.PrepareScript("""
Expand All @@ -107,6 +133,8 @@ public void Setup()
_engine.Evaluate(_strictEqualTest);
_engine.Evaluate(_looseEqualTest);
_engine.Evaluate(_moduloEqualTest);
_engine.Evaluate(_ifChainLoop);
_engine.Evaluate(_varDeclBody);
_engine.Evaluate(_arrayLengthBound);
_engine.Evaluate(_stringLengthBound);
}
Expand Down Expand Up @@ -138,6 +166,12 @@ public void Setup()
[Benchmark]
public JsValue ModuloEqualTest() => _engine.Evaluate(_moduloEqualTest);

[Benchmark]
public JsValue IfChainLoop() => _engine.Evaluate(_ifChainLoop);

[Benchmark]
public JsValue VarDeclBody() => _engine.Evaluate(_varDeclBody);

[Benchmark]
public JsValue ArrayLengthBound() => _engine.Evaluate(_arrayLengthBound);

Expand Down
211 changes: 211 additions & 0 deletions Jint.Tests/Runtime/TightLoopTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,215 @@ public void ConstraintConfiguredEngineStillEnforcesInsideLoops()
Assert.Throws<Jint.Runtime.StatementsCountOverflowException>(() =>
engine.Evaluate("(function () { var x = 0; for (var i = 0; i < 100000; i++) { x += 1; } })()"));
}

[Fact]
public void IfChainBodyMatchesGenericPathExactly()
{
var engine = new Engine();
// same computation twice: the first shape is tight-eligible (if/else chain + var decls),
// the second carries a never-taken break that structurally disqualifies it, forcing the
// generic path — both must agree
var result = engine.Evaluate("""
(function () {
function tightShape() {
var counts = [0, 0, 0, 0, 0];
for (var x = 0; x < 200; x++) {
var z = x ^ 3;
if (z % 2 == 0) counts[0]++;
else if (z % 3 == 0) counts[1]++;
else if (z % 5 == 0) counts[2]++;
else if (z % 7 == 0) counts[3]++;
else counts[4]++;
var v = counts.length;
}
return counts.join(',');
}
function genericShape() {
var counts = [0, 0, 0, 0, 0];
for (var x = 0; x < 200; x++) {
var z = x ^ 3;
if (z % 2 == 0) counts[0]++;
else if (z % 3 == 0) counts[1]++;
else if (z % 5 == 0) counts[2]++;
else if (z % 7 == 0) counts[3]++;
else counts[4]++;
var v = counts.length;
if (x > 100000) break;
}
return counts.join(',');
}
var a = tightShape();
var b = genericShape();
return (a === b) + ':' + a;
})()
""").AsString();

Assert.StartsWith("true:", result);
}

[Fact]
public void IfChainBodyRunsTightAtTopLevelWithTrailingStatement()
{
var engine = new Engine();
// trailing expression statement kills the loop's completion value → tight path engages
// at script top level; the loop calls closures exactly like the stopwatch benchmark
var result = engine.Evaluate("""
var n = 0;
var o = { inc: function () { n++; }, dec: function () { n--; } };
for (var x = 0; x < 100; x++) {
var z = x ^ 5;
if (z % 2 == 0) o.inc();
else if (z % 3 == 0) o.dec();
var v = o.inc;
}
n;
""").AsNumber();

var expected = 0;
for (var x = 0; x < 100; x++)
{
var z = x ^ 5;
if (z % 2 == 0) expected++;
else if (z % 3 == 0) expected--;
}

Assert.Equal(expected, result);
}

[Fact]
public void ThrowInsideTakenBranchPropagatesWithState()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var i = 0, ran = 0;
try {
for (i = 0; i < 10; i++) {
var z = i ^ 1;
if (z % 2 == 0) ran++;
else mustNotExist();
}
} catch (e) {
return (e instanceof ReferenceError) + ':' + i + ':' + ran;
}
return 'no-throw';
})()
""").AsString();

// i=0 → z=1 → else branch throws before any increment
Assert.Equal("true:0:0", result);
}

[Fact]
public void NestedBranchBlockStopsAfterThrowingStatement()
{
var engine = new Engine();
// inside a multi-statement branch block, statements after a throwing one must not run
var result = engine.Evaluate("""
(function () {
var a = 0, b = 0;
try {
for (var i = 0; i < 3; i++) {
if (true) { a++; mustNotExist(); b++; }
}
} catch (e) {
return a + ':' + b;
}
return 'no-throw';
})()
""").AsString();

Assert.Equal("1:0", result);
}

[Fact]
public void BreakContinueReturnBodiesKeepExactSemantics()
{
var engine = new Engine();
// break/continue/return statements structurally disqualify the tight path; semantics
// must be untouched
var result = engine.Evaluate("""
(function () {
var s = '';
for (var i = 0; i < 5; i++) {
if (i === 2) continue;
if (i === 4) break;
s += i;
}
function returner() {
for (var j = 0; j < 10; j++) {
if (j === 3) return 'r' + j;
}
return 'end';
}
return s + ':' + i + ':' + returner();
})()
""").AsString();

Assert.Equal("013:4:r3", result);
}

[Fact]
public void FlattenedConstBodyKeepsPerIterationTdz()
{
var engine = new Engine();
// let-header + const-body loops flatten into the pooled loop env and stay tight-eligible;
// the body slots must be re-TDZ'd each iteration, so a read before the const initializes
// throws — also on iterations after the first (stale value from iteration 1 must not leak)
var untaken = engine.Evaluate("""
(function () {
let total = 0;
for (let i = 0; i < 3; i++) {
if (false) z;
const z = i * 2;
total += z;
}
return total;
})()
""").AsNumber();
Assert.Equal(6, untaken);

var secondIteration = engine.Evaluate("""
(function () {
try {
for (let i = 0; i < 3; i++) {
if (i === 1) z;
const z = i;
}
} catch (e) {
return (e instanceof ReferenceError) + ':tdz';
}
return 'no-throw';
})()
""").AsString();
Assert.Equal("true:tdz", secondIteration);
}

[Fact]
public void ConstraintConfiguredEngineStillEnforcesInsideIfChainLoops()
{
var engine = new Engine(options => options.MaxStatements(50));
Assert.Throws<Jint.Runtime.StatementsCountOverflowException>(() =>
engine.Evaluate("(function () { var x = 0; for (var i = 0; i < 100000; i++) { if (i % 2 == 0) x += 1; else x -= 1; } })()"));
}

[Fact]
public void UsingDeclarationBodyDisposesPerIteration()
{
var engine = new Engine();
// using declarations are excluded from the tight shape; per-iteration dispose ordering
// must be untouched
var result = engine.Evaluate("""
(function () {
var log = [];
for (var i = 0; i < 2; i++) {
using r = { [Symbol.dispose]() { log.push('d' + i); } };
log.push('u' + i);
}
return log.join(',');
})()
""").AsString();

Assert.Equal("u0,d0,u1,d1", result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@ public JintConditionalExpression(ConditionalExpression expression) : base(expres

protected override object EvaluateInternal(EvaluationContext context)
{
JsValue testValue;
var suspendable = context.Engine?.ExecutionContext.Suspendable;

// In a plain synchronous frame nothing can suspend or resume mid-expression, so the test
// can take the unboxed boolean path (comparison lanes return raw bools) instead of
// materializing a JsValue only to feed TypeConverter.ToBoolean.
if (suspendable is null && context.Engine is not null)
{
return _test.GetBooleanValue(context)
? _consequent.GetValue(context)
: _alternate.GetValue(context);
}

JsValue testValue;
if (suspendable is { IsResuming: true }
&& suspendable.Data.TryGet(this, out LeftOperandSuspendData? suspendData))
{
Expand Down
26 changes: 26 additions & 0 deletions Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,32 @@ private void SetupDisposeSuspension(Engine engine, AsyncFunctionInstance asyncFn
internal Completion ExecuteFlattenedContents(EvaluationContext context)
=> _statementList is not null ? _statementList.Execute(context) : ExecuteSingle(context);

/// <summary>
/// Tight-loop entry; see <see cref="JintStatement.ExecuteDiscarded"/>. Valid only for blocks
/// without lexical declarations (enforced by the enclosing loop's shape predicate), so no block
/// environment is created or attached. A statement after a deferred error must not run, so the
/// engine error slot is polled between statements; the caller polls after the last one.
/// </summary>
internal void ExecuteDiscardedContents(EvaluationContext context)
{
if (_singleStatement is not null)
{
_singleStatement.ExecuteDiscarded(context);
return;
}

var list = _statementList!;
var count = list.Count;
for (var i = 0; i < count; i++)
{
list.GetStatement(i).ExecuteDiscarded(context);
if (context.Engine._error is not null)
{
return;
}
}
}

private Completion ExecuteSingle(EvaluationContext context)
{
Completion blockValue;
Expand Down
4 changes: 4 additions & 0 deletions Jint/Runtime/Interpreter/Statements/JintEmptyStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ protected override Completion ExecuteInternal(EvaluationContext context)
{
return new Completion(CompletionType.Normal, JsEmpty.Instance, _statement);
}

internal override void ExecuteDiscarded(EvaluationContext context)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
/// Callers guarantee a non-suspendable frame, no constraint/debug checks and dead
/// completion values; expression evaluation still tracks the current node for errors.
/// </summary>
internal void ExecuteDiscarded(EvaluationContext context)
internal override void ExecuteDiscarded(EvaluationContext context)
{
if (_expressionCanDiscard)
{
Expand Down
Loading
Loading