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
13 changes: 13 additions & 0 deletions Jint/Runtime/Environments/DeclarativeEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,19 @@ internal sealed override void CreateImmutableBinding(Key name, bool strict = tru
_dictionary.CreateImmutableBinding(name, strict);
}

/// <summary>
/// Slot-lane variant of <see cref="InitializeBinding"/> for lexical declarations whose slot
/// index was already resolved against this environment's slot layout by the caller. Only
/// valid for let/const targets (DisposeHint.Normal); mirrors the slot arm of
/// <see cref="InitializeBinding"/> exactly.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void InitializeSlotBinding(int index, JsValue value)
{
ref var binding = ref _slots![index];
binding = binding.ChangeValue(value);
}

internal sealed override void InitializeBinding(Key name, JsValue value, DisposeHint hint)
{
if (_slots is not null)
Expand Down
6 changes: 4 additions & 2 deletions Jint/Runtime/ExecutionContextStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ public void ReplaceTopLexicalEnvironment(Environment newEnv)
var array = _stack._array;
var size = _stack._size;
ref var executionContext = ref array[size - 1];
executionContext = executionContext.UpdateLexicalEnvironment(newEnv);
// In-place single-field write instead of rebuilding the whole 10-field struct; the
// instance lives in a mutable array slot, so casting away the field's readonly is safe.
Unsafe.AsRef(in executionContext.LexicalEnvironment) = newEnv;
}

public void ReplaceTopVariableEnvironment(Environment newEnv)
{
var array = _stack._array;
var size = _stack._size;
ref var executionContext = ref array[size - 1];
executionContext = executionContext.UpdateVariableEnvironment(newEnv);
Unsafe.AsRef(in executionContext.VariableEnvironment) = newEnv;
}

public void ReplaceTopPrivateEnvironment(PrivateEnvironment? newEnv)
Expand Down
19 changes: 19 additions & 0 deletions Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ public Completion ExecuteBlock(EvaluationContext context)
data.OuterEnvironment = oldEnv;
}
}
else if (!blockEnv.HasDisposeResources)
{
// Nothing was registered for dispose (no using/await-using declaration ran), so the
// dispose state machine is a no-op — finalize inline instead of round-tripping a
// DisposeStepResult through CompleteDispose. Mirrors its finalize arm exactly.
suspendable?.Data.Clear(this);
if (oldEnv is not null)
{
engine.UpdateLexicalEnvironment(oldEnv);

if (blockEnv._slots is not null)
{
blockEnv._outerEnv = null;
ResetSlots(blockEnv._slots, _blockState.SlotTemplates!);
_cachedEnv = blockEnv;
}
}
return blockValue;
}
else
{
return CompleteDispose(
Expand Down
131 changes: 110 additions & 21 deletions Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Jint.Native;
using Jint.Native.Function;
using Jint.Runtime.Environments;
using Jint.Runtime.Interpreter.Expressions;

namespace Jint.Runtime.Interpreter.Statements;
Expand All @@ -15,6 +16,27 @@ private sealed class ResolvedDeclaration
internal JintExpression? Init;
internal JintIdentifierExpression? LeftIdentifierExpression;
internal bool EvalOrArguments;
internal bool CanUseSlotLane;
internal SlotLane? Lane;
}

/// <summary>
/// Monomorphic cache mapping a slotted environment's layout (by <see cref="Key"/>-array
/// identity, stable across engines for shared prepared scripts) to the declared name's slot
/// index, so repeated lexical initialization skips the Reference rent/return and name scan.
/// A single reference field keeps publication atomic on shared handler trees; Index -1
/// records "no slot in this layout" so misses don't rescan.
/// </summary>
private sealed class SlotLane
{
internal readonly Key[] SlotNames;
internal readonly int Index;

internal SlotLane(Key[] slotNames, int index)
{
SlotNames = slotNames;
Index = index;
}
}

public JintVariableDeclaration(VariableDeclaration statement) : base(statement)
Expand Down Expand Up @@ -43,13 +65,24 @@ public JintVariableDeclaration(VariableDeclaration statement) : base(statement)
}

var leftIdentifier = left as JintIdentifierExpression;

// let/const identifier targets whose initializer needs no naming side channel
// (function definitions and class expressions call SetFunctionName/EvaluateWithName
// with the reference's name) can initialize straight into a resolved slot.
var canUseSlotLane = statement.Kind is VariableDeclarationKind.Let or VariableDeclarationKind.Const
&& leftIdentifier is not null
&& leftIdentifier.HasEvalOrArguments == false
&& (declaration.Init is null
|| (declaration.Init is not ClassExpression && !declaration.Init.IsFunctionDefinition()));

