Commit Graph

411 Commits

Author SHA1 Message Date
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
074e0e585e wcc/ww: struct-local stack slot is the checker's natural size, not slot-padded
wwstage's slotsize() shared its TY_STRUCT arm with TUPLE/ARRAY and
returned ti.slotsize — the SUM of the slot-padded field widths. For a
struct LOCAL that over-reserves the frame slot whenever a field is a
sub-8 nested composite: a nested inner{x:u8,y:u8} (size 2, slotsize 8)
pads its in-struct footprint, and the local inherits that pad. cstage
has no slotsize SSoT — it reserves the local at f->type->size, the
checker's NATURAL r.size (cmd/w6c/cgen.c). So on outer{a:u8,
p:inner{x:u8,y:u8}, z:i64} wwstage emitted frame $32 / struct-base
-24(BP) while cstage emitted $16 / -16(BP): a uniform -8 BP shift on
every field access. Both stages exit 0 (each self-consistent), so it is
runtime-invisible — but it is a cs!=ww .s divergence (rule 10) and a
latent byte-id gate-landmine the day such a struct enters the corpus.
Same dual-SSoT leak as #44 (field-OFFSET) / #55, one notion over:
struct-local-slot-SIZE.

Fix: split the TY_STRUCT arm out and return round8(ti.size). The TUPLE
arm (8B/elem slot, user ruling #60) and the ARRAY arm (element stride,
#48 [N]Alias 24B) keep ti.slotsize — those are deliberate, ruled
divergences and are untouched. The struct-local slot consumers
(cgendecl.ww letslotsize via cglet, cgenstmt.ww) all flow through this
arm; si.totsize (registerstruct → structabisize / global-emit) is a
separate consumer and is not this path.

CLASS-N corpus-neutral: every corpus struct local is 8-aligned, so
round8(ti.size) == slotsize for all of them and the w6c_ww/wwdump_ww
emission does not move (994 byte-id on 18 corpus inputs + 995 5-tool
self-rebuild both green post-fix). 989_structlocal_frame is the
FRAME-ABSOLUTE proof (w6c vs w6c_ww .s byte-diff; nested3 + tail_u32 +
flat control) — the .s twin of the runtime 989_nestfield_run, which
deliberately does not gate the frame and points here for it.
2026-06-13 13:57:21 +09:00
037d59cf4e wcc/ww: registerstruct field offset is the checker's natural tfield.offset
wwstage carried TWO struct-layout sources. registerstruct (cgenutil.ww)
recomputed each field's `fi.foff` via fieldsize — slot-padded, round-8 —
for the WRITE (construction / field store) path, while the READ path
(cgplaceaddr / dotbaseaddr) used the checker's natural `tfield.offset`.
They diverged iff a struct had a nested sub-8 composite field
(slotsize != size) plus a successor: ww wrote the successor at the
slot-padded offset and read it at the natural offset, mis-addressing its
own field. cstage has no structinfo and reads tfield directly, self-
consistently natural (cmd/w6c/cgen.c).

Fix: make `fi.foff` a VIEW of the checker's already-built natural layout.
Lock-step walk tstruct.list (AST N_TFIELD) and ti.fields (tfield) — both
head-first declared order, both skip non-TFIELD identically — and copy
foff = tf.offset, fsz = tf.type_.size. si.totsize keeps the slot-padded
stack-slot number (ti.slotsize, already 8-rounded at check.ww:2259).
fieldsize is no longer called here (its `*p OP=` scalar-width caller is
untouched). LOUD nil-guards on tstruct.type_ / tichase / a tfield walk
desync — all unreachable post-check, never silent. fi.tnode stays the
AST node (its node-keyed readers need it); repointing the ~60 fi.foff
readers to tfield is the out-of-scope (ii-b) follow-up.

This unifies ww's second source onto the value cstage already emits, so
cs==ww is preserved, not newly created (wwstage-cgen only; no cstage
edit). The shape is corpus-absent — ww uses both sources on its own
structs, so a divergent struct would have broken the bootstrap — hence
gate-blind; 989_nestfield_run is the proof (nested inner{x:u8,y:u8} in
outer{a:u8,p:inner[,z:i64]}, every field read back == written, dual-stage
cs==ww). It also makes 681 ragged_tail_12B genuinely correct: the
predecessor #71 already shrank the whole-struct copy to the source's
natural length, so packing mark at natural offset 12 no longer clobbers.
2026-06-13 13:39:10 +09:00
559b77db40 selfhost/cmd/w6a: align parsenum to strtoll(base 0) semantics (#62)
w6a's parsenum diverged from the C twin's strtoll(s,end,0)
(cmd/w6a/lex.c:30) on three hand-written-asm edge shapes (all
gate-blind — w6c emits the canonical $5/$8/-8(BP), never these):
  (a) `$ 5`  — leading whitespace: strtoll skips it (->5); ww had no
              skip and silently encoded imm 0.
  (b) `$08`  — strtoll base-0 reads a leading 0 as octal, stops at '8'
              (->0); ww parsed it as decimal 8.
  (c) `-(BP)` — strtoll/cstage require a digit after the sign, so a bare
              `-(` is unrecognised operand; ww silently took it as 0(BP).
Add the whitespace skip + octal base-0 detection to parsenum, and the
digit-after-sign guard to the operand scanner — both assemblers now
agree byte-for-byte (a/b) and both reject (c).

Not a Hare item (w6a is ww's plan9-lineage assembler); reference is the
C strtoll twin. w6a embeds into its own combined.ww snapshot; regen'd.
530_w6a_parsenum pins the byte-identity + both-reject matrix.
2026-06-13 11:03:42 +09:00
427b67f656 lib/dirs: abort loudly on over-long path, not silent truncation (#69)
dirs build() capped the composed path at the 256B pathbuf with a silent
break, so a HOME (or XDG_*) near/over ~240 bytes produced a truncated
path that lookup() then mkdir'd and returned rc=0 — a silently-wrong,
freshly-created directory. ref/hare/dirs/xdg.ha routes through
path::set/push whose too_long error the `!` aborts loudly. Precompute
the composition length in build() and rt_abort when it won't fit; drop
the now-dead silent caps. (Shape (a); routing dirs through lib/path is
the filed fidelity follow-up.)

975_dirs_toolong_run pins the abort + no-stray-dir on both driver twins.
2026-06-13 10:38:51 +09:00
5cab22ecec lib/ww/parse: error on unknown top-level decl in parsefile fallback (#55)
parsefile's recovery fallback chewed an unrecognized top-level
construct to the next ';' without emitting an error or bumping p.errs,
so a typo'd keyword / stray token silently vanished from the AST and
the build succeeded rc=0 with the declared work gone — no link error
catches a dropped @test or unreferenced exported fn. The C twin
(parse.c:1395-1400) errorf+p->errs++ and rejects. Emit errmsg in the
fallback arm; wwstage now rejects in lockstep with cstage.

lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_unknowndecl_reject pins the reject-matrix on both driver twins.
2026-06-13 10:33:48 +09:00
cf0789c897 lib/ww/lex: port the u64 overflow guard into parseint (#53)
wwstage parseint dropped the pre-multiply overflow guard the C twin
carries (cmd/wcc/lex.c:156, if (v > (u64)~0ULL / (u64)base)), so any
integer literal exceeding u64 was silently accepted mod 2^64 while
cstage loudly rejected with 'bad integer literal' — a rule-10 stage
divergence and a silent wrong constant. Port the guard before the
multiply-add; wwstage now rejects in lockstep with cstage.

lib/ww embeds into the w6c/wwdump combined.ww snapshots; both regen'd.
989_intoverflow_reject pins the reject-matrix on both driver twins.
2026-06-13 10:32:56 +09:00
8f370533bb ww driver: zero discovered tests is a loud failure
rundirtests returned rc=0 when it discovered no tests — a typo'd
path or empty corpus read as success. Fail loudly, matching the
cstage driver.
2026-06-13 10:20:46 +09:00
675a0368cd w6l: dynamic e_entry rebases with the actual text offset, both stages
When the dynamic section pushed the header past the first page, the
entry point kept its first-page address — every sufficiently large
dynamic binary SIGSEGV'd into the headers. Recompute e_entry as
entry - 0x1000 + text_off in both stages (ELF: the entry must point
into .text wherever it lands). Both stages move in one commit: one
ELF contract; the 989_dynentry gate pins the field and the run.
2026-06-13 10:17:51 +09:00
95da870734 w6a/ww: DATAR naming an undefined slot is a loud reject
A DATAR relocation against an undefined data slot was silently
dropped — the relocation vanished from the object. Reject loudly,
matching the cstage single-pass resolution behavior.
2026-06-13 04:43:57 +09:00
2ce94a194a wwdump: -c/-r gate on parse errors
wwdump's -c/-r modes emitted output from a garbage parse silently.
Gate on the parse-error count first (the w6c main gate, main.ww:162);
wwstage-only — the C wwdump has no -c/-r modes.
2026-06-13 04:40:58 +09:00
4dc8459a90 ww driver: enumeratedir grows past 256 entries
The wwstage driver silently dropped directory entries past a 256
cap; the cstage twin already grows by realloc-doubling. Grow the
same way (seed 8, double) so both stages agree on any directory size.
2026-06-13 04:37:47 +09:00
f7845057a7 wcc: loop-label stack guards its depth loudly, both stages
wwstage's unguarded loop-label push wrote out of bounds at depth 17
(compiler-heap corruption); cstage guarded but emitted a wrong break
target. Loud cap error at the limit, both stages, agreeing wording.
2026-06-13 04:34:36 +09:00
92cd573197 wcc: defer capacity 32 with a loud cap error, both stages
wwstage capped defers at 16 and SILENTLY DROPPED the 17th; cstage
capped at 32. Align the cap at 32 and make exceeding it a loud
compile error in BOTH stages — the silent 16-vs-32 split was the bug
(a defer that never runs is a leaked resource). Both stages move in
one commit: one cap contract.
2026-06-13 04:31:12 +09:00
1f2bc7fc26 wcc/ww: exprtypeoftry prefers the current module's symbol
The try-operand resolution used bare lookups (ident + bare-leaf call);
a cross-module same-leaf collision mistyped the operand. Prefer the
current module. The historic 995 byte-id break attributed to this swap
was contamination from the guard-bug-carrying bundle — re-probed clean
in isolation and at the full stack. The N_DOT module-keyed leaf stays
task #51. Report item [11], lookup half.
2026-06-13 03:04:52 +09:00
51e3b8f134 wcc/ww: &fn synthesis prefers the current module's fn
The address-of-fn synthesis used a bare lookup — import order could
bind a same-leaf fn from another module, silently LEAQ-ing the wrong
function into a fn-ptr slot. Prefer the current module (mirror
check.c:410/1305). Report item #4 (loud and silent faces pinned).
2026-06-13 03:01:31 +09:00
63e837e820 wcc/ww: scopelookuptype prefers the current module's symbol
A type lookup in a bundled build resolved to the newest-installed
same-leaf symbol from ANY module; prefer the current module first
(mirror cstage sym.c:131; the prior attempt's failure was its own
u64-vs-i32 guard bug, not a deeper layer — probe-proven). Also adds
the rule-7 #58 notes at the latent varianterr/scruttype pair and
rewrites the stale deferral block to closing cites. Report item [5].
2026-06-13 02:58:09 +09:00
ffe37deaee wcc/ww: def-dim array slice-arg default-hi takes the length from the type table
Passing buf[1:] of a [MAX]u8 as a call arg dropped the default-hi
length (the arg-push N_SLICE arms' fallback covered only named-alias
bases). Same type-table resolve at pushargsrev local+global. This
closes the def-dim dimension family by construction: every N_INTLIT-
keyed dim consumer (cgslice, cgdot, letemitsize, arg-push) now
carries the tichase().alen fallback — grep-proven, no consumer
remains. Fourth member surfaced by the family grep.
2026-06-13 01:56:01 +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
ffb858bba6 wcc/ww: under-length array-literal tail zero-fills
The unspecified tail of a short array literal repeated the last value
instead of zeroing — wwstage only (cstage already zeroes; the review's
both-stages reading didn't survive ground truth). Zero-fill the tail
per the zero-value semantics ruling. Review item #13.
2026-06-13 00:17:57 +09:00
94a55c565f wcc/ww: no-init array global emits one DATAW slot, not two
The str-size arm of the global data emit was size-keyed and matched a
24-sized array, emitting a second DATAW for the same symbol. Gate on
the array kind (!isarr8). Review item #12.
2026-06-13 00:14:35 +09:00
347f6c42c8 wcc/ww: paramfieldsize sizes a tagged-union destructure binding
A tagged for-range destructure binding took the 8-byte default
(paramfieldsize had no tagged arm), skipping the full-extent copy —
the wwstage twin of the just-closed cstage destructure family. Read
the stamped tinfo size through the type table (twin of the slice
arm; cstage reads tp->type->size). Task #53.
2026-06-13 00:07:36 +09:00
77747061c6 w6c: global *struct base field store loads the pointer from g(SB)
cstage dereferenced 0(BP) for the base — a SEGV on every store
through a module-global struct pointer; wwstage was already correct
(the inverse-set member). Mirror ww's global-pointer load. New
989_globptrfield_run pins cs==ww both stages. Task #47.
2026-06-12 22:48:37 +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
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
87367ae332 wcc/ww: match-as-expression unifies arm yields (coarse-family gate)
exprtype N_MATCH took the first arm's yield type without walking the
rest — int-vs-str arms silently produced garbage downstream. Walk all
arms: typeeq-equal accepts; a definite coarse-family mismatch
(num/str/bool via the new yieldclass classifier, unknown classes stay
lenient) rejects. Same-family non-assignable pairs remain lenient —
the documented precision residual is task #52 (needs tinfo-level
type_assignable). The wave's table-driven reject test lands here:
989_catA_f2_reject, 19 rows x 2 stages, each member pre-fix-red-proven.
2026-06-12 11:31:53 +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
e416885d96 wcc/ww: widen of a module-global tagged ident copies the whole box
Widening a global tagged union into a wider tagged slot copied
nothing of the box; treat the global ident as a tagged source and
copy the full box from g(SB) through the nested arm. Both-wrong pair:
cstage spills frame garbage as the box (filed task #44, residual
non-deterministic so the rows assert divergence only). The non-nested
SUBSET-widen shape remains open as task #49 (site comment at the
fall-through). Review item #51.
2026-06-12 09:21:16 +09:00
32848194d7 wcc/ww: widen of a module-global struct ident copies the full payload
Widening a global struct into a tagged slot copied word0 only; copy
the full payload from g(SB). Both-wrong pair: cstage zero-fills the
payload (filed task #43); rows pin ww-runtime-correct with the
documented cstage residual. Review item #50.
2026-06-12 09:16:52 +09:00
4b8c8fd9ed wcc/ww: return of a module-global struct ident copies the global's bytes
Returning a global struct by name emitted nothing into the return
scratch (ww) — copy from the g(SB) base. Both-wrong pair: cstage
zeroes the retscr instead (filed task #42); rows pin ww-runtime-correct
with the documented cstage residual. Review item #41.
2026-06-12 09:13:26 +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
1eceb46ac3 wcc/ww: global-str default-hi slice arg loads its len word
Slicing a module-global str with default hi emitted nothing for the
bound; load the len word from g(SB)+8 (mirror the cstage twin).
Review item #47; dual-stage rows red-proven.
2026-06-12 09:00:15 +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
e0df2adf47 wcc/ww: tagged-union normalization at tinfofornode (never-drop, dedup, collapse, nullable fold)
wwstage computed tagged sizes/tags off the raw variant list — size()
folded wrong constants (size((*u8|void)) 16 vs 8, (i32|never) 16 vs 4)
and duplicate variants got divergent tag numbering vs cstage, while
ww's own cgen layout folded nullable but its size() didn't. Make
tinfofornode's N_TTAGGED arm the normalization SSoT mirroring cstage
resolve_type (check.c:801-882): never-drop, duplicate dedup via
structural typeeq, single-variant collapse, nullable fold on the
normalized pair; astsize/astalign delegate, and voidvariantindex reads
the normalized ti.params (cgen.c:900-911) so construct/match/void tag
readers agree. Corpus-neutral (zero-move on all combineds);
989_tagnorm_run pins the folds dual-stage, red-proven. Review items
#1/#3; residual #45 filed (AST-keyed nullable gate at global emit).
2026-06-12 07:03:45 +09:00
d6052e0829 wcc/ww: nodefnptr keys the bare-ident arm on the stamped type
nodefnptr matched bare idents by NAME against the fn table, so a
global var colliding with a fn leaf classified as a fn pointer —
wwstage silently built what cstage rejects at link (review finding
#14). Key on the stamped type; the #124 &mod.fn arm is preserved.
989_fnptrcollide_run pins both stages reject (red 1/2 pre-fix:
wwstage built rc=7).
2026-06-12 00:21:16 +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
0625af1309 wcc/ww: nodeisunsigned resolves module-global idents via the stamp
A bare module-global unsigned operand got signed IDIV/SAR/Jcc — the
predicate's ident arm only consulted the local table, so globals fell
through to signed (review finding #25). Read the stamp for the global
arm; corpus emission is unmoved (no bootstrap code div/shift/cmps a
bare unsigned global). 989_gunsigned_run pins cs==ww (red 3/8
pre-fix).
2026-06-12 00:14:32 +09:00
7afc4df652 wcc/ww: paramfieldsize sizes slice/tuple fields through the type table
for-range destructure of an element with a slice- or tuple-typed field
strode by the default 8 (paramfieldsize had no N_TSLICE/N_TTUPLE arms),
silently reading the wrong words (review finding #43; live repro
cs=42 vs ww=8). Add the arms routed through tinfo per rule 13.
989_tupfieldsize_run pins cs==ww (red 1/2 pre-fix); rows assert
convergence, not absolutes — cstage's own single-word destructure-load
bug is filed as task #40. The N_TARRAY arm is deferred (task #39,
rule-7 comment at the fall-through).
2026-06-12 00:11:11 +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
cbbfd1a4a6 wcc/ww: nodeisslice/nodeisstr gain the N_INDEX arm (stamp-keyed)
A slice/str ELEMENT of an indexed expression passed as a call-arg
pushed one word instead of the 24B/16B header — the predicates had no
N_INDEX arm, so element-typed args fell to the scalar path (review
findings #45/#46). Read the element-type stamp; dual-stage rows in
989_idxarg_run pin cs==ww (red 2/8 on pre-fix binaries).
2026-06-12 00:04:22 +09:00
556a65ee86 wcc: general call-arg typecheck via assignability union, both stages
wwstage's desugarcallargs ran no general per-arg typecheck (only the
narrow #258 array-to-slice arm): any mistyped scalar call-arg silently
miscompiled (int read as a 24B slice header; the -T face was a user
const __wwtests building a garbage test binary). Route every call-arg
through the predicate union isassignable()||assignableaddrfn(),
mirroring cstage type_assignable||assignable_addrfn and the check.c:1869
diagnostic. Confident scalar/aggregate and aggregate/aggregate
kind-mismatch rejects live in shared isassignable; the concrete-to-
tagged arm is shape-matched-lenient via tagshape() (AST mirror of cgen
taggedvariantindext) so genuine variant members keep flowing while
shape-mismatched aggregates reject. Reserve __wwtests under -T in both
stages (mirror the main reservation, check.c:2996). New table-driven
989_callarg_typecheck, 31 fixtures, reject rows proven red on pre-fix
binaries.

Deferred, filed, site-commented: the assign seam rides #178->#36
(typeeqast cannot compare variadic/module-qualified fn sigs); the
same-coarse-shape same-leaf nominal collision over-accept rides #37
(#10/#66 — the distinguishing module is absent from the AST surface
isassignable operates on).
2026-06-11 20:45:02 +09:00