Commit Graph

90 Commits

Author SHA1 Message Date
d617a698b0 selfhost/cmd/wcc: route cgalloc field-store foff through emitdispreg
Four ad-hoc emit sites in cgalloc's N_STRUCTLIT field-store loop
(cgenexpr.ww:2770-2802) wrote the displacement via
emitint(foff: i64); emitline("(REG)\n"), producing 0(REG) for
foff=0. cstage's txt.c:130-134 omits the zero displacement, so
ww2.s (cstage compiling wwstage) and ww3.s (wwstage compiling
wwstage) would diverge the moment any selfhost site migrates to
alloc(T{...})!. Dormant today only because selfhost source has
no alloc(T{...})! yet.

Route the four sites through emitdispreg (cgen.ww:786), the
existing SSoT that already omits zero displacement.

Extends test/wcc/758_cgalloc_str_field.c with 4 table-driven
asm_disp_rows pinning the displacement text for {str/int/f64
at offset 0, str at offset 8}. Internal subtest count: 16 → 20.
The 3 foff=0 rows fail without the fix.
2026-05-20 17:41:41 +09:00
4c51bce244 selfhost/cmd/wcc: route cgalloc CALL through ffiresolve
Wwstage's cgalloc hardcoded `CALL rt_alloc(SB)` at cgenexpr.ww:2747 and
cgenstmt.ww:647. Cstage already routes through ffi_resolve("alloc")
at cmd/w6c/cgen.c:4149 — when a fixture lacks the @symbol("rt_alloc")
decl in scope, cstage falls back to `CALL alloc(SB)` while wwstage
still emits `CALL rt_alloc(SB)`. The divergence is dormant in
ww build (combined.ww always pulls lib/os/os.ww's decl) but activates
under direct `w6c file.ww` and any other single-file path.

Replace the hardcoded line with the ffiresolve(c, "alloc") pattern
already used for user-function calls. The @symbol decl in lib/os/os.ww
is unchanged and propagates via the combine step.

Extends test/wcc/758_cgalloc_str_field.c with 4 table-driven asm rows
that compile a fixture via direct w6c (no combine) and `cmp` the
CALL <sym>(SB) line between stages. The 3 noscope rows fail without
the fix and pass with it; the withsym row pins the positive ffi-hit
path. Test count internal: 12 → 16; total make test: 132/132.
2026-05-20 17:18:32 +09:00
af1549d9c5 selfhost/cmd/wcc: cgalloc str-field store + regression test
wwstage cgalloc N_STRUCTLIT branch emitted MOVQ AX,foff(BX) for every
non-float field. For a str field the cgexpr result is (AX=ptr, BX=len)
and the single MOVQ clobbered BX with the heap pointer, dropping len.
Mirror cmd/w6c/cgen.c:4184-4190: isstrtype branch routes through CX
so BX=len survives. TY_STR only — slice/tagged/fn-pair have the same
gap on both stages (task #23, parked behind Phase 2).

New test/wcc/758_cgalloc_str_field.c is table-driven (6 rows), fails
without the fix under wwstage with predicted exit codes.
2026-05-20 16:51:32 +09:00
caa72f2365 cmd/w6c+selfhost/wcc: route cgparam/MLET/spill sizes through SSoT
#43 (8e93b31 + 087c85c) routed many sizeof(str) / sizeof(slice)
sites through primtypesize / tyslicesize / ty_*->size, but missed
the cgparam regs-fit, cgparam stack-stitch, cgmlet mixed
scalar+str receive, and vararg slice gather paths in both stages.
A bare #1 bump (str→24B) on top of #43 reds ~60 tests because
those paths still hardcoded 16/24.

Cstage:
- cgen.c:7360-7361 cgmlet: sz0/sz1 → (int)u0->size / (int)u1->size.
- cgen.c:7557 cgparam regs-fit: slice|is_str → (int)pu->size.
- cgen.c:7586 cgparam stack-stitch: same.
- cgen.c:4368 cgcall vararg gather: localoff slice descriptor →
  (int)vsu->size (the cstage twin of cgenexpr.ww:3084).

Wwstage:
- cgendecl.ww:225, :243 cgfnparams: 16 → primtypesize("str"): i32.
- cgenexpr.ww:3084 cgcall vararg gather: 24 → tyslicesize(): i32.

Plus a latent-bug fix at cgenstmt.ww cglet :1031 / :1040: the
str-init and slice-init arms dispatched on size only. Under #1's
str→24, both arms would have fired on a str let (duplicate
MOVQ BX,off+8 + bogus MOVQ CX,off+8). Added isstrtype / isslicetype
kind gates mirroring cstage cgen.c:6439's
`type_isstr(lt) && sz == ty_str->size`. Zero asm change today
because the size constants implicitly disambiguate at 16 vs 24.

Probe with temporary #1 bump (str.size=24) confirms 990_selfhost +
994_w6c_ww go green — the cgen-routing slice for #1 is now
closed. Remaining red under bump is lib/ww/typ.ww's parallel SSoT
seed + stringstest cap*16u64 strides + w6l_ww runtime SIGSEGV;
all tracked separately.

EIGHTBYTES register-count sites (cgen.c:7553-7554, cgendecl.ww:224
/:260) intentionally NOT touched — those are str ABI in-flight
3-reg work (task #34), not slot-width SSoT.
2026-05-20 10:08:15 +09:00
087c85c3cf selfhost/cmd/wcc: route remaining wwstage size dispatch through SSoT
Followup to 8e93b31 (#43).  Audit caught dispatch-gate sites the
sweep missed:

  - cgen.ww letpreintern's `sz == 16` str-let detector — would
    desync from emitletdataw's matching `sz == primtypesize("str"):
    i32` strlit-init branch under #1.
  - cgenstmt.ww cglet str-init MOVQ-BX gate and slice-init MOVQ-BX/CX
    gate (and the belt-and-suspenders N_TSLICE shape check at l.607).
  - cgenexpr.ww cgindex str-element loads (3 sites: globalarr,
    baselocal, generic fallback) and the matching cgassign N_INDEX
    str-element write pair (BX spill + post-index store).

All gates now read `primtypesize("str"): i32` / `tyslicesize(): i32`,
so #1's ty_str.size bump propagates through the same two-place edit
the original commit advertised.  Combined files (w6c/wwdump) updated
in lockstep.

131/131 + 994 + 995 byte-identity green; smoke.combined.ww (lib-only
consumer) emits the same asm pre vs post, confirming the change is
SSoT routing only (no behaviour shift).
2026-05-20 09:06:50 +09:00
61705fb39e cmd+rt+selfhost+test: graduate alloc to (*T | nomem) / ([]T | nomem)
Per Hare convention, alloc is a typed builtin that returns a tagged
union carrying nomem as the OOM variant. Callers spell their policy:
`alloc(T)!` aborts on OOM (the old behavior), `alloc(T)?` propagates
when the enclosing fn already returns nomem.

cstage: check builds TY_TAGGED{*T | nomem} (or {[]T | nomem}); cgen
emits AX=tag, DX=ptr per the general tagged-return ABI (the (*T|!void)
nullable-ptr fold gated in ea76ee4 keeps this clean). wwstage cgalloc
mirrors. rt/alloc.s zeroes AX on syscall error so the builtin's null
check sees a clean 0 instead of mmap's -errno leaking through as a
poisoned pointer.

Migration: 3 `!` sites in test/wcc/700_e2e.c, 1 `!` site in
rt/ensure.ww (preserves the pre-existing sizeof bug tracked by #27),
1 `?` site in selfhost/test/tagged_ptr_ret.ww (allocbox exercises
real `?` propagation against a (*T | nomem) return).

130/130 tests green, 994_w6c_ww + 995_self_rebuild stage byte-identity
preserved. Follow-ups #31 (wwstage checkletassign leniency), #32
(wwstage slice-form gap), #33 (tagged_ptr_ret.ww make-test wiring).
2026-05-19 20:25:14 +09:00
3fe968c8a0 cmd+selfhost+test: gate alloc builtin behind same-module fn alloc
Mirrors the existing abort/assert gates in cstage check.c (strict
same-module lookup rather than scope_lookup_prefer, since lib/os.alloc
under a `use os;` import must not suppress the bare-alloc builtin in
client code). cgen.c shadows the resolution: only fire the rt_alloc
path when the typer left N_CALL.lhs->type == ty_err. wwstage gets a
new samemodfn helper for the matching gate.

Test fixtures: package-main repair for the 3 alloc rows in 700_e2e.c
that the parser was inheriting curmod="os" from the concat'd os.ww;
new shadow-test row asserts a same-module `fn alloc(n: i64) i64`
beats the builtin in cgen.
2026-05-19 18:51:07 +09:00
a1d9f36d11 selfhost+cstage+test: graduate alias-chain unwrap to transitive (#22)
Single-peel TY_NAMED.under bottoms out at the inner alias when
chain length is 2+, surfaces in two stages with different
mechanisms: cstage's gates inline `if (t->kind == TY_NAMED)
t = t->under` at every callsite (cgreturn, cglet sizing, cgexpr
N_DOT, cgassign N_DOT, cg_sret_retsize) — graduated to a
while-loop via new type_chase_named helper across 11 sites.
wwstage routes all field-walks through structlookup, which
registers only direct struct definitions (not aliases) — missing
the alias-recurse fallback. New structlookupchain helper mirrors
slotsize's N_TARRAY arm precedent; sretretsize + 4 cgenexpr.ww
sites route through it. Splitting would either land cstage
without unblocking wwstage's strings.tokenize wrapper shape
(rule 10 byte-id regression) or land wwstage without cstage
gate parity (breaking 995 self-rebuild). 756 sentinel exercises
4 rows × cstage RC + wwstage RC + byte-id = 12 fixtures; pre-fix
rows 2 + 4 (slice-fields single alias, i32 double alias) fail
on both RC and byte-id. The ~67 cstage / ~26 wwstage candidate
sibling sites are #17-style structural-close follow-up; this
commit fixes the immediate strings.tokenize-wrapper blockers.
2026-05-19 15:09:57 +09:00
d5e8d699d1 selfhost: graduate wwstage &N_DOT[N_INDEX] to cstage canonical lean form (#21)
Latent #21 has two surface shapes — register polarity in cgun
TK_AMP N_INDEX's complex-base arm, and indexbaseesz's
over-broad .ptr pseudo-field gate — that share a single semantic
path: &N_DOT[N_INDEX] where the inner N_DOT cannot be peeled
into a plain ident base. Polarity-A (cgenexpr.ww) lifted to
cstage's three-line shape; stride-B (cgenutil.ww) narrowed so
the .ptr arm only fires on actual str/slice inners and falls
through to the generic struct-field arm for struct N_TNAME
bases. The fixes compose at the same call site (esz from
indexbaseesz, then the IMULQ-or-elide gate, then complex-base
emit), so splitting them into two commits would leave a
half-fixed intermediate — neither half stands alone as a
bisect-clean closure. Sentinel 755_amp_dot_idx exercises both
shapes across 4 stride classes (slice-elem 24, struct-elem 16,
u8 stride-1 elide, i64 stride-8); pre-fix 5/12 fail, post-fix
12/12 ok. Latent silent miscompile in lib/memio + lib/bufio's
.ptr[i] shape also unmasked.
2026-05-19 13:43:45 +09:00
006df414aa selfhost+test: route convenience-wrapper N_DOT probes via fnretlookupmod (#17)
Structural close of the #4-trio convenience-wrapper audit. Session-6's
#4-trio + #11/#16 graduated individual lookup helpers (fnret/fnparams/
enum/struct/def) to same-module-first via *mod variants. The close
didn't enumerate every cgcall-context callsite — convenience wrappers
that take a *node callee and probe its return shape via bare-leaf
fnretlookup stripped the N_DOT module hint, same wedge shape as #16
(callee_variadic_param, d9b0c90) through a different family of
consumers.

Eight LATENT sites in selfhost/cmd/wcc fixed (each mirrors #34's
nodeisslice two-arm route — N_IDENT uses cmod=c.curmod, N_DOT uses
cmod=callee.lhs.str, terminal call routes through fnretlookupmod):

- cgenstmt.ww  cgreturn forwardtagged probe
- cgenstmt.ww  cgmlet tuple-return shape probe
- cgenexpr.ww  cgdot fn-rvalue probe (mod.fn LEAQ)
- cgenexpr.ww  cgtryprop succisstr probe
- cgenexpr.ww  cgtryunw  succisstr probe
- cgenutil.ww  callsretsize (sret arg-prep)
- cgenutil.ww  inferletcalltype (let x = f()? tnode)
- cgenutil.ww  rhstaggedabicall N_CALL branch

cstage carries no sister bug: cmd/w6c/cgen.c reads every callee
return shape from the typed n->lhs->type per TY_FN sig. Mirror of
#4d/#28/#31/#34/#16 cstage no-sister notes.

753_convwrap_audit: table-driven sentinel exercising cgmlet's tuple-
shape probe. alpha exports foo() (i64, str); beta exports foo()
(i64, i64); main calls beta.foo() — source order puts alpha LAST so
alpha.foo prepends to head of c.fnrets, pre-fix bare walk picks
alpha's str-branch dispatch for beta's call. Post-fix routes to
beta.foo via fnretlookupmod. Asserts MOVQ\\tCX, absent in main.run
TEXT (no str.len store; would fire pre-fix). Remaining 7 sites
covered structurally by shape-mirror — single wedge shape, single
exercise.

make test 127/127; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 10:44:16 +09:00
5ed6293330 selfhost+test: bump wwstage varargseq per cgcall (#8)
Latent surface from #15: cgcall variadic-gather block read seq from
n.uval, which post-#15 is always 0 because scanlocals (which used to
stamp it during pre-pass) was deleted. Every variadic callsite in a
fn aliased to @vararg_d_0 / @vararg_sl_0. When two callsites in one
fn had differing arities, the second hit #15's first-use+fail-loud
guard ("localadd: @-prefix slot grew within fn") — correctly, since
the slot was being asked to grow mid-fn.

Fix: read seq from c.varargseq + bump in cgcall's gather branch.
Mirrors cstage's mklabel("vararg_d/sl") natural seq bump.
cgeninit zeroes c.varargseq per-fn (existing), so the counter is
correctly per-fn scoped.

cgen.ww varargseq comment refreshed — replaces stale "bumped only at
emit time" misclaim with the post-#15 per-call shape + the #15
grow-on-pin discipline that surfaced the wedge.

751_vararg_seq_percall: table-driven 3 rows x 2 stages = 6 fixtures.
mixed_arity_two_calls (the wedge), same_arity_two_calls (no-regress),
three_arity_drift (1/2/3 mints @vararg_d_0/1/2).

make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.

Surfaced by worker-strcontains2 attempting strings.contains tagged-
variadic graduation — mixed-arity spec test rows triggered the wedge.
Unblocks #9 + #10 (strings/bytes.contains).
2026-05-19 04:04:41 +09:00
5609d0456f selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15)
Subsumes #36. Drop wwstage scanlocals pre-pass; both stages converge on
first-use+fail-loud frame growth, rule-10 polarity DOWN to leaner side.
#36's surfaces (frame-total divergence on match-arm case-let; sibling
offset divergence in variadic+iter+match-prev compositions) close
naturally — running-max c.frame includes every first-use binding.

selfhost/cmd/wcc: add atlocals persistent @-prefix registry surviving
cgblock save/restore; add cgoutbuf/cgoutmode/cgout_enable/disable/flush
for deferred prologue (emit body to buffer, finalise c.frame, then
TEXT/SUBQ + flush); localadd @-prefix dedups against atlocals +
fail-louds on size-grow (rule 7 — no silent truncate); cgreturn-tagged
routes through @retscr (was colliding with @tagscr on arg-widen sizes);
variadic gather esz uses raw primsize (rune->4) not slotsize (rune->8)
— matches cstage and fixes the #36 sibling runtime miscompile in
non-leaf variadic+iter+match-prev callees.

cmd/w6c/cgen.c: drop the over-allocation hack ("for byte-id with
wwstage scanlocals reservation") since wwstage no longer over-reserves;
add fail-loud on @sretscr size-grow; @tagscr sites pass actual slot_sz
instead of stale c.tagscrsz.

748_size_strategy_convergence: table-driven 4 rows x 2 stages
(tag_variadic_runearm, trim_iter_match_prev, variadic_gather_rune_stride,
leaf_baseline). Each exercises a #36 surface shape; 8/8 ok.

Net -1565 lines. Sister latents filed as cosmetic (cs/ws frame size
drift on multiple-variadic-call fns): labelseq drift + varargseq
stuck at 0 — both bootstrap-byte-id safe (ww2==ww3==ww4 holds since
both ww2 and ww3 are wwstage outputs).

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 02:13:58 +09:00
7a278c1a2d selfhost+cstage+test: graduate deflookup mod-qualified same-module-first (#11)
cstage Sdef walk #2 N_DOT branch used c->cur_mod where n->lhs->str is
the correct module hint. Sister of #4c wwstage graduation; same shape
as the TY_FN branch which already uses mafn(c, n->str, n->lhs->str).

cmd/w6c/cgen.c: add sdef_mod_match_hint(s, hint); walk #2 routes hint
first then head-pick fallback, matching #4a/#28/#31/#34 *mod variant
pattern. selfhost: add deflookuprhsmod(c, name, mod); cgdot N_DOT
mod-qualified str-def value-load routes through it. Rule-10 symmetric
stages: both stages now share the lhs.str polarity (was: both used
cur_mod / cur-module hint).

747_def_modqual_modshadow: table-driven sentinel — gamma calls
alpha.MSG with beta.MSG (same-leaf-name) at head of c.defs/sdefs.
want_imm "$38," (alpha strlit len), bad_imm "$27," (beta strlit len),
plus cs-vs-ws byte-id. Reverting cstage walk #2 to head-pick → fails
$38 on cstage + diverges cs-vs-ws; reverting wwstage cgdot to plain
deflookuprhs → fails $38 on wwstage.

make test 121/121; ww2==ww3==ww4 byte-id holds.
2026-05-19 00:58:28 +09:00
d985622cb1 selfhost+test: strlit-inline str-def value-load shape (#12)
Class A silent miscompile. wwstage cgenexpr.ww cgident's bare-ident
deflookup→true branch and cgdot's module-qualified leaf branch
emitted `MOVQ <mod>.<name>(SB), AX` for a `def MSG: str = "..."`
value reference — a load from a SB symbol that emit_data never
writes. Str defs are not laid out at SB; they live as interned
strlits the .ptr/.len fold (post-#4c) and value-load consume.
Cstage already strlit-inlines via Sdef walks #1 (case N_IDENT
non-local) and #2 (case N_DOT untyped-lhs); wwstage now matches
the (LEAQ _S_<n>(SB), MOVQ $<len>, BX) emit shape per rule 10.

Surfaced by reviewer-def during #4c R3 while attempting option (B)
for the cstage Sdef walks #1/#2 prefer-pass — both walks'
cs-vs-ws byte-id sentinel rows could not pass while wwstage
emitted the bogus DATAW shape. Filed as #12 and deferred until
the wwstage emit shape was fixed. Unblocks #11 + #13 (cstage
prefer-pass graduations).

Latent: no in-tree corpus referenced a str def as a value (only
as .ptr/.len via cgdot field-fold) prior to lib/strings c3 —
same corpus-coverage-blind shape as the #4a-#4e graduations.

746_strdef_inline pins both sites with 2 rows: bare ident +
mod-qualified. Each row asserts `LEAQ _S_` + `MOVQ $<strlit_len>,`
inside the caller TEXT before RET, anti-checks the pre-fix
`<mod>.<name>(SB)` symbol-load, and cs-vs-ws byte-id per row.

120/120 ok. ww2 == ww3 == ww4 byte-id holds.
2026-05-18 23:56:58 +09:00
049ebc14a1 selfhost+lib+test: route cgcall + nodeis{slice,str} N_DOT through fnretlookupmod (#34)
Class A silent miscompile, surfaced by landing strings.slice in
Hare's natural delegation form `fromutf8_unsafe(utf8.slice(begin,
end))` (ref/hare/strings/iter.ha:75). strings.slice itself returns
str, so the inner utf8.slice (cross-module N_DOT) call's cgcall
return-ABI fixup hit post-#4e fnretlookup's same-module-first walk
and grabbed strings.slice's own str return — emitted a spurious
`MOVQ DX, BX` after the cross-module CALL even though utf8.slice
returns []u8 (selfhost/cmd/wcc/cgenexpr.ww cgcall return-ABI fixup,
line 3249-3261 pre-fix). Every other consumer of cgcall:3249's
str-shuffle decision sat on the same bare-leaf table and was
silently miscompiling on the same collision shape pre-#34.

Sibling: nodeisslice + nodeisstr N_CALL arms in
selfhost/cmd/wcc/cgenutil.ww were N_IDENT-only — for a cross-
module N_DOT call returning a slice or str, pushargsrev fell
through to the natural 1-word PUSHQ AX, dropping the `.len`
(and `.cap` for slices) of the return value when consumed as a
call arg. strings.slice's body passes utf8.slice's []u8 result
to fromutf8_unsafe; pre-fix wwstage pushed 1 word vs cstage's
3, breaking the receiver's slice-3-pop drain.

Cstage carries no sister bug: cmd/w6c/cgen.c reads return shape
from the typed `n->lhs->type` (TY_FN sig) for both str-shuffle
and slice-/str-arg push counts — module-aware via the typed AST,
sidestepping any bare-leaf table. Mirror of #4e's cstage-no-
sister-bug note.

Fix: route cgcall return-ABI fixup + nodeisslice/nodeisstr N_CALL
arms through fnretlookupmod with `callee.lhs.str` (N_DOT
qualifier) or `c.curmod` (N_IDENT). Mirror of #28
fnparamslookupmod / #31 fnretlookupmod N_DOT re-routing.
Remaining bare-leaf fnretlookup consumer sites (~8 sites across
cgenexpr/cgenutil/cgenstmt/cgendecl listed in task #34a) stay
on the graduated bare-leaf path — none of the present-corpus
N_DOT leaf collisions have return-shape divergence at those
sites. A future stdlib port introducing a return-shape-divergent
same-leaf N_DOT collision will need the *mod re-routing — filed
as #34a sibling-latents.

Bundled three concerns per rule 11: cgcall fix, nodeisslice/
nodeisstr fix, and strings.slice retire + sentinel. (a) alone
leaves strings.slice byte-id breaking on slice-arg push count.
(b) alone leaves a phantom MOVQ DX, BX on the inner cross-
module CALL. (c) alone fails 995_self_rebuild without (a)+(b).
The three cannot land separately bisect-cleanly; the 745
sentinel pins the primary repro (cgcall str-shuffle) which
sentinel-flips on a cgcall:3257 revert.

745_fnret34_modshadow pins the fix with 1 row: caller.slice
returns str (same leaf as the cross-module callee, divergent
return shape); caller.run calls myutf8.slice returning []u8.
Asserts CALL myutf8.slice present inside caller.run TEXT +
`MOVQ DX, BX` anti-check on each stage plus cs-vs-ws byte-id.

strings.slice retired in lib/strings/strings.ww: the deferral
block becomes the natural Hare delegation form with two local
utf8.decoder reconstructions for the iterator endpoints — ww
has no anonymous-embed (parallel to the existing `move` helper).
iter_slice_cases mirrors ref/hare/strings/iter.ha:110-127;
sidesteps the Hare `let t = s;` iterator-copy via fresh
strings.iter() to stay clear of #35's sibling latents.

119/119 ok. ww2 == ww3 == ww4 byte-id holds.
2026-05-18 23:37:16 +09:00
4f1d7a462d selfhost+test: route N_DOT base through indexvaluetnode + scanlocals for N_INDEX-lhs cgassign chain (#28+#30)
Wwstage's N_INDEX-lhs cgassign dispatch chain had a triple-site
N_DOT base gap (sister latents filed during #24 / #27 review):

  Read (#28): `obj.mat[i][k]` over a struct field mat: **u8.
  cgindex routes the outer N_INDEX's N_INDEX base through
  indexvaluetnode; the recursion bottomed out at the inner
  N_INDEX's N_DOT base with bt=nil. esz fell through to 8 +
  signed_elem to false — wwstage emitted a stray outer
  `MOVQ $8, CX; IMULQ CX, AX` plus `MOVQ (AX), AX` (8-byte
  read over a 1-byte u8) instead of cstage's bare
  `MOVZBQ (AX), AX`.

  Write (#30): `obj.arr[i] = v` over a struct field arr:
  [N]Tagged (e.g. (i64|str)). cgassign's N_DOT-base arm
  computed esz via indexbaseesz but never set elemtn, so the
  tagged-element store gate missed and the 24-byte tagged slot
  was overwritten by a single scalar MOVQ — wrong-width store
  + tag/payload junk in the upper 16 bytes.

Cstage walks `n->lhs->type` directly via the typed AST
(cmd/w6c/cgen.c idx_eff + the N_INDEX-lhs N_ASSIGN branch).
Wwstage now mirrors via indexvaluetnode, which #24 (aa8ca47)
introduced for the N_INDEX-base case; #28/#30 graduate it for
N_DOT base via the existing dotfieldtnode helper.

Bundle graduates N_DOT base for the entire N_INDEX-lhs cgassign
chain: (a) indexvaluetnode in cgenutil.ww handles N_DOT base via
dotfieldtnode; (b) cgassign N_DOT-base arm in cgenexpr.ww calls
indexvaluetnode for elemtn; (c) scanlocals N_DOT-base arm in
cgendecl.ww parallels the existing N_IDENT arm for tagscr-bump.
Splits are bisect-incoherent: (b)-alone clobbers locals via
under-sized frame, (a)-alone leaves the write path with wrong
elemtn, (c)-alone has no consumer. Only the triple delivers a
complete N_DOT-base graduation matching #24's N_INDEX-base
pattern.

Cstage's first-use+fail-loud strategy for @tagscr (#26 commit
069548d) handles the N_DOT-base shape naturally; the scanlocals
N_DOT arm is wwstage-specific. Long-term rule-10 convergence
(wwstage DOWN from scanlocals to first-use+fail-loud on BOTH
stages) is filed as task #15.

Class A wwstage cgen UNDER. No in-tree consumer; sister latents
filed during #24 + #27 reviews. Test 741_dotbase_chained pins
the dispatch + cstage-byte-identical asm for both rows.

Sister latent (filed): indexbaseesz has no N_TARRAY arm for
scalar struct-field array writes — `s.arr: [N]i32` scalar write
falls through to esz=8 on wwstage. No in-tree exerciser; tight
scope kept here.

115/115 ok. ww2 == ww3 == ww4 byte-id.
2026-05-18 20:25:59 +09:00
3ba19227ba selfhost+test: route chained N_INDEX outer element size through indexvaluetnode on write path (#27)
Wwstage cgassign's N_INDEX-lhs base-inspection (cgenexpr.ww) only
computed esz/elemtn when base.kind == N_IDENT or N_DOT. For a
chained `names[i][k] = v` (names: **u8) the outer N_INDEX has
base.kind == N_INDEX; esz fell through to the default 8 so the
outer store emitted `MOVQ AX, (BX)` into a 1-byte u8 slot (8 bytes
written — adjacent memory corrupted) plus a stray
`MOVQ $8, CX; IMULQ CX, AX` scaling on the outer index that cstage
doesn't emit. Wrong-width-store: the byte slot was written as 8
bytes and the outer offset multiplied by sizeof *u8 instead of
sizeof u8.

Cstage walks `n->lhs->type` directly via the typed AST at the
N_ASSIGN N_INDEX-lhs branch (cmd/w6c/cgen.c eff->sub->size = 1).
Wwstage now mirrors via indexvaluetnode (already graduated for
cgindex in #24, commit aa8ca47) — the cgassign N_INDEX-lhs branch
gains the parallel base-N_INDEX arm: call indexvaluetnode, then
elemsizeofc for esz and one-layer-strip for elemtn (so the tagged-
element gate keys honestly on the element type, matching the
N_IDENT branch's pattern).

Class A wwstage cgen UNDER. Sister latent of #24's surfaced read-
path bug; filed during the #24 graduation with selfhost + lib grep
empty for chained-write. No in-tree consumer surfaced this before
the fix, so test 740_chained_write is the sole exerciser — pins
cstage-byte-identical asm for **u8 (MOVB store, 1 inner-stride-8
IMULQ pair, no outer scale) + **i32 (MOVL store, inner $8 + outer
$4 IMULQ pairs). Anti-check on the u8 row guards against the pre-
fix stray `MOVQ AX, (BX)` regression.

Sister latents filed (no in-tree consumer):
  cgassign N_DOT-base elemtn drop (sister of cgindex N_DOT-base
  in #24 review): tagged-element store via obj.arr[i] over a
  struct-field array falls through to scalar store.

114/114 ok. ww2 == ww3 == ww4 byte-id.
2026-05-18 19:55:30 +09:00
aa8ca47943 selfhost+test: route chained N_INDEX outer element size through indexvaluetnode (#24)
Wwstage cgindex's base-inspection (cgenexpr.ww) only computed esz/
signed_elem when base.kind == N_IDENT or N_DOT. For a chained
`names[i][k]` (names: **u8) the outer N_INDEX has base.kind ==
N_INDEX; esz fell through to the default 8 so the outer load
emitted `MOVQ (AX), AX` over a 1-byte u8 plus a stray
`MOVQ $8, CX; IMULQ CX, AX` scaling on the outer index that cstage
doesn't emit. Wrong-width-narrow-load: the byte was read as 8 bytes
(reaching into adjacent memory) and the outer offset multiplied by
sizeof *u8 instead of sizeof u8.

Cstage walks n->lhs->type directly via the typed AST
(cmd/w6c/cgen.c idx_eff → eff->sub->size at N_INDEX). Wwstage
needed the parallel via indexvaluetnode — return the value-type
of an N_INDEX expression by stripping one element layer off base's
type, recursing for chained inner. cgindex's else-if chain now
adds the N_INDEX arm: call indexvaluetnode + elemsizeofc/
elemissignedc.

Class A wwstage cgen UNDER. Surfaced first time the codebase
exercised the **T[i][k] shape — through expanddir in
selfhost/cmd/ww/main.ww (post-#22 dir-enum, commit 9e0816e). The
workaround there split names[i][k] into `let nm: *u8 = names[i];
nm[k]` to route through the bare-pointer index path. Retired in
this commit: expanddir uses the natural chained form since the
read path is now byte-identical across stages.

Bundling justification (rule 11): the workaround retirement is
the in-tree verification this fix works — without retiring,
neither bootstrap byte-id nor 995_self_rebuild exercises the
chained read shape. Test 739_chained_index pins cstage-byte-
identical asm for **u8 (MOVZBQ load, 1 inner-stride-8 IMULQ pair,
no outer scale) + **i32 (MOVSXD load, inner $8 + outer $4 IMULQ
pairs).

Sister latents filed (no in-tree consumer, no probe):
  Task #27 — cgassign chained-write N_INDEX: write path
  `names[i][k] = v` for **u8 has the same dispatch gap. Selfhost +
  lib grep is empty.
  New latent (filed during review) — cgindex N_DOT base on chained
  index: `obj.mat[i][k]` over a struct-field base falls back to
  esz=8. indexvaluetnode currently handles N_IDENT + N_INDEX bases
  only.

113/113 ok. ww2 == ww3 == ww4 byte-id.
2026-05-18 19:40:41 +09:00
79d9528a00 toolchain+lib+test: Go-style package/import keywords (#18)
User-mandated language redesign: source files declare their own
namespace via the new `package <name>;` keyword and pull dependencies
via `import <path>;`. Both keywords use Plan-9 `.` separator (user
override on Hare's `::` — `import encoding.utf8;`). Internal token-
kind enum values TK_MODULE=86 and TK_USE=17 kept stable for 990
wwdump byte-diff symmetry; only kwtab strings + tokname spellings
rotated. Executables (selfhost/cmd/{ww,w6c,w6a,w6l,wwdump}/main.ww)
declare `package main;` per Go convention; lib/ + selfhost/cmd/wcc/
files declare their parent-dir basename.

One-commit bundle per the brief's all-at-once directive: a per-stage
split breaks bootstrap byte-id mid-rewrite (cstage with new keyword
can't parse old `module`/`use` files and vice-versa). Body documents
the bundle per rule 11.

Two retained divergences from the user's stated ask, both filed per
rule 7 / rule 8 with inline task pointers at the deferred sites:

  Task #22 — Directory-as-module enumeration in the driver. User
  asked: "module is combination of files in directory" (golang/hare
  shape). After this commit lib/ww/{ast,sym,typ}.ww all declare
  `package ww;` but are still pulled into the compilation unit via
  explicit sibling `import` chains (sym.ww does `import ast;` etc.),
  not via dir enumeration. The cstage scaffold for true dir
  enumeration was drafted and reverted because the symmetric wwstage
  port requires a ww-side opendir/readdir wrapper around getdents64
  (~150-200 lines new ww). Inline citation at locate_import_in /
  locatein in both stages points to task #22.

  Task #23 — Parser strict missing-`package` error. The original
  brief mandated: parser errors when a .ww source omits `package
  <name>;` as its first non-comment item. Softened here to silent-
  default because 63 test wrappers (200_parse, 100_lex, 300_check,
  400_w6c, ..., the inline-source-fragment family) build ad-hoc ww
  source strings that lack `package` and the strict error cascaded
  into 60+ test failures. Migration is mechanical-sed but deferred
  so this commit ships green. Inline citation at parsefile in both
  stages points to task #23.

Node.module renamed to Node.nmod and modent.module to modent.nmod
in wwstage source — the field name `module` would collide with the
freshly-reserved TK_MODULE token. The rename is left in place as
clean separator between AST-field-name and reserved-keyword
namespaces. Cstage's n->module retained — C has no `package` or
`module` keyword.

rt/ensure.ww deliberately ships WITHOUT a package declaration so
its `export fn rt_ensure` keeps the bare linker symbol; adding
`package rt;` would mangle to `rt.rt_ensure` and break libwwrt.a
linkage. Documented at the file head.

111/111 ok (110 + new 738_module_decl sentinel). 995_self_rebuild
byte-id holds (ww2 == ww3 == ww4). All 5 frozen
selfhost/cmd/*/main.combined.ww regenerated under the new driver.
CLAUDE.md rule 5 amended with the language-layer divergence note.
2026-05-18 18:25:36 +09:00
45339d2f5b selfhost+cstage+test: graduate enumlookup same-module-first + N_DOT enumlookupmod (#4a)
Class A silent miscompile, latent until two modules export the same
enum leaf name. Wwstage's enumlookup (selfhost/cmd/wcc/cgen.ww)
walked c.enums head-first by ename; cgdot handed it the bare leaf
from N_DOT.lhs.str for both `Color.MEMBER` (lhs N_IDENT) and
`pkg.Color.MEMBER` (lhs N_DOT) shapes, silently dropping the
explicit qualifier on the second. Cstage's enum-member fold
(cmd/wcc/check.c cexpr N_DOT) was carrying the same head-pick on
the lhs-ident lookup — pre-fix the mismatch surfaced as a
"not assignable to <same-leaf>" checker error rather than a silent
wrong-constant because resolve_typename for the fn return spec
already used scope_lookup_prefer correctly, so the rhs's wrong-
module-Color clashed with the return type's right-module-Color.
No in-tree corpus currently declares two same-leaf enums, so
995_self_rebuild stayed green and the latent miscompile only
surfaces once a stdlib port introduces the collision (same shape
as #27 surfacing when lib/strings dragged utf8's invalid alias
into the chain alongside strconv's invalid).

Fourth leaf of the trio leaf-name lookup graduation (after #27
aliaslookup, #28 fnparamslookupmod, #31 fnretlookupmod): wwstage
enumlookup grows a same-module-first walk before the head-walk
fallback, mirroring aliaslookup's two-pass shape (cgen.ww:75).
The N_DOT consumer surface — `pkg.Enum.MEMBER`, already used
in-corpus by os.flag.RDONLY, temp.mode.RDWR, os.whence.SET etc.
— routes through a new enumlookupmod variant with the explicit
N_DOT.lhs.lhs.str as the mod qualifier (mirror of fnret/
fnparamslookupmod). Cstage's check.c cexpr N_DOT lhs lookup
graduates from scope_lookup to scope_lookup_prefer to align
symmetrically (rule 10: both stages pick same-module-first on
the bare-leaf shape).

733_enum_modshadow pins both surfaces with 3 rows: row 1 bare-leaf
in module M must fold against M's own Color even with another
module's same-leaf Color at the head of c.enums; row 2 same-module
`mod.Color.MEMBER` from inside that mod pins the API surface; row 3
cross-module `othermod.Color.MEMBER` from a third module with no
local Color sentinel-flips the cgdot etmod tracking + enumlookupmod
path independently of row 1's same-module-first fallback. Asserts
the matching \$N, immediate inside the right TEXT sym + bad_imm
NOT-presence anti-check on both stages plus byte-id between stages
per row.
2026-05-18 13:32:56 +09:00
c34abf47b1 selfhost+test: route N_DOT callee through fnparamslookupmod (#28)
Wwstage cgcall now mirrors cstage's typed-AST callee-params
lookup for module-qualified mod.fn(...) calls, restoring tagged-
union widening on cross-module slice args. Class A runtime
miscompile — masked from 995_self_rebuild because wwstage tools
don't call bytes.index directly; surfaced by lib/strings landing
dragging utf8 + bytes into the wwstage-tool dep chain via
strings.byteindex's `bytes.X(toutf8(...), n)` call sites.

Pre-fix: wwstage's cgcall (cgenexpr.ww) looked up calleeparams
only when callee.kind == N_IDENT. For N_DOT callees (the
module-qualified mod.fn() form), calleeparams stayed nil →
pushargsrev's widening detection gated on param != nil never
fired → wwstage fell through to the N_IDENT-slice fast path
pushing only 3 slot words (cap, len, ptr) WITHOUT the variant
tag. Receiving fn's `match (needle)` then dispatched on
(needle.ptr in CX) instead of needle.tag, with R8/R9 carrying
.len/.cap instead of .ptr/.len. Wrong arm + wrong payload.

Cstage handles N_DOT natively via the checker-set type on
n->lhs->type (cmd/w6c/cgen.c:4156-4165), so cg_widen_tagged_push
slice path pushes 4 words including tag.

Polarity catalog: wwstage UNDER — calleeparams lookup missing
N_DOT dispatch arm. Sister to #19 (N_TSLICE variantindex arm),
#21 (N_CALL pushargsrev arm), #24 (N_CALL nodeisslice arm), #27
(aliaslookup same-mod-first). The pattern: wwstage dispatchers
keep missing arms cstage has natively via typed-AST resolution.
Convergence wwstage → cstage (rule 10's spirit overrides letter
when correctness is at stake — Path 2 of aligning cstage DOWN
would create a runtime miscompile in both stages).

Fix: cgcall N_DOT branch pulls module from callee.lhs.str and
function name from callee.str, calls new fnparamslookupmod
helper. Helper does same-module-first walk then existing
first-match fallback (mirrors #27's aliaslookup fix shape). New
fnret.fmod field carries module identity; collectfnrets sets
f.fmod = d.module at registration. Module-qualified pkg.fn path
unchanged.

Tests:
  - 727_modcall_widen_slice pins MOVQ $1 + PUSHQ AX (tag-synth)
    before the receiving fn's CALL on canonical mod.fn(slice, ...)
    shape with the callee param widened to a tagged union. Three
    assertions per row: cstage tag-synth presence, wwstage
    tag-synth presence, cstage↔wwstage cmp -s byte-id. Sentinel-
    flip-verified: comment out fnparamslookupmod call →
    wwstage tag-synth absent + cmp diverges.

99/99 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 10:20:01 +09:00
53c9e46c21 selfhost+test: route N_TSLICE variant through shape-aware index helper (#19)
Class A wwstage cgen miscompile, silent until wwstage path engaged.
Pre-fix wwstage's name-keyed flatvariantidx returned -1 for `[]T`
variants (pat.str empty on N_TSLICE), so cgmatch and
cgtagvariantidx collapsed every `(scalar | []T)` arm to tag 0.
Internally consistent within wwstage; cstage's structural
`type_eq` (cmd/w6c/cgen.c:466 cg_tag_for_variant) matched
correctly. Bootstrap stayed green because no selfhost-corpus path
exercises `(scalar | []T)` until lib/bytes / lib/strings landing
pulls bytes.index through wwstage compilation — 967_bytes_run
uses `ww run` (cstage only), so the wwstage path was never
exercised.

Polarity catalog entry: wwstage UNDER (missing N_TSLICE dispatch
arm in variantindex lookup), not REVERSE — worker's deeper read
corrected rob's initial diagnosis. cstage's structural type-eq is
the leaner-correct side; wwstage converges to it per rule 10.

Fix: new `flatslicevariantidx` helper in cgenutil.ww keyed on
N_TSLICE shape walking pat.lhs against vt.lhs alongside the existing
name-keyed flatvariantidx; extend `taggedvariantindex` shape-fallback
with a `wantslice == ivisslice` axis alongside the existing str
axis; route N_TSLICE in cgenexpr.ww's cgtagvariantidx (is/as)
and cgmatch (case) through the helper. No edits to cgenmatch's
dispatch codegen (CMPQ/JNE/spill) — that's symptom, the bug is
in the variantindex lookup.

Surfaced the 7th corpus-coverage-blind unmask of session 5 (sister
shape to STATUS-4 #11 / #14 / #21 wwstage UNDER family). Latent
within lib/bytes (a6abac2) since landing today; 967_bytes_run's
cstage-only `ww run` driver kept it dormant.

Tests:
  - 722_match_slice_variant pins cmp -s byte-id between stages
    for the canonical (u8|[]u8), reverse-order ([]u8|u8), and
    three-arm (u8|[]u8|str) shapes.
  - 926_match_slice_variant_run runtime-pins 7 rows × 2 stages
    (cstage + wwstage drivers): canonical, reverse-order, and
    other scalar-vs-slice-of-same-primitive matrices (i8|[]i8,
    i32|[]i32, u64|[]u64, rune|[]rune), three-arm with str.
    Verifies both arms reachable and payload survives.

Filed follow-ups (latent, NOT in this commit's scope):
  - flatslicevariantidx falls back to first slice slot when no
    element-name matches; `([]u8 | []i32)` would mis-route. No
    in-tree consumer.
  - 926 missing nested ((u8|[]u8) | i32) row per rob's spec.
  - 3-arm 32B tagged sequential-push payload corruption (both
    stages, asm byte-id passes, only 9xx runtime catches).
  - Chained inline pick() over 32B 3-arm slot (both stages,
    bind-to-let workaround documented at 926 row).

93/93 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 01:51:57 +09:00
dd274315a0 cstage+selfhost+test: wire sret return-forwarding (#9)
Class A compile-time fatal retirement — `return f()` from an sret
callee bailed both stages with "sret return-forwarding for >24B
struct not wired (task #23)" at every site, forcing every caller
into a `let r = f(); return r;` workaround that materialised an
intermediate >24B copy in outer's frame. Forwarding now elides the
copy: outer reloads its own @sretarg into RDI for the inner CALL
via `MOVQ @sretarg(BP), DI` (NOT `LEAQ <local>, DI`), inner writes
directly into outer's caller-prealloc dest, RAX (inner's returned
dest pointer per the sret discipline) is already outer's return
value.

Wires 2 sites × 2 stages (same triangle as #23): caller arg-shift
in cgcall/pushargsrev gains an RDI-source switch via
cg_sret_forward / c.sretforward; callee return-arm in cgreturn
replaces the fail-loud abort with cgexpr-into-cgcall + epilogue.
The @sretscr scratch slot is still pre-allocated on the forwarding
branch (unused) — eliding would need AST-walk awareness in
scanlocals; symmetric-allocate is the simpler path and keeps
byte-id with non-forwarding callers.

Latent surfaced and filed during probe (NOT in this commit's
scope): multi-sret-receive in a single fn diverges between stages
— cstage always allocates @sretscr on first sret CALL, wwstage
only when sretdestoff == 0. Bootstrap stays green because the
selfhost corpus has zero >1-sret-receive call sites.

Tests:
  - 721_sret_struct_return gains 2 forwarding rows + a 4th asm-
    presence sentinel: at the inner CALL site inside outer fn, the
    RDI source must be `MOVQ -K(BP), DI` (reload of outer's saved
    @sretarg) NOT `LEAQ -K(BP), DI` (a temporary local would write
    inner's payload into outer's frame, not caller's dest).
  - 925_sret_struct_return_run gains 3 forwarding rows: simple
    quad forward, multi-arg inner (pair-by-value + scalar args
    alongside the hidden RDI), and slice-payload (decoder
    { i64, []u8 } — the utf8 iterator shape, asserts ptr/len/cap
    survive the @sretarg chain).

90/90 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 00:15:34 +09:00
7e0c280691 cstage+selfhost+test: System V AMD64 sret discipline for >24B struct return (#23)
Class B shared miscompile pre-fix: cstage skipped the CALL emit at the
receive site (frame collapsed, exit 11); wwstage emitted CALL but
truncated 32B return to AX only (slice payload garbage, segfault on
g.b[0]). Both stages now lower plain TY_STRUCT > 24B through the SysV
sret discipline: caller pre-allocates dest, passes &dest in RDI as a
hidden first-arg (user args shift to SI/DX/CX/R8/R9/+stack), callee
saves RDI to @sretarg at the prologue and writes through it, returns
RDI in RAX. Surfaced by lib/encoding/utf8 pre-flight when the
Hoehrmann decoder (32B) hit 698_cgreturn_struct.c's OUT-OF-SCOPE
marker.

Scope: plain TY_STRUCT > 24B only — tagged unions, tuples, str, slice
keep their existing register-return ABIs. `return f()` forwarding
from a sret callee is fail-loud-not-wired (compile-time error in
both stages, follow-up filed); the workaround `let r = f(); return
r;` is wired and byte-identical. Discard-context calls (`f();` of an
sret-returning function) share a per-fn single-slot @sretscr;
consecutive discards reuse the same slot.

698_cgreturn_struct.c's OUT-OF-SCOPE marker retired in the same
commit; three positive rows (32B quad, 32B decoder, 40B five) now
assert the sret discipline across both stages via byte-id diff.

Tests:
  - 721_sret_struct_return pins three asm-presence sentinels per
    row: (a) LEAQ -K(BP), DI immediately before CALL at the receive
    site, (b) MOVQ -K(BP), AX before RET in the callee (sret return-
    the-pointer), (c) negative-assert no MOVQ AX, -K(BP) capture for
    return type >8B. Three rows × both stages × cmp -s byte-id.
  - 925_sret_struct_return_run runtime-pins 7 rows × 2 stages
    including the collision row (25B+ struct BOTH returned AND passed
    by-value as arg — catches arg-shift, sister site to #11), nested
    struct payload, slice payload, reassign-receive, N_IDENT return
    rhs.

89/89 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-17 23:17:03 +09:00
6ab865d933 selfhost+test: route tagged-CALL arg through natural push (#21)
Wwstage call-arg-emit recognized tagged args only when the source
was an IDENT (already-materialized var). For N_CALL returning a
tagged-union, the natural-push path mis-routed: AX (tag) pushed
twice, AX clobbered with widentag(=0) between pushes, DX (payload)
dropped entirely. After POP, DI ← 0, SI ← tag — both reversed and
the payload word lost. Class A runtime miscompile, masked by zero
in-tree call sites of the shape until lib/encoding/utf8's iterator
API surfaced it via pre-flight A probe.

Fix aligns wwstage DOWN to cstage (rule 10). cgenutil.ww:pushargsrev
aistagged guard now fires for N_CALL whose callee returns a tagged
whose slot matches the param's tagged slot (mirrors cmd/w6c/cgen.c:
4216-4221's type_eq guard), and the natural-push fallthrough adds a
tagged-CALL arm pushing R8/CX/DX/AX high→low by slot size (mirrors
cmd/w6c/cgen.c:4373-4387). cgenexpr.ww:cgcall's per-arg pop-count
picks up the same taggedcallslot helper so the next arg's POPQ
doesn't land on residual tag/payload words.

Sister-family to #11/#14 in the variant-widen ABI chain — call-site/
caller-side surface, distinct from callee-side #11 (param decompose)
and scratch-side #14 (return slot). Fifth corpus-coverage-blind
unmask this session (catalog: i64 div/mod CQO #16; wwstage IDENT-
local /= no-op #16-B2; cstage signed-DATA module-scope #19; wwstage
silent-zero arrays #19 mirror; #21 call-arg DX drop).

Test: 720_tagged_call_arg asm-presence row (PUSHQ DX appears
between CALL and next CALL, before PUSHQ AX) + 924_tagged_call_arg_
run 9xx semantic row (5 rows: 4-variant CALL-source, 4-variant
IDENT-source regression guard, 2-variant ptr/err, multi-arg tagged
+ scalar). Bootstrap byte-id (ww2 == ww3 == ww4) holds.
2026-05-17 07:33:55 +09:00
4fa4bcf34e cstage+selfhost+test: revert compound-assign div/mod workarounds (post-#16)
B1 (63332fe) landed CQO in both stages' assemblers and switched the
binary `/` and `%` paths to it. The compound-assign sisters (`/=`,
`%=`) were six explicit workarounds across both stages, all calling
out either "fallback for TK_SLASHEQ" or just falling through with no
case at all. With CQO available, every site mechanically ports to the
same "park rhs in CX, slot value into AX, CQO/IDIVQ CX, ferry result
back" sequence.

wwstage cgenexpr.ww:5381-5403 silently no-op'd IDENT-local signed
compound div/mod — `x /= y` and `x %= y` produced no IDIV emit at
all, just a load-bearing `MOVQ BX, off(BP)` that wrote the freshly
loaded slot value back unchanged. Bootstrap byte-id passed because
no selfhost-corpus path exercises signed compound. Latent miscompile
retired alongside the workaround revert.

cstage cgen.c:3735 (top-level-let global compound) was NOT in the
initial five-site bundle and surfaced via worker probing the
wwstage:5147 fix — `let gs: i32 = 100; gs /= 7;` returned 7 (divisor)
on cstage but 14 (correct quotient) on wwstage. Rule 10 caught the
would-be Class A divergence; the sixth site bundles in.

Six sites, one family:
  cmd/w6c/cgen.c:3549              deref-compound  `*p OP= v`
  cmd/w6c/cgen.c:3735              top-level-let   `gs OP= v`
  cmd/w6c/cgen.c:3765              IDENT-local     `x  OP= v`
  selfhost/cmd/wcc/cgenexpr.ww:3338  deref-compound
  selfhost/cmd/wcc/cgenexpr.ww:5147  top-level-let
  selfhost/cmd/wcc/cgenexpr.ww:5381  IDENT-local (silent-no-op)

test/wcc/978_intdiv_signed.c adds 7 compound rows × 2 drivers = 14
fixtures (now 68/68): IDENT-local /= /=- /=u, deref *p /= *p %= *p
/=u, with negative-dividend, negative-divisor, and unsigned-high-
bit-set coverage. Top-level-let compound coverage is deferred per
task #18 — single-file inline drivers hit a pre-existing linker
`undefined reference to '<file>.gs'` for LEAQ name(SB) targets.
cstage:3735 and wwstage:5147 are code-review-verified for rule-10
symmetry until #18 lands.

Grep-sweep (`if (n < 0) { neg = true; n = -n; }`) returned two sites
in lib/fmt/fmt.ww i64dec and lib/strconv/strconv.ww — both mirror
ref/hare/strconv/itos.ha's pre-negate idiom for INT64_MIN safety.
Per the Hare-faithful filter, both stay.
2026-05-17 02:40:40 +09:00
63332fef50 cstage+selfhost+test: sign-aware codegen for signed int div/mod (#16)
Shared miscompile in both stages — not a divergence. Bootstrap byte-id
passed throughout because both stages emitted the same wrong asm. Both
the C cgen (cmd/w6c/cgen.c TK_SLASH/TK_PERCENT) and the ww cgen
(selfhost/cmd/wcc/cgenexpr.ww) prepped IDIVQ with `MOVQ $0, DX`, which
is the unsigned 128-bit dividend shape. For a negative RAX, the CPU
then divides 2^64 + (-RAX) by the divisor — unsigned wraparound, not
signed division. Surfaced via lib/time/add() needing the verbatim Hare
signed-%-normalisation in ref/hare/time/arithm.ha.

Fix: emit CQO (sign-extend RAX into RDX:RAX, REX.W 99) on the signed
arm; keep MOVQ $0, DX on the unsigned arm where the DIVQ-vs-IDIVQ
dispatch was already correct. Since both stages always emit 64-bit
IDIVQ regardless of source width, a single CQO suffices for
i64/i32/i16/i8 — the dividend already lives in RAX sign-extended. No
CDQ/CWTL/CBTW needed.

Symmetric stages (rule 10): both stages were broken identically; both
get the same surgical fix. Adds A_CQO to each assembler's opcode set:
cstage in cmd/w6c/6.out.h + cmd/w6c/txt.c + cmd/w6a/{parse,asm}.c;
wwstage in selfhost/cmd/w6a/{types,parse,asm}.ww.

Class B (shared miscompile) — new in the session's polarity catalog.
Bootstrap byte-id is useless for catching it; semantic 9xx runtime
tests are the right shape. test/wcc/978_intdiv_signed.c covers 27 rows
× 2 drivers = 54 fixtures across {i8,i16,i32,i64,u8,u16,u32,u64} ×
{/, %} with width-boundary minima (INT8_MIN, INT16_MIN, INT32_MIN,
INT64_MIN/2) and high-bit-set unsigned anchors. INT64_MIN is spelled
(-INT64_MAX) - 1 per task #17 (wwstage NEGQ-over-imm drops digits on
-9223372036854775808i64); that literal-cgen bug is unrelated to this
fix.

Two known compound-assign workarounds at cmd/w6c/cgen.c:3765
(TK_SLASHEQ IDENT-local) and :3549 (TK_SLASHEQ/TK_PERCENTEQ
deref-compound) remain in tree; both depend on the assembler having
CQO, so they revert in a follow-up commit citing this one.
2026-05-17 02:12:29 +09:00
f4176b8749 selfhost+test: size match-spill slot by scrutinee, not 24B (#9)
wwstage cgmatch hardcoded `spillsz = 24` + unconditional CX write
where cstage emits `slot_size = (su->kind == TY_TAGGED) ? su->size
: 16` with `if (slot_size > 16)` gating. For 1-word-payload variants
like `(*u8 | oserror)` the slot is 16B; wwstage over-allocated and
over-wrote past the receiver's read window.

Factor cgmatch's non-ident scrutinee-type resolution + spill sizing
into matchscrutt + matchspillsz in cgenutil.ww. cgmatch gates CX
write on `spillsz > 16`; R8 gate `> 24` already correct. scanlocals
N_MATCH branch uses the same helpers — scan+emit lockstep.

Test 716: 4 rows × {cstage runtime, wwstage runtime, asm-byte-id}.
Aliased (*u8 | oserror) ok/err arms, raw (*u8 | i64) for hypothesis
breadth, (str | i64) 24B regression guard.
2026-05-17 00:00:55 +09:00
28f36d84d8 selfhost+test: single-source-of-truth @tagscr scratch reservation (#38)
Closes STATUS latent #1: @tagscr shared 24B reservation across the
four tagged-scratch sites (cgreturn, pushargsrev, cgindex
tagged-elem, pointer-rooted struct-field tagged write). Any fn that
needed >24B (e.g. slice-in-tagged-field 32B) silently overflowed
into the neighbor frame slot. Surfaced concretely as getopttest's
errortable wwstage exit 16 after #37 fixed the upstream gaps.

c.tagscrsz: i32 on the cgen struct is the single source of truth.
tagscrbump(c, need) in scanlocals raises the max across all 4
reservation sites and returns the frame delta. All emit sites
(cgreturn / pushargsrev / cgindex / cgwidentaggedstore pointer-
rooted) read c.tagscrsz instead of hardcoded 24. Mirrors the existing
cgwidentaggedstore precedent; @tagbase keeps its 8B scanseenmark
dedup (always 8B, correct).

Unmasked latent bug (now fixed): scanlocals's pointer-rooted struct-
field tagged-write detection uses localfindnode(c, base.str) to
resolve the *struct base. For `fn fill(h: *holder)`, h's scan-time
stub from scanseenmark had tnode=nil, so the @tagscr reservation
never fired. Pre-#38 the hardcoded 24B masked this; #38's correctly-
sized slot exposed it. cgfn's param scan loop now sets
c.locals.tnode = scanp.lhs after scanseenmark so localfindnode
resolves param types at scan time.

Test 714 (tagged_return_scratch): 4 rows × 2 stages = 8 fixtures.
Direct adjacency repro; match-arm field-by-field read; **mixed-
sizes-one-fn** (16B pushargsrev widen + 32B cgreturn widen in the
same body — pins the lockstep invariant that a sibling site can't
undersize the shared slot); call-site struct-payload widen. Row 3
specifically would regress if a future refactor ever forgets to
route an emit site through c.tagscrsz.

982 getopt_run green through both stages (was the original surface);
995 self_rebuild byte-id holds.
2026-05-16 13:45:33 +09:00
de3bd5cc3b selfhost+test: wwstage type-info loss through struct-field N_INDEX (#37)
The original #37 symptom (worker-34's `..findflag` mangle) cannot
reproduce on master — was a runtime miscompile misattributed to a
link-time issue. Investigation surfaced three real wwstage cgen
gaps in the N_INDEX-through-struct-field family, sister bugs to
#34 (N_INDEX N_IDENT-base) and #36 (primsize-default-to-8).

1. `indexbaseesz` slice-element stride defaulted to 8 for named-
   struct elements. `&opts.ptr[i]` for `opts: *[]option` computed
   MOVQ $8 instead of $24. Fix: route slice case through
   `elemsizeofc(c, innert)`; ptr-to-named-struct via structlookup.

2. `cgun TK_AMP N_INDEX` ignored N_DOT base. `&p.ptr[i]` left
   esz=8 because only N_IDENT base was handled. Mirror cgindex's
   existing N_DOT arm.

3. `nodeisstr` N_INDEX arm only walked N_IDENT bases. `cmd.argsptr[i]`
   for `argsptr: *str` returned false; pushargsrev dropped the
   .len half at call sites. Add N_DOT-base arm that walks the
   struct field's pointee.

Test 713 (struct_field_index): 3 rows × 2 stages = 6 fixtures, one
per fix shape. Runtime-only; bootstrap byte-id (995_self_rebuild)
covers cross-stage drift.

Residual: getopttest's errortable still fails through wwstage with
a slice-of-str-via-&arr[expr] miscompile. Filed as task #38.
2026-05-16 12:52:36 +09:00
2fb594748c cstage+selfhost+test: principled identity-cast skip (#33)
Generalizes b5632b1's single-site dst_is_enum gate. Skip the narrow-
clamp MOVL when src.width == dst.width && src.signed == dst.signed.
Closes #25's followup.

Both stages need symmetric source-type derivation for byte-id. cstage
deliberately throws away the checker's richer typed-AST and uses a
structural walker (castsrcprim) that mirrors wwstage's exprprimresolved
case-for-case. Otherwise cstage's `.len: i32` resolves to i32 (skip)
while wwstage's misses the pseudo-field (clamp) — bootstrap diverges.
Pseudo-fields, N_BIN, N_INDEX, N_CALL, match-bindings all yield sz=0
→ clamp emits defensively on both.

The N_TENUM walker now follows enum aliases in wwstage's
typenodeprimresolved (was the original lacuna behind #25), and bool
is excluded early in the same helper (mirrors cstage's
type_isint(TY_BOOL)=false). bool→bool keeps its dedicated is_bool
ANDQ $255 emit; bool→i8 / bool→u8 etc. fall through to the clamp on
both stages.

Walker shape (cstage castsrcprim / wwstage exprprimresolved):
  N_INTLIT     → tsuffix gated, untyped excluded
  N_IDENT      → trust local's resolved tnode
  N_CAST       → recurse on declared dst
  N_UN         → recurse on operand
  N_DOT        → real-struct only (TY_STRUCT or TY_PTR→TY_STRUCT)
  others       → sz=0 → identity false → clamp emits

Test 710 grew from 5 → 16 rows: 6 identity-width pins (u32/i32/u8/
i8/u16/i16 self), 1 sign-change pin (u32→i32 clamp MUST fire), 2
silent-miscompile exit-validating rows (truncate via divide), 1
pseudo-field defensive pin (`s.len: i32`), 1 bool-source pin
(`b: i8`). Asm byte-id asserted on every row.

Out of scope: redundant clamps remain for patterns wwstage can't
structurally derive (N_BIN, N_CALL, N_INDEX, pseudo-fields). A
sibling task extending wwstage's type inference closes those.
2026-05-16 12:20:56 +09:00
b8b32ab80c cstage+selfhost+test: refuse same-block let / param redecl (#32)
cmd/wcc/check.c silently accepted `let a; let a;` in the same block
and similar redecls. Pre-#27 the localoff dedup masked it; post-#27
last-write-wins via head-first localfind. Surfaced by worker-27
during the #27 review.

Cstage: 5 guard sites (check_scope_define-NULL → err) covering
N_LET block-bind, N_MLET tuple binders (incl. same-tuple
`let (a,a)`), N_FORRANGE tuple binders, top-level let, fn param.
Voice: "<kind> '<name>' redeclared in same scope" for inner;
"duplicate let %s" for top-let, matching the existing
"duplicate <kind>" idiom at 1812/1851/1872.

Wwstage: TODO(#11) comments at the 4 mirror sites (installdecl,
N_FORRANGE, N_LET, installparams). Full enforcement waits on the
checkfile pass per rob.

**Unmasked by #32 (worth flagging):** selfhost/cmd/wcc/cgenexpr.ww
cgcall had `let callee: *node = n.lhs;` twice at fn-body scope
(copy-paste, identical value). Pre-fix silent-redecl absorbed it;
post-fix the new guard rejects. Removed the second decl — outer
`callee` stays visible across the intermediate block.

Test 712 (redecl): 10 rows (6 neg + 4 pos), cstage-only per rob.
Negative rows cover all 5 guard sites + same-tuple-dup. Positive
rows pin the legal counter-shapes (cross-block, name-only bucket,
forrange body, mcase-per-arm).

Test 300 row 34 ("shadowing in inner scope; same scope flagged")
was incorrectly asserting the bug; flipped to expect "redeclared"
and added a sibling row pinning cross-block shadow stays ok. Test
709's `same_block_redecl_pin` canary (explicitly documented as
flipping under #32) removed; pointer to 712 left in its place.
2026-05-16 11:17:20 +09:00
10de5a5e0e selfhost+test: cgdot LEAQ-of-fn for module-qualified N_DOT (#12)
wwstage cgdot lacked a TY_FN branch for module-qualified N_DOT
rvalues. `let p = mod1.ping` fell through to the MOVQ/LEAQ-narrow
fallback, loading 8 prologue bytes from the fn's first instruction
instead of taking its address. Cstage cgdot already handled this
case (wired during #9, f1440bf).

Mirror cstage: gate on fnretlookup(c, fld) before the localloadop
fallback; emit `LEAQ <module>.<name>(SB), AX` via emitfnname with
the hint from lhs.str.

Extend 706_fnlabel_mangle: pos.ww now stores mod1.ping/mod2.ping
into local fn-pointer slots and dispatches through them in addition
to the existing direct calls. Expected exit 56 → 112. Regression
shape: without the new branch, MOVQ leaf(SB) loads the prologue
bytes; indirect call jumps into garbage → SIGSEGV.

ww2 == ww3 == ww4 byte-identical at the new emit.
2026-05-16 00:07:00 +09:00
f1440bf9e8 cstage+selfhost+test: mangle fn labels by module (#9)
Both stages emitted fn TEXT labels by leaf only; lib/os and lib/io
exporting the same leaves (read, write, close) collided at link.
lib/fmt + lib/log worked around with @symbol("rt_syscall") stubs.

Drop d->export from the fn skip rule in mod_collect (both stages) so
exported fns mangle as <module>.<name>. Let/def/type keep current
behavior. Skip retained for {@symbol, main, empty-module}.

Add cur_mod thread through cgfn + mod_lookup_for_fn(name, hint) at
all 4 label-emit sites (TEXT def, LEAQ N_IDENT, CALL N_IDENT, CALL
N_DOT). Wwstage mirror: emitfnname + modlookupforfn + curmod.
Invariant comment pinned in both stages.

ww2 == ww3 == ww4 byte-identical at the new label format.
706_fnlabel_mangle covers same-leaf cross-module CALL + private-leaf
cur_mod disambiguation through a fn-pointer rvalue.

Wwstage LEAQ-of-fn N_DOT (`let p = mod.fn` rvalue) is a pre-existing
gap; deferred to a follow-up. fmt/log rt_syscall stubs untouched
here; cleanup follows.
2026-05-15 23:41:01 +09:00
99a68a6a57 cstage+selfhost+test: extend structlit-fill helper to N_ASSIGN N_DOT lhs (4 flavors)
Sister fix to #17. The BP-rel helper from #17 covered N_LET /
N_ASSIGN N_IDENT-lhs / N_RETURN; the four N_ASSIGN N_DOT-lhs
structlit walks still went through the inline `cgexpr(field.lhs);
store-AX-sized` shape and silently dropped trailing bytes when a
struct-typed field's value was itself an N_STRUCTLIT. Affected dot
flavors: single-dot via_ptr / global / BP-rel and the chained-dot
walker (depth >= 2, all three root flavors).

Extend `cg_structlit_fill_bp` / `cgstructlitfillbp` into
`cg_structlit_fill` / `cgstructlitfill` taking a destination mode
(DST_BP / DST_PTR_LOCAL / DST_GLOBAL = 0/1/2), srcoff (PTR_LOCAL),
srcname (GLOBAL), and disp accumulator. `disp` grows by foff on
descent; srcoff/srcname stay constant across the call tree. The
pre-#17 wrappers are preserved byte-identically by delegating with
mode=DST_BP — 995_self_rebuild byte-identity holds for the no-
nested-STRUCTLIT case that selfhost source actually uses.

The non-BP modes reload BX before the ELLIPSIS zero-fill loop AND
before every field store (tagged, scalar, and the cgexpr leaf).
This is correctness-by-construction — cgexpr clobbers BX between
fields, and the redundant reload only fires on shapes that didn't
compile before. The four dot-flavor sites in each stage now compute
their dst mode + disp and call the shared helper (reducing each
from ~80-130 inline lines to ~5-12 lines of dispatch).

Stage signature asymmetry: cstage threads Local** for cgexpr; ww-
stage takes explicit totsize because #15 (split totsize into
naturalsize + slotsize) is still pending and the dot sites need
structnaturalsize while the BP-rel sites need si.totsize. Both
asymmetries are documented in the helper docstrings.

704 covers 8 rows (24 checks: 8 cstage exits, 8 wwstage exits, 8
cstage-vs-wwstage .s byte-identity diffs): 6 dst-flavors (single-
dot local/ptr/global, chained-dot local/ptr/global) plus single-
local 3-deep and single-ptr 3-deep to pin disp threading through
the helper's recursion and through DST_PTR_LOCAL BX reloads.

The nested struct-typed CALL rhs in field-walks has the same shape
as the STRUCTLIT bug fixed here but the helper only handles
STRUCTLIT — tracked as task #20.
2026-05-15 18:45:21 +09:00
9d03e02881 cstage+selfhost+test: fix nested STRUCTLIT silent zero in BP-relative fills
Pre-existing landmine surfaced by #5. For a struct literal whose
field value is itself an N_STRUCTLIT of a struct-typed field, the
inline field-walk did `cgexpr(field.lhs); store-AX-sized`. cgexpr
has no whole-struct-in-register convention, so the nested literal
landed AX = first qword and the trailing bytes silently stayed zero
(or stack garbage). Three BP-relative sites in each stage hit it:
N_LET, N_ASSIGN N_IDENT-lhs, and N_RETURN N_STRUCTLIT.

Fix: shared cg_structlit_fill_bp (cstage) / cgstructlitfillbp
(wwstage) helper handles TK_ELLIPSIS autofill, tagged-field
widening, float vs scalar store dispatch, AND recurses on
struct-typed N_STRUCTLIT field values at bp_off + field_off. All 3
sites in each stage now call the helper instead of the inline walk.

Scalar store dispatch is the explicit {1->MOVB, 4->MOVL, else MOVQ}
shape (not fieldstoreop, which would emit MOVW for fsz==2) to stay
byte-identical with cstage pending task #13. Sister N_ASSIGN N_DOT
structlit walks (via_ptr / global / BP-relative-through-N_DOT) keep
their inline walk and still drop nested-STRUCTLIT silently — tracked
as task #18.

703 covers 6 rows: let_nested_i64, let_nested_3deep, let_nested_i32,
let_nested_middle (i64; switch to i32 once #15 lands),
assign_ident_nested, return_nested. 995_self_rebuild byte-identity
preserved.
2026-05-15 18:14:33 +09:00
c4347499ee cstage+selfhost+test: fix (*p).f silent drop in N_DOT lhs (read+write)
Pre-existing landmine surfaced by #5 (whole-STRUCT N_ASSIGN). Both
stages' N_DOT dispatch gated on `lhs->lhs->kind == N_IDENT`; the
parser produces N_UN(STAR, IDENT(p)) for `(*p).f`, so both sides fell
off:
  - Write side (cgassign N_DOT base): emitted nothing, store dropped.
  - Read side (case N_DOT pointer-auto-deref): cgexpr derefed the
    pointer as a scalar, AX = first qword of struct, field offset
    dropped.

Fix: retarget base / dot_lhs to the inner IDENT when shape is
N_UN(STAR, IDENT). The existing via_ptr branch fires identically to
`p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` (non-IDENT
pointer expression) is tracked separately as task #19.

702 covers 7 rows: write_i64/i32/str, read_i64/i32/str_len, roundtrip
2026-05-15 18:13:02 +09:00
f2643a846a cstage+selfhost+test: cgen N_ASSIGN whole-STRUCT (call+structlit, 5 sites)
Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of
size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15],
CX=[16..23], zero-padded to 24B; receive sites here read the regs and
write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized
tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT
mirror the sender's three uniform MOVQs, else trailing 1..7B chunks
overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable
under WW struct align rules (size%align==0) and fall through.

Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww +
cgenstmt.ww), call-result + structlit rhs at each:
  - N_LET   `let s: T = bar()` / `= T{...}`           cgenstmt cglet
  - N_ASSIGN N_IDENT-lhs   `s = bar()` / `= T{...}`   cgenexpr cgassign
  - N_ASSIGN single-DOT local-base   `o.f = ...`
  - N_ASSIGN single-DOT ptr-base auto-deref   `p.f = ...`
  - N_ASSIGN single-DOT global-base   `g.f = ...`
  - N_ASSIGN chained-DOT depth>=2   `o.m.in = ...`
(The four dot-flavors share one shape pattern, hence "5 sites".) Where
the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded
into BX after the call so CX stays as the third value word; for
structlit field-walks BX is reloaded before each store since cgexpr
clobbers AX/BX between fields.

wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww):
si.totsize is mis-named — it's slot-padded to 8 by registerstruct for
stack-slot use, while the receive ABI wants the type's natural size
(max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is
tracked as the wwstage struct sizing follow-up (task #15); until that
lands, the helper recovers the natural size at receive sites.

Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value,
wwstage value, asm byte-identity), wired in Makefile after 698. The
headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B
via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16.
A regression to a MOVQ tail there overruns 4B past the slot and
flips the exit-code check.

smoke.combined.ww is the auto-regen ride-along of strings.freeall
landing in 714d089 (worker-shlex).

Pre-existing gaps surfaced and tracked separately (not fixed here,
out of scope):
  - task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs.
  - task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN
    initializer — the field_chain and field_global test rows use
    explicit field writes (`o.m.t = 10i64;`) rather than nested
    literals as a fixture-level workaround.
  - task #9: module-name-mangle for fn labels avoided in the
    field_global_call fixture by `let g: outer;` (no init).

make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap
byte-identity is the load-bearing proof for this commit's scope.
2026-05-15 16:43:07 +09:00
df74d9c429 selfhost: cgmatch bsz for TY_STRUCT variant bind (closes #31)
Match-arm bind size in wwstage hardcoded str=16, []T=24, else=8 in
both cgmatch (emit) and scanlocals (frame pre-scan). A TY_STRUCT
variant fell into the 8B fallback: only the first quadword reached
the bind, and the prologue SUBQ underbooked the frame so the
emit-time localalloc(bsz=24+) wrote past SP.

Replace the hand-rolled table with slotsize(c, pat) at both sites.
slotsize already covers N_TNAME named structs (returns si.totsize),
str (16), []T (24), tuples, aliases, and primitives (8). Mirrors
cstage cgen.c cgmatch which falls through to bu->size for TY_STRUCT.

Cstage is correct; no mirror needed.

Test 695 covers seven shapes: the 3xi64 headline repro, a 4xi64
struct via let-init scrut (exercises >24B bind), a mixed-quadword
struct (i32+i32+i64+i64), str/slice/i32 negative controls, and a
direct let-init scrutinee variant. The wider 4xi64 row uses the
let-init shape because the N_DOT spill path in cgmatch tops out at
AX/DX/CX/R8 — a separate, unrelated gap from the bind size.
2026-05-15 02:14:00 +09:00
bacbf4b845 w6c+selfhost: cgdot N_DOT tagged-field source ABI (closes #28)
cgdot of a tagged-union struct field previously dropped the AX/DX/CX/R8
payload-register convention used by tagged-union returns: cstage's
direct-struct branch stopped at CX (size > 16) and never loaded R8
(slice-payload variants, slot 32B); the via_ptr branch had no TY_TAGGED
handler at all, falling through to fldloadop and yielding only the tag
in AX. The N_DOT scrutinee fallback in N_MATCH similarly stored only AX
into the spill slot. Wwstage cgdot had no TY_TAGGED branch in any of
the direct, *struct, or top-level-global field-load paths, and cgmatch's
non-ident scrutinee branch didn't recognise N_DOT — dispatch always
computed want = 0 and the spill scratch was hardcoded 24B. The combined
effect: any code reading `s.taggedfield` and consuming more than one
quadword of the payload saw garbage in the upper halves.

Cstage: extended the direct-struct TY_TAGGED branch with an R8 load for
size > 24 (CX still loaded last so global LEAQ-into-CX rooting
survives), added a parallel TY_TAGGED handler to the via_ptr (TY_PTR
inner TY_STRUCT) field branch, and extended the N_DOT scrutinee spill
fallback in N_MATCH to write DX/CX/R8 alongside AX.

Wwstage: new cgloadtaggedfield helper emits the four-register load with
CX-last ordering, and dotfieldtnode resolves a field's declared type
node for a local-ident or *struct base. cgdot grew three TY_TAGGED
branches (direct local, *struct deref staging in BX, top-level global
through CX). cgmatch's non-ident-scrutinee branch grew an N_DOT type-
extraction path mirroring the N_CALL / N_INDEX shapes and now sizes the
@match_spill slot from slotsize(scrutt) so slice-payload variants don't
overflow the historical 24B alloc. rhstaggedabicall accepts N_DOT so
`let copy: ev = h.e;` and tagged-arg call sites pass through the
tagged-source spill branch of cgwidentaggedstore.

Out of scope for #28 and left as separate latents: wwstage's match-arm
bind for a TY_STRUCT-typed variant copies only 8B (cstage falls back
to bu->size; wwstage's bsz=8 default), and the variant-index lookup
for an i64 literal in (i32 | i64) picks the wrong tag on the write
side. Both surface in struct-payload tagged unions and merit their
own tasks; the new test rows steer clear so #28's fix verifies
end-to-end on scalar / str / slice payloads.

Test 693_dot_tagged_source — three variant shapes (16B i64, 24B str,
32B slice) read from direct local, *struct param, top-level global,
and let-init round-trip. The 32B-slice rows verify v.cap (R8 / +24)
so dropping the upper-word load isn't masked by len-only checks; the
top-level-global row routes the write through *p because the direct
global-LHS tagged store is a separate wwstage gap (followup). Three
negative controls (untagged i32 / str / slice fields) keep the new
TY_TAGGED guard from shadowing the existing field-load paths. Wired
into make test; 37 tests total. Bootstrap ww2 == ww3 == ww4
byte-identical.
2026-05-15 01:08:52 +09:00
6402d8deb7 w6c+selfhost: cgen N_DOT slice-field through *T root in call args (closes #29) 2026-05-14 23:45:14 +09:00
a5919ed8da w6c+selfhost: cg_widen_tagged_store basereg + N_ASSIGN tagged field (closes #26)
Extended cg_widen_tagged_store (cstage) / cgwidentaggedstore (wwstage)
to take a base_reg/basereg parameter so the primitive supports non-BP
destinations. Cstage extends body in-place via via_outer gate +
spill+scratch+copy-out; wwstage splits into wrapper (non-BP) +
cgwidentaggedstorebp (BP-only) to dodge the no-goto constraint. New
N_ASSIGN field TY_TAGGED branch routes through the primitive for all
rhs shapes.

Scope-adjacent: fieldsize recurses through N_TTAGGED via slotsize and
TNAME-aliased-to-tagged via aliaslookup. Needed for the test fixtures.

Wwstage read-side N_DOT-of-tagged-field source is filed as task #28;
test rows use mark-canary verification until that lands.
2026-05-14 19:12:20 +09:00
1726bcef18 w6c+selfhost: cgen N_ASSIGN TY_STRUCT field branch (closes #25)
Parallel to TY_STR/TY_SLICE branches at cgen.c:1939/1962. Word-copy
from src slot to field+k*8 via AX, MOVL/MOVB ragged tail. N_IDENT
rhs only — struct-call-result and struct-literal rhs are different
code paths, filed as task #27. Symmetric in N_ASSIGN field case AND
spine-walker terminal; mirrored in wwstage cgassign four sub-shapes
(*struct base, direct local, global, spine terminal).
2026-05-14 18:16:51 +09:00
eb0602d070 w6c+selfhost: cgen N_ASSIGN TY_SLICE field branch (closes #24)
Parallel to the existing TY_STR branch at cgen.c:1939. Stores AX/BX/CX
at field+0/+8/+16 across three sub-shapes (via_ptr, is_global, direct
local) — DX as addr scratch where needed so CX (cap) survives.
Symmetric in wwstage cgassign. Surfaces tasks #25 (whole-struct rhs)
and #26 (whole-tagged rhs) in the same locus class.
2026-05-14 03:18:49 +09:00
d66aef382a w6c+selfhost: cgen *T-rooted chained N_DOT (closes #22)
Spine walker now accepts *T root at the last hop (cur->lhs->kind ==
N_IDENT, pu->kind == TY_PTR), substitutes pointee struct, emits
MOVQ off(BP), CX before offset arithmetic. Symmetric in N_DOT and
N_ASSIGN. Distinct gate from existing mid-chain *T-field branch
(cgen.c:4615) — no shadow.

Unblocks task #18 (bufio writer first-field embed). Pre-existing
*T-field mid-chain path unchanged.
2026-05-14 02:44:57 +09:00
d2f4659305 w6c+selfhost: localloadop helper for sign-aware ident loads (closes #19)
Read-side fix dual to fldloadop: signed-narrow local/global ident loads
now MOVSXD/MOVSWQ/MOVSBQ from the slot instead of raw MOVQ. Deref-stores
(MOVL/MOVW/MOVB) no longer corrupt downstream i64 widens. Compound RMW
restructured to gate direct-mem ADDQ/SUBQ on load_op == MOVQ. Top-level
lets use LEAQ+indirect (w6a doesn't expose MOVSXD/MOVSWQ/MOVSBQ for
D_EXTERN).

dotchainresolve out-params restored to natural *i32 (workaround retired).
selfhost/CLAUDE.md graduated.
2026-05-14 01:58:52 +09:00
5f87c60e6c selfhost: retire dotchain bundle workaround (closes #17)
dotchain struct + bundle pre-declares retired. dotchainresolve now
takes 6 direct out-params; per-iter locals declared inline. Bootstrap
ww2==ww3==ww4 byte-identical — end-to-end validation of session's
N_DOT / N_INDEX / TK_AMP / fldloadop fixes.

i64 widths retained on out-params via task #19 workaround (i32 deref-
stores leave caller slot upper 4B stale, MOVQ reads zero-extend to
garbage). Documented in selfhost/CLAUDE.md active-workarounds.
2026-05-14 01:11:07 +09:00
6354f5267c w6c+selfhost: cgassign N_DOT N_INDEX-lhs branch (closes #16)
Symmetric write-side counterpart of #8, bundled across both stages.
cstage [N]Struct write was broken (1986 gated on TY_PTR); wwstage had
no N_DOT(N_INDEX) write branch at all. New branch covers both
[N]*Struct and [N]Struct via viaptr flag, uses fldstoreop for
scalar/sub-word, MOVSS/MOVSD for float, two-MOVQ for str rhs.
Compound (PLUSEQ etc.) wired for integer scalar.
2026-05-14 00:09:46 +09:00
9ef9bef340 w6c+selfhost: cgen N_DOT N_INDEX-lhs branch (closes #8)
cstage cmd/w6c/cgen.c gained the missing N_DOT N_INDEX-lhs branch.
Covers both [N]*Struct and [N]Struct via fldloadop. wwstage already
handled [N]*Struct since 7c75dd2; refactored to mirror cstage exactly
and added [N]Struct. The spill workaround in dotchainresolve stays
(Pike rule); task #14 retires it as a follow-up.

Wwstage cgassign N_DOT(N_INDEX,...) silent store-drop discovered in
scope, filed as task #16.
2026-05-13 23:45:35 +09:00
d053560b80 w6c+selfhost: TK_AMP through chained N_DOT (closes #9)
Three shapes handled: value-struct chains (&o.i.a), pointer-field
(&p.f), slice/str pseudo-fields (&s.len). cstage inlines #6's spine
walker; wwstage reuses dotchainresolve unchanged. Silent-drop fallback
preserved.

Slice-header width mismatch in *&s.len writes filed as task #13.
2026-05-13 23:17:15 +09:00