Commit Graph

274 Commits

Author SHA1 Message Date
214ced303f wwstage: displacement store for global-ptr scalar field, align to cstage
cgassign had dedicated N_DOT-store arms for a local-ptr base, a global
value-struct, and chained bases, but none for a global-pointer scalar
field. That case fell through to the generic cgplaceaddr/dotchainaddr
route, which folds the field offset (ADDQ $foff,BX) then stores to (BX).
cstage emits a single displacement store (MOVQ AX,foff(BX)) via its
via_ptr global scalar arm, so the two stages diverged on asm shape
(rule 10). Both forms are runtime-correct here -- BX is a fresh throwaway
in the generic route -- so this was a byte-id divergence, not a
miscompile.

Add the missing displacement-store arm, predicate-mirroring cstage's
via_ptr global scalar arm exactly: plain assignment only, scalar field
only; non-scalar field types stay on the generic path (their global-ptr
deref is a separate deferred item). glob_ptr_field_test.ww gains an
off-8 row as the regression pin -- offset-0 cannot catch it because
ADDQ $0 is suppressed.

Surfaced by the fold-2 Fam-5 migration.
2026-06-22 14:56:11 +09:00
c814856550 wwstage: C-FFI variadic call codegen parity with cstage (#10)
Mirror cstage's C-variadic call handling in the ww self-host: parse a
bare `...` param (decl.ww), skip param-keyed desugar for it to avoid a
nil-deref (check.ww), and emit AL = XMM-reg count plus CVTSS2SD
promotion of f32 args in the variadic tail (cgenutil.ww, cgenexpr.ww).
Closes the cat-A wwstage silent miscompile (AL=0, unpromoted f32 tail).

Parse/check/cgen are one atomic align-up (parse alone miscompiles, so
not bisect-splittable). 989_ffivariadic now runs dual-stage (cstage ww
+ wwstage ww_ww), 12/12; w6c==w6c_ww byte-identical. Byte-id alone is
blind here (the bootstrap calls no float-bearing C variadic), so the
ww_ww runtime rows are the real net.
2026-06-21 11:50:18 +09:00
75a03a7d69 wcc: qualify all references to the syntax package (#75)
After the frontend consolidated into one syntax package (#74), wcc still referenced syntax symbols unqualified — residue of the old flat combined namespace, where bare refs resolved by accident. Under separate compilation Hare and Go both require the package qualifier, so those bare refs would not sep-resolve.

Qualify every wcc reference to a syntax type, function, or enum member as syntax.X across the seven syntax-importing files. Resolution-only: the resolved symbol and emitted code are unchanged, so the two combined.ww regenerate textually but all five _ww binaries hold byte-for-byte. The struct-literal sites resolve via #76. This makes w6c fully separate-compilable.
2026-06-16 22:45:17 +09:00
7a8acfb952 lib/ww,wcc: consolidate frontend into one syntax package (Go-compiler model, #74)
The ww compiler frontend was split across packages lex (lex+tok), ww
(ast+sym+typ), and parse — mirroring Hare's ref/hare/hare/{ast,lex,parse}.
That split's only payoff is third-party reuse, which ww has zero of: the
frontend is consumed by exactly one client, the wcc backend. The split's
cost is a wide cross-package export surface — every fn over a sibling
package's type must export it, and under separate compilation that
re-triggers check_exported_type, plus a phantom `import tok;` (tok lives
in package lex). Consolidate into ONE package lib/ww/syntax/, modelled on
Go's cmd/compile/internal/syntax. The 9 files move in (package syntax);
the intra-frontend mutual references become same-package; wcc and the
tool mains import syntax. No cstage C change (the C frontend mangles from
the source package clause). Internal data shapes (AST kinds, token model,
lexer/parser state) still mirror ref/hare/hare per rule 6/12 — only the
module decomposition collapses; the stdlib is untouched.

USER-approved (#74); spec .ai/rob-frontend-reorg.md (drew2 fidelity-
confirmed). Rule-6 carve-out documented in CLAUDE.md. Dissolves the tok
phantom import; collapses the intra-frontend export sprawl. Byte-id
rebaseline (lex.X/parse.X/ww.X -> syntax.X); cs==ww held. The residual
syntax->wcc export surface (10 types) + the unqualified-ref question are
separate follow-ups (#72/#75).
2026-06-16 19:56:34 +09:00
0c4a5ecea0 wcc/ww: path-qualify exported decls + drop exact-or-bare value mangle (#53)
Under M1 mangling, EXPORTED non-fn decls (let/def/type) skipped path-
qualification and emitted a BARE symbol (`types.I64_MAX` -> `I64_MAX`).
Under separate compilation two packages exporting the same data leaf
would then collide at w6l. Masked in-tree only because no two packages
export the same non-fn leaf.

§7-A (USER-locked, harec's model): path-qualify EVERY exported decl
(fn AND data) at the single mangle choke-point — mod_collect /
collectmods. Retire the `!isfn && d->export` (cstage) and `exported==0`
(wwstage) skips: every decl with a module now mangles `<mod>.<name>`.
The ONLY bare symbols left are @symbol FFI overrides (ffi_resolve at
emit) and the ROOT unit's `main` — both already carved out before the
map insert.

With exported decls in the map the exact-(name,hint)-or-bare value
dance is dead — its sole purpose was the bare-exported case. Delete
mod_lookup_value / mod_mangle_value / mahint (cstage) and
modlookupvalue / emitsymnamehint (wwstage); the value-global sites now
route through the same hint-aware-with-fallback lookup as fns
(mod_mangle_fn/mafn, emitfnname). Net negative LOC in the mangler.

Transparent rename on the live combined path: ref and def move in
lockstep, so cs==ww byte-id holds and the self-host still builds + runs
(fixed-point/995). Byte-id REBASELINE — all 5 ww binaries shift. The
w6c/wwdump combined.ww embed wcc cgen and are regenerated.
2026-06-16 02:43:01 +09:00
f308818b4b wcc/ww: mangle imported symbols on dotted import path (#22 M1, #32)
Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical.

Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id).
2026-06-15 17:37:18 +09:00
c86c6a3bbf wcc/ww: match on a global value-struct tagged field reads g(SB) (#29)
A match whose scrutinee is a tagged field of a GLOBAL value-struct read
the tag/payload from the BP region (saved-BP + return-addr) instead of
g(SB) and returned garbage. Both stages were identical-wrong, so the
byte-id gate could not see it -- a gate-blind regression introduced by
M1 (#25): M1's in-place N_DOT match arm uses localfind(base), which
returns the 0 not-found sentinel for a global base, so 0+field.offset
landed in the frame.

Gate the in-place arm on a confirmed-local base -- `localfind(base)==0
&& let_islet/isletvar(base)`, verbatim from cstage's own global test at
cgen.c:2000 (both stages, same spelling). A global base now falls
through to the existing spill path, which cgexprs the scrutinee and
resolves g(SB). M1's local-field in-place ($32) path is untouched.

Regenerates the w6c and wwdump combined.ww. Table-driven 841 test
(global int/reassign/str-payload + a local-field M1 regression row),
runtime-discriminating: pre-fix returns garbage, post-fix 42 on both
stages; rob's direct-global-field spill caveat confirmed at runtime.
2026-06-14 16:52:00 +09:00
533333bd1a wcc/ww: match on a tagged struct-field scrutinee reads it in place (#25)
wwstage cgmatch unconditionally spilled any non-ident match scrutinee
-- including an addressable BP-relative N_DOT struct field -- into
@match_spill and dispatched off the copy (frame $48); cstage reads
such a field in place at its BP offset ($32). Both stages were already
runtime-correct (latent rule-10 leanness, not a miscompile); this
aligns wwstage down to cstage so the asm is byte-identical.

The new in-place arm mirrors cstage cgen.c:10241-10296 verbatim: an
N_DOT scrutinee with a bare N_IDENT base whose type chases to a value
TY_STRUCT and whose field is found by name reads tag/payload at
localfind(base)+field.offset. The *ptr-field and call-result cases
stay on the spill path by construction (their base does not chase to
TY_STRUCT) -- no extra guard. A global value-struct base mis-resolves
identically in both stages (localfind returns 0); left untouched as a
shared latent (#29), since a ww-only guard would break byte-id.

Regenerates the w6c and wwdump combined.ww. Table-driven 831 test:
6 rows (local-field, *ptr-field, plain-ident, call-result, payload
remap, str payload) x runtime-both-stages + cs-vs-ww byte-id.
2026-06-14 11:59:11 +09:00
33f940e17c wcc/ww: compound OP= on a tagged index/ident is a loud reject (#20/#21)
Compound `OP=` through an index (gs[i]/a[i]) or a bare ident (g) on a
tagged union silently misbehaved: cstage dropped the index compound and
plain-stored, and BOTH stages compiled an ident compound into an add on
the tag word -- byte-identical, so the gate stayed green while the tag
was corrupted. A compound op on a whole union is nonsense.

Gate the index plain-store arm on TK_ASSIGN so a compound falls to the
existing #133 reject (wwstage's byte-id twin); add a dedicated #21 ident
reject in both stages. This closes the compound half of the tagged-payload
write class (deref #18, dot #34 already reject).

#19 (global tagged-array static-init DATA) is a separate emitter, still open.
2026-06-14 01:14:51 +09:00
769be55905 wcc/ww: reject compound *p OP= v through a *tagged, like cstage (#18)
The wwstage compound-deref arm narrowed the store for scalar pointees and
otherwise emitted a single MOVQ, so `*p OP= v` with p:*tagged clobbered
one word (the tag) and returned -- silently miscompiling what cstage
already rejects. A compound op on a whole union is nonsense. Gate the arm
on a scalar pointee size and let a tagged pointee fall through to the
existing assign-resolver reject, the byte-id twin of the cstage fatal.
cstage is unchanged.

This closes the deref member of the compound-on-tagged class; the index
and ident members (gs[i] OP= v, g OP= v) reject in a follow-up (#20/#21).
2026-06-14 00:40:34 +09:00
1074239859 wcc/ww: store through a *tagged pointer widens, both stages (#17)
The N_UN/TK_STAR plain-deref assign arm fell to a single fldstoreop for
every pointee, so `*p = v` with p:*tagged wrote the rhs into the tag word
and never the payload -- identically in both stages, leaving the byte-id
gate green while the store corrupted the tag (#263-class, gate-blind).

Gate on TY_TAGGED and route through cg_widen_tagged_store into a scratch
slot, then word-copy to the destination -- the proven runtime-index arm.
Scalar pointees keep the single-store path unchanged.
2026-06-13 23:54:12 +09:00
dd24de1134 wcc: whole-struct field-copy completes the ragged tail greedily, both stages
A `x.f = o` copy of a whole struct field emits a MOVQ run for the
8-byte chunks plus a tail. Both stages inlined a tail that handled only
{4,1}: a 4-byte remainder went MOVL, a 1-byte MOVB, but {2,3,5,6,7} fell
through to an 8-byte MOVQ that OVER-READS the source and OVER-WRITES the
field's natural-offset successor. With #44 packing a successor at its
natural offset, that is a live clobber: outer2{i:inner2{u8,u8}, mark:i32}
copies i with `MOVQ -8(BP),AX; MOVQ AX,-16(BP)` and wipes mark@-12; the
correct move is a single MOVW. Same defect in cstage (cgen.c) and the
four wwstage field-copy sites (cgenexpr.ww: via-ptr, direct-BP-local,
global, and the multi-hop dot-chain CX variant).

Fix: replace each inline {4,1} tail with the descending greedy 4/2/1
(MOVL/MOVW/MOVB) the canonical aggregate-copy emitters already use, so
the tail is complete on every natural size. This is path (alpha) of the
#73 brief — a corpus-neutral, no-workaround completion of the inline
tail. Routing field copies through the shared aggcopy/cg_aggcopy choke-
point (beta) is the balloon: those emitters hardcode (SI)->(BX) at offset
k with zero base displacement, but the four field-copy dsts are
heterogeneous (foff(BX), boff+foff(BP) with no base reg, totaloff(CX)),
so routing forces per-site-per-stage LEAQ src->SI + LEAQ dst->BX rewrites
with no mechanical cross-stage mirror at the CX site = a gate-blind
cs!=ww risk. The emitter extraction is filed as a later addressing-
unification arc (#12). The ragged tail is corpus-absent (every corpus
field copy is tail in {0,4}, where greedy 4/2/1 emits exactly what the
old {4,1} tail did), so this is CLASS-N: zero corpus move on both stages,
byte-id holds by construction.

The cstage <=24 N_CALL receive site (cgen.c:5234) is a different copy
family (sret result read from AX/DX/CX, not a mem-to-mem field copy) and
already handles 4/2/1; left untouched. The str/slice/tagged/tuple 4/1
sites (#76) are likewise a separate family, filed not folded.

989_structcopytail_run pins it on both driver twins: tail2 (MOVW), tail6
(MOVL+MOVW), tail7 (the full MOVL+MOVW+MOVB ladder, the MOVB-path row),
plus an 8-aligned ctl8 (tail-0 control). Pre-fix cstage clobbers mark and
exits non-zero -> cs!=ww; post-fix 4/4 ok cs==ww.
2026-06-13 15:06:15 +09:00
a00d052833 wcc/ww: whole-struct field copy uses the source's natural length
The four wwstage whole-struct field-copy sites (cgenexpr.ww) copied
`ssi.totsize` — the slot-padded, round-8 structinfo size — instead of
the SOURCE struct's natural size. cstage copies `f->type->size` (the
field struct's aligned r.size; cmd/w6c/cgen.c:5302). wwstage over-copied
into the field's slot padding.

Fix: length = copysrcnatsize(c, n.rhs) = tichase(src.type_).size, read
from the SOURCE node's stamped tinfo (the checker's natural r.size,
check.ww N_TSTRUCT). This never reads structinfo / fi.foff / fi.fsz, so
it is correct at HEAD unconditionally and independent of the
registerstruct natural-offset change (#44/#55) that follows — a pure
wwstage convergence onto the length cstage already emits. Distinct from
the existing structnaturalsize (structinfo max(foff+fsz), a #44-coupled
source).

LENGTH ONLY. The ragged-tail completeness (both stages' field copies
inline a tail handling only {4,1}; a natural size %8 in {2,3,5,6,7}
falls through to an 8-byte MOVQ over-read) is a SEPARATE both-stage
class — cstage cgen.c:5302 has the identical incomplete tail — folded
into #73 (route both stages' field copies through the canonical greedy
aggcopy emitter). Touching only ww's tail here would create a gate-blind
cs!=ww on narrow-tail inputs, so it is deliberately left for the
both-stage fix.

No isolated runtime repro: the over-copy writes [natural, totsize),
which under HEAD's slot-padded field layout is the field's OWN padding
(the successor parks at the next slot). It only becomes a clobber once
#44 packs the successor at its natural offset (the 681 ragged_tail_12B
regression that forced this ordering). So this commit is byte-id-clean
and a no-op on the present corpus; its proof is the all-green run plus
the #44 commit that depends on it.
2026-06-13 13:15:31 +09:00
b97be7301f wcc/ww: def-dim array .len field-reads resolve the dimension from the type table
buf.len on a [MAX]u8 returned 0 — the cgdot len arms (local/global/
def) and letemitsize only read an N_INTLIT dimension. Route a
non-literal dimension through tichase().alen (the #21 fix mirrored
into the field-read consumers; .ptr arms are dim-independent).
Review-era task #56.
2026-06-13 01:52:42 +09:00
faf1a2908b wcc/ww: alloc of an alias struct literal chases the alias for size and fill
alloc(alias{...}) keyed the size and field-fill off the syntactic
alias name — it under-allocated and emitted zero field stores. Chase
the alias via structlookupchain to the resolved struct (depth-2
chains verified). The scalar else-branch keeps its pre-existing
benign cs!=ww divergence, surfaced here and deferred as task #57
(site note at the arm). Review item #26.
2026-06-13 01:49:07 +09:00
36e17b58f2 wcc/ww: try-unwrap str success reads the stamped operand type
The ?/! success-is-str decision was name-keyed off the FIRST variant
and only handled call operands — an ident operand with junk registers
unwrapped garbage, and error-first unions picked the wrong variant.
Key on the stamped success variant (successvariant + typeisstr,
mirror cgen.c:10459-10466/10595-10602). Review item #16.
2026-06-13 01:44:27 +09:00
75c1b278e6 wcc/ww: def-dim array slice takes len and cap from the type table
Slicing an array whose dimension is a def constant gave len 0 — the
default-hi and cgbasecap arms only read an N_INTLIT dimension. Route
the dimension through the type table (one root, four arms: default-hi
and cgbasecap, local and global each), byte-identical for the def-dim
SLICE shape. The def-dim array .len/.ptr FIELD-read keeps the
N_INTLIT-only limitation — filed as task #56 (cgdot sibling).
Review item #21.
2026-06-13 00:21:21 +09:00
a4a4cd7c16 wcc/ww: cgcall drain pops widened GP words before the float arm
The arg-drain loop checked node_isfloat before popping a widened
arg's GP words, so a float arg adjacent to a widened (tagged) arg
read the wrong stack slot: the f64 took the widened payload (#30), or
the float arm ate the tag word into X0 and the payload landed in DI
as the tag (#48). One missing branch, two manifestations — mirror
cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48
(fold reviewer-verified one-mechanism against the cstage twin).
2026-06-12 21:11:35 +09:00
f00775759d wcc: chained-dot str leaf loads the cap word, both stages
A str field reached through a chained dot (o.i.s) emitted two loads
(ptr, len) and stored a stale CX as the cap — both stages, at any
non-zero chain depth. Emit the full header at the chained-dot leaf.
Review item #29.

Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
2026-06-12 21:07:55 +09:00
0a6f500b8c wcc: tuple slice-element read loads the full 24B header, both stages
Reading a slice-typed tuple element (t.0) loaded only the pointer
word; len and cap took whatever was left in BX/CX — silent garbage in
BOTH stages once anything clobbered the registers between build and
read. Load all three header words at the tuple-element arm. Review
item #28.

Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
2026-06-12 21:04:35 +09:00
850746cfa8 wcc/ww: str-to-slice cap synth keys on the source type
Casting a global str to []u8 dropped the cap=len synth (the trailing
MOVQ BX,CX) — the synth was gated on a local-ident source shape.
Key it on the source type so local/global/field/call sources all get
the header. Review item #19.
2026-06-12 21:00:59 +09:00
c405e777d3 wcc: single-dot field compound assignment routes through one combine helper
s.f *= v silently became s.f = v (and the other non-+=/-= ops dropped
likewise) in BOTH stages across five lvalue sub-arms: via-ptr field,
direct local field, str/slice pseudo-field, and the two global-field
forms. Funnel all five through a shared combine dispatch
(cgdotfieldcombine / cg_dotfield_combine) emitting the load-OP-store
sequence at field width, hard-erroring the unhandled kinds — close-by-
construction so no arm stays on the old PLUSEQ-only path (the #133
BUS-routing lesson; #227 sites A/B are the closed siblings). The
refactor routes the corpus's existing +=/-= sites through the same
helper output-identically (byte-id held). Review item #34.

Both stages move in one commit: one emission contract; splitting the
halves would leave the byte-id gates red in between.
2026-06-12 19:29:58 +09:00
1be0e6b5db wcc: indexed-field compound assignment wires all ten ops, both stages
arr[i].field /= %= <<= >>= silently dropped the op (load-combine-store
emitted plain assignment) in BOTH stages — gate-blind, the #133 class.
Route every compound op through the combine dispatch at the indexed-
field arm and hard-error the unhandled operand kinds (float/str/slice/
tagged), per the #133 template (3986818). The runtime-correct target is
the op's own algebra (a OP= b == a = a OP b). Review item #33.

Both stages move in one commit: the fix is a single emission contract —
splitting cstage cgen.c from selfhost cgenexpr.ww would leave the
byte-id gates red between the halves.
2026-06-12 19:26:24 +09:00
02eb867036 wcc/ww: cgtypeassert gains the nullable arm
'as' on a nullable value compared the pointer itself to a tag (the
missing arm). Mirror cgtypetest's nullable fold and the cstage twin
(cgen.c:10694). Review item #17.
2026-06-12 19:22:51 +09:00
6f3c896fea wcc/ww: cgtryprop/cgtryunw gain the nullable arm
Try-propagation on a nullable value fell to the tagged path and
compared the pointer to a tag — the null check came out inverted.
Mirror ww's own cgtypetest nullable fold (cgenexpr.ww:652) and the
cstage twin (cgen.c:10366/10504). Review item #15.
2026-06-12 19:19:26 +09:00
9f4626c546 wcc/ww: global float-struct by-value arg keys the SSE drain off the type
A module-global float-bearing struct passed by value drained all-GP —
the SSE-cursor classify keyed on the node shape and missed the global
ident; key off the stamped struct type (per-eightbyte classify,
F7-flavored stamp fix). Review item #31; dual-stage rows red-proven.
2026-06-12 09:24:39 +09:00
b3a355744f wcc/ww: tagged GLOBAL reassign stores tag and payload
Reassigning a module-global tagged union stored the payload into the
tag word; emit the full tag+payload store to g(SB). Both-wrong pair:
cstage silently DROPS the store entirely (filed task #41) — rows pin
ww-runtime-correct and the documented cstage residual. Review item #32.
2026-06-12 09:10:16 +09:00
94bfca761c wcc/ww: dotbaseaddr #128b probe gates on an untyped module-qualifier inner
The probe accepted any inner ident, hijacking same-leaf locals as a
module qualifier; gate on the untyped(module) inner only (mirror the
cstage twin). Review item #20; dual-stage rows red-proven.
2026-06-12 09:06:51 +09:00
4961a91d14 wcc/ww: is/as on a module-global tagged ident loads from g(SB)
The global tagged ident operand read saved BP instead of the global:
'is' compared garbage as the tag; 'as' never had a payload. Route the
load through the g(SB) base — tag at +0, payload at +8, cap at +16 for
str (one mechanism, both consumers). The 'is' half aligns ww UP
(cs==ww pinned); the 'as' half is a both-wrong pair — cstage spills an
uninitialized payload register (its N_TYPEASSERT assumes cgexpr filled
AX/DX/CX; filed as task #46), so its rows assert ww-runtime-correct
with the cs divergence documented until #46 lands. Review item #18.
2026-06-12 09:03:40 +09:00
fee84528da wcc/ww: global-struct slice-field store widens the str arm to slices
A slice-typed field of a module-global struct stored only the str-form
words; widen the arm to the full slice header via the g(SB) base
(mirror cgen.c sibling arm). Review item #35; dual-stage rows red-proven.
2026-06-12 08:57:04 +09:00
47b971330b wcc/ww: tagged-element classify keys on the stamp, not a base whitelist
cgindex's tagged-element classification whitelisted base node kinds;
call- and slice-based tagged elements fell off the list and dropped
the payload words (review finding #23). Classify by the stamped
element type. 989_taggedidx_run pins cs==ww (red 2/6 pre-fix).
2026-06-12 00:17:55 +09:00
f25f5021d7 wcc/ww: chained-index str/slice element loads the full header
cgindex read one word for a str/slice ELEMENT of a chained index
(xs[i][j], f().s[i]) — the element-kind gate keyed on node shape and
missed non-simple bases, dropping the 24B/16B header load (review
finding #22). Key on the element-type stamp; 989_chainidx_run pins
cs==ww (red 3/8 pre-fix).
2026-06-12 00:07:45 +09:00
00b9933ee0 wcc/cgen: enum as int is a value conversion, not a tagged assertion (ww stage)
The value-passthrough arm gated on node shape (isenumexpr); a
constant-folded enum member reaches cgen as an int-literal node,
missed the arm, and fell into the tagged-union assertion path
(CMPQ $0 + JNE -> exit 1) — silent wrong code for every `enum as
int`, fnmatch's flag tests included. Gate on the stamped operand
type (tichase == TY_ENUM) at the cgtypeassert choke point, mirroring
cstage cgen.c N_TYPEASSERT; dead node-shape helpers deleted. Tagged
`as` assertion path byte-id-unmoved (control rows). Six @test pins
in attest_pass.ww, dual-stage. (#27b-team, cat-A)
2026-06-11 02:37:32 +09:00
606c16a28f wcc/cgen: #19+#22 uniform index element-size for non-ident bases (wwstage)
Indexing a non-ident pointer-yielding base -- a direct cast
((&a):*[4]u32)[i], a call result mk(&a)[i], a slice, a type-assertion --
used wwstage's default 8-byte element stride/load instead of the real
element type's, reading garbage (cast-base i32 index: cs=30, ww=0). The
cgindex esz derivation gated on a whitelist of base node-kinds (DOT /
UN-deref / INDEX); an N_CAST/N_CALL/N_SLICE/N_TYPEASSERT base matched none.

Rather than extend the whitelist (whack-a-mole), this mirrors cstage's
uniform idx_eff read: N_INDEX keeps its own arm (chained-index byte-id
preserved), and every other non-ident base now derives esz/stride/load-
width/signedness from the stamped n.type_ -- closing the class by
construction (base set ident/dot/un/index/cast/call/slice/typeassert).
cstage was already correct (uniform); w6c md5 unchanged. byte-id 990-997
8/8, no lib pin flips. test/wcc/830 (9 base shapes, byte-id per width,
signed + unsigned). Folds the N_CALL sibling #22.
2026-06-09 01:18:32 +09:00
5dd239d01e wcc/cgen: #146 wwstage str ==/!= via rt_streq, not ptr-only CMPQ (the #154 ww-twin)
wwstage compiled str ==/!= as a single CMPQ on the eager-eval'd ptr word
(len ignored), so two distinct-pointer equal-content strings compared
unequal. cstage was already correct (CALLs rt_streq, the #154 cbinop fix).
The wwstage cgbin had no str-awareness -- every comparison fell to the
generic CMPQ tail; the #154 fix was never mirrored.

wwstage-only: a cgstreqpush helper + a str ==/!= branch at the top of
cgbin (before the generic eval collapses the header), byte-matching cstage
cbinop:4564-4623 -- push rhs/lhs (len,ptr), POPQ DI/SI/DX/CX, CALL
rt_streq, XORQ $1 for !=. Gated on typeisstr (= cstage node_isstr, which
also catches module-global str idents). cstage cgen unchanged (w6c md5
unchanged). The str== .s is byte-identical cs==ww for local, global,
aliased, chained, and condition operands.

Graduates 3 lib byte-id pins (test/wcc/989_lib_byteid #59.1 asciitest,
#59.11 toktest, #59.12 asttest) M_DIVERGE->M_ID -- they used == on str and
were pinned divergent because of this bug; now byte-identical. byte-id
990-997 8/8. test/wcc/827 table-driven.
2026-06-08 23:49:16 +09:00
fca979470f wcc/cgen: #52 error-first tagged-union success tag — successtag helper not hardcoded 0 (wwstage)
An error-first tagged union -- error variant at tag 0, success at tag 1+,
e.g. (myerr | u16) -- was silently miscompiled by wwstage: the try/propagate
codegen hardcoded success = tag 0, so the actual success value (tag 1)
failed the CMPQ $0 and fell to the error path -> exit(1) instead of the
value (44). cstage was correct (computes the success tag via
cg_tagged_success_tag = first non-error variant).

wwstage-only: a successtag/successvariant helper (mirroring cstage) replaces
the hardcoded tag-0 / first-param assumption at all four try sites --
cgtryprop (?), cgtryunw (!), and the two latent shift sites cgtrytupleshift
+ cgtrytaggedshift (which bite an error-first union with an aggregate
success payload). Success-first unions (the Hare idiom + what the selfhost
uses) keep successtag=0 -> CMPQ $0 unchanged -> byte-id-neutral on 990-997.
cstage untouched (w6c md5 unchanged).

byte-id 990-997 8/8. test/wcc/825 table-driven (errfirst must/prop +
tuple-success + success-first control). A separate nested-tagged-union
construction divergence is filed (#10/#125).
2026-06-08 22:15:19 +09:00
ef6fcbfc04 wcc/cgen: #135 inferred-float module-global — default untyped_float to f64 (wwstage)
let pi = 3.5; pi * 2.0 (an inferred-type float module-global) was silently
miscompiled by wwstage: untyped_float wasn't defaulted, so letemitsize
sized it 0 -> no DATAW emitted -> the pi load was dropped, X0 kept a stale
spill -> 2.0*2.0 = 4 not 7. cstage became correct via #150-B's sym-repoint
(stamps f64 -> MOVSD), so this aligns wwstage UP, byte-identical.

wwstage-only: cgen.ww defaultinferredlets gains the untyped_float->f64 arm
(mirrors the untyped_int->int arm; the codebase's own #135-deferred
carve-out at cgen.ww:1079-1082, unblocked now that #150-B killed the
rule-10 divergence it feared), and cgenexpr.ww cgident gets a letfloatprim
fallback (the same primitive-TNAME SSoT letemitsize already uses, since a
renamed primitive TNAME carries no tinfo stamp). cstage cgen unchanged
(w6c md5 unchanged). int-inferred globals stay integer.

byte-id 990-997 8/8. test/wcc/824 table-driven. The N_CAST-no-recurse
parity (check.c:1276) is filed separately (#19).
2026-06-08 21:46:14 +09:00
1aaa0a3670 wcc/cgen: #8 def str-array element load — emit + pre-intern def-twin + ww load (both stages)
def C:[N]str; C[i] was loud (undefined main.C) both stages. Three folded
fixes, one commit (splitting would ship a bisect point where wwstage
silently returns an element address instead of .len):

P0: the str-array static-init emitter dropped its vestigial directive
=="DATAW" gate so a def table rides the same DATAW-header + DATAR-reloc
path as let. A def str/slice table lives in DATAW by w6a's A_DATAR-holder
constraint -- placement only; def immutability stays checker-enforced.

P1: let_pre_intern / letpreintern walked N_LET only, so a def str-array's
element string-literals were never interned (dangling _S_n). Extracted a
pre_intern_strarray SSoT helper, called for a def str-array arm too, both
stages. Scoped to str fixed arrays; def []T / def [N][]T stay loud (#270).

P2: wwstage cgenexpr lacked a defvartnode fallback in the indexed-element
classify, so a def str-array element load returned the element address
instead of the slice header -- a silent miscompile. One line, aligning
wwstage up to cstage (which was correct). C[1].len now = 3 both stages,
byte-identical.

byte-id 990-997 8/8; w6c/w6c_ww move. test/wcc/819 table-driven. The
def-global scalar str index sibling (def S:str; S[0]) stays task #14.
2026-06-08 18:52:55 +09:00
267e81b89e wcc/cgen: GAP-A.ptr global-array base — LEAQ name(SB) not (BP) (#11, both stages)
A global fixed array's .ptr (= &A[0]) must take the SB base, but cstage
emitted frame-relative LEAQ off(BP) for BOTH let- and def-global arrays
-> *A.ptr read frame garbage (0 instead of the element). cstage-SILENT;
wwstage def-global was a loud link-error. The .ptr read arm now gates
off==0 && (let_islet || def_isarraydef) -> LEAQ name(SB), reusing the
def-array index base predicate (cgen.c:4367, the #94/#231/#48 class).
Locals (off != 0) stay BP-relative -- the 14 toolchain backing-ptr sites
unaffected.

wwstage let-global was already correct; this adds the missing def-global
arm (cgenexpr.ww), converging cstage/wwstage byte-identical across all
three flavors (local / let-global / def-global) and closing a latent
cstage-only let-global cs!=ww divergence.

Byte-id 990-997 8/8 (corpus has no global .ptr); w6c/w6c_ww binaries move
(cgen changed). test/wcc/818 table-driven, build+run+byte-id per flavor.
2026-06-08 17:39:45 +09:00
7b0e09e065 wcc/cgen: GAP-A.len def-global array .len — def-twin cgdot arm (#7 lineage, wwstage align-up)
wwstage .len on a def-global array fell to the cgdot SB-fallback (w6l: undefined reference to 'len') — the #7 let-array arm gates on letvartnode (c.lets only), so def-globals (c.defs) missed it. Add a def .len-only arm in cgdot using the existing defvartnode (the def-side mirror of letvartnode), emitting the length immediate from the #11-stamped N_TARRAY length child. cstage cgen.c was already correct, so this is a wwstage-only source change: w6c unchanged, w6c_ww + wwdump regen'd (they embed the wcc cgen).

.ptr (cstage itself buggy — emits LEAQ (BP), filed GAP-A.ptr) and .cap (wwstage silent garbage; arrays have no cap, filed GAP-A.cap) are NOT folded (rule-11, separate concerns). Pin: table-driven test/wcc/816_def_arr_len (def [3] + [_] inferred + 1-elem + u8 stride .len, both stages + byte-id), teeth-proven.
2026-06-08 16:19:29 +09:00
6e1d958d9b wcc/cgen: #145 slice-copy-assign LHS s.arr[lo:hi]=bs — N_SLICE-LHS arm, runtime byte-copy loop, esz via type table (both stages)
Probe-first find for the path c2 appendlit (buf.buf[lo..hi]=bs): a
slice-copy-assign into a struct-field array sub-range emitted ZERO code —
silent NO-OP, both stages, both-wrong-identical (#263), so runtime is the
only net. N_ASSIGN gains an N_SLICE-LHS arm (cgen.c + cgenexpr.ww
slicebaseesz twin) reusing the N_SLICE-read base/esz cascade and copying
(hi-lo)*esz bytes from rhs.ptr via a runtime loop (len is runtime; no
REP/MOVSB). esz routed through the type table (rule 13; [N]u8->1). Hare
len(bs)==hi-lo assert deferred to #149.
2026-06-08 09:58:22 +09:00
2c09d13ca3 wcc/cgen: #59 append/insert struct-literal value eval-order — eval-to-scratch pre-grow + precise copy (both-stage)
append/insert of a struct-LITERAL value evaluated the literal's field
exprs AFTER the grow, so a field reading the destination (e.g. len(xs))
saw the grown length. Both stages, #263 gate-blind (cs==ww byte-identical,
both wrong — runtime is the only net). #50 fixed the scalar/boxing value
arm; the struct-lit arm still post-grew.

Fix (mirror #50, both stages): resolve the struct, fill the literal into a
fresh per-site scratch (@appendstructscr, sized esz, survives rt_ensure +
nested-append clobber) BEFORE the grow, then copy scratch -> post-grow slot.

The copy uses the precise descending 8/4/2/1 ladder (the proven N_IDENT
struct arm directly below), NOT a raw 8B-word block copy: a struct's size
rounds to maxalign (check.c:916), so a sub-8B struct packs at a 4/2/1B
slice stride and an 8B copy over-writes past the slot — at a power-of-2
capacity boundary that clobbers the adjacent allocation (heap corruption,
both stages). The ladder never reads past esz (no uninit high bytes) nor
writes past the slot; esz=8 stays a single MOVQ (byte-id preserved).

insert() rides by construction: both stages desugar it to append and
re-dispatch into this arm. The #49 aplace path already uses the precise
ladder (verified, not exposed). #59 closes the last composite-value
eval-order hole in append/insert.

Pin: 946_append_structlit_evalorder_run — append / insert / narrow-neighbor
(i32-field at the cap boundary with an adjacent-allocation survival assert)
rows, each base-fail at 39432f7 and post-pass with cs==ww byte-id.
2026-06-07 12:10:38 +09:00
39432f717c wcc/cgen: #64+#68 tuple-literal cursor-fill decl-blind — massign + call-arg widen (both-stage)
A tuple LITERAL with a declared-tagged element reached the cursor-fill
helper (cg_tuple_lit_to_cursor) through the generic cgexpr(N_TUPLE) arm
with no declared type, so the element was stored stamped-keyed at its
constructed scalar width rather than widened into the declared tagged box.
Both consumers ran silent and wrong on both stages (#263 gate-blind:
cs==ww byte-identical, both wrong — runtime is the only net).

#64 massign: N_MASSIGN derives a declared tuple type from the lvalue
binding types and threads it into cg_tuple_lit_to_cursor + the receive
loop (mirror of the #57 N_LET wire); a `_` target falls back to the rhs
literal element type for cursor stride.

#68 call-arg: the send is made param-aware (fill over the PARAM tuple) and
the restage guard graduates a declared-tagged element to a real widen
(reusing cg_widen_tagged_store); nested tuple/struct/array elements and
tagged elements with no param decl stay rule-7 loud. The matching
pop/drain is made param-aware too so push count == pop count: a
param-aware send pushes the box's N words, so the drain must pop N or the
SysV arg sequence skews. This is a push/pop balance requirement of the
send change, not a separate latent under-drain (the standalone trailing-
arg drain is already correct at HEAD).

Closed by construction: the only remaining cg_tuple_lit_to_cursor caller
passing NULL/nil is the generic cgexpr(N_TUPLE) arm, provably non-widening
(constructed type == governing type). The four widening consumers — LET,
RETURN, MASSIGN, call-arg — are all decl-wired. Whole-tuple single-ident
reassign from a tuple literal is rule-7 loud (task #49), not a silent
widening consumer, so the residual NULL arm stays non-widening.

Pin: 945_tuple_lit_declblind_run — massign / call-arg / `_`-control /
call-arg-drain / nested-tuple-ERR rows, each base-fail at abd97e6 and
post-pass with cs==ww byte-id.
2026-06-07 11:23:48 +09:00
f3750ae3ce lib/io: empty() stream; wcc/cgen: #129 sretretsize + #130 global tagged-field store
io.empty (discard+EOF stream, ref/hare/io/empty.ha:4-17) — needed by getopt's
two-pass printusage width measurement. Diverges from Hare's `const empty: *stream`:
a `let _empty_vt` + `fn empty()` that wires the fn-ptr slots per call, because
const-init of a vtable struct with fn-ptr fields is blocked (#118, ruled accept).

Co-discovered while making empty() byte-identical across stages: three
wwstage-only cgen fixes (cstage was already correct; wwstage aligned down):
- #129 sretretsize: consult the same-module pointer-alias before structlookup's
  any-module struct fallback (io.stream = *vtable was mis-sized as memio's 56B
  struct -> spurious sret save).
- #129 callsretsize: swap curmod to the callee's module before sret-size
  classification (cross-module callee context).
- #130 cgassign global-struct tagged-union field store: add the missing arm
  (was a 1-word store) mirroring cstage cgen.c:4893-4912.

The three are inseparable from io.empty here — splitting them out leaves a
divergent-asm intermediate (993/995 red), so they ride one commit per the
one-class gate-repair carve-out (#133-expanded precedent). Regenerates the
embedded combined.ww; 989_lib_byteid pins bufio + fmt graduated to M_ID.
(cgenexpr.ww fix-3 inline comment cites the #129 cluster; narrow to #130 on
next touch to avoid a regen for a comment.)
2026-06-07 06:19:13 +09:00
754944a755 wcc/cgen: #121 indexed tuple-element read + literal-store round-trip (both-stage)
Reading or storing a tuple element of an indexed array element was
broken across the board (the fold-6 read-path). One fused commit,
both stages, four faces of indexed tuple-element access:

 - FIELD read `tbl[i].N`: was loud ("unsupported field-read shape" --
   the field-read dispatch keyed on an N_IDENT base; an INDEX base fell
   to a fatal). Now resolves &tbl[i] via the place-spine and reads the
   field at addr+foff through the existing per-kind arms (str-triple /
   scalar / fn-ptr).
 - WHOLE read `let e = tbl[i]`: was a silent word0-only truncation
   (plain-tuple kin of #37/#58, which covered only tagged). Now a full
   cursor fill from &tbl[i].
 - STORE `a[i] = (3,4)` (N_TUPLE-literal rhs): was a silent word0-only
   store -- the write face of the read. The aggregate-store-into-index
   site handled ident/dot/deref tuple rhs but not the literal; now it
   materializes the literal and word-copies. Narrow: N_IDENT base only
   (N_DOT/chained stay deferred, #270).
 - for-range over a const-slice-of-tuple: was a divergent SEGV; now a
   symmetric loud-stop on both stages (filed #122).

The store and read were a round-trip that passed test 809 only by luck
(broken store XOR broken read canceled). Fixing the read alone exposed
the silent store; rule-7 obliges fixing both, so 809 is now genuinely
correct, not luck-correct. Both faces are byte-id-blind (#263) -- the
net is a runtime round-trip pin with distinct-per-word values and a
real call clobbering the cursor registers between store and read, so a
word0-only store or read is caught. Both stages byte-identical
(990-997 green). Pin 947_tuple_index_read_run.
2026-06-06 22:23:43 +09:00
351abb0ab3 wcc/cgen: #58 indexed tagged-field read+assign cursor arm (both-stage)
Reading or writing a tagged field of an indexed array element
(xs[i].field) was broken on BOTH stages, byte-identically and
silently (#263 gate-blind): the arr[i].field branches had arms for
array/str/slice/float but no TY_TAGGED arm, so the tagged field fell
to the single-word scalar path. READ loaded only the tag word (stale
payload -> `xs[i].min as T` read garbage); ASSIGN stored the raw
unboxed scalar into the tag slot, corrupting the box.

Insert a TY_TAGGED cursor arm before each scalar fallback, both
sites both stages (cgen.c read + assign; cgenexpr.ww cgdot N_INDEX-lhs
read + cgassign indexed-field). READ mirrors cg_tagged_memread
(payload -> DX/CX/R8, tag -> AX last). ASSIGN synthesizes the tag for
the concrete variant (taggedvariantindext) and stores tag+payload via
the str/slice 3-word store spine -- not the source-remap widener
(concrete rhs has no source tag to remap).

>32B / multi-word / float payloads are loud-stopped at all four arms
(emission not yet wired; see #114). That shape is reachable today via
a narrow-variant ctor, so it louds rather than silently miscompiling.
Both stages get the same arm -> byte-id preserved (990-997 green; the
runtime is the net for this #263 class). Pin 944_idx_tagged_field_run
(read/assign runtime rows + >32B expect-loud rows).
2026-06-06 16:21:45 +09:00
5d596206c6 wcc/cgen: #94 def-array indexed &-base leg (both-stage)
`&D[i]` over a module-level DEF array SEGV'd on BOTH stages: the
TK_AMP N_INDEX N_IDENT base classify checked only the local and let
legs, so a def-array base fell to a wrong else — cstage zero-based
the addend (XORQ BX,BX -> wild pointer, cgen.c) while wwstage
value-loaded the symbol (MOVQ name(SB) = D[0], not its address,
cgenexpr.ww complex-base fallback). Divergent asm, both wild.

Add one def-array leg per stage, mirroring the working let leg:
- cs: `def_isarraydef(base) -> LEAQ name(SB),BX` alongside let_islet.
- ww: the `defvartnode` fallback the read-side cgindex already takes
  (cgenexpr.ww:1762) -> N_TARRAY classifies isglobalarr -> LEAQ
  name(SB).
The def DATA symbol already exists (plain &D + D[i]-read work), so
once the base is the address the existing i*esz scale + ADDQ
round-trips. cs and ww now emit BYTE-IDENTICAL LEAQ-SB asm — the
both-broken -> both-correct convergence is the point (#263 class).

Rows (944_def_amp_idx_run, all 0/0 byte-id): amp_int [3]int,
amp_u32 [3]u32 esz=4 (narrow scale), amp_arg &D[2] as a func-arg;
controls ctrl_plain (&D), ctrl_read (D[i]), ctrl_2d (&M[1][1]) keep
working. *p spelled `let v: T = *p` — `*p: T` parses as `*(p: T)`.

OUT (filed #112): &D[..] slicing a def-array is a distinct parse
reject needing a Hare-fidelity ruling — not this leg.
2026-06-06 13:27:24 +09:00
26ba1ad1b5 wcc_ww/cgen+peellint: #109 close #101 primsize-alias family by construction
Route the 16 routable bare-primsize GUARD sites (is-primitive /
struct-vs-prim dispatch) through the #101 aliasprimsize SSoT helper.
Byte-NEUTRAL by construction: an alias-narrow name is already
neutralized downstream by the same arm, so routing emits no new asm
(the empty-flip-set ken oracled).
  Shape-A exclude-prim-early (3): cgenutil sretretsize / structparamsize
    / structfloatclass — `primsize>0 return` then structlookup→nil
    returns the same value; route returns it early, same.
  Shape-B prim-guard-then-structlookup (13): cgenutil 4604/4650 +
    cgenexpr 4136/10244 + the 9-site CALL/assign cluster — primsize==0
    →structlookup→nil→fall to normal; route skips the block→same normal.

Install the peellint bare-primsize FINALE (B7 lint-fuse contract):
tools/peellint now rejects any bare primsize() in the ww stage outside
the annotated whitelist.  Evasion-hardened per the B7 lesson — a
character scan (comments + string/char literals stripped first) and a
LEFT+RIGHT word-bounded match of the bare `primsize` TOKEN (not just
`primsize(`), so the aliasprimsize() wrapper is never a hit and every
compiling spelling reds: the call primsize(nm), the paren-wrap
(primsize)(nm), the function-value bind `let p = primsize`, and any
line-split.  ww-only (the C stage dealiases via type_chase_named, no
primsize symbol).  Two independent exemption windows (peel-ok vs
primsize-ok) so neither rule blinds the other.  Runs as a make-test dep.

Whitelist the 6 designed exemptions with primsize-ok WHY-annotations:
  machinery — aliasprimsize body (SSoT chase) | typenodeprimresolved +
    exprprimresolved (#11/#33 prim-resolver chasers) | cgcast leaf-loop +
    cgenexpr #11 deref-store (own ps==0 fallback; route would regress
    #11) | the primsize oracle/definition itself (nothing below to chase).
  structural — elemsizeof x2 + paramfieldsize (chase lives in the -c
    twin elemsizeofc; threading c is the dormant #110).

Empty-flip-set proof: zero C bytes; cstage binaries bit-identical;
bootstrap byte-id 990-997 + 950 all green (w6c == w6c_ww on the full
selfhost, self-rebuild identical); combined.ww (w6c + wwdump) regen
idempotent; sizelint 0; peellint 0 (raw-peel AND bare-primsize over the
whole tree = the close-by-construction proof, zero unwhitelisted
survivors).  Tests: 944_peellint_gate +14 rows (bare / space-before-paren
/ name-at-EOL split / string-blind opener / paren-wrap / fn-value-bind
RED; aliasprimsize wrapper + primsize-ok annotated GREEN; corrupt
annotation RED; independent peel/primsize windows; C-file out-of-scope).

Closes the #101 primsize-alias family by construction.  #109.
2026-06-06 13:06:37 +09:00
4459a49d3a wcc/cgen: #87 plain tagged-union module-global DATA + match SB-resolution (both-stage)
A PLAIN (non-alias) module-level tagged-union global SEGV'd on BOTH
stages: no static DATA was emitted (let_emit_size/letemitsize returned 0
for TY_TAGGED) so the global was never registered, and the match
scrutinee resolved it as a frame-local at offset 0 — reading saved BP as
the tag. Two sub-sites, one route (neither half ships alone — DATA
without SB-resolution still SEGVs; SB-resolution without DATA reads
nothing):

(a) DATA-emitter — a non-nullable TY_TAGGED arm emits the box that
    byte-MIRRORS a runtime LOCAL of the same type: tag word at +0 (the
    const-selected variant index via cg_tag_for_variant / taggedvariant-
    index), payload at +8, zero-padded to the union box size. int and
    str/slice literal variants are wired (str carries a DATAR ptr patch
    at +8); any other variant payload loud-stops (rule 7). emit_tagged_
    data + emittaggeddata are the per-stage twins; let_pre_intern/
    letpreintern gain the matching str-variant intern. Nullable stays 0
    so the (*T|void) one-word fold keeps the 8B scalar arm.

(b) match-scrutinee global resolution — the PLAIN-tagged twin of #78:
    a global tagged ident scrutinee LEAQs name(SB) and copies the box
    into an @match_spill slot the dispatch indexes off BP.

DATA target (mirror of the local box, verified byte-for-byte): for
(i32|str)=42 the 32B box is tag0 | 42@8 | zero-pad; for ="x" it is
tag1 | ptr0@8(DATAR _S_n) | len@16 | cap@24. cs and ww emit byte-
identical asm.

Pins (rob §3, dual-stage 910 cstage + 997 wwstage, attest_pass.ww): the
tagged global-vs-local byte-identity pin (match over the GLOBAL gives the
same arm/value as over a LOCAL — was SEGV both stages) and the str-
variant tag-1 pin, plus the #86 tuple global-vs-local lock-pin guarding
the already-correct emitter path.

929 fail_global_src graduates: a >48B tagged GLOBAL by-value arg now
resolves through the cgplaceaddr MEMORY-class arm (LEAQ g(SB) + blit)
instead of the #38b loud-stop, and runs correctly (uninit zero box ->
first variant); the row becomes a positive run pin. The struct-variant
>48B init still loud-stops via the data emitter.

The first-class-VALUE copy of a tagged ident (`let q = g`) stays a
pre-existing silent #49/#46 sibling (local and global identically),
filed separately — out of this fold's two sub-sites.
2026-06-06 12:05:56 +09:00
45f5415209 wcc_ww/cgen: #101 narrow-alias fill-stride via aliasprimsize SSoT
A struct-literal array fill sized a narrow-alias element off a bare
primsize(name): `type my32 = u32` gave primsize("my32")=0, so the
element width defaulted to 8 and a [3]my32 strode MOVQ -24/-16/-8 —
field n collided with arr[2] (kw1_101 run exit 1). cstage chases
my32->u32->4 (MOVL stride-4) at the twin sites and is runtime-correct;
this is a ww-only align-up, cs untouched.

Fix: a new aliasprimsize(c, nm) SSoT helper — primsize(nm), else an
aliaslookup-chase N_TNAME loop then primsize — and route the SIZE-use
primsize() family through it. The 7 c-bearing bare-no-chase size-use
sites are routed: cgen:993 (letemitsize), cgenstmt:1896 (cgarrlitfillbp),
cgenutil:1579 (elemsizeofc fallback)/1657+1665 (nodeprimwidth)/4791
(cgstructlitfill = the kw1_101 site), cgenexpr:6902 (cgcall vararg esz).
This is the rule-13 close-by-construction shape (one accessor for
"resolved primitive size"), not a per-site patch.

kw1_101 is the SOLE asm mover (byte-id NO->YES, run 1->0, MOVL
stride-4); every other routed site is latent/byte-neutral. Bootstrap:
all 5 combined units stay w6c==w6c_ww byte-identical. sizelint 0,
peellint 0, test-unit 296/296.

Scope fence (rob route-7-decline-6 ruling): three DESIGNED-exemption
sites carry inline primsize-ok annotations — elemsizeof :1475/:1499 and
paramfieldsize :3541 are structural (no-`c`, non-chasing) BY DESIGN;
their alias-chasing twin elemsizeofc is the routed :1579 leg. These are
the #109 peellint-whitelist seeds. Three further declines are already
correct chasing paths, not bare-no-chase bug shapes (typenodeprimresolved
:2026 / exprprimresolved :2063 are the chase machinery itself; cgassign
:7631 already chases via typenodeprimresolved, #11). The ~17 GUARD sites
(is-primitive dispatch) + the peellint finale are the committed #109
follow-on. Threading `c` into the structural sizers is dormant #110.

#101
2026-06-06 10:04:37 +09:00