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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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).
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.
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).
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.
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.
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
#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).
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
CLAUDE.md rule 9 amended with the explicit carve-out: ww is C/Plan-9-
lineage — no GC, no "safe" baseline to be unsafe relative to — so the
Hare `_unsafe` suffix flags an axis ww doesn't have. The convention
is dropped wholesale in lib/.
Concrete changes:
- lib/strings: `fromutf8_unsafe` → `frombytes` (pure reinterpret). The
validating sibling `fromutf8` is deleted entirely (28 lines, plus its
84-line fromutf8_cases test). Callers that need validation write the
two lines inline at the IO source: `utf8.validate(b)?;
let s = strings.frombytes(b);`. `fromutf8` name reserved for a future
true validating helper.
- lib/strings α-batch: concat/join/lpad/rpad migrate from
`rt.malloc(N): *u8` to `alloc([], N)!` + `buf.len = N;` +
`return frombytes(buf);`. Same dup-pilot pattern (4c07ef0). Task #41.
- lib/memio header comment trimmed: drops a stale reference to
"lib has no fromutf8 today"; cites the rule-9 carve-out instead.
- Caller renames across selfhost combined.ww files (auto-regen) +
cgenutil.ww comment ref.
Rule-11 disclosure on the bundle: the rename and the α-batch are
nominally separable concerns (symbol-naming policy vs amalloc→
alloc-slice migration), but they touch the same 4 functions in
lib/strings/strings.ww — the α-batch's first emission of `frombytes`
postdates the rename. The α-batch was applied on top of the rename
sweep mid-flight by the pre-commit reviewer; splitting them back
out is fiddly text surgery for marginal bisect value. The rename is
the primary concern; α-batch is one entry in #8's sized-slice
migration.
Verified: make test 132/132, 995_self_rebuild byte-identity holds.
Closes#42; advances #41.
Hare puts runtime allocation in rt::, not os:: (ref/hare/rt/malloc.ha:27,
README). ww's `@symbol("rt_alloc") fn alloc(n: u64) *void;` lived at
lib/os/os.ww as a historical bootstrap shortcut; this commit relocates
it to a new lib/rt/malloc.ww and sweeps every site that depended on
`import os` for the alloc decl over to `import rt`.
This is commit 1 of 3 in the lib/rt extraction (#35):
1. (this) move decl, sweep imports — preserves shape
2. rename rt_alloc → rt_malloc (#38)
3. nullable return type + OOM-propagating builtin lowering (#39)
No rename here. Symbol stays rt_alloc, function stays `alloc`, return
stays *void. Behavior identical — same ffi resolution outcome, just
sourced from a different module file. The rt::ensure runtime helper at
selfhost/rt/ensure.ww is its own compilation unit with a local decl and
is untouched.
Side effect: every wcc cgen file used `rt` as a local *node variable
name for "return type." `import rt;` shadows the module, so each
selfhost/cmd/wcc/{check,cgenstmt,cgenexpr,cgenutil}.ww site renamed
to `rtyp`. Mechanical follow-through; only the wcc module-import was
forced to do this rename.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip byte-identical).