Commit Graph

197 Commits

Author SHA1 Message Date
fdfc2ce318 wcc+w6c+w6c_ww: tuple slot layout SSoT — checker size = cgen slot stride (C-t0)
The checker computed TY_TUPLE size as the packed element-size sum
((u32,u32) = 8B) while every cgen cursor-transport site strode 8B
slots (16B). 16B tuples were blind to the split (slot == packed);
packed tuples hit it everywhere: cstage let-receive keyed on sz 16/32
missed sz 8 and dropped word 1, the cgfn param receive spilled
8B/element into a packed-sized local (saved-BP clobber, SIGSEGV), and
mixed (u32,f64)/(u32,str) shapes missed the receive arms entirely.

Slot layout is now the SSoT (user-ratified): the flip lives in the two
checkers' N_TTUPLE size computation only (check.c, check.ww
tupleelemslot + stamp); cgen's packed-keyed walks (t.N read, #235 len
arm, over-cap sret send/receive pair) align onto the slot stride, and
the wwstage t.N read gains the natural-width load (tnodeloadop) to
byte-id with cstage's fldloadop. ttupleelem.offset re-stamped
slot-cumulative (no consumers yet). The #242/#243 eightbyte-share
loud-stop dissolves by construction (no two narrows ever share an
eightbyte) — 940's eightbyte_share row graduates to a runtime
round-trip. Hare-layout divergence documented at both checker sites;
re-alignment is task #60. #32 send skew and #33 wwstage literal-let
receive are separate commits on this base.

941_tuple_slot_layout_run pins the matrix: 4 packed rows fail at the
parent (8/21 checks), 3 neutral anchors prove 16B/32B emission
untouched.
2026-06-04 19:06:54 +09:00
9861f73bbb wcc+w6c+w6c_ww: insert() builtin — single-element slice insertion (part of #35)
Hare's insert(xs[idx], v) (ref/harec/src/check.c:745
check_expr_append_insert — append/insert share the checker arm,
"insert" at :786): checker accepts an INDEX place over a slice plus
one value, stamps void; idx == len is a legal end-insert (the
ref/hare os/exec/platform_cmd.ha:86 idiom). Loud-rejects with exact
texts: spread form insert(xs[i], vs...) (filed, #35 — also covers
harec's with-length form via the arity check), range place (not
Hare; harec only parses ACCESS_INDEX, :784), non-index operands,
array bases, wrong arity. delete()-parity throughout.

Lowering (both stages, converged byte-identical by construction) is
a DESUGAR: append(xs, v) — reusing append's grow (rt_ensure) and the
entire #34 value-store dispatch (scalar / str-slice header / tagged
widen / struct fill) verbatim, one boxing choke-point — lands v at
slot len-1; then a rotate-right of [idx, len) moves it home through
a fresh per-site esz frame scratch (@insscr). The rotate is delete's
shift loop in reverse (descending j, the safe memmove-up direction)
and is a same-slice whole-stride raw byte move — no boxing exists
for any element kind. idx evaluates BEFORE the grow (Hare's
left-to-right operand order — pinned by the pregrow_len_idx row,
insert(xs[len(xs)-1], v): pre-grow [7,13,11] vs post-grow [7,11,13];
an idx==len(xs) end-insert cannot discriminate, the rotate
degenerates either way). Base shapes: local slice ident (LEAQ) and
deref-of-local ptr-to-slice (MOVQ); others rule-7 loud-stop, like
delete.

test/807: 57 fixtures — front/middle/end + idx==len via len(xs) +
the pre-grow eval-order pin, esz 1/2/4/8/16/24/56 (MOVB/MOVW/MOVL
tails, struct body, str header, 7-qword tagged from a typed local
[the regex fold-3 ha:347 newinst shape] and from a cast rvalue
[ha:419/441]), empty-slice grow, (*p)[i] deref base, front-insert
loop, 6 checker reject rows with diagnostic-text checks; every
accept row cs==ww asm byte-id.
2026-06-04 17:24:57 +09:00
c34a48a81f wcc+w6c_ww: append() struct-element sources via split place-resolve (#49)
#49 (#35's single-element sibling, tranche-C pre-check PC2): the
struct-element append arm dispatched on SOURCE node kind — N_STRUCTLIT
(literal fill) and N_IDENT (local word-copy) only; every
place-resolvable chain died on the rule-7 fatal in BOTH stages,
including search()'s result-build line
`append(res, threads[best_idx].root_capture)` (regex.ha:819).

Wire those shapes with a SPLIT resolve around the grow (the #49
ruling): the chain's rvalues — deref-root pointer expr, index expr —
evaluate exactly once PRE-grow into @appendsroot/@appendsoff (an index
reading the slice header sees the pre-append len, Hare's argument
order), then only the BASE re-derives POST-grow from the live storage
and the stashed offsets land back on top, so a self-append source
re-roots in the post-realloc buffer. harec resolves an aggregate
source address wholly PRE-grow (gen.c: gen_load returns the address
for STORAGE_STRUCT, gen_store copies after rt.ensure) — a
use-after-free under a reclaiming allocator; per #263 we align to the
runtime-correct side, not the reference. A pointer ALIASING the grown
buffer keeps Hare's own stale-base hole (sound today only because
rt/malloc.ww never reclaims). Supported shapes are bounded: root
(local/global ident | deref) + at most one index + trailing direct
fields; all else stays on the #34 fatal, including CALL rvalues (the
#42-style bound, new reject row pins the text in both stages). The
N_STRUCTLIT/N_IDENT fast-paths keep their emission byte-identical.

806_append_place grows eight rows: indexed-field 56B capture (the
ha:819 shape, header readback), computed-index whole element,
deref-spine param pair, deref source, self-append ×33 crossing three
cap-doubling reallocs, the split-order semantics pin (a CALLED index
helper reading len must run once and see the PRE-grow len — the
pre-split emission failed exactly there), an element-kind ×
place-source matrix row (scalar/narrow/str/slice/tagged route via the
pre-existing arms — regression net), and the CALL-source reject. The
six fix rows verified FAILING against a pristine 796d41b build on
both drivers (loud #34 fatal, identically in cstage and w6c_ww —
there was no silent path at master); 70/70 fixtures green here
including per-row cs/ww asm byte-cmp.

