installparams normalizes a decl's `T...` param lhs to []T in place
(cstage instead sets the resolved tp->type and never mutates AST), so
typeeqast compared a decl-synthesized fn type's []i64 against a fn
TYPE expr's surface i64 and confidently rejected
`let f: fn(args: i64...) void = sum;` — cstage accepts and runs. The
normalization wrapper now carries an op marker and typeeqast peels
exactly it, so both sides compare at the declared element type without
admitting genuinely different element depths. Fixture fnptrlet_variadic;
corpus pin 1486/2972.
The last frontend-gap pin: wwstage had no Hare struct embedding
(struct { hash.hash, ... }), rejecting lib/crypto/sha256 at parse.
- parse.ww: the three member forms (named / anonymous struct / bare
dotted-ident embed), consume-then-branch since this parser has no
peek; embeds carry f.str == "" and the type in f.lhs.
- check.ww N_TSTRUCT flatten: promote the inner struct's flattened
fields at base+src.offset (check.c:961-990); the embed is one
nested-struct unit in the slot ladder; the resolved inner AST is
planted on the TFIELD rhs for cgen.
- check.ww walkers: astoffset / exprtype N_DOT / #251 struct-lit
field lookups descend embeds through shared helpers; the collision
and non-struct-embed rejects live in validatestructfields (the
once-per-decl diagnostic site).
- cgenutil.ww registerstruct: regfieldrun walks the AST against the
flattened tfield cursor, descending embeds via the planted inner
AST so promoted fieldinfo entries keep the inner field's own name
and type node.
- wwi printers unchanged (both stages already emit nameless fields).
sha256_test compiles byte-identically end to end and its 6 tests
pass; 989_lib_byteid is now 44 id / 0 divergent / 0 wwreject.
Fixtures r5913_* (promoted rw, offset shift, anonymous embed,
two-level embed + promoted fn-ptr callee, three rejects); corpus pin
1485/2970.
fnptrcalleetfn's N_DOT arm accepted only a bare N_TFN field tnode, so
a call through a field declared `cb: *fn(...)` missed, fell through to
the name registry, and emitted CALL cb(SB) — an undefined symbol
(loud at link; cstage calls the stamped ptr-to-fn indirectly). Same
TPTR peel the N_IDENT arm already had. Fixture fnptrfield_call covers
the by-value and via-pointer shapes; corpus pin 1478/2956.
The migrate-and-retire audit left the surviving carriers as the only
files still describing the retired combined.ww amalgamator as live:
m2wwi/m3sep headers claimed .wwi was dead code, four byteid carriers
carried dead cleanup of never-written .combined.ww intermediates, and
sepbuild's enumeration mirror plus lib_byteid's completeness scan
still skipped the retired artifact form (the same silent
accommodation just removed from both drivers — dropping it here
surfaced and flushed 39 stale untracked amalgamator outputs across
lib/, which the gates now reject loudly). Remaining mentions are
past-tense history or quoted diagnostics.
Audit-driven migrate-and-retire wave (one read-only auditor per
carrier batch, verdicts row-checked through the full corpus gate):
every retired carrier's assertions are now owned by declarative
fixtures or an existing owner. 238 new fixtures land (corpus 1,239 ->
1,477; cells 2,954), covering enum/str/slice/tagged array elements,
inferred-length arrays, global slice/str zero-init and literals,
global array/pointer field reads and stores, struct-literal slice
fields, struct returns, float arithmetic, size/int limits, sort and
log-vstream behavior, place-addressed stores, and append places.
Rejection rows STRENGTHEN the old nonzero-exit checks to required
diagnostic fragments; all byte-id claims fold into the blanket
test-data-byteid sweep (now 1,142 compared, 0 pinned-divergent).
Four carriers were fully redundant with existing fixtures/lang tests
(749, 722, 765, and 771's byteid rows) and retire without new rows.
Native carriers 171 -> 147 (residual 135 -> 111).
PROJECT_PLAN.md was a historical disclaimer wrapping five still-open
T1 driver items — those move to docs/test-system-v2.md (Open driver
work); the rest is dated 2026-08-05 design history, archived in git.
Also retired-mechanism cleanup: both drivers drop the *.combined.ww
enumeration skip (the amalgamator is gone; leftover debris now fails
loudly instead of being silently accommodated), the consumer-less
m4_combined_epoch.md5 pin is deleted, and the wwi.ww/ww.h/check.ww
comments that still described combined.ww as the live path are
re-cited to the sep reality.
The wwstage variadic call classification was name-keyed: a fn-ptr
FIELD call whose local base shadowed the current module name
(lib/log's log.println(log, args...)) picked the module fn's
signature — nfixed off by one, the fixed arg boxed into the gather,
the spread emitted as zeros — and a no-collision fn-ptr callee missed
the registry entirely, leaking the raw N_SPREAD as a single $0 word
(SIGSEGV / exit 255 in 8 of 11 logtest tests on the wwstage leg).
fnptrcalleetfn resolves a local fn-ptr callee (bare local or struct
field) to its N_TFN once, shared by the CALL-target choice,
callee_variadic_param (with the []T wrap registry params get from
installparams), calleecvariadic, and the widening param lookup, so
target and arg prep can never disagree. Graduates the #59.8 logtest
pin — DATABYTEID_DIVERGED-era M_DIVERGE count is now zero.
A TYPE name used as a VALUE (an error-singleton `return too_long;`)
stamped the flattened BODY type, so structurally identical !void
singletons in one union were indistinguishable and flatvariantidxt
loud-rejected the ambiguity — the real cause of the lib/path wwstage
reject (the pinned #120/#29 global-slice-const blame was stale; that
family had drained). The N_IDENT arm resolves through a synthesized
TNAME (the #66 N_STRUCTLIT precedent); the module-qualified N_DOT twin
reads the sym's cached NAMED. Graduates the path M_WWREJECT pin (#142).
An enum OR-fold under `as` ((m.A|m.B) as u32) was never typed:
scruttype resolves IDENT/DOT only, and the post-order restamp then
re-derived the folded member literals as untyped_int. The unstamped
operand missed cgtypeassert's #27b enum-reinterpret gate and lowered
as a phantom tagged assert — unconditional exit 1 at runtime on the
wwstage leg. checkisas now types an N_BIN lhs and the N_BIN restamp
preserves an existing enum stamp. Graduates the #59.9 stattest pin.
The wwstage nodeisstr/nodeisslice recognizers were name-keyed for
every non-local shape: an indirect fn-pointer callee ((*f)() — the
errnotest #59.5 divergence, streq receiving a shifted register file)
and module-global let/const idents (path.sepstr — union-widen pushes
zero-filled len/cap) both fell to false while cstage keys off the
checker stamp unconditionally. Both recognizers now fall back to the
stamped n.type_; push and pop sites share them, so the drain stays
balanced by construction. Graduates the #59.5 errnotest pin.
test/package/runtime/ was a manual README-driven corpus with no
target. Its unique surface — the driver's single-file ww test leg,
the unmet-expected-abort outcome, and the published -c binary's own
CLI (repeated globs, -list, -timeout-ms, direct-binary timeout and
descendant handling) — is now asserted by three new @test fns in
package_test.ww; the README drops its manual-run instructions.
Wwstage-driver-leg byte identity gets one owner. The three carriers'
content identity is already blanket-owned by their r815_/r940_/r951_
corpus twins via test-data-byteid; their only non-redundant assertion
was the ww_ww driver leg, which 989_lib_byteid's 43-unit sweep covers
apart from the zero-dep root-only edge — folded in as a new probe row
(44 units). r940 fixture comments re-cited off the retired carrier.
x.slicefield[:] / x.strfield[2:] emitted MOVQ $0 for the omitted hi
bound on BOTH stages (N_IDENT-gated dispatch; symmetric, so byte
identity never caught it) in all four sites: cgexpr N_SLICE + pushargs
(cstage), cgslice + pushargsrev (wwstage). The new arm re-evaluates
the pure field read for its {ptr,len,cap} header and takes .len,
covering local, viaptr, dot-chain, (*p), arr[i], and global inners.
Call inners still loud-reject upstream. Sibling of the #252/#257
array-field arms.
let x: []T = g.buf on a module-global struct dispatched only str
fields to the 3-word header load; a []T field fell to the scalar
tail (ptr word only), so len/cap read stale registers and the shape
was byteid-divergent against cstage's TY_STR||TY_SLICE arm (#263).
The four graduated pin families byte-compared under test-data-byteid
but their original fixtures masked the miscompiles at runtime (lone
frame slot, dead code path, pointer accident). Add the discriminating
programs as run fixtures: adjacent-element clobber for the 2-byte
deref store, second-variant match dispatch for fn-type dedup, payload
integrity across the tagged widen of a padded struct, and base64
clear() actually zeroing its buffers. Corpus pins move to 1,229
fixtures / 137 run / 766 run-exit / 2,458 cells with the new identity
hash; the data-byteid floor moves to 915.
pushargsrev's N_SLICE arm resolved esz and the base address from the
stamped tinfo of an N_DOT [N]T-field base but let the omitted hi bound
fall to $0, so the #258 array-to-slice borrow desugar handed callees a
zero-length slice: base64 clear()'s bytes.zero(e.ibuf) was a runtime
no-op under the wwstage frontend. Take the element count from the
field's array tinfo like the cgslice #252 arm and cstage's bu->alen.
Graduates r989_libprecond_decodedsize_aligned and the 989_lib_byteid
#59.3 base64 pin.
Every probe's assertion is owned by a current gate: the compile and
link probes by make all and the bootstrap fixed point; build/run and
cs/ww byte identity by the fixture corpus, test-data-byteid, and
989_lib_byteid; wwstage driver and toolchain parity by 993/995;
checker-diagnostic parity by the corpus' both-stage //ww:error rows.
The wwdump -t/-a dump-parity probes gated the frontend port's
convergence, which the compiler-output identity gates now own end to
end; carrier ran green at retirement.
What was still uniquely alive migrates: smoke.ww becomes corpus
fixture selfhost_smoke (upgraded from a cstage-only build to both
frontends, byte-identical, exit 42 on both toolchains; corpus pins
move to 1,225/763/2,450 with the new identity hash in the same
commit), and sym_link.ww's scope/sym behavior rows become in-language
lib/ww/syntax/symtest.ww under LIBRARY_TESTS. uses.ww (parser-stub-era
-a fixture) and the already-orphaned tagged_ptr_ret.ww/trypromote.ww
retire with the probe corpus. Bootstrap native gates drop to six;
frontend numeric-sync comments now cite the rule-6 mirror instead of
the retired diff probe.
Same contract as the 27 repaired byte/artifact survivors: checked
acquisition, one all-exit cleanup funnel per carrier, ENOENT-tolerant
checked unlinks, exact-path deletion, and cleanup failure fails a
passing carrier without overwriting its diagnostic. Also closes the
vacuous-green channels: empty-artifact pairs no longer byte-compare
equal, capture and staging failures fail their row loudly, 950 requires
a clean checker exit on no-error rows and hard-fails on a missing
wwdump_ww, 992 keys its scratch per invocation so a failed cleanup
cannot leak stale objects into the next tool's compare, and 995 no
longer pre-sweeps a workdir it never created. No assertion is weakened;
990_selfhost is retired in the next commit rather than repaired.
Presence-is-registration wrapper classification replaces the 296-entry
TESTS list, its per-wrapper rules, the test/run scheduler, the
last-green cache, and auto -j; test is five in-process units plus one
fixture smoke; test-commit composes behavior suites; byte identity,
bootstrap, and platform stay explicit gates. test-data-byteid builds
every non-error corpus fixture twice through the fixed cstage driver
swapping only WW_W6C and byte-compares every per-package .s, with a
910-fixture vacuity floor and the DATABYTEID_DIVERGED loud-pin ledger
(8 real cs/ww divergences: r660, r71, r76_typeeq_fn x4, r940, r989 -
the base64 #59 pin). The 61 carriers whose only assertion that
comparator subsumes retire with it, leaving the 27 survivors that
observe asm patterns, symbols, frames, .wwi round-trips, inline-only
sources, or the wwstage driver leg.
One commit because the wildcard classification sweeps any leftover .c
into test-compiler, where the retired carriers' --sep invocations
cannot run against the current driver; the 61 deletions, the 27-entry
byteid list, and the blanket comparator are a single consistent state.
tools/peellint and tools/sizelint (and the 944 gate wired to them)
retire with the target graph that carried them; rule-13 layout
discipline stays on the authoritative-helper rule.
Behavior and reject carriers whose rows now live as test/wcc/data
fixtures or test/lang @test tables; the 31 library launchers plus
900_stdlib, whose only assertion was an existing @test source's exit
status (LIBRARY_TESTS runs those sources directly); and the test/run
shell harness, its protocol self-test, and the runww corpus runner.
Their Makefile registrations dangle until the target-graph flip in
the next commits.
local_zeroinit_test.ww takes the 840/944 zero-init seam (dirty-frame
prime and probe share one @test because the runtime forks per test);
alias_cgen_b6, alloc_nested_field, array_static_init, and
strarray_static gain the rows their retired wrappers held; the
remaining files re-point reject-row citations at the r788_* and
stage-matrix fixtures.
Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.
Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
000_smoke drops its popen ww -V leg (driver version parity moves to
949_driver_flagargs); 400_w6c feeds its sources through lex/parse/
check/cgen into open_memstream instead of spawning w6c. The five unit
binaries now hold zero subprocess calls.
Discovers test/wcc/data, hard-pins corpus identity (1,224 fixtures,
314 error / 12 compile / 136 run / 762 run-exit, sha256 name hash),
schedules both frontend cells per fixture through os.exec with
per-cell capture dirs, and emits the strict wwfix TSV result stream.
integration.sh is the black-box owner of the CLI/protocol boundary.
The r-prefixed waves absorb the runtime, reject, and byte-compare rows
of the migrated native carriers; each fixture is one directory with one
case.ww and a //ww:error, //ww:compile, //ww:run, or //ww:run-exit
directive covering both frontends.
Scratch acquisition is one bare mkdir that refuses a pre-existing
.sepwork; the path reaches cleanup only after acquisition succeeds.
ww build and ww test -o keep the tree for the caller; ww run and
no-output ww test build under an owned /tmp/ww_{run,test}_<pid> dir
and remove it on every result, escalating rc on cleanup failure.
The content-keyed package cache is deleted from both stages; -S stops
after per-package .s emission; --sep is gone and unknown flags are
rejected; directory ww test delegates to the wwtest coordinator; the
wwstage driver routes every subprocess through os.exec.
The wwstage Makefile recipes move off the persistent <tool>_ww.d
build-in-place shape to a per-invocation mktemp workspace: the old
recipes relied on the retired pre-wipe semantics and any rebuild
would trip the new refusal on their leftover main.sepwork.
The three 989_pkgcache_* carriers lose their subject with the cache
and go with it; the rest of the wave's carrier adaptation follows in
its own commits.
Single-directory package-test planner: discovers *.ww, groups white-box
and external <pkg>_test sources, composes the combined test package,
builds it through the sibling ww driver, and runs it with the runtime's
-package/-list/-timeout-ms contract. test/package holds its E2E corpus
and the manual runtime fixture set.
Captured-async start/poll/cancel plus inherited-stdio run/runstdio with
process-group creation, close-on-exec errno marker for exec-setup
failure, TERM-grace-KILL escalation, and a signalfd interrupt watch.
test/wwfixture/process/main.ww is its self-exec harness, built by both
driver stages.
A module-global base has no frame slot; treating its local-lookup miss
as offset zero wrote .ptr/.len/.cap at the caller return address. LEAQ
the symbol like the struct-field global arms do. Both stages.
The a[i] = src copy loop enumerated ident/field/deref sources; an
N_INDEX rhs fell to the scalar tail and truncated the element. Route
it through the generic place-address funnel. Both stages.
The direct-field assignment arms enumerate CALL, STRUCTLIT, and local
IDENT producers; an addressable N_DOT/N_INDEX/deref rhs fell through to
the scalar tail, so a 16-byte struct field copied only its first word.
Resolve both places through the existing address funnels and use the
tail-aware aggregate copier. Both stages.
A str `def` has no name(SB) header; the streq push arms read a frame
slot that does not exist. Load the literal through cgexpr (AX=ptr,
BX=len) instead. Both stages.
wwstage exprtype's N_CALL fn-value arm peeled ALL pointer levels before the
TY_FN gate, so a deref-less `pf(21)` where pf:**fn type-checked and lowered --
then segfaulted at runtime (a silent miscompile). cstage peels exactly one
level and loud-rejects (the C6a discipline, check.c:1947).
Align wwstage DOWN: peel one level (loop -> if); a remaining non-TY_FN callee
hits a loud "calling non-function" reject mirroring cstage's message. Both
stages now reject the exotic deref-less `**fn`/`***fn` shape; the legitimate
`(*pf)(21)` and one-level deref-less `f(21)` (f:*fn) still compile + run.
Multi-level autoderef is a separate deferred FEATURE, not a miscompile to lower
(rule 7/10 -- align the richer stage down to the leaner, no value ships).
ww-only change (cstage is the correct oracle); a reject emits no asm, so the
byte-id baselines and LANGBYTEID floor are unchanged.
Pins: cfail test/wcc/data/fnptr_pp_derefless_reject (both stages reject,
reddens-on-revert -- the silent miscompile resurfaces if the fix is reverted) +
test/lang/fnptr_derefless_call_test (positive guard (*pf)(21)==42 and one-level
f(21), value-asserted + byte-id, so the fix does not over-reject the legitimate
one-level autoderef).
The W1/W2/W5/W4b store / structlit-fill arms still resolved struct field
LAYOUT by bare-leaf name (structlookupchain / structlookup / letvarstructinfo)
inside the per-field store loop -- the write-side twin of commit-1's read fix.
Under a cross-module same-leaf collision (two modules each exporting a `pair`,
16B vs 24B) the field is stored at the wrong offset/width; an interior 2-byte
field store silently corrupted neighbours. cstage walks the resolved Type*
(never a name) and is correct; this aligns wwstage UP (ww-only change).
Convert each store loop to a tinfo-native fill. Two new helpers in cgenutil.ww:
sretretsizetn (a verbatim port of cstage cg_sret_retsize -- 4 kind-branches,
no TY_BANG, dropping the #129 module-juggling that was pure name-keying
compensation, redundant once the lookup walks resolved tinfo) and
cgstructlitfilltn (the tinfo-native twin of cgstructlitfill, recursing on
tichase(tf.type_).fields with NO name lookup at any nesting level). Supporting
twins storeopsz / tfieldlookup / cgdotfieldhardstoptn. Because the fill never
bottoms out on a name, the W4b "decisive wall" (a structlit that needed a
(sname,smod)-keyed structinfo the stamped leaf-only tinfo couldn't supply)
dissolves by construction, with no module-aware tinfo->structinfo bridge.
This CLOSES the nine in-loop nested sub-arms (N_CALL / N_STRUCTLIT / N_IDENT
field receive) by construction -- the tf walk has no name lookup. The
non-reddenable global arms (W5/W4b) are converted-for-construction: a qualified
global decl resolves its bare leaf correctly today, so byte-id (cs.s==ww.s) is
their net, not a value pin. `!T` is kind-preserving in both stages
(resolve_type / tinfofornode), so a `!struct` sret sizes as the inner struct,
never TAGGED -- the #129 drop changes no size.
Completes the 2-commit #31 arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): the
#224 name-keyed receiver-layout cluster is now closed by construction. The
residual nested-field surface (the shared cgstructlitfill kept for its non-#31
callers + the cgenstmt/cgenutil let-receive/fill family) is tracked as #32.
Pins: test/wcc/797 value-asserts ptr-WRITE (W1) and val-WRITE (W2), each
reddening under independent per-arm revert; nestfill_box drives
cgstructlitfilltn's nested recursion (bootstrap-uncovered -- the corpus has no
struct-literal field store -- so the value pin is its only net).
wwstage cgen resolved struct field LAYOUT (offset + field type) by bare-leaf
name (structlookupchain / structlookup / letvarstructinfo) at nine field
READ / addr-of / let-copy receiver arms whose base is a *struct pointer, a
value struct, or a module global. Under a cross-module same-leaf collision
(two modules each exporting a `pair`, 16B vs 24B) the bare-leaf lookup
first-matches the wrong-size struct -> the field is read / addressed / copied
at the wrong offset with the wrong width. cstage type-keys off the stamped
node.type_ (type_chase_named(base->type)->fields) and is correct; this aligns
wwstage UP to it (ww-only change).
Route the nine arms off the stamped receiver tinfo: R1/R2 *struct reads via a
new shared choke-point cgptrfieldloadtf (the tinfo twin of cgptrfieldload);
A1/A2/A3 addr-of and W3 scalar global-store via tichase(recv.type_)->fields;
C1/W4a copy/size via structabisizetn(tichase(.type_)). Mirrors #21 (5ae6e34);
the *struct arms peel the pointee with tichase(.type_).sub, the faithful twin
of cstage type_chase_named(bu->sub).
The global value-struct arms (R2/R3/A3/W3/W4a) are converted-for-construction:
a global struct's type is always explicitly qualified, so the bare leaf already
resolved correctly and they cannot be reddened -- byte-id (cs.s==ww.s) is their
net, not a value pin. Labelled so the absent reddening pin is explained, not
silent.
Commit 1 of a 2-commit arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): closes the
field-LAYOUT read/copy/addr surface. Commit 2 converts the W1/W2/W5/W4b store
loops to a tinfo-native fill (cgstructlitfilltn + sretretsizetn) and closes the
in-loop nested sub-arms by construction. Part of the #224 name-keyed-cgen
cluster retirement.
Pin: test/wcc/797 value-asserts R1 ptr-read / C1 let-copy / A1/A2 addr, each
reddening under independent per-arm revert (a ratchet proves nothing for this
silent-capable class).
wwstage cgen resolved struct LAYOUT by bare-leaf name (structlookup /
structparamsize) at three caller-side sites — the by-value arg push
(cgenutil), the let-receive copy width (cgenstmt), and the field-read
offset (cgenexpr). Under a cross-module same-leaf collision (two modules
each exporting a `pair`, 16B vs 24B) the name lookup first-matches the
WRONG type, so the push dropped the 2nd eightbyte, the receive over-copied,
and the field read the wrong offset. cstage type-keys off the stamped
tinfo and is correct; this aligns wwstage UP to it (ww-only change).
Route all three sites through the stamped node.type_ via a new
structabisizetn(*tinfo) accessor (push + receive) and tichase(type_).fields
(field-read, structlookupchain removed). One commit (rule-11 carve-out):
the collision drives all three at once and no per-site fixture isolates, so
it cannot bisect-split. A scoped slice of the #209/#211 name-keyed-cgen
cluster retirement; the cgdot *struct-ptr/global and let-copy siblings stay
name-keyed and are filed (#31).
New table-driven test 793_xmod_struct_argpush_collide_run (4 scenarios:
push/recv/field over 16B and 12B tails) reddens under a revert of the three
cgen files. Full make test green (336 passed); make sizelint clean.
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).
The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.
Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).
Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.
All byte-id gates green; full make test reports "all 335 tests passed".
Completes the #14 close-by-construction begun by the helper extraction (e7fefa3): every <=24B aggregate register-store now routes through cg_agg_reg_store/cgaggregstore and handles all tail sizes. B/C/D/G fix a silent both-stage drop of a 3/5/6/7-byte tail (byte-id-blind: both stages dropped identically, so the gate could not see it). A/F enable a previously loud-rejected shape (a cgen backend gap, not a type rejection; harec accepts and lowers it). G (global g=f() array) routes symmetrically, dest_padded=false. The #11 arr[i].f scratch loops fold into the helper (dest_padded=true, byte-id zero-change), completing the grep-audit. Pins value-assert each eightbyte (the class is byte-id-blind) and redden under each stage's independent revert; site H's 3/5/6/7 let-receive stays a loud fatal (#22).
A stack array literal returned into a tagged-union slice success variant (fn mk() ([]i32|e) = { return [10,20,30]; }) slipped past reject_arrlit_borrow — it bailed when the dst was not TY_SLICE, but a union dst is TY_TAGGED — so cgen built an all-zeros slice header: a silent both-stage miscompile (and the .ptr would dangle anyway, no outliving backing). Extend the reject to chase a TY_TAGGED dst to its slice success variant, then apply the existing reject; this is the #25/#31 treatment seen through the union, and matches Hare (rule-9). Reached by all acceptance sites (return/assign/call-arg) so the class closes by construction. Both stages converge on an identical accept/reject decision (an array literal assignable to a union is assignable to a slice or array variant; neither stage can accept it). Full support — promoting the literal to an outliving backing — is the separate #33 arc. Compile-error fixture + a positive over-reach guard (a real slice into the union still compiles). No asm emitted by a reject, so byte-id is unchanged (no floor ratchet).
cg_structlit_fill/cgstructlitfill dispatched scalar field stores as {1->MOVB, 4->MOVL, else->MOVQ} with no fsz==2 case, so a 2-byte field was stored with an 8-byte MOVQ. Interior over-stores were harmlessly overwritten by the next field, but the LAST field at the frame edge corrupted the saved base pointer: an (S|e) union success variant places the struct payload after the 8B tag, landing the last field at -4(BP), so MOVQ AX,-4(BP) wrote into saved [BP] and POPQ BP restored garbage — a silent both-stage caller-frame clobber. Route the scalar store through the existing fldstoreop/fieldstoreop helper ({1->MOVB,2->MOVW,4->MOVL,else->MOVQ}), both stages; the #13 graduation comments already pre-documented this resolution. Pure width fix, no loud-stop (scalar widths are always {1,2,4,8} and narrowing is always correct). Value-asserting pin: an i64 sentinel live across the union-maker call (detects the clobber directly) + all members, with a non-union control.
A struct/array success variant in an (S|e)! / r? unwrap dropped eightbytes on BOTH stages (byte-id blind). Two layers: (L1) the unwrap success shuffle (cgtrytaggedshift) matched no arm for a struct/array success and fell to a bare MOVQ DX,AX, materializing only w0 — widen the existing nested-TAGGED shift's gate to admit TY_STRUCT/TY_ARRAY (the in-cap union packs the payload as raw GP words past the tag, so that shift is exact); (L2) the aggregate store arms gated on rhs.kind==N_CALL and stored one word for an unwrap rhs — relax to also admit N_TRYUNW/N_TRYPROP at the three silent store shapes (arr[i]=, single-dot field, indexed-field), reusing the materialise scratch path (now #10-correct). Rule-7 LOUD-STOP for a float-bearing success variant (an SSE eightbyte cannot ride the GP {AX,DX,CX} shift, #165). The four already-loud unwrap consumers (let-receive #7, call-arg #271, assign-existing #49, resolver-field #24) stay loud; global/chained single-dot field (#16) and the sub-8-tail-through-unwrap union-maker frame clobber (#15) are separate follow-ups. Value-asserting pin, reddens under each stage's independent revert.
The in-cap aggregate-receive materialise emitted a single narrow tail MOV that fell to MOVB for a 3/5/6/7-byte sub-8 tail, storing one byte while the scratch->dest copy read the full tail from uninitialised scratch — silently dropping members at the C2c whole-element arm (arr[i]=mk()) and loud-stopping at the #11 field arm. The scratch slot is ceil-8 padded (local_alloc/localadd round to 8) and the copy reads only tsz bytes, so flipping the tail default MOVB->MOVQ stores the full register harmlessly into the slot's own pad (in-bounds for in-cap <=24B); 1/2/4-byte tails stay byte-identical. Both stages symmetric. Removes the now-redundant #11 sub-8-tail loud-stop (keeps the float #165 and over-cap #234 loud-stops). The same narrow-tail materialise recurs at 6 other cstage sites (task #14).
Retires the obsolete idx_dot_aggret_subtail_loud //ww:error fixture (both stages now compile the case) and converts it to a positive cstage run-test; the struct-field shape is byte-id-divergent only via the pre-existing #9 frame-size bug, so the value pin uses array-field shapes. Value-asserting, reddens under each stage's independent revert.
The N_TRYUNW/N_TRYPROP success shuffle materialized {ptr,len,cap} into {AX,BX,CX} only when the success variant was a str; a slice success got only MOVQ DX,AX (ptr), leaving every slice-unwrap consumer (call-arg push, let-receive store, ident-source) reading junk .len/.cap — silent on BOTH stages (byte-id blind, cstage not the oracle). Widen the success gate to type_isstr||type_isslice (cstage) / typeisstr||typeisslice (wwstage) at all four shuffle sites; str and slice share the identical 24B header shuffle. Stays str||slice-specific — a struct success variant uses a different {AX,DX,CX} ABI (task #12). Value-asserting pin (len!=cap, poison-decoy) reddens under each stage's independent revert.
Follows #6 Mech A (Fix-R); order forced (C1 first or the slice call-arg push reddens byte-id).
nodeisslice/nodeisstr lacked an N_TRYUNW/N_TRYPROP arm, so a str/slice produced by an unwrap (f()!, r!, r?) and passed as a call arg fell to the 1-word scalar push, dropping .len/.cap; cstage's type-keyed node_isslice/node_isstr already pushed 3 words. Add the type-keyed arm reading the checker-stamped success-variant n.type_, mirroring #9's N_UN/TK_STAR arm. Fixes the str case (wwstage align-up to cstage); the slice success shuffle that both stages still get wrong is fixed in the Mech B follow-up.
The arr[i].f=src legacy assign block enumerated scalar field-type arms then fell to a 1-word scalar default, so a non-call aggregate source (ident/dot/index) cgexpr'd only its first word into AX and stored one eightbyte — silent on BOTH stages (byte-id blind). The non-indexed bases (local/deref/chained/global) reach the general assign resolver's canonical aggargsrcaddr+aggcopy; the indexed arm short-circuited before it. Route the indexed base through the block's own proven &arr[i] spine into the same aggargsrcaddr+aggcopy emitters (DRY — no third copy), dual-site symmetric. Unlike #11's in-cap arm, the source is a memory address so aggcopy is a pure memcpy: float bits and the sub-8 tail transport verbatim, no loud-stop needed. Did not fall through to the general resolver because its cgplaceaddr N_INDEX arm rejects a *[N]S (TY_PTR) base (latent resolver gap, filed separately).
Contained to the indexed base + non-call aggregate-field rhs; value-asserting pins redden under each stage's independent revert.