Skip to content

Merge train: #9821, #9825, #9828, #9833, #9838, #9848, #9850, #9854 - #9875

Merged
proggeramlug merged 26 commits into
mainfrom
train130
Sep 6, 2026
Merged

Merge train: #9821, #9825, #9828, #9833, #9838, #9848, #9850, #9854#9875
proggeramlug merged 26 commits into
mainfrom
train130

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Merge train: #9821, #9825, #9828, #9833, #9838, #9848, #9850, #9854.

Assembled, rebased onto current main after train #9867 landed, and validated once as a tree.

Conflict resolutions worth knowing about

scripts/gc_runtime_root_holders.json is not text-mergeable, and this train hit it three times. Entries are JSON objects, so a line-wise union splices one entry's fields into the middle of another. Worse, the PASS1_MARKED entry carries both source pins and a running re-audit log, and a text union gets each wrong in a different direction: it can restore an older branch's pin hash over main's, and it can drop the written justification that a changed pin is required to carry.

So the resolution parses both sides, unions entries by (file, name), recomputes the window pins from the working tree, and unions the re-audit sentences (dropping any that another sentence strictly contains). That last part matters concretely here: #9838's own re-audit for the gc/policy.rs change would otherwise have been dropped while its pin hash was updated — a green gate with nobody having re-argued the window, which is the exact failure the pin exists to prevent.

benchmarks/gc_ratchet/probes/10_store_receiver_across_alloc.ts conflicted purely in prose: #9833 here and #9837 (landed in #9867) are the same fix with two writeups, and both set ITERATIONS = 2400000. Kept the landed writeup.

gc/tests/mod.rs was a module-declaration list — sorted union, all three kept.

Two build fixes

  • Two byte-identical copies of the canonical_char_cache test module reached the tree (E0428).
  • note_descriptor_target was re-exported from object/mod.rs with no consumer anywhere in the workspace, failing -D warnings.

Both were caught by the warnings gate, which had been failing for both reasons at once.

Validation

run_lint_gates: all 64 gates passed; 2 CI-only skipped

suite passed failed
perry-runtime 3220 0
perry-codegen 1920 0
perry-hir 621 0
perry-stdlib 130 0

Summary by CodeRabbit

  • New Features

    • Added perry-solid, enabling Solid applications to render native Perry interfaces with reactive properties, events, keyed lists, reparenting, and disposal.
    • Added native widget reordering and improved cross-platform child movement.
    • Added optional diagnostics for native-instance registration and buffer address filtering.
  • Performance

    • Reduced allocations during iterator and string operations.
    • Improved garbage-collection behavior during repeated small parses and memory-pressure scenarios.
  • Bug Fixes

    • Preserved correct iterator behavior when prototypes are patched, accessor-based, deleted, or non-callable.
    • Improved native widget ordering and reattachment behavior on macOS and Windows.

Ralph Küpper and others added 26 commits September 6, 2026 12:11
…hat create them

`PERRY_NATIVEINST_DIAG=1` prints one line per native-instance registration:

    [nativeinst] REGISTER push_module name="O" -> child_process::Instance

`register_native_instance` and `push_module_native_instance` are the only two
entry points through which a native-instance tag can come into existence, so a
diagnostic on them cannot miss a tag. That placement is the point of the
change: an earlier attempt at the same question instrumented four plausible
construction sites out of the 165 that build `Expr::NativeMethodCall`, printed
zero, and the zero was uninterpretable.

