Commit Graph

359 Commits

Author SHA1 Message Date
d9cfb91cb9 types: add INT/UINT limit constants, derived from size(int) (#114)
ww's int/uint are machine words (8B on amd64, type.c:58), not the 4B
Hare gives them on amd64 (arch+x86_64.ha maps INT_MAX->I32_MAX). So the
limits can't alias a per-arch literal; they DERIVE from size(int) the
Go way (cf math.MaxInt), staying correct on any word width:

  INT_MAX:  int  = (1 << (size(int)*8 - 1)) - 1
  INT_MIN:  int  = -1 << (size(int)*8 - 1)
  UINT_MIN: uint = 0
  UINT_MAX: uint = ~(0: uint)

All four const-fold in def-init; on amd64 they evaluate to I64_MAX,
I64_MIN, 0, U64_MAX. UINT_MAX uses the all-ones complement to dodge the
1<<64 overflow. Per the user ruling (2026-05-26): derived, not literal.

Probe 959_types_intlim_run asserts each value vs both the literal and
the i64/u64 limit const, plus wrap-through-i32 arithmetic usability.
combined.ww regenerated for all 5 selfhost tools + smoke (all embed
lib/types).
2026-05-26 08:31:41 +09:00
b1c598651f types: add SIZE/UINTPTR limit constants
Faithful port of ref/hare/types/arch+x86_64.ha:16-26. SIZE_MAX is the
no-cast `def SIZE_MAX: size = U64_MAX;` — size is in the unsigned class
and 8B on amd64, so the u64->size init coerces without a cast (#113);
UINTPTR_MAX keeps Hare's explicit `U64_MAX: uintptr` since uintptr is
outside the unsigned class. Probe 958_types_sizelim_run asserts MIN==0,
MAX==U64_MAX, and arithmetic usability for both types.

INT_MIN/MAX + UINT_MIN/MAX deferred to #114 (ww int=8B vs Hare 4B on
amd64 leaves the value open); RUNE_MAX deferred to #112 (no \U lexer).
combined.ww regenerated for all 5 selfhost tools + smoke.combined.ww
(all embed lib/types).
2026-05-26 03:07:01 +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
bd7181ae1f wcc: add the size primitive type (TY_SIZE), classify as unsigned int (#85)
fold-1: type exists + classifies; mirrors TY_UINTPTR at every site, both stages. size(T)/len() return types UNCHANGED (fold-2). Regenerates the 5 combined.ww (lib/ww embedded).
2026-05-26 01:26:03 +09:00
529eb51af3 selfhost: resync smoke.combined.ww to current lib/types (#110)
#28 regen'd the 5 cmd combined.ww but missed smoke, which pulls lib/types transitively. Baseline for the freshness gate below.
2026-05-26 01:07:25 +09:00
c40b2df097 lib/types: add U*_MIN and RUNE_MIN
Mirror Hare's types::limits U8_MIN..U64_MIN (all 0) and RUNE_MIN
('\0'), ref/hare/types/limits.ha:30,36,42,48,54. Pure literals,
byte-id-neutral; the U*_MIN unblock checked sat_subu* which clamp to
types.U*_MIN.
2026-05-26 00:35:53 +09:00
4578ce98a0 selfhost: resync w6c/wwdump combined.ww to current wcc source (#109)
Catch-up regen only; no source change. w6c and wwdump embed the wcc cgen, whose post-#97 edits landed without regenerating these two tools' combined.ww -- the byte-id gates are freshness-blind, so master stayed green while shipping a stale artifact. Permanent freshness gate filed as #110.
2026-05-26 00:33:50 +09:00
ec19d0ad20 cgen: tuple receive spills f64 word from XMM, not integer reg (both stages, #105)
A (f64,i64)/(i64,f64) tuple returns its f64 word in X0 (the SSE return
reg) and its integer word in an integer reg (tuple_rseq AX/DX). All three
tuple-from-call receive forms — single-var (cglet), destructure (N_MLET),
reassign (N_MASSIGN) — share the #83 tuple_rseq cursor and all spilled
the f64 word via MOVQ from the integer cursor; that reg holds garbage
(the float is in X0), and #103-FACE-Z's field read (MOVSD slot,X0) then
reads it. A single-return callee masked it (a float-literal return leaves
the f64 bits in AX, and X0 stays live); a branched callee with a non-
literal f64 word has an inner CALL clobber AX, exposing the corruption.

Make every receive spill class-aware: an f64/f32 word spills MOVSD/MOVSS
from X0 (the single SSE return reg, which survives the reg->mem stores
regardless of the word's position), an integer word spills MOVQ from its
tuple_rseq reg as before. cstage applies this at all three inline sites
(cglet, N_MLET, N_MASSIGN); wwstage at the cglet branch and in the shared
tupstore helper (covering cgmlet and cgmassign). The integer/str/slice
path is byte-identical to before, so bootstrap codegen is unperturbed.
Multi-float tuples collide on X0 at the RETURN (#107), out of scope here.
2026-05-25 16:23:47 +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
6a586cf792 cgen: wwstage cglet spills both eightbytes of a 16B tuple-from-call receive (#102)
wwstage-only. cglet had no 16B whole-tuple-from-call receive branch, so
`let t = call()` whose callee returns a 2-eightbyte (16B) tuple fell
through to the generic single-word store (MOVQ AX, off(BP)) and never
spilled word1 (the DX eightbyte) — silent loss of t.1. Align to cstage
cgen.c:6652, which spills both AX->off+0 and DX->off+8.

Not a tupstore cursor off-by-one and not f64-specific: the destructure
form `let (a,b) = call()` (cgmlet + tupstore cursor) was already byte-id;
only the whole-tuple N_LET receive dropped word1, for any element mix
incl. all-integer (i64,i64). An f64 element surfaced it first. The f64
element rides its eightbyte in AX/DX at receive and is re-read from
X0/XMM at field-read (already byte-id), so no SSE cursor is needed.
2026-05-25 14:29:34 +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
fa136d0b88 cgen: f64 compare consults parity flag for NaN, 4 relops (both stages, #97)
UCOMISD/UCOMISS set PF=ZF=CF=1 on unordered (a NaN operand). The old
arms keyed on ZF/CF only, so 4 of the 6 relops mishandled NaN:
`nan != nan` was false (JNE keys on ZF=0), `nan == nan` was true, and
`<`/`<=` (JB/JBE) fired on the unordered CF=1. IEEE-754: any relop
with a NaN operand is unordered — `!=` true, the rest false. `!=` now
jumps to true on JNE OR JP; `==`/`<`/`<=` jump to false on JP before
the ordered Jcc.

`>`/`>=` (JA/JAE) are LEFT UNCHANGED: they require CF=0, which an
unordered UCOMISD never produces, so they already reject NaN
correctly. Adding a PF guard there would only churn their .s (an extra
JP on every >/>= float compare) for no correctness gain, so their arm
stays byte-identical to the pre-#97 single template.

Bundles the cgen fix with JP-mnemonic support in both assemblers
(w6c enum/printer + w6a/w6a_ww parse+encode, 0F 8A). They can't split:
the cgen emits JP, which has no encoding without the assembler change,
so a cgen-only commit would not build. JP is the only PF-sensitive
jump on amd64 — there is no alternative instruction.
2026-05-25 12:44:56 +09:00
2f2a73bd41 cgen: f64 deref-load -> MOVSD/MOVSS into X0 (both stages, #96) 2026-05-25 12:38:57 +09:00
0d1ae17dd0 check: def rhs const-fold resolves sibling/imported defs + casts (#88)
ww top-level def rhs const-fold was literal-only (fold_int_literal at the codegen emit-defs step), so a def referencing another def, an imported def, or a cast was inexpressible -- blocking faithful types/types::c/math/strconv ports whose defs cross-reference.

Fold at CHECK time: a recursive eval_def_const (pass-2 N_DEF arm, both stages) resolves N_IDENT/N_DOT via the checker's existing scope lookup to the target def's rhs, evaluates N_BIN through a shared fold_binop core (factored out of eval_enum_value so both compile-time-int-eval paths share one wrap/shift/divide table), strips identity/widening casts, and stamps rhs -> N_INTLIT. cgen is UNTOUCHED -- its existing literal-emit lays the DATA row. Gated to fire only when the plain literal fold fails, so existing defs keep their node and emitted asm is byte-identical (990-997 unperturbed by construction).

Guards (rule 7): recursion depth cap fails loud on a def cycle (same/cross-module); a narrowing cast (rhs outside target range) fails loud rather than silently truncating. Both stages' eval_def_const stamp identically (shared fold_binop semantics) so the substituted literal -- and byte-id -- holds across stages (rule 10, at the check pass).

a1 (same-module) + a2 (cross-module imported def) land together: the driver concatenates imports into one flat scope. Coverage: test/wcc/732_def_const_fold.
2026-05-25 11:27:00 +09:00
5a0427ef32 cgen: N-ary tuple destructure positional store + loud-stop (both stages, #83)
Replace the str-only XOR (e0_is_str ^ e1_is_str) at the tuple send
(N_RETURN) and receive (N_MLET/N_MASSIGN) sites with a positional
per-element register cursor, mirroring harec create_unpack_bindings
(ref/harec/src/check.c:1354-1416). Each element rides consecutive
eightbytes over [AX,DX,CX,R8]; a slice/str rides its 3-word
{ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), a scalar rides 1.
Send and receive walk the SAME type-table widths so element->register
agrees. This routes []u8 elements through the 3-word path (the XOR was
slice-blind, dropping len+cap to the scalar fallback) and closes the
pre-existing (scalar,slice) cs!=ww divergence by construction. cstage
and wwstage emit byte-identical asm.

Both receive sites derive each element's width from the rhs tuple's
element types (n->rhs->type->params / the callee return type) -- the
SAME producer view the send site walks -- NOT the binding type: a `_`
lvalue is an N_IDENT with empty str the checker never type-stamps, so a
binding-typed width mis-sized a wide `_` and desynced the cursor for the
next element (cstage read DX, wwstage R8). harec `_` skips the store but
CONSUMES its tuple offset; the cursor advance honours that.

Loud-stop (rule 7): the register file holds 4 eightbytes; a tuple whose
elements sum to >4 (([]u8,[]u8)/(str,str)=6) cannot be register-returned,
so the send site aborts at compile time citing the return-ABI capacity
(#10) rather than silently miscompiling. The receive loop guards the
same predicate (defense-in-depth). Routed through each stage's EXISTING
pinned-fatal idiom: cstage fatal() (cmd/wcc/err.c), wwstage the inline
os.write(2,...)+os.exit(1) at cgen.ww:604 -- no new diagnostics path.

N_MASSIGN (`a,b=f()`, bare comma, pre-declared) is a retained
ww-EXTENSION beyond Hare's binding-only tuple-unpack (Go/rob-pike
multi-assign, rule-9 carve-out); the loop covers it identically to
N_MLET.

Test 945_tuple_nary_destructure_run: (i64,[]u8)+(i64,str) store+read
len/cap for both N_MLET and N_MASSIGN, a single-str control, a wide-
first blank `_,a=f()` row (the cursor-desync discriminator), and a
([]u8,[]u8) row asserting the loud BUILDERR carries the cited
diagnostic; dual ww/ww_ww drivers.
2026-05-25 10:08:56 +09:00
1304db8871 cgen: *p=sliceval deref store -> 3-word (both stages, #79)
A slice VALUE stored through a whole-deref lhs `*p = v` dropped len+cap:
the `*p = v` arm kind-gated its 3-word {ptr,len,cap} stash+store on str
ONLY, so a slice fell to the 1-word fldstoreop default (ptr only). The
deref READ is 3-word, so the reader got garbage len/cap -- correctness,
not perf. str IS []u8 since #1, so the str machinery applies verbatim;
widen the gate str -> str||slice (kind-OR, not a sz==24 test). This is
the project #75 str-only-gate one level down (deref-store).

cstage cmd/w6c/cgen.c:3792/3800 (two gates); wwstage cgenexpr.ww `*p=v`
twin detects N_TSLICE syntactically (mirror str). Both stages dropped
identically, so cs==ww + 990-997 + byte-id are all gate-blind here --
only a store->read roundtrip catches it. New 944_deref_slice_store_run
asserts the {ptr,len,cap} survives a poisoned dst, via the direct local
and the field-deref read; str-deref + (*p).field controls guard the
untouched arms. Verified fail-before (1-word ptr store) / pass-after
(8/8), byte-identical asm both stages.

Out-of-gate, deferred to #80: the wwstage syntactic detection is
alias-BLIND -- a slice-alias `*Foo` (Foo=[]T) or non-ident deref-store
stays 1-word, the SAME retained divergence str already carries (cstage's
resolved-type vt fires in both). #80 unifies detection by aligning the
wwstage UP, not gating cstage down. Separately surfaced (filed apart,
not touched here): the whole-deref READ-into-let `let v = *p` drops
len+cap for a slice while the str form is 3-word -- the read-side twin
of this store hole.
2026-05-25 03:13:48 +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
451e2ebec9 cgen: slice-elem store/read -> 3-word via kind-OR (both stages)
The G-cluster gave str its 3-word {ptr,len,cap} store/read at indexed/field/chained sites, but each arm was gated on str only; the slice arm fell through to the 1-word fldstoreop default, dropping len+cap. A []T value stored through arr[i]=, arr[i].f=, *struct.f=, or value-spine o.i.f= (and read back via arr[i] / arr[i].f) silently lost length and capacity.

Widen all six arms (4 stores + 2 read mirrors) with a kind-OR (TY_STR||TY_SLICE / typeisstr||typeisslice), never a size test: str and slice are both 24B, so a width gate would fire on both and mask the missing slice arm. The str kind stays distinct and nominal -- the arm is widened, the kinds are not collapsed. cstage and wwstage mirrored.

Gate-blind class: store and read were both short, so byte-identity and cstage==wwstage stayed green on self-consistent garbage; only a runtime len/cap round-trip exposes it (test 941, table-driven, 4 shapes x 2 stages, fail-before/pass-after on both ww and ww_ww).

Deref store (*p=) and tuple-elem store (N_MLET/N_MASSIGN, distinct DX,CX,R8 return-ABI) are the same bug class but separate folds.
2026-05-25 00:20:16 +09:00
7bb40d924f cgen: merge byte-identical str+slice cgassign arms onto kind-gates -- Phase 2 C4.4
Post the str->24B lifts, cgassign had SEPARATE str and slice arms emitting byte-identical 3-word {ptr,len,cap} code. Collapse each identical pair into ONE kind-gated arm (rule 12, sea-of-stars; removes a drift hazard) -- the structural str==[]u8 unification, byte-id-NEUTRAL (each stage's emission unchanged for both str and slice inputs). Pairs: field store s.f=v + ident reassign name=v. cstage gates on the EXACT predicate union (raw kind==TY_STR OR'd with TY_SLICE -- NOT type_isstr, which would also match TY_UNTYPED_STR); ww on isstrtype||isslicetype and letvarisstr||letvarisslice (ww local field/reassign were already merged). Mirrors the in-tree deep-value-chain precedent (cstage 3274). str-only arms with no slice pair (arr[i].field=/chained, G1/G2) untouched.

Verified per-stage PRE==POST byte-identical (focused 5-path fixture + 4 large real combined.ww inputs, both stages); the 5 pairs were byte-identical pre-merge. main.combined.ww regenerated via the canonical make path (md5-stable). A pre-existing global-slice-field-store divergence (g.sl=b: cstage 3-word, wwstage 1-word) surfaced during review -- filed (#26/#10), NOT a C4.4 concern (PRE==POST).
2026-05-24 22:39:04 +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
3d7c707bd2 cgen: ww str for-range loop-var narrows to MOVZBQ -- Phase 2 step-3 Fold 1 (align-up)
Ranging a str (for (let b .. = s)) and reading the loop var back emitted MOVZBQ on cstage (correct u8 zero-extend) but MOVQ on wwstage (the missed case, #14). Align wwstage UP. ww cgforrange derived the element-type node only for slice/array; for a str scrutinee it left elemt=nil, so the loop var registered with no type and localloadop short-circuited to MOVQ. Fix: for a str scrutinee, synthesize a u8 element node (type_ = str.sub = u8, from F1) as elemt, so localadd hands the loop var a u8 tnode and the GENERIC narrow-load fires (MOVZBQ) -- consuming str.sub as F1 intended, mirroring how []u8 supplies its element node. NOT an if-str special-case. cstage already correct, untouched (ww-only). str's own type stays nominal.

GATE is the ASM SHAPE byte-id (cstage==wwstage at the loop-var read), NOT a runtime probe: the divergence is runtime-benign (MOVQ and MOVZBQ read the same zero-extended byte) so a runtime test passes both ways and cannot distinguish -- it was a byte-id-INVISIBLE divergence (990-997 green despite cstage!=ww, since no bootstrap input exercises a narrow-read str loop var). Verified fail-pre (the cstage-MOVZBQ vs wwstage-MOVQ 1-line diff) / pass-post (.s byte-identical). []u8/slice/array for-range emission unchanged. test/wcc/940 carries the fixture (runtime corpus coverage, both drivers).

main.combined.ww regenerated via the canonical make path.
2026-05-24 17:29:20 +09:00
80527f3868 cgen: str a,s=call() N_MASSIGN tuple-elem store -> 3-word -- Phase 2 G3 (both stages)
Reassign-destructuring a (scalar,str) tuple (a, s = call(), N_MASSIGN) stored only the str's ptr (DX->slot+0), dropping len/cap -- the last STORE-cluster gap. Reachable (valid ww; checker accepts str tuple elements) but unexercised in bootstrap (all N_MASSIGN sites returned <=8B tuples). Mirror the N_MLET destructure-store oracle (cgen.c:7475): on the one-str XOR, route the str's 3 words DX/CX/R8 -> slot+0/+8/+16; the slot pre-exists (localfind, not localadd). wwstage has no checker, so it derives str-ness from the callee return-type tuple via fnretlookupmod (structurally identical to cgmlet). Both XOR positions (str at l0 and l1). Kind-gated, never size==24. cstage==wwstage byte-identical.

Scope = one-str only, matching N_MLET exactly; str+str-both is unhandled by N_MLET too and is filed as a shared gap (task #22), with WHY-comments at both destructure sites. N_MLET emission unchanged (its edit is comment-only, verified byte-identical).

test/wcc/939: table-driven write-then-read-cap over both XOR positions (a,s=mk() and s,a=mk2()); cap!=len via mutation (not a sub-slice, #20); pre-poisoned via a non-G3 let-init; full triple+scalar asserted; fail-before/pass-after on both drivers. Completes the str-cap STORE cluster -- the read/write round-trip is now whole. main.combined.ww regenerated via the canonical make path.
2026-05-24 16:54:14 +09:00
b51a7daa25 cgen: str chained <expr>.field = v store -> 3-word -- Phase 2 G2 (both stages)
Storing a str into a field reached through a *struct-valued expression (e.g. r.sym.flag = v) wrote only 2 words (ptr,len), dropping cap -- the second STORE-cluster fold. Direct transfer of G1 (c692923): the prior arm spilled only ptr/len across the base eval; now spill the full value (PUSHQ CX/BX/AX) after the rhs eval and before the base-expr eval (the stack slot insulates it, base-formation-agnostic), stage the *struct ptr in DX (off the AX/BX/CX str convention), store ptr/len/cap at foff+{0,8,16}. Mirrors the s.f=v oracle (cgen.c:2603); G2 adds the spill the oracle skips because the oracle's base is a slot read, not a clobbering expr. Kind-gated (TY_STR/typeisstr, never size==24). cstage==wwstage byte-identical at the store site.

test/wcc/938: table-driven write-then-read-cap over depth-2 (r.sym.f=) and depth-3 (r.a.b.f=) chained bases, both asm-confirmed to hit the chained arm. rhs is a cap!=len str; all 3 slot words pre-poisoned via a non-G2 direct store; full {ptr,len,cap} triple asserted. fail-before/pass-after verified on both drivers.

main.combined.ww regenerated via the canonical make path (md5-stable).
2026-05-24 16:18:49 +09:00
c6929231dc cgen: str arr[i].field = v store -> 3-word -- Phase 2 G1 (both stages)
Storing a str into a field of an indexed element (arr[i].f = v) wrote only 2 words (ptr,len), dropping cap -- the write-side mirror of the arrfield read (c3bbe17), and the first STORE-cluster fold. The trap: the index scale (IMULQ via CX) clobbers CX=cap and the index-expr eval clobbers AX=ptr before the store. Fix composes two proven oracles -- arr[i]=v (cgen.c:3650) spills the value (PUSHQ CX/BX/AX) across the index/address computation, then s.f=v (cgen.c:2603) stages the dst address in DX (off the AX/BX/CX str convention) and stores ptr/len/cap at foff+{0,8,16}. Kind-gated (TY_STR/isstrtype, never size==24). cstage==wwstage byte-identical.

test/wcc/937: table-driven write-then-read-cap over [N]S / []S / [N]*S arr[i].f= ; rhs is a runtime cap!=len str (not a literal, which would be cap==len); all 3 slot words pre-poisoned via a DIFFERENT already-3-word store path so a stale 2-word store is detectable; asserts the full {ptr,len,cap} triple. Meaningful only now the reads are 3-word. fail-before/pass-after verified on both drivers.

main.combined.ww regenerated via the canonical make path (md5-stable).
2026-05-24 15:35:37 +09:00
c3bbe17163 cgen: str arr[i].field read -> 3-word -- Phase 2 C4.6 arrfield (both stages)
A str-typed field read of an INDEXED element (arr[i].f) loaded 2 words (ptr,len), dropping cap -- the last 2-word str VALUE-read in the cluster. At the leaf the element base is always in AX; insert cap->CX at foff+16 (final order len->BX+8, cap->CX+16, ptr->AX+0 LAST). ONE shared leaf covers value-array / slice / pointer-element sub-cases (base-formation differs upstream, unaffected). Author-to-ABI, matched to the proven cgslicehdr(D_AX) / caseB slice-arm shape. Kind-gated (TY_STR / isstrtype, never size==24). cstage==wwstage byte-identical at the leaf.

test/wcc/936: table-driven runtime .cap-survives over [N]S-local / []S-local / [N]*S-pointer-elem reads; a 2-word read cannot coincidentally pass (the index scale-multiply clobbers CX, plus an interposed call). fail-before/pass-after verified independently on both drivers.

main.combined.ww regenerated via the canonical make path (md5-stable). Completes the str 3-word VALUE-read cluster (F2 element; C4.6/caseB/S3 fields; arrfield indexed-field). Store-side cap-drop and a struct-slice-creation divergence are separately filed.
2026-05-24 14:57:05 +09:00
877a1af6d3 cgen: str tuple-positional element read -> 3-word -- Phase 2 C4.6 S3 (both stages)
Reading a str-typed tuple element by position (t.N) loaded 2 words (ptr,len), dropping cap -- the C4.6 coda with NO adjacent slice-element arm to mirror. Author the 3-word triple directly to the canonical {ptr,len,cap} ABI (AX,BX,CX off the BP frame slot; add cap->CX at +16). Kind-gated (TY_STR / isstrtype, never size==24). cstage==wwstage byte-identical -- cross-stage identity is the correctness oracle here, since there is no local slice sibling.

test/wcc/935: table-driven runtime .cap-survives over let s = t.1; poison rides the return ABI (R8) into the slot's cap word; a CX-clobbering call ensures a 2-word read cannot coincidentally pass. fail-before/pass-after verified INDEPENDENTLY on both stages.

main.combined.ww regenerated via the canonical make path (md5-stable).
2026-05-24 14:22:25 +09:00
634cbefc22 cgen: str chained-*struct field read -> 3-word -- Phase 2 C4.6 caseB (both stages)
The chained N_DOT path (o.p.f, depth>=2, base AX) still loaded a str-typed field as 2 words, dropping cap -- the C4.6 sibling deferred to caseB. Fold the str case onto the adjacent 3-word slice-field arm (widen kind-gate: cstage type_isstr, ww typeisstr; never size==24). Emits len->BX+8, cap->CX+16, ptr->AX+0 LAST (AX is the base). cstage==wwstage byte-identical; the slice arm is unchanged for slices.

test/wcc/934: table-driven runtime .cap-survives over the chained read; the row interposes a CX-clobbering call so a 2-word read cannot coincidentally pass on stale CX (per the 933 discriminator lesson). Verified fail-before/pass-after on both drivers.

main.combined.ww regenerated via the canonical make path (md5-stable).
2026-05-24 13:57:20 +09:00
90deeb3821 cgen: str N_DOT field read -> 3-word {ptr,len,cap} -- Phase 2 C4.6 (both stages)
Reading a str-typed struct field loaded only 2 words (ptr,len), dropping the cap word. Fold the str-field read onto the adjacent proven slice-field arm by widening its kind-gate to include str (type_isstr/isstrtype, never size==24). Sites: S1 direct struct field (local BP + global CX base) and S2 field through a *struct local (pst.f). cstage==wwstage byte-identical; the slice-field arms stay unchanged for slices.

C4.6 bundles the S1 local-field fold with a FORCED global-field lift -- the rule-11 reason they cannot split: cstage reads a field with ONE unified base_reg arm, so folding str covers local AND global together. For byte-id, ww's global field path must then lift in the SAME commit -- but ww splits local/global and its global arm has no slice sibling, so it is authored as ww's own local slice-field arm retargeted to the CX base (cap->CX last, base survives). The underlying cstage-unifies / ww-splits field-arm divergence is a separate filed structural follow-up, not resolved here.

test/wcc/933: table-driven runtime .cap-survives over local/global/*struct field reads, both drivers; verified fail-before/pass-after. The local-field row interposes a CX-clobbering call so a 2-word read cannot coincidentally pass on stale CX (the field store otherwise leaves the cap word lingering in CX).

main.combined.ww regenerated via the canonical make path (md5-stable), per the 1140a59 precedent.
2026-05-24 13:31:58 +09:00
97707155ae cgen: str N_INDEX read -> 3-word {ptr,len,cap} -- Phase 2 F2 (both stages)
str element value read at N_INDEX dropped the cap word (2-word ptr,len load); str is 24B {ptr,len,cap} since 1140a59. A new named helper cgslicehdr (both stages) loads the full 3-word header and is called by the N_INDEX str-element sites, kind-gated type_isstr/elemisstr -- never size==24, since str and slice collide at 24B. The base-targeting word loads last (clobber-safe). cstage==wwstage byte-identical. The #9 typeassert leaf is split out to F2b (it needs a wwstage spill twin first).

test/wcc/932: table-driven runtime .cap-survives probe over both N_INDEX base forms and both drivers; verified fail-before/pass-after. NNN<950 mirrors the 928 precedent -- the fixtures are self-contained (/tmp, no imports), so rule-14's selfhost-sibling race does not apply.

main.combined.ww regenerated via the canonical make path (md5-stable) and committed alongside source, per the 1140a59 precedent.
2026-05-24 12:27:15 +09:00
fb4c567e0d wcc: populate str.sub = u8 -- Phase 2 F1 foundation (both stages)
str IS []u8 (#1 landed the 24B layout); F1 populates the element type
so the step-3 checker collapse can read str.sub instead of special-
casing TY_STR. No reader consumes str.sub yet, so this is byte-id-
neutral: every shared ->sub reader a TY_STR value can reach is
invariant under NULL->u8 -- u8 is unsigned + size-1, matching the
prior NULL-defaults (size->1, signed->0, isstr/istagged->false); the
only ->size derefs are guarded behind esz>1, which stays false for
str.

Verified inert: compiling a fixed source with the pre- and post-F1
compilers emits byte-identical asm on both stages; cross-stage
byte-id holds and full make test (135 tests incl. 990-997) is green.

cstage cmd/wcc/type.c, wwstage lib/ww/typ.ww; combined.ww regenerated
via the canonical make path.
2026-05-24 09:34:54 +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
3866ea24f5 selfhost/cmd/wcc/check: build per-decl TY_NAMED in tinfofornode (#64, Phase-N step 2)
tinfofornode's TNAME arm collapsed aliases to their underlying tinfo; flip
it to build a per-decl TY_NAMED wrapper cached on sym.type_, so every TNAME
resolving to the same decl yields one tinfo pointer -- ptr-identity =
nominal identity. Mirrors cstage's two-phase type_named (cmd/wcc/check.c:
1900-1929, resolve_typename :60-88): create the NAMED, pre-bind sym.type_
BEFORE resolving under (self-ref cycle-break, e.g. `type node = struct
{next: *node}`), then patch under + copy size/align/slotsize off the
immediate body. CHAINS not flatten (`type a=b` gives under=NAMED(b)),
matching resolve_typename returning the inner NAMED.

aliassym factored out of resolvealias for the one-level decl lookup;
resolvealias delegates and is behaviorally identical.

Semantically INERT until typeeq consumes nominal identity (step 3, #16) --
live type equality today is the AST-keyed typeeqast, and typeeq has no live
callers. The #63 structural-walker peels + cstage-mirrored single-if peels
keep all tinfo.kind sites correct with NAMED flowing -- byte-id 990-997
unchanged (133/133, independently re-confirmed on a quiescent tree).
2026-05-23 19:33:04 +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
e0c0f75b2a selfhost/cmd/wcc/check: flatten tagged spreads + iserror on tinfo.params (#61a)
A.6.3 #61 prerequisite (additive, no consumer changes). The tagged-variant
machinery (taggedvariantindex / flatvariant* / cgwidentagremap / cgmatch)
is AST-keyed -- it walks N_TTAGGED.list and spread-flattens `...inner` at
read time. To migrate it onto tinfo.params (#61b/c) the chain must first
carry the flattened variant set + per-variant error mark, matching cstage's
Type.params / Type.iserror.

tinfofornode's TTAGGED arm now splices `...inner` tagged spreads into
ti.params (dealias one NAMED level, require TY_TAGGED, inline its already-
flattened variants in declaration order) -- mirror of cstage check.c:366-389.
Each variant gets an iserror flag via varianterr (TBANG / `!`-aliased).
size/align stay accounted off the surface member so ti.size is byte-identical
to before; the flatten + iserror have zero readers this commit (the lone
TY_TAGGED params reader, nullableptrtag, only fires on 2-variant nullable
unions with no spreads).

iserror rides the shared tparam struct rather than a sidecar: a cstage-mirror
divergence from harec, which carries no per-variant flag (models `!T` as a
STORAGE_ERROR type node, ref/harec/include/types.h:144, src/types.c:151-159).
Faithful port filed as #62. Spread-only flatten (cstage check.c:373 also
flattens non-spread anonymous-nested unions) is a known symmetry gap, inert
in bootstrap, tracked for #61b.

make test 133/133 (quiescent tree, byte-id 990-997 green).
2026-05-23 17:36:39 +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
6c9a3b369e selfhost/cmd/wcc/check: populate tinfo.fields + .tupleelems (#57, A.6.3i-phase-1)
Phase 1 of A.6.3i: populate the field chain in tinfofornode's TSTRUCT
and TTUPLE arms so Phase 2/J/K (#58/#59/#60) can retire dotfieldtnode,
dotinnerstructptr, dotchainresolve, and indexbaseesz off their AST-keyed
structinfo walk and onto a tinfo read. Direct analog 26724fe (#50 phase
1, A.6.3f-a) for the head/tail append-list pattern.

TSTRUCT walks n.list's N_TFIELD chain in lockstep with the existing
natural-layout offset accumulator: alloc tfield {name, type_, offset,
tnext}, link head/tail, set r.fields after the loop. Mirrors cstage
cmd/wcc/check.c:468-527. Harec cite: ref/harec/include/types.h:109-115
struct_field and ref/harec/src/type_store.c:314-347 struct_init_from_atype.
Anonymous-embed promotion not populated here (#13 per the cstage cite
at check.ww:1263).

TTUPLE adds a new ttupleelem struct {type_, offset, tnext} on a new
tinfo.tupleelems slot, distinct from .fields per Rob's call: harec
splits struct_field vs type_tuple at types.h:109-115 vs :122-126
because tuples are positional/anonymous and struct members are named,
and the name="" idiom #50 reused for tagged-variants-on-tparam would
conflate two semantic axes. Diverges from cstage cmd/wcc/check.c:329-345
which stores tuple positionals on t->params (Tparam, no offset, consumer
recomputes by walking at cgen.c:5723-5750); storing the offset matches
the A.6 stamp-once-read-many arc Phase 2/J/K consume. Offset is raw-sum
(no per-element padding) matching cstage cgen.c:5723-5750, distinct
from harec's add_padding at type_store.c:561.

Purely additive: r.fields and r.tupleelems have zero readers today.
Phase 2/J/K consume. make test 133/133 (worker port); test-unit 124/124
post comment-only review trim.
2026-05-23 15:13:07 +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