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