What it is for (#9847). The tag table is keyed by identifier TEXT with
module-wide scope. On a minified bundle that compiles as one module the same
short name is routinely claimed by several unrelated native classes, and every
method call on any local with that name is then lowered as a native-instance
call of whichever class won. On claude-code's `cli_2.1.112.js` this report
prints 795 registrations whose most-registered identifiers are Y(71), z(65),
K(65), _(65), A(54), O(52), w(37), q(35) — every one a single letter — with `O`
registered as `stream::Instance`, `child_process::Instance`,
`transform_stream::TransformStream` and `readable_stream::ReadableStream` at
once. Reading that took one 30-second compile; deriving it from source took a
day of hypotheses, four of which were wrong.

`PERRY_NATIVEINST_DIAG` is excluded from the build-level cache for the same
reason `PERRY_OPT_REPORT` is: a cached build reuses the finished binary and
never lowers HIR, so the report would come up empty — and empty is
indistinguishable from "no tag was ever registered", which is precisely the
reading this diagnostic exists to make impossible.

Off, the cost is one relaxed atomic load per registration and nothing else.
…alues

`MAX_BLOCK_LOAD_PRODUCT` guards the reachability walk in `apply_to_function`,
and that walk runs once per `groups` entry — one per ROOT LOAD. The check
multiplied by `values.len()` instead, which since #7664 counts every pure-bit-op
derivation as its own `Reloadable`. On a wide function that is several times the
group count, so the pass declined on functions whose real cost was well inside
the bound.

Declining is not correctness-neutral under the native root lowering. The
constant's comment claimed "the pass is an improvement, not a correctness
precondition, so declining is safe"; that is false, and it is why the cliff went
unnoticed. When the pass does not run, nothing re-reads the slot: a receiver
read out of its root, unmasked to an i64/double and carried across a call is a
value RS4GC cannot relocate, so the store lands in a from-space object.

Found on Claude-of-Duty's `Arm.constructor` (4924 blocks, 747 root loads, 2102
values): 10,350,248 by the old metric against an 8M cap, 3,678,228 by the new
one. It declined, and `this.upper = buildSleeve(...)` wrote through a stale
receiver — a SIGBUS under `PERRY_GC_PROTECT_FROMSPACE=1`, and silent field
corruption without it (`THREE.Object3D.add: object not an instance of
THREE.Object3D. undefined` two frames later).

The bound itself is unchanged; only the term it is measured against. A function
that genuinely exceeds `blocks x groups` still declines and can still carry a
stale register — that residual risk is now stated at the constant rather than
denied.

Note that `scripts/gc_root_dominance_check.py --stale-registers --moving-only`
does NOT flag this shape: it reported 0 stale uses on the faulting module (17
found, all `source=global`), so it cannot serve as a guard here.

The regression test replicates the `masked_receiver` shape across 1100 blocks
with a MAX_RECIPE-length derivation, sized to clear the cap by the old metric
and sit an order of magnitude inside it by the new one. It inserts 0 reloads
before this change and 1100 after.
`call_overridden_iterator_next` minted a fresh 4-byte "next" key string on
every built-in iterator step, purely to run a by-name prototype lookup that
concluded nothing was patched. The `ITERATOR_PROTOTYPE_PTR == 0` early-out
that was supposed to prevent this is dead after the first iterator any
program allocates: every iterator allocator calls `attach_iterator_prototype`
-> `ensure_iterator_prototypes`, which materializes the tower.

Adds `prototype_next_is_canonical`: the prototype's own `next` slot holds a
closure whose native entry is the canonical thunk, and no accessor descriptor
is recorded for "next". Both reads are non-allocating. Any other state falls
through to the by-name path, unchanged.

This is the third-ranked site by count in the 2026-09-06 claude-code
allocation census (~122,880 x 32 B per 400-character reply), which had
attributed it to `Intl.Segmenter` substring copying. Caller walk in the
shipped binary `cc_relink/cc_int_0905`:

  js_for_of_next+0xd0
    -> dispatch_array_iterator_method_inner+0x218   (bl call_overridden_iterator_next)
      -> call_overridden_iterator_next+0x67c        (bl js_string_from_bytes_with_capacity)
        -> string_storage_alloc

Measured on a relinked claude-code binary carrying this fix plus a
measurement-only hit/miss counter. Before the fix every probe allocated, so
`hits + byname` is the pre-fix count and `byname` is what survives:

  400-char reply, run A   144,189 probes   byname 0
  400-char reply, run B   144,303 probes   byname 0
  3300-char reply          887,076 probes   byname 0

`byname = 0` on every one of the 173 per-minor reports across the three runs:
the proof answers 100 % of probes on a real program, which is what rules out
the one silent failure mode (the accessor half is a per-key Bloom bit, so a
colliding accessor on the prototype would disable the fast path with no test
failing).