_declarations[i] = new ResolvedDeclaration
{
Left = left,
LeftPattern = pattern,
LeftIdentifierExpression = leftIdentifier,
EvalOrArguments = leftIdentifier?.HasEvalOrArguments == true,
Init = init
Init = init,
CanUseSlotLane = canUseSlotLane,
};
}
}
Expand All @@ -61,34 +94,51 @@ protected override Completion ExecuteInternal(EvaluationContext context)
{
if (_statement.Kind != VariableDeclarationKind.Var && declaration.Left != null)
{
var lhs = (Reference) declaration.Left.Evaluate(context);
var value = JsValue.Undefined;
if (declaration.Init != null)
// Slot lane: the declaration's own binding lives in the current environment by
// construction (blocks/loops instantiate their env before executing statements),
// so when that env stores bindings in slots we can initialize the slot directly
// and skip the Reference rent/evaluate/return round-trip and the name scan.
if (declaration.CanUseSlotLane
&& engine.ExecutionContext.LexicalEnvironment is DeclarativeEnvironment denv
&& denv._slotNames is not null)
{
if (declaration.Init is JintClassExpression classExpr && declaration.Init._expression.IsAnonymousFunctionDefinition())
{
value = classExpr.EvaluateWithName(context, lhs.ReferencedName.ToString()).Clone();
}
else
var lane = declaration.Lane;
if (lane is null || !ReferenceEquals(lane.SlotNames, denv._slotNames))
{
value = declaration.Init.GetValue(context).Clone();
declaration.Lane = lane = new SlotLane(
denv._slotNames,
denv.FindSlotIndex(declaration.LeftIdentifierExpression!.Identifier.Key));
}

// Check for generator suspension after evaluating initializer
if (context.IsSuspended())
{
engine._referencePool.Return(lhs);
return new Completion(CompletionType.Normal, value, _statement);
}

if (declaration.Init._expression.IsFunctionDefinition() && declaration.Init is not JintClassExpression)
if (lane.Index >= 0)
{
((Function) value).SetFunctionName(lhs.ReferencedName);
JsValue laneValue;
if (declaration.Init is null)
{
laneValue = JsValue.Undefined;
}
else
{
laneValue = declaration.Init.GetValue(context).Clone();

// Check for generator suspension after evaluating initializer
if (context.IsSuspended())
{
return new Completion(CompletionType.Normal, laneValue, _statement);
}
}

denv.InitializeSlotBinding(lane.Index, laneValue);
continue;
}
}

lhs.InitializeReferencedBinding(value, _statement.Kind.GetDisposeHint());
engine._referencePool.Return(lhs);
// Slow arm kept out-of-line so this method stays compact for the var path.
var completion = InitializeLexicalBindingSlow(context, engine, declaration);
if (completion is not null)
{
return completion.Value;
}
}
else if (declaration.Init != null)
{
Expand Down Expand Up @@ -160,4 +210,43 @@ protected override Completion ExecuteInternal(EvaluationContext context)

return Completion.Empty();
}

/// <summary>
/// Initializes a lexical (let/const/using) identifier binding through the Reference-based
/// path (targets the slot lane can't serve). Returns a completion only when the initializer
/// suspended (generator/async); null means the binding was initialized and the caller
/// continues with the next declarator.
/// </summary>
private Completion? InitializeLexicalBindingSlow(EvaluationContext context, Engine engine, ResolvedDeclaration declaration)
{
var lhs = (Reference) declaration.Left!.Evaluate(context);
var value = JsValue.Undefined;
if (declaration.Init != null)
{
if (declaration.Init is JintClassExpression classExpr && declaration.Init._expression.IsAnonymousFunctionDefinition())
{
value = classExpr.EvaluateWithName(context, lhs.ReferencedName.ToString()).Clone();
}
else
{
value = declaration.Init.GetValue(context).Clone();
}

// Check for generator suspension after evaluating initializer
if (context.IsSuspended())
{
engine._referencePool.Return(lhs);
return new Completion(CompletionType.Normal, value, _statement);
}

if (declaration.Init._expression.IsFunctionDefinition() && declaration.Init is not JintClassExpression)
{
((Function) value).SetFunctionName(lhs.ReferencedName);
}
}

lhs.InitializeReferencedBinding(value, _statement.Kind.GetDisposeHint());
engine._referencePool.Return(lhs);
return null;
}
}
Loading