diff --git a/Jint.Tests/Runtime/SlotLocationCacheTests.cs b/Jint.Tests/Runtime/SlotLocationCacheTests.cs
new file mode 100644
index 000000000..23e6972a7
--- /dev/null
+++ b/Jint.Tests/Runtime/SlotLocationCacheTests.cs
@@ -0,0 +1,90 @@
+namespace Jint.Tests.Runtime;
+
+///
+/// Pins SlotLocationCache behavior for handler trees shared across function instances
+/// (per-engine definition reuse): a node whose cached environment is unreachable from the
+/// current chain must re-resolve against it — declining forever would silently disable every
+/// slot-backed fast lane from the second re-evaluation of a prepared script onward.
+///
+public class SlotLocationCacheTests
+{
+ [Fact]
+ public void RepeatedPreparedEvaluationsKeepProducingCorrectResults()
+ {
+ var engine = new Engine();
+ var prepared = Engine.PrepareScript("""
+ function f() {
+ var n = 0;
+ for (var i = 0; i < 1000; i++) { n += i; }
+ return n;
+ }
+ f();
+ """);
+
+ // the cached-definition handler tree serves a fresh function instance (fresh env) per
+ // evaluation; each must resolve slots against its own chain
+ for (var e = 0; e < 5; e++)
+ {
+ Assert.Equal(499500, engine.Evaluate(prepared).AsNumber());
+ }
+ }
+
+ [Fact]
+ public void AlternatingFunctionInstancesSharingOneTreeStayCorrect()
+ {
+ var engine = new Engine();
+ // two instances of the same nested declaration alternate calls: the shared tree's
+ // slot caches flip between the instances' environments and must stay correct
+ var result = engine.Evaluate("""
+ (function () {
+ function make(base) {
+ function inner() {
+ var acc = 0;
+ for (var i = 0; i < 100; i++) { acc += base; }
+ return acc;
+ }
+ return inner;
+ }
+ var a = make(1), b = make(1000);
+ var sum = 0;
+ for (var r = 0; r < 4; r++) { sum += a() + b(); }
+ return sum;
+ })()
+ """).AsNumber();
+
+ Assert.Equal(4 * (100 + 100_000), result);
+ }
+
+#if NET
+ [Fact]
+ public void SlotLanesSurviveRepeatedPreparedEvaluations()
+ {
+ var engine = new Engine();
+ var prepared = Engine.PrepareScript("""
+ function f() {
+ var n = 0;
+ for (var i = 0; i < 20000; i++) { n += 1; }
+ return n;
+ }
+ f();
+ """);
+
+ // evaluation 1 uses the first-build tree, 2 the freshly cached tree; from 3 on the
+ // cached tree runs under a different environment than the one that populated its
+ // slot caches — the lanes must re-resolve and stay unboxed instead of falling to
+ // the materializing generic path (which allocates a JsNumber per iteration here)
+ for (var e = 0; e < 3; e++)
+ {
+ engine.Evaluate(prepared);
+ }
+
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ engine.Evaluate(prepared);
+ var perEvaluation = GC.GetAllocatedBytesForCurrentThread() - before;
+
+ Assert.True(
+ perEvaluation < 150_000,
+ $"expected slot lanes to stay live across re-evaluations; allocated {perEvaluation} bytes in one evaluation");
+ }
+#endif
+}
diff --git a/Jint/Runtime/Environments/SlotLocationCache.cs b/Jint/Runtime/Environments/SlotLocationCache.cs
index 250cbcd76..987527e3e 100644
--- a/Jint/Runtime/Environments/SlotLocationCache.cs
+++ b/Jint/Runtime/Environments/SlotLocationCache.cs
@@ -10,6 +10,10 @@ namespace Jint.Runtime.Environments;
/// Validity reasoning: closure-captured environments cannot be pooled (escape detection
/// prevents it), so a cached env reference is stable for the lifetime of the function
/// instance that captured it, and slot indices are immutable for the lifetime of the env.
+/// Handler trees are shared across function INSTANCES though (per-engine definition reuse
+/// for re-evaluated scripts, class members and nested declarations), so a node outlives any
+/// one instance's environment: an unreachable cached env means the node moved to another
+/// instance and must re-resolve against the current chain, not decline.
/// Slot layout is deterministic per AST node, so a node shared across engines via
/// Prepared<Script> resolves to the same index everywhere; the engine-identity
/// gate only avoids wasted walks against another engine's environments.
@@ -80,9 +84,13 @@ public bool TryResolve(
search = search._outerEnv;
}
- slotEnv = null!;
- slotIndex = -1;
- return false;
+ // The cached location is not reachable from this chain. Handler trees are shared
+ // across function instances (per-engine definition reuse for re-evaluated scripts,
+ // class members, nested declarations), so the same node runs under a fresh
+ // environment per instance — re-resolve against the current chain exactly as a
+ // fresh node would (mirroring the cross-engine Prepared