`cargo test -p perry-runtime --release --lib -- --test-threads=1`: 3,171
passed, 0 failed. Four sabotage arms, each failing only its named assertion:
removing the fast path entirely reads exactly 32,000 bytes over 1,000 probes;
dropping only the accessor half fails only the accessor test; dropping only
the native-entry comparison fails only the replaced-`next` test.
An integration arm for the allocation-free proof: compiles
`test-files/test_gap_iterator_prototype_next_patch.ts` and byte-compares
stdout against node v26.5.1, captured 2026-09-06 on this box.

Three of the lines are the ones that can only pass if the proof is exactly
right:

  F-bound-copy 100,200  a `bind` of the original has the SAME native entry as
                        the builtin thunk but a different `this`; a proof that
                        compared native entries without first reading the
                        prototype's own slot would print `1,2`.
  G-accessor 1,2 true   `defineProperty(proto,"next",{get})` leaves the old
                        closure in the data slot, so the own read alone still
                        sees the canonical closure — only the per-key accessor
                        Bloom bit makes the proof decline.
  H true                a deleted `next` must throw a TypeError, never fall
                        through to the builtin advance.
The allocation-free proof reads the prototype's own `next` slot as a RAW
value before deciding anything, so a number, a string, `undefined`, `null`
and a plain object each have to defeat it and throw a TypeError rather than
be mistaken for the builtin closure. Node v26.5.1 throws for all five;
pinned in the integration arm.
The fragment was written before the issue existed and carried 9840, which is
an unrelated open GC issue. #9846 is the filed report for this defect.
…w can no longer be

`is_registered_buffer` is the largest single leaf in cc's profile
(`is_registered_buffer_slow`, 3.19 % of active main-thread CPU on
`cc_main_0905`), and it is reached from property access rather than I/O: a
"is this value a buffer?" test run on values that are not buffers.

Its gate is `BUFFER_LIKE_ADDR_WINDOW`, a process-global min/max span. The
98.0 % rejection rate in its doc comment is measured on `claude-code --help`,
which registers **10** buffers. A streaming turn registers **213**, scattered
across a **527 MB** span, so `[lo, hi]` covers half a gigabyte of ordinary heap
and stops rejecting. `PERRY_BUFFER_DIAG` (added here), one 400-char reply:

    probes=34,603,009  admits=25,476,705 (73.63 %)  rejected 26.37 %
    true_positives=53,109 (0.208 % of admits)
    window [0x4c95a298460, 0x4c979e7db80] span 507.9 MB
    registrations=213 unregistrations=12 live_max=201

25.5 million out-of-line probes per reply, 99.79 % of which find nothing.

That is the failure `RegistryAddrFilter` was built for after #9272 — its doc
names "entries are ordinary heap objects interleaved with everything else" as
the case a window cannot serve, and measured `is_registered_symbol` at 38.3 %
(window) against 99.58 % (filter). Buffers kept the window because it rejected
100 % of `is_uint8array_buffer`'s calls ON `--help`.

The capacity question that structure demands was asked BEFORE adopting it.
`RegistryAddrFilter` accrues bits per admission and never clears them, so a
high-churn set saturates it — the trap #9807 documented, where a 4,096-bit
filter held 162,258 keys and answered "may hold" to every probe. Buffers are
the opposite case: probing is hot, registration is rare. **213 cumulative
admissions against 1,024 bits and 3 hashes is a 10.0 % false-positive rate.**
The counter that establishes this ships with the change.

One binary, one environment variable apart:

    PERRY_BUFFER_ADDR_FILTER=0   admits 25,476,705 (73.63 %)  rejected 26.37 %
    filter on                    admits  1,223,944 ( 3.54 %)  rejected 96.46 %

**24.25 million out-of-line calls removed per 400-character reply**, true
positives preserved (53,109 vs 53,092 — the difference tracks one fewer
registration in that run; a Bloom filter has no false negatives).

Soundness is machine-checked, not argued: the existing debug assertion
re-derives every rejection from the authoritative tables, so a false negative
panics. The whole suite in DEBUG — 3,171 tests — passes with it armed.

