diff --git a/Jint/Native/Function/ScriptFunction.cs b/Jint/Native/Function/ScriptFunction.cs
index 3b3f3dda2d..cd62b07a8f 100644
--- a/Jint/Native/Function/ScriptFunction.cs
+++ b/Jint/Native/Function/ScriptFunction.cs
@@ -75,9 +75,9 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg
Throw.TypeError(calleeContext.Realm, $"Class constructor {_functionDefinition.Name} cannot be invoked without 'new'");
}
- // Check if slot array can be cached after this call
+ // Capture funcEnv for end-of-call pool return when bindings can't escape
state = _functionDefinition.Initialize();
- if (state is { UseFixedSlots: true, EnvironmentMayEscape: false })
+ if (state is { EnvironmentMayEscape: false })
{
funcEnv = (FunctionEnvironment) calleeContext.LexicalEnvironment;
}
@@ -120,12 +120,19 @@ protected internal override JsValue Call(JsValue thisObject, JsCallArguments arg
}
finally
{
- // Cache the slot array for reuse by next call to the same function (thread-safe)
- if (funcEnv?._slots is { } slots)
+ if (funcEnv is not null)
{
- System.Array.Clear(slots, 0, slots.Length);
- Interlocked.Exchange(ref state!._cachedSlots, slots);
- funcEnv._slots = null;
+ // Cache the slot array for reuse by next call to the same function (thread-safe)
+ if (funcEnv._slots is { } slots)
+ {
+ System.Array.Clear(slots, 0, slots.Length);
+ Interlocked.Exchange(ref state!._cachedSlots, slots);
+ funcEnv._slots = null;
+ }
+
+ // Return the env itself to the per-State pool so the next call to a function
+ // sharing this State (typically the same Function instance) avoids the allocation.
+ Interlocked.Exchange(ref state!._cachedEnv, funcEnv);
}
_engine.LeaveExecutionContext();
}
diff --git a/Jint/Runtime/Environments/DeclarativeEnvironment.cs b/Jint/Runtime/Environments/DeclarativeEnvironment.cs
index fee93dd062..a613a35432 100644
--- a/Jint/Runtime/Environments/DeclarativeEnvironment.cs
+++ b/Jint/Runtime/Environments/DeclarativeEnvironment.cs
@@ -353,6 +353,14 @@ public void Clear()
_dictionary = null;
}
+ ///
+ /// Reset for env-pool reuse: drop any using/await-using disposable resources tracked from the previous call.
+ ///
+ internal void ClearDisposeCapability()
+ {
+ _disposeCapability = null;
+ }
+
internal void TransferTo(List names, DeclarativeEnvironment env)
{
var source = _dictionary!;
diff --git a/Jint/Runtime/Environments/FunctionEnvironment.cs b/Jint/Runtime/Environments/FunctionEnvironment.cs
index 6a9ff5e03c..2ce173af2f 100644
--- a/Jint/Runtime/Environments/FunctionEnvironment.cs
+++ b/Jint/Runtime/Environments/FunctionEnvironment.cs
@@ -23,7 +23,8 @@ private enum ThisBindingStatus
private JsValue? _thisValue;
private ThisBindingStatus _thisBindingStatus;
- internal readonly Function _functionObject;
+ // Mutable so a pooled env can be re-bound to a different Function instance that shares the same State (e.g. function literals re-evaluated in loops).
+ internal Function _functionObject;
public FunctionEnvironment(
Engine engine,
@@ -42,6 +43,23 @@ public FunctionEnvironment(
}
}
+ ///
+ /// Re-initialize a pooled FunctionEnvironment for a new call. Leaves slot/dictionary handling
+ /// to the caller — they may either clear in place or replace with fresh storage.
+ ///
+ internal void Reset(Function functionObject, JsValue newTarget, Environment? outerEnv)
+ {
+ _functionObject = functionObject;
+ NewTarget = newTarget;
+ _outerEnv = outerEnv;
+ _thisValue = null;
+ _thisBindingStatus = functionObject._functionDefinition?.Function.Type is NodeType.ArrowFunctionExpression
+ ? ThisBindingStatus.Lexical
+ : ThisBindingStatus.Uninitialized;
+ _dictionary?.Clear();
+ ClearDisposeCapability();
+ }
+
internal override bool HasThisBinding() => _thisBindingStatus != ThisBindingStatus.Lexical;
diff --git a/Jint/Runtime/Environments/JintEnvironment.cs b/Jint/Runtime/Environments/JintEnvironment.cs
index 13fb4002cf..0585fa8685 100644
--- a/Jint/Runtime/Environments/JintEnvironment.cs
+++ b/Jint/Runtime/Environments/JintEnvironment.cs
@@ -77,12 +77,27 @@ internal static DeclarativeEnvironment NewDeclarativeEnvironment(Engine engine,
///
internal static FunctionEnvironment NewFunctionEnvironment(Engine engine, Function f, JsValue newTarget)
{
- var env = new FunctionEnvironment(engine, f, newTarget)
+ var state = f._functionDefinition?.Initialize();
+ FunctionEnvironment env;
+
+ // Reuse a pooled FunctionEnvironment when the function's bindings cannot escape the call
+ // (no closure capture, not a generator/async). Re-bind to the new function/target/outer env
+ // and reset transient state. Slot storage is handled below.
+ if (state is { EnvironmentMayEscape: false }
+ && Interlocked.Exchange(ref state._cachedEnv, null) is { } cachedEnv
+ && ReferenceEquals(cachedEnv._engine, engine))
{
- _outerEnv = f._environment
- };
+ cachedEnv.Reset(f, newTarget, f._environment);
+ env = cachedEnv;
+ }
+ else
+ {
+ env = new FunctionEnvironment(engine, f, newTarget)
+ {
+ _outerEnv = f._environment,
+ };
+ }
- var state = f._functionDefinition?.Initialize();
if (state is { UseFixedSlots: true })
{
env._slotNames = state.SlotNames;
diff --git a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
index 729766e51f..14d0d99839 100644
--- a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
+++ b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs
@@ -309,6 +309,7 @@ internal sealed class State
public bool CanUseFastFDI;
public bool EnvironmentMayEscape;
public Binding[]? _cachedSlots;
+ public Environments.FunctionEnvironment? _cachedEnv;
public SourceText SourceText;
@@ -558,18 +559,27 @@ internal static State BuildState(IFunction function, string? fullSourceText = nu
state.VarSlotCount = varsToInitialize.Count;
state.UseFixedSlots = true;
state.CanUseFastFDI = lexicalBindingCount == 0;
-
- if (function.Generator || function.Async)
- {
- state.EnvironmentMayEscape = true;
- }
- else
- {
- state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscapeWithReferences(function, slotNames);
- }
}
}
+ // Compute EnvironmentMayEscape unconditionally so consumers (e.g. FunctionEnvironment pooling)
+ // can rely on it without first checking UseFixedSlots. Generators / async functions / direct eval
+ // always escape; otherwise inspect the body. When the function qualified for fixed slots, prefer
+ // the slot-aware analysis (only escapes if a closure actually references a slot variable);
+ // otherwise fall back to the conservative "any inner closure means escape" check.
+ if (function.Generator || function.Async || state.NeedsEvalContext)
+ {
+ state.EnvironmentMayEscape = true;
+ }
+ else if (state.UseFixedSlots)
+ {
+ state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscapeWithReferences(function, state.SlotNames!);
+ }
+ else
+ {
+ state.EnvironmentMayEscape = EnvironmentEscapeAstVisitor.MayEscape(function);
+ }
+
state.SourceText = new SourceText(fullSourceText);
return state;