Unblocks regex fold-2b tranche C (search) — PC2 was the lone
pre-check failure; PC1/PC3/PC4 passed at base.
2026-06-04 14:37:24 +09:00
796d41bb9f wcc+w6c_ww: len() over place-resolved operands (F2/FA2)
C5 (tasks #10 + #41): the len() builtin's operand handling was an arm
enumeration that leaked FOUR siblings over time (#235 tuple-elem →
#19 indexed-elem → F2 len(xs[i].field) → FA2/FB1 len(*p)) — every
unhandled slice/str operand shape fell to a bare cgexpr fallback that
returned the slice DATA POINTER as the length. Silent ptr-garbage,
byte-id both stages, gate-blind. Probing at d642017 surfaced the full
family: len(*p) (param 48 / local 64), len(xs[i].field) (147),
len((*p)[i].field) (10), len(s.field) (75), len(p.field) (87) — plus
the same garbage for non-place operands len("abc") (40),
len(xs[1:3]) (48), len(mk()) (0). Review widened it twice more:
len(**pp) (chained deref, garbage 208 at e481cb8) and the EMPTY-slice
deref (len 0 reported as .ptr — masked by exit-code truncation, hence
the branchy test row).

The enumeration is closed by construction (ken's verdict): enumerated
fast-paths keep their pre-fix asm byte-identically (ident local/global,
#235 tuple element — not resolver-addressable, cgplaceaddr has no
TY_TUPLE hop — #19 indexed element, TY_ARRAY const fold), then ONE
uniform header-place route via cgplaceaddr resolves every other
slice/str place and reads the .len word at place+8 (the same offset
math as the ident arm). Non-place operands (string literal, slicing
expr, call result) die LOUD per rule 7 — previously the same silent
ptr-garbage; Hare instead const-folds len of literals, that parity is
filed as #46. The ident arm's off==0 non-let residue (MOVQ 8(BP)
garbage) now also routes resolver-or-loud. cstage's dispatch peel is
aligned to wwstage's existing TY_NAMED loop-chase (single-peel +
loud tail would have surfaced as cs-rejects/ww-accepts on 2-level
aliases).

Asm-neutrality: all five embedded main.combined.ww corpora compile
byte-identically under pristine-parent w6c vs fixed w6c; per-shape
pins (ident local/global, tuple, index, array) NEUTRAL + cs==ww.
802_lenidx_run grows 14 rows: the nine garbage shapes (incl. computed
index through a deref spine, param-vs-local *p, chained **pp, empty
slice), two neutrality controls (global and tuple fast-paths have
dedicated runs: 797, 903), three reject rows pinning the exact rule-7
text in BOTH stages; all fix rows verified FAILING against a pristine
build of the parent e481cb8 (12/19 fail there, 19/19 green here).

Consumers unblocked: regex fold-2b tranche C ha:795/798
len(threads[i].captures); lib/regex add_thread's (*threads).len
dodge (regex.ww:238, WHY comment cites #41) reverts to len(*threads)
with the tranche-C port, not here.
2026-06-04 13:37:19 +09:00
d642017643 wcc+w6c_ww: aggregate let/range element copies via cgplaceaddr (F5)
F5 (task #7): the N_LET aggregate-copy arm's source-addr enumeration
(cgen.c #265/#268) had TY_ARRAY-ident/N_DOT/N_INDEX bases but no
TY_SLICE base and no deref-spine shapes, so `let e: th = xs[0]` fell
out with havesrc=0 — cstage emitted NOTHING (slot uninitialised),
wwstage fell to its scalar default (8B truncation): gate-blind cs≠ww
(p6min13). Every remaining ADDRESSABLE rhs now resolves through
cgplaceaddr (the C1 resolver; enumerated arms dispatch first, their
asm untouched), and the arm closes by construction with a loud tail —
nothing below it can initialise a >8B struct/array slot, so any
unhandled rhs shape dies loud instead of silently. A pre-tail #38b
guard keeps the established `?`/`!`-on-sret loud-stop marker in
wwstage (mirror of cstage's pre-arm fatal; pre-fix that shape reached
the cgtryunw/cgtryprop gates which the tail now pre-empts in let
position).

Reviewer-C2 inheritance: `let c: capture = (*ts)[i].cap` (aggregate
leaf behind a deref spine) — wwstage's documented cgdot aggregate-leaf
loud is retired for let position (cglet routes the copy through the
resolver before cgexpr sees the leaf; the loud stays as the guard for
non-let expr positions), and cstage's silent no-copy on the same shape
is fixed by the same resolver fallback.

By-value RANGE payloads ride the same class: N_FORRANGE's single-bind
load truncated every aggregate element to one fldloadop word. Both
stages now word-copy the full element extent (MOVQ run + sized
MOVL/MOVW/MOVB tail, the #270-1b idiom) for esz > 8. wwstage esz is
re-keyed elemsizeof→elemsizeofc (the 8-sentinel hid struct elements
from the copy gate — the #8 named-narrow precedent), with a
stamped-slc.type_ fallback + element-tnode synthesis for non-ident
scrutinees (tinfo SSoT, #209/#211). The wwstage checker now binds the
ELEMENT type on single-bind ranges via a synthetic N_LET binder node
(mirror of cstage check.c N_FORRANGE scope_define(..., elem, ...));
pre-fix the binding's decl was the N_FORRANGE node itself, so any
field read off a by-value binding asserttyped-bailed. The checker
half folds in under rule 11 because the split is unsound in either
order: cgen-first is untestable (every field read off the binding
still bails), checker-first converts that loud bail into the 8B
SILENT truncation — only the pair closes the class.

FC0 graduates: regex.finish's by-value range over 24B charset elems
(non-ident scrutinee re.charsets) was the lib/regex byte-cmp's ONLY
hunk since fold-1 — cstage 8-of-24-byte copy + IMULQ $24 vs wwstage
1-byte MOVZBQ, runtime-masked by the no-op loop body. The byte-cmp is
now ZERO hunks (regex_test.combined.ww, w6c vs w6c_ww).

#36 disposition: NOT folded. p6min9/p6min10's remaining failure is the
struct-ident field rhs inside a struct LITERAL (cg_structlit_fill
under-copy) — a different choke-point from the let-copy source-addr
machinery; they still exit 4 here and stay blocked on #36 (read half
landed in C2).

Residual filed as task #43: an UNANNOTATED aggregate let
(`let e = xs[0]`) still skips the wwstage arm (aggn/letslotsize are
annotation-keyed; cstage keys the stamped n->type and now full-copies)
— cs≠ww on that shape remains, #38-family. A landmine comment in
test 805 marks the gap.

test 805: +6 rows — let-from-slice-elem 16B (p6min13 verbatim) /
24B/40B/12B(MOVQ+MOVL tail) matrix / deref-spine leaf / by-value range
([]struct both-fields sum, []capture 40B, []str 24B header) / range
edges (empty slice, by-VALUE binder-mutation pin, 12B elem MOVL tail)
/ reject row pinning the loud-tail text on both stages. All six fail
at the pristine parent 403625e (re-verified post-rebase; 121 prior
fixtures stay green there).
2026-06-04 13:00:03 +09:00
403625e433 wcc+w6c_ww: >48B tagged args from place-resolved sources (#38b cont.)
Task #40 (FB3): the #38b MEMORY-class staging only accepted ident /
aggarg_srcaddr-enumerated sources, loud-stopping on a slice-element
arg — the regex run_thread loop-condition shape
is_consuming_inst(re.insts[threads[i].pc]) (regex.ha:602) and B4's
(*p)[i] spelling. The staging now falls back to cgplaceaddr (the F6
resolver) for the slot's source address; the existing word-push
machinery is unchanged. Placed AFTER aggarg_srcaddr so every
pre-#40 shape keeps its asm byte-exact (verified vs a master w6c).

Boundaries kept loud (rule 7): exact-type rvalue (no place to
resolve), sret-class call source (#40-family follow-up), global
tagged let (task #25), variadic element, register-overflow mixing.
Diagnostic re-worded to match: slice-element is wired, rvalue and
unresolvable-place sources are not.

test/929 grows 6 run rows (element source with literal+computed
index, deref-spine, nested re.insts[threads[i].pc] spelling,
element mixed with register args both orders, fn-call index with
two mem args per call — the inner CALL runs inside the mem
pre-pass with the other slot already staged — and global-slice
element, the storage-backed twin of the loud fail_global_src
boundary — all build-fail loud at master) and 1 reject row pinning
the new boundary text on both stages.
2026-06-04 12:35:07 +09:00
76994a8279 wcc+w6c_ww: typed dot-read resolver — kill both silent N_DOT fallbacks (F4)
A typed depth-2+ field chain behind an index/deref spine
(threads[0].cap.end, (*p)[i].f.g) aborted the cgdot chain walker
(N_IDENT roots only) and fell into the module-qualified-leaf fallback
— a silent `MOVQ <leaf>(SB)` global read of a colliding symbol
(p6min10 exit 66) or a link error. Single-dot reads behind a deref-
index ((*ts)[i].pc, (*p)[i].slicefield) fell into the bottom catch-all
instead, which is offset- and header-blind: offset-0 scalars read
correctly by COINCIDENCE, nonzero offsets and slice headers were
silently wrong in BOTH stages (FA3, pA5). `&threads[0].cap` dropped
the address-of silently and SEGFAULTed at the deref (task #6,
reviewer-A route).

cgplaceaddr (C1) grows an N_IDENT root (local / let / DATA-backed
def) and the N_INDEX base gate relaxes to recursion, so indexed-ident
and deref-rooted spines resolve; enumerated arms still dispatch first,
keeping every pre-C1 shape's asm. case N_DOT routes any TYPED read no
arm matched through the resolver (scalar fldloadop, float X0, str/
slice 3-word header, [N]T address); the module-leaf fallback is gated
to UNTYPED chains, the catch-all to untyped-str pseudo-fields, and
the TK_AMP tail is resolver-or-loud. Leaf kinds without a register
convention (tagged, aggregate) and unaddressable shapes die LOUD
(rule 7). wwstage mirrors symmetrically; two of its arm gaps must not
take the resolver (its sequence differs from cstage's arms — cs!=ww):
ident-indexed alias reads loud-cite C3 (task #8) and non-local-rooted
ptr-chains loud-cite task #37. A third verdict divergence is comment-
documented at the wwstage aggregate gate: cstage's let-init consumes
`let c = (*ts)[i].cap` BEFORE its N_DOT tail (emitting NO copy — the
F5 bug), so that shape cs-builds/ww-louds until the F5 let-copy lands
(task #7); absent from the gate corpus.

806 identroot_dot graduates from BUILD_FAIL: the C2 ident root makes
append(h.xs, v) through *holder resolve via C1.5's place consumer
(run-verified, byte-id). p6min9/p6min10 read-halves are fixed but the
probes stay blocked on the #36 literal under-copy this commit
unmasked (struct-ident field rhs copies 8B; repro filed with the
task).

test/805: +7 rows (typed depth-2 behind ident-index incl the 777
global-collision pin, deref-index, width/float/[N]u8 matrix, FA3
slice-field + .cap-behind-spine, &-route with compound-through-
pointer, C1's reject_tail graduated to stores, neutrality pins) and
+4 reject rows pinning the new loud texts; the C1.25 raw-byte
readbacks graduate to typed depth-2 reads.
2026-06-04 11:07:13 +09:00
b630a7cf20 wcc+w6c_ww: append through pointer-to-slice place via cgplaceaddr (FA1)
Re-key the append() lowering from BP-displacement assumptions onto a
resolver-provided header PLACE (task #15, the add_thread hard-blocker;
cgplaceaddr's third consumer after C1/C1.25). One mirrored choke-point,
two failure modes: cstage 0-defaulted sn_off for any non-ident target,
so 0(BP)/8(BP) became the "slice header" and rt_ensure corrupted the
CALLER frame (SIGSEGV); wwstage cgappend silently emitted nothing
(gate-blind cs!=ww).

cg_append_grow/cg_append_slot (mirror cgappendgrow/cgappendslot) factor
the 5 grow + 5 slot header-access sites. Ident-local targets keep the
legacy BP-disp emission byte-identical (probed across all 9 existing
source shapes, before/after .s). Non-ident targets resolve once through
cgplaceaddr and spill the header address to an @apphdrscr slot:
rt_ensure may realloc .ptr but never moves the header, so the slot
stays valid; every access reloads from it. The slot is allocated fresh
per append SITE, not cached per fn: a nested append-through-pointer
inside a value expression (match-yield arm) spills its own resolve, and
a shared slot would hand the outer grow/slot reloads the inner target's
header — silent cross-slice corruption (pinned by the reentrant_value
row). Indirect mode keys esz/element-kind/load-op off the
checker-stamped target tinfo (no declared tnode behind `*p`; the
#209/#211 discipline). Unwired target places die LOUD "#15: append()
target place unsupported (rule-7)" on BOTH stages — the
silent-corruption class is closed by construction.

The FA4/#35 boundary is unchanged: non-ident spread SOURCES stay loud
(pinned by a reject row). Surfaced pre-existing checker divergence
filed as task #34 (wwstage rejects global slice-lit let).

test/wcc/806: 14 runtime rows (element kinds x target shapes, spread,
narrow-signed spread load, cap-crossing realloc loop with branched
callee + caller-frame sentinels, deref-spine target, nested-append
reentrancy, direct-arm neutrality pin) + 2 exact-text reject rows,
both drivers + per-row cs==ww asm byte-id.
2026-06-04 10:44:18 +09:00
cfc2985c61 wcc+w6c_ww: aggregate-field stores via cgplaceaddr (C1.25)
Wire struct/array/tuple field STORE through the C1 assign-resolver
(task #23): structlit rhs materialises into a FRESH-per-use @placescr
slot (the @slicescr discipline — a cached slot is the #31 multi-live
trap) then word-copies to the resolved address; addressable rhs
(ident/global/dot/deref) sources via aggarg_srcaddr with the dest
spilled around the dispatch (#270-1b order). Kept loud: compound on
aggregate, sret call rhs (#234-tail), <=24B call rhs (task #24),
unaddressable literal rhs, ww-only anonymous-struct structinfo miss.

test/wcc/805: +6 rows (40B structlit incl ... autofill, ident+deref
source, nested literal, [3]u8 MOVW/MOVB and [3]u32 MOVL tails,
two same-size stores in one fn pinning fresh-per-use) +3 exact-text
reject rows. Tuple-field row blocked by the pre-existing tuple
param/let-init word-2 drops (tasks #32/#33, documented in-row).
p7b_capstore_only graduates byte-id and runs; p7_composed builds and
runs on cstage, wwstage stays behind the pre-existing #29 asserttyped
bail (verified identical at master with w6c_ww).
2026-06-04 10:00:16 +09:00
32063d0da0 wcc+w6c_ww: >48B tagged by-value args — MEMORY-class two-phase push (#38b)
Task #19 (the #38b residual surfaced by FC2 evidence): a tagged arg
whose slot exceeds the 6-reg convention (>48B) is MEMORY-class per
ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). Caller
stages the whole slot below every register-class word (two-phase
push, rightmost-first, leftmost mem arg at 16(BP)); callee registers
the param in place at positive BP offsets with zero prologue bytes;
the merged slot count feeds the existing caller-cleanup ADDQ.
Argument-side mirror of the #38 tagged-sret fix, same classify
machinery (tagged_memarg_size / taggedmemargsize beside their
register-class siblings).

Pre-fix, the exact-typed arg loud-stopped on both stages, but
WIDENING a concrete variant into a >48B param slipped the old guard
silently — cstage pushed one scalar word while wwstage emitted an
uncapped greedy stitch (wrong on both AND cs≠ww, gate-blind). Widen
sources now route through the @tagscr scratch for mem slots.

Loud boundaries kept (rule 7), each with its own diagnostic:
sret-class tagged CALL result as mem-arg source (#40-family
follow-up), global tagged let (task #25, broken at any size
pre-existing), >48B variadic element, and mem-arg + register-
overflow mixing (caller check + callee prologue mirror).

Single commit: caller staging, callee receive, and both stages are
one inseparable ABI class — landing any half alone breaks byte-id
or runtime correctness (the #38 flip precedent); test/929 (15
table-driven rows: 56B/64B slots, widen-slip pin, source shapes,
mixed orders both ways, two-mem call, 200k-call loop, 48B-boundary
absence pin byte-id'd vs master, 5 reject rows pinning the exact
per-guard diagnostic on both stages) rides with it.
2026-06-04 09:36:15 +09:00
e3e6b5a820 wcc+w6c_ww: cgplaceaddr resolver — deref-base assign stores (F6)
(*ts)[i].field = v / OP= v (the regex run_thread hot shape, task #4)
compiled to NOTHING in both stages, byte-identically: the N_DOT lhs
roots at N_UN(STAR), so the arr[i].field arm (idxbase must be IDENT)
and the chained-ptr-field arm (base must be *struct) both miss and
the N_ASSIGN dispatch fell off the switch silently, rhs unevaluated.

cgplaceaddr (one per stage) is ADDRESS COMPUTATION ONLY — N_UN(STAR)
root, N_INDEX hop over a slice/array place (.ptr hop for slice),
N_DOT struct-field hop with one deref for a *struct base. Call-sites
keep their own emission: scalar fldstoreop store, str/slice 3-word
header store staged through DX, 10-op compound template with the
chained-ptr-field register roles. Ident-rooted spines stay with the
enumerated arms — verified asm-neutral over the 84 fold2b probe
sources against fresh master-HEAD binaries (7 diffs = the F6 family
now emitting stores; 2 verdict flips = aggregate-field stores, now
loud).

Silent dispatch tails go LOUD for N_DOT lvalues the resolver can't
address and for unresolved-identifier targets (cstage float-ident arm
aligned to wwstage's resolve-first order). Aggregate-field stores
loud-reject pending the follow-up resolver commit (task #23, ≤24B
N_CALL rhs split to #24). The non-DOT tail stays silent deliberately:
going loud there would asymmetrically surface the pre-existing
str-base element-store divergence — task #22, cited at both sites.

test/805: 17 rows x 2 drivers + 12 cs==ww byte-id fixtures — widths
(incl narrow-compound fldloadop sign/zero-extension), all 10 compound
ops (DIVQ/IDIVQ/SHLQ/SARQ/SHRQ), str + slice 3-word stores, *[N]T
base, runtime call index, ident-base neutrality pins, and 5 reject
rows asserting exact diagnostic text.
2026-06-04 09:13:16 +09:00
37febab9d5 wcc+w6c+w6c_ww: delete() builtin — single-element slice removal (part of #35)
Hare's delete(xs[i]) (ref/harec/src/check.c:1981-2027): checker accepts
an N_INDEX over a slice-typed base, stamps void; loud-rejects the range
form delete(xs[i..j]) (stays filed on #35 — regex fold-2b's consumers
are all single-element), non-index operands, array bases, wrong arity.

Lowering (both stages, converged byte-identical by construction):
ascending word-copy loop shifts [i+1..len) down one esz stride, then
hdr.len -= 1; cap unchanged. The move is a same-type whole-stride byte
copy — src and dst are elements of the SAME slice, so no boxing exists
for any element kind; one loop serves scalar/narrow/str/struct/tagged.
esz off the STAMPED base type (#34/#48 discipline). Base shapes: local
slice ident (LEAQ) and deref-of-local ptr-to-slice (MOVQ — the fold-2b
delete_thread shape); others rule-7 loud-stop.

test/804: 38 fixtures — first/middle/last/to-empty, esz 1/4/8/24/56
(MOVB/MOVL tails + 7-qword tagged), cap-unchanged, (*threads)[i], 4
checker reject rows; every accept row cs==ww asm byte-id.
2026-06-04 06:41:12 +09:00
9732061a7e w6c+w6c_ww: free() compiles to a no-op (ww has no free) (fix #27)
The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.

930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
2026-06-04 06:07:18 +09:00
c2308a11c7 w6c+w6c_ww: size-keyed @tagscr — one tagged scratch per slot size (fix #44)
A fn mixing two tagged slot sizes smaller-first (regex compile(): 56B
append-element widen then 64B sret return) hit the #15/#26c rule-7
grow-fatal — the single shared per-fn @tagscr is first-use-sized and
its pinned offset can't grow. Key the scratch by slot size instead:
@tagscr<sz>, one first-use-allocated slot per distinct size, all three
sites (widen-store via_outer, widen-push, N_INDEX tagged-element
assign) funnelled through cg_tagscr_slot / tagscradd in both stages.
Single-size fns emit byte-identical asm to pre-fix (control row pinned
+ hand-cmp'd vs master w6c). 736's tagscr_size_grow_fatal fixture
pinned the now-unreachable fatal; converted to a byte-id succ row.
Runtime rows live in 926_tagscr_sizes_run.
2026-06-04 04:34:54 +09:00
147a8a26b9 w6c: unstale the cg_sret_retsize header doc for the #38 tagged arm (#38 review) 2026-06-04 04:01:15 +09:00
4f3967835e w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)
A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP
eightbytes = 32B slot); wider slots were silently truncated at the
return crossing — payload word 4+ built in the callee frame and died
there, byte-identical on both stages (gate-blind). Blocks regex fold-2a
((regex | error | nomem) = 64B slot).

Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm
(<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B
boundary class is pinned unchanged byte-for-byte vs master). Callee:
cgreturn writes the slot through *(@sretarg) via the existing widener
non-BP base (bare return stores the void tag); exact-type 'return f();'
rides the #9 sret-forward. Receive: let/assign/discard reuse the
generic #23/#10 sret protocol; the match scrutinee passes its spill
slot as the sret dest (tagged-specific, no tuple precedent).

This could NOT land as a gate-first interim loud-stop (the planned
#38a): lib/errors/errors.ww errno() already returns a 40B
(errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent —
so a bare gate breaks the build. errno graduates to sret here instead;
errnotest pins it at runtime (its cstage run; the wwstage run was
already failing at master via an unrelated pre-existing indirect-call
arg-classification divergence, reported separately) and test/926's
errno-shaped row reads the previously-dropped tail word on both stages.

The unwired cursor consumers of an sret-class call result loud-stop
(rule 7) rather than read a cursor the callee no longer fills:
widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed
#40), ?/!/is/as operands, argument position, and the >48B tagged-arg
class both stages previously mishandled silently. One-class-one-commit
per the #133 carve-out: post-flip those consumers would read AX (now
the dest pointer) as the tag — a gates-trailing commit would leave a
silently-wrong bisect point, so the flip and its gates are not
separable.

test/926: 15 rows — 56B regex-shaped round-trips (literal/local/
assign/match-scrutinee/forward/str-variant/multi-call), 40B repro +
bare-return-void, the errno-shaped tail-read graduation row, 32B
boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop
rows pinned as build failures on both stages.
2026-06-04 03:47:44 +09:00
5f15eb3d09 w6c+w6c_ww: tagged widen-store struct-literal slice field keeps the 3-word header
The cg_widen_tagged_store / cgwidentaggedstorebp struct-payload arm
gated the 3-word {ptr,len,cap} field store on TY_STR only; a slice-
typed field inside a union-payload struct literal fell to the scalar
tail and silently dropped .len/.cap (the #24 gap's widener twin, both
stages symmetric so byte-id gates were blind). Surfaced by #38's
regex-shaped consumer: wide{xs: []u8, ys: []u8, n} widened into
(wide | error | nomem).
2026-06-04 03:42:15 +09:00
36be9f469d w6c+w6c_ww: loud-stop non-ident/non-local append spread source (#34 review)
A spread whose source was not a local ident fell PAST the spread arm:
cstage continued into the single-value stores with the N_SPREAD node
(garbage store), wwstage silently SKIPPED the value entirely — a silent,
cs!=ww-divergent miscompile (append(ys, f()...): cs exit 0 / ww exit 144,
want 3), reachable for every element kind and predating #34 for scalars.
Both stages now rule-7 loud-stop the shape (deferred, task #37).

Also pins the widener's already-tagged single-value source path
(tagged_ident_src row, i64 + bool members) and adds the
spread_call_loudstop BUILD_FAIL row — test 800 is now 15 rows / 43
fixtures.
2026-06-04 02:27:06 +09:00
faade48513 w6c+w6c_ww: append() stores the full element width per element kind (fix #34)
Both stages lowered the append element store as one sized mov from AX —
correct only for scalars <= 8B. A str/slice element kept only .ptr
(byte-id-blind), a tagged element got its raw payload written into the
tag slot (the #12 pathology, no boxing), a struct element kept only its
first qword. wwstage additionally fed rt_ensure membsz from bare
elemsizeof, whose 8-sentinel under-allocated and mis-strided named
tagged/struct elements (the #8 family; cs!=ww on the SI imm + stride).

Fix, keyed on the DECLARED slice local's element type (cstage
su->sub->size as before; wwstage elemsizeofc off the stamped tnode —
never the value node, the #25/#31 esz=0 trap), applied to both the
single-value and spread bodies (2 arms x 2 stages):

- scalar 1/2/4/8: untouched (u8 asm byte-identical to pre-fix).
- str/slice: AX/BX/CX pushed across rt_ensure, dst in DX (BX holds the
  element .len after the pops — the #24 register discipline), 3-word
  store.
- tagged: grow first, dst -> BX, box via the #12 widen choke-point
  (cg_widen_tagged_store / cgwidentaggedstore via_outer).
- struct: grow first; literal -> dst spilled to per-fn @appendscr
  (cached on cstage to mirror wwstage's @-prefix localadd dedup) +
  structlit fill DST_PTR_LOCAL; local ident -> word-copy; any other
  source shape is a rule-7 loud-stop, never a silent scalar
  fall-through. struct-from-call deferred.
- spread: the source element is already a fully-formed T (tag
  included), so the wide arm grows first and whole-width word-copies
  &items[i] -> dst, recomputing both addresses from the slice headers
  after the possibly-reallocating rt_ensure.

The elemsizeofc swap also corrects the named-scalar-alias membsz
(wwstage fed SI=$8 where cstage fed $4); no in-tree consumer appended
to such a slice, so nothing was riding the wrong 8 (lib/selfhost append
sites are all u8).

Test 800_append_wide_elem: 13 rows (runtime readback per kind, 2-append
realloc survival, spread str+tagged, @appendscr dedup, enum-alias esz,
loud-stop build-fail) + per-row cs==ww byte-id, which subsumes the
frame canary.
2026-06-04 02:14:14 +09:00
bf1037d8c4 wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31)
A one-step `let xs: []T = [e0,e1,..]` had two faults. #31 (silent, cs!=ww):
the #258 array→slice borrow wrapped the un-addressable N_ARRLIT directly as
the N_SLICE base and cgen never spilled it to a stack slot, so .ptr dangled
(`let xs:[]i32=[10,20,30]; xs[1]` returned the un-stored header 1; []u8/[]str
segfaulted). #25 (over-strict): a slice target fell through to the exact-
element type_eq borrow gate, rejecting bare-int-width ([]u8=[1,2,3]) and str
elements the array-init path coerces.

Fix (re-stamp + per-borrow scratch; both stages byte-identical asm):
 - Checker re-stamps the slice arrlit as [count]T, reusing the array-init
   per-element coercion + range-check (#25): in-range accepts, out-of-range
   loud-rejects. cstage arrlit_init_fits gains a TY_SLICE arm; wwstage
   checkletassign mirrors it and stashes the synthesized [count]T tnode on
   arrlit.lhs (free for N_ARRLIT) so cgen can size the backing NODE-wise
   (elemsizeofc) and count from the tnode's .rhs intlit — the arrlit's own
   value tinfo carries the literal's untyped element (unsized), so node-first
   sizing is required (a cstage/wwstage representation divergence; cstage's
   Type IS sized and reads base->type).
 - cgen materialises the N_ARRLIT borrow base into a FRESH per-borrow
   @slicescr stack slot (distinct slot per borrow: a borrow's backing must
   outlive the lowering, so it can't share a cached @aggargscr/@tagscr-style
   slot — two live borrows would alias one backing; localalloc/local_alloc
   is always-fresh), filled by REUSING the array-init element fill extracted
   from the N_LET path (cstage cg_arrlit_fill_bp, wwstage cgarrlitfillbp —
   same store sequence the byte-id-green `let a:[N]T=[..]` uses, the
   frame-order + store-op guarantee), then LEAQ'd as the base.

Supported ONLY at a `let` init. In call-arg / return / assign position
there is no addressable backing, so both stages LOUD-REJECT ("bind it to a
`let` first") — aligning cstage DOWN to wwstage (which already refused the
untyped arrlit element) per rule-10; this closes #31's silent call-arg
segfault as a compile error. Full non-let support is deferred (#33).

Escape (rule-8 WHY): a `let xs:[]T=[..]; return xs;` returns a slice into a
freed frame slot = dangling, IDENTICAL to the pre-existing named-array
borrow and Hare-consistent (no escape analysis / GC / heap promotion).

Test 953_arrlit_slice_run: 8 accept rows (cstage runtime readback +
cs==ww byte-id, frame-size canary incl.) covering the #31 i32 pin, bare-int→u8
coercion, str readback, the multi-live soundness pin (xs[0]+ys[0]=5, not 8 —
proves fresh-per-borrow), and a mutate-through-borrow proof; 4 reject rows
(out-of-range element + the three non-let contexts, loud in both stages).
Tuple-element slices stay blocked by the pre-existing #30 array-init FATAL.
2026-06-04 01:44:39 +09:00
c490ed3ec1 w6c+w6c_ww: store full slice header for struct-literal slice fields (fix #24)
cg_structlit_fill / cgstructlitfill had a TY_STR arm that stored all
three header words (ptr@+0, len@+8, cap@+16) but no TY_SLICE arm, so a
slice field in a struct literal `cl{ items = b, n = .. }` fell through to
the generic scalar tail and stored only the ptr word — the field's .len
and .cap read 0. str fields (the same 24B {ptr,len,cap} shape) worked;
slice fields silently dropped two words.

Both stages emitted IDENTICAL wrong asm, so the 990-997 byte-id gate was
green on both-wrong; runtime readback is the only correctness net. Same
is_str/is_slice discrimination gap as #10 part-b, here in the
struct-literal field-init path.

A slice is the same 24B header shape as str, so widen the str arm's
guard to TY_STR || TY_SLICE (cstage) / isstrtype || isslicetype
(wwstage) and let a slice ride the already-correct 3-word store. The
TAGGED arm stays ordered before it, so a nullable/tagged slice
(TY_TAGGED) still routes to the widener, not the 3-word store.

Test 689 (table-driven, runtime readback + dual-stage asm byte-id):
slice .len/.cap/.ptr, a scalar field beside/before the slice, a slice at
a non-zero field offset, two slice fields, and a str field beside a
slice (str-arm regression pin). 33/33 ok.
2026-06-03 23:51:28 +09:00
8dda8ea76c w6c+w6c_ww: global-base arm for indexed struct-element field read (fix #21)
The `arr[i].field` N_DOT read branch in both stages was gated on a LOCAL
base lookup (cstage `localfind != 0`, wwstage `localfindnode != nil`). A
module-GLOBAL base (`let g: [2]pt = [...]`) missed it:

  - cstage fell to a generic index-load that drops f->offset — it read
    element[i] at offset 0, so `g[i].b` returned a's value (g[0].b -> 1,
    g[1].b -> 3 instead of 2, 4).
  - wwstage fell to the module-qualified SB fallback — garbage, no main.g
    load at all.

Silent, byte-id-divergent. This is the READ twin of #11 (the global
`g[i] = v` write fix) and the #15 sibling. Local `[N]struct` bases read
correctly (tests 680/681 cover only those), which is why it was never
caught.

Fix (both stages, converged byte-identical): resolve the global the same
way the N_INDEX arm does — cstage `let_islet || def_isarraydef`, wwstage
`letvartnode || defvartnode` — and dispatch the base load by shape: array
-> LEAQ name(SB) (the symbol IS the storage), slice/ptr -> MOVQ name(SB)
(the symbol's first word IS the .ptr). The field then loads at f->offset
exactly as the local arm does. esz (element stride) and f->offset both
come from the type table (rule 13). Mirrors #11's write-side global-base
resolution. combined.ww embeds (w6c + wwdump) regenerate.

688_global_arr_elem_field: global `[2]pt` reads of .a/.b on both elements
(the .b reads are the bug), a non-8-aligned `[2]rec {tag:u8,x:i32,y:i64}`
to stress f->offset + a u8 sub-word leaf, and a slice-base read
(`let g: []rec = arr;`) that exercises the MOVQ-deref .ptr arm. Runtime
(cstage build+run) + cstage==wwstage byte-id per row. The slice row is
byte-id ONLY: its read asm is correct and identical on both stages, but a
slice-of-struct module global does not data-emit a symbol yet (a separate,
pre-existing data-emission gap, sibling of #10/#20), so it cannot link/run.
2026-06-03 22:38:02 +09:00
d40224755a w6c+w6c_ww: emit module-level slice-literal static-init (header+backing+reloc) (fix #10 part a)
`let g: []T = [v0, v1, …];` at module scope had no cgen arm: emit_lets /
emitletdataw handled str-lit and array-lit but not slice-lit, so NO
`DATAW main.g` was emitted and BOTH stages failed to link ("undefined
reference to main.g"). byte-id-blind — only the link step exposed it.

emit_slice_data / emitslicedata (parallel to the #18 str-array reloc
helper, generalized to a 24B header + array-backed data):
  1. writable backing DATAW "<mangled g>.d" holding the k element bytes,
     routed through the emit_array_lit_bytes / emitarraylitbytes choke-
     point via a synthesized [k]T (int/float element kinds reduce exactly
     as a [N]T global's do);
  2. 24B header { ptr-placeholder, LE len, LE cap } (len = cap = k), word
     sizes from the type table (ty_uintptr/ty_size, primtypesize) per
     rule-13;
  3. DATAR g+0 -> backing patches the ptr word.
The backing label's second '.' can't collide with a user global (source
identifiers carry no '.').

New emit_lets / slice arm gated on N_ARRLIT + slice-typed; rides on #18,
which keeps the module-level initializer as N_ARRLIT in both stages.

Aliased-slice spelling (`type S = []T; let g: S = [...]`): cstage
let_isslice already resolves the alias via type_unwrap, but wwstage
letvarisslice keyed only on the syntactic N_TSLICE node — unlike its
siblings letvarisstr/letvarisstruct/letvarisfloat, which all walk the
N_TNAME alias chain. So an aliased-slice global misrouted to the str arm
and never reached emitslicedata, link-failing on wwstage while cstage
emitted correctly (a cs≠ww divergence this fix would otherwise introduce).
letvarisslice now walks the alias chain exactly as letvarisstr does
(align wwstage UP to runtime-correct cstage, the #211 pattern); an alias
of a slice IS a slice. emitslicedata gains the nil/non-slice guard cstage
emit_slice_data already had (rule-10 symmetry; unreachable behind the
gate, guards the su.sub deref).

rule-7 loud-stops, symmetric both stages: read-only `def` slice-literal
(DATAR holder must be DATAW, w6a asm.c:362), `...` repeat (a slice
literal has no target length), and slice-of-{str,slice,tagged} elements
(per-element relocs / #17) — never silent no-emit.

Deferred (filed): struct-element module-level slice-literal surfaces a
separate checker cs!=ww ("let: not assignable" on wwstage, wrong runtime
on cstage) — out of #10's data-emission scope.

Test 687 (table-driven): []u8/[]i64/[]i32 element read-back + len + cap +
1-element edge + aliased-slice-type, dual-stage runtime + asm byte-id,
plus 3 build-fail rows for the loud-stops. selfhost combined.ww
regenerated.
2026-06-03 22:05:48 +09:00
63142770de w6c+w6c_ww: box [N]tagged array-literal elements via the tagged-store path (fix #12)
A [N]tagged-union array-literal element fell through the is_agg
multi-word-copy path (STRUCT/ARRAY/TUPLE/str/slice only) to the scalar
1-word store: the raw value landed in word 0 (the tag slot) with no tag
written and no payload boxed, so a later match found no variant. Both
stages under-copied identically, so the copy-depth bug was byte-id-blind
— a stride-only fix would still store 1 word and pass the gate green on
both-wrong.

Route each tagged element through cg_widen_tagged_store / the N_LET "BP"
tagged-store wrapper — the same choke-point let-init, vararg gather and
struct-field stores already use — so boxing, tag-remap and zero-pad-to-
slot come for free. esz now comes from the stamped slot size (rule-13);
the wwstage narrow override only covered widths 1/2/4, leaving a 16/24B
tagged element on the wrong 8-byte sentinel stride. rule-7 loud-stops
the unwired `[N]tagged=[x...]` repeat-fill (the widen call consumes the
node and trashes AX).

test/wcc/685: table-driven runtime readback (106/42/13) + a build-fail
row for the repeat-fill loud-stop, both stages.
2026-06-03 20:32:22 +09:00
84ed2ab15a w6c+cgen: read .cap of an indexed str/slice array element (fix #13, #20 read-sibling)
`t[i].cap` (t a `[N][]u8` / `[N]str`) miscompiled in BOTH stages,
divergently — the read-side sibling of #20's store fix. cgexpr on the
indexed element leaves the full {ptr,len,cap} header (AX/BX/CX via
cgslicehdr), but the `.cap` field-selector never shuffled CX→AX:
cstage's typed pseudo-field else-branch handled only .ptr/.len, so
`.cap` fell through returning AX=.ptr; wwstage's cgdot non-ident
catch-all likewise handled only .ptr/.len, emitting no read (stale AX).
`t[i].len` already worked (BX→AX shuffle) — only `.cap` was missing.

Fix mirrors the .len shuffle: add the .cap CX→AX arm in both stages.
The shuffle fires ONLY for a typed slice/str base (TY_SLICE/TY_STR
after NAMED-chase); an untyped str literal (`"abc".cap`) leaves only
AX=ptr/BX=len and must return AX unshuffled — keeping the wwstage
catch-all byte-identical with cstage, whose cap-shuffle lives in the
typed branch, not the untyped catch-all.

Validated direct `t[i].cap` (slice + str, elements 0/1) against the
whole-element-copy oracle (`let q=t[i]; q.cap`, made correct by #20),
plus .len-after-index regression pins, in test 683; dual-stage runtime
+ byte-id (36/36 ok). combined.ww regenerated.
2026-06-03 17:50:54 +09:00
b3d4d2df32 w6c+cgen: full 24B header store for str/slice array-literal elements (fix #20, #270 str/slice arm)
A `let t: [N][]u8 = [a, b]` / `[N]str` literal init lowered each
element's {ptr,len,cap} header into AX/BX/CX (cgexpr) but stored only
some words: a slice element fell through to the scalar 1-word MOVQ
(dropping .len AND .cap), a str element stored 2 words (dropping .cap,
latent). Each element is 24B (post-#1) and must be copied whole.
wwstage was worse — a slice element matched no esz branch, so esz
stayed the 8 sentinel: the per-element stride collapsed (element i+1
overwrote element i's tail), the -96-vs-80 cs!=ww frame divergence.

This is the str/slice arm of the #270 aggregate-element-store family.
struct/array/tuple already copy correctly via the #270-1c is_agg
multi-word path; str/slice were the documented follow-up (cgen.c:9037,
cgenstmt.ww deferral). They can't join is_agg (that path word-copies
from a source slot and rejects non-ident/structlit elements, whereas
str/slice elements are commonly exprs cgexpr lowers into registers) —
the correct mechanism is the existing register header store, extended.

Fix (BOTH stages, converged byte-identical): cstage adds
is_slice_el = type_isslice(esub) and stores 3 words (incl CX->base+16,
the cap) for `is_str_el || is_slice_el`, in the main loop and the
repeat-fill. wwstage adds isslicel (esubti.kind == TY_SLICE -> esz =
esubti.size, fixing the stride) and the matching 3-word store. Closes
[N][]u8 (the bug) and the latent [N]str cap-drop in one branch.

The latent str cap-drop is now stored, but the indexed-element `.cap`
READ (`t[i].cap`) stays broken — a distinct cgindex/dot-selector bug,
cs!=ww divergent, filed as task #13. The new test validates the stored
cap via a whole-element copy (`let q = t[i]; q.cap`), which reads
through the correct ident-load path. [N]tagged literal init is the
remaining sibling (is_agg excludes TY_TAGGED), task #12.

Test 683_arr_strslice_elem: table-driven, dual-stage runtime + asm
byte-id; slice/str .len, 3-element stride-24, cap-via-copy, .ptr deref,
plus a [N]struct regression pin proving the is_agg path is untouched.
2026-06-03 17:35:54 +09:00
6b67655eca w6c+cgen: len() over indexed str/slice element extracts .len (fix #19)
`len(xs[i])` over a [N]str/[]str (and []T slice) element returned the
element's .ptr, not its length, on BOTH stages (shared gap, not rule-10):
the len() builtin had no N_INDEX arm, so it fell to the bare-cgexpr
fallback, where the N_INDEX str/slice load (cgslicehdr) leaves AX=.ptr,
BX=.len, CX=.cap — and len() returned AX (the ptr) as the length.

Add an N_INDEX arm gated on a (TY_SLICE||TY_STR) element in both stages:
cgexpr the element, then MOVQ BX,AX to shuffle the len word into the
result reg — the same shape as the #14 .len pseudo-field fix. Byte-id
neutral (no bootstrap source uses len(indexed-element)); regenerated
w6c + wwdump combined.ww. New 802_lenidx_run pins runtime + cs==ww.
2026-06-03 11:56:16 +09:00
58c8f4be02 w6c+cgen: emit module-level [N]str static-init data + relocations (fix #18)
A module-level `let xs: [N]str = ["a","b",...];` static init emitted no
.data: a str element carries a ptr->rodata relocation, not just bytes, so
it fell through the byte-only array-emit path and left the table symbol
undefined (w6l: undefined reference). Shared gap on both stages, not
rule-10.

emit_strarray_data / emitstrarraydata apply the scalar-str-global pattern
per element at offset idx*esz: a DATAW row of {0-ptr placeholder, LE len,
cap} plus a per-element DATAR sym+idx*esz,_S_n reloc. let_pre_intern /
letpreintern pre-intern each element strlit so the _S_ rodata rows precede
the DATAR references. Stride routes through etype->size (rule 13). Scoped
to the DATAW (`let`) directive: A_DATAR requires a DATAW holder, so
`def [N]str` and str-in-aggregate stay a filed follow-up.

919_strarray_static_run pins runtime (len-sum, element .ptr deref, var
index, empty slot, repeat suffix) + cs==ww byte-id. w6c + wwdump
combined.ww regenerated.
2026-06-03 11:35:03 +09:00
711762b6d8 w6c: emit length for string-literal .len (fix #14, align cstage to wwstage)
A string literal is TY_UNTYPED_STR, not TY_STR, so `"abc".len` missed
the typed slice/str pseudo-field gate in cgen.c's N_DOT and fell to the
final base-eval fallback, which left AX=.ptr — `.len` returned the
pointer instead of the length. wwstage's cgdot catch-all already did the
BX->AX shuffle, so the two stages diverged (rule-10). Align cstage UP:
the N_DOT fallback emits MOVQ BX,AX for `.len`. `.ptr` is unchanged
(already returned AX); `.cap` deliberately not added (wwstage catch-all
is ptr/len only — mirror exactly).

byte-id was blind here: no bootstrap source uses literal `.len` (lengths
are hardcoded around literals), so the gate never exercised it. New test
801 pins both dimensions (cstage run + cs==ww byte-id) over
len/empty/multibyte/ptr-deref/arg-passthrough rows.
2026-06-03 00:28:16 +09:00
418dd21f34 w6c+wwstage: wwstage alias-aggregate-return loud-stop + #276 citations (#272 review)
Review fixes for the #272 fold (reviewer272b gate; rob+ken ruling). Bundled
because the wwstage catch-all message carries the citation and the combined.ww
regen covers both .ww edits.

- wwstage cgreturn close-by-construction catch-all keyed on the SYNTACTIC
  return-type node (N_TARRAY / N_TNAME+structlookup), so a named-alias
  aggregate return type (type a=[N]T / type a=struct) bypassed both the
  handling arms AND the loud-stop, falling to the scalar default = silent
  segfault/truncation; cstage (type_chase_named at all 4 N_RETURN sites)
  stayed correct. Re-key the catch-all on the RESOLVED tinfo (chase
  TY_NAMED -> TY_ARRAY/TY_STRUCT) so wwstage LOUD-STOPS (rule 7) instead of
  miscompiling. cstage stays correct; the full wwstage tinfo-kind dispatch
  (align UP, byte-id) is #277. Established wwstage-stricter divergence
  (cf #264), no bootstrap consumer (990-997 green).

- #276 citations at-site (both stages): the cstage >24B array-literal return
  loud-stop and the <=24B STRUCT global-receive residual now cite #276. The
  wwstage >24B array-literal routes through the tinfo-keyed catch-all
  (#272/#276/#277). Correction: ALL <=24B struct globals truncate
  symmetrically (byte-id-clean), not only float-bearing -- #276 broadened.

- Cosmetic: fix a double-encoded U+2264 (mojibake) in the cgen.c commit-2
  comment.

combined.ww regenerated (#110).
2026-06-02 15:26:03 +09:00
9d81ba77b7 w6c+wwstage: array global-aggregate-receive g = f() (#272 commit-2)
The caller-half of the global case: `g = mk()` into a GLOBAL array
stored only the first word — a ≤24B reg-return landed `MOVQ AX, g(SB)`
(8 of 24 bytes); a >24B sret-return hit the #220 sret-to-symbol gate
which was TY_STRUCT-only and fell through to the same truncation.

≤24B: the local aggregate-receive arm was `off != 0`-only, so a global
array fell to the scalar IDENT store. Add a global ARRAY arm — LEAQ
name(SB), DI then store the full+tail words from AX/DX/CX (an array is
never float-class, so AX/DX/CX is always the transport; no `g+8(SB)`
operand form exists). Mirrors the str/slice global arm.
>24B: add TY_ARRAY to the #220 sret-to-symbol gate (cg_sret_dest_sym /
sretdestnode) — the callee writes the whole array through RDI.

A ≤24B STRUCT global receive can be float-class (X0/X1, not AX/DX/CX),
so it is left at its pre-existing symmetric behaviour — no consumer.

949_aggret_source_run gains global_recv (c → 15) and global_recv_sret
(>24B → 22), both with per-row byte-id.
2026-06-02 15:00:55 +09:00
0d39129741 w6c+wwstage: aggregate return from any addressable source (#272 commit-1)
The N_RETURN aggregate arms gated the return source on N_IDENT ||
N_STRUCTLIT; every other aggregate rvalue (array literal, o.field N_DOT,
a[i] N_INDEX, *p deref) fell through to the scalar-AX default = a silent
8-byte truncation. Both stages emitted byte-IDENTICAL wrong asm, so the
byte-id gate could not catch it (#263 class) — the fix converges on the
runtime oracle.

Mirror the arg-side closure #271 landed: both arms (≤24B @retscr and
>24B sret) now funnel N_ARRLIT through the literal element fill and
N_DOT/N_INDEX/deref through aggarg_srcaddr + the #265/#268 whole-
aggregate copy. Type-agnostic, so struct AND array returns are closed.
A close-by-construction loud-stop (rule 7) guards any future unhandled
aggregate source from reaching the scalar default.

Closes the callee-half of (b)/(c) and the addressable siblings. The
g = mk() global-receive caller-half is commit-2.

949_aggret_source_run pins the class: array-literal / N_DOT / N_INDEX /
deref / named-ident control / >24B-sret-deref / struct-field / struct-
deref, each summing all members (full readback) with per-row byte-id.
2026-06-02 14:52:44 +09:00
42dd70dc0c w6c+wwstage: aggregate arg from any non-ident source via the closed addr machinery (#271) — close aggregate-arg family
Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).

The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).

The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.

#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.

949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
2026-06-02 14:01:03 +09:00
3c37b98164 w6c+wwstage: [N]struct literal element store (#270-1c)
`let x: [2]inner = [inner{..}, inner{..}]` left the array unpopulated:
the N_ARRLIT per-element store handled scalar/str/float ONLY, so a
struct/array/tuple element hit the multi-word-store gap and stored just
the first 8 bytes (cs0/ww0). Both stages symmetric-broken; converge on
the populated result (#263).

Fix: an aggregate element of an array literal fills each element slot
from its source — cg_structlit_fill_bp for an N_STRUCTLIT element,
word-copy for an N_IDENT element (reusing COMMIT 2's per-element copy
shape). esz is the element's natural size (cstage esub->size). cgen.c
N_ARRLIT arm + cgenstmt.ww cglet. An aggregate `...` repeat and other
element shapes hard-stop loud (rule-7).

949 rows: arrlit_structlit, arrlit_structident (8B struct, byteid=1,
full readback). All 96 pass; test-unit 241 green; smoke OK.
2026-06-02 12:54:01 +09:00
6f18f42a4a w6c+wwstage: &aggregate-array-element addressing + store/copy (#270-1)
The array-of-struct element store/copy family — one primitive (&(array
element) for an AGGREGATE element, used as address, never deref/truncate)
across three consumers. Both stages were symmetric-broken; converge on
the runtime-correct full-address/full-copy (#263).

(1a) `a[i].m[j] = v` (a:[N]struct) segfaulted: the `arr[i].field` arm
computed &a[i] then DEREF'd it (loaded the struct's first 8 bytes as a
value) for an `[N]T`-typed field → garbage base. Now an array-typed
field of an array element leaves the field ADDRESS (the #135 read-side,
applied to the array-element base). cgen.c arm + cgenexpr.ww cgdot
N_INDEX-lhs branch.

(1b) `a[i] = aggregateval` truncated the copy to an 8B MOVQ. New
aggregate (struct/array/tuple >8B) element-store branch word-copies the
element from the rhs source address (ident / N_DOT field / `*p` deref) —
the WRITE-twin of the #268 let-init loop. cgen.c N_INDEX store +
cgenexpr.ww cgassign.

(3a) `let c = x.arr[i]` (N_DOT base) / `let c = a[i][j]` (nested) dropped
the copy: the #268 let-init N_INDEX source-addr arm was N_IDENT-base-
gated. Now computes &base[idx] via cg_dotbase_addr (N_DOT field) or the
&abase[bidx] spine (nested N_IDENT-array base). cgen.c N_LET +
cgenstmt.ww cglet.

949 rows: elemfield_store, elem_struct_store, elem_arr_store,
letcopy_{dot,nest}_prim, letcopy_subarr (byteid=1); letcopy_{dot,nest}_
struct (byteid=0 — run-correct, byte-id blocked by the orthogonal
value-nested-struct frame divergence #254). All 94 pass; test-unit 241
green.
2026-06-02 12:49:25 +09:00
ebbc3f98c2 w6c+wwstage: array return-by-value via the struct-return ABI (#267 fold-2)
Wire TY_ARRAY into the existing struct-return gates so arrays ride the
same reg-class (<=24B in AX:DX:CX) / sret-class (>24B) path the struct
return ABI already emits byte-identically. No new ABI machinery.

Both stages, uniform gate-widen:
- cg_sret_retsize / sretretsize: +TY_ARRAY (natural size sub.size*len,
  the type table) -> auto-enables sret send/recv + the >24B sret N_IDENT
  word-copy + return-forward, all keyed on the shared sret SSoT.
- cgreturn <=24B reg-send: +TY_ARRAY (N_IDENT scratch word-copy ->
  AX/DX/CX). reg-class return-forward rides the default cgexpr passthrough.
- let-init / assign <=24B recv: +TY_ARRAY (AX/DX/CX sized stores).

struct_float_class stays struct-only: pure-int element arrays only; no
pure-float-array-return consumer exists today.

949 +11 rows: reg-class 8/16/24B + sret-class 32B, [N]u32 and [N]u8,
at let-init/assign/return-forward, full-member readback, + a struct-
return regression control. All cstage-run + cs==ww byte-id.
2026-06-02 12:00:58 +09:00
35b517ca3e w6c+wwstage: aggregate let-init copy from a struct-DEF global (#268 reviewer)
The fold-1b unified arm (bb2f4e1) added an N_IDENT addressable-rhs source
setup, but the two stages gated the GLOBAL case differently: cstage used
let_islet || def_isarraydef, wwstage used isletvar || deflookup (ANY def).
On a struct-typed `def` used as an aggregate-copy rhs (`let c: T = G`)
wwstage copied the whole value (correct) while cstage truncated to the 8B
scalar tail — a cs!=ww divergence (rule-10). A struct-LET global already
copies on both, so the def gap was also an internal cstage inconsistency.

Struct defs are first-class laid-out aggregates (DATA storage + field
load, #129 A.2/A.3), so converge on the correct full copy on both: add
def_isstructdef to cstage's predicate and replace wwstage's broad
deflookup with the def_is{array,struct}def pairing already held identical
in defisaddressable. 949 +2 rows (array-def + struct-def global, full
readback, byteid=1).
2026-06-02 11:37:34 +09:00
bb2f4e1dfe w6c+wwstage: aggregate let-init copy for ident-array/N_DOT/N_INDEX rhs (#268 fold-1b) — close addressable-rhs copy family
#265 fold-1 landed the deref-rhs aggregate copy as one slot→slot memcpy
loop fed from a source address in SI. fold-1b adds the remaining
addressable-rhs source-address setups, all routed into that SAME loop:

  - array IDENT `let c: [N]T = s`  — LEAQ the source slot into SI.
    Pre-fix both stages truncated to the 8B scalar tail.
  - N_DOT field `let c: A = o.i`   — cg_dotchain_addr / dotchainaddr
    (#253) lands &(o.i) in SI. Pre-fix truncated to 8B.
  - N_INDEX element `let c: A = a[i]` — the &base[i] spine (#252:
    scaled index + LEAQ base) lands the element address in SI. Pre-fix
    scalar-loaded the element address as a value → segfault.

Size (the #254 non-slot-padded ABI extent) comes from the declared let
type for every shape (lu->size / structabisize|tinfo.size), independent
of the rhs; only the per-rhs address setup differs. The deref arm
becomes one branch of the unified arm. Struct-IDENT keeps its own #32
slot-copy arm above (unchanged). With those, the whole addressable-rhs
let-init-copy family is closed by construction: struct-ident / array-
ident / deref / N_DOT / N_INDEX all full-copy, both stages byte-identical
(rule-10).

949 gains 9 full-readback rows (every member written distinct + summed,
so a partial copy fails): array-ident 16B/32B + 12B(MOVL)/11B(MOVW+MOVB)
tails; N_DOT struct-field 16B + array-field 32B + 11B-tail struct field;
N_INDEX struct element 16B/32B. The N_INDEX source array is populated
through a `*inner` to `&a[i]` (the #135/#252 store path) because the
array-of-struct element direct store (`a[i].m[j]=v` / `a[i]=s` / struct-
array literal) segfaults on a SEPARATE pre-existing bug, reported
alongside this fold. w6c+wwdump combined.ww regen (#110). 70/70 949,
test-unit 241, sizelint, smoke green.
2026-06-02 11:22:01 +09:00
4d3f8467a8 w6c+wwstage: full-size aggregate copy for deref-rhs let-init (#265 fold-1)
A `let c: T = *p` (T a struct or array, >8B) copied no full aggregate:
cstage dropped the init entirely (c read garbage); wwstage emitted only
the scalar `MOVQ AX,off(BP)` tail (first 8 bytes). Both wrong, differently
— converge BOTH stages on a size-driven slot-to-slot memcpy: cgexpr the
deref operand to the source address in AX, MOVQ AX,SI, then a MOVQ run
plus a sized MOVL/MOVW/MOVB tail over the #254 non-slot-padded ABI extent
(lu->size / structabisize for a struct, tinfo.size for an array). Mirror
arms in cgen.c N_LET and cgenstmt.ww cglet, byte-identical (rule-10).

Unblocks sha256's faithful `let copy = *h`. The by-value aggregate RETURN
ABI (array/struct return truncates to AX) is fold-2 (#267, deferred).

949 gains 6 full-readback rows (every member written distinct + summed,
so a truncated copy fails): struct{[4]u32} 16B, struct{[8]u32} 32B via
both *(&s) and *p (sha256 shape), bare [4]u32, and non-8-mult tails
([3]u32 12B → MOVL, [11]u8 11B → MOVW+MOVB). w6c+wwdump combined.ww regen
(#110). 61/61 949, test-unit 240, sizelint, smoke green.
2026-06-02 09:31:44 +09:00
0afe4225cd w6c: materialize full tagged slot on N_IDENT-source return (#263) — cstage align-up to wwstage
cgreturn's passthrough predicate was TYPE-only (istagged && type-eq), with
no source-kind filter. It forwarded the source's AX/DX/CX unchanged, which
is correct ONLY when the source already materialized the full tagged slot
into registers — N_CALL / N_INDEX / N_DOT (the #261-broadened set). For a
tagged LOCAL ident, cgexpr loads only word0 (the tag) into AX, never the
payload into DX, so passthrough dropped the payload: `return v` of a
`(i32|void)=7i32` exited 0 instead of 7. wwstage was already correct — its
forwardtagged kind filter excludes N_IDENT, routing it through the
scratch-widen path. The runtime oracle (cstage 0, wwstage 7) proved cstage
is the bug; this aligns cstage UP.

Gate passthrough to {N_CALL,N_INDEX,N_DOT}; a tagged-ident return now falls
to the existing scratch-slot widen path (cg_widen_tagged_store tagged-subset
N_IDENT arm), byte-identical to wwstage's return scratch-widen. cstage-only
(no combined.ww regen — combined.ww embeds the unchanged wwstage source;
byte-id is blind here, the new 949 rows are the net).

test/949: tagged_ident_ret_i32 (7) + tagged_ident_ret_void (void tag
survives) + register-resident controls tagged_call_ret_ctrl /
tagged_dot_ret_ctrl (passthrough must still fire); INDEX control already
present. All dual-stage run + cs==ww byte-id.
2026-06-02 06:28:00 +09:00
6bcb0929f8 w6c+wwstage: tagged-element indexed store via dotbaseaddr + align dotchainaddr guard (#259,#256)
#259: the tagged-union array-field indexed STORE arm computed &arr[i]
from a non-ident base (`x.o[1]=v` where o:[N](T|void)) with a plain
cgexpr(base) — the N_DOT array field auto-derefs (loads the field's
first 8 bytes AS a pointer) -> garbage dest -> SEGFAULT. Route the base
through the array-gated helper cg_dotbase_addr/dotbaseaddr (dst BX keeps
the scaled index live in AX; viaptr + chained handled by the shared
helper), mirroring #257. Symmetric both stages. This was the last
unrouted cgexpr(base) cell in the array-field-base-address family
(#135/#252/#253/#255/#257) — proof-grep of both stages now shows ZERO
unrouted base cells in the slice/decay/addr/index/store builders, so the
family is closed by construction. (The chained-ptr-field scalar/str/
float store sites at cgenexpr.ww:6489+ / cgen.c:4379+ correctly cgexpr
the pointer spine and are the #133 family, not array-field-address.)

#256: align wwstage dotchainaddr's N_IDENT non-local arm to carry
cstage cg_dotchain_addr's `let_islet || def_isstructdef` guard (here
isletvar || deflookup) instead of emitting LEAQ name(SB) unconditionally.
Unreachable on valid input (a struct-typed chain root is always local /
let-global / struct def) so zero divergent asm — never-silent ethos only.

Tests (949): store-only byte-id rows (tagged_store_own/_ptr) gate the
#259 store base-address emission cs==ww; store+readback rows
(tagged_store_*_rd) are run-only (cstage) proving the store wrote the
right slot (66/77) and no longer segfaults. byte-id on the readback rows
is blocked by an ORTHOGONAL newly-surfaced divergence in the N_DOT-base
tagged-element READ materialization (sibling of #255: wwstage loads one
word + zeroes the tag where cstage copies the full 16-byte slot) — the
store base is already byte-id; only the read-back diverges. Reported
separately for triage.

combined.ww regen'd (w6c + wwdump embed cgen).
2026-06-02 05:17:45 +09:00
0f2587d294 w6c+wwstage: struct-array-field slice as call-arg via dotbaseaddr (#257)
An inline slice of a struct `[N]T`-field passed DIRECTLY as a call
argument (`rd(x.o[lo:hi])`) materialized the slice .ptr from the field
VALUE, not its ADDRESS: the pushargs/pushargsrev N_SLICE inline builder's
non-ident else-arm did plain cgexpr(base), so the N_DOT field auto-derefs
(MOVL field,AX used as .ptr) -> callee derefs garbage -> SEGFAULT. The
let-init / assign-rhs / return / hoist-to-local contexts already routed
through the cgslice #252 choke-point; only this call-arg builder kept a
private duplicate. cs==ww both segfaulted identically pre-fix (gate-blind).

Fix (symmetric both stages):
  - route the else-arm through cg_dotbase_addr / dotbaseaddr (the cgslice
    #252 choke-point: array-field-gated, so `[]T`/str/`*T` fields fall
    through to cgexpr; chained inner `o.p.m` handled via its #253 arm);
  - extend the N_IDENT-only esz gate to N_DOT bases, taking the element
    width from the checker-stamped base->type (rule-13 type table), so
    non-u8 call-arg slices scale stride.

Before: `MOVL -8(BP),AX; PUSHQ AX` (field value as .ptr). After:
`LEAQ -8(BP),AX; PUSHQ AX` (field address). cs==ww byte-identical.

Helper note: used dotbaseaddr (not dotchainaddr as first scoped) — it is
the established cgslice choke-point and is array-field-gated, so a slice/
str-typed field base keeps the correct cgexpr header-ptr load; bare
dotchainaddr lacks that gate and would mis-emit the field address for
those. dotbaseaddr already handles the chained `o.p.m` inner via #253.

Tests: test/wcc/949 gains 6 call-arg rows (u8, i32-esz-stride, via-*struct,
chained, + hoist-to-local and bare-local-array controls), each run-
correctness AND cs==ww byte-id.

PROOF-GREP residual: the tagged-union-element indexed-STORE arm
(cgen.c:~4972 / cgenexpr.ww:~5024) is the same N_DOT-base auto-deref shape,
still unrouted in BOTH stages (symmetric, segfaults) — a distinct
consumption axis filed separately; NOT fixed here.
2026-06-02 04:38:41 +09:00
585ec50676 w6c+wwstage: chained-base array-field address via dotbaseaddr — close the family (#253)
cg_dotbase_addr / dotbaseaddr rejected a non-ident inner, so a chained
base (`o.p.m[i]` / `o.i.m[i]` / `o.a.b.m[i]`) fell to cgexpr(base) which
auto-derefs the array field's first 8 bytes AS a pointer -> garbage base
-> segfault (base64 fillobuf `s.enc.encmap[...]` blocker). Extend the one
helper per stage to accept a chained inner: a new cg_dotchain_addr /
dotchainaddr recovers the container base via the dot-chain spine (recurse
to &x, deref when x is a *struct, sum field offsets), keeping the same
no-AX/no-stack spill contract. dotbaseaddr then takes the pointer VALUE of
inner when viaptr, else its ADDRESS, and adds the field offset. One fix
closes every op (index r/w, addr-of, slice, compound) since all route
through the helper. Symmetric cs==ww byte-id.

test/949: +22 rows. Chained-PTR (rd/wr/addr/slice x2/compound), deeper
(value+ptr leaf links, triple-pointer exercising the internal deref),
non-u8 esz stride (i32 addr+slice), and single-level controls — all
byte-id. The chained VALUE-container arm (`o.i.m`) is run-only (byteid=0):
it needs a value nested-struct instance, which trips THREE orthogonal
pre-existing cs!=ww emission divergences (bare-let zero-init policy,
global DATAW byte count, i32 element-load opcode in the index fallback)
unrelated to #253. Run correctness proves the segfault is gone for that
cell; byte-id there awaits the separate wwstage value-nested-struct fix.
2026-06-02 03:44:49 +09:00
5ebd9eb6db w6c+wwstage: addr-of/slice struct array-field via dotbaseaddr (#252)
Taking &x.o[i] (address-of) or slicing x.o[lo:hi] / x.o[lo:] of a
struct's [N]T-typed FIELD computed the field's VALUE as the base
address (MOVL off(BP),AX) instead of its ADDRESS (LEAQ off(BP),AX) ->
garbage pointer -> segfault. The index read/write path was fixed in
#135; this is the unwired addr-of + slice sibling — both base-address
paths fell to the generic cgexpr(base) auto-deref.

Wire the #135 cg_dotbase_addr / dotbaseaddr helper into the addr-of
N_INDEX complex-base arm and the N_SLICE base arm, symmetric on both
stages (guarded if(!dotbase) cgexpr(base)). Extend the slice element
stride (esz) and default-hi length to an N_DOT array-field base too,
read from the field's element tinfo / array length via the type table
(rule-13) — so non-u8 element slices scale correctly and s.obuf[lo:]
gets the array's element count.

cstage already derived default-hi via base->type (alen); only wwstage
needed the N_DOT default-hi arm. cs==ww byte-identical on every shape.

test/949_dotbase_addr_slice_run: 7 dual-stage rows (addr-of local +
*struct param, explicit + default-hi u8 slice, non-u8 [4]i32 stride,
bare-local control), run + cs==ww byte-id. Regen w6c/wwdump combined.ww.
2026-06-02 02:52:59 +09:00
0fb4bae337 w6c+wwstage: store struct-literal array-field init (#249 BUG A)
A struct literal initialising an array-typed field as a local
(`e{ encmap = [..] }`) silently dropped the initializer: cg_structlit_fill
(cstage) / cgstructlitfill (wwstage) had no TY_ARRAY field arm, so the
array field fell to the generic scalar tail — cgexpr the N_ARRLIT (→ AX≈0)
then store one sized word — losing every element. cstage returned 0;
wwstage emitted byte-identical wrong code. (The GLOBAL literal-init path
is unaffected: it goes through emit_struct_lit_bytes, already correct via
#129 A.3.)

Both stages now element-wise store the N_ARRLIT at base+field_off+i*esz,
reusing the proven N_LET array-init shape (cgen.c:8467 / cgenstmt.ww:1393)
for int and float elements plus its `...` repeat fill; esz routes through
the type table (rule 13). str/slice/struct/tagged ELEMENT arrays are the
N_LET path's documented multi-word gap (cgen.c:8462) — converted from the
silent drop to a LOUD rule-7 error in both stages, not left silent.
Symmetric both stages (rule 10), byte-identical .s.

The `...` repeat in a struct-literal array field is checker-unreachable
today (the field type-check rejects `[v...]` length inference — a
separate checker gap); the arm mirrors N_LET's repeat for symmetry.

Test 949_structlit_arrfield_run: +local literal-init reads (idx 0 / last
element), cstage run + cs==ww byte-id.
2026-06-02 01:19:46 +09:00
16b519465a w6c+wwstage: read array field of a global struct (#249 BUG B)
Reading an array-typed field of a module-global struct value (`G.arr[i]`)
silently miscompiled: the N_INDEX fallback's cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) helper — the #135 sibling that computes &(s.field)
for a `[N]T` field — had no module-global-struct base arm. cstage emitted
`LEAQ (BP)` (localfind returns 0 for a global, so it read the stack frame
→ 0); wwstage's localfindnode returned nil and the fallback keyed on the
FIELD name, so it returned false and the caller's cgexpr(N_DOT) loaded the
field VALUE as a pointer → SEGFAULT. The .data was already correct
(emit_struct_lit_bytes #129 A.3); only the READ base address was wrong.

Both stages now emit `LEAQ name(SB) (+ ADDQ field_off)` for a global
value-struct base, mirroring the scalar global-field read (cgen.c:7532);
const globals resolve via def_isstructdef. Symmetric both stages (rule
10), byte-identical .s. Unblocks base64's `const std_encoding.encmap[i]`
reads (#22).

Test 949_structlit_arrfield_run: global `let`/`def` struct array-field
read, cstage run + cs==ww byte-id.
2026-06-02 01:12:40 +09:00
5d023c0ef0 w6c+wwstage: cgexpr materializes tuple rvalues + unwrap-shift for tuple-payload destructure (#241)
cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:

  - an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
  - a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
    `let q = t`), leaving DX/CX stale;
  - the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
    stranding word1 in CX (the scalar/str success ABI).

Fix (both stages, byte-identical per rule 10):

  - cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
    cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
    N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
    (cg_tuple_slot_to_cursor / cgtupleslottocursor);
  - the ?/! unwrap shifts a tuple success payload down one integer reg past
    the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
    loud-stopping a float/slice/str payload element (the SysV per-eightbyte
    tagged-tuple-payload classification is #243);
  - wwstage's checker recovers the popped match-arm binder type for a
    `yield <binder>` operand (matchyieldtype's scope-free fallback to the
    arm's declared type), so the destructured binders stamp — cstage reads
    the operand's already-stamped ->type, wwstage caches only a tinfo.

Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.

NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.

Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
2026-06-01 21:27:49 +09:00
6fc85f9aaf w6c+wwstage: construct + bind tuple-in-union payload (#242)
A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):

(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
    shuffle, which ZEROED tag + payload — the operands were never packed.
    Route the tuple variant through the scratch-slot widen path; add a
    TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
    union payload at the register-ABI 8B stride + sets the variant tag.

(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
    bound union payload) left the un-annotated binders UNTYPED, so the bin
    node reading them was untyped -> asserttyped abort. The element-type
    distribution only fired for an N_CALL rhs. Consume the rhs tuple type
    for ANY rhs (mirror cstage check.c:2017).

(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
    left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
    IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
    Copy each element from the ident's slot at the 8B stride.

Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.

Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:

  - a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
    caught by the 8+payload > slot-size guard (the eightbyte tuple
    classification is #243);

  - a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
    `7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
    -1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
    so flatvariantidxt WOULD resolve the variant — a program cstage rejects
    but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
    mirrors cstage's CONDITION (a bare-literal element), not its -1
    mechanism, with an explicit guard that aligns the richer side DOWN. Lift
    BOTH guards together when #241 lands cstage literal typing -> symmetric
    accept.

Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
2026-06-01 20:24:43 +09:00
b79f005489 w6c+wwstage: agree on mixed-scalar tuple sret layout (#240)
An over-cap tuple mixing a scalar with slices/str (e.g. (int,[]u8,str),
56B) laid out differently in the two stages — gate-blind, since no
bootstrap path returns such a tuple. Two silent cs!=ww bugs, one per
ABI side:

  - callee SEND (cstage cgen.c N_RETURN over-cap-tuple arm): foff
    advanced by the LITERAL expression's type size. A bare int literal
    element is stamped TY_UNTYPED_INT (size 0), so `e->type->size`
    added 0 for a leading scalar — the next element clobbered it at
    offset 0 and every trailing element packed 8 bytes low. wwstage
    already sized from the return-type tuple (c.fnret.list), so the
    callee frames diverged. Fix: size foff from cg_ret_type's tuple
    params (rule-13 type table), aligning cstage to wwstage and to the
    t.N reader's f->offset.

  - caller RECEIVE (wwstage cgenstmt.ww cglet N_TTUPLE arm): the
    in-cap register tuple-receive branch had no capacity gate, so a
    56B over-cap tuple was received via AX/DX/CX/R8 (+ R8 fill)
    instead of from the sret dest the callee wrote. cstage gates the
    twin branch on `sz == 16 || sz == 32` and falls over-cap tuples
    through to the sret receive. Fix: add the same size gate to
    wwstage, aligning it to cstage.

Both stages now emit byte-identical asm and the value round-trips.
Regen w6c + wwdump combined.ww (cgenstmt embeds in both).

New 940_mixed_scalar_tuple_sret_run: leading/trailing/middle scalar
shapes, annotated + inferred let, each self-asserting every element
(scalar direct, slice/str via len) — both drivers exit 0 + cs==ww
byte-id (12/12).
2026-06-01 19:01:58 +09:00
6acddc3a82 w6c+wwstage: len() of tuple-element slice reads .len not .ptr (#235)
len() special-cased only a plain N_IDENT slice operand (load .len at
BP+off+8) and an array operand (fold $alen); every other shape fell back
to a bare cgexpr(operand), which for a slice leaves AX=.ptr. A tuple-
element read (t.N) loads only AX=.ptr, so len(t.N) on a slice/str tuple
element returned the slice's .ptr word AS its length — a silent
miscompile, gate-blind because the bootstrap never does len() on a
slice-typed tuple element (sibling of the #234/#237 tuple-sret cluster).

Both stages: detect a slice/str tuple-element len() operand and load the
element's .len word directly at BP + element_off + 8, mirroring the
N_IDENT slice arm and the tuple-field-offset walk (element_off sums
preceding element sizes through the type table). Byte-identical asm
(rule 10). The separate tuple-element-read full-header gap is #238; a
leading-scalar mixed-tuple has its own pre-existing sret-layout cs/ww
divergence, filed apart from #235.

Test 903_tuple_elem_slice_len_run: 4 slice/str-only tuple rows (two/
three slices, str+slice, slice+str; distinct lengths), build+run both
drivers + cs==ww byte-id. 12/12 ok.
2026-06-01 18:23:29 +09:00