Stacked on the `for-in` branch (#9823) only because both add counters to
`hot_diag.rs`; the two changes are otherwise independent.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
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
…minor (#9840)

`GC_NEXT_TRIGGER_BYTES` is documented as "bumped after each `gc_collect_inner`
based on collection effectiveness". It was not.
`gc_finish_arena_trigger_collection` re-baselined it; the finisher for the SAME
nursery collection with the malloc sweep added,
`gc_finish_malloc_trigger_collection`, did not. So the whole-arena threshold was
measured from the last ARENA-KIND collection rather than from the last
collection, and a run of `MallocCount` minors could walk the arena total across
a threshold nothing had refreshed. The asymmetry predates the budgeted split
(9d3bd2e's pre-split `gc_check_trigger` had the same two branches). It is
justified in ONE direction only -- an arena minor may legitimately skip the
malloc sweep, so it must not move the malloc trigger -- and that direction is
unchanged and still pinned by
`test_gc_check_trigger_copied_minor_without_malloc_sweep_preserves_malloc_trigger`.
A `MallocCount` minor has no such exemption: it swept the arena.

The threshold re-baseline is factored out of
`gc_finish_arena_trigger_collection` into
`gc_rebaseline_arena_trigger_after_collection` and called from both nursery
finishers, with `pre_in_use` captured for `MallocCount` cycles on all three
paths that reach one (alloc-point direct, moving safepoint, budgeted). The base
stays `arena_total_bytes()` -- COMMITTED bytes -- because that is what
`next_arena_trigger_base()` is compared against. It is deliberately neither of
the two occupancy readings #9831 publishes at
`note_collection_finished_arena_occupancy`, whose doc comment now tabulates all
three quantities and their units, since two of them share that funnel and a
re-baseline from a bump-offset or live-census base would arm this trigger below
the arena's own total. `OldReclaim` and the idle reclaim stay out: after a full
that released blocks the un-moved trigger sits FURTHER above the new total,
which is the conservative direction, and a full's cadence belongs to the
old-generation band. A nursery-trigger cycle that `arena_growth_full_escalation_
due()` escalated to a full still finishes here, exactly as the arena arm's
escalated fulls already did.

Measured on the compiled claude-code TUI (PERRY_GC_DIAG=1, per firing, four
3300-character captures across two independently built binaries): the streaming
turn ran a strict 6:1 pattern -- six `MallocCount` minors promoting ~3.2 MB each
crossed the stale threshold inside the sixth minor, and at the very next
safepoint the `ArenaBytes` arm fired on a nursery of 856 bytes
(`promoted_bytes=216 freed_bytes=640`), paying the whole per-collection fixed
cost to free 640 bytes. Eight of ~60 collections per 3300-character turn.

Length is part of every figure: the shape needs a run of promoting
`MallocCount` minors, and the 400-character capture has 48-62 fewer of them
than the 3300-character ones -- it has ZERO. So this change is predicted flat
at 400 on every counter, and that holds with or without the in-flight change
moving `RegExpHeader`s (the arm's only measured input on this program) to the
nursery.

One coupling is stated because it touches a fix that landed hours earlier:
`GC_STEP_BYTES` had exactly one production writer -- the arena finisher -- and
#9831 made it an INPUT to the tiny-parse pressure guard's headroom, so scoring
a `MallocCount` minor's productivity moves that guard too. That is the same
symmetry rather than a side effect (the step is documented as "collection
effectiveness", not "arena-kind collection effectiveness"). Estimated over 209
`MallocCount` firings in the same captures, `pct_freed` has a median of 4-5 %
and lands <10 % in 194 cases, 10-24 % in 10, 25-84 % in 5 and >84 % in none: 93
% take the "<10 % -> double" band and push the step UP, so on this program the
coupling makes #9831's guard MORE conservative, not less.

The arm's dueness predicate is byte-identical, so when it is due it fires the
same collection. `PERRY_GC_ARENA_REBASELINE_ALL=0` restores the old asymmetry;
its OFF state is asserted in CI as the GC knob kill-policy requires, by a test
that is simultaneously the sabotage proof for the two ON-state tests -- the OFF
branch IS the deleted call, so the proof runs in CI instead of being performed
by hand and lost. PERRY_GC_DIAG=1 gains
`[gc-arena-rebaseline] arm=... next_trigger=... total=... headroom=... pct=...
step=...`, whose field names are disjoint from the reclaim keys
`scripts/gc_repsel_matrix.sh` sums; `[gc-step]` stays ArenaBytes-only for that
same reason, so the ratchet's reclaim total does not gain an addend from a
change that reclaims nothing new.

Tests: `direct_malloc_minor_also_rebaselines_the_whole_arena_trigger` (direct
synchronous arm), `test_budgeted_malloc_minor_rebaselines_the_whole_arena_trigger`
(budgeted arm, the one cc takes), and
`direct_malloc_minor_arena_rebaseline_kill_switch_restores_the_stale_threshold`
(the OFF state, and the sabotage proof).
…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
…iptors on the primitive-string path

Three allocations a JS program can never observe, found with the
allocation-site sampler (`PERRY_ALLOC_SITE_SAMPLE`) on the compiled
claude-code TUI, where they are the largest attributed source of garbage in
both the streaming turn and the render pass that follows it.

1. A one-ASCII-character string is now the canonical per-thread header.
   `js_string_char_at` minted a fresh 32-byte heap string per character read,
   and everything that walks a string a character at a time goes through it:
   `s[i]`, `charAt`, string spread, the String-wrapper index installer. There
   are 128 possible contents. The table has the same residency contract as the
   small-integer string table next to it (longlived arena, `refcount = 0` so it
   is never mutated in place, pinned out of the young generation) and rides
   that table's existing root scanner rather than registering a 96th one.

2. Runtime-internal constant property names resolve through the intern table.
   The `globalThis` builtin lookup, `x.constructor`, `toString` resolution and
   primitive-method dispatch each built a fresh heap string for a literal name
   on every call; `js_get_global_this_builtin_value` alone accounted for 133 MB
   of the 990 MB one 3300-character reply allocates. `string::canonical_key`
   routes them through the content-keyed per-thread table that
   `js_string_materialize_to_heap` already uses, which is also what the
   property read/write fast paths require of a key.

3. A `String` wrapper no longer stores a property descriptor per character.
   ECMA-262 §10.4.3 gives every in-range index of a String exotic object
   `{ writable: false, enumerable: true, configurable: false }` — a fact of the
   class and the boxed length, not per-object state — so `get_property_attrs`
   answers it from the wrapper's payload. Storing it cost, per boxed character,
   a Rust `String`, a `PROPERTY_DESCRIPTORS` entry only a full collection's
   dead-owner prune could reclaim, an owner-index entry, and one program-wide
   `prop_plan_epoch_bump()`. A sloppy method call on a string primitive boxes
   its receiver, so the TUI paid all of it per rendered line. A real stored
   descriptor still wins, so `Object.freeze`/`defineProperty` on a wrapper are
   unchanged.

`PERRY_GC_DIAG=1` also gains `[gc-primitive-dispatch]`: which
`<Builtin>.prototype.<method>` names reach the primitive-method fallback, how
often, and how many wrapper index properties they cost — the counter that says
whether a boxing fix ran.

Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2
`codePointAt` had a `String.prototype` thunk but no arm in the native
string-method dispatch, so every call fell through to
`call_primitive_builtin_prototype_method`: resolve
`globalThis.String.prototype.codePointAt`, clone that closure to rebind `this`,
and — the thunk not being registered strict — run `ToObject` on the receiver,
minting a `String` wrapper whose own index properties are one per UTF-16 code
unit.

The new `[gc-primitive-dispatch]` counter says how much that cost: on the
compiled claude-code TUI, `codePointAt` is the ONLY method name that reaches
the fallback at all, and it reaches it 99,008 times per 400-character streamed
reply — 99,008 `globalThis` lookups, 99,008 closure clones and 99,008 String
wrappers, because grapheme-aware text measurement calls it once per character.

The arm is the sibling of `charCodeAt` one line above it and reads the receiver
the same way. The test asserts the WRAPPER COUNT rather than the return value:
the fallback computes the same number, so an answer-only test would pass with
the arm deleted. A positive control pins that the counter can move.
Two byte-identical copies of the module reached the train tree, so
`perry-runtime`'s test build failed with E0428. Keep one.
…rs change

The structural JSON merge recomputed the census-window pin from the tree
but did not carry the author's written re-audit. A pin whose hash tracks
the tree while its justification lags is exactly the gap the pin exists
to catch: the gate stays green and nobody has re-argued the window.
`cargo check --all-targets -D warnings` fails on it: nothing in the
workspace consumes `crate::object::note_descriptor_target`, only
`descriptor_state.rs`'s own internal callers, which do not go through
the re-export.
@proggeramlug
proggeramlug merged commit a681446 into main Sep 6, 2026
15 of 19 checks passed
@proggeramlug
proggeramlug deleted the train130 branch September 6, 2026 11:02
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: b5138ee5-0602-4b58-8e89-ad43cb8f3b91

📥 Commits

Reviewing files that changed from the base of the PR and between 35c36f4 and bb66131.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • packages/perry-solid/package-lock.json is excluded by !**/package-lock.json
  • tests/release/packages/solid-reactivity/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (53)
  • changelog.d/9795-string-code-point-at-dispatch.md
  • changelog.d/9825-solid-native-renderer.md
  • changelog.d/9828-buffer-registry-addr-filter.md
  • changelog.d/9833-probe10-margin.md
  • changelog.d/9838-tiny-parse-pressure-pricing.md
  • changelog.d/9840-arena-trigger-rebaseline-symmetry.md
  • changelog.d/9846-iterator-next-override-probe-allocation.md
  • changelog.d/9847-nativeinst-registry-diag.md
  • crates/perry-codegen/src/root_reload.rs
  • crates/perry-codegen/src/root_reload_tests.rs
  • crates/perry-dispatch/src/ui_table/part_a.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-runtime/src/buffer/header.rs
  • crates/perry-runtime/src/gc/policy.rs
  • crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs
  • crates/perry-runtime/src/gc/tests/debt_pacer.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/gc/tests/tiny_parse_pressure.rs
  • crates/perry-runtime/src/hot_diag.rs
  • crates/perry-runtime/src/object/iterator_prototypes.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-runtime/src/object/native_call_method/code_point_at_dispatch_tests.rs
  • crates/perry-runtime/src/object/native_call_method/string_methods.rs
  • crates/perry-runtime/src/registry_latch.rs
  • crates/perry-runtime/src/string/tests.rs
  • crates/perry-ui-macos/Cargo.toml
  • crates/perry-ui-macos/src/widgets/mod.rs
  • crates/perry-ui-macos/tests/native_widget_order.rs
  • crates/perry-ui-windows/src/ffi/widget_layout_extras.rs
  • crates/perry-ui-windows/src/widgets/mod.rs
  • crates/perry/src/commands/compile/build_cache.rs
  • crates/perry/tests/issue_9846_iterator_prototype_next_patch.rs
  • packages/perry-solid/.gitignore
  • packages/perry-solid/README.md
  • packages/perry-solid/examples/counter.ts
  • packages/perry-solid/package.json
  • packages/perry-solid/src/index.ts
  • packages/perry-solid/src/renderer.ts
  • packages/perry-solid/test/native-smoke.py
  • packages/perry-solid/test/native-smoke.ts
  • packages/perry-solid/test/renderer.test.ts
  • packages/perry-solid/tsconfig.json
  • scripts/gc_runtime_root_holders.json
  • test-files/test_gap_iterator_prototype_next_patch.ts
  • tests/release/packages/perry-solid/.gitignore
  • tests/release/packages/perry-solid/expected.txt
  • tests/release/packages/perry-solid/fixture.sh
  • tests/release/packages/solid-reactivity/.gitignore
  • tests/release/packages/solid-reactivity/README.md
  • tests/release/packages/solid-reactivity/entry.ts
  • tests/release/packages/solid-reactivity/expected.txt
  • tests/release/packages/solid-reactivity/fixture.sh
  • tests/release/packages/solid-reactivity/package.json

📝 Walkthrough

Walkthrough

The PR adds a Solid native renderer, updates GC pressure and arena-trigger handling, optimizes runtime dispatch, improves native widget ordering, and changes root-reload cost accounting. It also adds diagnostics, regression tests, release fixtures, examples, and changelog entries.

Changes

Solid native renderer

Layer / File(s) Summary
Renderer and package API
packages/perry-solid/...
Adds native Solid elements, properties, retained nodes, keyed movement, reparenting, disposal, examples, and package configuration.
Renderer validation and release fixtures
packages/perry-solid/test/*, tests/release/packages/perry-solid/*, tests/release/packages/solid-reactivity/*
Adds unit, smoke, reactivity, native-compilation, and disposal coverage.

GC pressure and arena triggers

Layer / File(s) Summary
Tiny-parse pressure pricing
crates/perry-runtime/src/gc/policy.rs, crates/perry-runtime/src/gc/tests/*, changelog.d/9838-tiny-parse-pressure-pricing.md
Uses adaptive growth-priced headroom for tiny-parse collection decisions and adds diagnostics and tests.
Malloc-trigger arena rebaselining
crates/perry-runtime/src/gc/policy.rs, changelog.d/9840-arena-trigger-rebaseline-symmetry.md
Rebaselines the whole-arena trigger after malloc-count minors and passes pre-collection arena usage through direct and budgeted paths.
GC regression coverage and metadata
crates/perry-runtime/src/gc/tests/*, scripts/gc_runtime_root_holders.json, changelog.d/9833-probe10-margin.md
Adds trigger, kill-switch, probe-margin, and root-holder verification coverage.

Runtime dispatch and diagnostics

Layer / File(s) Summary
String and iterator dispatch
crates/perry-runtime/src/object/*, test-files/test_gap_iterator_prototype_next_patch.ts, crates/perry/tests/issue_9846_iterator_prototype_next_patch.rs
Adds native codePointAt dispatch and an allocation-free canonical iterator probe while preserving patched and accessor behavior.
Buffer registry filtering
crates/perry-runtime/src/buffer/header.rs, crates/perry-runtime/src/hot_diag.rs, crates/perry-runtime/src/registry_latch.rs
Adds address filtering, buffer probe diagnostics, registration statistics, and shared window bounds access.
Native-instance diagnostics
crates/perry-hir/src/lower/context.rs, crates/perry/src/commands/compile/build_cache.rs
Adds optional native-instance registration output and disables cached builds when it is active.

Native widget ordering

Layer / File(s) Summary
macOS stack ordering
crates/perry-ui-macos/...
Uses top-gravity stack operations, refreshes parent mappings, supports reparenting and reordering, and adds AppKit integration coverage.
Windows child reorder API
crates/perry-dispatch/src/ui_table/part_a.rs, crates/perry-ui-windows/src/widgets/mod.rs, crates/perry-ui-windows/src/ffi/widget_layout_extras.rs
Changes reorder arguments to floating-point values and implements indexed child movement with layout requests.

Root reload cost accounting

Layer / File(s) Summary
Root-load cap calculation
crates/perry-codegen/src/root_reload.rs, crates/perry-codegen/src/root_reload_tests.rs
Measures the cap using root-load groups and verifies that derived values do not cause an incorrect pass decline.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SolidApp
  participant PerrySolid
  participant NativeDriver
  participant NativeWidget
  SolidApp->>PerrySolid: render signals and components
  PerrySolid->>NativeDriver: create widgets and set properties
  NativeDriver->>NativeWidget: insert child handles
  SolidApp->>PerrySolid: update signal or keyed list
  PerrySolid->>NativeDriver: update text or move child
  NativeDriver->>NativeWidget: apply native ordering
  SolidApp->>PerrySolid: dispose owner
  PerrySolid->>NativeDriver: release callbacks and subtree
Loading
sequenceDiagram
  participant Parser
  participant GcPolicy
  participant Arena
  participant GcCollector
  Parser->>GcPolicy: request tiny-parse pressure check
  GcPolicy->>Arena: read in-use bytes and post-collection baseline
  GcPolicy->>GcCollector: schedule collection when priced growth is due
  GcCollector->>GcPolicy: report collection outcome and pre-collection usage
  GcPolicy->>Arena: rebaseline whole-arena trigger
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch train130

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant