Slot-backed strict-eval environments with per-source pooling#2565
Merged
Conversation
EvalExecutionBenchmarks isolates the dromaeo-core-eval execution shape (hot/fresh eval, new Function, plain-function reference floor, parse-only bound); TimeProbe (--profile-time) splits dromaeo scripts per test() with Stopwatch, complementing MemoryProbe's allocation split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strict eval created a fresh dictionary-backed environment per call, so every identifier access in the eval body paid a dictionary probe and the per-AST-node slot caches (identifier reads, unboxed i++) could never engage - while the identical body under new Function() runs on fixed slots. dromaeo-core-eval spends about half of each op inside one such body. The eval compile-cache entry now precomputes the environment slot layout (deduplicated var + function names initialized to undefined, let/const as TDZ templates) plus the hoisting scope; PerformEval instantiates the strict-eval environment from the templates and EvalDeclarationInstantiation skips the var/lexical passes via a new bindingsPreInitialized flag. Bodies that cannot capture the environment (no closures, nested eval or with) pool it across calls, keeping the shared statement tree per-node caches valid. The layout is built only when it can amortize: a loop in the body, or a source seen twice (promotion). Function bookkeeping in EvalDeclarationInstantiation is now lazily allocated, and ShadowRealm reuses CachedHoistingScope for prepared scripts instead of re-walking the AST per eval. Same-window stash A/B: EvalHotReusedEngine -18.6%, EvalFreshEngine -17.5%, EvalSameSource -24.7% time / -47.9% alloc, EvalDistinctSources flat (miss guard), ShadowRealm_ParsedScript -9.2%, dromaeo-core-eval driver -8.5% source / -5.7% prepared; new Function and plain-function reference rows unchanged. Jint.Tests 3159/3097, PublicInterface 79/79, CommonScripts 28/28, Test262 99260/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code-review findings on the slot-backed eval work, all reproduced before
fixing:
The identifier read fast path's cached-slot walk hopped over intermediate
declarative environments by reference comparison only, so a node whose
cache pointed at an outer environment read a stale binding when executed
under a chain that shadows the name - reachable through the eval
compilation cache (the same body under a shadowing block or a nested
eval of the same source) and through sloppy direct eval injecting a var
into an enclosing function environment. The walk now probes each hopped
declarative environment with HasBinding (pure; the common hop-0 hit pays
nothing) and falls back to full resolution when an intermediate
environment owns the name.
EvalDeclarationInstantiation validated eval-declared var names with
CanDeclareGlobalFunction where the spec requires CanDeclareGlobalVar
(sec-evaldeclarationinstantiation step 8.a.ii.1), so eval("var undefined;")
and redeclaration of any existing non-configurable global threw a
spurious TypeError; pre-existing, but the lines live in this change.
TryPoolEvalEnvironment parked environments with the completed call's
binding values still in the slots, keeping arbitrary object graphs
reachable from the eval cache for the engine's lifetime; the slots are
now reset to templates (and the dictionary and dispose capability
cleared) at park time, and the rent path no longer needs per-rent
cleanup.
EvalScriptAnalyzer no longer lets loops inside arrow functions mark the
eval body as loop-bearing (arrows still descend for arguments/new.target
detection); those loops run in the arrow's own environment and cannot
amortize the eval body's slot layout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
|
Adversarial multi-agent review of the stack surfaced issues; fixed in fbd90a5:
Five regression tests cover the repro shapes. Full suites + Test262 99,260/0 re-verified on the stack. 🤖 Generated with Claude Code |
lahma
enabled auto-merge (squash)
July 4, 2026 08:25
This was referenced Jul 4, 2026
This was referenced Jul 6, 2026
This was referenced Jul 13, 2026
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Strict eval creates a fresh environment per call, and it was always dictionary-backed: every identifier access in the eval body paid a dictionary probe, and the per-AST-node slot caches (identifier reads, unboxed
i++updates) could never engage — while the identical body undernew Function()runs on fixed slots.dromaeo-core-evalspends about half of each op inside one such body.What this does
var+ function-declaration names initialized to undefined,let/constas TDZ templates) plus the hoisting scope, soPerformEvalinstantiates the strict-eval environment directly from templates.EvalDeclarationInstantiationgains the precomputedHoistingScopeparameter (kills a per-call AST walk) and abindingsPreInitializedflag that skips the var/lexical passes; its function bookkeeping (LinkedList/HashSet) is now allocated lazily — a win for all function-free evals, sloppy included.with— the existingEnvironmentEscapeAstVisitor.MayEscapegate) pool the environment across calls, keeping the shared statement tree's per-node slot caches valid on reused engines.CachedHoistingScopefor prepared scripts instead of re-walking the AST per call.Non-strict direct eval (vars land in the caller's environment) is untouched.
Measurements (same-window stash A/B, BenchmarkDotNet default job)
EvalExecutionBenchmarks(new in this PR) isolates the dromaeo-core-eval execution shape;--profile-time(new) splits dromaeo scripts pertest()with Stopwatch, complementing--profile-memory.Together with the follow-up compound-assignment PR, the EngineComparison
dromaeo-core-evalrow moves 2.507 → 1.941 ms (−22.6%) / prepared 2.480 → 1.909 ms (−23.0%), allocation flat (same-runtime worktree A/B).Gates
Jint.Tests3159/3097 (net10/net472),Jint.Tests.PublicInterface79/79,Jint.Tests.CommonScripts28/28 — all greenJint.csprojRelease buildEvalTestscover: pooled-env binding freshness, closures escaping eval (pooling gate), re-entrant same-source eval, function-declaration hoisting in slots, const TypeError, let TDZ, >cap fallback, sloppy leak semantics, indirect strict eval🤖 Generated with Claude Code