Repository navigation
cc --help parity gate down on main: defineProperty-on-non-object at startup — window pins #9326 (indexed writes with a recorded custom array prototype) #9341
Description
Activity
Revert merged (#9345) —
mainshould be back to a workingcc --help.I merged #9326, so this one is on me, and the miss is worth stating precisely because it wasn't a gap in the analysis of the change itself.
I validated #9326 against the gap fixture it targets (
test_gap_9220_9221_array_proto_paths.ts, byte-identical to the pinned oracle), theperry-runtimesuites, the full lint set, and a clean release build. All green. What I did not do was run thecc --helpparity workload — which #9323 had restored to service three merges earlier, in a PR I had reviewed and merged myself that same evening.That is the actual lesson here: a runtime-semantics change to indexed writes is exactly the kind of thing that primary workload exists to catch, and the fact that the gate had just come back online is what should have made me reach for it. A green gap fixture and a green unit suite were never going to see this — as #9341 notes, the runtime lib suite is green at the failing tip too.
One clarification on scope, since #9326 was #9297 plus an adjustment of mine. My part was moving
array_spec_setback intoindexing.rsso its seven pre-existing raw-handle sites kept their existing ceiling, plus apub(crate)visibility bump and an import fix — a pure relocation with no semantic change, which compiled and tested clean. The behavioural change in the window is #9297's own: honouring a recorded custom array prototype on indexed writes. I mention this only so the fix-forward looks in the right place, not to move the responsibility for merging it.Happy to help with the diagnosis when someone picks this up.
Object.defineProperty called on non-objecton a path that now consults a recorded array prototype suggests the new lookup is answering for a receiver the old one declined, and the defineProperty path is then handed something that isn't an object — but that's a hypothesis, not a finding, and the preserved known-good/known-bad binaries at/root/claude-shapedesc/are a better starting point than my guess.A measured finding that matters beyond this issue: the gap suite cannot see this regression.
I built the pre-revert commit (
6bc775d9b9, containing #9326) as a known-bad binary and ran the full 609-test gap suite against it, then the same suite against post-revertmain. The two runs are indistinguishable:known-bad (#9326 present) post-revert main Parity pass 604 604 Parity fail 5 5 failing tests same 5 same 5 verdict Gap snapshot OKGap snapshot OKexit 0 0 The 5 failures are the recorded known set in both cases. I also wrote targeted
definePropertyprobes — prototype index setters,Array.prototypeindex installation, defineProperty on values read back through a custom-prototype array, sparse reads, growth through the custom-prototype path — and every one is byte-identical to node on the known-bad build.So the detection surface for this class is currently:
perry-runtime --libgreen, 609-test gap suite green, hand-written probes green, andcc --helpthe only thing that fails. That is a single point of detection for a path that changes indexed-write semantics.Two consequences worth acting on:
This is why #9326 landed. I validated it against its target gap fixture, the runtime suites, the lint set and a clean build — all green, and now demonstrably all incapable of catching it. The lesson I drew earlier ("I should have run
cc --help") is right but incomplete: no locally available gate would have caught it either.A fixture for this would close a real hole. Whatever minimal shape reproduces the
Object.defineProperty called on non-objectfailure is worth adding totest-files/as a gap test, because the class it represents — prototype-sensitive[[Set]]routing — is exactly the kind of change that gets made often and can currently land green. The preserved binaries at/root/claude-shapedesc/are the obvious starting point; my probes above did not find the shape, so it likely needs something structural the bundle does that a small fixture has to reproduce deliberately.I have held #9339 back for the same reason and said so there — it reintroduces the sole call site of the reverted path, and a green gap run on it would not be evidence of anything.
Negative result, posted so nobody repeats it: I tried to minimise this into a fixture against a purpose-built known-bad binary (
6bc775d9b9, containing #9326) and could not reproduce it. Three rounds, all byte-identical to node on the broken build:- Direct shapes —
definePropertywith a prototype index setter, installing an index onArray.prototype,definePropertyon non-objects (null, a number) to check the TypeError path itself. - Prototype + growth —
definePropertyapplied to values read back out of an array with a custom prototype, including after 40 indexed writes that grow it, and via a sparse read at index 100. - Bundler idioms — the esbuild/tsup
__defProp/__export/__copyPropshelpers over a module registry held in an array, the same with a custom-prototype registry, and the whole thing again inside aPromise.resolve().then(...)chain to match the "Uncaught (in promise)" framing.
So the trigger is something more specific than "defineProperty near a prototype-sensitive indexed write". That narrows it usefully: it is unlikely to be the plain helper shapes, and more likely to need something structural the bundle does — the wasm runtime path, a deeper prototype chain, or a receiver that only becomes non-object after a particular sequence.
Whoever picks this up is far better placed than I am: the known-good/known-bad binaries at
/root/claude-shapedesc/plus the bundle allow bisecting the input rather than guessing the shape. A--trace hir --focuson the frame that callsdefinePropertyin the failing binary would probably identify the receiver in one pass.Standing conclusion from the earlier comment is unchanged: the 609-test gap suite returns an identical verdict on the known-bad and known-good builds, so this class currently has exactly one detector, and it is not one that runs locally.
- Direct shapes —
Correction: #9326 was NOT the culprit. The revert (#9345) is merged and
cc --helpstill fails identically.Measured on
e539a6359— which contains the revert (14fcf6b942) — freshly built and compiled on perrymaster:=== PARITY GATE === perry rc=1 node rc=0 < Uncaught (in promise) TypeError: Object.defineProperty called on non-objectSame error, same rc, 5/5. So the revert removed correct work and fixed nothing, and the real culprit is still on main.
Where the attribution went wrong
The elimination rested on "#9323's fix branch was verified cc-green and contained everything through ~#9322." That premise was false, and I had already disproved it before accepting the conclusion: the #9323 work was based on
d6794cf771(#9313), which does not contain #9300, #9315 or #9317 — I checked exactly that withgit merge-base --is-ancestorand recorded "window is bigger than #9326," then let a second, independently-derived elimination override my own contradicting check instead of reconciling the two.The lesson, stated plainly because it is the reusable part: when a second line of evidence contradicts a check you have already run, the disagreement is the finding — reconcile it before acting. Two confident attributions agreeing on a wrong answer is exactly what "independent confirmation" is supposed to prevent, and it did not, because both inherited the same false premise about what the verified-good point contained.
The real window
Last verified-green:
83754818e(#9242, per the independent shapedesc build) /d6794cf771(#9313, the #9323 base).
Broken: current main.Candidates between them (all merged since), several of them mine:
#9312JSON tape gate ·#9314ELF/scrypt ·#9300GC map v5 ·#9315own-keys enumeration ·#9316strided fill kernel ·#9318affine index bound ·#9317tombstone old_carrier ·#9319mysql2 ·#9320/#9321/#9322lint/green fixups ·#9326**(now exonerated by the revert)`.On the error's own evidence,
#9315deserves first look — it changed class-member registration and own-keys enumeration, and the failure is adefinePropertyreceiver arriving as a non-object during help-path module init.#9300(GC map) and#9317(shape publish) are the other two runtime-semantics candidates.A proper bisect over that window is starting now. #9297's content should be re-landed once the true culprit is found — the revert bought nothing and #9220/#9221 remain real bugs.
Interim: three eliminations, one corrected artifact, and the throw site narrowed to one of two.
Posting before the attribution is finished because several of these are immediately reusable.
1.
/root/claude-keep/app_Dis not a reproducer of this bugIt is worth stating plainly, because it has been passed around as "the known-bad binary":
$ HOME=/tmp/cc_home /root/claude-keep/app_D --help Segmentation fault (core dumped) rc=139app_Dsegfaults. It never prints the TypeError. Anyone differentially debugging #9341 against it is chasing the pre-#9323 longjmp segfault, not this issue.The binaries that actually reproduce #9341 are:
binary SHA result /root/claude-shapedesc/app_basea03be729c(#9336)rc=1, Object.defineProperty called on non-object/root/claude-arch/app_r7(later tip) rc=1, same /root/claude-shapedesc/app_base383754818e(#9242)rc=0, 9175 bytes, byte-identical to node /root/claude-keep/app_A— rc=0, 9175 bytes So the confirmed bracket is green
83754818e→ brokena03be729c, anda03be729cpredates #9331/#9332/#9333/#9340. (measure_sd2.shalso records9943dd95c(#9328) as already failing, which is release-only — consistent with the culprit being at or before #9326's merge point.)2. The failure is deterministic and completely GC-independent
Five kill switches on the reproducing binary, all 5/5 identical failures:
configuration result default TypeError PERRY_GC_MOVING_LOOP_POLLS=0TypeError PERRY_GC_HEAP_LIMIT=4096TypeError PERRY_LAZY_STACK_MAPS=0TypeError PERRY_STACKMAP_WALKER=unwindTypeError PERRY_OBJECT_TOMBSTONES=0TypeError PERRY_LAZY_STACK_MAPS=0+PERRY_GC_MOVING_LOOP_POLLS=0TypeError Turning the moving collector off and raising the heap limit does not change the failure by one byte. This is not a GC or rooting bug. That eliminates, as mechanisms:
- perf(gc): v5 GC map — find one function's records without decoding the section (index build 63.5x less work, −214 MB RSS) #9300 (GC map v5) —
PERRY_LAZY_STACK_MAPS=0restores the v4-style eager path and the failure is unchanged; - fix(gc): tombstone shape publish arms old_carrier — deleted receivers no longer lose their keys array under evacuating GC (#9200) #9317 (tombstone publish arms
old_carrier) — its entire subject is a receiver's keys array under an evacuating minor GC, and there is no evacuating GC dependence here; - perf(runtime): tombstone deletes default-on again — the #9200 corruption that rolled them back is fixed (26x on populated delete) #9331 (tombstones default-on) — not in
a03be729cat all, and the kill switch is inert.
It also says the receiver is not a corrupted object. It is a value that was never object-like on this path.
3. There are exactly two throw sites — and it is the second one
crates/perry-runtime/src/object/object_ops/define_property.rs:429 crates/perry-runtime/src/util_inherits.rs:134 (set_super_property)Both emit the identical byte string, so the message alone cannot tell them apart. I found a genuine 4-line reproducer for the
util_inheritsone:const util = require("util"); class Base2 {} class Derived2 {} util.inherits(Derived2, Base2); console.log(typeof Derived2.super_, Derived2.super_ === Base2); // node: function true // perry: TypeError: Object.defineProperty called on non-object rc=1
But it fails identically on the green
83754818ebuild. It is a pre-existing gap, not this regression — filed separately below. Since83754818ecompilescc --helpbyte-identical to node, the bundle cannot be reaching that site. The cc throw isdefine_property.rs:429: a receiver that is neither a registered class-ref, nor object-like, nor a small handle.That matters for where to look. The guard is
let target_is_class_ref = super::super::class_ref_id(obj_value).is_some(); if !target_is_class_ref && !value_is_object_like(obj_value) { … throw … }
and
class_ref_idreturnsSomeonly ifis_class_id_registered(class_id). A class-ref whose id is not inREGISTERED_CLASS_IDSand has no vtable entry is an INT32-tagged non-pointer, so it failsvalue_is_object_likeand throws precisely this message. That is consistent with the earlier session's gdb note that the receiver "resolves via helper to a class-ref".4. What I could not reproduce in isolation (35 shapes, all byte-identical on the broken compiler)
So that nobody re-runs these: on the broken
a03be729ccompiler, all of the following are byte-identical to node —definePropertyon empty classes, static-field-only, getter-only, method-only, class expressions (named and anonymous), subclasses, classes returned from a factory, class prototypes, symbol-keyed classes, static-method-only;getOwnPropertyNameson constructors and prototypes;typeofon class refs; and the whole of #9315's changed computed-symbol lowering surface ([Symbol.iterator]generator and non-generator,[Symbol.asyncIterator],[Symbol.toPrimitive],[util.inspect.custom], static[Symbol.hasInstance],[Symbol.toStringTag]getter and method, and a mixed class where a computed member precedes static/accessor/method members). Also byte-identical: the realutil.inheritsshapes the bundle actually uses — delayed-stream, combined-stream, EventEmitter subclassing,zlib.Inflateas superCtor, andCtor.prototype = {…}wiring.A diagnostic runtime build (receiver bits, tag, class id,
is_class_id_registered, and the property key at the throw) is building now; onecccompile with it should name the failing receiver and key directly.
Unasked finding, worth its own issue:
util.inherits()with an ES class constructor throwsset_super_propertyhandles a closure ctor and an object ctor, and throws for everything else. A class constructor reaches it as an INT32-tagged class-ref, which is neither — soutil.inherits(SomeClass, X)throwsObject.defineProperty called on non-objectwhere Node setsSomeClass.super_. Reproducer is the 4 lines above; it fails on83754818eas well as on tip, so it is long-standing and unrelated to this regression. Note the asymmetry:util.inherits(function(){}, SomeClass)is fine — only a class in the first position fails. The fix is presumably to route a class-ref ctor through the class static-property store the waydefine_property.rsalready does viaclass_ref_id. Happy to file it with the fixture if nobody else has.- perf(gc): v5 GC map — find one function's records without decoding the section (index build 63.5x less work, −214 MB RSS) #9300 (GC map v5) —
Still broken on tip. #9340 did not fix it, and the revert of #9326 did not fix it.
Fresh build at
0f95fbc9f(current main — contains #9340 "preserve array prototypes across growth", #9351, #9337, the #9345 revert, and everything else):LIBS_RC=0 BIN_RC=0 CC_RC=0 PARITY: 0/5 byte-identical (rc=0) Uncaught (in promise) TypeError: Object.defineProperty called on non-objectState of the attribution
- Green:
83754818e(fix(release): clear field-shadow and WebAssembly parity blockers #9242) — independently confirmed byte-identical to node. - Broken:
0f95fbc9f(tip), and every point tested in between. - fix(runtime): indexed writes honour a custom array prototype (from #9297) #9326 exonerated — its revert (Revert #9326 — it breaks cc --help on main (defineProperty TypeError); fixes #9341 #9345) is merged and the break persists. That revert removed correct work (fix(runtime): indexed writes and the generic array engine honour a custom prototype chain #9297, fixing arr[i] = v never observes an inherited index accessor or non-writable inherited index — the strict store's fast lanes bypass array_spec_set #9220/Array.prototype.{join,indexOf,map,forEach}.call(arr) does not fill holes through a custom prototype #9221) and should be re-landed once the true culprit is known.
- fix(runtime): preserve array prototypes across growth #9340 exonerated as a fix; it remains a candidate culprit, having landed inside the window.
Remaining candidates: #9312, #9314, #9300 (GC map v5), #9315 (class own-keys enumeration), #9316, #9318, #9317 (tombstone shape publish), #9319, #9320/#9321/#9322, #9337, #9340, #9349–#9358.
A bisect over that range is running, along with fixture probes against paired green/broken builds — a minimized fixture is the higher-value deliverable and will be posted here as soon as it exists, with or without the commit attribution.
What this costs, stated plainly
cc --helpis the project's primary real-application parity workload. It has now been broken onmainfor the better part of a day, which means every cc measurement any session takes against main is void, and any branch measured on top of it appears to be the cause. This is the third such episode this week (#9169's window, #9305, this one) and the direct argument for the pre-merge cc parity gate proposed in #9346 — every one of the three shipped with its author's fixtures and unit suites green, and only the bundle catches the failing shape.- Green:
Status + one correction to my own earlier post, because it contradicts something I wrote above.
Reconciliation first
My previous comment listed
emitter-fire,zlib-inherits,protoobj-inheritsand ~35 other shapes as "byte-identical to node on the broken compiler". That is exactly what they are — identical, therefore useless as bisect fixtures. They do not differentiate green from broken; they differentiate nothing. I am stating that plainly because a list of green probes reads like progress and is not.The two shapes I did find that diverge from node both fail on the green build too:
fixture green 83754818ebroken a03be729cverdict util.inherits(SomeClass, X)→ TypeErrorfails fails pre-existing → #9362 factory class D extends <param>chained 2 levels → SIGSEGVfails fails pre-existing → #9364 So: I do not yet have a fixture that separates green from broken. That is the thing still missing, and I am not going to present either of the above as if it were one.
Confirmed: still broken on tip
Independently confirmed by the parallel run on
0f95fbc9f—CC_RC=0, thenPARITY: 0/5. #9340 did not fix it. The bracket is83754818egreen → tip broken, still live.The window is 52 commits, not 15
Worth correcting, because the candidate list in circulation (#9312, #9314, #9300, #9315, #9316, #9318, #9317, #9319, #9320-#9322, #9326) is only its tail.
git rev-list --count 83754818e..a03be729c= 52. The head of the window contains, among others:9ee99197c #9250 inline array-store tier covers tagged receivers 322b6c1e4 #9257 receiver-region model + equivalence lint c1c8623ed #9270 use live receiver in stable packed loops 6d08afc29 #9272 inline address window in front of the registry probes 3a9b203ad #9284 keep shape barriers in source modules 87270e6f2 #9291 Bloom filter for the heap-allocated registries; #9225 linear scan gated 0515179a4 #9295 classify prototype wiring as class-default 986f4e121 #9296 preserve renamed namespace constructor args 730e0a997 #9302 overflow-slot properties prime the constant-key ICsSeveral of these are much closer to "a receiver arrives as a non-object" than anything in the tail. Anyone reasoning about this window should be using the full 52.
A narrowing that is evidence rather than a hypothesis
The failing throw is
define_property.rs:429(established above: the only other site isutil_inherits.rs, and that one fails on green, which compilescc --helpbyte-identical). The guard that reaches it is exactly:let target_is_class_ref = class_ref_id(obj_value).is_some(); if !target_is_class_ref && !value_is_object_like(obj_value) { … is_small_handle fallback … throw }
value_is_object_like=typed_array_addr_from_value∨is_valid_obj_ptr∨is_closure_ptr;class_ref_id= tag0x7FFE∧is_class_id_registered.Asking git which of the 52 commits touch any file implementing those predicates gives exactly two:
$ git log --oneline 83754818e..a03be729c -- <the predicate files> 6d08afc29 #9272 closure/dynamic_props.rs (is_closure_ptr: CLOSURE_MAGIC tag read hoisted above the arena check) 8b2cfe6e7 #9315 class_registry/registration.rs (additions only; is_class_id_registered itself unchanged)descriptor_helpers.rs,addr_class.rs,class_ref_values.rs,define_property.rsandclass_meta.rsare untouched across the whole window. So if the receiver is a closure that stopped being recognised, #9272 owns it; if it is a class ref whose id stopped being registered, the registration call sites (codegen/HIR) own it rather than the predicate. That does not name the culprit, but it is a real constraint rather than a prior. (I read #9272'sis_closure_ptrchange closely — it is a pure reordering of a conjunction, and I could not fault it on inspection. Inspection is not measurement.)What the receiver can actually be
Instrumenting
Object.definePropertyon the realnode cli_2.1.112.js --helprun (patching the builtin and filtering by stack to calls originating in the bundle) gives 5,177 distinct call shapes, of which the dominant ones are zod v4's$constructor:[651] object ZodString key=_zod @ $ZodType.z (cli.js:68:78640) [540] object ZodOptional key=_zod @ $ZodType.z [161] CLASS (class A extends Y{}) key=name @ b1 (cli.js:68:78939) [161] function O key=init / Symbol(Symbol.hasInstance) / name @ b1$ZodType.zisObject.defineProperty(inst, "_zod", …)whereinst = params?.Parent ? new Definition() : this— i.e. the receiver isthis, which lines up with the earlier session's gdb note that the receiver "resolves via helper to a class-ref". That is the single most-executeddefinePropertyreceiver on the help path, so it is where I would expect the failure to sit.I could not reproduce it in isolation: faithful reductions of
$constructor(with and withoutParent, chained 3 deep, 200 evaluations,Symbol.hasInstance, per-evaluation class identity, 500 repeated class evaluations) are node-identical on the broken compiler, and the real zod 4.4.3 (spike/tests/02-zod.tsagainst a realnode_modules) compiles and runs byte-identically on green and broken. So whatever it is needs more of the bundle's context than a reduction reproduces — which is why I stopped reducing and went to instrumentation.In flight
A diagnostic runtime built at
a03be729cprints, at thedefine_property.rs:429throw, the receiver's raw bits, NaN-box tag, class id,is_class_id_registered, pointer/undefined/null predicates and the property key — with theutil_inheritssite given a distinct marker so the two can never be confused again. Validated first on fixtures that hit each site deliberately (a diagnostic that cannot fire teaches nothing). Thecccompile with it is running now; it should name the receiver and the key directly.Method note for whoever picks this up next
/root/claude-keep/app_Dsegfaults rather than reproducing this bug (see my earlier comment) — and separately, twocc-scale compiles were running concurrently on perrymaster earlier today with available memory at 0 GB. Acccompile peaks around 29 GB, and an OOM kill leaves a plausible-looking binary that onlyrc=137reveals. Worth gating onfreebefore starting one.Root cause identified. The failing call is
Object.defineProperty(undefined, "getContentType", …)— a class's.prototyperead is returningundefined.The measurement
A diagnostic runtime built at
a03be729c, instrumenting thedefine_property.rs:429throw (and giving theutil_inherits.rssite a distinct marker so the two can never be confused). Onecccompile, one--helprun:TEMPDIAG9341 DP bits=0x7ffc000000000001 tag=0x7ffc cid=0 proto_flag=false registered=false is_ptr=false is_undef=true is_null=false key=Some("getContentType") Uncaught (in promise) TypeError: Object.defineProperty called on non-objectExactly one such throw in the whole run. The receiver is
undefined— tag0x7FFC,is_undef=true. It is not a class ref, not a closure, not a handle, not a corrupted pointer. It is a value that should have been an object and isundefined.That kills three standing theories outright, including one of my own: it is not an unregistered class ref (
cid=0,registered=falsebecause there is no class id at all), not a closure the predicate stopped recognising, and not theutil_inheritssite.Where it is in the bundle
The key names it.
getContentTypeis generated by axios'sAxiosHeaders.accessor, which incli_2.1.112.js(line 68) is:static accessor(q){ let _=(this[lb7]=this[lb7]={accessors:{}}).accessors, z=this.prototype; // <-- z is undefined function Y(A){let O=VU6(A);if(!_[O])xp5(z,A),_[O]=!0} return H1.isArray(q)?q.forEach(Y):Y(q),this } ... kU6.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);
xp5(obj, header)is axios'sbuildAccessors, which doesObject.defineProperty(obj, "get"+Name, …)forget/set/has.Content-Typeis the first entry andgetis the first method, sogetContentTypeis the first accessor built — i.e. this fails on the very first call, which is why there is exactly one diag line.So the receiver
xp5gets isz, andz = this.prototypeevaluated toundefinedinside a static class method wherethisis the class.Restating the bug in one line: reading
.prototypeoff a class returnsundefinedin the compiled bundle. Everything downstream — the TypeError, the "in promise" framing, the 0-byte--help— follows from that.Two corroborating facts
--versionworks on every broken build (rc=0, 21 bytes, byte-identical to node) while--helpfails. The failure is on the command/option-tree construction path that--versionshort-circuits past.- Under
PERRY_FOR_IN_DIAG=1the broken binary emits only 4 for-in checks before dying, so this is early on that path, not deep in help rendering.
What does not reproduce it (so nobody repeats this)
I reduced the axios shape faithfully and it is byte-identical to node on the broken compiler, and identical between the green and broken builds:
this.prototypeinside a static method — fine;Object.defineProperty(this.prototype, …)inside a static method — fine;- the full
AxiosHeaders.accessorshape: symbol-keyed static writethis[SYM]=this[SYM]={accessors:{}},let z=this.prototypecaptured by a nested named function,Array.isArray(q)?q.forEach(Y):Y(q),thiscomma-sequence return,["get","set","has"].forEachinside the builder, over the real six-header list — fine, accessors built and callable; - the same as a class expression assigned to a variable and aliased — fine;
- with and without the symbol write, the comma sequence, the closure capture — all fine;
- real axios is not the only consumer: real zod 4.4.3 (
spike/tests/02-zod.tsagainst actualnode_modules) compiles and runs byte-identically on green and broken.
One genuine divergence turned up on the way —
C.accessor = function(){ return this.prototype }returnsundefinedin perry and the prototype object in node — but it fails on the green build too, so it is pre-existing and not this regression. (Filed as part of the sweep below.)The reduction gap is itself informative: the shape is correct but the context is not. A property read that is correct cold and wrong warm is the signature of an inline cache, which is where I would look first.
Which commits can do this
Asking git which of the 52 window commits touch the class-
.prototyperead path (object/field_get_set/,object/class_registry/,object/prototype_chain.rs) gives exactly four:87270e6f2 #9291 Bloom filter in front of the heap-allocated registries; #9225's linear scan gated 730e0a997 #9302 overflow-slot properties prime the CONSTANT-KEY ICs (27 -> 5 ms) d6794cf77 #9313 keep main green after the #9302/#9303/#9307 batch 8b2cfe6e7 #9315 class-prototype own-keys enumerationthis.prototypeis a constant-key property read, and #9302 changes how constant-key ICs are primed — a warm-only wrong answer is exactly what that can produce, and it is exactly what the reduction gap points at. #9313 exists specifically to "keep main green after the #9302 batch", which says that batch had fallout. #9291's filter is release-only in effect (its audit is#[cfg(debug_assertions)]), which would explain invisibility to every debug-run suite.I am not claiming any of these yet — this list is what the failing path can reach, not evidence.
In flight
A build at
d6794cf77(#9313) with #9323's longjmp fix cherry-picked (it applies cleanly — #9323's own base isd6794cf77) is compiling now. The cherry-pick matters: every point below #9323 segfaults oncc --helpfor the unrelated reason #9323 fixed, so without it a bisect step below that line answers a different question.That point splits the four candidates cleanly:
- green ⇒ culprit is in fix(link,crypto): ELF archive grouping and scrypt cost parameters (from #9311) #9314…fix: restore three lint gates that went red during the merge batch #9322, where fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0) #9315 is the only one of the four;
- broken ⇒ culprit is at or before fix: keep main green after the #9302/#9303/#9307 batch #9313, i.e. perf(runtime,codegen): overflow-slot properties prime the constant-key ICs (27 → 5 ms, 5×) #9302 or perf(runtime): a Bloom filter for the heap-allocated registries, and #9225's linear scan gated — cc --help −1.25% instructions, −2.37% cycles #9291 — and it also disproves the "verified green at fix: keep main green after the #9302/#9303/#9307 batch #9313" premise that this investigation has been leaning on unverified since the start.
Either answer is worth having; the second especially, because that premise is the one that produced both previous wrong attributions.
Sweep findings filed separately
- util.inherits(SomeClass, …) throws "Object.defineProperty called on non-object" — set_super_property has no class-ref arm #9362 —
util.inherits(SomeClass, …)throws the identical message (set_super_propertyhas no class-ref arm). Pre-existing; matters here because the two throw sites emit the same bytes. - SIGSEGV: factory returning
class D extends <param>, chained two levels, then instantiated (zod v4 $constructor shape) #9364 — SIGSEGV on a factory returningclass D extends <param>chained two levels then instantiated (the zod v4$constructoridiom). Pre-existing on green and broken.
Two more filings from the reduction sweep, both pre-existing (identical on green
83754818eand brokena03be729c) but both in the exact machinery this regression lands on:- Own property named
prototypeis silently dropped when written through a function parameter (and after a dynamic-key write) #9365 — a statically-namedo.prototype = vstore is silently dropped after a single computed-key write on the receiver. Three lines. Scoped precisely: of 14 stores (7 names x static/computed), onlyo.prototype = vin its static-name form is discarded;o["prototype"] = vand every other name survive. - Exported class EXPRESSION reads
.prototypeasundefinedfrom an importing module (class declarations are fine) #9366 — an exported class expression reads.prototypeasundefinedfrom an importing module, while a class declaration is fine. Two files. This is the shape of the failing site: axios'skU6 = class { ... static accessor(q){ let z = this.prototype; ... } }is a class expression.
Together with #9362 and #9364 that is four distinct ways
.prototypehandling answersundefinedor crashes on green, all silent. That is the reason this bug cost three wrong attributions: the failure mode is a silentundefined, so it surfaces hundreds of instructions later as adefinePropertytype error with no connection to its cause. A store that cannot be honoured should not be discarded silently — making #9365 loud would likely have turned #9341 into a one-hour bug.To be explicit about what is still missing: none of these four is the regression, and I do not yet have a fixture that separates green from broken. Roughly 90 shapes tried (full axios and zod
$constructorreductions, real zod 4.4.3, esbuild lazy-init class expressions, per-evaluation class identity, forced codegen-unit splitting at 2/8/32 units) are either identical across the two builds or diverge from node on both. The bisect is the path; a build atd6794cf77(#9313) + #9323's longjmp fix is in itscccompile now.- Own property named
Bisect step 1:
d6794cf77(#9313) + #9323's longjmp fix is GREEN — 3/3 byte-identical to node.p9313 d6794cf7719165dcc2cb06ba03265646981ec32b GREEN (3/3)Two things follow.
1. The premise everyone has been leaning on is now actually verified. "#9323's branch was cc-green on base
d6794cf77" has been load-bearing since the first attribution and had never been independently rebuilt — it is the claim whose containment half was false and which produced the #9326 misattribution. Its greenness half holds: freshly built and compiled here, that content is byte-identical to node.The cherry-pick matters and is worth stating: every point below #9323 segfaults on
cc --helpfor the unrelated reason #9323 fixed, so a bisect step down there without it answers a different question. #9323's own base isd6794cf77, so it applies cleanly.2. The window is now
#9312 … #9336, i.e. the commits betweend6794cf77and the confirmed-brokena03be729c:69b068a23 #9312 JSON.parse<T[]> tape gate 9c8cdfc2e #9314 ELF archive grouping + scrypt 015ec5fe1 #9300 GC map v5 8b2cfe6e7 #9315 class-prototype own-keys enumeration 19460b717 #9316 strided constant-fill kernel b96f2fb0d #9320 lint/green fixups 124b38ecc #9318 affine index magnitude bound 804663479 #9317 tombstone shape publish / old_carrier f850ec1df #9319 mysql2 prepared statements 192ff13d2 #9321 rustfmt cb0102fb1 #9322 lint gates 1f8ad06fa #9326 (exonerated — its revert is on main and the break persists) 9943dd95c #9328 release: macOS dependency cache 21f1c4c15 #9329 dyn-IC transition fast path a03be729c #9336 keep main green after #9329Against the evidence already in hand, #9300 and #9317 are out as mechanisms (the failure is completely GC-independent — five kill switches, all identical), and #9315 is the only commit in this list that touches the class-
.prototyperead path the diagnostic implicates. So the coordinator's original prior on #9315 now has evidence under it rather than intuition — but that is still a prior, and I am not going to call it before the build says so.Step 2 is running:
015ec5fe1(#9300), which is #9315's immediate parent.- GREEN ⇒ culprit is at or after fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0) #9315, and given the list above that is fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0) #9315 itself with high probability — one more build to confirm.
- BROKEN ⇒ culprit is runtime: JSON.parse<T[]> routes through the shared tape gate — typed roundtrip beats node (526→132ms) #9312, fix(link,crypto): ELF archive grouping and scrypt cost parameters (from #9311) #9314 or perf(gc): v5 GC map — find one function's records without decoding the section (index build 63.5x less work, −214 MB RSS) #9300, and fix(runtime): class-prototype own-keys enumeration — accessors listed, symbols real, spec order (36 diverging fixture lines → 0) #9315 is exonerated.
Either way the next step after it is decisive. Infrastructure, fixtures, eliminations and the decision tree are written up in
/root/claude-9341b/README.mdandNEXT_STEPS.txton perrymaster so this is resumable by anyone.The minimized fixture — green passes, broken throws the exact #9341 error
class H1 { set(k, v) { this[k] = v; return this; } toJSON() { return { a: 1 }; } [Symbol.iterator]() { return Object.entries(this.toJSON())[Symbol.iterator](); } static accessor(list) { const z = this.prototype; list.forEach((h) => { Object.defineProperty(z, "get" + h, { value: function () { return h; }, configurable: true }); }); return this; } } H1.accessor(["ContentType"]); console.log(typeof H1.prototype.getContentType);
node : function perry 83754818e (#9242) : function <- GREEN perry a03be729c (#9336) : TypeError: Object.defineProperty called on non-object <- BROKENSame message, same site, no bundle required. ~2 minutes per arm instead of a 25-minute
cccompile.The control that makes it a bisect fixture
Delete one line — the computed
[Symbol.iterator]()member — and the identical class works on the broken build:class H2 { // no computed member set(k, v) { this[k] = v; return this; } toJSON() { return { a: 1 }; } static accessor(list) { const z = this.prototype; /* …same… */ } } H2.accessor(["ContentType"]); // broken build: "function" ✔
So the trigger is: a non-generator computed
[Symbol.iterator]()member in a class, followed by a static method that readsthis.prototypeand hands it toObject.defineProperty. With the computed member,this.prototypeinside the static method becomes a non-object; without it, it is the prototype.It also reproduces with the class as an expression assigned to an outer variable (
H3in my fixture), which is the form the bundle uses.Why that is exactly the bundle
axios's
AxiosHeadersis that class, member for member:kU6 = class { … [Symbol.iterator]() { return Object.entries(this.toJSON())[Symbol.iterator](); } // non-generator, computed static accessor(q){ … let z = this.prototype; … xp5(z, A) … } // z === undefined }; kU6.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);
and
xp5=buildAccessors, whose firstdefinePropertyisgetContentType— the exact key the diagnostic reported.Attribution so far
point fixture 83754818e(#9242)passes 015ec5fe1(#9300) — immediate parent of #9315 — + #9323's longjmp fixpasses a03be729c(#9336)fails And at the
cc --helplevel,d6794cf77(#9313) + #9323's fix is GREEN 3/3 byte-identical.8b2cfe6e7(#9315) is building now. If it fails the fixture, the attribution is complete: green at its parent015ec5fe1, broken at it.#9315 is the prior mechanism too, and this fixture is precisely the surface it changed. #9315 narrowed
generic_computed_member_keyso that only three shapes keep the special lowering — a generator[Symbol.iterator], a static[Symbol.hasInstance]method, and a[Symbol.toStringTag]getter:let needs_special_lowering = (well_known == Some("iterator") && method.function.is_generator && !method.is_static) || (well_known == Some("hasInstance") && method.is_static && …) || (well_known == Some("toStringTag") && !method.is_static && …);
Before that change,
is_symbol_iterator_key(&computed.expr)matched any[Symbol.iterator]member. So a non-generator[Symbol.iterator]()— axios's exact form — moved from the special path into the generic computed-member path, and the generic path setsseen_generic_computed_member, which gates how the members after it register (can_source_order_register). The staticaccessormethod that follows is one of those members.That is a hypothesis about the why; the build will settle the whether.
Please add this to
test-files/This is the fixture that has been missing all week — the class this bug belongs to (a computed class member changing how later members register) can currently land green through every local gate:
perry-runtime --lib, the 609-test gap suite, hand-writtendefinePropertyprobes and the real libraries all pass on the broken build. I verified separately that real zod 4.4.3 and ~90 hand-built shapes are byte-identical across green and broken; only this one separates them.Worth pinning both arms — the failing class and the
H2control — since a fixture that only asserts the failing case cannot show that the computed member is the trigger.Culprit: #9315 (
8b2cfe6e7) — green at its parent, broken at it015ec5fe1 #9300 (immediate parent of #9315) + #9323 longjmp fix fixture PASSES <- green parent 8b2cfe6e7 #9315 + #9323 longjmp fix fixture FAILS <- broken atBoth arms built with the identical recipe and the identical cherry-picked #9323 fix, and both tested with the same fixture. The failure at
8b2cfe6e7is the exact #9341 message:H1-static-this-prototype => object H1-accessor !! Object.defineProperty called on non-object <- #9341 H1-iterate => [["a",1]] H2-no-computed-accessor => function <- control, still works H3-expr-computed-accessor !! Object.defineProperty called on non-object015ec5fe1is8b2cfe6e7's immediate parent, so the window is closed to one commit.Full evidence chain
point what was measured result 83754818e(#9242)cc --helpGREEN, 9175 bytes byte-identical 83754818e(#9242)fixture passes d6794cf77(#9313) + #9323cc --helpGREEN 3/3 byte-identical 015ec5fe1(#9300) + #9323fixture passes 8b2cfe6e7(#9315) + #9323fixture FAILS — Object.defineProperty called on non-objecta03be729c(#9336)cc --help+ fixturefails both 0f95fbc9f(tip)cc --helpfails A
cc --helpbuild at015ec5fe1is finishing now as independent confirmation at the bundle level; the fixture chain above already closes the attribution.Mechanism
#9315 narrowed
generic_computed_member_keyincrates/perry-hir/src/lower_decl/class_decl.rs. Before, any well-known-symbol computed key kept the special lowering:if is_symbol_iterator_key(&computed.expr) || is_inspect_custom_key(ctx, &computed.expr) || symbol_well_known_key(&computed.expr).is_some() { return None; }
After, only three shapes do:
let needs_special_lowering = (well_known == Some("iterator") && method.function.is_generator && !method.is_static) || (well_known == Some("hasInstance") && method.is_static && matches!(method.kind, MethodKind::Method)) || (well_known == Some("toStringTag") && !method.is_static && matches!(method.kind, MethodKind::Getter));
Note the
is_generatorconjunct. A non-generator[Symbol.iterator]()— which is exactly axios's form — therefore moved from the special path into the generic computed member path. The generic path setsseen_generic_computed_member, which gatescan_source_order_registerfor every member declared after it. In the failing class the member after it is thestatic accessormethod, and inside that methodthis.prototypecomes back a non-object.The control in the fixture isolates it: delete only the
[Symbol.iterator]()line and the same class works on the broken build.That this reached
cc --helpand nothing else is consistent with the shape being uncommon — a computed well-known-symbol member followed by a static method that reflects onthis.prototype. axios'sAxiosHeadersis that class, andgetContentTypeis the first accessor it builds, which is the key the diagnostic reported.Recommendation
The narrowing is not wrong in intent — it is what makes the #9226 own-keys enumeration correct — so I would not reflexively revert #9315 and lose that. The defect is that the generic path's
seen_generic_computed_member/can_source_order_registergating changes how subsequent members register in a way that leaves the class's prototype unreachable from a later static method. The narrow fix is in that gating, not in theis_generatorpredicate.If a revert is wanted for release safety in the meantime, revert #9315 — but please re-land it with the gating fixed, since it is the only thing holding the #9226 fixture at 0 diverging lines.
Also worth acting on, from the same investigation: #9297's content should still be re-landed (the #9326 revert bought nothing — #9314/#9326 were never the cause), and #9365 should be fixed on its own merits, because a
.prototypestore that silently does nothing is what turned a one-line lowering change into a week of misattribution.Reproduction
Fixture and both arms:
/root/claude-9341b/fx/r1.tson perrymaster;README.mdthere has the full method, the eliminations, and the four adjacent pre-existing bugs (#9362, #9364, #9365, #9366). Each arm is a ~6 minute compiler build plus a ~2 minute fixture compile — nocccompile needed to test this any more.Sharper root cause — and a smaller fixture that shows the contradiction directly
class H1 { toJSON() { return { a: 1 }; } [Symbol.iterator]() { return Object.entries(this.toJSON())[Symbol.iterator](); } static probe() { return "this===H1:" + (this === H1) + " | typeof this.prototype=" + typeof this.prototype + " | typeof H1.prototype=" + typeof H1.prototype; } } console.log(H1.probe());
node : this===H1:true | typeof this.prototype=object | typeof H1.prototype=object perry 83754818e (GREEN) : this===H1:true | typeof this.prototype=object | typeof H1.prototype=object perry 8b2cfe6e7 (#9315) : this===H1:true | typeof this.prototype=undefined | typeof H1.prototype=objectInside one function, on one object, the same property read gives two different answers depending on whether it is reached through
thisor through the class's own name.this === H1istrue,typeof thisis"function",this.nameis"H1"— the receiver is correct by every other measure. Onlythis.prototypeisundefined.Remove the
[Symbol.iterator]()line andthis.prototypeis an object again. Verified pre-existing-free:r2is byte-identical to node on83754818e.Why that produces #9341
The class-ref
.prototyperead lives inget_field_by_name.rs:if name == "prototype" && class_id != 0 && is_class_id_registered(class_id) && !is_prototype_ref { let value = class_registry::class_decl_prototype_value(class_id); if value.to_bits() == TAG_UNDEFINED { return class_prototype_ref_value(class_id); // synthetic fallback } return value; }
Reached with a recognised class ref it cannot return
undefined— there is a fallback. Sothisin the static method is not being recognised as a class ref on this path, the branch is skipped, and the read falls through to a generic lookup that finds nothing. ReadingH1.prototypethrough the named binding still takes the class-ref path, which is why the two disagree.What #9315 changes is upstream of that. Its narrowing of
generic_computed_member_keymoves a non-generator[Symbol.iterator]()into the generic computed-member path, which setsseen_generic_computed_member; from that point on, every subsequent member also getslower_noncomputed_class_member_registrationpushed intocomputed_members(class_decl.rs, the threeif seen_generic_computed_member && can_source_order_registersites). Thestatic accessor/static probemethod is one of those members, and the extra registration is what leaves itsthisin a form the.prototypebranch does not accept.Suggested direction for the fix
Not the
is_generatorpredicate — that narrowing is what makes #9226's own-keys enumeration correct, and reverting it loses that. The defect is that the extra source-order registration applied to members after a generic computed member changes the receiver form those members see. Two candidates, in order of preference:- Make the extra registration purely additive metadata (declaration order for
[[OwnPropertyKeys]]) without altering how the member is emitted or howthisis bound in it — that is all getOwnPropertyNames(C.prototype) omits accessors and leaks a symbol key as the literal string "@@iterator" #9226 needed. - Failing that, make the class-ref
.prototypebranch accept the receiver form thatthistakes in these methods, so the two reads cannot disagree.
Whichever is chosen, the pair
H1(with the computed member) andH2(without) belongs intest-files/— asserting only the failing case would not show that the computed member is the trigger, and this whole bug is a case of a change to member registration silently altering member dispatch.Fixtures on perrymaster:
/root/claude-9341b/fx/r1.ts(axios-shaped, throws the #9341 message) and/root/claude-9341b/fx/r2.ts(the three-waythisvs named-binding probe above). Both ~2 minutes per arm.- Make the extra registration purely additive metadata (declaration order for
Correction to my own mechanism, and the real one
My previous comment said the trigger was the ordering gate —
seen_generic_computed_membercausing members declared after the computed one to re-register. That is wrong, and my next measurement contradicted it, so here is the reconciliation before anything else.A static method declared before the computed member fails identically to one declared after:
static-BEFORE-computed => undefined <- kills the ordering story static-AFTER-computed => undefined instance-method-after => object (instance methods unaffected) named-binding => object (H1.prototype still fine)So it is not about order. It is: any class carrying a generic computed member loses
this.prototypeinside every static method of that class.What #9315 actually did
Sweeping the computed-key kinds across both builds:
class member green 83754818ebroken 8b2cfe6e7(#9315)[Symbol.iterator]() {}(non-generator)object undefined *[Symbol.iterator]() {}(generator)object undefined [Symbol.asyncIterator]() {}object undefined [Symbol.toPrimitive]() {}object undefined [K]() {}whereK = "dyn"+"Key"(plain computed)undefined undefined Read the last row carefully. A plain computed-key member already broke
this.prototypeon the green build. The defect is not new. What #9315 did was route the well-known-symbol computed members onto that same already-broken path:generic_computed_member_keywas narrowed, so non-generator[Symbol.iterator],[Symbol.asyncIterator],[Symbol.toPrimitive],[util.inspect.custom]and friends stopped getting special lowering and became generic computed members;- and separately, the generator
[Symbol.iterator]branch changed frommethods.push(wrapper)tocomputed_members.push(ClassComputedMember { … }), which puts that one on the generic path too — which is why the generator row broke as well even though it keptneeds_special_lowering.
So the accurate statement of #9341 is:
#9315 is the culprit, but it did not introduce the defect — it moved every well-known-symbol computed class member onto a generic computed-member path that already lost
this.prototypein static methods. axios'sAxiosHeadershas a non-generator[Symbol.iterator]()and astatic accessor()that readsthis.prototype, so the bundle moved onto the broken path andcc --helpwent down.This is worth stating plainly because it changes the fix: reverting #9315 restores
cc --helpbut leaves the underlying hole, one plain computed key away from the next occurrence — and it costs the #9226 own-keys fix. Fixing the generic computed-member path's effect on static-methodthisfixes both, and closes the pre-existing plain-computed-key case at the same time.That pre-existing half is filed separately as #9367 so it can be fixed on its own merits whichever way the revert decision goes.
Fixtures
All three are ~2 minutes per arm, no
cccompile:/root/claude-9341b/fx/r1.ts— axios-shaped; throws the literal cc --help parity gate down on main: defineProperty-on-non-object at startup — window pins #9326 (indexed writes with a recorded custom array prototype) #9341 message; green passes./root/claude-9341b/fx/r2.ts— the contradiction in one line:this === H1is true,typeof H1.prototypeisobject,typeof this.prototypeisundefined./root/claude-9341b/fx/r3.ts— the table above; separates the regression from the pre-existing case, and is the one worth committing since it pins which member kinds may take the generic path.
I would put
r3intest-files/over the other two: it is the only one that would have caught #9315 and would catch a future change that widens the generic path again.Correction to the comment above: the pre-existing half is #9369, not #9367 — I wrote the number before the issue was created and guessed wrong. Everything else in that comment stands.
#9369:
class E { [K]() {} static probe() { return typeof this.prototype; } }with a computed keyK—this.prototypeisundefinedinside every static method whileE.prototypeis an object, on green as well as broken.Bundle-level confirmation is in.
cc --helpis GREEN at #9315's immediate parent.p9313 d6794cf77 (#9313) + #9323 fix CC --help GREEN (3/3 byte-identical) p9300 015ec5fe1 (#9300) + #9323 fix CC --help GREEN (3/3 byte-identical) <- #9315's parentCombined with the fixture result, the attribution is closed at both levels:
point cc --helpfixture 83754818e(#9242)GREEN passes d6794cf77(#9313) + #9323GREEN 3/3 — 015ec5fe1(#9300) + #9323 — parent of #9315GREEN 3/3 passes 8b2cfe6e7(#9315) + #9323— FAILS with the #9341 message a03be729c(#9336)BROKEN fails 0f95fbc9f(tip)BROKEN — 015ec5fe1is8b2cfe6e7's immediate parent, and both arms were built with the same recipe and the same cherry-picked #9323 fix. Culprit: #9315 (8b2cfe6e7).Restating the finding one last time, because the fix decision turns on it: #9315 did not introduce the defect. A class with a generic computed member has always lost
this.prototypeinside its static methods (#9369, reproduces on green). #9315 routed the well-known-symbol computed members —[Symbol.iterator]in both generator and non-generator form,[Symbol.asyncIterator],[Symbol.toPrimitive],[util.inspect.custom]— onto that path. axios'sAxiosHeadersis a class with a non-generator[Symbol.iterator]()and astatic accessor()that opens withlet z = this.prototype, and its first accessor isgetContentType— the exact key the diagnostic reported.So: reverting #9315 will restore
cc --helpand will lose #9226's own-keys fix while leaving #9369 live. Fixing #9369 restorescc --help, keeps #9226, and closes the plain-computed-key case too. I would fix #9369.Independent of either: #9297's content should be re-landed — the #9326 revert was collateral from a wrong attribution and bought nothing.
Culprit: #9315 (
8b2cfe6e7) — but the defect it exposes is older, and the right fix is #9369, not a revertEstablished with green-parent/broken-at evidence at both levels. Every arm built with the identical recipe and the identical cherry-picked #9323 longjmp fix — that cherry-pick is essential, because every point below #9323 segfaults on
cc --helpfor an unrelated reason, so a step down there without it answers a different question:point cc --helpfixture 83754818e(#9242)GREEN, 9,175 B passes d6794cf77(#9313) + #9323GREEN 3/3 — 015ec5fe1(#9300) — #9315's immediate parentGREEN 3/3 passes 8b2cfe6e7(#9315)— FAILS with the #9341 message a03be729c,0f95fbc9f(tip)BROKEN fails Root cause
The failing call is
Object.defineProperty(undefined, "getContentType", …)— receiver is plainundefined(tag0x7FFC), exactly one occurrence in the run. It is axios'sAxiosHeaders.accessor, wherelet z = this.prototypeevaluates toundefined.#9315 did not introduce the defect. A class with a generic computed member has always lost
this.prototypeinside its static methods — that is #9369, and it reproduces on green with a plain computed key. What #9315 did was narrowgeneric_computed_member_keyand move the generator-[Symbol.iterator]branch frommethods.pushtocomputed_members.push, routing all well-known-symbol computed members onto the already-broken path.AxiosHeadershas a non-generator[Symbol.iterator]()plus astatic accessor()opening withthis.prototype.The sharpest symptom, inside that one static method:
this === H1istrueandtypeof H1.prototypeis"object", buttypeof this.prototypeis"undefined"— same property, two answers, one function.Recommendation: fix #9369, do not revert #9315
A revert restores
cc --helpbut loses #9226's own-keys fix and leaves the hole one plain computed key away from the next occurrence. #9369 is the real bug and it is independently reachable.Minimized fixtures — ~2 min per arm, no cc compile
/root/claude-9341b/fx/{r1,r2,r3}.ts;r3.tsis the one fortest-files/— the only one that separates this regression from the pre-existing case and pins which member kinds take the generic path (Symbol.iterator,Symbol.asyncIterator,Symbol.toPrimitive, generator form, and a plain computed key).Corrections to things in circulation
/root/claude-keep/app_Dis NOT a reproducer — it segfaults (rc=139), which is the pre-fix(runtime): no Rust frame is ever a longjmp target — cc --help segfault fixed, parity gate back online (#9305) #9323 bug. Anyone differentially debugging against it has been chasing the wrong failure. My earlier pointer to it was wrong; disregard it.- The window was 52 commits, not the 11-commit tail I circulated.
- The failure is fully GC-independent — five kill switches, identical results — which eliminated perf(gc): v5 GC map — find one function's records without decoding the section (index build 63.5x less work, −214 MB RSS) #9300/fix(gc): tombstone shape publish arms old_carrier — deleted receivers no longer lose their keys array under evacuating GC (#9200) #9317/perf(runtime): tombstone deletes default-on again — the #9200 corruption that rolled them back is fixed (26x on populated delete) #9331 as mechanisms early.
- fix(runtime): indexed writes and the generic array engine honour a custom prototype chain #9297 should be re-landed: the fix(runtime): indexed writes honour a custom array prototype (from #9297) #9326 revert was collateral from my wrong attribution, and its content is exonerated. Branch is ready.
Five pre-existing
.prototype-adjacent bugs filed along the way#9362 (
util.inheritswith a class constructor), #9364 (SIGSEGV on chainedclass D extends <param>factory), #9365 (o.prototype = vsilently dropped after a computed-key write), #9366 (exported class expression reads.prototypeasundefinedcross-module), and #9369.Handover with the method, the eliminations, and ~90 non-differentiating shapes is at
/root/claude-9341b/README.mdso none of that ground is walked twice.
Symptom
The
cc --helpparity gate is DOWN on main: the natively-compiled claude-code bundle (cli_2.1.112.js, the standardPERRY_NO_CACHE=1 … --no-auto-optimize --enable-wasm-runtimerecipe) exits rc=1 withinstead of printing the 9,175-byte help text. Deterministic — three independent binaries, every run:
83754818e(#9242)9943dd95c(#9328)a03be729c4(#9336)All three compiled on perrymaster with the same recipe and verified against the same
node cli_2.1.112.js --helpoutput (rc=0, 9,175 bytes). The perry-runtime lib suite is green at tip (2904 passed) — the breakage is invisible to it.Window
83754818e..9943dd95cis 28 commits, but d1bef4c (#9323, "cc --help segfault fixed, parity gate back online") re-verified the gate at its own merge point — which already contains #9300/#9302/#9315/#9316/#9317/#9318. That leaves exactly two commits between the verified-green #9323 and the failing #9328 tip:1f8ad06fa1— fix(runtime): indexed writes honour a custom array prototype (from #9297) #9326: indexed writes honour a custom array prototype (from fix(runtime): indexed writes and the generic array engine honour a custom prototype chain #9297) — runtime behavior change on indexed reads/writes/HasProperty for arrays with a recorded custom prototype (array_static_proto_recordedlatch +real_array_uses_recorded_spec_path)9943dd95c3— fix(release): prime the macOS x86_64 dependency cache (from #9327) #9328: release-infra only (macOS dependency cache), cannot affect the compiled runtimeso unless #9323's gate claim was not actually run, #9326 is the culprit by elimination.
Mechanism hypothesis, consistent with the error: the bundle retargets array prototypes early (polyfill/subclass boilerplate), which arms the process latch; after that,
array[k]/ HasProperty on any array with a recorded prototype routes through the newarray_spec_get/array_spec_has_indexlanes. A read that now yieldsundefined(or a changed HasProperty verdict feeding an index walk) lands inObject.defineProperty(<that value>, …)→throw_object_type_error(b"Object.defineProperty called on non-object")(object_ops/define_property.rs:429).Repro
Found while measuring an unrelated runtime change (shape-descriptor no-copy reads) whose A/B had to be moved back to
83754818ebecause neither arm can pass the gate on current main. Also note: tonight's r6 headline measurement in/root/claude-arch/measure_r6.logsilently measured the BROKEN binary (its --help run does ~0.69B instructions vs node's 4.05B because it dies early) — those numbers should be discarded.