Commit Graph

521 Commits

Author SHA1 Message Date
34c1051a63 wcc/ww: reject a duplicate top-level main (F-D)
A second top-level decl named `main` (fn/let/def/type) collides with
the entry main on the single bare `main` symbol: today both lower to a
bare `main`, w6l silently accepts the duplicate, and the program links
rc=0 then segfaults (or runs wrong), in both stages. The existing
duplicate-decl rejects key on (name, module), so a cross-module
`foo.main` vs the bare entry `main` read as distinct and slip through.

Add a program-global, name-only, cross-module uniqueness check on
`main` in the checker (both stages), colocated with the duplicate-decl
rejects and counting user decls before the -T synthesized test main.
Corpus-safe: a lone `fn main` in any package stays legal (ww has no
package-main convention -- cmatrix/lisp/mandelbrot are non-main-package
entries and keep building). This converts the silent segfault to a
loud compile error and subsumes the w6l silent-dup-main case (#31);
correct package-aware mangling of a non-entry main is deferred to the
root-unit entry-detection work (#22/#32).

Regenerates the w6c and wwdump combined.ww. Table-driven 842 test:
reject rows for let/fn/def/type main (genuine cross-module import form)
plus a negative single-main corpus-safe row that must still build+run.
2026-06-14 17:05:46 +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
7a6b67fecc w6l: size -L/-l/input arrays by argc, not a fixed 64 (F-C)
libdirs, lflags and inputs were fixed 64-slot arrays written with no
bound check; the 65th -L/-l flag (or input) wrote past the allocation
-> heap corruption. Size all three by argc instead, the true upper
bound since each argv slot yields at most one entry, mirroring cstage
cmd/w6l/main.c:63-67 (calloc(argc, ...)). Drop the now-dead maxinputs
"too many inputs" cap -- cstage has none, and argc-sizing makes it
unreachable.

Regenerates the w6l combined.ww. Table-driven 632 test reaches a lib
only via the Nth -L (N in {1,64,65,100,128}, both stages); pre-fix the
nflags=65 row fails (slot one past the 64-array).
2026-06-14 12:36:36 +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
1752305be7 fmt: scandigits rejects the i32-overflowing last digit (F-A)
The one-sided guard `v > 214748364` never fired for the last digit:
at v==214748364 a next digit of '8'/'9' made `v*10+digit` overflow
i32 and wrap negative, slipping past the signed args-index bound
check at fmt.ww:703 -> OOB arg read -> SIGSEGV on any format
directive carrying an over-i32 digit run (index, width or precision).

Complete it to the canonical two-part pre-multiply Horner guard
(MAX/10, MAX%10). Hand-rolled in signed i32, not Hare scan_sz's
unsigned post-multiply wrap-check (ref/hare/strconv/stou.ha:60),
which would be signed-overflow UB-class here; noted at the site.

Table-driven subprocess test over all three scandigits call sites,
5 rows x both stages; reverting the guard reproduces exit=139.
2026-06-14 11:46:10 +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
323607d1d0 wcc: store to a global tagged-array element resolves the (SB) base (#16)
The indexed tagged-element assign arm computed its base without the
isglobal -> LEAQ name(SB) branch the scalar element arm already has, so
`gs[i] = v` on a global tagged array stored to a junk frame base and was
lost -- cstage rc=0 where wwstage (which has the branch) rc=42. Mirror
the scalar arm's base resolution; cstage aligns up to wwstage. Local
tagged arrays and scalar globals are unchanged.

The global tagged-array static initializer still mis-packs its DATA in
both stages -- a separate emitter path, filed as #19.
2026-06-14 00:14:42 +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
728d86518e wcc: nullable-global storage is a loud reject pending #15, both stages
A module-level nullable `(*T | void)` GLOBAL has no storage path in
either stage: let_emit_size / letemitsize returned 0 for the nullable
TY_TAGGED, so let_collect skipped registration and emit_lets skipped
DATA. The three READ paths then miscompiled SILENTLY and identically-
wrong (a #263-class both-wrong gap, not a wwstage align-up): match read
0(BP) = saved BP via the let_islet-gated #87 arm falling to localfind;
`g is *T` / `g as *T` emitted MOVQ name(SB) for a symbol with no DATA →
w6l undefined-reference. cstage's #87 match arm was itself `!is_nullable`-
gated, so both stages were wrong.

This is the silent→loud bridge: die loud at the size/storage layer the
instant a nullable global is declared, so all three read paths hit one
diagnostic instead of a silent miscompile. A silent gap here is exactly
what "stable before CSP" forbids — CSP's process/handle/chan singletons
(`let c: *Chan | void`) are THE canonical nullable-global consumer. The
full storage + read-class arc (real DATA, nil/void/address-of init, let-
registration, the three SB-resolution read arms) is deferred to task #15
(CSP-prereq); the `&`-init sub-problem additionally couples to the #48
static address-of relocation gap (which already bites a plain `*T` global
init the same way).

Diagnostic core text is identical both stages ("nullable-global storage
unimplemented (task #15)"); cstage's fatal() adds the harness-wide "ww: "
err.c prefix err.ww does not, the same per-stage asymmetry every existing
both-stage reject carries. Byte-id-neutral: the corpus declares zero
nullable globals (grep-verified), so the loud path is unreached in self-
compile and the emitted asm is zero-move; the embedded w6c/wwdump
combined.ww amalgamations are regenerated for the cgen.ww source change.

New 989_nullableglobal_reject: 6 reject rows (match/is/as on a &gv init,
plus nil-init and void-init match, plus an inline non-aliased nullable
form) prove rc!=0 + the shared diagnostic on both stages, init- and
form-invariant; 2 controls (non-nullable tagged global, plain nil-init
*T global) prove the reject is keyed on the nullable TY_TAGGED and the
#87 storage path is untouched.
2026-06-13 18:44:10 +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
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