You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
flaky: capture-bearing class factory reports .name === undefined on some builds (build-nondeterministic) #6482
#6470 (be2e23f86, "per-evaluation captures for class declarations in function bodies") regressed class .name for a captured-param class-declaration factory. A named class declared inside a factory function that captures an outer parameter now reports .name === undefined instead of its declared name.
Repro
functionmakeTagged(tag: string){classInner{readonly_tag=tag;// captures the outer `tag` paramwho(){return"tagged:"+this._tag;}}returnInner;}constT=makeTagged("S");console.log(JSON.stringify(T.name));// node: "Inner" perry(main): undefinedconsole.log(newT().who());// node: "tagged:S" perry: "tagged:S" (construction is fine)
Construction, the captured field, and method dispatch all work — only .name is lost. Covered by the existing gap test test_gap_5952_mixin_factory_binding (case 7), which now fails on main.
For a capturing class declaration with no static state, #6470 sets fresh_binding = true and binds the name to a ClassExprFresh per-evaluation object (lower_decl/body_stmt.rs). The fresh object gets the right class_id (so methods/construction work), but its class_id is never entered into the CLASS_NAMES registry: name registration (codegen/string_pool.rs → js_register_class_name) only fires for class_ids collected into named_classes, and this fresh-factory template ends up excluded (classified anon / name-mismatched). .name then resolves via class_name_for_id(class_id) → None → undefined. Named class expressions through the same ClassExprFresh machinery keep their name, so the gap is specific to the declaration route + the specialize_captured_class_factories interaction.
Impact
Blocks conformance-smoke (shard containing test_gap_5952) on every PR branched off current main — surfaced while merging #6476 (unrelated streams PR). Mixin/class-factory patterns that read .name (incl. effect-style tagged classes) are affected.
Correction — #6470 is NOT the culprit. I re-verified: the lowered HIR and the js_register_class_name codegen for test_gap_5952 are byte-identical at b4b47ba34 (#6459, PASSES) and current main (FAILS) — same Tagged = ClassRef("Inner"), same cid 11 → "Inner" registration. So this is a runtime regression, and #6470 (a HIR-only lower_decl/body_stmt.rs change that produces identical HIR here) cannot be it.
The regression is a runtime change in the range (#6459 .. current main]. Ruled out by scope: #6464 (buffer_dispatch only), #6457 (intl/icu4x only), #6470 (HIR-only). Remaining suspects: #6474 (class_constructors.rs + codegen/method.rs, "default Error-init on every dynamic construct path") and #6466 (init-call topo ordering). Bisecting now.
Symptom precisely: Tagged resolves to ClassRef("Inner") (class_id whose name IS registered as "Inner"), construction + methods work, but the runtime .name read returns undefined. So the break is in how .name resolves on a class-ref value / a captured-factory class at runtime.
Second correction — this is a build-variable flake, not a clean regression, and NOT #6474 either.
After deeper testing the bisect does not hold up as a code regression:
test_gap_5952 (and a 2-line repro function f(t){ class Inner { x = t } return Inner } f("S").name) fails on current-main local builds (undefined vs node "Inner"), reproducibly within a given compiler binary (5× identical .o).
But it passed on the CI builds of the PRs merged earlier today (their conformance shards 2–8 were green, which includes this test's shard), and a no-capture variant (class Inner { x = 1 }, no factory param) always passes.
Different compiler builds of the same commit produced different results for the capturing-factory case.
So the pass/fail tracks the compiler build, not the source commit — a build-order-nondeterministic latent bug in how a capture-bearing class factory (class declared in a function body that closes over a param) resolves its .name at runtime. #6470/#6474 are cleared.
Root cause is in the runtime per-evaluation capture path (the class the factory returns resolves to a class_id whose name registration is order-dependent), NOT the HIR display-name recording — I tried recording class_display_names for function-body class declarations in lower_decl/body_stmt.rs and it did not fix it, confirming the break is downstream in the capture/cid resolution.
Repro files + the (ineffective) HIR patch attached in the investigation. Needs someone with RegisterClassCaptures / per-eval-capture context. Marking as a flaky/nondeterministic gap failure.
Resolved by #6500 (08a6a4f3a, merged 2026-07-17) — no further code change needed.
Root cause
#6470 (be2e23f86) routes a capturing function-body class declaration through ClassExprFresh, so each factory evaluation returns a heap class object stamped with the template's class_id (construction/methods/_tag all dispatch through that id — which is why those worked). Reading .name on that heap object hit the OBJECT_TYPE_CLASS field path, which had no arm routing .name to the template's registry name — so it fell through to undefined. The registration itself (js_register_class_name(cid, "Inner", …)) was present and correct; the read side just never consulted it for this value shape.
The "build-nondeterministic" appearance came from the object cache: a given compiler binary caches the .o (so repeated compiles = identical .o, "5× identical"), while a fresh binary re-runs codegen — but the deciding factor was always this missing read arm, not a genuine per-build coin-flip in registration.
Fix (already on main)
#6500 added a name arm next to the #4949prototype arm in the OBJECT_TYPE_CLASS tail (crates/perry-runtime/src/object/field_get_set/class_object_props.rs), routing .name to class_name_for_id(class_id) (own static name still wins; a deleted key still reads undefined). It also fixed a companion boxed-capture read (shared_mutable_capture.rs now exempts ClassExprFresh.captured_args, matching RegisterClassCaptures).
Verification on current main (9589f7153)
Exact repro from this issue → "Inner" (matches Node), and stable across 15 no-cache rebuilds (PERRY_NO_CACHE=1) — the flake does not reproduce.
test_gap_5952_mixin_factory_binding (case 7 is this repro) diffs byte-identical to node --experimental-strip-types, and is not listed in test-parity/known_failures.json.
Effect-style multi-evaluation variant (makeTagged("A") / makeTagged("B")) → each reports its own .name / ._tag correctly ("Inner" "Inner" / A B), stable across 6 no-cache rebuilds.
Activity
proggeramlug commentedon Jul 16, 2026
Correction — #6470 is NOT the culprit. I re-verified: the lowered HIR and the
js_register_class_namecodegen fortest_gap_5952are byte-identical atb4b47ba34(#6459, PASSES) and currentmain(FAILS) — sameTagged = ClassRef("Inner"), samecid 11 → "Inner"registration. So this is a runtime regression, and #6470 (a HIR-onlylower_decl/body_stmt.rschange that produces identical HIR here) cannot be it.The regression is a runtime change in the range (#6459 .. current main]. Ruled out by scope: #6464 (buffer_dispatch only), #6457 (intl/icu4x only), #6470 (HIR-only). Remaining suspects: #6474 (
class_constructors.rs+codegen/method.rs, "default Error-init on every dynamic construct path") and #6466 (init-call topo ordering). Bisecting now.Symptom precisely:
Taggedresolves toClassRef("Inner")(class_id whose name IS registered as "Inner"), construction + methods work, but the runtime.nameread returnsundefined. So the break is in how.nameresolves on a class-ref value / a captured-factory class at runtime.proggeramlug commentedon Jul 16, 2026
Second correction — this is a build-variable flake, not a clean regression, and NOT #6474 either.
After deeper testing the bisect does not hold up as a code regression:
test_gap_5952(and a 2-line reprofunction f(t){ class Inner { x = t } return Inner } f("S").name) fails on current-main local builds (undefinedvs node"Inner"), reproducibly within a given compiler binary (5× identical.o).class Inner { x = 1 }, no factory param) always passes.So the pass/fail tracks the compiler build, not the source commit — a build-order-nondeterministic latent bug in how a capture-bearing class factory (
classdeclared in a function body that closes over a param) resolves its.nameat runtime.#6470/#6474are cleared.Root cause is in the runtime per-evaluation capture path (the class the factory returns resolves to a class_id whose name registration is order-dependent), NOT the HIR display-name recording — I tried recording
class_display_namesfor function-body class declarations inlower_decl/body_stmt.rsand it did not fix it, confirming the break is downstream in the capture/cid resolution.Repro files + the (ineffective) HIR patch attached in the investigation. Needs someone with
RegisterClassCaptures/ per-eval-capture context. Marking as a flaky/nondeterministic gap failure.[-]regression: captured-param class-declaration factory reports .name === undefined (#6470)[/-][+]flaky: capture-bearing class factory reports .name === undefined on some builds (build-nondeterministic)[/+]proggeramlug commentedon Jul 18, 2026
Resolved by #6500 (
08a6a4f3a, merged 2026-07-17) — no further code change needed.Root cause
#6470 (
be2e23f86) routes a capturing function-bodyclassdeclaration throughClassExprFresh, so each factory evaluation returns a heap class object stamped with the template'sclass_id(construction/methods/_tagall dispatch through that id — which is why those worked). Reading.nameon that heap object hit theOBJECT_TYPE_CLASSfield path, which had no arm routing.nameto the template's registry name — so it fell through toundefined. The registration itself (js_register_class_name(cid, "Inner", …)) was present and correct; the read side just never consulted it for this value shape.The "build-nondeterministic" appearance came from the object cache: a given compiler binary caches the
.o(so repeated compiles = identical.o, "5× identical"), while a fresh binary re-runs codegen — but the deciding factor was always this missing read arm, not a genuine per-build coin-flip in registration.Fix (already on main)
#6500 added a
namearm next to the #4949prototypearm in theOBJECT_TYPE_CLASStail (crates/perry-runtime/src/object/field_get_set/class_object_props.rs), routing.nametoclass_name_for_id(class_id)(own staticnamestill wins; a deleted key still readsundefined). It also fixed a companion boxed-capture read (shared_mutable_capture.rsnow exemptsClassExprFresh.captured_args, matchingRegisterClassCaptures).Verification on current
main(9589f7153)"Inner"(matches Node), and stable across 15 no-cache rebuilds (PERRY_NO_CACHE=1) — the flake does not reproduce.test_gap_5952_mixin_factory_binding(case 7 is this repro) diffs byte-identical tonode --experimental-strip-types, and is not listed intest-parity/known_failures.json.makeTagged("A")/makeTagged("B")) → each reports its own.name/._tagcorrectly ("Inner" "Inner"/A B), stable across 6 no-cache rebuilds.Closing as fixed.