Commit Graph

123 Commits

Author SHA1 Message Date
954badd28f wwstage: name vararg-gather slots via mklabel; graduate fmt 777/780/781 (#227)
wwstage named variadic-gather slots with mkvarargname off a separate
varargseq counter, never bumping the shared labelseq that names match
labels. cstage names them via mklabel (cmd/w6c/cgen.c:5427,5431), which
advances labelseq twice per gather. So by the time main.main reached its
`match (wr)`, wwstage's match-label counter ran two behind cstage's
(_4/_5/_6 vs _6/_7/_8) — a pure label-numbering divergence that kept fmt
cs/ww byte-id failing.

Drop varargseq and the mkvarargname helper; call the existing mklabel
for the two gather slots, matching cstage's order (vararg_d only when
nvar>0, vararg_sl always). The slot names are locals-table keys only —
they resolve to BP offsets and never reach the asm — so only the
labelseq advance is observable, which is exactly what realigns the
downstream match labels. cstage untouched (align wwstage up).

This was the match-label half of fmt's divergence; with the earlier
compound-assign fix it completes fmt byte-identity. Graduate
777/780/781 to STAGE_CS|STAGE_WW with byte_id, and drop the now-stale
(void)asm_byte_identical guard in 777.
2026-06-01 05:04:46 +09:00
db9edb7c39 wwstage: classify tagged call-source by stamped result type, not callee leaf name (#211)
rhstaggedabicall keyed the tagged-vs-scalar call-source decision off the
callee result type looked up by leaf NAME (fnretlookupmod), with the
receiver variable used as the "module". A value-receiver fn-ptr field
call s.f(...) whose leaf collides with a same-named global fn then
mis-bound the global's register shape, so the source was misclassified
as scalar and widened wrong: silent cs/ww asm divergence and wrong
runtime. Read the checker-stamped N_CALL result type (src.type_)
instead, mirroring the N_DOT sister branch. cstage already reads u->ret
off the typed callee (cmd/wcc/check.c:1490) and harec selects by
interned type id, not name (ref/harec/src/types.c:714).

Graduates test/wcc/782 to STAGE_WW + byte_id.
2026-05-31 23:34:16 +09:00
1995d8e43a w6c+wwstage: zero high pad words on scalar/float widen into a >16B tagged union (#227)
cg_widen_tagged_store (cmd/w6c/cgen.c) and the wwstage twin cgwidentaggedstorebp (selfhost/cmd/wcc/cgenutil.ww) wrote only the tag (slot+0) and value (slot+8) in their scalar and float arms, leaving the high pad words (slot+16..sz) as stack garbage on the BP/let/assign/return-scratch path, which never pre-zeroes. A passthrough return or u8-reinterpret of a narrow scalar/float widened into a >16B union (fmt's field = (...formattable | *mods) is 32B via the str variant) then read that garbage. Both stages were wrong identically, so the byte-id gates stayed green while the runtime truncated; fmt's spread-union scalar widen is the first real consumer. Both arms now tail-zero slot+16..sz (gated size>16), mirroring the tagged-subset/struct tail-zeros and keeping the stages byte-identical (rule 10). Adds runtime test 793; regenerates w6c/wwdump combined.ww. fmt byte-id graduation still awaits the other residual, #226 (io.read nominal-remap).
2026-05-31 22:20:53 +09:00
45e5d74047 w6c+wwstage: tag a bare value widened into a NAMED-alias union variant via structural fallback (#15)
Variant selection (cg_tag_for_variant / flatvariantidxt) matched union variants by exact type only, so widening a bare value (e.g. *vtable) into a union with a NAMED ptr-alias variant (stream = *vtable, in handle = (file | stream)) found no match and the tag defaulted to 0 -- the wrong variant. In the compiler this hit emitbytes' io.write(&cgoutstream.vt) once io.write took a handle, writing the asm to a garbage fd -> empty .s -> w6c_ww miscompiled everything. Add a second selection pass: when the exact pass finds no variant, structurally compare the bare source against each NAMED-alias variant's unwrapped type; exact-match still wins in pass 1 (so a bare i64 stays the i64 variant, not oserror=!i64, which kept the os/errno union building). A >=2-structural-match collision guard (extending #218's) hard-errors LOUDLY on genuine nominal ambiguity (two ptr-aliases to the same struct) instead of silently first-picking, citing #199b/#10. Symmetric across cstage (cmd/w6c/cgen.c) and wwstage (selfhost/cmd/wcc/cgenutil.ww). One-level NAMED unwrap (chained ptr-aliases unmatched, unexercised -> #17). Adds test/wcc/789 (positive widen byte-id+runtime + degenerate-ambiguity reject guard, both stages). Unblocks post-eFinal #5's handle surface. rule-10 fix-up.
2026-05-30 09:04:33 +09:00
34c437fd63 wwstage: register error-structs in collectstructs so large-union struct-variant returns widen (#9)
collectstructs registered a struct only when the typedecl body is N_TSTRUCT, so an error-struct (type X = !struct{...}, whose body is N_TBANG{N_TSTRUCT}) never entered wwstage's c.structs table. The name-keyed structlookup then missed at the return-widen sites, and wwstage dropped the struct construction when returning a struct variant of a large (>4-eightbyte) union -- wrong runtime value and cs!=ww. cstage has no struct name-table (pure tinfo) and was correct. Peel the N_TBANG body in collectstructs so error-structs register; both existing cstage-mirrored widen arms then fire. Provably byte-id-inert: no committed source defines a !struct today. Adds test/wcc/785 (struct-variant return + named-void control, both-stage byte-id + runtime). The >4-eightbyte 5th-word truncation on return remains, symmetric (cs==ww) and unread by the tag/early-word path; #222's sret hidden-pointer cutover is the committed fix (table-retirement tracked as the wwstage->tinfo SSoT arc). Aligns wwstage up to cstage (rule-10).
2026-05-30 04:54:12 +09:00
80184a3acf wwstage: make the cgdot alias-peel struct-break module-aware (#223)
The wwstage cgdot #191 alias-peel loop broke on a name-keyed any-module
structlookup, so a receiver whose alias name collides with a struct of the
same name in ANOTHER module resolved to the foreign struct and fell through to
an undefined `name(SB)` global instead of the field load. The eFinal FLIP
renames io's `vstream` -> `stream`, which collides with memio's `stream`
struct, so io.read/io.write/io.close's `match (s.reader)` emitted
`MOVQ reader(SB), AX` (reader is also a type-alias) -> cs != ww (cstage chases
the nominal TY_NAMED.under pointer chain, module-correct). Gate-blind: on
master both stages emit the same wrong store so byte-id stays green; the FLIP
corpus is the first to put the io-alias/memio-struct collision in one build.

Fix (wwstage-only align-down; cstage is the authority and is untouched): make
the peel's struct-break MODULE-AWARE — break only on a same-module struct (a
genuine struct-value receiver); a same-module alias keeps peeling to its
underlying (io.stream -> *vtable -> the pointer field-load arm); a foreign leaf
keeps the prior any-module heuristic. New structsamemod / aliassamemod mirror
the same-module-first pass already in structlookup / aliaslookup. This is not a
naive alias-first reorder (which would reintroduce the mirror collision: a
same-module struct plus a foreign same-leaf alias). Peel-only — the direct-
struct arm's broader cross-module same-leaf-STRUCT name-keying is filed as #224.

#208-family (name-keyed resolution dropping to a wrong global) but in cgen, not
the checker; #213 is distinct (cosmetic local-struct-match divergence).

test/wcc/784_xmod_alias_struct_collide_run: collision (cross-module alias-vs-
struct, same leaf), symmetric (guards the same-module-struct break against a
naive reorder), and a no-collision control — branched callee. The discriminating
net is cs.s == ww.s (the path is gate-blind and cstage is correct, so byte-id
flips when wwstage is fixed); confirmed by source-revert. The FLIP's combined.ww
is now cs.s == ww.s byte-identical.
2026-05-30 02:00:07 +09:00
176904dffc w6c+wwstage: tag the outer widen of a nested multi-variant union (#218)
The outer widen of a NAMED multi-variant union value into an enclosing union
mis-tagged: the store took the tagged-subset path (inner value at slot+0 plus
a sub-variant remap, collapsing every inner sub-variant onto outer tag 0),
while the match-extract reads the nested layout (outer tag at +0, inner 16B
value at +8). Store and extract disagreed, so the match selected the first
arm. Pre-existing silent miscompile, latent because error-origination sites
(`let e: io.error = <leaf>; return e`) were gate-blind — no test discriminated
a freshly-originated error at a branched caller; the io vstream surface is the
first to do so.

Fix, both stages, byte-identical: cg_variant_match (cmd/w6c/cgen.c) and its
wwstage mirror cgvariantmatch (cgenutil.ww) fall back to structural equality
of the unwrapped tagged unions when the alias collapse loses nominal identity
(a NAMED outer variant vs an unwrapped-tagged source); the widen store now
writes the inner value at slot+8 and the outer tag at +0, matching the
extract. The inner union's build/payload/extract already worked (a destructure
through the outer round-trip recovers the inner payload) — only the
outer-widen store was wrong.

Collision guard (the fallback is unsound without it): structural matching
cannot disambiguate two nominally-distinct same-shape variants in one outer
union. That is unreachable under today's nominal-lossy collapse but inverts
the moment #199b lands the nominal layer, so if >=2 outer variants
structurally match the source we hard-error at compile time citing #199b —
both stages, an enforced invariant rather than a "rare, trust it" assumption.

Folds #219: the wwstage tinfo typeeq (lib/ww/typ.ww) had no TY_TAGGED branch
and fell through to `return true` (any two tagged unions compared equal);
cstage type_eq (type.c:269) has the structural branch. The structural fallback
above is the first and only caller to compare two bare tagged unions, so #219
is unexercised — and therefore ungateable — in isolation; it folds here per
the rule-11 couldn't-split carve-out (same structural reason as #206's
N_TTUPLE fold). The added branch mirrors cstage type_eq, tightening wwstage
into alignment.

test/wcc/925_nested_union_widen_run: outer-arm select, destructure-after-
propagation (payload survives the round-trip), destructure-let, single-variant
control, and the collision-guard compile-error, each with a cstage==wwstage
byte-id check (the path is gate-blind). Interim until #199b/B-full lands the
true nominal wrapped-slot layout.
2026-05-29 23:19:36 +09:00
66a91c8969 wcc: converge let-IDENT memcpy, IDENT-assign recv, nested struct call-recv onto structabisize
Three more wwstage cgen sites still used unrounded structnaturalsize where
cstage rounds via lu->size — pre-existing gate-blind cs!=ww latents the #169b
reviewer surfaced: cgenstmt N_LET struct-IDENT memcpy (let p2: T = p1; twin
cgen.c:7869), cgenexpr N_ASSIGN N_IDENT-lhs register RECV (s = mk(); twin
cgen.c:4700-4737), and cgenutil's nested struct N_CALL recv inside
cgstructlitfill (twin cgen.c:2121).

Converge all three onto structabisize, completing the same-class closure
started by #169 and continued by #169b. Also corrected the inline comment at
cgenutil.ww:3273-3286 that wrongly claimed fl->type->size was natural
(check.c:760 sets ABI). sretretsize at cgenutil.ww:1301 is gate-equivalent
natural and is left alone.

Probe 698 +3 rows (one per converged site) with cs==ww .s byte-cmp and a
pre-fix-rebuild discriminator. 990-997 byte-id hold.
2026-05-28 14:07:58 +09:00
39f9267bc9 wcc: converge DOT-recv, structlit-fill, and bare-let zero-init onto structabisize
Three wwstage cgen sites still used the unrounded structnaturalsize where cstage
rounds via lu->size (check.c:760), pre-existing gate-blind cs!=ww latents
flagged in #169's reviewer notes: cgenexpr DOT register-RECV for obj.f = mk()
(~5175/5393/5628/6164); cgstructlitfill's TK_ELLIPSIS zero-fill branch
(cgenutil); and cglet bare 'let z: T;' zero-init of a maxalign<8 struct
(cgenstmt). Each produced MOVQ-vs-MOVL or wider-write divergence vs cstage on
the trailing word of a sub-eightbyte tail.

Converge all three onto the maxalign-rounded structabisize the #169 work
established at the register-ABI sites (cite cstage cgen.c:7720 RECV twin +
cgen.c:2085 cg_structlit_fill). cgstructlitfill's signature drops the external
totsize parameter in favor of one internal source; the field-walk path is
untouched, only the ELLIPSIS zero-fill uses the ABI size. cglet's slot
allocation stays on the frame size; only the zero-fill extent uses ABI.

Gate-blind (the bootstrap exercises none of these shapes); covered by 5 new
rows in probe 698 with cs==ww .s byte-cmp and a pre-fix-rebuild proving the
exact MOVQ-vs-MOVL discrimination. 990-997 byte-id hold.
2026-05-28 13:02:47 +09:00
d92c199d25 wcc: retire exprfloatkind, read the checker stamp directly
exprfloatkind was wwstage cgen's structural float-classifier — a workaround for
the checker stamp being untrustworthy. With the previous commit arming the
asserttyped bail, every checked value-node is now stamped (or cited-exempt),
so its job collapses to a 2-liner reading n.type_ — the same path cstage cgen
has always taken. Retire it: inline the stamp-read at its eight sites (cgcast,
cgun, cgbin lhs+rhs, cgcall pop, pushargsrev, cgwidentaggedstorebp, cgreturn
x2 collapsed), delete the wrapper, and delete the two residual
sibling-evidence loud-aborts (cgbin float-arith, cgwidentaggedstorebp
float-arm) — their operands are real source value-exprs the armed bail now
stamps, so the guards can never fire.

One synth-post-checker value-node remained outside the bail's reach: the
variadic-slice descriptor pushed in pushargsrev/cgcall (cgenexpr.ww). Stamp
it at synthesis with the variadic param's []T slice tinfo so the inlined
reads see a stamped node, no nil special-case. Byte-id-neutral by design
(slice tinfo and nil both read non-float); 990-997 confirm.

Closes the bail-rearm arc — wwstage now reads the same float-class SSoT
cstage does, the gate-blind float-classification family is closed, and the
build+test corpus is asserttyped-clean by construction.
2026-05-28 11:35:45 +09:00
d4e500f61c wcc: struct ABI size maxalign-rounded via structabisize (#169)
wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where
cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign
8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the
return path — unpacked with a different trailing-word width (MOVL vs MOVQ)
between stages. Value-correct either way, but a cs!=ww asm divergence.

Add a dedicated structabisize = round(natural, maxalign) used only at the two
register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and
memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural
size, so the two are different sizes — rounding the shared metric breaks 995.
maxalign derives from each field's tinfo.align (mirrors cstage check.c:708),
not an fsz ladder (a ladder over-rounds composite [N]u8 fields).

Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the
discriminator is the cs==ww .s byte-cmp; covered by probe 698.
2026-05-28 01:02:41 +09:00
a917533fbf wcc: struct-param float fields via SSE arg regs (#165)
struct params were passed GP-only, so a struct{f64,f64} argument landed in
DI/SI instead of X0/X1 — value-correct for internal ww calls (the bits
round-trip) but not SysV register-class conformant. Add a per-eightbyte
classifier (struct_float_class) routing a qualifying struct's float eightbytes
through the SSE arg cursor, reusing #163's dual-cursor plumbing and #164's
field classification. A struct qualifies only when every eightbyte is
pure-integer or a lone f64 exactly filling it (and >=1 f64); anything else —
any f32, multiple floats per eightbyte, a straddling or aggregate field —
falls back to the unchanged GP path (f32 sub-eightbyte packing deferred #165b).
Both stages' predicates are alias-aware and identical in coverage.

Gate-blind and value-correct either way, so the discriminator is the callee's
receive instruction (MOVSD vs MOVQ), scoped per-function — covered by probe
946.
2026-05-27 23:31:19 +09:00
0465c423c1 wcc: tuple-param ABI via SSE/GP arg cursors (#163)
Tuples were unhandled as parameters — no tuple arm in arg-push, arg-pop, or
callee-recv in either stage — so a tuple param fell to the 1-GP-word else and
dropped all but its first element (integer tuple params too; floats doubly
lost). Add tuple-param arms (SEND push+pop, callee RECV) across both stages,
reusing #164's per-element SysV classify with the 6-GP (DI,SI,DX,CX,R8,R9) +
8-SSE (X0-X7) arg cursors. A frame slot @tupargscr decouples the producing
call's return cursor from the overlapping arg cursor (capture-before-clobber).
Overflow (>6 GP / >8 SSE) fails loud (rule 7). Scoped to the N_CALL producer;
first-class tuple values (ident/literal) remain a separate unimplemented gap.

Gate-blind (the bootstrap passes no tuple params) — covered by table-driven
probe 905, which proves pre-fix element-drop and the loud-stop.
2026-05-27 22:13:46 +09:00
393ac1a882 wcc: nodeisunsigned N_CALL reads stamped type_ (#168)
Twin of #134 (N_INDEX arm): the wwstage signedness classifier did not
consult the checker-stamped type_ for an N_CALL result, so an
unsigned-returning call got signed IDIVQ/SARQ instead of DIVQ/SHRQ.
cstage was already correct (reads the stamped operand type; check.c:1433),
so this is a wwstage-only arm — symmetric outcome both stages.

Gate-blind (the bootstrap lacks the shape) — covered by table-driven
runtime probe 906, which also asserts w6c==w6c_ww .s byte-identity.
2026-05-27 18:43:08 +09:00
cbeffea7d8 wcc: 2D array [N][M]T static-init + double-index read (#156, A.3 capstone)
Close A.3's deferred shape-14 (nested array). (a) emit_array_lit_bytes
gains a TY_ARRAY-element arm (mechanical clone of the TY_STRUCT-element
arm — recurses; esz=etype->size, rule-13; ...-nested loud-reject). (b)
double-index read tbl[i][j]: when the indexed element is TY_ARRAY, leave
the sub-array ADDRESS in AX instead of dereferencing (sister of #135's
N_DOT-base fix, on the N_INDEX path) — new elemisarrayc/tinfoisarray
helpers, both stages. Storage + read = one 2D-end-to-end concern (A.2/A.3
storage+LOAD precedent).

Unblocks strconv fold-4's powers_of_ten[596][2]u64 (direct double-index
access). Bootstrap-NEUTRAL (new arms gate on TY_ARRAY-element; 1D
consumers byte-identical, 990-997 green). Test 919 +2D rows + 3D +
...-nested-reject. Deferred siblings: #155 (sub-array bind / whole-
aggregate copy), #160 (global-struct-field index base).
2026-05-27 12:21:15 +09:00
36bf60350c wcc: nodeisunsigned N_INDEX reads stamped type_ (#134)
wwstage cgenutil nodeisunsigned N_INDEX arm now reads
typeisunsigned(n.type_) directly, mirroring the N_DOT arm at line
1007 and cstage cgen.c:2541 which reads type_isunsigned on the
stamped operand. Embodies the #121 principle (collapse structural
onto stamp). Byte-id-neutral at master (no current N_INDEX-of-non-
IDENT-base unsigned compare sites in bootstrap); fix is for forward
consumers in strconv decimal.ha (>= 5u8 on d.digits[nd] with N_DOT
base) and similar Hare idioms. Closes #134.
2026-05-26 21:28:14 +09:00
0a2747ce5b wcc: collapse exprfloatkind structural body onto checker stamp (#121)
wwstage exprfloatkind now reads n.type_ via classifytinfo (typeisf32→1,
typeisfloat→2, else 0) — wwstage classifies float-ness from the cstage-
equal stamp; delete the 9-arm structural body + the transient bridge.

Permanent residual sibling-evidence guards: cgbin float-arith (primary,
fires on unstamped float operand), tupstore + cgwidentaggedstore float
arms (pins, unreachable today, contract assertions against future
regression). cgcast excluded (int→float CVTSI2SD legitimately nil-typed).

Closes the gate-blind cs≠ww float family root (STATUS:14, named (b)-phase
goal). Builds on 98e1665 (A-narrow N_IDENT destructure stamp) +
1c4cea4 (transient floatness-disagreement bridge).
2026-05-26 18:30:18 +09:00
1c4cea4e2c wcc: exprfloatkind structural/stamp floatness-disagreement bridge (#121)
Add classifytinfo(n.type_) stampkind alongside structural structkind;
abort on floatness-disagreement only ((structkind!=0)!=(stampkind!=0));
return structkind (byte-id-neutral). TRANSIENT — deleted in commit 3 (the
flip + permanent residual sibling-evidence guard). The corpus-wide
validation that licenses commit 3's flip.
2026-05-26 17:46:39 +09:00
0917fee48d wcc: float array-element loads to X0 + indexed-float consumer (#119)
cgindex's element-load sites ended in the integer loadopsz (MOVQ/MOVL
into AX), with no float branch — so an f32/f64 array element landed in
a GPR while the consumer's ADDSD/MOVSD read a stale X0. Add a float-
element branch (MOVSS f32 / MOVSD f64 into X0) at all three wwstage
cgindex sites (global, baselocal, fallback) and both cstage N_INDEX
element-load sites, deriving float-ness from the SAME stamped element
tinfo the esz already reads: new elemisfloatc/elemisf32c helpers
(mirroring elemissignedc) for ident bases, typeisfloat/typeisf32(n.type_)
for N_DOT/N_INDEX bases — never a fresh node-stamp that could hit an
unstamped base (#121).

The load fix cannot land alone: the wwstage consumer (cgbin/cgcast)
classified an indexed float operand as INTEGER (no exprfloatkind N_INDEX
arm) and fell to PUSHQ/ADDQ/MOVSXD, while the cstage read the stamped
operand type and used ADDSD/CVTTSD2SI. That divergence is pre-existing
on master (proven: master cs vs ww already differ on `a[0]+a[1]`),
contradicting the original "consumer already expects X0, cs==ww"
premise; load-only would leave the wwstage incoherent (value in X0,
consumed from AX) and still cs!=ww. So this also adds the exprfloatkind
N_INDEX arm — safe because the index-result type_ IS checker-stamped
(cgindex reads it for esz), unlike the unstamped-N_MLET case deferred
under #121. With both, f64 arrays are runtime-correct and both stages
emit byte-identical asm.

946_floatarr_run: f64 element add / trunc / non-adjacent index assert
the value + cs==ww; the f32 row asserts cs==ww only — its runtime value
is blocked by a SEPARATE store-side bug (f32 array-element store writes
AX raw double low-bits instead of CVTSD2SS-narrowed X0), filed as
#119-store. Regen w6c/wwdump combined.ww (cgenexpr.ww + cgenutil.ww
embedded).
2026-05-26 13:10:46 +09:00
9a265a31f5 wcc: name-bind the size type in type position (#85 fold-2)
Resolve `size` -> TY_SIZE at the type-name resolver (C lookup_builtin /
ww tinfofornode's N_TNAME chain), mirroring uintptr, both stages. This
makes `size` writable as a type (`let x: size`, struct field, etc.),
the prerequisite for lib/types SIZE_MAX.

Twins every NAME-keyed uintptr arm in the wwstage so it behaves like
the cstage's kind-keyed Type switches (already TY_SIZE-aware from
fold-1): primtypesize + astalign (8B/8-align), primsize + letscalarprim
(8B scalar slot), isinttypeast + isnumerictname (int/numeric). rule-10
symmetric; dead on the size-free selfhost corpus so 990-997 stay byte-id.

Coexists with the size(T) size-of operator (separate c.top SK_FN seed +
N_CALL fold, NOT a type path) and `.size` field access (N_DOT); neither
touched. No c.top SK_TYPE "size" seed (would collide with the operator
seed at check.ww:96). Regenerates w6c/wwdump combined.ww (checker
embedded). New probe 957_size_type_run exercises type-position `size`
and the operator in one scope.
2026-05-26 01:55:54 +09:00
c9e39c6782 cgen: f64-typed int-literal + tuple-field materialize in X0 (both stages, #103)
Two sites, same class: an f64 value failing to reach XMM (X0) before an
SSE op. Both gate-blind — cstage and wwstage emitted the same wrong asm —
so the fix touches both stages identically.

FACE X — a no-decimal float-typed integer literal (`0f64`, `8f64`) is an
N_INTLIT carrying float TYPE. The integer-immediate path stranded it in
AX, so `n == 0f64` compared a stale X0 (true for all n) and
`(8f64 * 10.0): i32` read garbage. Route the float-typed N_INTLIT through
the float-constant-in-X0 emit (cgen.c cgexpr_float, factored from
N_FLOATLIT; cgenexpr.ww cgfloatbits). The wwstage also needs the
exprfloatkind N_INTLIT arm so the downstream f64->i32 cast emits
CVTTSD2SI not MOVSXD — cstage reads the checker-stamped type directly,
so this is the same #101 structural-vs-stamped asymmetry.

FACE Z — a tuple positional f64 field read (`r.0`, r:(f64,i64)) loaded
via the integer op into AX, so `r.0 == 0.0` was wrongly true. Add a
fld_isfloat branch -> MOVSD/MOVSS into X0 (cgen.c:5910 tuple arm;
cgenexpr.ww tuple arm), mirroring the struct-field float load at
cgen.c:1462,1838 (the #96 pattern).
2026-05-25 15:26:43 +09:00
7b3abf0ec5 cgen: wwstage fnretlookup resolves cross-module callee f64 return type (#98/#101)
exprfloatkind's N_CALL arm only set the callee name for an N_IDENT
callee, so a module-qualified `mod.g()` callee never reached any
return-type lookup and fell through to integer (kind 0). Both f64
consumers then took the integer path for an imported f64-returning
fn: cgcast emitted MOVSXD instead of CVTTSD2SI (#101), and pushargsrev
spilled the call result as a GPR PUSHQ/POPQ instead of the MOVSD float
spill (#98) — one root, two symptoms.

Route the N_DOT callee through fnretlookupmod with the module
qualifier, mirroring nodeisslice / nodeisstr's #34 N_DOT arm, so a
cross-module f64 call resolves to kind 2 exactly like same-module
already does. This aligns wwstage UP to cstage, whose cg_isfloat reads
the resolved call result type directly (cmd/w6c/cgen.c:117,155) and is
correct for both cases. Consumers (cgcast, pushargsrev) unchanged.
2026-05-25 13:48:31 +09:00
ab9de65b8e cgen: sub-slice ptr = base + lo*esz (both stages, #76)
A sub-slice base[lo:hi] advanced its data pointer by lo (element
COUNT) instead of lo*esz (BYTES), so the base pointer was wrong for
any esz>1 element. Pointer arithmetic is membsz-unit per the rt
invariant (ref/hare/rt/ensure.ha:30); esz==1 (u8/str) is unchanged.

Four emission sites, fixed byte-identically across stages (rule 10):
  - value path:  cmd/w6c/cgen.c N_SLICE  <-> cgenexpr.ww cgslice
  - call-arg:    cmd/w6c/cgen.c:4646     <-> cgenutil.ww pushargsrev

Scaling mirrors the cgindex idiom: esz from the type table (rule 13;
cstage bu->sub->size, wwstage elemsizeofc) gated to an N_IDENT base,
uniform IMULQ (no SHL special-case, no immediate form -- w6a is
reg-reg only). The live lo reg is the multiplicand so the one free
GP (DX value / BX arg) holds esz*lo; lo is preserved for len (hi-lo)
and cap (base_cap-lo, #20). The esz==1 path keeps the single ADDQ,
byte-identical to before (#75/#20/str unaffected). Non-ident bases
stay unscaled in both stages (wwstage has no tnode there), tracked
as a #76 residual alongside #74.

New 943_subslice_ptresz_run: table-driven, dual-driver (ww/ww_ww),
esz in {2,4,8} array+slice base, lo>0, let-form + call-arg form;
asserts s[0]==base[lo] & s[1]==base[lo+1]. Fails on every fixture
pre-fix on both stages, passes post-fix. Registered in Makefile
(TESTS + target) so test/run builds and runs it.
2026-05-25 02:36:54 +09:00
8b23ff3517 cgen: sub-slice cap = base_cap - lo
A sub-slice `base[lo:hi]` now sets cap to base_cap - lo (the storage
remaining to the underlying end; Go/Hare-identical) instead of hi - lo
(== len). base_cap is the array length N for [N]T, or the .capacity
word carried in a slice/str header at +16. Authored once per stage in
the cg_base_cap / cgbasecap helper, applied at both cap sites: the
N_SLICE value path (which serves let-init since the prior commit) and
the call-arg push. Both stages stay byte-identical (find-4 closed).

cap arithmetic per ref/harec/src/eval.c:1017 (slice: slice.cap -=
start) and eval.c:1024 (array: cap = array.length - start); capacity
is a distinct field per ref/hare/rt/ensure.ha:4-8 and cap >= len per
ref/harec/src/check.c:596. Only the cap arithmetic transfers: the ptr
stays unscaled (lo*esz is #76) and eval.c's stricter start>=end bound
is not ported (ww's runtime bound is start>end).

str[lo:hi] yields str with a real .capacity (D1), so the str base uses
the same +16 load -- no downgrade to []u8. base_cap falls back to len
(prior behavior) where it isn't cleanly available: a non-ident base
(its header cap was discarded by cgexpr; len is likewise wrong for a
defaulted hi there, pre-existing) and a global str base (wwstage
cgslice has no global-str load, #73 -- the carve-out keeps both
stages byte-identical).

Test: 942_subslice_cap_run, table-driven over both drivers, array /
slice / str base + an append-no-realloc row, each shape chosen so
base_cap-lo != hi-lo.

Fold in three pre-existing fixtures that asserted the old cap == len
and so failed under the corrected semantics (project #20):
681_arr_elem_field_write (slice_field_value_write,
slice_field_ptr_write, slice_field_distinct_bytes),
693_dot_tagged_source (local_struct_slice_variant,
via_ptr_slice_variant, letinit_slice_roundtrip, top_level_global_slice),
and 695_match_bind_struct (slice_neg_control). Each cap word updated to
base_cap - lo: a [8]u8 base sliced at lo=0 yields cap 8 (5->8, 3->8);
distinct_bytes slices a [16]u8 at lo=0, yielding cap 16 (6->16). len /
mark / ptr assertions are unchanged -- only the cap word moved.
2026-05-25 01:59:14 +09:00
3637993bb2 cgen: collapse redundant TY_STR esz special-cases onto str.sub -- Phase 2 step-3 Fold 2
F1 set str.sub = u8; the str-element-size readers no longer need a TY_STR special-case. cstage: delete the two 'if (kind==TY_STR) esz = ty_u8->size' blocks -- the general 'esz = sub->size' path already yields 1 for str (str.sub=u8), as the third index site (which never had the special-case) proves. Provably byte-id-NEUTRAL for ALL inputs: ty_str is the sole TY_STR instance and str.sub==ty_u8, so sub->size==ty_u8->size==1 in every case. No kind-gate (type_isstr/isstrtype arm-selectors) touched.

wwstage elemsizeof (cgenutil.ww) is COMMENT-ONLY: it names primtypesize("u8") directly because it operates on a raw type node with no stamped tinfo at the ident-base index path (str.sub lives on .type_.sub, unstamped here -- cf. cgforrange's 'if sti != nil'); that IS the str.sub-equivalent value, identical asm. Added the WHY + retargeted the citation to the surviving cstage path. The structural collapse there is blocked on tinfo-stamping, not intent -- filed (task #24); byte-id 990-997 guards the residual coupling.

Zero asm change both stages (cstage/wwstage .s byte-identical pre/post and cross-stage). main.combined.ww regenerated via the canonical make path (comment propagation only).
2026-05-24 18:01:32 +09:00
b416e7114e cgen: fold str tagged-variant payload store onto slice arm (both stages)
Phase 2 step 4. The str and slice tagged-union payload stores were
byte-identical adjacent arms (3-word ptr/len/cap @ slot+8/+16/+24 +
tag) since #1 made str a 24B {ptr,len,cap}. Delete the dedicated str
arm and widen the slice arm's gate to accept str (cstage type_isstr,
wwstage nodeisstr). One site, both stages. Byte-id-neutral: str now
flows the identical slice arm; full test incl 990-997 green.
2026-05-24 09:00:53 +09:00
a5ca21ddba cgen: str index-stride via type table, not literal 1 (both stages)
Phase 2 step 2. The N_INDEX element-stride for str hardcoded esz=1;
route it through the type table (cstage ty_u8->size, wwstage
primtypesize("u8")) so str's element width tracks the u8 SSoT. When
the TY_STR collapse (Phase 2 step 5) folds str onto the slice-of-u8
path, these sites need no second edit. Byte-id-neutral: u8 size is 1,
so emitted stride is unchanged; full test (incl 990-997 byte-id
gates) green.
2026-05-24 09:00:36 +09:00
1140a590bf wcc: str -> 24B {ptr,len,cap}, 3-reg ABI -- parity with []u8 (both stages)
A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.

Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
  emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
  path already used by 32B slice-variant unions -- str-variant is now
  structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
  existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
  dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.

Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.

Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
2026-05-24 08:11:14 +09:00
f45caf1cd9 selfhost/cmd/wcc: index-base esz from n.type_.size + delete indexbaseesz (#72, A.6.3k)
The N_INDEX node carries the checker-stamped element tinfo (#60 arc); cgindex,
cgun TK_AMP, and cgassign now read the element stride off that .type_.size
instead of indexbaseesz's manual N_DOT-pseudo-field + structlookup walk,
retiring the helper (0 callers, ~96 LOC). Aligns down to cstage's
idx_eff(base->type)->sub->size (cmd/w6c/cgen.c:3517-18, natural element size) --
strictly more cstage-faithful than indexbaseesz's totsize/slotsize derivation
(byte-id held only because firing shapes have totsize==natural; cstage reads
natural and ww-old==cstage, so the flip is structural). Each site keeps its
nil->default-8 fallback; cgindex stays esz-only (signed_elem unset for the
N_DOT base, as before).

Closes the A.6.3 cgenutil-collapse arc: every type/size/offset query in cgen
now reads the checker-stamped tinfo, and the AST-walker / structinfo-walk
helpers it replaced (dotfieldtnode, indexvaluetnode, rhstargetname,
dotinnerstructptr, indexbaseesz) are retired. make test 134/134, byte-id
990-997 hold. Coverage: 713/741/755 + self-rebuild.
2026-05-24 03:16:14 +09:00
0763377806 selfhost/cmd/wcc: dotchainresolve walks tinfo.fields (#71, A.6.3j)
The chained value-struct spine-walker resolved root + total offset + leaf
type via structinfo/fieldinfo (slot-padded foff). Swap its guts onto the
stamped root-ident type_: peel TY_NAMED to TY_STRUCT (one TY_PTR hop for a
*struct root) and accumulate tfield.offset down the spine, mirroring cstage's
cur->lhs->type fields walk (cmd/w6c/cgen.c:3156-3216 write, :1955-2030 AMP/
read). Leaf out-param becomes *tinfo; the three callers read leaf-ness via
typeis*/loadopsz, and the cgassign struct-terminal recovers the struct name
from the leaf tinfo's NAMED wrapper for the still-structinfo cgstructlitfill.

Root classification (local/ptr/global) is unchanged, so the firing set and
addressing mode stay byte-identical; the global-root path remains its pre-
existing shared breakage (#27), untouched. Natural tfield.offset equals the
old slot-padded foff for every shape the byte-id gate exercises -- and since
cstage already reads the natural offset and ww-old==cstage held, the flip is
structurally identical, not coincidental.

Latent (off-corpus, byte-id-neutral): the tinfo-peel now fires the str/slice
pseudo-leaf on an aliased-str/slice field where the old structinfo path
bailed -- a faithful-toward-cstage gap closure, filed for a probe + sentinel.

make test 134/134, byte-id 950+990-997 hold.
2026-05-24 02:35:53 +09:00
6971f57356 selfhost/cmd/wcc: collapse chained-dot offset-emit onto tinfo.fields + delete dotinnerstructptr (#70, #12)
Both chained-*struct-field sites (cgdot read, cgassign store) read the inner-
struct layout off the stamped inner-dot tinfo (peel NAMED->under, TY_PTR->.sub
->NAMED->under->TY_STRUCT) and walk tinfo.fields for offset + per-field type,
replacing dotinnerstructptr's structinfo re-walk + structlookup. Type dispatch
(str/slice/float, load/store op) keys on tfield.type_ via the existing *tinfo
predicates; ft.slotsize reproduces the old fieldsize exactly (== size for all
kinds reached here except struct/array). cstage parity: cgen.c:1653-1655
(read fl->offset), 2514/2541 (store f->offset).

Behavior-preserving: a strict per-level gate (every chain dot must peel through
a *struct, root must be N_IDENT && localfindnode != nil) reproduces
dotinnerstructptr's EXACT locals-only-with-all-*struct-intermediates firing set
-- a by-value intermediate dot bails to the identical pre-existing generic path.
asm is byte-identical (990-997 hold; both the strict gate and the looser
superset tested 134/134, confirming the corpus has no by-value-intermediate
chain). Deliberately NOT widened past the old set: global-root and by-value-
intermediate chained dots stay on their old paths -- the global case is a
pre-existing SHARED base-eval defect (cstage mis-evals the global base as
(BP)->segfault; wwstage no-stores), filed #27 to fix in both stages together.

dotinnerstructptr deleted (0 callers). Offset now sources from tinfo.fields
(natural layout) vs old structinfo (slot-padded); they coincide for flat
structs (all reached here), with nested-by-value-struct divergence pre-existing
and left to #57/#6/#7 territory.
2026-05-24 01:41:05 +09:00
e5092709e4 selfhost/cmd/wcc: drop indexvaluetnode from cgassign + delete 3 retired AST-walkers (#69, #61d)
cgassign's two element-store sites (N_DOT and N_INDEX index targets) fed the
index node into indexvaluetnode to recover the element type. The tagged-store
machinery reads tinfo directly post-#68, so both sites now read the checker-
stamped element tinfo via lhs.type_: esz from .size (mirror #60), tagged gate
via the shared istaggedtype/slotsize/cgwidentaggedstore path (all NAMED-
peeling). cstage parity: cgen.c:3507-3523 (idx_eff(base->type)->sub->size).
The new esz reads the element's natural .size, where the old elemsizeofc
routed struct elements through slotsize -- so a padded-struct chained-index
store (`[][]Point`) now matches cstage's ->size instead of diverging; that
shape is untested (#7 indexbaseesz territory), so this is faithfulness, not
a corpus change.

With cgassign migrated, indexvaluetnode has zero external callers; dotfieldtnode's
sole caller was indexvaluetnode; rhstargetname is self-recursive only -- all
three retired (-132 LOC). This closes the A.6.3 AST-walker arc: the *node
type-resolvers that existed because tinfo was lossy on nominal identity are
gone, now that Phase-N (#63-#68) built the TY_NAMED layer and every consumer
reads the stamped tinfo. make test 134/134, byte-id 950+990-997 hold.
2026-05-24 00:16:14 +09:00
901ddf20b9 selfhost/cmd/wcc/cgenutil: tagged-store machinery reads tinfo.params (#68)
cgwidentaggedstore/storebp/cgwidentagremap took a type *node and re-derived
the tagged shape via resolvetagged + N_TTAGGED.list walks. Migrate them onto
the stamped tinfo (the store-side parallel of #66's match-side flip): dst is
now the tagged *tinfo (peel TY_NAMED->du, gate TY_TAGGED), variant lookup +
tag-remap read tinfo.params by typeeq, mirroring cstage cg_widen_tagged_store
/ cg_widen_tag_remap / cg_tag_for_variant (cmd/w6c/cgen.c:1273/1177/503). The
N_CAST widening test flips from surface-name streq to `castu==dt || (castu
tagged && typeeq(castt,dst))` (cgen.c:1295).

Node-form flatvariantidx/taggedvariantindex become thin shims over new tinfo
cores (flatvariantidxt/taggedvariantindext) so node-side callers (cgreturn
cgenstmt:269, pushargs cgenutil:192) are untouched. The 9 cgwidentaggedstore
callers pass node.type_ (each already istaggedtype/slotsize-gated). resolvetagged
is retained for its 7 match-side callers.

Incidentally retires two latent ww-vs-cstage divergences, both byte-id-neutral
on the corpus: the old N_CAST test set castisdst for ANY N_TTAGGED regardless
of type equality (cstage guards on type_eq), and the old remap walked the
UNflattened src.list (cstage walks the flattened params). Unblocks cgassign's
indexvaluetnode drop (#69/#61d). make test 134/134, byte-id 990-997 hold.
2026-05-23 22:57:13 +09:00
aec48e8c13 selfhost/cmd/wcc: matchscrutt returns scrutinee node, gate on istaggedtype (#67)
matchscrutt's N_DOT branch resolved the field type via the dotfieldtnode
AST walk + resolvetagged; post-#66 the N_DOT node carries the field tinfo
on .type_, so return the scrutinee node directly and let cgmatch gate on
istaggedtype(scrutt) instead of scrutt.kind == N_TTAGGED. All six scrutt
consumers read .type_ (peeling TY_NAMED), none reads node structure, so a
value node vs a type node is invisible downstream; the istaggedtype guard
preserves the old nil-for-non-tagged contract.

Symmetry-improving (rule 10): cstage derives the dispatch type from s->type
with one unconditional NAMED-peel (cmd/w6c/cgen.c:4847-4850; the N_IDENT
check at :4854 is only the slot-offset fast-path). The old dotfieldtnode
required base.kind == N_IDENT -- a restriction cstage never had -- so the
chained-dot widening this enables matches cstage (out-of-corpus, #14).

Drops matchscrutt's dotfieldtnode caller (external callers 2->1; the
indexvaluetnode:1084 internal recursion remains, and the cgassign tagged-
store sites still need the node-keyed resolvetagged machinery, so neither
walker is deletable yet -- store-migration + #61d follow). N_DOT-match
coverage: 693/694/695/700/744 (runtime). byte-id 990-997 unchanged, 134/134.
2026-05-23 21:55:52 +09:00
3036ba766d selfhost/cmd/wcc: type-key tagged variant match (#66, Phase-N step 3)
The user-ruled B-full semantic change: flip tagged-union variant matching
from surface-NAME to TYPE-identity (typeeq over tinfo.params), mirroring
cstage cg_variant_match (cmd/w6c/cgen.c:451). A cross-module `a.T` != `b.T`
and `type linerr=!str` != str are now distinguished by the per-decl TY_NAMED
pointer (Phase-N #64). ww has no type_assignable, so the untyped/loose arm
keeps the str/slice shape fallback (rule-10 align-down). The 5 helpers
(flatvariantidx, flatslicevariantidx, taggedvariantindex, cgtagvariantidx,
cgmatch dispatch) flip; nomem propagation (NAMED-name scan, no source value)
and the f64 widen arm (float-kind classification, no pattern node) are not
arm-by-value discrimination and stay name/kind-keyed.

The flip requires value nodes to carry nominal identity. exprtype's
N_STRUCTLIT arm stamped the flattened body, so `overflow{}` (overflow=!void)
got TY_VOID and missed its variant -- fixed to stamp the per-decl NAMED
(mktname(lhs.str) -> tinfofornode reuses the #64 NAMED build/cache, same ptr
the union variant resolved to), mirroring the N_CAST/N_IDENT arms + cstage.
Returns the body node unchanged (only e.type_ rides NAMED); struct-lit layout
is unaffected -- cgstructlitfill is structlookup(name)-keyed, never reads
NAMED.fields. The fix now hits all `T{}` stamps, kept byte-id by the #63/#65
structural-walker peels.

931_variant_typekey_run: table-driven, both stages, /tmp-isolated. Two rows
widen an alias-FIRST variant from a call (no surface name): `(linerr|str)`
str-via-call -> idx 1, `(ec|i32)` i32-via-call -> idx 1. Empirically
discriminating: FAILS pre-flip (wwstage falls to the leading-shape variant,
exit 10; cstage exit 0) and PASSES post-flip -- locking in the capability
byte-id can't reach (the corpus has no name-key/type-key-disagreeing
co-variant, which is why name-keying survived).

make test 134/134 (byte-id 990-997 green; 995 self-rebuild green).
2026-05-23 21:22:24 +09:00
0fde4beeb4 selfhost/cmd/wcc: peel NAMED in elemissignedc + refresh #63 comments (#65)
#64 flowed per-decl TY_NAMED wrappers, falsifying two #63-era assumptions
surfaced in the flip review.

elemissignedc read ti.sub for element signedness without peeling TY_NAMED;
a NAMED-of-indexable would read NAMED.sub (nil) instead of the underlying's.
Add the transitive peel ahead of the .sub read, mirroring cstage idx_eff's
type_unwrap (cmd/w6c/cgen.c:790) before eff->sub (:3518-3520). Byte-id-neutral:
every aliased indexable in-tree has a u8 element (typeissigned=false either
way). Independent .sub/.under inventory confirms elemissignedc was the sole
structural reader missing a peel (castsrcprim + TK_AMP already NAMED-guarded;
typeis* handle element NAMED via .under recursion; typeeq is nominal by
design and must not peel).

Refresh the 5 #63 peel-site comments (slotsize, fieldsize, nullableptrtag,
tupleelemslot, fieldslotsize): the peel now actively fires (#64 builds NAMED)
rather than being a no-op; byte-id holds because NAMED collapses to the
alias-invariant underlying.

make test 133/133 (byte-id 990-997 green).
2026-05-23 19:54:00 +09:00
ae6a59a355 selfhost/cmd/wcc: peel TY_NAMED in 5 structural walkers (#63, Phase-N step 1)
Phase-N prerequisite (additive, byte-id unchanged). slotsize / fieldsize /
nullableptrtag (cgenutil) and tupleelemslot / fieldslotsize (check) read
size/slot/kind off a tinfo without peeling TY_NAMED. Once Phase-N step 2
(#64) makes tinfofornode build per-decl TY_NAMED wrappers, an unpeeled
reader would misbehave (fall through to 8 / take natural size not slot /
miss NAMED-of-tagged). Prepend a transitive `for (t != nil && t.kind ==
tykind.TY_NAMED) { t = t.under; }` peel + nil re-guard at each, mirroring
cstage's `while (t->kind == TY_NAMED) t = t->under` and the recursive
typeis* predicates.

Additive no-op today: tinfofornode still collapses aliases, so no NAMED is
ever built and the loop never executes. byte-id 990-997 unchanged (133/133).

Audit (worker + reviewer, independently, across all selfhost/cmd/wcc/*.ww
+ lib/ww/*.ww): these 5 are the ONLY non-peeling structural walkers. typeis*
recurse on .under; typeeq is nominal by design (ptr-identity, the step-3
goal); typeisuntyped cannot receive a NAMED; check.ww type constructors and
localloadop read only .size/.slotsize, which typenamed copies from .under so
they stay numerically correct on a NAMED without peeling.
2026-05-23 18:44:12 +09:00
38d6eb667c selfhost/cmd/wcc: inline N_INDEX elem tinfo at 3 tagged push/index sites (#60)
pushargsrev and cgindex re-derived an index element's type via
indexvaluetnode (a base->TPTR/SLICE/ARRAY .lhs AST walk). The checker
stamps the element type on the N_INDEX node itself (indexresult,
check.ww:1710; N_INDEX is in the asserttyped gate), so read n.type_
directly at the 3 sites that only need tagged-ness + element size/sign:
  cgenutil pushargsrev L178/490: pass the N_INDEX arg to istaggedtype/
    slotsize (arg.type_ is the element tinfo).
  cgenexpr cgindex L726: esz = n.type_.size, signed = typeissigned(n.type_).

Polarity DOWN per rule 10: cstage has no indexvaluetnode -- it reads
base->type->sub->size directly (cmd/w6c/cgen.c:2070-2071, :3518). The new
path reads element natural size (tinfo.size == primsize: rune=4, u8=1,
str=16, slice=24), matching cstage; it also retires two latent elemsizeofc
divergences (elemsizeofc returned slotsize N*8 for *[N]i64 / 2D-array
elements where cstage uses sub->size=8) -- those shapes are absent from
self-compile, so byte-id stays green.

indexvaluetnode is NOT deleted: its remaining callers at cgenexpr
L3720/3729 feed cgwidentaggedstore -> resolvetagged -> resolvetype, still
node-keyed (nil for non-N_TNAME). Deleting it waits on the resolvetagged
-> tinfo migration (#11).

make test 133/133 (byte-id 990-997 green, independently re-confirmed on
quiescent tree).
2026-05-23 17:04:31 +09:00
827a05e548 selfhost/cmd/wcc/cgenutil: exprprimresolved N_DOT off dotfieldtnode (#59)
exprprimresolved's N_DOT arm derived a struct field's prim width+sign by
walking the field's declared *node (dotfieldtnode + typenodeprimresolved
alias recursion). Read the checker-stamped tinfo instead, mirroring cstage
castsrcprim N_DOT (cmd/w6c/cgen.c:323-344): the base's n.lhs.type_ must
resolve to a struct (peel NAMED->PTR->NAMED, require TY_STRUCT) before the
field counts -- excluding pseudo-fields .len/.cap/.ptr (stamped i32/*T at
check.ww:1987-2004; their base is TY_SLICE/TY_STR/TY_ARRAY, never TY_STRUCT,
so the guard yields sz=0) and tuple positionals, which stay sz=0 to preserve
995 byte-id. The field's width/sign comes from the N_DOT's own n.type_
(check.ww:2012), one NAMED peel then typeisint?size:0. Bool exclusion is
now free via typeisint(bool)=false, dropping the old streq("bool") arm.

Polarity DOWN per rule 10: cstage castsrcprim already reads the stamped
type. typenamed() has zero callers in wwstage (tinfofornode collapses all
alias depth to the body), so no tinfo carries kind TY_NAMED -- the NAMED
peels are dead/inert, output matches cstage.

Removes one dotfieldtnode caller (3->2; remaining cgenutil 1073, 1871).
typenodeprimresolved retained (3 callers: cgenutil 1459/1465, cgenexpr 431).
Part of the dotfield* consumer-migration arc.

make test 133/133 (quiescent tree, byte-id 990-997 green). Coverage:
710_cast_enum_movl rows struct_field_rt + pseudo_field_clamp + bool_to_i8.
2026-05-23 16:10:46 +09:00
8ccda588c6 selfhost/cmd/wcc/cgenutil: trim rhstaggedabicall N_DOT onto n.type_ (#58, A.6.3i-phase-2)
rhstaggedabicall's N_DOT arm re-derived the field type via dotfieldtnode
(a base-ident-only structlookup walk) then asked istaggedtype. The
checker now stamps the field's resolved type on the N_DOT node itself
(check.ww struct-field arm), so read src.type_ directly. typeistagged(nil)
is false, preserving the old ft==nil bail.

Polarity DOWN per rule 10: cstage's gate was already wide. cg_widen_tagged_
store reads src->type inline, NAMED-resolved, then tests TY_TAGGED
(cmd/w6c/cgen.c:1302-1305) -- no standalone helper. Pre-trim wwstage's
gate was NARROWER (dotfieldtnode required base==N_IDENT, firing only for
`ident.field`); reading src.type_ also fires for chained-dot bases
(`a.b.c`) since the checker stamps N_DOT.type_ at any depth. This aligns
wwstage UP to cstage's existing coverage. Byte-id 990-997 green confirms
the widened shape does not occur in bootstrap sources -- byte-id-neutral.

Drops one dotfieldtnode caller; the *node-returning callers
(typenodeprimresolved, indexvaluetnode, matchscrutt) stay until their
consumers migrate to *tinfo (the closing dotfield* deletion follows once
none need *node). First clean slice of that consumer migration; same
g/h-style stamp read as #55/#56.

Known pre-existing gap (filed, not introduced here): neither stage's
chained-DOT read path has a tagged-union leaf branch, so a chained-dot
tagged ABI source would store stale DX/CX/R8. Symmetric across stages
(byte-id stays green); the retained `cgdot loads AX=tag, DX=word0, ...`
comment is accurate only for ident.field bases.

make test 133/133 (quiescent tree, byte-id 990-997 green).
2026-05-23 15:41:31 +09:00
8d4a9700a0 selfhost/cmd/wcc/cgenutil: collapse slice+str N_DOT arms (#56, A.6.3h)
nodeisslice + nodeisstr each walked base->struct->field on N_DOT,
gated on `base.kind == nkind.N_IDENT` (with dotinnerstructptr and
dotchainresolve fallbacks for chained / value-struct cases) --
duplicating cstage at the AST level while silently dropping
multi-level chains rooted at non-IDENT/DOT bases (e.g. `t.1.field`
for a tuple positional). After A.6.2 the checker stamps n.type_ on
every N_DOT (check.ww:1947-1996 pseudo-field, struct-field, and
tuple-positional arms), and A.6.3b landed typeisstr / typeisslice.
Both arms collapse to one tinfo read.

Polarity DOWN per rule 10: cstage was already aligned. cgen.c:168-170
node_isstr = type_isstr(n->type); cgen.c:182-184 node_isslice =
type_isslice(n->type). Wwstage was the laggard; this brings wwstage
to cstage's leaner shape, mirroring A.6.3g (8da1414).

Silent-false coverage gain: multi-level chains and tuple-positional
bases (`t.1.field` for a str element) now resolve via the checker
stamp instead of returning false. The "Not covered: tuple-positional"
bullet in nodeisstr's docstring is removed accordingly. dotchainresolve
itself is untouched -- still used by cgenexpr (cgdot read-path, &-of
address-of, structlit BP write-back). Byte-identity (994/995) is the
gate.

make test 133/133 ok. Net cgenutil.ww -110.
2026-05-23 14:39:05 +09:00
8da14147b3 selfhost/cmd/wcc/cgenutil: collapse N_DOT arms onto n.type_ (#55, A.6.3g)
nodeisunsigned + exprfloatkind each manually walked base->struct->field
on N_DOT, gated on `base.kind == nkind.N_IDENT` -- duplicating cstage's
behaviour at the AST level while silently dropping the nested-N_DOT case
(`a.b.c` returned the conservative default). After A.6.2 the checker
stamps n.type_ on every N_DOT expression (check.ww:1973 struct-field
arm of exprtype), and A.6.3a/b landed the tinfo helpers (typeisunsigned,
isfloattype/isf32type) that the inner reads already use. Both arms
collapse to one tinfo read.

Polarity DOWN per rule 10: cstage was already aligned. cmd/w6c/cgen.c:
2330-2331 reads type_isunsigned(n->lhs->type) directly; cgen.c:152-156
node_isfloat = cg_isfloat(n->type); cgen.c:195-199 node_isf32 =
type_isf32(n->type). No exprfloatkind-equivalent walker exists in
cstage -- it is pre-A.6.2 wwstage scaffolding. Wwstage now reads the
same shape as cstage on N_DOT.

Nested N_DOT (a.b.c) now resolves to the field type instead of returning
the conservative default. Byte-identity (994/995) confirms the codegen
matches cstage on the test set -- cstage was already getting nested-dot
right via checker-stamped n->type, wwstage was the laggard.

make test 133/133 ok. Net cgenutil.ww -41 / +3.
2026-05-23 13:59:06 +09:00
908875682a selfhost/cmd/wcc/cgenutil: collapse nullableptrtag onto tinfo.params (#50 phase 2, A.6.3f-b)
Phase 2 of A.6.3f, closing A.6.3 entirely (a/b/c/d/e/f-a/f-b all
landed). With #50 phase 1 (26724fe) populating TY_TAGGED.params,
nullableptrtag retires its AST-keyed predecessor: walk ti.params,
strip TY_NAMED via .under on each variant, return idx of first
TY_PTR.

Mirrors cstage cmd/w6c/cgen.c:404-416 line-for-line. Source-order
semantics preserved by phase 1's append-tail head/tail
construction (head = first n.list variant). For a `(*T | void)`,
*T-first → returns 0; void-first → walks past void (vt.kind==
TY_VOID, no match), *T at idx 1 → returns 1.

Two paths the new tinfo-keyed body handles that the AST walk
missed (cstage parity, dead-in-bootstrap today, parallel to #49
TY_TUPLE / #51b typed-float corrections):

  - Aliased *T variant: `type ip = *int; let x: (ip | void);` —
    cstage cgen.c:415 strips TY_NAMED via .under; new wwstage
    body mirrors. Bootstrap has zero aliased-ptr-variant
    callsites today (`grep "^type [a-z]+ = \*"` yields only an
    out-of-union test/uses.ww case).
  - void-first ordering: cstage walks past TY_VOID and finds *T
    at idx 1. Same in wwstage. No test/wcc fixture exercises
    void-first today.

Outer-type TY_NAMED strip (cstage cgen.c:409 `if t->kind==TY_NAMED
t = t->under`) intentionally skipped: tinfofornode for N_TNAME
already resolves the alias and returns the underlying tinfo
unwrapped — `typenamed` is declared at lib/ww/typ.ww:238 but has
zero producers in selfhost today. The strip would be a no-op
under current invariants. Tracked as part of #13 (TTAGGED
normalization parity) for when wwstage starts producing
TY_NAMED.

Net +30 LOC across cgenutil.ww + two .combined.ww bundler regens
(new function body is ~14 lines vs ~4; new WHY comment shorter
than the pre-graduation note's 7).

Byte-identity (994/995) is the gate; full make test green at
133/133 confirms.
2026-05-23 02:21:53 +09:00
883665e962 selfhost/cmd/wcc/cgenutil: collapse fieldsize onto tinfo (#49, A.6.3e)
fieldsize selected the slot-padded byte width of a struct field's
type-AST via the same TBANG/TNAME/TPTR/TSLICE/TARRAY/TTAGGED walker
shape slotsize used pre-#48 — with TNAME branching into
structlookup, enumlookup, and aliaslookup to follow the AST chain
back to a primitive size or struct totsize. After A.6.2 stamping
+ #48's slotsize collapse, the same data is reachable through
the populated tinfo:

  TY_STRUCT / TY_ARRAY → ti.slotsize
  TY_TAGGED / TY_SLICE / TY_STR → ti.size
  TY_PTR / TY_FN / TY_CHAN → 8
  primitives + TY_ENUM + TY_TUPLE → ti.size (catch-all > 0)
  fallback → 8

cstage SSoT is `f->type->size` at cmd/w6c/cgen.c:1386, :1656, :2515,
:2535 — wwstage routes through ti.slotsize for composites (the
#48 verdict centralizes the slot-pad rule on tinfo) and through
ti.size where natural width and in-struct width coincide.

Two latent behavior fixes ride alongside the collapse, both
cstage-parity and dead-in-bootstrap (995 byte-id is the regression
gate, currently green at 133/133):

  - TY_TUPLE-typed struct field: old walker had no TTUPLE arm and
    fell through to 8; cstage `f->type->size` reads the natural
    sum (e.g., 24 for `(i64, str)`). New code reads ti.size,
    matching cstage.
  - !T-typed struct field: old walker had no TBANG arm and fell
    through to 8; cstage iserror-passthrough returns the inner
    type's size. tinfofornode strips N_TBANG (check.ww:1154-1161)
    so the new dispatch sees the inner ti directly.

No fixtures in selfhost exercise either case today; both fixes
are pre-correct for future code that does.

Body went from ~47 LOC to ~16 LOC. `c: *cgen` retained unused for
callsite stability (slotsize / localloadop precedent, a828c03 /
68219a1). Two callsites in cgenutil.ww:1870 + cgenexpr.ww:3594
untouched.

Dot-chain helpers (dotinnerstructptr, dotfieldtnode,
dotchainresolve) are split off as #49b — they walk a cgen-side
`structinfo` table keyed by field name + offset, and tinfo.fields
is not yet populated by check.ww's TSTRUCT/TTUPLE arms. Parallel
structure to #50's TY_TAGGED.params gap; treat as a separate
populate-then-port pair.

Net -48 LOC across cgenutil.ww + two .combined.ww bundler regens.
Full make test green at 133/133.
2026-05-23 01:28:38 +09:00
a828c036d0 selfhost/cmd/wcc/cgenutil: collapse slotsize onto tinfo (#48, A.6.3d)
slotsize selected the slot-padded width for a frame slot via an AST
walker over TBANG / TPTR / TFN / TCHAN / TSLICE / TTUPLE / TTAGGED /
TNAME / TARRAY / TSTRUCT — duplicating the size + slot-pad math
tinfofornode already runs at check time. With A.6.2 stamping every
N_T* kind's n.type_, plus #61 A.5 splitting tinfo.slotsize from
tinfo.size, the dispatch collapses to a kind switch over the
populated tinfo:
  - TY_VOID → 0
  - TY_PTR/SLICE/CHAN/FN/STR/TAGGED → ti.size (size == slotsize for
    these kinds)
  - TY_STRUCT/TUPLE/ARRAY → ti.slotsize (slot-padded by check.ww's
    TSTRUCT/TTUPLE/TARRAY arms with the same field-pad / stride
    rules cgenutil's registerstruct uses)
  - primitives → catch-all 8 (pad-to-8 lives at the read site, not
    in ti.slotsize, so [N]i32 stride stays 4)

Ww-to-Hare divergence (deliberate, team consensus): Hare's struct
type carries only `size` (ref/harec/include/types.h:134-137); QBE
handles slot padding downstream. ww emits Plan 9 amd64 asm directly,
so slot-padding is part of the ww calling convention and belongs on
the type table. Cstage scatters pad-to-8 inline at ~30+ localoff
sites + the `(su->kind == TY_TAGGED) ? su->size : 16` tagged-spill
pattern at cmd/w6c/cgen.c:4850 / :5193 — that scattering would be a
rule-13 (no hardcoded size literals) violation in ww. Centralizing
on tinfo.slotsize IS the rule-13-compliant shape; #61 A.5 put it
there, and #48 just consumes it. Rule 9 is lib/-scoped; tinfo is
compiler-internal (already diverges via node.type_ as a checker
invariant Hare/harec lacks). Rule 10 covers inference power, not
data shape — byte-identity (994/995) holds at 133/133.

Body went from ~150 LOC AST walk to ~15 LOC tinfo dispatch. Two
nil guards (typn == nil, ti == nil) fall through to 8 — same outcome
as cstage's `(t && t->size > 0) ? sz : 8` defensive shape.
`c: *cgen` retained unused for callsite stability (localloadop
precedent, 68219a1).

Net -453 LOC across cgenutil.ww + two .combined.ww bundler regens.
Follow-up filed: grow cstage Type.slot_size symmetrically and
sizelint-ok-annotate the surviving inline 8s until ported.
2026-05-23 00:55:44 +09:00
68219a119c selfhost/cmd/wcc/cgenutil: collapse localloadop onto tinfo (#47, A.6.3c)
localloadop selected MOVBQSX/MOVBQZX/MOVSWQ/MOVL/MOVQ for scalar
local/let loads via an AST walk down TBANG/TENUM/TNAME-alias chains,
re-consulting aliaslookup and ending in fieldsize + fieldissignedc.
Three precursors retire the walk: #53 (e3237d1, scopelookupprefer
fixes flat-alias collision in resolvealias), #51 (1ce0f63, INTLIT
tsuffix stamp), #52 (747279c, typenameisunsigned collapse onto
typeisunsigned). Plus the A.6.2 type-AST stamp invariant at
check.ww L426-436.

Body collapses to two nil guards + ti.size + size cascade +
typeissigned(ti). cstage cmd/w6c/cgen.c:357-362 is the SSoT;
wwstage now reads tinfo.size directly the same way cstage reads
t->size, with TBANG / TENUM / TNAME-alias chains pre-folded by
tinfofornode (check.ww:1102-1153 TNAME, 1154-1161 TBANG,
1196-1208 TENUM).

Defensive nil branches mirror cstage's `(t && t->size > 0) ? sz : 8`
fall-through: when type info is missing, sz fails the != 1/2/4
discriminator and the helper returns MOVQ. Both nil paths are
reachable: cgenexpr.ww callers (4 sites: 643, 664, 5380, 5471)
populate the *node via a search loop that may exit with nil.

Signature unchanged — `c: *cgen` is retained unused for callsite
stability, mirroring A.6.3a fieldissignedc(c, t) which followed the
same precedent. 5 callsites in cgenexpr.ww untouched (body-only
collapse).

Net -33 LOC across cgenutil.ww + two .combined.ww bundler regens.
Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
2026-05-23 00:11:05 +09:00
747279c029 selfhost/cmd/wcc: collapse typenameisunsigned onto tinfo (#52, A.6.3c-cast)
A.6.3a (#45) deferred typenameisunsigned because its two callers —
typenodeprimresolved and exprprimresolved — consumed a raw str:
TNAME.str / INTLIT.tsuffix. #51 (1ce0f63) stamped tsuffix-typed
N_INTLIT.type_ via tinfofornode; together with check.ww L426-436
type-AST stamping, both call sites now read a stamped n.type_
instead of a raw name.

typenodeprimresolved L1583 swaps typenameisunsigned(nm) for
typeisunsigned(cur.type_: *tinfo). cur is the walked N_TNAME; its
type_ is stamped at check time.

exprprimresolved L1620 swaps typenameisunsigned(s) for typeisunsigned
(n.type_: *tinfo). n is the N_INTLIT whose tsuffix is s; type_ is
stamped by the #51 arm. primsize(s) > 0 IS the tsuffix-resolves-to-
builtin gate, so the stamp is guaranteed at the read site.

typenameisunsigned (cgenutil.ww L850-866) deleted: -17 LOC of body
+ WHY block. Net change is three files (cgenutil.ww + two
.combined.ww bundler regens, same -17 each).

Mirrors cstage cmd/wcc/type.c:178 type_isunsigned plus cmd/w6c/cgen.c
castsrcprim routing INTLIT.tsuffix through n->type, then
type_isunsigned. Aligned, not richer.

Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
2026-05-22 23:44:31 +09:00
b8e5a921f8 selfhost/cmd/wcc: collapse type-kind predicates onto n.type_ (A.6.3b, #46)
The node-keyed kind helpers (typeis8byteprimitive, isstrtype/raw,
isslicetype/raw, istaggedtype/raw, isfloattype, isf32type/raw,
isf64typeraw, isnullabletype) each re-walked TNAME aliases via
aliaslookup and peeled TBANG by hand — duplicating cstage's single-
peel kind predicates at the AST level. After A.6.2 every type-AST
kind these read is tinfo-stamped at check.ww L426-436, and
tinfofornode collapses N_TBANG (check.ww:1145-1152) and the TY_NAMED
chain, so each predicate folds to one tinfo read.

Six new tinfo helpers in lib/ww/typ.ww mirror their cstage SSoT
verbatim:

  typeisstr      — cstage cgen.c:159 `type_isstr`     (TY_STR / TY_UNTYPED_STR)
  typeisslice    — cstage cgen.c:174 `type_isslice`
  typeistagged   — cstage cgen.c:516 `type_istagged`
  typeisf32      — cstage cgen.c:188 `type_isf32`
  typeisnullable — cstage cgen.c:396 `type_isnullable` (reads tinfo.nullable
                                                       stamped at check.ww:1309-1318)
  typeis8byteprim — cstage cgen.c N_LET sz==8 ladder (slot-pad set)

Rule 9 carve-out per the A.6.3a precedent: each helper has a named
cstage counterpart; the wwstage shape mirrors it directly. The five
dead AST-walking variants (isstrtyperaw, isslicetyperaw,
istaggedtyperaw, isf32typeraw, isf64typeraw) are deleted; the five
remaining callsites (cgenstmt cglet / cgmlet str-routing, cgenexpr
cgdot tuple-field) graduate to the alias-aware isstrtype(c, t).

nullableptrtag stays AST-keyed for now — tinfofornode doesn't
populate TY_TAGGED.params (check.ww:1287-1337 sets size / align /
nullable but not the variant chain), so the tinfo equivalent of
cstage cgen.c:405 `nullable_ptr_tag` can't read params today. WHY
comment at the site cites #50 / A.6.3f as the graduation point,
alongside the variant-index work and the tparam-population glue.

Byte-identity (994/995) is the behavior gate; full `make test` green
at 133/133 confirms.
2026-05-22 05:53:34 +09:00
03e4718199 selfhost/cmd/wcc: collapse signedness predicates onto n.type_ (A.6.3a, #45)
The node-keyed signedness helpers (typenodeisunsigned,
typenodeisunsignedc, elemissigned, elemissignedc, fieldissignedc) each
re-walked TBANG / TENUM / TNAME chains and re-consulted alias / enum
registries — duplicating cstage's type_isunsigned (cmd/wcc/type.c:178)
and fld_issigned (cmd/w6c/cgen.c:240) at the AST level. After A.6.2
every type-AST kind we read here is tinfo-stamped at check.ww L426-436,
so the predicates collapse to a single tinfo read.

Two new arms close the wwstage divergence from cstage: typeisunsigned
gains TY_RUNE and TY_ENUM (recurse on .sub), matching type.c:178
verbatim. typeissigned is added as the cgen-facing predicate per
fld_issigned semantics (TY_BOOL excluded for sub-word storage —
0/1 → MOVZBQ — so it's not just !typeisunsigned). Rule 9 carve-out:
the helper exists in cstage; harec keeps the same pair.

elemissigned was fully dead (no callers); deleted. typenameissigned
was internal-only and dead post-collapse; deleted. typenameisunsigned
survives — two call sites (typenodeprimresolved, exprprimresolved)
hold only a raw `str` (TNAME.str / INTLIT.tsuffix). paramissigned in
cgenstmt.ww unchanged. Both deferrals close in A.6.3c (#47).

Byte-identity (994/995) is the behavior gate for the alias/enum
sites — full `make test` green at 133/133 confirms.
2026-05-22 05:24:49 +09:00
805c841f34 selfhost+lib/ww: N_TPARAM wrapper for tuple chains (A.6.2.0b-pre)
A.6.2.0b worker hit a real shared-`.next`-aliasing bug and stopped
per rule 7. Wwstage's N_TTUPLE chained element type ASTs via the
nodes' own `.next` field. `exprtype` routinely returns shared
nodes (sym.decl.lhs, struct field's `.lhs`, another N_TTUPLE's
`.list` element). Naive chain construction in the checker
corrupts source ASTs.

Introduce N_TPARAM = 67 as a chain wrapper for N_TTUPLE.list:

  - `.lhs` holds the (possibly-shared) element type AST.
  - `.next` chains within the parent N_TTUPLE.
  - Other fields unused; never appears outside N_TTUPLE.list.

Mirrors cstage's Tparam at cmd/wcc/check.c:1437-1451. Cstage
keeps it at the Type layer; wwstage has no separate type layer
for tuple chains so the wrapper sits at the AST. Hare's design
intent at ref/hare/hare/ast/type.ha:117 uses `[]*_type` slice-of-
pointer — same principle, slice-flavored.

Migrations:
  - lib/ww/ast.ww: kind + nkname + pr() unwrap (transparent for
    the 990 -a astprint byte-diff).
  - lib/ww/parse/parse.ww: parsetype N_TTUPLE construction wraps
    each element in N_TPARAM (sole construction site).
  - selfhost/cmd/wcc/check.ww: 4 readers (astalign, astsize,
    tinfofornode TY_TUPLE, exprtype N_DOT-tuple-positional). The
    last change retires the latent A.6.1.5b shared-`p` return.
  - selfhost/cmd/wcc/cgenutil.ww: slotsize TY_TUPLE arm.
  - selfhost/cmd/wcc/cgenexpr.ww: cgdot tuple-positional
    (size/load op + str-check).
  - selfhost/cmd/wcc/cgenstmt.ww: cglet TTUPLE init, cgmlet
    call-return walk, cgforrange elem-size + bind-walk.

Out of scope: N_TFN params, N_TTAGGED variants, N_TSTRUCT fields.
N_TFIELD already wraps struct fields; N_TFN/N_TTAGGED aren't
currently chain-mutated by checker synthesis. If they ever are,
the same pattern applies.

Unblocks A.6.2.0b stamp on a clean foundation. Retires task #16.

Verified 132/132 incl. 990 AST byte-diff (astprint unwrap) + 995
self-rebuild byte-identity.
2026-05-21 20:55:40 +09:00