chore: land #10846 + #10842 + #10843 (v0.5.1629) - #10875
Merged
Merged
Conversation
`Object.setPrototypeOf(o, null)` says there is nothing above `o` any more.
Perry's property reads walked up anyway and answered from the prototype `o`
was BORN with, while `"a" in o` on the same object correctly answered false.
The object contradicted itself:
const P = { a: 1 };
const o = Object.create(P);
Object.setPrototypeOf(o, null);
o.a // node: undefined perry: 1
"a" in o // node: false perry: false
Object.getPrototypeOf(o) // node: null perry: null
Perry bakes class ids at allocation time, so every "the own-key scan missed,
what does this object inherit?" path in the runtime ends at the receiver's
CLASS surface — its vtable, its declaration prototype, or `Object.prototype`.
That fallback is right for an object whose chain was never touched. There was
no test for whether the chain had been ENDED, so it ran for those too.
The issue reports one case. There are ten, in three families, and they are one
root cause:
* the receiver's own chain ended — on an `Object.create(P)` result, on a
`new C()` instance, on a declared-class instance, by `setPrototypeOf` or
by `__proto__ = null`; data reads and method reads both (they resolve
through different paths, and both were wrong);
* a prototype INSIDE the chain ended, the receiver never touched:
`Object.setPrototypeOf(K.prototype, null)` and then `new K().toString`.
Neither the receiver's guard nor the holder's can see that — the statement
is one hop above the receiver and one below the answer;
* the recorded prototype is itself an object with no prototype
(`setPrototypeOf(o, Object.create(null))`). The sharpest row: `o.a` kept
answering from the prototype `o` was born with, which is no longer in its
chain at all.
`Object.create(null)` was already right, because a birth with no prototype has
its own header bit (`OBJ_FLAG_NULL_PROTO`, #1175) and the fallback tested it.
This is the same answer, asked of the whole chain instead of one cell.
## The fix
One predicate, `prototype_chain::prototype_chain_ends_in_explicit_null`, and
two gates.
The predicate walks the chain a READ walks — per-instance record first, then
the hop's class link, with the same declared-prototype-before-synthetic
precedence `native_get::try_data_get_bytes` uses, so it cannot answer about a
hop a read never visits. A chain ends explicitly at a recorded `TAG_NULL` or a
cell born with `OBJ_FLAG_NULL_PROTO`; a hop that merely has no record is an
ordinary object standing on the class default, which is exactly the case the
fallback exists for, and the walk answers false there.
* `prototype_override::inherited_field_if_overridden` now distinguishes its
two kinds of miss. A miss on a chain that ends in an explicit null is the
final answer and it is `undefined`. Every other miss still returns `None`
and defers, which is what #9244 requires: the arms below it are not only
the class vtable, they are also everything Perry SYNTHESIZES rather than
stores on a real prototype (a plain function's `.prototype`, the
boxed-wrapper builtins, the iterator helpers), and swallowing those made
them unreachable.
* `accessors::ordinary_object_prototype_property_value` asked only whether
the RECEIVER was born without a prototype. It now asks the chain.
Cost is paid only on a MISS. An ordinary receiver answers false from one
absent meta record plus one header bit. Measured on a loop whose body is three
property misses (an `Object.create` receiver, a class instance, a plain
object): 34040.8 instructions per iteration before, 33663.9 after — no
regression, and the miss path is so expensive already (~11 k instructions per
miss) that this is invisible inside it. `class_prototype_object` and
`class_decl_prototype_object` are pure registry reads, so the walk allocates
nothing and cannot re-enter.
## Tests
`test-files/test_parity_explicit_null_prototype.ts` — 20 rows against node,
each printing the READ, the `in` and `getPrototypeOf` together, because the
bug's signature is the object contradicting ITSELF. A fixture that printed
only the read could be "fixed" by making `in` wrong too; this one cannot be.
It includes the four rows that were always correct (`Object.create(null)`,
an own key on an ended prototype, a chain ended and then restored) so a
future fix that reaches for the class surface again cannot regress them
unnoticed.
Must-fail proof: compiled with the unfixed compiler, **10 of the 20 rows
differ from node**; compiled with this one, 0 differ. The two binaries `cmp`
different.
Five unit tests on the predicate itself: an ended chain, an ended INTERIOR
prototype, a hop born without a prototype, an untouched object (which must
stay false — that is the common case and the whole fallback depends on it),
and a recorded prototype cycle, which must terminate.
`cargo test -p perry-runtime -- --test-threads=1`: 4113 passed, 0 failed.
(cherry picked from commit 6d3ff25)
… one flags word replaces two registry probes Two stages of the same change, in one commit because the second cannot compile without the first: both facts live in the same word, and that word had to move before either was safe. ## Stage (i): the per-hop walk becomes one compare #10834 re-proves a cached inherited read with one ShapeId compare per hop, up to four dependent loads through prototype objects that are usually cold. It is proportional to the DEPTH of the chain, and it is a LOOP, so the hit can only ever live behind a call: an emitted property-read site cannot branch on a variable number of compares. The root cause is that a mutation of an object somebody INHERITS from is invisible to the objects below it. This is V8's prototype validity cell, collapsed to one global counter (`object::proto_validity`): * An object is MARKED (`OBJECT_META_FLAG_IS_PROTOTYPE`) by the `[[Prototype]]` install funnel, and the read cache REFUSES to record a hop that is not already marked. * Every shape-word CHANGE on a marked object bumps the counter, hooked at `stamp_object_shape_id_with_carrier_note` — the runtime's single structural-mutation publication funnel, which its own header already names as such. * `prop_plan_epoch_bump` and `class_lookup_surface_gen_bump` bump the same word, so it also stands for everything the semantic property epoch stands for, and for a re-registered class prototype object. A plain value store to an existing key deliberately does not invalidate: an entry records (holder, slot) and LOADS the value on every hit. ## Stage (ii): two registry probes become one bit A cached hit asked `is_arguments_object` (14.0 instructions) and `is_process_env_ptr` (5.0), both address-keyed registry probes, on every read. `OBJECT_META_FLAG_EXOTIC_READ_RECEIVER` is a per-object summary of both, set inside each registry's single writer in the same breath as the insert, and it sits in a word the hit path already loads — beside `elements`, which folds in too. Each probe keeps a `debug_assert` that a registry hit implies the flag, so an insert that skips the mark fails those suites loudly. Decisively for the next stage: an emitted read sequence could not have called either probe at all. ## The word these flags live in, and the one they do not Both started in `GcHeader::_reserved`, on the claim in that file's `OBJ_FLAG_*` block that bits 12..13 were "the last free bits". **They are not free.** `gc/layout.rs` owns 12 (`GC_OBJ_TYPED_LAYOUT_INTACT`), 13 (`GC_LAYOUT_ALL_POINTERS`) and 14..15 (`GC_LAYOUT_STATE_MASK`) in a separate constant namespace in a different file, and `set_layout_state` CLEARS bit 13 on every layout-state change. A mark placed there is not merely shared, it is silently ERASED — so the reader answers `false` for an object the writer marked, the invalidation never fires, and a cached entry returns a stale value. Found by measuring: claiming bit 12 regressed the `Object.create` fixtures from 383 to 1533 instructions per read, because those receivers all carry `GC_OBJ_TYPED_LAYOUT_INTACT`. That regression is what sent me to read `gc/layout.rs` and find bit 13 under the mark this PR had already shipped. Both facts now live in `ObjectMeta::flags` bits 5 and 6, verified against #8690's reservation comment and every reader in the tree. It is the better home, not a worse one: the hit path already loads `meta`, so the facts cost the hit nothing, and nothing in the layout machinery can reach them. This commit also installs the complete `_reserved` bit map — both namespaces, one table — on `OBJ_FLAG_RESERVED_BIT_MAP_SEE_DOC` in `gc/types.rs`, and points `gc/layout.rs` at it. #8690 hit the same trap and left its warning in `ObjectMeta::flags`' doc comment, which is not a file anyone reads when spending a header bit. ## The polarity, and why the cache still marks The install funnel marks; the cache refuses an unmarked hop. When the prime meets one it marks it and ABANDONS the walk without recording anything — marking allocates a meta record, which can move `obj`, `next` and every address in `hops` — and the next read of that pair primes normally. That keeps the invariant that matters absolutely (no entry is ever recorded through a hop that was not already marked before the walk began) while making coverage self-healing: an install route the funnel misses costs one declined read, not a permanent loss. The refusal is not remembered, because marking bumps no validity and a negative entry would decline the pair for the life of the process. `class_prototype_object_root_store` looked like the place to mark the `Object.create` route and SIGSEGVs the suite: it holds a bare `proto_ptr` that it re-uses for an address-index rekey and a write barrier, so a mark that allocates leaves both stale. Any mark that allocates must be the last thing its caller does with the pointer. ## Measured `perf stat -x, -e instructions:u`, min of 3, fitted 200 k -> 5 M, two trees whose binaries `cmp` different, output identical to node on every row. Inheritance = fixture minus its own-read twin; the twins are unchanged to the instruction (`own1` 130.00, `ownm` 303.00, `ownpoly` 218.25). | fixture | #10834 | this | node | bun | |---|---|---|---|---| | 1-level `Object.create` | 278 | **226** | 2.1 | 0.4 | | 3-level chain | 296 | **226** | 0.1 | 0.8 | | class prototype | 286 | **236** | 0.0 | 0.5 | | method through the prototype | 282 | **232** | 0.7 | 0.6 | | 4 receiver shapes, one prototype | 370 | **319** | 4.0 | 0.4 | Still depth-independent: a three-level chain costs exactly what a one-level one costs, because the guard no longer has a length. That is the property the emitted sequence needs. Key-add churn with a live entry: 420 -> 368. `own1` 130.00 before and after. Over-invalidation of the GLOBAL counter, measured: a fixture that structurally mutates an UNRELATED marked prototype once per 64 inherited reads costs 710.91 per iteration against 710.92 for one that mutates the prototype being read. #10834's semantic-epoch check was already global, so the only event class this makes global that was not is a plain key add on an object used as a prototype. One invalidation costs one re-prime: 1494 - 356 = 1138 instructions. ## Tests `cargo test -p perry-runtime -- --test-threads=1`: 4136 passed, 0 failed. The coverage test is the one worth reading. It builds a receiver six ways — `setPrototypeOf` on a literal, `Object.create`, a class-default link, a class-evaluation link, two hops, and a key added to the prototype after the receiver exists — and asserts each read is SERVED BY THE CACHE, because a cache that declines everything returns exactly the values the chain walk would and is invisible in a program's output. Its first version shared ONE prototype across all six styles, so five of them were marked by the first and passed vacuously; giving each style its own prototype turned it red immediately. Each style now gets a fresh prototype. Plus: an unmarked prototype is refused rather than cached; the three invalidation controls from #10842's first revision (mark/hook, class-surface bump, semantic-epoch fold); and the 27 cache tests, the two GC root tests and the differential fixture against node. Hit evidence, from compiled programs with enough misses elsewhere to make `PERRY_IC_DIAG` dump: `hits=25811149 primes=1 declines=1` for a one-level chain and `hits=26087239 primes=1 declines=3` for a three-level one — exactly one mark-and-abandon per hop, then steady hits. (cherry picked from commit e3fdbb8)
…c-resource registry `get_field_ic_miss_impl` asked `is_async_resource_handle(obj)` before the inherited-read cache could answer. Once that registry's latch is armed — which anything creating one `AsyncResource` does, for the life of the process — the probe is a thread-local registry lookup costing 16.0 instructions per call (callgrind, `--separate-callers=1`), and it ran on EVERY inherited read whether or not the cache could serve it. The lookup moves above it. It cannot be confused by an async resource handle: those are `Box::into_raw` native allocations outside the GC arena, so their word at payload +4 is the high half of a small counter rather than a live ShapeId, `object_shape_stamp` answers 0, and the lookup returns `Unknown` about ten instructions later without dereferencing anything further. Nothing that was below the probe moves, and the async-resource dispatch itself is unchanged. Measured (`perf stat -x, -e instructions:u`, min of 3, fitted 200 k -> 5 M, two trees, binaries `cmp` different, output identical to node on every row): | fixture | before | after | |---|---|---| | 1-level `Object.create` | 356 | **334** | | 3-level chain | 356 | **334** | | class prototype | 366 | **344** | | method through the prototype | 535 | **513** | | 4 shapes, one prototype | 537.75 | **515.75** | | key-add churn, live entry | 368 | **346** | -22 on every inherited row and **0.00 on every own-read row** — `own1` 130.00, `ownm` 303.00, `ownpoly` 218.25, all unchanged to the instruction, because an own read reached the probe before this change and reaches it after. This also documents, at the one read that depends on it, why the identity load at payload +0/+4 is safe on the three pointer-tagged values that #10828's rule 3 does NOT cover — `SymbolHeader`, `AsyncHookHandle` and `AsyncResourceHandle` are `Box::into_raw` allocations outside the GC kind table. Two are safe by construction (`registered` is 0 or 1; `index`'s high half is zero). The third, `AsyncResourceHandle.ids.async_id`, is safe only by MAGNITUDE — its high half is zero until a process creates 2^32 async resources — which is the same class of argument #10824 refused for buffer capacities. It is not load-bearing: `is_shape_id`'s range test rejects the word either way, and an emitted guard keeps that protection for free because a site's expected ShapeId is always in [0x8000_0000, 0xC000_0000). Anyone dropping that range test would be resting on the magnitude argument, and should say so. `cargo test -p perry-runtime -- --test-threads=1`: 4136 passed, 0 failed. (cherry picked from commit 38ddd83)
…ty read Every descriptor install, per-key removal and bulk clear on an ordinary object transitions its ShapeId (#10824), and the prime side already refuses a descriptor-bearing receiver, so the ShapeId compare subsumes the OBJ_FLAG_HAS_DESCRIPTORS test. The GC-kind test becomes a single obj_type byte compare on every target; the packed kind+descriptor i32 word and the native-endian i16 reserved-halfword pair are gone with the flag. (cherry picked from commit 58faf25)
#10828 closed rule 3: for any POINTER-tagged value that passes the tag test, the u32 at payload +4 equals a live object ShapeId only if the cell is a GC_TYPE_OBJECT carrying that shape. The ShapeId compare therefore proves the kind, and the header word at receiver-8 is no longer loaded on the way to the slot. pic.recv_hdr is gone; the compact-word load moves into pic.token. The one key that still reads the kind byte is .size, only to serve a native Map/Set from its own arm; a non-collection receiver takes the ShapeId compare like every other key. (cherry picked from commit bf79b37)
…d's shape-gated hit #10826 made every successful delete a shape transition: the receiver's +4 word always changes and, when the keys array is owned, the predecessor id is retired. A compact word primed before a delete therefore cannot match after it, and a ShapeId hit proves the slot it names is live. Every inline slot is born TAG_UNDEFINED, so nothing but a delete ever writes a hole into one. The hit block now ends in the slot load and a branch to the merge; pic.hit.deleted is gone. The hole stays in the slot: the way path, the spill arm and every keys-array walker keep treating it as absent. (cherry picked from commit 5eb9d1e)
…ip the closure/buffer/typed-array ladder for ordinary objects On a read site that sees many shapes every read is a miss, and the miss handler walked the closure probe (magic read + accessor side table), the buffer registry and the typed-array registry for every heap receiver before reaching the object path: 33% of such a read (measured by #10833), against 3% for the key scan. Those kinds are distinct GC kinds (shape_rule3.rs classifies all 21), so one validated header read decides the ladder; a GC_TYPE_OBJECT goes straight to the object path, which now also takes its kind, descriptor and forwarding bits from that same read instead of three more. The elements-backed Array-subclass probe is gated on the key first. The full-outline twin (pic_outlined_mru_hit) mirrors the three emitted guard removals, and its hit is gated on the exact POINTER tag like the inline one. Two latent defects found by #10833: - js_in_operator_presence_ic resolved its cache before deciding whether it could arm, leaving word 0 at zero for a prototype-chain true; the emitted guard then matched any unstamped receiver (parent_class_id 0), so "k" in {} answered true at such a site. The cache is now resolved with word 0 pre-seeded to IN_PRESENCE_UNARMED (1 << 32) before publication (pic_slot_resolve_init). - keys_find_slot_by_bytes_resolved scanned forward, contradicting #10595's most-derived-wins rule that its two twins already follow; it scans back-to-front now and is pinned by the #10595 test. test-files/test_parity_read_guards.ts is the differential probe for all three guard removals against node. (cherry picked from commit 4a3b92c)
…sentinel test (cherry picked from commit e002ce5)
… the shape word from memory For every key but .length the tag test is the exact POINTER test, so the handle is bits ^ POINTER_TAG rather than bits & POINTER_MASK: the same value on the pointer path, and the value the tag test is computed from, so the mov $0x30/bzhi pair after the compare is gone. The hot ShapeId load now has a single use (the compare): pic.token.miss re-reads the word through an atomic load that GVN will not merge, which lets isel fold the hot load into the compare itself. (cherry picked from commit 31a2471)
An absent-key miss falls through to js_object_get_field_by_name, whose own dispatch still probes the registries for an object receiver (eight probes, measured); that ladder is not this change's, so the test asserts the count only across the own-key miss, which is the read the skip was measured on. (cherry picked from commit 2792581)
…! calls (cherry picked from commit c922629)
…er-primed edge, before calling out A read whose key lives on the prototype chain is never an own slot on the receiver's shape, so a site that only reads such a key never resolves its per-site cache, and every read of it reaches pic.token.ways with `present` false. That edge used to go straight to the exit, where the read paid the slow entry's prologue and dispatch (79 of an inherited read's 204 instructions, measured by the inherited-reads lane) just to reach the same lookup inside get_field_ic_miss_impl. It now asks js_inherited_read_cache_hit_f64(masked receiver, interned key) first (#10834/#10842's cache); TAG_HOLE is its decline sentinel, so the answer is one compare with the served edge as the true edge, and a decline continues to the one exit exactly as before. Nothing primes from emitted code. The call is a pure state read: declared in runtime_decls, a leaf in gc_call_effects and root_reload, and in the dominance checker's NONCOLLECTING set. Placement, measured: asking on every path into the exit charged each own-key miss a declining probe (+88 per read on a 64-shape site, +89 on a spill read). The never-primed edge is the one only an inherited-only site takes, so every other path is unchanged to the instruction. Typed-feedback builds keep the old edge, so their record edges stay byte-identical. The full-outline twin deliberately does not get the hook: it is already inside the runtime, and its miss handler asks the same cache first. (cherry picked from commit 2101c18)
…urvives The census required `cond_br(&is_plain_kind, &tok_label, &cold_label)` in the generic read PIC -- the `obj_type == GC_TYPE_OBJECT` fence in front of the ShapeId compare. #10843 deletes it, and the census refused the tree. It was right to refuse and the removal is right. #10828 closed rule 3 (no pointer-tagged non-object cell holds a ShapeId-range value at payload +4), so a successful shape compare proves the kind by itself and the header load is redundant. Under that object model, asserting the branch asserts nothing about safety. Assert the licence instead: no row of `RULE3_KINDS` in `object/shape_rule3.rs` carries `Rule3Word::RangeReachable`. Stronger on the axis that can regress -- the branch could only notice its own deletion, this notices a NEW kind whose +4 word can alias a live ShapeId, and names it. Guarded against a vacuous pass: the kind set is re-derived from `gc/types.rs` and must be fully classified, stale rows are red, and both the `RangeReachable` variant and the runtime test that walks the table must still exist. Two sabotage arms: reopen rule 3 on `DateCell`, and delete its row. (cherry picked from commit d721436b47c5a78abe3fe9e13290f0bff90741d3)
- object/mod.rs 2030 -> 1979 via an ObjectMeta::flags split (meta_flags.rs) - prototype_chain.rs's new hand-typed handle floor routed through addr_class::is_above_handle_band rather than ratcheting the baseline - two -D warnings failures: an unnecessary unsafe, and non_snake_case on #10846's test name (renamed; emphasis moved to a comment)
Four of five store regexes matched = without excluding ==, so (*meta).elements == 0 was reported as a raw heap pointer field store. RUST_POINTER_FIELD_STORE_RE two lines below already had the (?!=) guard. Not silenced with a marker: GC_STORE_AUDIT on a comparison would document a store that does not exist. Verified still non-vacuous -- a planted real store at the same line is caught (exit 1) and the tree is clean without it.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (37)
✨ 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 21, 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.
Lands #10846, #10842 and #10843 together as v0.5.1629. They touch the same property-read path and could not be separated.
Expedited at the owner's request: merged on the gate set plus targeted tests rather than a full train sweep.
Contents
fix(#10827): an explicitly ended prototype chain ends the READ tooperf(runtime): one validity word replaces the per-hop chain walk; one flags word replaces two registry probesperf(codegen): the generic own-property read is tag test → shape compare → loadIntegration work this train had to do
The
ic_miss.rsconflict. #10843's kind-byte commit re-inserts hook A andlet mut inherited_declinedat the old call site, but #10842 has hoisted both above the async-resource probe. Taking that hunk verbatim compiles fine and silently produces a secondinherited_read_cache_lookupper read plus a shadowed binding. Resolved by keeping #10842's structure and taking only the commit's real contribution — the single validatedtry_read_gc_headerthat replaces three later header reads. Asserted afterwards: exactly oneinherited_declineddeclaration and one hook-A lookup survive.object/mod.rs2030 → 1979 — theObjectMeta::flagsregion (5 constants plus the bit map, 57 lines) moved toobject/meta_flags.rs. The map is what a reader needs in order to add a bit, and the "BIT 7 IS THE LAST FREE BIT" warning is only useful beside the constants it constrains.prototype_chain.rshandle-floor 3 vs baseline 2. #10846's chain walk copied a localtop16 == 0 && bits > 0x10000idiom — a hand-typed handle-band floor, the shape #6321 fixed. Routed throughaddr_class::is_above_handle_bandrather than ratcheting the baseline.gc_store_site_inventory.pyreported a comparison as a store.(*meta).elements == 0is a read; four of the scanner's five store regexes matched=without excluding==, whileRUST_POINTER_FIELD_STORE_REtwo lines below already had the(?!=)guard. Fixed the scanner rather than adding aGC_STORE_AUDITmarker, which would document a store that does not exist, or an allowlist entry, which would leave it over-matching every other reader of those six fields. Proven still non-vacuous: a real unmarked store planted at the same line is caught (exit 1), and the tree is clean with it removed.Two
-D warningsfailures CI'swarningsjob would have rejected: an unnecessaryunsafeininherited_read_cache_tests.rs, andnon_snake_caseon #10846'sa_null_ENDED_interior_prototype_...test (renamed; emphasis moved into a comment — the repo's#[allow(non_snake_case)]precedent is for JS-API thunks, not test names).#10843's census retarget rides along: it asserts rule 3 is closed rather than requiring the
is_plain_kindinstruction that the GC-header-load removal deletes. Verified to discriminate by reopening rule 3 on a kind chosen independently of its own self-test (GC_TYPE_BUFFER) — exit 1 naming the kind, exit 0 restored.Evidence
Twelve gates green:
check_file_size,raw_handle_debt,gc_store_site_inventory,addr_class_inventory,shape_descriptor_census,gc_runtime_root_holders,string_payload_access_inventory,gc_rekeyed_key_tables,native_result_ledger,check_test_registration,cargo fmt --all -- --check, andcargo check -p perry-runtime --all-targetsunder-D warnings.Tests:
inherited_read_cache31 pass,proto_validity10,prototype_chain11. Theobject::suite is 487 pass / 1 fail, and that one failure —transition_fast_rejects_object_prototype_even_with_a_cached_edge— reproduces identically onmainwith zero train commits, so it is not this train's. It is an ordering dependency introduced by v0.5.1627'sresolve_prototype_addrchange: the test passes inside the full suite and fails in isolation, and is tracked separately.Not run, and stated rather than implied: the full release unit suites, the compiler-output suites,
repsel_census, and the gap sweep.