Repository navigation
default-derived ctor forwarding part 2: HIR fixed-arity synthesis truncates args (rest/extends-expr parents); runtime construct dispatchers need sig-cap counts + rest packing #5957
Description
Follow-up to #806 / the #5917 behavioral-matrix fixture test_issue_806_default_derived_constructor_forwarding. The static no-own-ctor walk mis-split is fixed (PR incoming), which covers ctor-free subclasses of capturing ancestors reached via extends_name — but two sibling shapes remain broken, with the mechanisms fully traced:
Shape 1: fixed-arity default-ctor synthesis truncates/drops user args
synthesize_class_captures (crates/perry-hir/src/lower_decl/class_captures.rs, the "spec default ctor" arm ~L414-483) does NOT synthesize constructor(...args) { super(...args) }. It walks the nearest pending-ancestor ctor's user arity and mints that many FIXED params (__perry_dflt_arg_i, is_rest: false), forwarding them positionally. Consequences, both reproduced:
- Rest-param ancestor counts as arity 1 →
class Drain extends Sink {}whereSink { constructor(...parts) }captures an outer local:new Drain("a","b","c")forwards only"a"(--trace llvmshows the new-site callingDrain_constructor(this, %arg, cap)with the first arg raw). OutputP[a]instead ofP[a,b,c]. - extends-EXPR parent (mixin:
class W extends WithSuffix(Logged) {}): the arity walk keys onextends_name—Nonehere — so arity resolves to 0 and the synthesized ctor forwards NOTHING. Also applies to ancestors not inpending_classes(module-level parents of a function-nested capturing subclass) per the "no-arg baseline" fallback.
Fix: synthesize the real spec ctor — one is_rest: true param + Expr::SuperCallSpread([Spread(LocalGet(pid))]). The arity walk disappears. This routes the forwarding through the spread-super machinery, which is where part 2 below matters.
Shape 2: runtime construct dispatchers guess the user/cap split and can't pack rest
crates/perry-runtime/src/object/class_constructors.rs has four ctor dispatchers. Two (replay_registered_class_constructor, the heap-class-object dispatcher) already pack rest via lookup_closure_rest (#wall3). The two SUPER dispatchers do neither:
js_super_construct_apply(~L347) andrun_class_constructor_on_this_flat(~L649) binduser_params = total_params − caps.len()raw args, no rest packing — a rest-param parent binds its rest slot to the FIRST raw arg (whichsuper(...args)then spreads as a string/array-like: the observedP[a]).- All four compute the user/cap split as
total_params − snapshot_len(ormax(ctor_caps, snapshot)), which assumes cap params ride in the ctor signature. Dynamic-parent classes compile CAPLESS ctor signatures ((this, seed)— the parent is fetched via the per-cid registry) while a decl-site snapshot IS registered (extends-expr refs join the capture union), souser_params = 1 − 1 = 0and the user arg is dropped:new WrappedLogged("alpha")→js_fetch_or_value_super→ class-object construct dispatch →seed = undefined→ the fixture'ssuper-args.log: undefined:wrapped.
Fix: register the SIGNATURE cap-param count alongside the ctor — extend js_register_class_constructor(cid, fn_ptr, param_count) with a 4th arg (n_sig_caps, counted at the ctor_triples emit in codegen/string_pool.rs ~L655 from __perry_cap_* params; 0 for arity-override/codegen-synthesized ctors, which are capture-free by the synthesized-ctor invariant). All four dispatchers then compute user_params = total − n_sig_caps and fill exactly n_sig_caps trailing slots from the snapshot; the two super dispatchers additionally adopt the #wall3 rest packing (lookup_closure_rest on the resolved fn ptr).
Acceptance tests (in-tree, #[ignore]d)
crates/perry/tests/issue_806_default_derived_ctor_forwarding.rs:
default_derived_forwards_through_capturing_mixin(shape 2 + shape 1's extends-expr variant)walk_into_rest_param_capturing_ctor(shape 1 rest + shape 2 rest packing)
Un-ignore both when this lands. Note the template/inline-init cid sharing (both __anon_class_* copies register ctor+snapshot under one cid, last-wins) is by design (replay) and becomes harmless once the split stops depending on snapshot length.
Activity
fix(codegen): #806 — no-own-ctor walk must bind the ancestor ctor wit…
fix(codegen): #806 — no-own-ctor walk binds ancestor ctor with caps-a…
wip(#10660): extend WeakMap debug instrumentation + rule out 4 more r…