AGENTS.md · diff

git:20261012.ea5c615 to git:20261012.34201ab

11 added, 4 removed. Audit B to B.

# JSSE - JavaScript Engine in Rust
## Project Overview
A from-scratch JavaScript engine implemented in Rust. No JS parser/engine libraries allowed as dependencies — every detail must be implemented by us. Utility crates (parsing combinators, math, etc.) are fine.
**GitHub repo: `pmatos/jsse`** — always use this owner/repo for GitHub MCP calls.
**Ultimate goal: 100% test262 pass rate.**
## Repository Layout
- `spec/` — ECMAScript spec submodule (tc39/ecma262). **NEVER modify.**
- `test262/` — Test suite submodule (tc39/test262). **NEVER modify.**
- `tests/` — Custom tests that don't fit test262.
## Key Rules
1. **Never modify** files in `spec/` or `test262/` submodules.
2. No importing a JS parser or engine crate. Implement everything from scratch.
3. Dependencies for parsing utilities, math, etc. are allowed.
4. When implementing a feature, identify relevant test262 tests to validate against.
5. The spec is the ultimate source of truth with respect to JavaScript. Use it to determine the syntax and semantics of operations.
6. For debugging and comparison, `node` is available as a reference engine. Authority order: (1) ECMAScript spec, (2) test262, (3) node.
## Source Layout
- `src/main.rs` — CLI entry point (`jsse <file>` or `jsse -e "code"`)
- `src/lexer.rs` — Tokenizer
- `src/ast.rs` — AST node types
- `src/types.rs` — JS value types (`JsValue`, `JsString`, `JsSymbol`, `JsBigInt`, etc.)
- `src/parser/` — Recursive descent parser
- `mod.rs` — Parser struct, parse_program(), utility helpers
- `expressions.rs` — Expression parsing
- `statements.rs` — Statement parsing
- `declarations.rs` — Function, class, variable, destructuring parsing
- `modules.rs` — Module-specific parsing
- `src/interpreter/` — Tree-walking interpreter
- `mod.rs` — Interpreter struct, new(), run(), object arena, realm management
- `types.rs` — Completion, Environment, JsFunction, PropertyDescriptor, JsObjectData, etc.
- `helpers.rs` — Type conversion, equality, JSON, date math helpers
- `gc.rs` — Mark-and-sweep GC with ephemeron support
- `exec.rs` — Statement execution (exec_statements, loops, try/switch)
- `eval.rs` — Expression evaluation (eval_expr, eval_call, eval_new, etc.)
- `property.rs` — Property-access MOP operations ([[Get]], [[Set]], [[DefineOwnProperty]], [[Delete]], [[HasProperty]], proxy traps, array/typed-array exotic, chain walkers)
- `scheduler.rs` — Job scheduler: microtask queue, async-generator/async-function state, and the `setTimeout`/`setInterval` timer queue serviced by the event loop
- `builtins/` — Built-in object setup
- `mod.rs` — module declarations and `setup_globals`, which calls each `setup_*` in dependency order
- `object.rs` — setup_object_constructor, setup_object_statics (Object constructor statics and Object.prototype methods)
- `reflect.rs` — setup_reflect (the `Reflect` namespace object)
- `function.rs` — setup_function_prototype (call, apply, bind, toString)
- `function_constructors.rs` — setup_function_constructors (eval, Function, AsyncFunction, GeneratorFunction, AsyncGeneratorFunction and their prototype wiring)
- `host.rs` — setup_host_globals (console, print, timers)
- `errors.rs` — setup_error_builtins (Error, native errors, AggregateError, SuppressedError)
- `math.rs` — setup_math (the `Math` namespace object and its f16/precise-sum helpers)
- `json.rs` — setup_json (the `JSON` namespace object)
- `shadow_realm.rs` — setup_shadow_realm
- `primitive_constructors.rs` — Symbol, String, Number and Boolean constructors, Number statics, String.raw, String.fromCharCode
- `global_functions.rs` — setup_global_functions (parseInt, parseFloat, isNaN, isFinite, URI functions, escape/unescape)
- `global_object.rs` — setup_global_object (globalThis, global-object wiring, $262)
- `array.rs` — setup_array_prototype
- `string.rs` — setup_string_prototype
- `number.rs` — setup_number_prototype, setup_boolean_prototype, setup_symbol_prototype
- `iterators.rs` — setup_iterator_prototypes, setup_generator_prototype
- `collections.rs` — setup_map/set/weakmap/weakset_prototype
- `date.rs` — setup_date_builtin
- `atom.rs` — `Atom`: randomised-interner identifier atoms (pointer equality, precomputed hash)
- `binding_map.rs` — `BindingMap`: atom-keyed `Environment.bindings` storage and the `BindingKey`/`BindingName` lookup traits
- `scripts/` — Test runners and utilities
- `test262-pass.txt` — Regression baseline of currently passing test262 tests. **Not rewritten by default.** The runner reads the baseline from `origin/main:test262-pass.txt` (override with `--baseline-ref`) so feature branches don't conflict on it. Pass `--update-baseline` to rewrite the working-tree file — typically only done on `main` (or a branch targeting it) to roll the baseline forward.
- `test262-extra/` — Custom spec-compliance tests not covered by test262
## Building
- `cargo build --release` — always build in release mode for test262 runs (debug is too slow)
- The project uses Rust nightly features (`let_chains`, etc.)
## Conventions
- PR titles: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
PRs are squash-merged by default - clean-history branches.
## Testing
- Primary validation: test262 suite
- Custom tests: `tests/` directory
- After any implementation work, run the full test262 suite.
- Run test262: `uv run python scripts/run-test262.py`
- Run linter: `./scripts/lint.sh`
- Python scripts are run via `uv run python` (no virtualenv setup needed).
- Ensure forward progress.
- We should implement new features to ensure new tests pass without regressing on previously passing tests.
- Each test runs under a time limit (default 120s) and a memory limit (512 MB) to prevent runaway tests from crashing the system. These limits are enforced in `scripts/run-test262.py`.
- We implement all optional test262 features including intl402 (Intl API) and Temporal. The default test runner covers `language/`, `built-ins/`, `annexB/`, and `intl402/`. Staging tests are tracked separately — run them explicitly with `uv run python scripts/run-test262.py test262/test/staging/`.
- Any validation that's spec-correct but not in test262 should have its own tests in test262-extra/. This includes regressions for engine-internal heuristics when the failure changes an observable ECMAScript value or throw; exact host-compatibility diagnostics and engine resource-limit or stress checks remain in `tests/`.
- it should include spec part that is tested and follow the exact same patterns of test262 tests.
- Run test262 on a specific directory: `uv run python scripts/run-test262.py test262/test/built-ins/Symbol/`
- Run custom tests: `uv run python scripts/run-custom-tests.py`
## GC Stress Mode
- `JSSE_GC_STRESS=N` (any release or debug binary; read once per interpreter) forces a collection at every Nth safepoint that would not otherwise collect, alternating major (finds missing roots) and minor (finds missing write barriers). Unset, `0` or unparseable leaves it off; the off path costs one predictable branch per safepoint.
- - Safepoints are statement boundaries and loop back-edges. The bytecode VM reaches both: `Op::Safepoint` fires once before every statement-list entry that compiles to any bytecode (function/script top level and `Statement::Block` bodies; a statement compiling to nothing, e.g. `;` or `var a;`, drops its safepoint too — there's nothing new to collect), matching the tree-walker's per-statement safepoint, in addition to the existing back-edge safepoint on negative-offset `Op::Jump` — so bytecode (the default) stress coverage is close to tree-walker parity for compiled chunks, modulo `CompileGoal`/`CompileError::Unsupported` bail coverage (constructs the compiler doesn't accept still run on the tree-walker). Collection never fires at allocation time.
+ - Safepoints are statement boundaries and loop back-edges. The bytecode VM reaches both: `Op::Safepoint` fires once before every statement-list entry that compiles to any bytecode (function/script top level and `Statement::Block` bodies; a statement compiling to nothing, e.g. `;` or `var a;`, drops its safepoint too — there's nothing new to collect), matching the tree-walker's per-statement safepoint, in addition to the existing back-edge safepoint on negative-offset `Op::Jump` — so bytecode (the default) stress coverage is close to tree-walker parity for compiled chunks, modulo `CompileGoal`/`CompileError::Ineligible` bail coverage and the AST Fallback nodes (constructs the compiler doesn't accept still run on the tree-walker; a hand-off node's own statements hit the tree-walker's safepoints). Collection never fires at allocation time.
- The test262 runner and integration tests that spawn the binary inherit the variable (in-crate unit tests ignore it and opt in with `GcPacer::set_stress_period`): `JSSE_GC_STRESS=1 uv run python scripts/run-test262.py test262-extra/ --timeout 300`. `N=1` is only practical on small directories; use `N=16..1000` with `--sample`/`--seed` for broad runs.
- Triage: a stress-only failure means a live value was unreachable from `collect_gc_roots` at a safepoint (a freed object whose arena id was recycled usually shows up as a wrong-typed value or `TypeError`). Reproduce with a minimal script plus `$262.gc()`, then diff against the same run without the variable. Timeouts under stress are cost, not GC bugs. Never `--update-baseline` under stress.
- `ci.yml` runs a blocking `JSSE_GC_STRESS=7` pass over `test262-extra/` (normal and `--no-bytecode`) on every PR/push, reusing the plain release binary (#809). The unflagged pass exercises the now-default bytecode VM; the `--no-bytecode` pass keeps the tree-walker covered.
- `nightly-gc-stress.yml` runs a non-blocking, higher-intensity (`N=2`) `--sample 0.003` stress pass over the full `test262/` suite daily, seeded from the run id so a red run stays reproducible; the tiny sample is what keeps `N=2` affordable despite the `N=16..1000` broad-run guidance above (#809).
## GC Root-Stack Discipline
- `gc_temp_roots` is a strictly LIFO `RootStack`; roots are released in reverse order of push (`gc_unroot_id` debug-asserts the id is on top, frames truncate). Persistent captures belong in a Pinned Native Root or `RootedSlots`, never on the stack. Balance is also asserted after every native call, microtask/timer job and program run (`gc_assert_root_depth`).
- `gc_bytecode_roots` (the bytecode VM's operand-stack roots, pushed/popped from `bytecode/vm.rs`) shares the same `RootStack` type, so it gets the same LIFO-release assert for free. It additionally gets its own chunk-exit balance check in `run_chunk_with_var_prologue`: `debug_assert_eq!` against the entry depth, skipped for `Throw`/`Exit` completions because a mid-chunk abrupt exit can legitimately leave operands from an outer in-progress expression still rooted (issue #331) — the unconditional `truncate` right after handles releasing those. It does not yet get `gc_assert_root_depth`-style equality checks at the native-call/microtask-job/timer-callback/program-run boundaries that `gc_temp_roots` has (#810) — every nested `run_chunk` call already truncates unconditionally before returning past those boundaries, so adding the checks is sound, just not done yet (issue #807 follow-up).
- These are `debug_assert!`s. Run them at release speed with `cargo build --profile release-checked` and `uv run python scripts/run-test262.py --binary target/release-checked/jsse ...`; CI does this for test262-extra (normal and `--no-bytecode`) and the 10% sample. The checked binary has debug-sized recursion/parse-depth limits (`cfg!(debug_assertions)`), so keep `tests/recursion-limit-*.js` on the plain release build. `cargo test` (debug) executes them too.
## Long-Running Builds & Tests
- Never rebuild the binary while a test262 / full-suite run is in flight; snapshot the binary to a temp path first or wait for completion.
- Never use `pkill -9 <pattern>` — patterns have matched Claude's own shell. Kill by recorded PID only, and clean up temp files explicitly.
- Clear stale compile caches before declaring a repro 'still failing'.
- Long compiles exceed the default 2-minute Bash timeout: pass an explicit longer timeout up front.
## Execution Counters (performance investigations)
- Off by default and absent from the shipped binary. Build with
`cargo build --release --features perf-counters`; every run **that executes
JavaScript** then writes tab-separated counters to **stderr** at exit — file,
`-e`, `--prelude`, and REPL alike, including a prelude that throws (covered by
`tests/perf_counters_report_paths.rs`). A run that dies before any interpreter
exists — an unreadable main file on the no-prelude path — reports nothing,
deliberately: there are no counters to report, and an all-zero block would
read as a completed measurement of nothing.
- Reports the compiled/tree-walker split by **work** (VM opcodes vs
`exec_statement`/`eval_expr` entries), not just by invocation — the two
disagree by an order of magnitude on real code, see
`docs/perf/2026-08-26/mandreel-bytecode-work-share.md`.
- `local_slot_hits` / `local_slot_misses` count `LoadLocal`/`StoreLocal`/
`UpdateLocal` executions whose slot hint matched or fell back to by-name
resolution; `atom_intern_calls` / `atom_lookup_calls` count entries into the
hashing identifier interner. A call path that is free of identifier hashing
keeps the two atom counters flat as the call count grows
(`tests/perf_counters_env_hashing.rs`).
- `PERF` totals, `OP` per-opcode histogram, `BAIL` compile-bail reasons named by
- AST variant (`statement:Labeled`), `BODY` per-function tree-walker work units
- spent *exclusive* of nested bodies, plus GC collection counts and time.
+ AST variant (`statement:Labeled`) for bodies that stay wholly on the
+ tree-walker, `FALLBACK` static hand-off nodes per reason inside *compiled*
+ bodies (one row count per literal), `PERF ast_fallback_execs` /
+ `ast_units_in_fallbacks` (dynamic hand-offs and the tree-walker work they
+ spend, attributed to the enclosing function's `BODY` row but not counted as
+ an AST-body invocation), `BODY` per-function tree-walker work units spent
+ *exclusive* of nested bodies, plus GC collection counts and time. A function
+ with no `BAIL` can still run hot constructs on the tree-walker: read
+ `FALLBACK`/`ast_units_in_fallbacks` before calling a body "compiled".
- Generator/async state-machine bodies do not pass through `dispatch_body`, but
retain their originating function identity for attribution, so their work is
reported under individual function names and kept off the calling function's
exclusive total. Top-level script, module, and `eval` bodies have no function
identity and remain in the synthetic `BODY` rows `<script body>`,
`<module body>`, and `<eval>`.
- `BODY` rows are keyed by function *identity*, not name. Two functions sharing
a name render as `name#<object id>`; a unique name stays bare, so only genuine
ambiguity costs readability.
- `compile_ok` and the `BAIL` table cover **script** compile attempts as well as
function bodies, so a script-only workload no longer reports zero for both.
A compiled script counts as a `body_non_function_execs` execution, never as a
`body_dispatch_compiled` invocation. A function body's verdict is shared by
every closure of its literal (#900), so `compile_ok` and `BAIL` count each
literal once (again only after a memo eviction), while every closure's `BODY`
row still names the bail reason.
- `body_dispatch_compiled` / `body_dispatch_ast` count **function invocations
only** — that is the split #524 published, so keep it comparable. Executions
of the non-`dispatch_body` paths are reported separately as
`body_non_function_execs`, which counts **executions, not invocations**:
generators replay, so one generator call with N yields registers roughly 4N
state-machine steps there. The execution count on each resolved
generator/async `BODY` row has the same state-machine-step semantics, not a
function-call count; replayed work is real work and is counted each time.
- `ast_units_per_ast_body` divides **function** work by function invocations —
its numerator is `ast_units_in_functions`, not the run-wide `ast_work_units`,
which also covers script/generator/module/eval work.
- Every count is deterministic, so a shared/loaded host does not compromise it.
**Never time an instrumented build** — take counts from the feature build and
wall times from the default build.
- JetStream per-phase driver: `scripts/gen-mandreel-phases.py <mandreel.js> -o
driver.js [--subphases]`. Op-mix microbenchmark:
`benchmarks/scripts/bench_opmix.js`.
## Mutation Testing
- Local-only (not in CI). Driver: `./scripts/run-mutants.sh` (forwards args to `cargo mutants`).
- Requires `cargo install cargo-mutants --locked` and `uv` on PATH (or at `~/.local/bin/uv`).
- Examples:
- `./scripts/run-mutants.sh --list` — enumerate mutants without running.
- `./scripts/run-mutants.sh --shard 0/8` — single shard of the corpus.
- `./scripts/run-mutants.sh --file src/lexer.rs` — restrict to one file.
- Oracle is `cargo test --release` plus `tests/test262_smoke_oracle.rs`, which runs a 0.5% random test262 sample (~3,500 scenarios, ~20 s on a fast machine). The sample is unseeded, so kill verdicts are non-deterministic across mutants — the trade-off is broader cross-section coverage over many runs.
- Configuration in `.cargo/mutants.toml`. Generated tables (`unicode_tables.rs`, `emoji_strings.rs`) and two combinatorially-explosive Temporal helpers (`duration.rs`, `plain_date_time.rs`) are excluded.
- Output in `mutants.out/` (gitignored): `caught.txt`, `missed.txt`, `unviable.txt`, `outcomes.json`, plus per-mutant diffs/logs.
## Library Tests (Node-compat harness)
Real-world npm libraries are run as engine stress tests via a generalized
harness. See `scripts/README.md` for the full recipe and how to add a library.
- Run one library: `./scripts/run-library-tests.sh <lib>` (e.g. `decimal.js`, `big.js`, `acorn`).
- Run on Node only (reference oracle): `./scripts/run-library-tests.sh <lib> --node`.
- Force a fresh clone/rebuild: `./scripts/run-library-tests.sh <lib> --clean`.
- Each library has a config at `scripts/libs/<lib>.sh` (pinned repo/ref, bundle entry, prepare hook, verdict). The runner clones the pinned ref, bundles with a pinned esbuild, prepends the shared JS-only Node-globals shims (`scripts/node-shim.js` for `process`/`console`, `scripts/node-buffer-shim.js` for `Buffer`/`TextEncoder`/`TextDecoder`) — never baked into jsse's globals, so test262 is unaffected — runs on jsse, and cross-checks the test count against Node. Caches live under `/tmp/jsse-libtests/<lib>/`.
- The shim stubs are guarded so they are inert on Node, letting `--node` run the identical bundle as the reference.
- **`Buffer`** is a pure-JS subclass of `Uint8Array` (`node-buffer-shim.js`), needing zero new engine object kinds; `instanceof Uint8Array` holds. Self-verifying fixtures live in `scripts/shim-fixtures/`; run them (jsse + Node cross-checked) with `./scripts/run-shim-fixtures.sh`.
- **Currently green (cross-checked vs Node):** `decimal.js` (22,624), `big.js` (47,456; ~7 min — heavy arbitrary-precision arithmetic on the tree-walker), `acorn` (13,507), `prismjs` (2,563), `uglify-js` (4,233; ~15 min — full compress DSL transform/mangle/codegen corpus), `highlight.js` (731; ~30 min for the full auto-detection competition set), `uuid` (75; pinned to the browser build so v3/v5 exercise its own pure-JS MD5/SHA-1 and v1/v4/v6/v7 draw randomness from a `crypto.getRandomValues` shim backed by `__host_random_bytes` — see `node-crypto-shim.js`).
- **`bignumber.js`** is wired but blocked on a jsse bug it surfaced (strict-mode `return <call-returning-non-object>` in a constructor returns that value instead of `this`, jsse#238); it goes green once that engine bug is fixed.
- **`luxon`** is wired at 3.7.2 with an exact 1,152-test Node cross-check. Node itself fails 13/1,152 on this host (ICU/Node version drift, not a jsse bug); jsse currently passes 1,002, with the remaining Intl/system-zone gaps tracked in jsse#262–#265.
- **`zod`** is wired at v4.4.3 in separate normal and jitless processes with an exact 2,184-case Node cross-check. Node is green; jsse runs every case with no skips and reports 2,178 passing, with the 6 residual failures tracked in jsse#313 (1 per mode) and jsse#314 (2 per mode). jsse#310's artificial `500 → 0` refinement-delay patch in `gen-zod-entry.js` is still in place.
- **`moment`** is wired at 2.30.1 with an exact 3,871-test Node cross-check. Node is green with 162,868 assertions; jsse executes every test and records 198 visible failing assertions, tracked in jsse#311.
### Acorn (`./scripts/run-acorn-tests.sh` — thin wrapper over the harness)
- Pinned to **acorn 8.16.0** (13,507 tests, green on jsse and Node). 8.17.0+ added a parser stack-guard test (`"[".repeat(2000)`) that expects the engine to *throw* a "stack space" error; jsse's tree-walker aborts (SIGABRT) before acorn's guard fires — a jsse deep-recursion robustness gap tracked separately. Bump the pin once that lands.
- **esbuild comment stripping**: esbuild removes comments from function bodies. The `TestComments` test relies on `Function.prototype.toString()` preserving comments, so a pre-bundle patch (`scripts/patch-acorn-comments.js`) replaces the `.toString()` call with a string literal.
## Architecture Notes
- - The interpreter is a single-pass tree-walker over the AST. A bytecode compiler/VM in `bytecode/` is the default execution mode (`bytecode_enabled` defaults on; `--no-bytecode` opts out); unsupported constructs fall back to the tree-walker per body. Property-access MOP operations ([[Get]], [[Set]], [[DefineOwnProperty]], [[Delete]], [[HasProperty]], proxy traps, array/typed-array exotic) are consolidated in `property.rs`.
+ - The interpreter is a single-pass tree-walker over the AST. A bytecode compiler/VM in `bytecode/` is the default execution mode (`bytecode_enabled` defaults on; `--no-bytecode` opts out); in Function goal a construct the compiler cannot lower falls back to the tree-walker per *node* (`Op::EvalAst`/`Op::ExecAst`, ADR-2026-10-12-2214), and only `CompileError::Ineligible` constructs (direct `eval`, `with`, `super`, `new.target`, `yield`/`await`, private `in`, compiler resource limits, a body that is nothing but hand-offs) keep a whole body on the tree-walker; the Script goal still bails whole-body. Native lowerings include `switch`, `throw`, `typeof`, member `++`/`--`, `instanceof`, `in`, BigInt literals and function-top-level `let`/`const`; array/object literals, closures, `try`, `for-in`/`for-of` and block-scoped lexicals run as tree-walker nodes (see the `FALLBACK` counters). Property-access MOP operations ([[Get]], [[Set]], [[DefineOwnProperty]], [[Delete]], [[HasProperty]], proxy traps, array/typed-array exotic) are consolidated in `property.rs`.
- Built-in prototypes (e.g. `string_prototype`, `symbol_prototype`) are stored as fields on `Interpreter` and wired up in `setup_builtins()` / `setup_*_prototype()` methods.
- GC is mark-and-sweep with ephemeron support for WeakMap/WeakSet. Realm prototype fields are still rooted in `gc_safepoint()`. Kind-specific roots are derived from `ObjectKind` via an exhaustive `match` in `gc::trace_object_fields` — adding a new variant fails to compile until the GC walker handles it.
- `ObjectKind` (in `interpreter/types.rs`) is the canonical "what shape is this object?" discriminator. Each variant owns the kind-specific slot data (`ProxyData`, `ArrayBufferData`, `TypedArrayInfo`, `IteratorState`, `PromiseData`, `BoundFunctionData`, `IterHelperData`, `FinalizationRegistry`, `Map`, `Set`, `Arguments`, `Array`, `ModuleNamespace`, `DisposableStack`, `TemporalData`, `IntlData`, `WrappedFunctionData`, `ShadowRealm`, `RegExpData`, `DataView`, `Ordinary`, `PrimitiveWrapper`). The disjunction is type-enforced — an object cannot be e.g. both a Proxy and a TypedArray simultaneously. Cross-cutting aspects (`callable`, `constructor_kind`, `prototype_id`, `properties`) remain orthogonal fields on `JsObjectData`. Predicate methods (`is_proxy`, `is_class_constructor`, `arraybuffer_is_shared`, etc.) and accessor methods (`obj.bound()`, `obj.proxy()`, `obj.typed_array_info()`, etc.) pull from `kind` and are the supported read API; direct `obj.kind` matching is fine when destructuring is wanted.
- `Environment.bindings` is a `BindingMap` (`binding_map.rs`) of interned `Atom`s (`atom.rs`): linear scan up to 16 entries, identity-hashed atom index above that, `this` held in `Environment.this_binding`, and the `arguments` binding declared only when the body can observe it. The interner is the only place identifier text is hashed (randomised, see `docs/specs/2026-07-25-environment-binding-hashing-design.md`). Compiled function bodies use `LoadLocal`/`StoreLocal`/`UpdateLocal` with slot hints verified by atom identity (ADR-2026-10-12-0900).
- Generators are compiled by `generator_transform.rs` into a `GeneratorStateMachine` (`generator_runtime.rs`): each resume jumps straight to the stored state id and suspends via `StateTerminator::Yield`/`Await`, with no replay. A `self.generator_context`/`SentValueBindingKind::InlineYield` fallback still exists in `generator_runtime.rs` for any construct the transform's analysis doesn't (yet) decompose — it re-executes the current state's statement list and fast-forwards past already-fired yields — but it is a degraded-behavior backstop for undiscovered gaps, not the primary mechanism (see issue #625). In async generators an inline `yield`/`yield*` suspends through the same terminator machinery as a lowered one (no blocking `await_value`, see ADR-2026-09-21-2157); only the replay of the state's preceding statements remains.
- Closures of one function literal share an `Rc<FunctionLiteralInfo>` (parameter patterns, `uses_arguments`, `has_simple_params`) held in the AST node's `LiteralInfoCell`, alongside the per-`Body` `BodyMemo`s (#900). The cell is filled on first closure creation and `Clone` yields an empty cell, so a cloned-then-mutated node never carries a stale value; a path that clones the node per evaluation (e.g. the per-step `StateTerminator` clone in generator/async state machines) only loses sharing, never correctness.
- Function objects are built by `create_function_with` from pre-interned `FunctionKeys` via `JsObjectData::push_fresh_property` (no per-closure key hashing; `tests/perf_counters_function_creation.rs` gates `key_intern_calls`). `PropertyMap` stays a linear scan up to `LINEAR_MAX = 8` entries before hashing. `JsObjectData.function_realm` replaces the old function-to-realm map; `None` keeps the caller's realm. `class_name` is `Cow<'static, str>`: assign `"X".into()` for static tags.
- The `Object()` constructor calls `to_object()` to wrap primitives (String, Number, Boolean, Symbol, BigInt).
## Agent skills
### Issue tracker
Issues live on GitHub at `pmatos/jsse` and are managed via the `gh` CLI; external PRs are not a triage surface (issues only). See `docs/agents/issue-tracker.md`.
### Triage labels
Canonical triage roles map to the tracker labels `needs-triage`, `needs-info`, `agent-ready`, `human-ready`, and `wontfix`. See `docs/agents/triage-labels.md`.
### Domain docs
Single-context layout — one `CONTEXT.md` + `docs/adr/` at the repo root (created lazily by `/domain-modeling`). See `docs/agents/domain.md`.