Merge train: #9821, #9825, #9828, #9833, #9838, #9848, #9850, #9854 - #9875
Conversation
…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
… issue reference
…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.
|
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 (3)
📒 Files selected for processing (53)
📝 WalkthroughWalkthroughThe 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. ChangesSolid native renderer
GC pressure and arena triggers
Runtime dispatch and diagnostics
Native widget ordering
Root reload cost accounting
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
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
✨ 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 |
Merge train: #9821, #9825, #9828, #9833, #9838, #9848, #9850, #9854.
Assembled, rebased onto current
mainafter train #9867 landed, and validated once as a tree.Conflict resolutions worth knowing about
scripts/gc_runtime_root_holders.jsonis 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, thePASS1_MARKEDentry 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 overmain'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 thegc/policy.rschange 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.tsconflicted purely in prose: #9833 here and #9837 (landed in #9867) are the same fix with two writeups, and both setITERATIONS = 2400000. Kept the landed writeup.gc/tests/mod.rswas a module-declaration list — sorted union, all three kept.Two build fixes
canonical_char_cachetest module reached the tree (E0428).note_descriptor_targetwas re-exported fromobject/mod.rswith no consumer anywhere in the workspace, failing-D warnings.Both were caught by the
warningsgate, which had been failing for both reasons at once.Validation
run_lint_gates: all 64 gates passed; 2 CI-only skippedSummary by CodeRabbit
New Features
perry-solid, enabling Solid applications to render native Perry interfaces with reactive properties, events, keyed lists, reparenting, and disposal.Performance
Bug Fixes