diff --git a/CLAUDE.md b/CLAUDE.md index 3c8c1b5eac..12bda97403 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1356 +**Current Version:** 0.5.1357 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 7be84629bc..708322f06b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1356" +version = "0.5.1357" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1356" +version = "0.5.1357" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1356" +version = "0.5.1357" [[package]] name = "perry-ui-tvos" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1356" +version = "0.5.1357" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 2ec8239abe..bb8a10c0c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1356" +version = "0.5.1357" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7627-layer1-slice2-instance-collections-rooting.md b/changelog.d/7627-layer1-slice2-instance-collections-rooting.md new file mode 100644 index 0000000000..cec98c114d --- /dev/null +++ b/changelog.d/7627-layer1-slice2-instance-collections-rooting.md @@ -0,0 +1,96 @@ +### Layer 1 rooting migration, slice 2 — `expr/instance_misc1.rs` + `expr/logical_collections.rs` + `lower_call/property_get/map_set.rs` (#7615) + +All three modules now root every operand through `crate::rooting` and are listed +in `MIGRATED_MODULES`; none names `expr::temp_root`. Follows the template +(#7617) and slices 1a (#7618) / 1b (#7620). + +**Two combinators, each arriving with its callers.** +`rooting::with_operands_rooted_across_call` roots a group across a window whose +`across` step is an **emitted runtime call** rather than a lowered expression. +`re.test(s)` / `re.exec(s)` unconditionally emit `js_jsvalue_to_string_coerce`, +which allocates and, on an object argument, dispatches a user `toString`; there +is no `Expr` for `any_may_trigger_gc` to read, and deriving the window from the +`string` operand answers *false* for a plain local, dropping the root at exactly +the site #7154 faults at. The window is therefore stated, conservatively, rather +than derived — the precedent being `temp_root::guard_store_operand_across`, which +has taken a `bool` for the same reason since #7201. `operand_protection` still +decides *how* each operand is protected, and all three `with_operands_rooted*` +forms now share one implementation so the family cannot grow three orderings. + +`rooting::with_rooted_accumulator` is the operand group's mirror image: an +operand is lowered once and read once, an accumulator is written, read, rewritten +and read again with arbitrary user code lowered between the writes. It enforces +what a raw handle cannot — the accumulator never exists as a register held across +an emission. Consuming calls re-read it as part of being emitted, a helper +returning a fresh address publishes it straight back (`advance`), and the one +point where a register escapes is `finish`, which runs below the last collection +point and above the release; both closures' `?` paths release. `RootedSlot` also +gained a `Repr`, so a boxed slot can no longer be read as a raw pointer — that +choice used to live at each call site, where a mismatch is a silent miscompile +rather than a type error. + +**Windows closed.** `with (o) { x = f() }` materialised the receiver *and the +interned property key* above the RHS and used them below it; the key is a load +from a `__perry_init_strings_*` handle global that evacuation rewrites, so the +write landed under a garbage key on a stale receiver (#7114 in a second arm). +`arr.filter/some/every(cb)` held the array across the callback's `js_closure_new` +— #7620's `find*` finding in three arms that live in a different file. Two raw +accumulators were unrooted outright: `Math.min`/`Math.max` with three or more +arguments threads an `ArrayHeader*` through `js_array_push_f64` across each +remaining argument's lowering, and the static-headers path of `fetch(u, {headers: +{k: f()}})` does the same with `js_object_alloc` — both #7154's `ObjectSpread` +bug, and both beyond what #7280's `root_reload` can repair because the stale +value is an `i64` derived above the window. `fetch`'s `url`/`method`/`body` sat +in registers across the headers construction and across +`js_fetch_headers_to_json`, which enumerates a program-supplied value's own +properties and so can re-enter user code. `"k" in process.env` and +`JSON.parse(, )` each held a string literal's handle across an +intervening allocating call, repaired by one re-emitted `load` and no runtime +call. Plus the ordinary operand-to-operand windows: `delete o[k]` (both forms), +`x instanceof `, `path.join` / `path.win32.*` / `path.relative` / +`path.basename(p, ext)`, `arr.includes` / `.splice` / `.join` / `.slice`, +`Array.from(it, fn)`, `Object.groupBy` / `Map.groupBy`, `s.match` / `s.matchAll`, +`JSON.parse(t, reviver)`, `parseInt(s, r)`, `new RegExp(p, f)`, +`Array.prototype..call(like, …)`, `map.delete(k)`, `a[i]++` and +`process.nextTick(cb, …args)`. + +**Scoped honestly.** `map_set.rs` was already hand-rooted end to end (#6970), so +its migration is a translation and not a repair — its IR is byte-identical. What +it buys is that the release stops being a statement a later edit can move into a +branch, the shape #7462 shipped in `URLSearchParams.delete`, the direct sibling +of these arms. `ObjectSpread` and `Object.assign` were likewise already rooted and +become the accumulator with no behavioural change. + +**Verified locally** (the CI backlog is deep, so this is the evidence), with the +#7622 double-compile control run *first*: clean on the probe set (172/172) and +reproducing 5 ordering-only permutations on the 149-module corpus, so nothing was +attributed before nondeterminism was excluded. IR over 4 purpose-built probes: +153/172 functions identical, `p1_mapset` byte-identical throughout, net delta +root plumbing only with nothing deleted and `js_write_barrier_root_nanbox` the +sole call-target change. Over the `gc-root-dominance` corpus: 2436/2452 +identical; of the 16 diffs, 11 are root plumbing and 5 ordering-only — 4 in the +control set and the fifth pinned by six repeat compiles with the *baseline* +binary. `gc-root-dominance` green in both gated modes with an empty allowlist +(0 violations, `--seeded-violations 40` at 40/40, `--unrooted-allocas` 0 over +7867 gc-capable allocas), all four checker static audits pass, and root stores +went 9826 → 9846 over the identical corpus, so the gate's subject is +demonstrably live. `cargo test -p perry-codegen --lib` 691 pass and `--doc`'s two +`compile_fail,E0499` arms still reject; `-p perry-runtime --no-fail-fast` 1886 +pass (one #7365 timing flake, green on rerun). 86 gap tests over 18 family +filters with identical verdict sets on both arms. All probes byte-identical in +stdout and exit code on both arms and again under `PERRY_GC_ZEAL=1 +PERRY_GC_PROTECT_FROMSPACE=1`. Ledger sabotage run per module — `map_set.rs`, +`logical_collections.rs` and `instance_misc1.rs` each turn the ledger red naming +both injected lines. Unlike slices 1a and 1b these three named `expr::temp_root` +before the migration, so the ledger line is load-bearing on the committed source +and not only under sabotage. + +**Deliberately not closed here.** `Expr::IndexUpdate` (`a[i]++`) still holds its +re-read receiver and index across `js_dyn_index_get`, `js_to_numeric` and +`js_numeric_step`, three calls that can each run user code, before +`js_dyn_index_set` consumes them. Closing it needs a *per-use* re-read inside the +body rather than one group-wide re-read — a different combinator, which per the +template's rule should arrive with the slice that needs it. The +operand-to-operand half is closed. Filed separately, as is a pre-existing SIGABRT +in `test_gap_fetch_request_from_node_incoming_message` that reproduces on a +pristine `main` build and is absent from `known_failures.json`. diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index f9f5c73110..787b844d18 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -3,11 +3,51 @@ //! Extracted from `expr/mod.rs` to keep that file under the 2000-line cap. //! Pure mechanical move — match arm bodies are verbatim copies, called from //! `lower_expr`'s outer dispatch. +//! +//! # Layer 1 migrated module (#7615, slice 2) +//! +//! Nothing here names `expr::temp_root`. Multi-operand arms go through +//! [`crate::rooting::with_operands_rooted`]; the two `RegExp` arms, whose +//! receiver is live across a collection point this module *emits* rather than +//! lowers, go through [`crate::rooting::with_operands_rooted_across_call`]. +//! `crate::rooting::migration_ledger` fails the build if this module reaches +//! back into the raw API. Single-operand arms keep their plain `lower_expr` — +//! with nothing lowered after the operand there is no window and +//! `operand_protection` would answer `Reuse` (the template's rule, #7617). +//! +//! ## What the migration found +//! +//! **`with (o) { x = f() }` (`Expr::WithSet`).** The receiver AND the interned +//! property key were materialised above the RHS, then used below it. The key is +//! a load from a `__perry_init_strings_*` handle global — a registered root that +//! evacuation **rewrites** — so the register named from-space while the global +//! did not. That is #7114 exactly, in a second arm: the write landed under a +//! garbage key, on a stale receiver. Both now materialise below the group's +//! re-read. +//! +//! **Operand-to-operand windows** in `delete o[k]` (both the string-key and the +//! dynamic form), `x instanceof `, `path.join`, `path.win32.*`, +//! `path.relative`, `path.basename(p, ext)`, `arr.includes(v, from)`, +//! `arr.splice(...)`, `Array.from(it, fn)`, `Object.groupBy` / `Map.groupBy`, +//! `s.match(re)` / `s.matchAll(re)`, `JSON.parse(t, reviver)` and `a[i]++`. +//! +//! ## Deliberately NOT closed here +//! +//! `Expr::IndexUpdate` (`a[i]++`) still holds its re-read receiver and index +//! across `js_dyn_index_get`, `js_to_numeric` and `js_numeric_step` — three +//! calls that can each run user code (a getter, a `valueOf`) — before +//! `js_dyn_index_set` consumes them. Closing that needs a *per-use* re-read +//! inside the body, which is a different combinator from the group-wide one +//! this campaign has (`RootedOperands::reread_one` is the raw-API shape it would +//! wrap). Per the template's rule, that combinator should arrive with the slice +//! that needs it rather than ahead of one. Filed separately; the operand-to- +//! operand half IS closed here. use anyhow::Result; use perry_hir::{BinaryOp, Expr, WithSetFallback}; use crate::nanbox::{double_literal, i64_literal, POINTER_MASK_I64}; +use crate::rooting; use crate::type_analysis::is_string_expr; use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; @@ -191,91 +231,102 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { fallback, strict, } => { - let obj = lower_expr(ctx, object)?; - let (key_box, key_raw) = emit_with_key(ctx, property); - // HasBinding probe AFTER the RHS evaluates — matches V8/node - // (`with (o) { var x = delete o.x; }` writes the hoisted var, - // not o.x — test262 variable/binding-resolution.js judges - // against node's order, not the spec's resolve-reference-first). - let value_reg = lower_expr(ctx, value)?; - let had = ctx.block().call( - I32, - "js_with_has_binding", - &[(DOUBLE, &obj), (I64, &key_raw)], - ); - let had_bool = ctx.block().icmp_ne(I32, &had, "0"); + // #7615 slice 2: `obj` was live across the RHS's lowering, and so + // was the interned key — whose register is a load from a + // `__perry_init_strings_*` handle global that evacuation rewrites + // (#7114). Root the receiver across the RHS and materialise the key + // BELOW the re-read; the HasBinding probe still runs after the RHS. + rooting::with_operands_rooted(ctx, &[object, value], |ctx, vals| { + let (obj, value_reg) = (vals[0].clone(), vals[1].clone()); + let (key_box, key_raw) = emit_with_key(ctx, property); + // HasBinding probe AFTER the RHS evaluates — matches V8/node + // (`with (o) { var x = delete o.x; }` writes the hoisted var, + // not o.x — test262 variable/binding-resolution.js judges + // against node's order, not the spec's resolve-reference-first). + let had = ctx.block().call( + I32, + "js_with_has_binding", + &[(DOUBLE, &obj), (I64, &key_raw)], + ); + let had_bool = ctx.block().icmp_ne(I32, &had, "0"); - let hit_idx = ctx.new_block("with.set.hit"); - let miss_idx = ctx.new_block("with.set.miss"); - let merge_idx = ctx.new_block("with.set.merge"); - let hit_label = ctx.block_label(hit_idx); - let miss_label = ctx.block_label(miss_idx); - let merge_label = ctx.block_label(merge_idx); - ctx.block().cond_br(&had_bool, &hit_label, &miss_label); + let hit_idx = ctx.new_block("with.set.hit"); + let miss_idx = ctx.new_block("with.set.miss"); + let merge_idx = ctx.new_block("with.set.merge"); + let hit_label = ctx.block_label(hit_idx); + let miss_label = ctx.block_label(miss_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&had_bool, &hit_label, &miss_label); - ctx.current_block = hit_idx; - let strict_i32 = if *strict { "1" } else { "0" }; - let hit = ctx.block().call( - DOUBLE, - "js_with_set_binding", - &[ - (DOUBLE, &obj), - (I64, &key_raw), - (DOUBLE, &value_reg), - (I32, strict_i32), - ], - ); - let hit_after = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } + ctx.current_block = hit_idx; + let strict_i32 = if *strict { "1" } else { "0" }; + let hit = ctx.block().call( + DOUBLE, + "js_with_set_binding", + &[ + (DOUBLE, &obj), + (I64, &key_raw), + (DOUBLE, &value_reg), + (I32, strict_i32), + ], + ); + let hit_after = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } - ctx.current_block = miss_idx; - let miss = match fallback { - WithSetFallback::Local(id) | WithSetFallback::SloppyImplicit(id) => { - store_prelowered_local(ctx, *id, &value_reg)? + ctx.current_block = miss_idx; + let miss = match fallback { + WithSetFallback::Local(id) | WithSetFallback::SloppyImplicit(id) => { + store_prelowered_local(ctx, *id, &value_reg)? + } + WithSetFallback::ThrowReferenceError => ctx.block().call( + DOUBLE, + "js_throw_reference_error_unresolvable_assignment", + &[(DOUBLE, &key_box)], + ), + WithSetFallback::ThrowConstAssignment => ctx.block().call( + DOUBLE, + "js_throw_type_error_const_assignment", + &[(DOUBLE, &key_box)], + ), + WithSetFallback::Ignore => value_reg.clone(), + }; + let miss_after = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); } - WithSetFallback::ThrowReferenceError => ctx.block().call( - DOUBLE, - "js_throw_reference_error_unresolvable_assignment", - &[(DOUBLE, &key_box)], - ), - WithSetFallback::ThrowConstAssignment => ctx.block().call( - DOUBLE, - "js_throw_type_error_const_assignment", - &[(DOUBLE, &key_box)], - ), - WithSetFallback::Ignore => value_reg.clone(), - }; - let miss_after = ctx.block().label.clone(); - if !ctx.block().is_terminated() { - ctx.block().br(&merge_label); - } - ctx.current_block = merge_idx; - Ok(ctx - .block() - .phi(DOUBLE, &[(&hit, &hit_after), (&miss, &miss_after)])) + ctx.current_block = merge_idx; + Ok(ctx + .block() + .phi(DOUBLE, &[(&hit, &hit_after), (&miss, &miss_after)])) + }) } Expr::InstanceOf { expr: e, ty, ty_expr, } => { - let v = lower_expr(ctx, e)?; // v0.5.749: dynamic dispatch when the type is a runtime // expression (function arg, local holding a class ref). // The runtime helper `js_instanceof_dynamic` extracts the // class_id from the INT32 NaN-tag and walks the chain. // Refs #420 / #618 followup. + // + // #7615 slice 2: `v` is live across the RHS's lowering, so the pair + // is rooted as a group. The static-RHS path below lowers nothing + // after `v` and keeps its plain `lower_expr`. if let Some(ty_e) = ty_expr { - let ty_v = lower_expr(ctx, ty_e)?; - return Ok(ctx.block().call( - DOUBLE, - "js_instanceof_dynamic", - &[(DOUBLE, &v), (DOUBLE, &ty_v)], - )); + return rooting::with_operands_rooted(ctx, &[e, ty_e], |ctx, vals| { + Ok(ctx.block().call( + DOUBLE, + "js_instanceof_dynamic", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + )) + }); } + let v = lower_expr(ctx, e)?; if let Some((submod_key, exported_name)) = ctx.import_function_node_submodule.get(ty) { if submod_key == "diagnostics_channel" && matches!(exported_name.as_str(), "Channel" | "BoundedChannel") @@ -665,45 +716,56 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(blk.call(DOUBLE, "js_delete_result", &[(I32, &i32_v), (I32, strict)])) } Expr::IndexGet { object, index } if is_string_expr(ctx, index) => { - let obj_box = lower_expr(ctx, object)?; - let key_box = lower_expr(ctx, index)?; - // `delete null[k]` / `delete undefined[k]` → TypeError, after - // the key expression is evaluated (spec - // EvaluatePropertyAccessWithExpressionKey: RequireObjectCoercible - // runs after ToPropertyKey's operand is evaluated). - ctx.block() - .call(DOUBLE, "js_require_object_coercible", &[(DOUBLE, &obj_box)]); - let strict = if ctx.is_strict_fn { "1" } else { "0" }; - let blk = ctx.block(); - // SSO-safe key unbox — `js_object_delete_field` - // dereferences the key as `*StringHeader`. #214 class. - let key_handle = unbox_str_handle(blk, &key_box); - // Raw receiver: primitive `delete prim[k]` no-ops to `true`. - let i32_v = blk.call( - I32, - "js_object_delete_field_value", - &[(DOUBLE, &obj_box), (I64, &key_handle)], - ); - Ok(blk.call(DOUBLE, "js_delete_result", &[(I32, &i32_v), (I32, strict)])) + // #7615 slice 2: the receiver is live across the key's + // lowering, and the key is unboxed to a raw `StringHeader*` + // below it — slice 1b's `BufferSlice` shape. + rooting::with_operands_rooted(ctx, &[object, index], |ctx, vals| { + let (obj_box, key_box) = (vals[0].clone(), vals[1].clone()); + // `delete null[k]` / `delete undefined[k]` → TypeError, after + // the key expression is evaluated (spec + // EvaluatePropertyAccessWithExpressionKey: RequireObjectCoercible + // runs after ToPropertyKey's operand is evaluated). + ctx.block().call( + DOUBLE, + "js_require_object_coercible", + &[(DOUBLE, &obj_box)], + ); + let strict = if ctx.is_strict_fn { "1" } else { "0" }; + let blk = ctx.block(); + // SSO-safe key unbox — `js_object_delete_field` + // dereferences the key as `*StringHeader`. #214 class. + let key_handle = unbox_str_handle(blk, &key_box); + // Raw receiver: primitive `delete prim[k]` no-ops to `true`. + let i32_v = blk.call( + I32, + "js_object_delete_field_value", + &[(DOUBLE, &obj_box), (I64, &key_handle)], + ); + Ok(blk.call(DOUBLE, "js_delete_result", &[(I32, &i32_v), (I32, strict)])) + }) } // delete obj[expr] — route dynamic keys through the runtime so // string-valued locals (for example `delete fn[name]`) still // use the ordinary property-delete path instead of being // misread as numeric array indexes. Expr::IndexGet { object, index } => { - let obj_box = lower_expr(ctx, object)?; - let idx_box = lower_expr(ctx, index)?; - ctx.block() - .call(DOUBLE, "js_require_object_coercible", &[(DOUBLE, &obj_box)]); - let strict = if ctx.is_strict_fn { "1" } else { "0" }; - let blk = ctx.block(); - // Raw receiver: primitive `delete prim[expr]` no-ops to `true`. - let i32_v = blk.call( - I32, - "js_object_delete_dynamic_value", - &[(DOUBLE, &obj_box), (DOUBLE, &idx_box)], - ); - Ok(blk.call(DOUBLE, "js_delete_result", &[(I32, &i32_v), (I32, strict)])) + rooting::with_operands_rooted(ctx, &[object, index], |ctx, vals| { + let (obj_box, idx_box) = (vals[0].clone(), vals[1].clone()); + ctx.block().call( + DOUBLE, + "js_require_object_coercible", + &[(DOUBLE, &obj_box)], + ); + let strict = if ctx.is_strict_fn { "1" } else { "0" }; + let blk = ctx.block(); + // Raw receiver: primitive `delete prim[expr]` no-ops to `true`. + let i32_v = blk.call( + I32, + "js_object_delete_dynamic_value", + &[(DOUBLE, &obj_box), (DOUBLE, &idx_box)], + ); + Ok(blk.call(DOUBLE, "js_delete_result", &[(I32, &i32_v), (I32, strict)])) + }) } _ => { let _ = lower_expr(ctx, operand)?; @@ -821,19 +883,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // #2773: `js_array_from_mapped` throws for nullish sources, validates // mapFn callability, calls mapFn(value, index) and binds the optional // thisArg. All three args are passed raw NaN-boxed (DOUBLE). - let iter_box = lower_expr(ctx, iterable)?; - let cb_box = lower_expr(ctx, map_fn)?; - let this_box = match this_arg { - Some(t) => lower_expr(ctx, t)?, - None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), - }; - let blk = ctx.block(); - let mapped = blk.call( - I64, - "js_array_from_mapped", - &[(DOUBLE, &iter_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], - ); - Ok(nanbox_pointer_inline(blk, &mapped)) + let mut operands: Vec<&Expr> = vec![iterable, map_fn]; + if let Some(t) = this_arg { + operands.push(t); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let this_box = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let blk = ctx.block(); + let mapped = blk.call( + I64, + "js_array_from_mapped", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1]), (DOUBLE, &this_box)], + ); + Ok(nanbox_pointer_inline(blk, &mapped)) + }) } Expr::Uint8ArrayFrom(iter) => { // #2774: materialize the source into a real Uint8Array (kind 1) so @@ -874,33 +939,29 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- path.join(a, b) -> string -------- // The HIR variant is binary; multi-arg path.join lowers to // chained PathJoin in the HIR. - Expr::PathJoin(a, b) => { - let a_box = lower_expr(ctx, a)?; - let b_box = lower_expr(ctx, b)?; + Expr::PathJoin(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { let blk = ctx.block(); - let a_handle = unbox_to_i64(blk, &a_box); - let b_handle = unbox_to_i64(blk, &b_box); + let a_handle = unbox_to_i64(blk, &vals[0]); + let b_handle = unbox_to_i64(blk, &vals[1]); let result = blk.call(I64, "js_path_join", &[(I64, &a_handle), (I64, &b_handle)]); Ok(nanbox_string_inline(blk, &result)) - } + }), // -------- path.win32.join(a, b) -> string (issue #810) -------- // Windows-style join with `\` separator, regardless of host // platform. Multi-arg path.win32.join lowers to chained // PathWin32Join in the HIR. - Expr::PathWin32Join(a, b) => { - let a_box = lower_expr(ctx, a)?; - let b_box = lower_expr(ctx, b)?; + Expr::PathWin32Join(a, b) => rooting::with_operands_rooted(ctx, &[a, b], |ctx, vals| { let blk = ctx.block(); - let a_handle = unbox_to_i64(blk, &a_box); - let b_handle = unbox_to_i64(blk, &b_box); + let a_handle = unbox_to_i64(blk, &vals[0]); + let b_handle = unbox_to_i64(blk, &vals[1]); let result = blk.call( I64, "js_path_win32_join", &[(I64, &a_handle), (I64, &b_handle)], ); Ok(nanbox_string_inline(blk, &result)) - } + }), // -------- path.win32.(...) (issue #1162) -------- // One arm covers every win32 sub-namespace method other than @@ -909,91 +970,92 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // js_path_win32_* runtime function. Expr::PathWin32 { method, args } => { use perry_hir::PathWin32Method; - // Lower all args up front into NaN-boxed JSValue locals. - let lowered: Vec<_> = args - .iter() - .map(|a| lower_expr(ctx, a)) - .collect::, _>>()?; - match method { - PathWin32Method::ToNamespacedPath => { - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_path_win32_to_namespaced_path_value", - &[(DOUBLE, &lowered[0])], - )) - } - PathWin32Method::Dirname - | PathWin32Method::Basename - | PathWin32Method::Extname - | PathWin32Method::Normalize - | PathWin32Method::Resolve => { - let fn_name = match method { - PathWin32Method::Dirname => "js_path_win32_dirname", - PathWin32Method::Basename => "js_path_win32_basename", - PathWin32Method::Extname => "js_path_win32_extname", - PathWin32Method::Normalize => "js_path_win32_normalize", - PathWin32Method::Resolve => "js_path_win32_resolve", - _ => unreachable!(), - }; - let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); - let result = blk.call(I64, fn_name, &[(I64, &h)]); - Ok(nanbox_string_inline(blk, &result)) - } - PathWin32Method::Relative => { - // #2995: validate both operands are strings (throwing - // ERR_INVALID_ARG_TYPE on a non-string) before computing - // the relative path. Pass the NaN-boxed doubles so the - // runtime can inspect their type. - let blk = ctx.block(); - let result = blk.call( - I64, - "js_path_win32_relative_checked", - &[(DOUBLE, &lowered[0]), (DOUBLE, &lowered[1])], - ); - Ok(nanbox_string_inline(blk, &result)) - } - PathWin32Method::BasenameExt | PathWin32Method::ResolveJoin => { - let fn_name = match method { - PathWin32Method::BasenameExt => "js_path_win32_basename_ext", - PathWin32Method::ResolveJoin => "js_path_win32_resolve_join", - _ => unreachable!(), - }; - let blk = ctx.block(); - let a = unbox_to_i64(blk, &lowered[0]); - let b = unbox_to_i64(blk, &lowered[1]); - let result = blk.call(I64, fn_name, &[(I64, &a), (I64, &b)]); - Ok(nanbox_string_inline(blk, &result)) - } - PathWin32Method::IsAbsolute => { - let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); - let i32_v = blk.call(I32, "js_path_win32_is_absolute", &[(I64, &h)]); - Ok(i32_bool_to_nanbox(blk, &i32_v)) - } - PathWin32Method::MatchesGlob => { - let blk = ctx.block(); - let p = unbox_to_i64(blk, &lowered[0]); - let pat = unbox_to_i64(blk, &lowered[1]); - let i32_v = - blk.call(I32, "js_path_win32_matches_glob", &[(I64, &p), (I64, &pat)]); - Ok(i32_bool_to_nanbox(blk, &i32_v)) - } - PathWin32Method::Parse => { - let blk = ctx.block(); - let h = unbox_to_i64(blk, &lowered[0]); - let result = blk.call(I64, "js_path_win32_parse", &[(I64, &h)]); - Ok(nanbox_pointer_inline(blk, &result)) - } - PathWin32Method::Format => { - // js_path_win32_format takes a NaN-boxed double (object handle). - let obj_box = lowered.into_iter().next().unwrap(); - let blk = ctx.block(); - let result = blk.call(I64, "js_path_win32_format", &[(DOUBLE, &obj_box)]); - Ok(nanbox_string_inline(blk, &result)) + // Lower all args up front into NaN-boxed JSValue locals. #7615 + // slice 2: each was live across the ones after it, and the + // two-operand methods unbox both to raw `StringHeader*` below. + let operands: Vec<&Expr> = args.iter().collect(); + rooting::with_operands_rooted(ctx, &operands, |ctx, lowered| { + match method { + PathWin32Method::ToNamespacedPath => { + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_path_win32_to_namespaced_path_value", + &[(DOUBLE, &lowered[0])], + )) + } + PathWin32Method::Dirname + | PathWin32Method::Basename + | PathWin32Method::Extname + | PathWin32Method::Normalize + | PathWin32Method::Resolve => { + let fn_name = match method { + PathWin32Method::Dirname => "js_path_win32_dirname", + PathWin32Method::Basename => "js_path_win32_basename", + PathWin32Method::Extname => "js_path_win32_extname", + PathWin32Method::Normalize => "js_path_win32_normalize", + PathWin32Method::Resolve => "js_path_win32_resolve", + _ => unreachable!(), + }; + let blk = ctx.block(); + let h = unbox_to_i64(blk, &lowered[0]); + let result = blk.call(I64, fn_name, &[(I64, &h)]); + Ok(nanbox_string_inline(blk, &result)) + } + PathWin32Method::Relative => { + // #2995: validate both operands are strings (throwing + // ERR_INVALID_ARG_TYPE on a non-string) before computing + // the relative path. Pass the NaN-boxed doubles so the + // runtime can inspect their type. + let blk = ctx.block(); + let result = blk.call( + I64, + "js_path_win32_relative_checked", + &[(DOUBLE, &lowered[0]), (DOUBLE, &lowered[1])], + ); + Ok(nanbox_string_inline(blk, &result)) + } + PathWin32Method::BasenameExt | PathWin32Method::ResolveJoin => { + let fn_name = match method { + PathWin32Method::BasenameExt => "js_path_win32_basename_ext", + PathWin32Method::ResolveJoin => "js_path_win32_resolve_join", + _ => unreachable!(), + }; + let blk = ctx.block(); + let a = unbox_to_i64(blk, &lowered[0]); + let b = unbox_to_i64(blk, &lowered[1]); + let result = blk.call(I64, fn_name, &[(I64, &a), (I64, &b)]); + Ok(nanbox_string_inline(blk, &result)) + } + PathWin32Method::IsAbsolute => { + let blk = ctx.block(); + let h = unbox_to_i64(blk, &lowered[0]); + let i32_v = blk.call(I32, "js_path_win32_is_absolute", &[(I64, &h)]); + Ok(i32_bool_to_nanbox(blk, &i32_v)) + } + PathWin32Method::MatchesGlob => { + let blk = ctx.block(); + let p = unbox_to_i64(blk, &lowered[0]); + let pat = unbox_to_i64(blk, &lowered[1]); + let i32_v = + blk.call(I32, "js_path_win32_matches_glob", &[(I64, &p), (I64, &pat)]); + Ok(i32_bool_to_nanbox(blk, &i32_v)) + } + PathWin32Method::Parse => { + let blk = ctx.block(); + let h = unbox_to_i64(blk, &lowered[0]); + let result = blk.call(I64, "js_path_win32_parse", &[(I64, &h)]); + Ok(nanbox_pointer_inline(blk, &result)) + } + PathWin32Method::Format => { + // js_path_win32_format takes a NaN-boxed double (object handle). + let obj_box = lowered[0].clone(); + let blk = ctx.block(); + let result = blk.call(I64, "js_path_win32_format", &[(DOUBLE, &obj_box)]); + Ok(nanbox_string_inline(blk, &result)) + } } - } + }) } // -------- queueMicrotask(fn) stub -------- @@ -1041,32 +1103,32 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let mut arg_refs: Vec<&Expr> = Vec::with_capacity(n + 1); arg_refs.push(callback); arg_refs.extend(args.iter()); - let (vals, guard) = super::temp_root::lower_exprs_rooted(ctx, &arg_refs)?; - let cb_box = vals[0].clone(); - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - for (i, v) in vals.iter().skip(1).enumerate() { + rooting::with_operands_rooted(ctx, &arg_refs, |ctx, vals| { + let cb_box = vals[0].clone(); + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, v) in vals.iter().skip(1).enumerate() { + let blk = ctx.block(); + let slot = blk.gep(DOUBLE, &buf, &[(I64, &format!("{}", i))]); + blk.store(DOUBLE, v, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf + )); let blk = ctx.block(); - let slot = blk.gep(DOUBLE, &buf, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, v, &slot); - } - let ptr_reg = ctx.block().next_reg(); - ctx.block().emit_raw(format!( - "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", - ptr_reg, n, buf - )); - let blk = ctx.block(); - // #3046: same callback validation on the trailing-args path. - let cb_handle = blk.call( - I64, - "js_timer_validate_callback", - &[(DOUBLE, &cb_box), (I32, "3")], - ); - blk.call_void( - "js_queue_next_tick_args", - &[(I64, &cb_handle), (PTR, &ptr_reg), (I32, &n.to_string())], - ); - super::temp_root::temp_root_release(ctx, guard); - Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + // #3046: same callback validation on the trailing-args path. + let cb_handle = blk.call( + I64, + "js_timer_validate_callback", + &[(DOUBLE, &cb_box), (I32, "3")], + ); + blk.call_void( + "js_queue_next_tick_args", + &[(I64, &cb_handle), (PTR, &ptr_reg), (I32, &n.to_string())], + ); + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + }) } // -------- RegExpTest -------- @@ -1074,13 +1136,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Receiver is a NaN-tagged i64 RegExpHeader pointer; arg is // a NaN-tagged string. Both must be unboxed before the call. Expr::RegExpTest { regex, string } => { - let regex_box = lower_expr(ctx, regex)?; // #7154: the receiver is live across BOTH the string operand's own // lowering and the `js_jsvalue_to_string_coerce` below it, and the // coerce is unconditional — it allocates, and on an object argument // it runs a user `toString`, which is arbitrary JS with its own - // back-edge polls. So the window always exists and `collects` is - // `true` rather than a `expr_may_trigger_gc(string)` test. + // back-edge polls. So the window always exists, which is what + // `with_operands_rooted_across_call` states: an emitted call is not + // an `Expr`, so `any_may_trigger_gc` has nothing to read and the + // answer cannot be derived from the `string` operand. // // This is the site the registry reproducer faults at // (`defineApiCall + 404`, `obj_type=3 size=80`): `js_regexp_new`'s @@ -1088,31 +1151,37 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // minor that moved it, and `js_regexp_test` dereferenced from-space. // The static checker could not see it because `ALLOC_RE` spelled the // allocator `regexp_alloc\w*` and the call is `js_regexp_new`. - let guard = super::temp_root::guard_store_operand_across(ctx, regex, ®ex_box, true); - let str_box = lower_expr(ctx, string)?; - // Per spec `RegExp.prototype.test` does `ToString(argument)`, so a - // String wrapper (`re.test(new String("x"))`), a number - // (`re.test(123)`), or an object with a custom `toString` must be - // coerced — and a throwing `toString`/`valueOf` must propagate. - // `js_get_string_pointer_unified` only unwraps real strings, so use - // the coercing ToString that dispatches `toString` on objects. - let str_handle = - ctx.block() - .call(I64, "js_jsvalue_to_string_coerce", &[(DOUBLE, &str_box)]); - // Re-read BELOW the coerce, then unbox. Unboxing above it is what - // parked the pre-move address in a register in the first place. - let regex_box = super::temp_root::reread_store_operand(ctx, &guard, regex, ®ex_box)?; - let blk = ctx.block(); - let regex_handle = unbox_to_i64(blk, ®ex_box); - let i32_v = blk.call( - I32, - "js_regexp_test", - &[(I64, ®ex_handle), (I64, &str_handle)], - ); - let out = i32_bool_to_nanbox(ctx.block(), &i32_v); - // After the call: `js_regexp_test` allocates while reading these. - super::temp_root::release_store_operand(ctx, guard); - Ok(out) + rooting::with_operands_rooted_across_call( + ctx, + &[regex], + |ctx| { + let str_box = lower_expr(ctx, string)?; + // Per spec `RegExp.prototype.test` does `ToString(argument)`, + // so a String wrapper (`re.test(new String("x"))`), a number + // (`re.test(123)`), or an object with a custom `toString` + // must be coerced — and a throwing `toString`/`valueOf` must + // propagate. `js_get_string_pointer_unified` only unwraps + // real strings, so use the coercing ToString that dispatches + // `toString` on objects. + Ok(ctx + .block() + .call(I64, "js_jsvalue_to_string_coerce", &[(DOUBLE, &str_box)])) + }, + |ctx, vals, str_handle| { + // Unbox BELOW the re-read. Unboxing above the coerce is what + // parked the pre-move address in a register in the first + // place. The release follows the call, which allocates while + // reading these. + let blk = ctx.block(); + let regex_handle = unbox_to_i64(blk, &vals[0]); + let i32_v = blk.call( + I32, + "js_regexp_test", + &[(I64, ®ex_handle), (I64, &str_handle)], + ); + Ok(i32_bool_to_nanbox(ctx.block(), &i32_v)) + }, + ) } Expr::RegExpExec { regex, string } => { // Returns ArrayHeader* or null. For a null (0) result we must @@ -1120,28 +1189,33 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // correctly — just NaN-boxing 0 with POINTER_TAG produces a // non-null pointer value that compares unequal to null, causing // infinite loops + segfaults when callers IndexGet on the result. - let regex_box = lower_expr(ctx, regex)?; // #7154, identical shape to `RegExpTest` above and found with it: // the receiver is live across the string operand's lowering and // across the unconditional coerce, which allocates and can run a // user `toString`. - let guard = super::temp_root::guard_store_operand_across(ctx, regex, ®ex_box, true); - let str_box = lower_expr(ctx, string)?; - // `RegExp.prototype.exec` does `ToString(argument)` — coerce String - // wrappers / numbers / objects (and propagate a throwing toString) - // rather than only unwrapping real strings (see RegExpTest above). - let str_handle = - ctx.block() - .call(I64, "js_jsvalue_to_string_coerce", &[(DOUBLE, &str_box)]); - let regex_box = super::temp_root::reread_store_operand(ctx, &guard, regex, ®ex_box)?; - let blk = ctx.block(); - let regex_handle = unbox_to_i64(blk, ®ex_box); - let result = blk.call( - I64, - "js_regexp_exec", - &[(I64, ®ex_handle), (I64, &str_handle)], - ); - super::temp_root::release_store_operand(ctx, guard); + let result = rooting::with_operands_rooted_across_call( + ctx, + &[regex], + |ctx| { + let str_box = lower_expr(ctx, string)?; + // `RegExp.prototype.exec` does `ToString(argument)` — coerce + // String wrappers / numbers / objects (and propagate a + // throwing toString) rather than only unwrapping real + // strings (see RegExpTest above). + Ok(ctx + .block() + .call(I64, "js_jsvalue_to_string_coerce", &[(DOUBLE, &str_box)])) + }, + |ctx, vals, str_handle| { + let blk = ctx.block(); + let regex_handle = unbox_to_i64(blk, &vals[0]); + Ok(blk.call( + I64, + "js_regexp_exec", + &[(I64, ®ex_handle), (I64, &str_handle)], + )) + }, + )?; let blk = ctx.block(); // Branch on result == 0 → TAG_NULL; else NaN-box as pointer. let is_null = blk.icmp_eq(I64, &result, "0"); @@ -1171,15 +1245,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // #2995: validate both operands are strings before computing the // relative path. The checked entry point inspects the NaN-boxed // doubles and throws ERR_INVALID_ARG_TYPE for non-strings. - let f_box = lower_expr(ctx, from)?; - let t_box = lower_expr(ctx, to)?; - let blk = ctx.block(); - let result = blk.call( - I64, - "js_path_relative_checked", - &[(DOUBLE, &f_box), (DOUBLE, &t_box)], - ); - Ok(nanbox_string_inline(blk, &result)) + rooting::with_operands_rooted(ctx, &[from, to], |ctx, vals| { + let blk = ctx.block(); + let result = blk.call( + I64, + "js_path_relative_checked", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + ); + Ok(nanbox_string_inline(blk, &result)) + }) } // -------- arr.includes(value) -> boolean -------- @@ -1188,41 +1262,46 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { value, from_index, } => { - let arr_box = lower_expr(ctx, array)?; - let v = lower_expr(ctx, value)?; - // #2804: optional fromIndex. has_from=1 + lowered index when - // present; otherwise has_from=0 with a placeholder DOUBLE (`v`). - let (from_box, has_from) = match from_index { - Some(fi) => (lower_expr(ctx, fi)?, "1"), - None => (v.clone(), "0"), - }; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // Use `js_array_includes_jsvalue` which does deep-value - // equality (string content, not pointer identity). The - // `*_f64` variant compares raw f64 bits which fails for - // strings created at different sites. - let i32_v = blk.call( - I32, - "js_array_includes_jsvalue", - &[ - (I64, &arr_handle), - (DOUBLE, &v), - (DOUBLE, &from_box), - (I32, has_from), - ], - ); - // Convert i32 boolean to NaN-tagged TAG_TRUE/FALSE so - // console.log prints "true"/"false". - let bit = blk.icmp_ne(I32, &i32_v, "0"); - let tagged = blk.select( - crate::types::I1, - &bit, - I64, - crate::nanbox::TAG_TRUE_I64, - crate::nanbox::TAG_FALSE_I64, - ); - Ok(blk.bitcast_i64_to_double(&tagged)) + let mut operands: Vec<&Expr> = vec![array, value]; + if let Some(fi) = from_index { + operands.push(fi); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let v = vals[1].clone(); + // #2804: optional fromIndex. has_from=1 + lowered index when + // present; otherwise has_from=0 with a placeholder DOUBLE (`v`). + let (from_box, has_from) = match vals.get(2) { + Some(fi) => (fi.clone(), "1"), + None => (v.clone(), "0"), + }; + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + // Use `js_array_includes_jsvalue` which does deep-value + // equality (string content, not pointer identity). The + // `*_f64` variant compares raw f64 bits which fails for + // strings created at different sites. + let i32_v = blk.call( + I32, + "js_array_includes_jsvalue", + &[ + (I64, &arr_handle), + (DOUBLE, &v), + (DOUBLE, &from_box), + (I32, has_from), + ], + ); + // Convert i32 boolean to NaN-tagged TAG_TRUE/FALSE so + // console.log prints "true"/"false". + let bit = blk.icmp_ne(I32, &i32_v, "0"); + let tagged = blk.select( + crate::types::I1, + &bit, + I64, + crate::nanbox::TAG_TRUE_I64, + crate::nanbox::TAG_FALSE_I64, + ); + Ok(blk.bitcast_i64_to_double(&tagged)) + }) } // -------- arr.splice(start, deleteCount?, ...items) -------- @@ -1235,76 +1314,86 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { delete_count, items, } => { - let arr_box = lower_expr(ctx, &Expr::LocalGet(*array_id))?; - let start_d = lower_expr(ctx, start)?; - let count_d = if let Some(d) = delete_count { - lower_expr(ctx, d)? - } else { - "2147483647.0".to_string() - }; - - // Evaluate splice-insert items and collect their f64 values. - let mut item_vals: Vec = Vec::new(); - for it in items { - item_vals.push(lower_expr(ctx, it)?); + // #7615 slice 2: the array, the start index, the delete count and + // every inserted item were each live in a register across the + // lowering of the ones after them. + let array_get = Expr::LocalGet(*array_id); + let mut operands: Vec<&Expr> = vec![&array_get, start]; + let has_count = delete_count.is_some(); + if let Some(d) = delete_count { + operands.push(d); } + operands.extend(items.iter()); + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let arr_box = vals[0].clone(); + let start_d = vals[1].clone(); + let items_at = if has_count { 3 } else { 2 }; + let count_d = if has_count { + vals[2].clone() + } else { + "2147483647.0".to_string() + }; + let item_vals: Vec = vals[items_at..].to_vec(); - let blk = ctx.block(); - // Scratch out-parameter slot — used only in this block to - // receive the modified-array handle from js_array_splice. - let out_slot = blk.alloca(I64); - blk.store(I64, "0", &out_slot); - let arr_handle = unbox_to_i64(blk, &arr_box); - // ToIntegerOrInfinity via the clamping helper: `fptosi` on - // ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted - // from index 0 (test262 splice/S15.4.4.12_A2.1_T3). - let start_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]); - let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); + let blk = ctx.block(); + // Scratch out-parameter slot — used only in this block to + // receive the modified-array handle from js_array_splice. + let out_slot = blk.alloca(I64); + blk.store(I64, "0", &out_slot); + let arr_handle = unbox_to_i64(blk, &arr_box); + // ToIntegerOrInfinity via the clamping helper: `fptosi` on + // ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted + // from index 0 (test262 splice/S15.4.4.12_A2.1_T3). + let start_i32 = + blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]); + let count_i32 = + blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); - let (items_ptr, items_count_str) = if item_vals.is_empty() { - ("null".to_string(), "0".to_string()) - } else { - // Allocate a stack buffer of [N x double] for the - // items, store each value, and pass the base pointer. - let n = item_vals.len(); - let items_count_str = format!("{}", n); - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in item_vals.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, items_count_str) - }; + let (items_ptr, items_count_str) = if item_vals.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + // Allocate a stack buffer of [N x double] for the + // items, store each value, and pass the base pointer. + let n = item_vals.len(); + let items_count_str = format!("{}", n); + let buf_reg = blk.next_reg(); + blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); + for (i, val) in item_vals.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + blk.store(DOUBLE, val, &slot); + } + (buf_reg, items_count_str) + }; - // Note: js_array_splice's return value is the DELETED - // array; the modified-in-place arr is written to *out_arr. - let deleted_handle = blk.call( - I64, - "js_array_splice", - &[ - (I64, &arr_handle), - (I32, &start_i32), - (I32, &count_i32), - (PTR, &items_ptr), - (I32, &items_count_str), - (PTR, &out_slot), - ], - ); - // Read the modified array from the out slot and write it - // back to the source local. - let modified_handle = ctx.block().load(I64, &out_slot); - let modified_box = nanbox_pointer_inline(ctx.block(), &modified_handle); - if let Some(slot) = ctx.locals.get(array_id).cloned() { - ctx.block().store(DOUBLE, &modified_box, &slot); - } else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() { - let g_ref = format!("@{}", global_name); - // GC_STORE_AUDIT(ROOT): module global array slot is a registered mutable GC root. - emit_root_nanbox_store_on_block(ctx.block(), &modified_box, &g_ref); - } - // Return the deleted array (NaN-boxed) as the splice - // expression's value. - Ok(nanbox_pointer_inline(ctx.block(), &deleted_handle)) + // Note: js_array_splice's return value is the DELETED + // array; the modified-in-place arr is written to *out_arr. + let deleted_handle = blk.call( + I64, + "js_array_splice", + &[ + (I64, &arr_handle), + (I32, &start_i32), + (I32, &count_i32), + (PTR, &items_ptr), + (I32, &items_count_str), + (PTR, &out_slot), + ], + ); + // Read the modified array from the out slot and write it + // back to the source local. + let modified_handle = ctx.block().load(I64, &out_slot); + let modified_box = nanbox_pointer_inline(ctx.block(), &modified_handle); + if let Some(slot) = ctx.locals.get(array_id).cloned() { + ctx.block().store(DOUBLE, &modified_box, &slot); + } else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() { + let g_ref = format!("@{}", global_name); + // GC_STORE_AUDIT(ROOT): module global array slot is a registered mutable GC root. + emit_root_nanbox_store_on_block(ctx.block(), &modified_box, &g_ref); + } + // Return the deleted array (NaN-boxed) as the splice + // expression's value. + Ok(nanbox_pointer_inline(ctx.block(), &deleted_handle)) + }) } // -------- ObjectFromEntries (passes through to runtime) -------- @@ -1320,51 +1409,53 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Both args are NaN-boxed f64; the runtime validates iterability and // callback callability (TypeError on failure) per Node semantics. Expr::ObjectGroupBy { items, key_fn } => { - let items_v = lower_expr(ctx, items)?; - let cb_v = lower_expr(ctx, key_fn)?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_object_group_by", - &[(DOUBLE, &items_v), (DOUBLE, &cb_v)], - )) + rooting::with_operands_rooted(ctx, &[items, key_fn], |ctx, vals| { + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_object_group_by", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + )) + }) } // -------- Map.groupBy(items, keyFn) -------- // Routes through `js_map_group_by(items_value, callback)` — returns a // Map keyed by callback results without string coercion. Expr::MapGroupBy { items, key_fn } => { - let items_v = lower_expr(ctx, items)?; - let cb_v = lower_expr(ctx, key_fn)?; - let blk = ctx.block(); - Ok(blk.call( - DOUBLE, - "js_map_group_by", - &[(DOUBLE, &items_v), (DOUBLE, &cb_v)], - )) + rooting::with_operands_rooted(ctx, &[items, key_fn], |ctx, vals| { + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_map_group_by", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + )) + }) } // -------- string.match(regex) -------- Expr::StringMatch { string, regex } => { - let s_box = lower_expr(ctx, string)?; - let r_box = lower_expr(ctx, regex)?; - let blk = ctx.block(); - // SSO-safe string-receiver unbox: `js_string_match` reads - // `byte_len` and the UTF-8 bytes from the StringHeader, which - // segfaults on SSO inline bits. SIGSEGV repro: - // `JSON.parse('"abc"').match(/b/)`. #214 SSO bug class. - let s_handle = unbox_str_handle(blk, &s_box); - let r_handle = unbox_to_i64(blk, &r_box); - let result = blk.call( - I64, - "js_string_match", - &[(I64, &s_handle), (I64, &r_handle)], - ); + let result = rooting::with_operands_rooted(ctx, &[string, regex], |ctx, vals| { + let (s_box, r_box) = (vals[0].clone(), vals[1].clone()); + let blk = ctx.block(); + // SSO-safe string-receiver unbox: `js_string_match` reads + // `byte_len` and the UTF-8 bytes from the StringHeader, which + // segfaults on SSO inline bits. SIGSEGV repro: + // `JSON.parse('"abc"').match(/b/)`. #214 SSO bug class. + let s_handle = unbox_str_handle(blk, &s_box); + let r_handle = unbox_to_i64(blk, &r_box); + Ok(blk.call( + I64, + "js_string_match", + &[(I64, &s_handle), (I64, &r_handle)], + )) + })?; // #4858: js_string_match returns null (0) on no-match. NaN-boxing // 0 with POINTER_TAG yields a value that is neither `null` nor a // valid heap pointer — `s.match(/x/g) === null` was false and // consumers that deref the result (JSON.stringify, .map) crashed. // Branchless null → TAG_NULL select, same as RegExpExec above. + let blk = ctx.block(); let is_null = blk.icmp_eq(I64, &result, "0"); let ptr_boxed = nanbox_pointer_inline(ctx.block(), &result); let ptr_bits = ctx.block().bitcast_double_to_i64(&ptr_boxed); @@ -1380,16 +1471,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // pattern value so runtime can validate RegExp globals or create a // global RegExp for string/non-RegExp patterns. Expr::StringMatchAll { string, regex } => { - let s_box = lower_expr(ctx, string)?; - let r_box = lower_expr(ctx, regex)?; - let blk = ctx.block(); - let s_handle = unbox_str_handle(blk, &s_box); - let result = blk.call( - I64, - "js_string_match_all_value", - &[(I64, &s_handle), (DOUBLE, &r_box)], - ); - Ok(nanbox_pointer_inline(blk, &result)) + rooting::with_operands_rooted(ctx, &[string, regex], |ctx, vals| { + let blk = ctx.block(); + let s_handle = unbox_str_handle(blk, &vals[0]); + let result = blk.call( + I64, + "js_string_match_all_value", + &[(I64, &s_handle), (DOUBLE, &vals[1])], + ); + Ok(nanbox_pointer_inline(blk, &result)) + }) } // -------- obj.field++ / obj.field-- (PropertyUpdate) -------- @@ -1581,33 +1672,42 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { op, prefix, } => { - let obj_box = lower_expr(ctx, object)?; - let idx_box = lower_expr(ctx, index)?; - let blk = ctx.block(); - let old = blk.call( - DOUBLE, - "js_dyn_index_get", - &[(DOUBLE, &obj_box), (DOUBLE, &idx_box)], - ); - // ToNumeric + numeric step so a BigInt element stays BigInt - // (`var x = [0n]; ++x[0] === 1n`). Mirrors the identifier Update + - // PropertyUpdate paths. #4918 prefix/postfix bigint. - let old_num = blk.call(DOUBLE, "js_to_numeric", &[(DOUBLE, &old)]); - let step_arg = match op { - BinaryOp::Sub => "0", - _ => "1", - }; - let new = blk.call( - DOUBLE, - "js_numeric_step", - &[(DOUBLE, &old_num), (I32, step_arg)], - ); - blk.call( - DOUBLE, - "js_dyn_index_set", - &[(DOUBLE, &obj_box), (DOUBLE, &idx_box), (DOUBLE, &new)], - ); - Ok(if *prefix { new } else { old_num }) + // #7615 slice 2 closes the operand-to-operand half only: the + // receiver was live across the index's own lowering. The three + // helpers below (`js_dyn_index_get`, `js_to_numeric`, + // `js_numeric_step`) can each re-enter user code, so `obj_box` and + // `idx_box` are still held across collection points before + // `js_dyn_index_set` consumes them. Closing that needs a per-use + // re-read inside the body rather than one group-wide re-read; see + // the module header. + rooting::with_operands_rooted(ctx, &[object, index], |ctx, vals| { + let (obj_box, idx_box) = (vals[0].clone(), vals[1].clone()); + let blk = ctx.block(); + let old = blk.call( + DOUBLE, + "js_dyn_index_get", + &[(DOUBLE, &obj_box), (DOUBLE, &idx_box)], + ); + // ToNumeric + numeric step so a BigInt element stays BigInt + // (`var x = [0n]; ++x[0] === 1n`). Mirrors the identifier Update + + // PropertyUpdate paths. #4918 prefix/postfix bigint. + let old_num = blk.call(DOUBLE, "js_to_numeric", &[(DOUBLE, &old)]); + let step_arg = match op { + BinaryOp::Sub => "0", + _ => "1", + }; + let new = blk.call( + DOUBLE, + "js_numeric_step", + &[(DOUBLE, &old_num), (I32, step_arg)], + ); + blk.call( + DOUBLE, + "js_dyn_index_set", + &[(DOUBLE, &obj_box), (DOUBLE, &idx_box), (DOUBLE, &new)], + ); + Ok(if *prefix { new } else { old_num }) + }) } // -------- path.basename -------- @@ -1621,17 +1721,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::PathBasenameExt(p, ext) => { // path.basename(path, ext) — strips trailing `ext` suffix. // Runtime: js_path_basename_ext(path_ptr, ext_ptr) -> *StringHeader. - let p_box = lower_expr(ctx, p)?; - let e_box = lower_expr(ctx, ext)?; - let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); - let e_handle = unbox_to_i64(blk, &e_box); - let result = blk.call( - I64, - "js_path_basename_ext", - &[(I64, &p_handle), (I64, &e_handle)], - ); - Ok(nanbox_string_inline(blk, &result)) + rooting::with_operands_rooted(ctx, &[p, ext], |ctx, vals| { + let blk = ctx.block(); + let p_handle = unbox_to_i64(blk, &vals[0]); + let e_handle = unbox_to_i64(blk, &vals[1]); + let result = blk.call( + I64, + "js_path_basename_ext", + &[(I64, &p_handle), (I64, &e_handle)], + ); + Ok(nanbox_string_inline(blk, &result)) + }) } Expr::PathParse(p) => { // path.parse(p) -> object with { dir, base, ext, name, root } @@ -1743,30 +1843,30 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Ok(blk.bitcast_i64_to_double(&result_i64)) } Expr::JsonParseReviver { text, reviver } => { - let s_box = lower_expr(ctx, text)?; - let r_box = lower_expr(ctx, reviver)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let r_handle = unbox_to_i64(blk, &r_box); - let result_i64 = blk.call( - I64, - "js_json_parse_with_reviver", - &[(I64, &s_handle), (I64, &r_handle)], - ); - Ok(blk.bitcast_i64_to_double(&result_i64)) + rooting::with_operands_rooted(ctx, &[text, reviver], |ctx, vals| { + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &vals[0]); + let r_handle = unbox_to_i64(blk, &vals[1]); + let result_i64 = blk.call( + I64, + "js_json_parse_with_reviver", + &[(I64, &s_handle), (I64, &r_handle)], + ); + Ok(blk.bitcast_i64_to_double(&result_i64)) + }) } Expr::JsonParseWithReviver(text, reviver) => { - let s_box = lower_expr(ctx, text)?; - let r_box = lower_expr(ctx, reviver)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let r_handle = unbox_to_i64(blk, &r_box); - let result_i64 = blk.call( - I64, - "js_json_parse_with_reviver", - &[(I64, &s_handle), (I64, &r_handle)], - ); - Ok(blk.bitcast_i64_to_double(&result_i64)) + rooting::with_operands_rooted(ctx, &[text, reviver], |ctx, vals| { + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &vals[0]); + let r_handle = unbox_to_i64(blk, &vals[1]); + let result_i64 = blk.call( + I64, + "js_json_parse_with_reviver", + &[(I64, &s_handle), (I64, &r_handle)], + ); + Ok(blk.bitcast_i64_to_double(&result_i64)) + }) } // -------- new Date() / new Date(ts) / new Date(year, month, ...) -------- diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index 0f2ec62ccf..62197e0a55 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -3,6 +3,43 @@ //! Extracted from `expr/mod.rs` to keep that file under the 2000-line cap. //! Pure mechanical move — match arm bodies are verbatim copies, called from //! `lower_expr`'s outer dispatch. +//! +//! # Layer 1 migrated module (#7615, slice 2) +//! +//! Nothing here names `expr::temp_root`. Multi-operand arms go through +//! [`crate::rooting::with_operands_rooted`]; the two arms that keep writing +//! into a half-built container while they lower more user code go through +//! [`crate::rooting::with_rooted_accumulator`]. +//! `crate::rooting::migration_ledger` fails the build if this module reaches +//! back into the raw API. Single-operand arms keep their plain `lower_expr`, +//! as the template module (`expr/url_main.rs`, #7617) does: with nothing +//! lowered after the operand there is no window and `operand_protection` would +//! answer `Reuse`. +//! +//! ## What the migration found +//! +//! **Callback receivers.** `arr.filter(cb)`, `arr.some(cb)` and `arr.every(cb)` +//! lowered the array, then lowered the callback — and a callback literal is a +//! `js_closure_new`. That is #7620's `find*` finding in three more arms that +//! were missed because they live in a different file. +//! +//! **Two unrooted accumulators.** `Math.min`/`Math.max` with three or more +//! arguments allocate a scratch array and then thread a **raw `ArrayHeader*`** +//! through `js_array_push_f64` — across each remaining argument's own lowering. +//! `Math.min(f(), g(), h())` therefore pushed into a pre-move address. The +//! static-headers path of `fetch(url, { headers: { k: f() } })` has the same +//! shape with `js_object_alloc`. Both are #7154's `ObjectSpread` bug, and +//! because what is stale is a raw `i64` derived above the window rather than a +//! NaN-boxed double, #7280's `root_reload` structurally cannot repair either +//! (slice 1b's finding 2). +//! +//! **`fetch`'s three string operands.** `url`, `method` and `body` sat in +//! registers across the whole headers construction *and* across +//! `js_fetch_headers_to_json`, which enumerates the own properties of a +//! program-supplied value and so can re-enter user code through an accessor — +//! the argument `Expr::ObjectSpread` already makes for `js_object_copy_own_fields` +//! below. That step is emitted rather than lowered, so the window is stated +//! rather than derived: see `with_operands_rooted_across_call`. use anyhow::{bail, Result}; use perry_hir::types::Type as HirType; @@ -10,6 +47,7 @@ use perry_hir::Expr; use crate::lower_conditional::lower_logical; use crate::nanbox::{double_literal, POINTER_MASK_I64, TAG_UNDEFINED}; +use crate::rooting::{self, Arg, Repr}; use crate::type_analysis::{is_definitely_string_expr, is_numeric_expr, map_static_type_args}; use crate::types::{DOUBLE, I32, I64, PTR}; @@ -111,18 +149,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Mirrors ArrayMap: takes a closure header pointer, returns // a new array. Expr::ArrayFilter { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - let result = blk.call( - I64, - "js_array_filter", - &[(I64, &arr_handle), (I64, &cb_handle)], - ); - Ok(nanbox_pointer_inline(blk, &result)) + // #7615 slice 2: the array is live across the callback's lowering, + // and a callback literal is a `js_closure_new`. Same window #7620 + // closed in the four `find*` arms. + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + // #4091: throw TypeError for a non-callable callback before iterating. + let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &vals[1])]); + let result = blk.call( + I64, + "js_array_filter", + &[(I64, &arr_handle), (I64, &cb_handle)], + ); + Ok(nanbox_pointer_inline(blk, &result)) + }) } // -------- fetch(url, { method, body, headers }) -------- @@ -139,140 +180,183 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { headers_dynamic, signal, } => { - let url_box = lower_expr(ctx, url)?; - let method_box = lower_expr(ctx, method)?; - let body_box = lower_expr(ctx, body)?; - // Lower `init.signal` (if any) up front so it can be stashed for - // `js_fetch_with_options` right before the call below. - let signal_box = match signal { - Some(s) => Some(lower_expr(ctx, s)?), - None => None, - }; - - // Obtain the headers as a NaN-boxed object value, then JSON-stringify - // it below. Two cases: - // * `headers_dynamic` — the headers value was a variable, a spread - // literal, or a call (`Object.assign`/`new Headers`/`JSON.parse`). - // Lower it directly; `js_json_stringify` enumerates its own - // properties at runtime (#4932). - // * otherwise — statically-extracted `{ "k": v, ... }` pairs, which - // we build into a fresh object field-by-field. - let headers_obj_box = if let Some(hexpr) = headers_dynamic { - lower_expr(ctx, hexpr)? - } else { - // Build the headers object: js_object_alloc(0, N) followed by - // js_object_set_field_by_name for each (interned key, value). - let n_str = (headers.len() as u32).to_string(); - let zero_str = "0".to_string(); - let headers_handle = - ctx.block() - .call(I64, "js_object_alloc", &[(I32, &zero_str), (I32, &n_str)]); - for (key, val_expr) in headers { - let key_idx = ctx.strings.intern(key); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let v_box = lower_expr(ctx, val_expr)?; + // Lower `init.signal` (if any) with the other operands so it can be + // stashed for `js_fetch_with_options` right before the call below. + let mut operands: Vec<&Expr> = vec![url, method, body]; + if let Some(s) = signal { + operands.push(s); + } + // The window is STATED, not derived (`with_operands_rooted_across_call`): + // the across step ends in `js_fetch_headers_to_json`, which enumerates + // the own properties of a program-supplied value and therefore can run + // a user accessor — the same argument `Expr::ObjectSpread` makes for + // `js_object_copy_own_fields` below. No property of the *headers + // expression* can rule that out, and `fetch` is about to do I/O, so + // the three string operands pay for slots unconditionally. + rooting::with_operands_rooted_across_call( + ctx, + &operands, + |ctx| { + // Obtain the headers as a NaN-boxed object value, then + // JSON-stringify it. Two cases: + // * `headers_dynamic` — the headers value was a variable, a + // spread literal, or a call (`Object.assign`/`new + // Headers`/`JSON.parse`). Lower it directly; + // `js_json_stringify` enumerates its own properties at + // runtime (#4932). + // * otherwise — statically-extracted `{ "k": v, ... }` + // pairs, which we build into a fresh object field-by-field. + let headers_obj_box = if let Some(hexpr) = headers_dynamic { + lower_expr(ctx, hexpr)? + } else { + // Build the headers object: js_object_alloc(0, N) followed + // by js_object_set_field_by_name for each (key, value). + let n_str = (headers.len() as u32).to_string(); + let zero_str = "0".to_string(); + let headers_handle = ctx.block().call( + I64, + "js_object_alloc", + &[(I32, &zero_str), (I32, &n_str)], + ); + // #7615 slice 2: the half-built headers object was a raw + // `i64` across every value's lowering — #7154's + // `ObjectSpread` bug in a second arm. + let protect = + rooting::any_operand_may_collect(ctx, headers.iter().map(|(_, v)| v)); + rooting::with_rooted_accumulator( + ctx, + Repr::Ptr, + &headers_handle, + protect, + |ctx, acc| { + for (key, val_expr) in headers { + let key_idx = ctx.strings.intern(key); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let v_box = lower_expr(ctx, val_expr)?; + let key_raw = { + let blk = ctx.block(); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + blk.and(I64, &key_bits, POINTER_MASK_I64) + }; + acc.call_void( + ctx, + "js_object_set_field_by_name", + &[Arg::Plain(I64, &key_raw), Arg::Plain(DOUBLE, &v_box)], + ); + } + Ok(()) + }, + |ctx, headers_handle| { + let blk = ctx.block(); + Ok(nanbox_pointer_inline(blk, headers_handle)) + }, + )? + }; + // Stringify the headers value into the flat `{name:value}` + // JSON that `js_fetch_with_options` parses. Routed through + // `js_fetch_headers_to_json` (not the generic + // `js_json_stringify`) so a `Headers` instance — a fetch-band + // registry handle, e.g. `headers: new Headers(h)` — is read + // from its registry instead of being dereferenced as a heap + // pointer (the `js_json_stringify`-on-handle SIGSEGV; same + // #5559/#5560 handle-band family). let blk = ctx.block(); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &headers_handle), (I64, &key_raw), (DOUBLE, &v_box)], + Ok(blk.call( + I64, + "js_fetch_headers_to_json", + &[(DOUBLE, &headers_obj_box)], + )) + }, + |ctx, vals, headers_str| { + let blk = ctx.block(); + // The runtime takes raw StringHeader pointers (i64). Unbox + // each input string. `body` may be undefined → unbox produces + // 0 which the runtime treats as "no body" via + // string_from_header(). The unbox happens BELOW the re-read, + // which is the only place it can be correct (slice 1b's + // `BufferSlice` finding). + let url_handle = unbox_to_i64(blk, &vals[0]); + let method_handle = unbox_to_i64(blk, &vals[1]); + let body_handle = unbox_to_i64(blk, &vals[2]); + // Stash the AbortSignal so `js_fetch_with_options` can cancel + // the request when it aborts (`controller.abort()` / + // `AbortSignal.timeout`). + if let Some(sig) = vals.get(3) { + blk.call_void("js_fetch_set_pending_signal", &[(DOUBLE, sig)]); + } + let promise = blk.call( + I64, + "js_fetch_with_options", + &[ + (I64, &url_handle), + (I64, &method_handle), + (I64, &body_handle), + (I64, &headers_str), + ], ); - } - let blk = ctx.block(); - nanbox_pointer_inline(blk, &headers_handle) - }; - - let blk = ctx.block(); - // Stringify the headers value into the flat `{name:value}` JSON that - // `js_fetch_with_options` parses. Routed through - // `js_fetch_headers_to_json` (not the generic `js_json_stringify`) so - // a `Headers` instance — a fetch-band registry handle, e.g. `headers: - // new Headers(h)` — is read from its registry instead of being - // dereferenced as a heap pointer (the `js_json_stringify`-on-handle - // SIGSEGV; same #5559/#5560 handle-band family). - let headers_str = blk.call( - I64, - "js_fetch_headers_to_json", - &[(DOUBLE, &headers_obj_box)], - ); - - // The runtime takes raw StringHeader pointers (i64). Unbox each - // input string. `body` may be undefined → unbox produces 0 which - // the runtime treats as "no body" via string_from_header(). - let url_handle = unbox_to_i64(blk, &url_box); - let method_handle = unbox_to_i64(blk, &method_box); - let body_handle = unbox_to_i64(blk, &body_box); - // Stash the AbortSignal so `js_fetch_with_options` can cancel the - // request when it aborts (`controller.abort()` / `AbortSignal.timeout`). - if let Some(sig) = &signal_box { - blk.call_void("js_fetch_set_pending_signal", &[(DOUBLE, sig)]); - } - let promise = blk.call( - I64, - "js_fetch_with_options", - &[ - (I64, &url_handle), - (I64, &method_handle), - (I64, &body_handle), - (I64, &headers_str), - ], - ); - Ok(nanbox_pointer_inline(blk, &promise)) + Ok(nanbox_pointer_inline(blk, &promise)) + }, + ) } // -------- arr.some(callback) -> boolean -------- // js_array_some returns a NaN-tagged TAG_TRUE/TAG_FALSE as f64, // so we forward it directly without conversion. Expr::ArraySome { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - Ok(blk.call( - DOUBLE, - "js_array_some", - &[(I64, &arr_handle), (I64, &cb_handle)], - )) + // #7615 slice 2: same callback window as `ArrayFilter` above. + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + // #4091: throw TypeError for a non-callable callback before iterating. + let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &vals[1])]); + Ok(blk.call( + DOUBLE, + "js_array_some", + &[(I64, &arr_handle), (I64, &cb_handle)], + )) + }) } // -------- arr.every(callback) -> boolean -------- Expr::ArrayEvery { array, callback } => { - let arr_box = lower_expr(ctx, array)?; - let cb_box = lower_expr(ctx, callback)?; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - // #4091: throw TypeError for a non-callable callback before iterating. - let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &cb_box)]); - Ok(blk.call( - DOUBLE, - "js_array_every", - &[(I64, &arr_handle), (I64, &cb_handle)], - )) + // #7615 slice 2: same callback window as `ArrayFilter` above. + rooting::with_operands_rooted(ctx, &[array, callback], |ctx, vals| { + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + // #4091: throw TypeError for a non-callable callback before iterating. + let cb_handle = blk.call(I64, "js_validate_array_callback", &[(DOUBLE, &vals[1])]); + Ok(blk.call( + DOUBLE, + "js_array_every", + &[(I64, &arr_handle), (I64, &cb_handle)], + )) + }) } // -------- arr.join(separator?) -> string -------- // The runtime wrapper applies Array.join separator semantics: // omitted/undefined means comma; every other value is ToString. Expr::ArrayJoin { array, separator } => { - let arr_box = lower_expr(ctx, array)?; - let sep_box = if let Some(sep_expr) = separator { - lower_expr(ctx, sep_expr)? - } else { - double_literal(f64::from_bits(TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - let result = blk.call( - I64, - "js_array_join_value", - &[(I64, &arr_handle), (DOUBLE, &sep_box)], - ); - Ok(nanbox_string_inline(blk, &result)) + let mut operands: Vec<&Expr> = vec![array]; + if let Some(sep_expr) = separator { + operands.push(sep_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let sep_box = vals + .get(1) + .cloned() + .unwrap_or_else(|| double_literal(f64::from_bits(TAG_UNDEFINED))); + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + let result = blk.call( + I64, + "js_array_join_value", + &[(I64, &arr_handle), (DOUBLE, &sep_box)], + ); + Ok(nanbox_string_inline(blk, &result)) + }) } // -------- Array.prototype..call/apply(arrayLike, ...) (#4597) -------- @@ -287,197 +371,201 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { receiver, args, } => { - let recv_box = lower_expr(ctx, receiver)?; - let mut arg_boxes: Vec = Vec::with_capacity(args.len()); - for a in args { - arg_boxes.push(lower_expr(ctx, a)?); - } - let undef = || double_literal(f64::from_bits(TAG_UNDEFINED)); - let nth = |i: usize| arg_boxes.get(i).cloned(); - let blk = ctx.block(); - let result = match method.as_str() { - // Callback iterators: (recv, callback, thisArg). - "forEach" | "map" | "filter" | "some" | "every" | "find" | "findIndex" - | "findLast" | "findLastIndex" => { - let cb = nth(0).unwrap_or_else(undef); - let this_arg = nth(1).unwrap_or_else(undef); - let fname = match method.as_str() { - "forEach" => "js_arraylike_forEach", - "map" => "js_arraylike_map", - "filter" => "js_arraylike_filter", - "some" => "js_arraylike_some", - "every" => "js_arraylike_every", - "find" => "js_arraylike_find", - "findIndex" => "js_arraylike_findIndex", - "findLast" => "js_arraylike_findLast", - _ => "js_arraylike_findLastIndex", - }; - blk.call( - DOUBLE, - fname, - &[(DOUBLE, &recv_box), (DOUBLE, &cb), (DOUBLE, &this_arg)], - ) - } - // Reducers: (recv, callback, has_init, init). - "reduce" | "reduceRight" => { - let cb = nth(0).unwrap_or_else(undef); - let (has_init, init) = match nth(1) { - Some(i) => ("1".to_string(), i), - None => ("0".to_string(), undef()), - }; - let fname = if method == "reduce" { - "js_arraylike_reduce" - } else { - "js_arraylike_reduceRight" - }; - blk.call( - DOUBLE, - fname, - &[ - (DOUBLE, &recv_box), - (DOUBLE, &cb), - (I32, &has_init), - (DOUBLE, &init), - ], - ) - } - // Search: (recv, value, fromIndex, has_from). - "indexOf" | "lastIndexOf" | "includes" => { - let value = nth(0).unwrap_or_else(undef); - let (has_from, from) = match nth(1) { - Some(f) => ("1".to_string(), f), - None => ("0".to_string(), undef()), - }; - let fname = match method.as_str() { - "indexOf" => "js_arraylike_indexOf", - "lastIndexOf" => "js_arraylike_lastIndexOf", - _ => "js_arraylike_includes", - }; - blk.call( - DOUBLE, - fname, - &[ - (DOUBLE, &recv_box), - (DOUBLE, &value), - (DOUBLE, &from), - (I32, &has_from), - ], - ) - } - // at(index): ToIntegerOrInfinity(undefined) === 0 when omitted. - "at" => { - let idx = nth(0).unwrap_or_else(undef); - blk.call( - DOUBLE, - "js_arraylike_at", - &[(DOUBLE, &recv_box), (DOUBLE, &idx)], - ) - } - // join(separator?): undefined separator → comma. - "join" => { - let sep = nth(0).unwrap_or_else(undef); - blk.call( - DOUBLE, - "js_arraylike_join", - &[(DOUBLE, &recv_box), (DOUBLE, &sep)], - ) - } - // slice(start?, end?): has-flags distinguish omitted from undefined. - "slice" => { - let (has_start, start) = match nth(0) { - Some(s) => ("1".to_string(), s), - None => ("0".to_string(), undef()), - }; - let (has_end, end) = match nth(1) { - Some(e) => ("1".to_string(), e), - None => ("0".to_string(), undef()), - }; - blk.call( - DOUBLE, - "js_arraylike_slice", - &[ - (DOUBLE, &recv_box), - (DOUBLE, &start), - (I32, &has_start), - (DOUBLE, &end), - (I32, &has_end), - ], - ) - } - // sort(comparator?): validated + run by the runtime engine. - "sort" => { - let cmp = nth(0).unwrap_or_else(undef); - blk.call( - DOUBLE, - "js_arraylike_sort", - &[(DOUBLE, &recv_box), (DOUBLE, &cmp)], - ) - } - // splice(...) / concat(...): variadic — pass an alloca buffer - // of raw NaN-boxed doubles + count (mirrors the dense - // `js_array_concat_variadic` lowering). - "splice" | "concat" => { - let n = arg_boxes.len(); - let (buf_reg, count_str) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_boxes.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; - let fname = if method == "splice" { - "js_arraylike_splice" - } else { - "js_arraylike_concat" - }; - blk.call( - DOUBLE, - fname, - &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], - ) - } - // pop() / shift(): no args, generic over a value receiver. - "pop" | "shift" => { - let fname = if method == "pop" { - "js_arraylike_pop" - } else { - "js_arraylike_shift" - }; - blk.call(DOUBLE, fname, &[(DOUBLE, &recv_box)]) - } - // push(...) / unshift(...): variadic — pass an alloca buffer of - // raw NaN-boxed doubles + count (mirrors splice/concat above). - "push" | "unshift" => { - let n = arg_boxes.len(); - let (buf_reg, count_str) = if n == 0 { - ("null".to_string(), "0".to_string()) - } else { - let buf_reg = blk.next_reg(); - blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); - for (i, val) in arg_boxes.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, val, &slot); - } - (buf_reg, format!("{}", n)) - }; - let fname = if method == "push" { - "js_arraylike_push" - } else { - "js_arraylike_unshift" - }; - blk.call( - DOUBLE, - fname, - &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], - ) - } - other => bail!("unsupported generic array-like method '{other}'"), - }; - Ok(result) + // #7615 slice 2: the receiver sat in a register across every + // argument's lowering, and each argument across the ones after it. + let mut operands: Vec<&Expr> = Vec::with_capacity(args.len() + 1); + operands.push(receiver); + operands.extend(args.iter()); + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let recv_box = vals[0].clone(); + let arg_boxes: Vec = vals[1..].to_vec(); + let undef = || double_literal(f64::from_bits(TAG_UNDEFINED)); + let nth = |i: usize| arg_boxes.get(i).cloned(); + let blk = ctx.block(); + let result = match method.as_str() { + // Callback iterators: (recv, callback, thisArg). + "forEach" | "map" | "filter" | "some" | "every" | "find" | "findIndex" + | "findLast" | "findLastIndex" => { + let cb = nth(0).unwrap_or_else(undef); + let this_arg = nth(1).unwrap_or_else(undef); + let fname = match method.as_str() { + "forEach" => "js_arraylike_forEach", + "map" => "js_arraylike_map", + "filter" => "js_arraylike_filter", + "some" => "js_arraylike_some", + "every" => "js_arraylike_every", + "find" => "js_arraylike_find", + "findIndex" => "js_arraylike_findIndex", + "findLast" => "js_arraylike_findLast", + _ => "js_arraylike_findLastIndex", + }; + blk.call( + DOUBLE, + fname, + &[(DOUBLE, &recv_box), (DOUBLE, &cb), (DOUBLE, &this_arg)], + ) + } + // Reducers: (recv, callback, has_init, init). + "reduce" | "reduceRight" => { + let cb = nth(0).unwrap_or_else(undef); + let (has_init, init) = match nth(1) { + Some(i) => ("1".to_string(), i), + None => ("0".to_string(), undef()), + }; + let fname = if method == "reduce" { + "js_arraylike_reduce" + } else { + "js_arraylike_reduceRight" + }; + blk.call( + DOUBLE, + fname, + &[ + (DOUBLE, &recv_box), + (DOUBLE, &cb), + (I32, &has_init), + (DOUBLE, &init), + ], + ) + } + // Search: (recv, value, fromIndex, has_from). + "indexOf" | "lastIndexOf" | "includes" => { + let value = nth(0).unwrap_or_else(undef); + let (has_from, from) = match nth(1) { + Some(f) => ("1".to_string(), f), + None => ("0".to_string(), undef()), + }; + let fname = match method.as_str() { + "indexOf" => "js_arraylike_indexOf", + "lastIndexOf" => "js_arraylike_lastIndexOf", + _ => "js_arraylike_includes", + }; + blk.call( + DOUBLE, + fname, + &[ + (DOUBLE, &recv_box), + (DOUBLE, &value), + (DOUBLE, &from), + (I32, &has_from), + ], + ) + } + // at(index): ToIntegerOrInfinity(undefined) === 0 when omitted. + "at" => { + let idx = nth(0).unwrap_or_else(undef); + blk.call( + DOUBLE, + "js_arraylike_at", + &[(DOUBLE, &recv_box), (DOUBLE, &idx)], + ) + } + // join(separator?): undefined separator → comma. + "join" => { + let sep = nth(0).unwrap_or_else(undef); + blk.call( + DOUBLE, + "js_arraylike_join", + &[(DOUBLE, &recv_box), (DOUBLE, &sep)], + ) + } + // slice(start?, end?): has-flags distinguish omitted from undefined. + "slice" => { + let (has_start, start) = match nth(0) { + Some(s) => ("1".to_string(), s), + None => ("0".to_string(), undef()), + }; + let (has_end, end) = match nth(1) { + Some(e) => ("1".to_string(), e), + None => ("0".to_string(), undef()), + }; + blk.call( + DOUBLE, + "js_arraylike_slice", + &[ + (DOUBLE, &recv_box), + (DOUBLE, &start), + (I32, &has_start), + (DOUBLE, &end), + (I32, &has_end), + ], + ) + } + // sort(comparator?): validated + run by the runtime engine. + "sort" => { + let cmp = nth(0).unwrap_or_else(undef); + blk.call( + DOUBLE, + "js_arraylike_sort", + &[(DOUBLE, &recv_box), (DOUBLE, &cmp)], + ) + } + // splice(...) / concat(...): variadic — pass an alloca buffer + // of raw NaN-boxed doubles + count (mirrors the dense + // `js_array_concat_variadic` lowering). + "splice" | "concat" => { + let n = arg_boxes.len(); + let (buf_reg, count_str) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf_reg = blk.next_reg(); + blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); + for (i, val) in arg_boxes.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + blk.store(DOUBLE, val, &slot); + } + (buf_reg, format!("{}", n)) + }; + let fname = if method == "splice" { + "js_arraylike_splice" + } else { + "js_arraylike_concat" + }; + blk.call( + DOUBLE, + fname, + &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], + ) + } + // pop() / shift(): no args, generic over a value receiver. + "pop" | "shift" => { + let fname = if method == "pop" { + "js_arraylike_pop" + } else { + "js_arraylike_shift" + }; + blk.call(DOUBLE, fname, &[(DOUBLE, &recv_box)]) + } + // push(...) / unshift(...): variadic — pass an alloca buffer of + // raw NaN-boxed doubles + count (mirrors splice/concat above). + "push" | "unshift" => { + let n = arg_boxes.len(); + let (buf_reg, count_str) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf_reg = blk.next_reg(); + blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); + for (i, val) in arg_boxes.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + blk.store(DOUBLE, val, &slot); + } + (buf_reg, format!("{}", n)) + }; + let fname = if method == "push" { + "js_arraylike_push" + } else { + "js_arraylike_unshift" + }; + blk.call( + DOUBLE, + fname, + &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], + ) + } + other => bail!("unsupported generic array-like method '{other}'"), + }; + Ok(result) + }) } // -------- map.delete(key) -> boolean -------- @@ -487,60 +575,62 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let use_number_key_map = !use_string_key_map && is_static_number_key_map(ctx, map) && is_numeric_expr(ctx, key); - let m_box = lower_expr(ctx, map)?; - let k_box = lower_expr(ctx, key)?; - let m_handle = { - let blk = ctx.block(); - unbox_to_i64(blk, &m_box) - }; - let i32_v = if use_string_key_map { - let (k_handle, i32_v) = { + // #7615 slice 2: the map is live across the key's lowering. + rooting::with_operands_rooted(ctx, &[map, key], |ctx, vals| { + let (m_box, k_box) = (vals[0].clone(), vals[1].clone()); + let m_handle = { let blk = ctx.block(); - let k_handle = unbox_str_handle(blk, &k_box); - let i32_v = blk.call( - I32, + unbox_to_i64(blk, &m_box) + }; + let i32_v = if use_string_key_map { + let (k_handle, i32_v) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let i32_v = blk.call( + I32, + "js_map_delete_string_key", + &[(I64, &m_handle), (I64, &k_handle)], + ); + (k_handle, i32_v) + }; + record_collection_string_key_selected( + ctx, + "MapDelete", + "collection_string_key.map_delete", + &k_handle, + "map", "js_map_delete_string_key", - &[(I64, &m_handle), (I64, &k_handle)], ); - (k_handle, i32_v) - }; - record_collection_string_key_selected( - ctx, - "MapDelete", - "collection_string_key.map_delete", - &k_handle, - "map", - "js_map_delete_string_key", - ); - i32_v - } else if use_number_key_map { - guarded_map_number_key_delete(ctx, &m_handle, &k_box) - } else { - let i32_v = { - let blk = ctx.block(); - blk.call(I32, "js_map_delete", &[(I64, &m_handle), (DOUBLE, &k_box)]) + i32_v + } else if use_number_key_map { + guarded_map_number_key_delete(ctx, &m_handle, &k_box) + } else { + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_map_delete", &[(I64, &m_handle), (DOUBLE, &k_box)]) + }; + record_collection_string_key_fallback( + ctx, + "MapDelete", + "collection_string_key.map_delete_generic", + &k_box, + "map", + "js_map_delete", + "receiver_or_key_not_static_string", + ); + i32_v }; - record_collection_string_key_fallback( - ctx, - "MapDelete", - "collection_string_key.map_delete_generic", - &k_box, - "map", - "js_map_delete", - "receiver_or_key_not_static_string", + let blk = ctx.block(); + let bit = blk.icmp_ne(I32, &i32_v, "0"); + let tagged = blk.select( + crate::types::I1, + &bit, + I64, + crate::nanbox::TAG_TRUE_I64, + crate::nanbox::TAG_FALSE_I64, ); - i32_v - }; - let blk = ctx.block(); - let bit = blk.icmp_ne(I32, &i32_v, "0"); - let tagged = blk.select( - crate::types::I1, - &bit, - I64, - crate::nanbox::TAG_TRUE_I64, - crate::nanbox::TAG_FALSE_I64, - ); - Ok(blk.bitcast_i64_to_double(&tagged)) + Ok(blk.bitcast_i64_to_double(&tagged)) + }) } // -------- Object.keys(obj) -> string[] -------- @@ -603,27 +693,45 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // temporary fixed-size array via js_array_alloc + push. Expr::MathMin(values) => { if values.len() == 2 { - let left = lower_expr(ctx, &values[0])?; - let right = lower_expr(ctx, &values[1])?; - let blk = ctx.block(); - return Ok(blk.call(DOUBLE, "js_math_min2", &[(DOUBLE, &left), (DOUBLE, &right)])); + return rooting::with_operands_rooted( + ctx, + &[&values[0], &values[1]], + |ctx, vals| { + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_math_min2", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + )) + }, + ); } let cap = (values.len() as u32).to_string(); let arr_handle_v = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - // Push each value. push_f64 may realloc, so we thread the - // returned pointer through. - let mut current = arr_handle_v; - for v_expr in values { - let v_box = lower_expr(ctx, v_expr)?; - let blk = ctx.block(); - current = blk.call( - I64, - "js_array_push_f64", - &[(I64, ¤t), (DOUBLE, &v_box)], - ); - } - let blk = ctx.block(); - Ok(blk.call(DOUBLE, "js_math_min_array", &[(I64, ¤t)])) + // Push each value. push_f64 may realloc, so the returned pointer is + // threaded through — and #7615 slice 2: it was threaded through a + // RAW SSA register that stayed live across the NEXT argument's + // lowering, so `Math.min(f(), g(), h())` pushed into a pre-move + // address. The accumulator is the fix; being an `i64` derived above + // the window is exactly what #7280 cannot repair. + let protect = rooting::any_operand_may_collect(ctx, values.iter()); + rooting::with_rooted_accumulator( + ctx, + Repr::Ptr, + &arr_handle_v, + protect, + |ctx, acc| { + for v_expr in values { + let v_box = lower_expr(ctx, v_expr)?; + acc.advance(ctx, "js_array_push_f64", &[Arg::Plain(DOUBLE, &v_box)]); + } + Ok(()) + }, + |ctx, current| { + let blk = ctx.block(); + Ok(blk.call(DOUBLE, "js_math_min_array", &[(I64, current)])) + }, + ) } Expr::MathMinSpread(arr_expr) => { let arr_box = lower_expr(ctx, arr_expr)?; @@ -635,24 +743,40 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- Math.max(...args) — same shape as Math.min -------- Expr::MathMax(values) => { if values.len() == 2 { - let left = lower_expr(ctx, &values[0])?; - let right = lower_expr(ctx, &values[1])?; - let blk = ctx.block(); - return Ok(blk.call(DOUBLE, "js_math_max2", &[(DOUBLE, &left), (DOUBLE, &right)])); - } - let cap = (values.len() as u32).to_string(); - let mut current = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); - for v_expr in values { - let v_box = lower_expr(ctx, v_expr)?; - let blk = ctx.block(); - current = blk.call( - I64, - "js_array_push_f64", - &[(I64, ¤t), (DOUBLE, &v_box)], + return rooting::with_operands_rooted( + ctx, + &[&values[0], &values[1]], + |ctx, vals| { + let blk = ctx.block(); + Ok(blk.call( + DOUBLE, + "js_math_max2", + &[(DOUBLE, &vals[0]), (DOUBLE, &vals[1])], + )) + }, ); } - let blk = ctx.block(); - Ok(blk.call(DOUBLE, "js_math_max_array", &[(I64, ¤t)])) + let cap = (values.len() as u32).to_string(); + let arr_handle_v = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap)]); + // Same raw-accumulator window as `MathMin` above (#7615 slice 2). + let protect = rooting::any_operand_may_collect(ctx, values.iter()); + rooting::with_rooted_accumulator( + ctx, + Repr::Ptr, + &arr_handle_v, + protect, + |ctx, acc| { + for v_expr in values { + let v_box = lower_expr(ctx, v_expr)?; + acc.advance(ctx, "js_array_push_f64", &[Arg::Plain(DOUBLE, &v_box)]); + } + Ok(()) + }, + |ctx, current| { + let blk = ctx.block(); + Ok(blk.call(DOUBLE, "js_math_max_array", &[(I64, current)])) + }, + ) } Expr::MathMaxSpread(arr_expr) => { let arr_box = lower_expr(ctx, arr_expr)?; @@ -700,21 +824,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- arr.slice(start, end?) -- new array slice -------- Expr::ArraySlice { array, start, end } => { - let arr_box = lower_expr(ctx, array)?; - let start_d = lower_expr(ctx, start)?; - let end_d = if let Some(end_expr) = end { - lower_expr(ctx, end_expr)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let blk = ctx.block(); - let arr_handle = unbox_to_i64(blk, &arr_box); - let result = blk.call( - I64, - "js_array_slice_values", - &[(I64, &arr_handle), (DOUBLE, &start_d), (DOUBLE, &end_d)], - ); - Ok(nanbox_pointer_inline(blk, &result)) + let mut operands: Vec<&Expr> = vec![array, start]; + if let Some(end_expr) = end { + operands.push(end_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let end_d = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &vals[0]); + let result = blk.call( + I64, + "js_array_slice_values", + &[(I64, &arr_handle), (DOUBLE, &vals[1]), (DOUBLE, &end_d)], + ); + Ok(nanbox_pointer_inline(blk, &result)) + }) } // -------- arr.shift() (HIR variant takes a LocalId) -------- @@ -740,11 +866,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // first enforces ECMA-262 13.10.1 step 5: a non-Object right operand // (`"x" in 5`, `... in null`, `... in Symbol()`, …) throws a TypeError. Expr::In { property, object } => { - let key = lower_expr(ctx, property)?; - let obj = lower_expr(ctx, object)?; - Ok(ctx - .block() - .call(DOUBLE, "js_in_operator", &[(DOUBLE, &obj), (DOUBLE, &key)])) + rooting::with_operands_rooted(ctx, &[property, object], |ctx, vals| { + Ok(ctx.block().call( + DOUBLE, + "js_in_operator", + &[(DOUBLE, &vals[1]), (DOUBLE, &vals[0])], + )) + }) } Expr::PrivateBrandCheck { class_name, @@ -809,15 +937,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // since the HIR-level fs.writeFileSync is void in JS. // -------- parseInt(string, radix?) -> number -------- Expr::ParseInt { string, radix } => { - let s_box = lower_expr(ctx, string)?; - let r_d = if let Some(r_expr) = radix { - lower_expr(ctx, r_expr)? - } else { - "0.0".to_string() - }; - let blk = ctx.block(); - let s_handle = blk.call(I64, "js_string_coerce", &[(DOUBLE, &s_box)]); - Ok(blk.call(DOUBLE, "js_parse_int", &[(I64, &s_handle), (DOUBLE, &r_d)])) + let mut operands: Vec<&Expr> = vec![string]; + if let Some(r_expr) = radix { + operands.push(r_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let r_d = vals.get(1).cloned().unwrap_or_else(|| "0.0".to_string()); + let blk = ctx.block(); + let s_handle = blk.call(I64, "js_string_coerce", &[(DOUBLE, &vals[0])]); + Ok(blk.call(DOUBLE, "js_parse_int", &[(I64, &s_handle), (DOUBLE, &r_d)])) + }) } Expr::ParseFloat(string) => { let s_box = lower_expr(ctx, string)?; @@ -873,20 +1002,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // identity shortcut (a RegExp pattern + undefined flags returns the // argument unchanged) before falling back to the same constructor. // `new RegExp(re)` keeps `js_regexp_construct` so it always copies. - let pattern_box = lower_expr(ctx, pattern)?; - let flags_box = if let Some(flags_expr) = flags { - lower_expr(ctx, flags_expr)? - } else { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) - }; - let ctor = if *is_call { - "js_regexp_construct_call" - } else { - "js_regexp_construct" - }; - let blk = ctx.block(); - let result = blk.call(I64, ctor, &[(DOUBLE, &pattern_box), (DOUBLE, &flags_box)]); - Ok(nanbox_pointer_inline(blk, &result)) + let mut operands: Vec<&Expr> = vec![pattern]; + if let Some(flags_expr) = flags { + operands.push(flags_expr); + } + rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let flags_box = vals.get(1).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let ctor = if *is_call { + "js_regexp_construct_call" + } else { + "js_regexp_construct" + }; + let blk = ctx.block(); + let result = blk.call(I64, ctor, &[(DOUBLE, &vals[0]), (DOUBLE, &flags_box)]); + Ok(nanbox_pointer_inline(blk, &result)) + }) } // -------- ObjectSpread literal -------- @@ -938,39 +1070,46 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // (`gc/policy.rs`, `GC_SAFEPOINT_PENDING`) and is conservative-scanned // or budgeted-non-moving otherwise, so the register stays valid. let protect_handle = parts.iter().any(|(k, _)| k.is_none()) - || super::temp_root::any_may_trigger_gc(ctx, parts.iter().map(|(_, v)| v)); - let rooted = super::temp_root::rooted_handle_begin(ctx, &obj_handle, protect_handle); - for (key_opt, value_expr) in parts { - if let Some(key) = key_opt { - // Static key:value pair. - let v = lower_expr(ctx, value_expr)?; - let key_idx = ctx.strings.intern(key); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let obj_handle = super::temp_root::rooted_handle_get(ctx, &rooted); - let blk = ctx.block(); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &v)], - ); - } else { - // `...expr` spread — copy all own fields from the - // source object into `obj_handle`. - let src_box = lower_expr(ctx, value_expr)?; - let obj_handle = super::temp_root::rooted_handle_get(ctx, &rooted); - ctx.block().call_void( - "js_object_copy_own_fields", - &[(I64, &obj_handle), (DOUBLE, &src_box)], - ); - } - } - let obj_handle = super::temp_root::rooted_handle_get(ctx, &rooted); - let boxed = nanbox_pointer_inline(ctx.block(), &obj_handle); - super::temp_root::rooted_handle_release(ctx, rooted); - Ok(boxed) + || rooting::any_operand_may_collect(ctx, parts.iter().map(|(_, v)| v)); + rooting::with_rooted_accumulator( + ctx, + Repr::Ptr, + &obj_handle, + protect_handle, + |ctx, acc| { + for (key_opt, value_expr) in parts { + if let Some(key) = key_opt { + // Static key:value pair. + let v = lower_expr(ctx, value_expr)?; + let key_idx = ctx.strings.intern(key); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let key_raw = { + let blk = ctx.block(); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + blk.and(I64, &key_bits, POINTER_MASK_I64) + }; + acc.call_void( + ctx, + "js_object_set_field_by_name", + &[Arg::Plain(I64, &key_raw), Arg::Plain(DOUBLE, &v)], + ); + } else { + // `...expr` spread — copy all own fields from the + // source object into the accumulator. + let src_box = lower_expr(ctx, value_expr)?; + acc.call_void( + ctx, + "js_object_copy_own_fields", + &[Arg::Plain(DOUBLE, &src_box)], + ); + } + } + Ok(()) + }, + |ctx, obj_handle| Ok(nanbox_pointer_inline(ctx.block(), obj_handle)), + ) } // -------- Object.assign(target, ...sources) -------- @@ -1009,24 +1148,25 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // can rule it out. (#7198 declined the "a helper's own allocation // initiates a moving collection" argument on evidence; this is the // route it accepted instead.) - let acc_slot = super::temp_root::temp_root_push_double(ctx, &acc); - for src in sources { - let src_box = lower_expr(ctx, src)?; - let acc_now = super::temp_root::temp_root_get_double(ctx, &acc_slot); - let next = ctx.block().call( - DOUBLE, - "js_object_assign_one", - &[(DOUBLE, &acc_now), (DOUBLE, &src_box)], - ); - // The helper now returns the post-collection target address, so - // publish that back into the root rather than keeping the - // pre-call one: `Object.assign(t, a, b)` threads it into `b`'s - // link, and the caller receives it. - super::temp_root::temp_root_set_double(ctx, &acc_slot, &next); - } - let acc = super::temp_root::temp_root_get_double(ctx, &acc_slot); - super::temp_root::temp_root_truncate(ctx, &acc_slot); - Ok(acc) + rooting::with_rooted_accumulator( + ctx, + Repr::Boxed, + &acc, + true, + |ctx, acc| { + for src in sources { + let src_box = lower_expr(ctx, src)?; + // `advance`: the helper returns the post-collection + // target address, so publish that back into the root + // rather than keeping the pre-call one — + // `Object.assign(t, a, b)` threads it into `b`'s link, + // and the caller receives it. + acc.advance(ctx, "js_object_assign_one", &[Arg::Plain(DOUBLE, &src_box)]); + } + Ok(()) + }, + |_ctx, acc| Ok(acc.to_string()), + ) } // -------- new Set(iter) -------- diff --git a/crates/perry-codegen/src/lower_call/property_get/map_set.rs b/crates/perry-codegen/src/lower_call/property_get/map_set.rs index 10cd6530d9..55d422ef60 100644 --- a/crates/perry-codegen/src/lower_call/property_get/map_set.rs +++ b/crates/perry-codegen/src/lower_call/property_get/map_set.rs @@ -1,11 +1,34 @@ //! Map / Set method dispatch + Map/Set/URLSearchParams `.forEach`. //! Pure code move from `property_get.rs` — no behavior change. +//! +//! # Layer 1 migrated module (#7615, slice 2) +//! +//! Nothing here names `expr::temp_root`. Every arm whose receiver is live +//! across the lowering of an argument goes through +//! [`crate::rooting::with_operands_rooted`], which lowers the group left to +//! right with each already-evaluated value rooted across the ones that follow, +//! re-reads them below the last collection point, and owns the release on every +//! path out including `?`. `crate::rooting::migration_ledger` fails the build if +//! this module reaches back into the raw API. +//! +//! This module was already hand-rooted end to end (#6970), so the migration is +//! a translation and not a repair: the emitted IR is unchanged. What it buys is +//! that the release stops being a statement a later edit can move into a branch +//! — the shape #7462 shipped in `URLSearchParams.delete`, which is the direct +//! sibling of these arms. +//! +//! The zero-argument arms (`clear`, `entries` / `keys` / `values`) keep their +//! plain `lower_expr`: with nothing lowered after the receiver there is no +//! window, `operand_protection` would answer `Reuse`, and wrapping them would +//! emit the same IR through more machinery. Same rule the template module +//! (`expr/url_main.rs`, #7617) states. use anyhow::Result; use perry_hir::Expr; -use crate::expr::{lower_expr, temp_root, unbox_to_i64, FnCtx}; +use crate::expr::{lower_expr, unbox_to_i64, FnCtx}; use crate::nanbox::double_literal; +use crate::rooting; use crate::type_analysis::{is_map_expr, is_set_expr, is_url_search_params_expr}; use crate::types::{DOUBLE, I64}; @@ -24,39 +47,41 @@ pub(crate) fn try_lower_map_set_methods( "set" if args.len() == 2 => { // #6970: each finished operand is live in an SSA register // across the ones that follow, and those can collect. - let (vals, guard) = - temp_root::lower_exprs_rooted(ctx, &[object, &args[0], &args[1]])?; - let (m_box, k_box, v_box) = (vals[0].clone(), vals[1].clone(), vals[2].clone()); - { - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void( - "js_map_set", - &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], - ); - } - temp_root::temp_root_release(ctx, guard); - return Ok(Some(m_box)); + return rooting::with_operands_rooted( + ctx, + &[object, &args[0], &args[1]], + |ctx, vals| { + let (m_box, k_box, v_box) = + (vals[0].clone(), vals[1].clone(), vals[2].clone()); + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + blk.call_void( + "js_map_set", + &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], + ); + Ok(Some(m_box)) + }, + ); } "get" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (m_box, k_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let value = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (m_box, k_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let m_handle = unbox_to_i64(blk, &m_box); - blk.call(DOUBLE, "js_map_get", &[(I64, &m_handle), (DOUBLE, &k_box)]) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(value)); + Ok(Some(blk.call( + DOUBLE, + "js_map_get", + &[(I64, &m_handle), (DOUBLE, &k_box)], + ))) + }); } "has" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (m_box, k_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let result = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (m_box, k_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let m_handle = unbox_to_i64(blk, &m_box); let i32_v = blk.call( @@ -64,17 +89,14 @@ pub(crate) fn try_lower_map_set_methods( "js_map_has", &[(I64, &m_handle), (DOUBLE, &k_box)], ); - crate::expr::i32_bool_to_nanbox(blk, &i32_v) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(result)); + Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))) + }); } "delete" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (m_box, k_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let result = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (m_box, k_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let m_handle = unbox_to_i64(blk, &m_box); let i32_v = blk.call( @@ -82,10 +104,8 @@ pub(crate) fn try_lower_map_set_methods( "js_map_delete", &[(I64, &m_handle), (DOUBLE, &k_box)], ); - crate::expr::i32_bool_to_nanbox(blk, &i32_v) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(result)); + Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))) + }); } "clear" if args.is_empty() => { let m_box = lower_expr(ctx, object)?; @@ -133,22 +153,19 @@ pub(crate) fn try_lower_map_set_methods( "add" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (s_box, v_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (s_box, v_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let s_handle = unbox_to_i64(blk, &s_box); blk.call_void("js_set_add", &[(I64, &s_handle), (DOUBLE, &v_box)]); - } - temp_root::temp_root_release(ctx, guard); - return Ok(Some(s_box)); + Ok(Some(s_box)) + }); } "has" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (s_box, v_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let result = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (s_box, v_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let s_handle = unbox_to_i64(blk, &s_box); let i32_v = blk.call( @@ -156,17 +173,14 @@ pub(crate) fn try_lower_map_set_methods( "js_set_has", &[(I64, &s_handle), (DOUBLE, &v_box)], ); - crate::expr::i32_bool_to_nanbox(blk, &i32_v) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(result)); + Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))) + }); } "delete" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (s_box, v_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let result = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (s_box, v_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let s_handle = unbox_to_i64(blk, &s_box); let i32_v = blk.call( @@ -174,10 +188,8 @@ pub(crate) fn try_lower_map_set_methods( "js_set_delete", &[(I64, &s_handle), (DOUBLE, &v_box)], ); - crate::expr::i32_bool_to_nanbox(blk, &i32_v) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(result)); + Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))) + }); } "clear" if args.is_empty() => { let s_box = lower_expr(ctx, object)?; @@ -222,9 +234,8 @@ pub(crate) fn try_lower_map_set_methods( "union" | "intersection" | "difference" | "symmetricDifference" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (s_box, other_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let boxed = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (s_box, other_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let s_handle = unbox_to_i64(blk, &s_box); let runtime_fn = match property { @@ -236,17 +247,14 @@ pub(crate) fn try_lower_map_set_methods( }; let result = blk.call(I64, runtime_fn, &[(I64, &s_handle), (DOUBLE, &other_box)]); - crate::expr::nanbox_pointer_inline_pub(blk, &result) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(boxed)); + Ok(Some(crate::expr::nanbox_pointer_inline_pub(blk, &result))) + }); } "isSubsetOf" | "isSupersetOf" | "isDisjointFrom" if args.len() == 1 => { // #6970: the argument's lowering can collect while the // receiver is live only in an SSA register. - let (s_box, other_box, guard) = - temp_root::lower_operand_pair_rooted(ctx, object, &args[0])?; - let result = { + return rooting::with_operands_rooted(ctx, &[object, &args[0]], |ctx, vals| { + let (s_box, other_box) = (vals[0].clone(), vals[1].clone()); let blk = ctx.block(); let s_handle = unbox_to_i64(blk, &s_box); let runtime_fn = match property { @@ -260,10 +268,8 @@ pub(crate) fn try_lower_map_set_methods( runtime_fn, &[(I64, &s_handle), (DOUBLE, &other_box)], ); - crate::expr::i32_bool_to_nanbox(blk, &i32_v) - }; - temp_root::temp_root_release(ctx, guard); - return Ok(Some(result)); + Ok(Some(crate::expr::i32_bool_to_nanbox(blk, &i32_v))) + }); } _ => {} } @@ -294,25 +300,24 @@ pub(crate) fn try_lower_collection_foreach( if args.len() >= 2 { operands.push(&args[1]); } - let (vals, guard) = temp_root::lower_exprs_rooted(ctx, &operands)?; - let m_box = vals[0].clone(); - let cb_box = vals[1].clone(); - let this_arg = vals - .get(2) - .cloned() - .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - { - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - blk.call_void( - "js_map_foreach", - &[(I64, &m_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - } - temp_root::temp_root_release(ctx, guard); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); + return rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let m_box = vals[0].clone(); + let cb_box = vals[1].clone(); + let this_arg = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + { + let blk = ctx.block(); + let m_handle = unbox_to_i64(blk, &m_box); + blk.call_void( + "js_map_foreach", + &[(I64, &m_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + } + Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))) + }); } if is_set_expr(ctx, object) { // #6970: the callback (a closure allocation) and the optional @@ -322,25 +327,24 @@ pub(crate) fn try_lower_collection_foreach( if args.len() >= 2 { operands.push(&args[1]); } - let (vals, guard) = temp_root::lower_exprs_rooted(ctx, &operands)?; - let s_box = vals[0].clone(); - let cb_box = vals[1].clone(); - let this_arg = vals - .get(2) - .cloned() - .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - { - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - blk.call_void( - "js_set_foreach", - &[(I64, &s_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - } - temp_root::temp_root_release(ctx, guard); - return Ok(Some(double_literal(f64::from_bits( - crate::nanbox::TAG_UNDEFINED, - )))); + return rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let s_box = vals[0].clone(); + let cb_box = vals[1].clone(); + let this_arg = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + { + let blk = ctx.block(); + let s_handle = unbox_to_i64(blk, &s_box); + blk.call_void( + "js_set_foreach", + &[(I64, &s_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + } + Ok(Some(double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + )))) + }); } // URLSearchParams.forEach((value, key, this) => …). The HIR // variant `Expr::UrlSearchParamsForEach` only fires when the @@ -357,23 +361,22 @@ pub(crate) fn try_lower_collection_foreach( if args.len() >= 2 { operands.push(&args[1]); } - let (vals, guard) = temp_root::lower_exprs_rooted(ctx, &operands)?; - let p_box = vals[0].clone(); - let cb_box = vals[1].clone(); - let this_arg = vals - .get(2) - .cloned() - .unwrap_or_else(|| double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); - { - let blk = ctx.block(); - let p_handle = unbox_to_i64(blk, &p_box); - blk.call_void( - "js_url_search_params_for_each", - &[(I64, &p_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], - ); - } - temp_root::temp_root_release(ctx, guard); - return Ok(Some(double_literal(0.0))); + return rooting::with_operands_rooted(ctx, &operands, |ctx, vals| { + let p_box = vals[0].clone(); + let cb_box = vals[1].clone(); + let this_arg = vals.get(2).cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + { + let blk = ctx.block(); + let p_handle = unbox_to_i64(blk, &p_box); + blk.call_void( + "js_url_search_params_for_each", + &[(I64, &p_handle), (DOUBLE, &cb_box), (DOUBLE, &this_arg)], + ); + } + Ok(Some(double_literal(0.0))) + }); } } Ok(None) diff --git a/crates/perry-codegen/src/rooting.rs b/crates/perry-codegen/src/rooting.rs index 4cce03df74..1767785d06 100644 --- a/crates/perry-codegen/src/rooting.rs +++ b/crates/perry-codegen/src/rooting.rs @@ -326,7 +326,34 @@ use anyhow::Result; use perry_hir::Expr; use crate::expr::FnCtx; -use crate::types::LlvmType; +use crate::types::{LlvmType, DOUBLE, I64}; + +/// How a rooted slot's contents are read back out. +/// +/// A temp-root slot is representation-agnostic — `temp_root_push_double` +/// bitcasts to `i64` and pushes the same word `temp_root_push_i64` does — so +/// the *reader* decides whether the word is a raw heap pointer or a NaN-boxed +/// JS value. Before this was carried on the slot, that decision lived at each +/// call site as a choice between `temp_root_get_i64` and `temp_root_get_double`, +/// and reading a boxed slot as a pointer is a silent miscompile rather than a +/// type error. Recording it at the push makes the pair impossible to mismatch. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum Repr { + /// A raw heap pointer in an `i64` — what a `js_*` helper returning `I64` + /// yields, and what `unbox_to_i64` derives. + Ptr, + /// A NaN-boxed JS value in a `double` — the ordinary operand currency. + Boxed, +} + +impl Repr { + fn llvm_ty(self) -> LlvmType { + match self { + Repr::Ptr => I64, + Repr::Boxed => DOUBLE, + } + } +} /// A slot the collector knows about, holding a GC-managed pointer for the /// duration of a lowering. @@ -341,6 +368,7 @@ use crate::types::LlvmType; #[derive(Debug)] pub(crate) struct RootedSlot { idx: String, + repr: Repr, } impl RootedSlot { @@ -362,9 +390,11 @@ impl RootedSlot { /// asserting the collector does not manage — an `i32`, a length, a literal, or /// a value another combinator has already re-read below the last collection /// point. +#[derive(Clone, Copy)] pub(crate) enum Arg<'a> { - /// Re-read this slot immediately before the call. The register never - /// exists as a value the caller can hold. + /// Re-read this slot immediately before the call, in the representation the + /// slot was pushed with. The register never exists as a value the caller + /// can hold. Root(&'a RootedSlot), /// A value the collector does not manage in this window. Plain(LlvmType, &'a str), @@ -378,15 +408,21 @@ pub(crate) enum Arg<'a> { fn materialize<'a>(ctx: &mut FnCtx<'_>, args: &'a [Arg<'a>]) -> Vec<(LlvmType, String)> { args.iter() .map(|arg| match arg { - Arg::Root(slot) => ( - crate::types::I64, - crate::expr::temp_root::temp_root_get_i64(ctx, &slot.idx), - ), + Arg::Root(slot) => (slot.repr.llvm_ty(), read_slot(ctx, slot)), Arg::Plain(ty, reg) => (*ty, (*reg).to_string()), }) .collect() } +/// The one place a rooted slot becomes a register, and it is private: every +/// public path out of it fuses the read to the emission that consumes it. +fn read_slot(ctx: &mut FnCtx<'_>, slot: &RootedSlot) -> String { + match slot.repr { + Repr::Ptr => crate::expr::temp_root::temp_root_get_i64(ctx, &slot.idx), + Repr::Boxed => crate::expr::temp_root::temp_root_get_double(ctx, &slot.idx), + } +} + fn borrow_args(args: &[(LlvmType, String)]) -> Vec<(LlvmType, &str)> { args.iter().map(|(ty, reg)| (*ty, reg.as_str())).collect() } @@ -408,7 +444,10 @@ pub(crate) fn call_rooted( .block() .call(ret_ty, callee, &borrow_args(&materialized)); let idx = crate::expr::temp_root::temp_root_push_i64(ctx, ®); - RootedSlot { idx } + RootedSlot { + idx, + repr: Repr::Ptr, + } } // A `root_i64(ctx, reg) -> RootedSlot` combinator -- "root a raw pointer some @@ -436,6 +475,15 @@ pub(crate) fn call_with_roots( .call(ret_ty, callee, &borrow_args(&materialized)) } +/// [`call_with_roots`] for a `void` helper — a mutator such as +/// `js_object_set_field_by_name` or `js_map_set`, which is most of the +/// accumulator surface. Returning nothing is the point: there is no register +/// for a caller to hold, so this form cannot reopen the window at all. +pub(crate) fn call_void_with_roots(ctx: &mut FnCtx<'_>, callee: &str, args: &[Arg<'_>]) { + let materialized = materialize(ctx, args); + ctx.block().call_void(callee, &borrow_args(&materialized)); +} + /// Lower `exprs` with every already-evaluated operand rooted across the /// evaluation of the ones that follow, run `body` over the re-read values, and /// release the group **on every path out**. @@ -486,10 +534,58 @@ pub(crate) fn with_operands_rooted_across<'f, T, R>( across_exprs: &[&Expr], across: impl FnOnce(&mut FnCtx<'f>) -> Result, body: impl FnOnce(&mut FnCtx<'f>, &[String], T) -> Result, +) -> Result { + let across_collects = + crate::expr::temp_root::any_may_trigger_gc(ctx, across_exprs.iter().copied()); + with_operands_rooted_window(ctx, exprs, across_collects, across, body) +} + +/// [`with_operands_rooted_across`] for a step that is an **emitted runtime +/// call** rather than a lowered expression. +/// +/// The two forms differ only in who answers "does this window collect?", and +/// for a call there is nothing for `any_may_trigger_gc` to read — the step is +/// not an `Expr`. `re.test(s)` is the shape: the arm unconditionally emits +/// `js_jsvalue_to_string_coerce`, which allocates and, on an object argument, +/// dispatches a user `toString`. Deriving the window from the `string` operand +/// answers *false* for a plain local and drops the root, which is #7154 at that +/// exact site (`js_regexp_test` dereferencing a from-space `RegExpHeader`). +/// +/// So this takes the answer rather than deriving it, and the precedent is +/// deliberate: `temp_root::guard_store_operand_across` already had to, for the +/// same reason and since #7201. What stays centralised is the part that can +/// drift — `operand_protection` still decides *how* each operand is protected +/// (root / re-derive / reuse). Only the window's extent is stated here, and it +/// is stated as "yes", the conservative answer. +/// +/// Use it only when the emitted step can re-enter user code or enumerate an +/// arbitrary object's own properties. For a helper that merely allocates, the +/// project's position (#7198) is that it cannot *initiate* a moving collection, +/// so a root there would be pure cost. +pub(crate) fn with_operands_rooted_across_call<'f, T, R>( + ctx: &mut FnCtx<'f>, + exprs: &[&Expr], + across: impl FnOnce(&mut FnCtx<'f>) -> Result, + body: impl FnOnce(&mut FnCtx<'f>, &[String], T) -> Result, +) -> Result { + with_operands_rooted_window(ctx, exprs, true, across, body) +} + +/// The one implementation behind all three `with_operands_rooted*` forms. +/// +/// They differ only in how `across_collects` is obtained; keeping the lowering, +/// the re-read point and the release in a single body is what stops the family +/// from growing three subtly different orderings (the drift that produced +/// #7114). +fn with_operands_rooted_window<'f, T, R>( + ctx: &mut FnCtx<'f>, + exprs: &[&Expr], + across_collects: bool, + across: impl FnOnce(&mut FnCtx<'f>) -> Result, + body: impl FnOnce(&mut FnCtx<'f>, &[String], T) -> Result, ) -> Result { use crate::expr::temp_root::{any_may_trigger_gc, root_operands_begin}; - let across_collects = any_may_trigger_gc(ctx, across_exprs.iter().copied()); let mut group = root_operands_begin(exprs.len()); let out = (|| { // Incremental, one operand at a time: each is rooted BEFORE the next is @@ -513,6 +609,155 @@ pub(crate) fn with_operands_rooted_across<'f, T, R>( out } +/// Does evaluating any of these expressions reach a collection point? +/// +/// Re-exported so a migrated module can answer the question a caller-supplied +/// `protect` flag needs (`{ ...a, k: f() }`, `Math.min(f(), g(), h())`) without +/// naming `expr::temp_root`. It is the same predicate `operand_protection` +/// consults, not a second copy. +pub(crate) fn any_operand_may_collect<'a>( + ctx: &FnCtx<'_>, + exprs: impl IntoIterator, +) -> bool { + crate::expr::temp_root::any_may_trigger_gc(ctx, exprs) +} + +/// A GC-managed value that generated code keeps **updating** while it lowers +/// further expressions: an object literal's half-built handle, `Object.assign`'s +/// threaded target, `Math.min(...)`'s growing argument array. +/// +/// It is the operand group's mirror image. An operand is lowered once and read +/// once; an accumulator is written, read, rewritten and read again, with +/// arbitrary user code lowered between the writes. #7154's `ObjectSpread` bug is +/// the canonical failure: the half-built object sat in a raw SSA register while +/// 269 spread values were lowered, an evacuating minor relocated it, and every +/// later field store wrote into abandoned from-space memory — silently, because +/// the fields simply did not appear on the copy the program kept. +/// +/// The invariant it enforces is the one a raw handle cannot: **the accumulator +/// never exists as a register the lowering holds across an emission.** Every +/// consuming call re-reads it as part of being emitted ([`RootedAcc::call`], +/// [`RootedAcc::call_void`]), and a helper that returns a fresh address +/// publishes it straight back into the slot ([`RootedAcc::advance`]) rather than +/// handing it out. The single point where a register does escape is the final +/// read, and [`with_rooted_accumulator`]'s `finish` closure owns it: it runs +/// below the last collection point and above the release, so there is no +/// program in which the escaped register outlives its root. +pub(crate) struct RootedAcc { + slot: Option, + repr: Repr, + /// The register as first produced. The answer when `protect` was false, in + /// which case nothing is emitted and the IR matches the un-rooted form byte + /// for byte. + value: String, +} + +impl RootedAcc { + /// The accumulator as a call argument. + fn as_arg(&self) -> Arg<'_> { + match &self.slot { + Some(slot) => Arg::Root(slot), + None => Arg::Plain(self.repr.llvm_ty(), &self.value), + } + } + + fn args_with_self<'a>(&'a self, rest: &[Arg<'a>]) -> Vec> { + let mut all = Vec::with_capacity(rest.len() + 1); + all.push(self.as_arg()); + all.extend_from_slice(rest); + all + } + + /// Emit `callee(, ...rest)` and return its result register. + /// + /// The accumulator is argument **0**, positionally and deliberately. It is + /// argument 0 at every site this exists for — `js_object_set_field_by_name`, + /// `js_object_copy_own_fields`, `js_array_push_f64`, `js_object_assign_one` + /// — and keeping it positional is what lets the re-read be fused to the + /// emission instead of handed back to the caller as a register to place. + pub(crate) fn call( + &self, + ctx: &mut FnCtx<'_>, + ret_ty: LlvmType, + callee: &str, + rest: &[Arg<'_>], + ) -> String { + call_with_roots(ctx, ret_ty, callee, &self.args_with_self(rest)) + } + + /// [`RootedAcc::call`] for a `void` helper. + pub(crate) fn call_void(&self, ctx: &mut FnCtx<'_>, callee: &str, rest: &[Arg<'_>]) { + call_void_with_roots(ctx, callee, &self.args_with_self(rest)); + } + + /// Emit `callee(, ...rest)` and make its result the new + /// accumulator value. + /// + /// For helpers that may **relocate** what they are handed and return the + /// current address: `js_array_push_f64` reallocs the element storage, + /// `js_object_assign_one` returns the post-collection target. Keeping the + /// pre-call register instead is how `Object.assign(t, a, b)` threaded a + /// stale `t` into `b`'s link before #7200. + pub(crate) fn advance(&mut self, ctx: &mut FnCtx<'_>, callee: &str, rest: &[Arg<'_>]) { + let next = self.call(ctx, self.repr.llvm_ty(), callee, rest); + match (&self.slot, self.repr) { + (Some(slot), Repr::Ptr) => { + crate::expr::temp_root::temp_root_set_i64(ctx, &slot.idx, &next) + } + (Some(slot), Repr::Boxed) => { + crate::expr::temp_root::temp_root_set_double(ctx, &slot.idx, &next) + } + (None, _) => self.value = next, + } + } +} + +/// Root a mutable accumulator for the duration of `build`, then hand its final +/// value to `finish` and release it. +/// +/// `protect == false` emits nothing at all — no push, no re-reads, no truncate — +/// so a site whose initializers provably cannot collect keeps the IR it had +/// before it was rooted at all. +/// +/// The split into two closures is what makes the release unmissable while still +/// letting the final value be *used*. `build` may not hold the accumulator in a +/// register across anything; `finish` receives one, but it runs below the last +/// collection point and above the release, and it is the only place the value +/// escapes. Both paths — `build`'s `?` and `finish`'s — release. +pub(crate) fn with_rooted_accumulator<'f, R>( + ctx: &mut FnCtx<'f>, + repr: Repr, + initial: &str, + protect: bool, + build: impl FnOnce(&mut FnCtx<'f>, &mut RootedAcc) -> Result<()>, + finish: impl FnOnce(&mut FnCtx<'f>, &str) -> Result, +) -> Result { + let slot = protect.then(|| { + let idx = match repr { + Repr::Ptr => crate::expr::temp_root::temp_root_push_i64(ctx, initial), + Repr::Boxed => crate::expr::temp_root::temp_root_push_double(ctx, initial), + }; + RootedSlot { idx, repr } + }); + let mut acc = RootedAcc { + slot, + repr, + value: initial.to_string(), + }; + let out = (|| { + build(ctx, &mut acc)?; + let final_value = match &acc.slot { + Some(slot) => read_slot(ctx, slot), + None => acc.value.clone(), + }; + finish(ctx, &final_value) + })(); + if let Some(slot) = acc.slot { + slot.release(ctx); + } + out +} + // --------------------------------------------------------------------------- // The per-module migration ledger (RFC step 3). // @@ -543,15 +788,22 @@ pub(crate) fn with_operands_rooted_across<'f, T, R>( /// No boundary is outstanding today. /// /// **Listing a module that never used the escape hatch passes vacuously.** That -/// is true of every module migrated so far except `expr/url_main.rs`: they named -/// no `temp_root` symbol before the migration, so -/// `migrated_modules_do_not_reach_past_the_rooting_api` went green the instant -/// the line was added. The listing only means something if the slice ALSO ran -/// the sabotage arm — inject a real, compiling `temp_root_push_*` / -/// `temp_root_truncate` pair into the migrated module, confirm the ledger test -/// goes red and names the lines, then revert. Slices 1a and 1b both did, and -/// recorded it in their PRs; a slice that skips it is adding a line that asserts +/// was true of both slice-1 modules: they named no `temp_root` symbol before +/// the migration, so `migrated_modules_do_not_reach_past_the_rooting_api` went +/// green the instant the line was added. The listing only means something if the +/// slice ALSO ran the sabotage arm — inject a real, compiling `temp_root_push_*` +/// / `temp_root_truncate` pair into the migrated module, confirm the ledger test +/// goes red and names the lines, then revert. Every slice so far has, and +/// recorded it in its PR; a slice that skips it is adding a line that asserts /// nothing. +/// +/// Slice 2's three modules are the first since the template where the listing is +/// **not** vacuous: all three named `expr::temp_root` before the migration +/// (`lower_exprs_rooted`, `guard_store_operand_across`, `rooted_handle_*`, +/// `temp_root_{push,get,set}_double`), so the ledger line is load-bearing on the +/// committed source and not only under sabotage. The sabotage arm was still run +/// per module — the assert stops at the first offender, so one run cannot speak +/// for three. #[cfg(test)] const MIGRATED_MODULES: &[(&str, &str)] = &[ ( @@ -570,6 +822,18 @@ const MIGRATED_MODULES: &[(&str, &str)] = &[ "crates/perry-codegen/src/expr/array_methods.rs", include_str!("expr/array_methods.rs"), ), + ( + "crates/perry-codegen/src/expr/instance_misc1.rs", + include_str!("expr/instance_misc1.rs"), + ), + ( + "crates/perry-codegen/src/expr/logical_collections.rs", + include_str!("expr/logical_collections.rs"), + ), + ( + "crates/perry-codegen/src/lower_call/property_get/map_set.rs", + include_str!("lower_call/property_get/map_set.rs"), + ), ]; /// Lines in `src` that reach past [`crate::rooting`] into the raw rooting API.