Merge train: #9795, #9844, #9845, #9853, #9857, #9860, #9862, #9864, #9865, #9868 (+ #9869) - #9883
Merged
Conversation
…9847) `lower_assign` handles `X = <native module>.<method>(...)`. Its class-name match ends in a catch-all, and it registered the result through `push_module_native_instance` — keyed on the identifier TEXT and scoped to the whole module, never truncated. So any method on any recognised native module, assigned to a variable, claimed that spelling for the rest of the program. Minified bundles reuse single letters everywhere, which makes the collision the normal case rather than a corner. `cli_2.1.112.js` (claude-code) compiles as one module, imports `child_process` as `fA1`, contains `let O; try { O = fA1.spawn(z.file, z.args, z.options) }` inside one helper, and binds the name `O` 5,381 times. Every later `O` was typed `child_process::Instance` — including the `for (let { segment: O } of ...)` binding in `string-width` that holds a grapheme STRING, whose `O.codePointAt(0)` lowered as `NativeMethodCall { module: "child_process", class_name: Some("Instance") }` and reached the right answer only because native-instance dispatch falls through to a generic path on a string receiver — once per grapheme, in the loop that dominates a claude-code turn. The tag now keys on the `LocalId` the assignment target resolves to. This is the same correction #7775 already made in this file for `new Proxy` bindings (`proxy_locals` -> `proxy_local_ids`) after a proxy bound to `a` in one function made every other function's `a.prop` lower to `js_proxy_get`. Scope-truncating the assignment path would NOT have worked: the module-wide table exists for the cross-function case — a module-level `let client;` assigned inside `init()` and read inside `handler()` — and truncating at scope exit would have dropped exactly that. Keyed on the binding it reaches just as far, because both functions resolve `client` to the same `LocalId`, while a same-named binding in another scope is simply a different binding. `lookup_native_instance` gains an id-keyed arm ahead of the module-wide one, short-circuited when the module has no bare-assignment handle at all (the arm sits on the miss path of every identifier property access). A target that resolves to no local — a bare global — still registers and resolves by name; that is the same hole #7775 documented, kept for the same reason and strictly no worse than the previous behaviour, which used it for every assignment. Nothing pattern-matches `child_process` or `codePointAt`: the mislowered call disappears because the tag never reaches that binding. (cherry picked from commit 47b5e7c)
(cherry picked from commit 6247d28)
(cherry picked from commit 9a7fc55)
`set_last_index_throwing` asks `get_property_attrs(re, "lastIndex")` on every global or sticky `test()`/`exec()`, because a user may make `lastIndex` non-writable and the spec's `Set(R, "lastIndex", n, true)` must then throw. That question is meant to be answered by #6759 phase C2's per-object meta summary without touching the tables — but `may_have_descriptor_entry` reached the summary through `meta_capable_object`, which answers only for `GC_TYPE_OBJECT`. A `RegExp` is its own cell type, so the filter returned the conservative "maybe" for every RegExp receiver and the probe ran: `key.to_string()` — a `String` allocation — plus a SipHash of `(usize, String)`, on roughly 96,500 global `test()` calls per 400-character claude-code reply. The capability was already there and simply unwired. #6759 phase 1 unified the metadata edge behind `cell_meta_slot`, which answers for Object, Error, Map, Set, RegExp, Promise and Date; `RegExpHeader::meta` is traced by `GcLayoutSlotKind::RegExpFields` and moves with its header. So this adds no state and no new invariant: it asks the narrower question the summary actually needs (`descriptor_summary_meta`) instead of the `ObjectHeader`-shaped one the other callers of `meta_capable_object` need, and every cell type with a meta edge benefits, not only RegExp. The three-way answer is the contract. `None` means the cell type has no meta edge and the caller must stay conservative; `Some(null)` means the edge exists and no record was ever installed, which PROVES absence; `Some(meta)` means read the summary words. Collapsing the first two would turn a conservative "maybe" into a false "no" for the types that still lack an edge. Install and probe move together, which is the safety argument: all five descriptor-summary sites now share one predicate, so an owner whose install set the key bit is always found. Every insert into `property_descriptors` / `accessor_descriptors` routes through `set_property_attrs` / `set_accessor_descriptor` and therefore through `note_meta_descriptor_key`; the touches outside this module are all removals, which can only make a probe more conservative. `js_regexp_new` writes `meta = null` on every construction, so a fresh header at a recycled address cannot inherit a dead tenant's bits. Counters, diagnostic only and armed with `PERRY_REGEX_DIAG`: `desc_regexp_probes` (RegExp receivers this filter sees) and `desc_regexp_meta_negative` (those it now proves absent). The second was 0 by construction before this change. Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp (cherry picked from commit 845bf69)
(cherry picked from commit db1401f)
(cherry picked from commit da2a844)
(cherry picked from commit fcbb0e5)
VisitedLevels stored each walked level as a plain NaN-boxed f64 and dereferenced it after the walk had crossed an allocating call and a possible Proxy getPrototypeOf trap, so a collection in that window left it holding a stale pointer. Store RuntimeHandles, which the collector rewrites; RuntimeHandle is Copy, so the inline arm still does not allocate.
`gc_check_trigger` runs on every `gc_malloc`, and `gc_budgeted_due_trigger` resolved eleven raw `thread_local!` declarations one `_tlv_get_addr` call at a time. Measured with `sample` on the compiled claude-code TUI streaming a 3300-char reply (14,578 active main-thread samples, callers resolved by an explicit ancestor walk): `_tlv_get_addr` was 380 main-thread leaf samples, 71 of them with `gc_budgeted_due_trigger` as the immediate caller, 36 in `old_page_account_dirty_slots`, 31 in `scan_dirty_object_slots`, 27 in `gc_malloc_header_is_tracked`. Sixty-seven declarations move to `crate::perry_thread_local!`. Why they were still cold is a measurement bug in the gate, not an oversight: `scripts/check_thread_locals.py` ratchets on raw `thread_local!` BLOCKS per file, and a block holds any number of declarations — so `gc/policy.rs` counted as 6 while declaring 28, and adding a `static` to a recorded block passed silently. In the same unit as the hot side, main was 318 hot against 339 cold declarations. The gate now ratchets on declarations (385/272) and `--self-test` gained the direction that catches it. `ARENA_TOTAL_BYTES`, `BLOCK_POOL` and `BLOCK_POOL_BYTES` stay raw and say so: they are read from `Arena::new`, which runs as `tls_hot::fill`'s first provider, so a `HotKey` there re-enters `fill` — which has not yet written the `temp_roots` field it gates on — and re-runs `ARENA`'s initializer without bound. Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m (cherry picked from commit 2e99865)
…TABLE
WIP — committed to preserve state while the lane is paused for box load
(load 298, 19.8/21.5 GB swap). NOT measured on the rig; do not land as is.
Replaces the 4-way round-robin page-generation cache with a direct-indexed
table over the arena's 1 MiB address classes. `PageGenerationMap` stays
authoritative: every miss falls through to it exactly as before, so this is a
cache replacement, not a map replacement. The 4-way set is retained in the same
binary behind `PERRY_GC_PAGE_CLASS_TABLE=0` as the positive control.
STATE OF THIS COMMIT
Applied, complete:
* the table itself (`lookup`/`insert`/`rebase_to_cover`/`invalidate`), base
taken from the first insert, epoch-stamped entries, O(1) whole-table
invalidation;
* sizing: `INITIAL_SPAN = 4096`, `BASE_SLACK = SPAN / 2`. The draft's
`S = 1024` was mis-tuned — with base `first_key - S` the span covered is
`min(S + 1, N - S)`, maximised at `S = N / 2`, so `S = 1024` covered 1,025
classes against a measured span of 1,021 while `S = N / 2` covers 2,048 for
the same 160 KB. A `const` assert now fails the build for any pairing
covering less than twice the measured span; the old pairing fails it;
* out-of-span coverage: an insert outside the table rebases it up to a
16,384-class cap, and past the cap the key is left uncached and falls
through to the map — never silently mis-indexed;
* the arm is a plain `u8` field in the set's first cache line, not the
`OnceLock` env read the draft had on the lookup path. That path runs ~440 M
times per turn and an acquire load on each would have been paid by BOTH
arms of the A/B while still being charged against main;
* `#[repr(C, align(64))]` so "the hot fields share one cache line" is true
rather than likely;
* counters (`hits`/`misses`/`inserts`/`oos`/`rebases`/`refused`) and the
`[gc-page-class]` line, emitted per copying minor under `PERRY_GC_DIAG`
because the rig SIGKILLs the process. `oos` is on the miss path only and is
what distinguishes a residual miss that is an unregistered address from one
that is the table failing.
Verified:
* all four tests pass on the pristine tree (`cargo test -p perry-runtime
--lib page_class_table`, dev profile, 4 passed);
* the four sabotages each fail on their own named assertion — base-from-first-
registration, out-of-span handling, range containment, and invalidation.
Two of them produce a literal misclassified pointer (`left: Old, right:
Nursery` and `left: Nursery, right: Old`), which is the failure mode this
structure has to be proof against;
* every `PAGE_GENERATIONS` mutation site was enumerated (three, plus one
read-only census walk) and each ends with an unconditional
`invalidate_generation_cache()`. The table holds ~2,000 entries where the
4-way set held 4, so a missing invalidation the old structure survived by
luck would be a live misclassification here.
NOT done — this is what the lane owes:
* the rig. The relink was killed mid-`cargo build` at the coordinator's
pause, so there is no candidate binary and NO number in this commit has
been measured on cc;
* `cargo test -p perry-runtime --release -- gc:: arena::`;
* `cargo fmt` (the `arena/mod.rs` re-export is not in sorted order) and
clippy;
* a changelog fragment.
Pre-registered falsifiers, written before any measurement, are in
`secret-tests/cc-perf-campaign/RESULT_page_class_table.md`. The headline is
that the spec's "miss rate below 2 %" bar is arithmetically unreachable: 22.3 %
of today's misses are on addresses in no registered block, which the map cannot
answer either, so nothing is cached for them in either arm. The derived floor
is ~4.5 %, and the decision turns on misses to REGISTERED classes going to ~0.
(cherry picked from commit 165aa78)
…xport Formatting and documentation only; no behaviour change. `cargo fmt` on the touched files, restricted to the lines this branch added. Note for whoever runs the fmt gate: `arena/mod.rs` is ALREADY not rustfmt-clean on main at an unrelated `#[cfg(test)]` re-export, and reformatting it would have put that pre-existing churn in this diff, so it is deliberately left alone. (cherry picked from commit 93ffee5)
Issue #9831 measured the ArenaBytes arm firing 51 times in one 66-delta claude-code reply, each collection freeing a median 131 KB, while the adaptive step sat saturated at 1 GiB. The issue located the discarded backoff in the arm's own re-arm arithmetic; correcting that (the issue's refuted branch) bought -10.8 % CPU for +22 % settled footprint and was rightly rejected. The arm's re-arm is not what re-fires it. Between two consecutive firings the arena grows a few hundred KB, against a trigger armed 16 MB (and below the ceiling, up to 128 MB) above the post-collection total. What pulls the trigger back down is the tiny-parse pressure guard: after every `JSON.parse` that grew the arena by <= 1 MB, `gc_bump_malloc_trigger` (and `gc_schedule_parse_boundary_collection_ if_pressure`, and the boundary collector they arm) tests the absolute `arena_in_use_bytes() >= 48 MB` and, if so, sets the trigger to "now". That threshold is a quantity no collection can lower below the live set, so on a program whose live set never drops under it every small parse -- one per SSE delta -- forced a minor at the next safepoint. The step those minors doubled was consulted by nothing. The guard now also requires the arena to have grown, since the last collection of any kind ended, by a headroom priced from the step: the step rescaled so that its power-on value (128 MB, the ceiling) buys the 16 MB floor, and each doubling the arm's ceiling clamp discards buys the guard one more doubling, bounded by the same ceiling. A productive collection halves the step and the guard keeps the cadence it always had; an unproductive one earns it room. The boundary collector re-prices a pending request so a collection that already satisfied it is not followed by a second one. Measured on the compiled claude-code TUI (cli_2.1.112.js, Linux, same perry binary, runtime-only A/B, 7 interleaved rounds, 3300-char streamed reply, chunk 50): turn CPU base 30.2-41.5 s (mean 35.1) fix 27.8-29.2 s (mean 28.6) post-turn RSS base 754-1057 MB (mean 803) fix 733-855 MB (mean 786) post-idle RSS base 527-1073 MB (mean 736) fix 517-843 MB (mean 722) peak RSS 1964-2062 MB both arms The fix wins CPU in every pair (-8 % to -30 %); footprint is flat within the base's own spread. The base arm is bimodal in both, which is what an absolute in-use threshold does. PERRY_GC_DIAG on one reply: copying minors 104 -> 84 (ArenaBytes 41 -> 13), old-gen fulls 19 -> 7, and the guard forced exactly one collection, after a genuine 16 MB of growth (`[gc-tiny-parse]` is the new witness line). test_memory_json_churn -- the guard's motivating shape -- is byte-identical in output and RSS in all four GC modes; 48/48 test_gap_gc_* and 8/8 test_gap_json_* pass. The arm's own arithmetic is left as it was and now says why. Claude-Session: https://claude.ai/code/session_015kqVkH6rHzfvXskGAj3tRv (cherry picked from commit 0d92005)
…name Two build breaks the merge produced: - `old_gen_in_use_bytes_slot_index` was re-exported twice from `arena/mod.rs` (E0252) after #9853 and #9827 both added the line. - `VisitedLevels` gained a lifetime parameter when its levels became RuntimeHandles, and an associated `Self::INLINE` is not permitted in the array length of a generic struct, so it became the free const `VISITED_INLINE`. enumeration_tests.rs still named the old path.
…ctions A declined idle compaction is currently a terminal state. The reducer's activity gate wants `2^backoff` collections it did not start, and `external_collections()` subtracts only its own — so a COMPACTION is what registers as external. When the compactor's residue gate declines, no compaction runs, nothing registers, `since_attempt` never reaches 1, and the reducer never runs again. The decision removes the only event that could revisit it. Measured on the claude-code TUI, 400-char turn then 120 s idle, quiet host, both rounds of each arm: A settles 757/759 -> 512/527 MB; R ends the turn 19 MB BETTER at 738/742 and finishes at 748/748 — 221 MB worse. R's residue ratio is 23.68/23.67 % against a 25 % gate and starts zero compactions; A is at 25.94/25.95 % and starts two. Within-arm spread is 0.01-0.02 points, so this is a stable operating point just under a threshold, not a coin-flip. The largest piece of the loss is downstream: A right-sizes arena capacity 182.45 -> 81.79 MB across three observations, R holds 168.82 MB on one. This adds `StartReason::IdleElapsed`, extending the exemption that already sits twelve lines above it for the identical deadlock — `ArenaRightSize` bypasses the same gate because arena blocks need a second full observation an idle mutator will never produce (#9709). A requirement denominated in mutator collections cannot be met by a heap whose mutator is idle, which is exactly when the reducer is wanted. The constant was not the fix, and that is measured rather than asserted: the same R binary in a 5 s window DID clear the residue gate at 25.81 %, compacted, and released 0 (`kept_promise=false`). A's own second compaction releases 0 at 54.6 % residue. Half of A's compactions in that capture released nothing, aborting ~4x earlier on what looks like a pause budget. Lowering 25 -> 23 % would have bought a compaction that releases nothing and a `backoff_shift` bump. Anti-spin needs no new rule: the wait is `IDLE_RECLAIM_REARM_MS << backoff_shift`, the SAME shift that prices the activity arm, so an unproductive full doubles it — 15 s, 30 s, 60 s, 120 s, 240 s — and the arm is DISARMED at `IDLE_RECLAIM_MAX_BACKOFF_SHIFT` rather than merely slowed, so a heap with nothing to give is asked five bounded times and then not again until real activity resets the shift. A productive full resets it, so a heap still giving memory back keeps being asked every 15 s. Tests, both sabotage-proved and each failing on its own named assertion: `a_parked_heap_is_re_armed_by_elapsed_idle_alone` (no external collection anywhere in the test; asserts the REASON via a counter, not the attempt count) and `an_unproductive_elapsed_streak_doubles_the_wait_and_then_disarms`. Removing the arm fails the first; removing the backoff scaling fails "must not re-arm before the doubled wait"; removing the disarm fails "at the maximum shift the elapsed arm is disarmed". `cargo test -p perry-runtime --lib -- gc:: arena::` is green at 1,143 passed / 0 failed. NOT addressed here, and measured rather than assumed: after R's single reclaim, `[gc-general-reclaim] examined=66 released=0 has_live=39 aging=22` — 39 of 66 arena blocks hold a live object, against 3 of 65 in A. Only an evacuation can consolidate those, and whether an idle young evacuation is also needed is a separate change. Refs #9831. (cherry picked from commit 0846d67)
…l origins, allocation-site sampling Three instruments for the cc-perf campaign, all inert unless asked for. `PERRY_GC_DIAG=1` gains the lines that say WHY the collector ran: `[gc-trigger]` (every predicate input at each decision site), `[gc-full]` (the arm behind each synchronous full mark-sweep, counted per site), `[gc-budgeted] start/done` (steps, per-phase step time, root-scan share), `[gc-charge]` (mutator-assist / synchronous-full time per calling site, resolved to JS display names) and `[gc-survival]` (per copying minor, the root that first reached each surviving byte — shadow stack, native stack map, named scanner, remembered set by old-parent type — with transitive reach charged to the originating root through a parallel worklist origin vector). `PERRY_ALLOC_SITE_SAMPLE=<bytes>` samples the arena allocation sites byte- proportionally across the runtime allocators AND the codegen inline bump path (the mirrored inline block limit is capped at one interval while sampling, so the fast path returns to the runtime once per interval). The survival test is sabotage-checked: disabling the drain propagation charges the 40 elements to `worklist_drain` and the test fails on that row. The knob's OFF state and magnitude parse are pinned next to the other GC knobs. `gc_diag_enabled()` gets the per-thread test override the census already has, so the diag paths are testable without touching the process environment. Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2 (cherry picked from commit 157afd9)
…es, engine prototype switch Rebased onto main after #9764 landed as ddbe0b1; the site cache, header-authoritative lookups and find-only global `test` are main's now and are gone from here. The program-cache coherence fix moved to #9801. Three changes remain. * The capture-group cliff. `repeat_matcher::capture_layout` takes a pattern off the linear engine when ECMA-262's RepeatMatcher capture semantics are observable — a capture directly under a quantifier, or a capture inside a negative lookaround. That routing is a correctness requirement, but the engine it routes to is a classical backtracker with no step budget, so adding parentheses fell from linear time to exponential (`/^(a+)+$/.test("a"*28 + "!")`: 16,522 ms; node 4,798 ms). 6.3 % of 4,463 real literals take that route. Both engines accept the same LANGUAGE and differ only in capture ASSIGNMENT, so `linear_rules_out_match` asks the linear program first and a subject it rules out — which is what every ReDoS input is — never reaches the backtracker. This removes the reachable exponential case; it does not bound the worst case (that needs the step budget open upstream as ridiculousfish/regress#177). * Allocation-free cache probes. The three compiled-program caches were `HashMap<(String, String), _>`, so every probe allocated two Strings and copied the pattern text, once per RegExp OBJECT. `ProgramKey = (Arc<str>, Arc<str>)` makes a probe two refcount increments; the remaining materialisations are cold (`RegExp.prototype.compile`, the syntax-error fallback). * `PERRY_REGEX_ENGINE=regress`, off by default: routes every pattern through the ECMAScript backtracker and installs a shared never-match placeholder as the standard program, so the tier-0 engine architecture can be measured in a real binary. Not a supported configuration — the backtracker has no budget. Tests: quantified_capture_pattern_does_not_backtrack_on_a_non_matching_subject, plus the `capture_layout` predicate assertions rewritten around the `(layout, needed)` pair. Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2 (cherry picked from commit 89be3b1)
…c arm `js_regexp_new` allocated every `RegExpHeader` with `gc_malloc`. On the claude-code TUI that is 199,873 of 199,926 malloc-tracked GC allocations per 400-character reply — 100.0 % of the malloc arm — at 80 bytes each, 99.2 % of them freed, with the registry swinging 101,929 -> 1,689 across one minor (`PERRY_GC_TRACE`). Each one costs a mimalloc allocation, a `MALLOC_STATE` push, a malloc-registry `PtrHashSet` insert that rehashes as it grows, and at death a sweep visit and a free. `GC_TYPE_REGEXP` has been `ArenaOrMalloc` and movable all along: the move hook rekeys `REGEX_POINTERS` / `REGEX_SOURCE_TABLE` / the expando owner, the layout kind traces `pattern_ptr` / `flags_ptr` / `meta`, and `test_movable_regexp_evacuation_migrates_all_address_owned_state` has exercised the arena arm through a test-only allocator. What blocked production was young death: the copied minor's from-space flip runs no per-object finalize hooks, so a nursery header dying young would leak its `Arc` programs and registry entries. Handled now the way `Map`/`Set`/`Error` handle theirs: * `finalize_dead_copied_minor_from_space_regexps` after a copied minor, * `collect_dead_registered_regexps_post_trace` at sweep entry for the non-copying cycle kinds, * the existing `gc_type_finalize_unmarked_payload` for a tenured header. Deadness reuses the audited `owner_is_dead_copied_minor_from_space` predicate (now exposed per-type), which requires `GC_FLAG_ARENA` set and `MARKED|FORWARDED` clear — so an evacuated header and a malloc'd one are both skipped. Every regex program cache keys on pattern/flags CONTENT, not on the header address, so nothing else needs rekeying. This changes the collection schedule, deliberately: the `MallocCount` trigger loses essentially all of its input while ~16 MB a reply moves into the nursery. Schedule numbers are reported with the change, not assumed. Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m (cherry picked from commit 85bedc3)
(cherry picked from commit 5bba53f)
(cherry picked from commit 87d6e78)
…he LIFO handle stack Four gate failures the assembled tree produced, and the rooting bug the suite caught: - The runtime handle stack is strictly LIFO (`Drop` truncates to the scope's base), so rooting into an OUTER scope while an inner one is live has the inner scope's drop discard the handle. #9869's `visited.push(&scope, ..)` sat inside #9864's per-level scope and hit "runtime handle used after its scope was dropped". The per-level scope now closes before the push. Caught by gc::tests::rooted_for_in::for_in_grown_result_and_receiver_survive_prototype_collection. - shape_descriptor_census asserted `gc_malloc(.. GC_TYPE_REGEXP)` at `js_regexp_new`; #9845 deliberately moves that birth to the nursery, so the assertion now accepts either allocator. What it checks is unchanged and is the point: RegExp is born with its OWN GcHeader kind, never as a generic object something later re-identifies by payload magic. Verified the updated gate still fails when the birth kind is blunted. - #9853's page-class table pushed arena/page_meta.rs to 2559 lines. Split into page_meta/{mod,page_class,tests}.rs; the page-class tests move next to their subject. Both feature configurations build. - That split also stranded six frontier entries in gc_runtime_root_holders.json on the old path, and the PASS1_MARKED census pin needed its re-audit for #9860's and #9845's gc/mod.rs re-export additions before the hash could move.
… split A file-cap split renames a path, and every gate and test that names that path keeps pointing at a file that no longer exists: - addr_class_allowlist.txt: three entries, retargeted to the child each line actually moved into (the GcHeader cast to mod.rs, the two band-literal test fixtures to tests.rs). - thread_local_cold_allowlist.json: the recorded count is per FILE, so the key moves to page_meta/mod.rs. Same 272 cold declarations after. - arena/tests.rs and arena/tests_promoted_runs.rs read page_meta's source to audit every function's flush obligation. They now concatenate all three parts. Pointing them at mod.rs alone would have gone green while silently dropping the functions that moved to page_class.rs from the audit — a smaller test that still passes. - Three prose references updated so nothing names the old path.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (79)
📝 WalkthroughWalkthroughChangesRuntime and garbage collection
Solid JSX compilation
FormData multipart requests
Native-instance binding scope
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)Solid JSX compilationsequenceDiagram
participant ProjectConfig
participant PerryCompiler
participant SolidJsxLowerer
participant PerrySolidRuntime
ProjectConfig->>PerryCompiler: select jsx: "solid"
PerryCompiler->>SolidJsxLowerer: lower parsed JSX module
SolidJsxLowerer->>PerrySolidRuntime: emit createElement and createComponent calls
PerryCompiler->>PerrySolidRuntime: link generated runtime imports
FormData request serializationsequenceDiagram
participant JavaScript
participant FormData
participant FetchOrRequest
JavaScript->>FormData: append or set Blob, File, or text
FetchOrRequest->>FormData: serialize multipart body
FormData-->>FetchOrRequest: bytes and generated content type
FetchOrRequest-->>JavaScript: construct request with body and headers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 6, 2026
Closed
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train: #9795, #9844, #9845, #9853, #9857, #9860, #9862, #9864, #9865, #9868, plus a rooting fix (#9869) the assembly surfaced.
A GC bug found while merging: #9869
#9864 roots
for-in's output and receiver across the callbacks thatownKeys/getOwnPropertyDescriptor/getPrototypeOfcan invoke. It could not reach one place, because the deferred-shadow-set rework (468a57d64) landed after that patch was written:VisitedLevelsrecords each walked prototype level as a plain NaN-boxedf64and dereferences it later, inbuild_shadow_set→mark_own_names→js_object_get_own_property_names.Between the record at level N and that read, the walk crosses
js_object_keys_value(allocates a key array) andjs_object_get_prototype_of(a ProxygetPrototypeOftrap is arbitrary user JS). Either can move the recorded object, andVisitedLevelsis a plain Rust struct no scanner reaches — the "unrooted cache of a raw heap pointer" shape thatgc_root_dominance_check.pystructurally cannot see, since it reads emitted LLVM IR. Filed as #9869; it was already onmain.Fixed by storing
RuntimeHandles, which the collector rewrites.RuntimeHandleisCopy, so the inline arm still costs no allocation and the "no malloc perfor-in" property the rework exists for is preserved.The first version of that fix was wrong, and #9864's own test caught it. The runtime handle stack is strictly LIFO (
Droptruncates to the scope's base), so rooting into an OUTER scope while an inner one is live has the inner scope's drop silently discard the handle — the lifetime cannot catch this, becauseRuntimeHandle<'scope>ties to the scope's borrow, not to stack position. The per-level scope now closes before the push.Gate changes, and why they are not relaxations
shape_descriptor_censusassertedgc_malloc(… GC_TYPE_REGEXP)atjs_regexp_new— exactly the malloc birth perf(regex): allocate the RegExp header in the nursery, not the malloc arm #9845 deliberately replaces with a nursery allocation, so perf(regex): allocate the RegExp header in the nursery, not the malloc arm #9845 could not have passedlintalone. The assertion now accepts either allocator. What it checks is unchanged and is the point: RegExp is born with its OWNGcHeaderkind, never as a generic object something later re-identifies by payload magic. Verified by blunting the birth kind and confirming the gate still fails.arena/page_meta.rsreached 2559 lines with perf(gc): replace the 4-way page-class cache with a direct-indexed table — classification misses 18.5% -> 6.0% #9853's page-class table. Split intopage_meta/{mod,page_class,tests}.rs, page-class tests moved beside their subject, both feature configurations built.That split then broke four path-keyed things, all fixed: three
addr_class_allowlist.txtentries, thethread_local_cold_allowlist.jsonkey (same 272 cold declarations after), and two tests that read page_meta's source to audit flush obligations. Those two now concatenate all three parts — pointing them atmod.rsalone would have gone green while dropping the functions that moved topage_class.rsfrom the audit.PASS1_MARKED's census pin needed its re-audit before the hash could move. fix(gc): re-arm the idle reclaimer on elapsed idle — a declined compaction currently parks the heap 221 MB high #9860's and perf(regex): allocate the RegExp header in the nursery, not the malloc arm #9845'sgc/mod.rshunks are re-export lists only. perf(regex): allocate the RegExp header in the nursery, not the malloc arm #9845's substantive work is in unpinned files, and the argument is recorded in the inventory: the copying-minor arm runs on a minor, which skips both census boundaries; the full-cycle arm walks the RegExp registry building a Vec of addresses with no GC allocation and no JS callback, and is reached from the sweep body — aftercensus_take_if_armed_at_full_sweep_starthas alreadytake()n the snapshot out of the thread-local.Conflict resolutions
#9845 was written against pre-split
regex.rsand re-adds ~430 lines inline (ProgramKey,NEVER_MATCH_PATTERN,expand_js_replacement_fancy,replace_regex_str_fancy). Those hunks were dropped after diffing against the split files — and that diff mattered:main's copies use the ratchet-compliantwith_mut_ptr::<ArrayHeader,_>(|a| a)where the PR's use the older rawget_raw_mut_ptr, so taking the PR's version would have quietly reverted a raw-handle-ratchet conversion. #9845's genuine change, theNEVER_MATCH_SOURCE→NEVER_MATCH_PATTERNunification, is kept; it also removes a duplicate constmainwas carrying.gc_runtime_root_holders.jsonconflicts are resolved structurally (union by(file, name), pins recomputed from the tree, re-audit sentences unioned with longest-wins on containment) rather than as text, which splices entries and can drop a required justification while updating its hash.Validation
run_lint_gates: all 64 gates passed; 2 CI-only skippedReviewer note
This train needed more intervention than its predecessors — the
page_metasplit is a refactor of mine riding along, and the census-gate edit changes a correctness gate's assertion. Both are worth a second pair of eyes.Summary by CodeRabbit
New Features
Bug Fixes
for…inenumeration reliability during garbage collection.Performance