Commit Graph

867 Commits

Author SHA1 Message Date
89f3e58458 wcc/check: #26 recurse over-fill walk into nested tuple element (wwstage)
extracts the shared checktuplearrfits helper (also used by #25); wwstage-only checker reject-align, cstage already louds.
2026-06-09 15:00:03 +09:00
46e8354056 wcc/check: #6 stamp inferred-type array global so wwstage compiles it (was asserttyped exit 1)
An inferred-type array global -- let xs = [1,2,3]; -- hard-failed wwstage
with 'asserttyped: int' exit 1, while cstage compiled+ran it. The
array-twin of the inferred-global family (#135 inferred-float, #150-B
inferred-Sym-repoint).

exprtype's N_ARRLIT arm synthesizes the array type for an unannotated
literal but left two synthesized child nodes unstamped: the count literal
(asserttyped trips on it -> the loud failure) and the element TNAME (cgen
then drops a non-scalar element's header load -> the silent miscompile
that merely accepting on alone would introduce: an inferred str-array's
xs[1].len read 24 not 3). Both are now stamped at the synthesis site:
cn.type_ (asserttyped facet) and elt.type_ (cgen facet). Inferred int /
u8 / str / struct / multi-dim array globals + locals + args now compile
byte-identically to the explicit-typed form (== cstage).

cstage unchanged (w6c md5 unchanged); selfhost has no inferred array
globals so byte-id 990-997 8/8, no lib pin flips. test/wcc/833.
2026-06-09 02:59:52 +09:00
dfdf99ffd8 wcc/check: #20 reject overlong array-literal in a tuple element (wwstage)
An overlong array literal as a tuple element -- let t: ([2]int, i32) =
([1,2,3], 5) -- was silently accepted by wwstage; cstage loud-rejects it.
The #12+#106 over-fill coverage wired checkarrlitfits for direct-array,
slice and alias lhs positions but not the tuple-element position.

wwstage-only checker, reject-align: checkletassign gains an N_TTUPLE arm
that walks the lhs element types (llhs.list) lockstep with the rhs values
(n.rhs.list), calling the existing alias-aware checkarrlitfits per array
element (no-ops scalars, recurses nested arrays). cstage unchanged (w6c
md5 unchanged); reject-only, 990-997 8/8, no lib pin flips. test/wcc/832.

Two sibling tuple-element positions stay open (filed, not folded -- they
are reject-aligns on invalid programs, no selfhost byte-id impact): #25
tuple-RETURN overlong, #26 nested tuple-in-tuple.
2026-06-09 02:34:33 +09:00
d8e2a0692c wcc/cgen: #15 empty zero-length array emit — no spurious DATAW, cstage frame formula (wwstage)
An empty zero-length array diverged cs!=ww in asm (both ran correct=7):
a [0]int global emitted a spurious DATAW main.X(SB),"", and a [0]int local
reserved a $16 frame slot. cstage emits neither. wwstage-only, byte-id-only.

The global DATAW emit is now gated on sz > 0 (skips the empty array).
The local frame: localreserve dropped its sub-8 floor (if asz<8 asz=8) to
mirror cstage's localslot formula (frame+sz+7)&~7 -- but that floor was
MASKING slotsize(TY_VOID)=0 (a void local), which cstage defaults to 8B;
removing the floor alone collided the zero-size void slot with a spilled
param (a real miscompile -- 1132 self-compile hunks). So letslotsize now
returns 8 for a void local, while empty-struct / [0]-array stay genuine 0.
The frame formula is byte-id-neutral for every sz>=1 local (round8 already
>= 8); only true zero-size cases change.

cstage unchanged (w6c md5 unchanged). byte-id 990-997 8/8 (the full
self-compile is what caught the void-local class); test/wcc/820 un-carves
the #9 empty-[0] byte-id exclusion + adds void-local/local-[0] rows.
2026-06-09 01:57:46 +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
ee8082a43b wcc/cgen: #151 push all 3 header words for let-global slice/str by-value arg (wwstage)
A let-global slice or str passed by value as an argument was silently
field-dropped by wwstage: pushargsrev's global branch had only the #150-A
struct arm, and wwstage's nodeisslice/nodeisstr are local-keyed (false for
a global), so a global slice/str ident fell to the scalar single-PUSHQ,
pushing one of the three header words {ptr,len,cap} -> the callee read
garbage for .len/.cap. cstage was already correct.

wwstage-only, caller-side only (slice/str params already received
correctly). A global slice/str arm in pushargsrev, type-keyed on
tichase(arg.type_).kind, with two arms byte-matching cstage's two distinct
sequences -- str via cgslicehdr (CX-base, cgen.c:1866), slice per-word
(BX-base, cgen.c:9124). Gate is isletvar-only (a def has no name(SB)
holder; cstage const-folds it -- def-str/slice-by-value is the residual
task #21). cstage unchanged (w6c md5 unchanged); byte-id 990-997 8/8,
no lib pin flips. test/wcc/829 table-driven, byte-id per type.
2026-06-09 00:44:44 +09:00
c10fffae16 wcc/check: #12+#106 reject overlong array-literal in return/call-arg/alias positions (wwstage)
An overlong array literal (more initializers than the declared length) is
invalid -- cstage loud-rejects it everywhere -- but wwstage silently
accepted (and truncated) it in several positions; #9 wired only the decl
position. This folds the remaining three (one class: checkarrlitfits
over-fill coverage), all wwstage-only reject-align:

- #12a return   fn f() [2]int = [1,2,3]            -- silently accepted.
- #12b call-arg g([1,2,3])                         -- louded only late via cgen #271.
- #106 alias    type A=[2]int; let g: A = [1,2,3]  -- silently truncated;
  checkarrlitfits bailed on the N_TNAME alias without chasing.

Four inserts in check.ww: an alias-chase (resolvealias) at the top of
checkarrlitfits (makes all callers alias-aware), the over-fill check wired
into checkretassign (hoisted above the isassignable short-circuit) and
desugarcallargs, and the alias-let-global guard made alias-aware. cstage
unchanged (w6c md5 unchanged); reject-only, so no asm moves -- 990-997 8/8,
no lib byte-id pin flips. A 5th position (tuple-element overlong) is a
separate pre-existing hole, filed (#20). test/wcc/828 table-driven.
2026-06-09 00:17:10 +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
83025b03a6 wcc: #99 alias-of-tuple — chase TY_NAMED in tuple coercion (cstage) + param spill (wwstage)
type pair = (int, int); let x: pair = (3, 4) -- an alias of a tuple
initialized from an untyped literal, and passing such a value to a fn --
was a both-stage bug, mirror-twins of the same TY_NAMED-not-chased root:

cstage CHECKER over-rejected the init (not assignable to declared pair):
type.c's tuple-assignable arm gated on the un-chased dst kind, so a
TY_NAMED alias skipped the per-element untyped->int coercion the direct
tuple path applies. Fix: chase TY_NAMED both sides (mirrors the #258
slice-borrow arm). Direct and typed-alias tuples already worked; only
alias+untyped was rejected.

wwstage CGEN dropped the second word of an alias-tuple fn-arg: the
tuple-param spill at cgendecl.ww gated on the syntactic N_TTUPLE, so an
alias param (N_TNAME) fell to the scalar path and spilled one slot ->
t.1 read frame garbage. Fix: chase the alias via aliaslookup to the
resolved N_TTUPLE and spill all its slots. cstage cgen was already
correct -- the bug was checker-only there. Converges cs==ww byte-id.

One commit: same construct, the two halves must ship together (either
alone leaves cs!=ww). test/wcc/826 (init/fn-arg/return, 2-field byte-id);
test/wcc/944 4 rows graduated err->run-correct. byte-id 990-997 8/8.
2026-06-08 23:12:06 +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
ebd5b8014c wcc/check: #150 inferred-type module-global Sym repoint (cstage, #18)
Every inferred-type module-global -- let n = 5; ... return n, or
let g = pt{...}; g.a -- yielded <nil> downstream in cstage: the module
N_LET pass-2 stamped d->type from the initializer but never repointed the
Sym, so later references resolved the still-unstamped Sym. The wwstage
checker already repointed correctly, so this aligns cstage UP (cstage-only;
no combined.ww / check.ww change, w6c_ww unchanged).

Mirrors the #11 [_]-array repoint. The fix is shape-agnostic (keyed on the
unstamped Sym, not the use site) -- verified across scalar/field/arg/index/
str/match/nested inferred-global shapes. Commit B of #150 (Commit A 5c37648
fixed the by-value struct-arg cgen). A pure inferred ARRAY global is now
correct in cstage but trips wwstage asserttyped -- opposite-stage, filed
#125-class. byte-id 990-997 8/8. test/wcc/823 table-driven.
2026-06-08 21:07:47 +09:00
5c3764828f wcc/cgen: #150 by-value module-global struct-arg base — load main.g(SB) all words (both stages)
Passing a module-global struct by value -- let g: pt = pt{...}; take(g)
-- was silently miscompiled, mirror-opposite on the two stages. cstage's
by-value struct-arg arm hit localfind(g)->0 and read 2 words from the
frame (MOVQ (BP)), never main.g(SB) -> returned garbage. wwstage used the
correct main.g(SB) base but fell through to the scalar single-PUSHQ
default, pushing one word for a 2-word struct -> dropped a field.

Both stages now take the off==0 global branch: LEAQ main.NAME(SB) and copy
all struct-size/8 eightbytes (reusing the GAP-A.ptr/#231 global-base
predicate), converging to one byte-identical sequence. The local path
(off!=0) is unchanged; >16B aggregates (#271) already resolved globals.

Commit A of the cluster; the cstage-only inferred-global-type Sym-repoint
(every let g = ... module-global yields <nil> downstream) is Commit B
(#18). Slice/str global-by-value args have the same wwstage field-drop --
filed (#10 G-valglobal-arg; struct closed here). byte-id 990-997 8/8.
test/wcc/822 table-driven, byte-id per stage.
2026-06-08 20:51:01 +09:00
03fc7c7abe wcc/check: #14 reject def-global scalar str index (silent segfault) (both stages)
def S:str = "hi"; S[0] silently segfaulted: a def is a compile-time
constant, never materialized as DATA (unlike let), so indexing it emitted
an unbacked main.S(SB) reference -> cstage ran into frame garbage,
wwstage link-failed. str[i] itself is valid ww (a deliberate Go-like
str[i]->u8 byte-index that lib/strings compare/dup depend on), so the fix
is narrow: the N_INDEX TY_STR arm now rejects an index whose operand is a
bare SK_DEF scalar-str symbol, both stages -- 'cannot index a def-constant
str; bind it to a let'. INDEX-ONLY: len(S) and &S are already loud, and a
def's .len/.ptr field reads (the load-bearing w6l INTERP) are N_DOT, a
different arm, and stay valid.

A rule-9 WHY-comment records str[i]->u8 as a sanctioned divergence from
Hare's strings.toutf8. The full make-it-work fold (len(S)->2, S[0]->byte)
is deferred (#16). byte-id 990-997 8/8. test/wcc/821 table-driven.
2026-06-08 20:17:05 +09:00
29a2ab2a72 wcc/check: #9 reject explicit [N]=[init] over-fill incl [0] (both stages)
An explicit [N]T = [init] with more initializers than N silently
mis-compiled for N==0: the over-fill length-mismatch check was suppressed
when alen==0, because alen==0 doubles as the [_] infer-sentinel after
resolve_type collapses the two. So def/let [0]int=[1,2] silently resized
(cstage exit 2) or OOB-read/segfaulted (wwstage) instead of the loud
length-mismatch that [N]=[init>N] gets everywhere else.

The AST keeps the distinction the Type loses: [_] leaves the N_TARRAY
length-child NULL, an explicit [N] carries N_INTLIT. cstage adds an
is_infer_arr() helper, drops the alen>0 exemption at the over-fill check,
and gates the 4 infer-resize/no-init sites on is_infer_arr so an explicit
[0] flows to the over-fill -> loud. wwstage flips the one shared count
gate (checkarrlitfits) from declen>0 to arrtn.rhs!=nil, which also
dissolves a wwstage local-resize/module-OOB inconsistency.

[_] inference, [0]=[] empty, and [_]-no-init louding all preserved.
Under-long (count<N) stays out of scope (#10). byte-id 990-997 8/8.
test/wcc/820 table-driven; its one empty-[0] global row carves out
byte-id (pre-existing spurious-DATAW divergence, task #15).
2026-06-08 19:54:29 +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
1c87881bda wcc/check: GAP-A .cap-on-array loud-reject; .ptr-on-array ratified valid (#12)
.cap on a fixed-size array is invalid (Hare has no capacity-read; arrays
can't grow) -> both stages now loud-reject at the checker. wwstage was
silently returning frame garbage for a local array's .cap; cstage typed
it then vaguely rejected at use. Unified to one early checker reject with
an identical diagnostic both stages.

.ptr on a fixed-size array is ratified VALID: array.ptr is &A[0], a
sanctioned ww spelling divergence from Hare; see task #13. The toolchain
already relies on it in 14 backing-pointer sites. WHY-doc added at both
checker .ptr-on-array sites. The def-global .ptr cgen base-selection bug
(#11) is a separate following commit.

Valid-program asm unchanged (byte-id 990-997 8/8); w6c/w6c_ww binaries
move (checker code changed). test/wcc/817 table-driven, model 684.
2026-06-08 17:22:09 +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
c35034df3f lib/fmt: #6 formattable += int|uint — bare-int print, the types::numeric widen (both stages)
ww had narrowed Hare's formattable (types::numeric) to a lone i64 arm, so a bare int/uint was not printable: cstage CORRECTLY rejected it, wwstage leniently accepted via the #128 size-keying. Append int|uint LAST (existing tags 0-4 frozen, zero byte-id churn) so BOTH stages accept by membership — closing the #128 int-path leniency by construction. int renders via the signed i64 path; uint via strconv.u64tos unsigned (+ a rawlenu64 width twin) — i64dec would render a high-bit uint negative. Staged cut: narrower widths + full types::numeric graduate per-caller (mirrors the f32-arm precedent).

fmt is NOT embedded in the selfhost compiler tools (grep-verified: 0 fmt fn-defs in w6c/wwdump combined.ww) — no combined.ww regen, compiler binaries unchanged. Pin: table-driven test/wcc/815_fmt_int_run (bare int, high-bit uint 2^63+1 positive, i64/str regression guards, byte-id).
2026-06-08 15:28:50 +09:00
0c5482fad0 wcc/check: #11 def [_]T length-inference — stamp the def decl path, the #7 let-twin (both stages)
def xs:[_]T=arrlit was sized 0 (no DATA emitted, garbage indexed reads) on BOTH stages, byte-id-identical: #7 wired [_] length-inference only on the let decl path, never def. cstage check.c N_DEF pass-2 infers the length from the initialiser and re-points both d->type and the SK_DEF Sym (an indexed read resolves the def through its Sym); wwstage check.ww runs inferarraylen before resolvewalk. Checker-only — cgen lays the DATA correctly once the length is stamped. w6c and wwdump combined.ww regen'd (both embed the wcc checker).

Pin: table-driven test/wcc/814_def_arr_infer_len (index reads int/u8/2d + 1-elem edge + negative build-fail), teeth-proven against a reverted inference. Filed separately, not folded (rule-11): def-global .len GAP-A (#7 cgdot twin), def str-array element DATA GAP-B (#270), [0]T-vs-[_] alen==0 conflation (pre-existing in the #7 let path too).
2026-06-08 14:50:03 +09:00
ba13940b96 lib/path: #153 c3 buffer-ops — abs/isroot-widen/local/parent/split/pop/peek faithful port, table-driven @test (cstage)
Faithful ref/hare/path/buffer.ha port of the buffer-mutating ops. isroot
widened *buffer->(*buffer|str) (existing callers ptr-ident-widen); abs str-arm
strings.hasprefix(p,sepstr) (rides #154); local lifts Hare's `static let buf`
to a module-private [MAX]u8 index-write (no ww static-append, regex.ww:1177).

Three documented respells (never silent): split binds each (str|void) elem to
an ident before return (#22b); peek binds split() to a local (#48); local's
index-write. parent via appendnorm + frombytes.

Tests mirror Hare's pop() @test 1:1 (stack.ha:147) + table-driven
abs/isroot/local/parent rows off a local buffer. cstage-first; wwstage str-arms
ride deferred #146 (path stays M_WWREJECT). lib/path non-embedded (no
combined.ww regen). Completes the c3 stage of the path arc.
2026-06-08 13:37:23 +09:00
3f6b68cbf2 wcc/cgen: #154 str==-global ident operand — name(SB) base in cbinop, not (BP) garbage (cstage)
The str==/!= arm of cbinop had an N_IDENT fast-path that assumed the operand
was a local: localfind returns 0 for a module-global str, so it loaded
(BP)/8(BP) — saved-BP/retaddr garbage — into rt_streq. `p == sepstr` silently
compared garbage (returned wrong). Mirror #148's global branch at both sub-sites
(rhs/lhs): off==0 && let_islet -> LEAQ name(SB) base, load ptr/len. Distinct
per-site fast-path, not a shared choke (the by-value-global-arg family
#148/#150/#151 closes separately). cstage-only; the wwstage str== twin is #146
(-> #125 batch).

Pin test/wcc/989_strglobeq (table-driven: const+let globals, rhs+lhs ident,
==/!=, unequal + len>1 rows; teeth-proven). Surfaced by the lib/path c3
buffer-ops gate-1 oracle.
2026-06-08 13:18:59 +09:00
feae910a9b wcc: #152 let-initializer scope — defer the binding's localfind link past its own init (both stages)
A let's own name was visible during its OWN initializer: cgen prepended the
new local into the name-keyed localfind chain BEFORE emitting the init, so
`let x = f(x)` read the fresh UNINIT slot, not the outer/param x. Both-wrong-
identical silent miscompile (gate-blind byte-id). Surfaced by path
dirname/basename (was the c3-posix path->p rename).

Align to Hare (harec check.c:1439 evals the init, then scope_insert). Fix,
both stages, IDENTICAL asm: reserve the frame slot BEFORE the init emits,
link the binding's name into the localfind chain only AFTER.
- cstage cgen.c: split localoff -> localslot(reserve)+link; N_LET's 12
  case-level breaks -> goto letlink (tail links once); the inner-for break
  is preserved; the 4 fatal() arms untouched.
- wwstage cgen.ww/cgenstmt.ww: new localreserve (= localalloc minus the
  chain-link); cglet -> cgletbody(c,n,off) + a cglet wrapper that
  reserves -> calls body -> links after.

Byte-id-safe on existing code: localfind is by-name, so deferring the link
is a no-op on every non-self-shadow let (grep = 0 self-shadow sites) — 990-997
stay green. Because both stages emit identical now-correct asm, byte-id
CANNOT catch this; the pin is a RUNTIME test, teeth-proven (revert -> pin
fails). test/wcc/989_letshadow{.ww,_run.c}: param-shadow, let-in-init shadow,
rename control, arrlit self-ref.

Embedded regen: selfhost/cmd/{w6c,wwdump}/main.combined.ww. Gate: all 325
passed, byte-id 990-997 green, w6c c587f4a1 / w6c_ww 7a69f898 (deterministic).
2026-06-08 12:17:18 +09:00
3c7f1aa027 lib/path: #138 c3-posix dirname/basename — faithful posix.ha port, table-driven @test (cstage)
dirname/basename ported verbatim from ref/hare/path/posix.ha:17,37,
re-routing Hare's byte scanning to bytes.rtrim/rindex. Additive to the
c2-stack buffer core; no harness/Makefile/module-const changes.

Work-var renamed path->p to avoid Hare's param-shadowing
`let path = toutf8(path)`: ww's cgen N_LET prepends the new local into the
name-keyed localfind chain BEFORE the init is emitted, so the shadow-init
reads the fresh uninit slot — a silent miscompile, both-wrong-identical,
filed #152 and fixed independently next. drew ruled path->p a permanent
acceptable spelling (internal work-var, not API surface; avoids a
param-shadow footgun; not a rule-7 workaround). WHY-comment + #152 pointer
at both sites.

Tests: 10-row dirname/basename table (posix.ha:51-71 verbatim) — root,
no-sep, empty, trailing-sep, multi-sep all covered, expecteds traced.

C-first: cstage @test green; the str-view-vs-literal rows are #146-deferred
(wwstage byte-id, rides #125). Gate: all 324 passed, byte-id 990-997 green,
w6c/w6c_ww unchanged (lib-only additive).
2026-06-08 11:47:43 +09:00
16f47da916 lib/path: #138 c2-stack buffer port — buffer/push/appendnorm/appendlit/string/isroot/set + dot/dotdot, cstage @test
Faithful realignment of lib/path to Hare's buffer-centric API
(ref/hare/path/{stack,buffer}.ha). Replaces the old str-only path.ww
wholesale (zero consumers). Scope = stack-core: init() deferred (returns
~4KB (buffer|error), rides the #40 arc / #147); abs/dirname/basename land
in c3; extension/join dropped.

appendlit uses the #145 slice-copy-assign arm (buf.buf[lo:hi]=bs), no hand
loop. dot/dotdot are faithful module-global []u8 (D2 #148). MAX =
os.PATH_MAX-1. Divergences (size->i32 indices, frombytes, module-global
consts) cited inline.

Tests: lib/path/pathtest.ww (table-driven), wired at test/wcc/989_path_run.c;
push rows mirror stack.ha:107-111 verbatim incl the restored "/d"
intermediate. Slot 989 (overflow bucket) since 970 is taken.

C-first: cstage @test green; wwstage byte-id deferred to the #125 batch.
path classified M_WWREJECT in 989_lib_byteid (w6c_ww rejects the module-
global slice consts = #120/#29; + the #148 twin #151) — self-graduates back
to M_ID the day wwstage accepts. path moved off the 900_stdlib standalone
list (import-dependent: os.PATH_MAX def-dim + match over imported error
types; coverage at 989_path_run), per the bytes/fmt/os precedent.

Gate: all 324 passed, byte-id 990-997 green, w6c/w6c_ww unchanged
(lib-only, non-embedded).
2026-06-08 11:25:11 +09:00
26d6e2abad wcc/cgen: #148 slice-global by-value call-arg — name(SB) base for global slice ident, not (BP) garbage (cstage)
The slice-IDENT call-arg fast path pushed the header words off off(BP)
where off=localfind(name); for a module-global slice localfind→0, so it
read saved-BP/RIP/caller garbage instead of name(SB). Add the global
branch (LEAQ name(SB) base, push 16/8/0 off it) mirroring the sibling
N_SLICE arm; local path unchanged. cstage-only: wwstage checker-rejects
the shape (#120), so byte-id-safe and the twin defers to #125. Unblocks
path c2-stack (dot/dotdot are faithful module-global []u8). Sibling
structarg fast-path filed #150.
2026-06-08 10:24:48 +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
f1dcd4ecae wcc/check: #141 def-dim array as struct field — fold def in dim, shared arrayelen across 3 ww readers (both stages)
A def-dimensioned array [MAX]u8 used as a struct field was BOTH-WRONG: cstage
loud-rejected ("array length must be an integer literal"); wwstage silently
sized the dim to 0, so the next field overlapped it (frame-smash). The
reference is neither stage — it is Hare: accept + fold the def.

cstage: fold the def into the dim via eval_def_const. The fold needs def NAMES
visible when resolve_typedecl walks struct bodies, so a stub loop binds
def-name stubs (type=NULL, filled in place by the existing def loop) before
resolve_typedecl — this extends check_file's existing names-first USE+TYPEDECL
pass to DEFs; def-TYPE resolution stays in its original order, and the
kind-filtered type lookup (#225) keeps the SK_DEF stub out of type position.

wwstage: one shared arrayelen(c, rhs) (INTLIT -> uval; else evaldefconst;
else 0) routed through astsize / tinfofornode / checkarrlitfits.

Closes #13's def-dim cstage-reject half (the slice-repeat clause stays open).
Pin test/wcc/951 (5 rows incl a cross-module os.PATH_MAX dim + a ~4KB shape;
teeth = cstage loud-reject + ww frame-smash). cgen-first blocker for the
path::buffer arc (type buffer = struct{[MAX]u8, ...}).
2026-06-08 01:00:29 +09:00
620e733444 wcc/cgen: #140 !void error-singleton variant as value — skip absent void payload load, emit tag-only (cstage, align up)
A void (size-0) error-singleton type-name used as a VALUE (return / let-init /
assign / call-arg) all share the N_IDENT non-local global-value load; the load
emitted MOVQ main.<singleton>(SB),AX for a payload symbol that never exists →
w6l undefined reference. Guard TY_VOID && !let && !def at the non-local
fallthrough so nothing is emitted; the enclosing widen arm stamps the variant
tag. wwstage was already tag-only correct — this aligns cstage up to it.

Pin test/wcc/949_void_error_singleton_run.c (6 rows, teeth = link-fail
pre-fix). cgen-first blocker for the path::buffer arc (error.ha is all !void).
2026-06-08 00:18:26 +09:00
d0a1e2a221 wcc/check: #133 const-expr scalar module-global — fold+stamp let-init like def, emit DATA (both stages)
A module-global let with a const-expr init (let s = 7*6) emitted NO DATA word: cstage LINK-FAILed (undefined main.s, loud), wwstage was SILENT (no DATA, MOVSXD on stale AX, exit 152). The DEF pass-2 arm already const-folds + stamps its rhs to N_INTLIT (the #88 eval_def_const/stamp_intlit machinery); the LET pass-2 arm omitted it. Mirror it: after the assignability check, fold the rhs and stamp N_INTLIT when the plain-literal fold missed AND the const-fold succeeded. The existing DATA-emit downstream then fires (DATAW 42 + load). Both stages, byte-identical. Closes the inferred const-expr global and the typed b-ii case (let s:i64=7*6, link-fail both stages) with one stamp.

Gated on genuine int-const success (the eval return value, not the out-param): str/struct/slice/call/runtime-operand rhs short-circuit before the stamp and are left untouched — never zeroed. Non-const rhs stays on its current loud route; div-by-zero stays loud. Latent in selfhost (no const-expr module globals → 990-997 byte-id unchanged).

Pin: 947 rows C1 inferred 7*6, C2 typed b-ii, C3 def-ref K*7, C4 unary-over-binop, C5 div-by-zero loud-guard; cs==ww byte-id.
2026-06-07 22:57:06 +09:00
2138e28f65 wcc/cgen: #134 wwstage inferred unary-int scalar global — peel +/-/~ over int-literal, default to int (align up)
defaultinferredlets gained a single unary peel: an inferred module-global let s = -42 (untyped_int annotation over N_UN(+/-/~) of N_INTLIT) was wwstage SILENT — no DATAW, no MOVQ, MOVSXD on stale AX (exit 168 vs cstage 214). Peel one unary level to the int literal and default the annotation to the 8B machine word int, so the inferred decl is structurally the typed control and the existing typed-path emit/read fires (DATAW + MOVQ, byte-identical to cstage). ww-only align-up; cstage already correct on neg.

Float leg (N_FLOATLIT) carved to #135: cstage integer-types inferred float globals (MOVQ not MOVSD), so a ww-only f64 default would be cs != ww (rule-10) — needs the both-stage cstage-use-site fix. Nested unary - -42 is #136 (single-level peel). #133 (const-expr 7*6) and unary-over-nonident stay loud.

Pin: 947 neg rows (inferred + typed control), cs == ww .s byte-id.
2026-06-07 22:24:03 +09:00
74e60e89f0 wcc/cgen: #66(b-i) wwstage inferred-literal scalar global — default annotation to int, emit DATA+load (align up)
An inferred-literal scalar module-global (`let s = 42;`) was wwstage
silent-wrong: the checker stamps the annotation N_TNAME("untyped_int"),
which letscalarprim does not recognise, so letemitsize returns 0 — the
global is dropped from collectlets (no DATA emitted) AND cgident falls to
the silent module-leaf (no load), running garbage. cstage defaults
untyped_int to an 8B int before emit (DATAW + MOVQ), which is correct.

Fix (wwstage-only, align up to cstage): defaultinferredlets in cgen.ww,
called from cgfile (cgendecl.ww) before collectlets, rewrites the
annotation "untyped_int" -> "int" (8B machine word, NOT i32 — the #108
truncation trap is the opposite polarity) for a module-level N_LET whose
rhs is N_INTLIT. All three consumers (letemitsize, emitletdataw, cgident
global-read) then resolve a concrete int. cstage is untouched.

Scope: N_INTLIT only. A const-expr inferred global (`let s = 7*6;`, N_BIN)
stays on its existing path — that is a separate live cs!=ww silent
miscompile tracked as #133, out of scope here.

Pin: 947_inferred_scalar_global_run — inferred `let s=42` (42, base
wwstage garbage) + typed control, cs==ww byte-id.
2026-06-07 12:27:38 +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
abd97e6c57 lib/getopt: printusage + printhelp
Port ref/hare/getopt/getopts.ha:202-314 (printusage / _printusage / containsh /
printhelp): the two-pass io.empty width measurement + the >72-col indent rule.
Table-driven tests. cstage-only (C-first); wwstage byte-id twin owed (#125).
printsubcmds + parse() deferred (#127 / #126).
2026-06-07 06:19:13 +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
3ae24c22b8 lib/regex: #123 POSIX character classes [:class:] — charclass_map + compile/exec arms
12-entry charclass_map (str, *fn(rune) bool) + bracket [:class:] recognition
(compile) + charset_class_item predicate dispatch (exec), porting
ref/hare/regex/regex.ha:74-87/190-204/726-733. Table-driven tests cover all 12
classes (±), negation, composition, and the "No character class after '[:'"
error path.

cstage-only (C-first per the speed pivot); wwstage byte-id twin owed in the
batch-converge phase.
2026-06-07 03:10:55 +09:00
66d69537a5 wcc/cgen: #124 cross-module &fn in a const — N_DOT reloc + checker accept (both-stage)
A cross-module `&module.fn` in a const emitted no static reloc (the
const was never defined -> w6l undefined-reference, both stages) and
wwstage's checker rejected the const fn-table. #117/#119 wired the
&fn->DATAR const-data reloc for SAME-module &fn only; charclass_map
(fold-6) needs cross-module (12x &ascii.isXXX).

cgen: add the N_DOT arm to the &fn->symbol helper (node_fnptr_sym /
nodefnptr + the two ww emit sites), emitting mafn(leaf, module-ident)
-- exactly the symbol a runtime &mod.fn or a direct cross-module call
already emits. The helper is the SSoT for both the scalar (#119) and
tuple-row (#117) const-data paths, so one arm closes both.

checker: type a cross-module `&mod.fn` as `*fn(...)` in the TK_AMP arm
(the N_DOT twin of #206's N_IDENT fn-ptr synthesis, gated on a resolved
SK_FN/N_FNDECL leaf), so isassignable affirmatively accepts the const
table -- aligning wwstage UP to cstage's actual acceptance reason
rather than by abdication. The SK_FN gate keeps a non-fn `&mod.var`
from synthesizing a fn type (the one pre-existing nonfn-scalar cs!=ww
slip is N_IDENT-base, untouched and reproduces same-module).

One consumer-coupled commit (the checker accept gates wwstage cgen, so
neither half is independently testable). Narrow: slice-row + scalar
only; fixed-array (#118) and struct-field (#129) stay separate. Both
stages emit the correct cross-module symbols at the right tuple-slot
offsets -> byte-identical (990-997 green). Pin 949_xmod_fnptr_const_run
(distinct fns so a wrong reloc is caught + the SK_FN-gate axis). This
was the last fold-6 cgen blocker; charclass_map is now unblocked.
2026-06-06 23:34:46 +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
761cfa4524 wcc/cgen: #117 pin scalar-tuple-slice row (broad-arm in-suite net)
rob A-corrected: the broad TY_TUPLE arm stays; pin an accepted non-(str,*fn)
scalar-tuple slice in-suite. const [](i64,str) with TYPED 1i64 (untyped int
is ww-checker-rejected, #120); scalar-first so the i64 reads back via the
word0-correct whole-element cursor (#121: .1/word3 truncated). Distinct
values + len(t) + cs==ww byte-id lock the scalar-in-slice-backing path.
Base f8be2ae has no TY_TUPLE arm -> louds. Test-only; 17/17 ok.
2026-06-06 21:13:32 +09:00
28396f012b wcc/cgen: #117 review — cite #120 at cstage tuple arm (cs-accept/ww-reject divergence)
The #117 emit_slice_data TY_TUPLE arm fires for any foldable tuple row,
but wwstage's checker admits only the (str,*fn) shape and loud-rejects
the rest before cgen. Document the retained acceptance divergence at the
site (rule 7/8) with the #120 pointer. Comment-only, asm-neutral.
2026-06-06 20:54:39 +09:00
1b4f25ac45 wcc/cgen: #119 scalar &fn global DATA via the #117 reloc helper (both-stage) 2026-06-06 20:28:50 +09:00
942abf0482 wcc/cgen: #117 const slice-of-(str,*fn) DATA + &fn->DATAR reloc (both-stage) 2026-06-06 20:27:43 +09:00
df1928182e wcc/cgen: #117 prep — factor emit_tuple_row backing-relative (byte-neutral) 2026-06-06 20:02:32 +09:00
f8be2ae8dd wcc/cgen: #116 non-literal tuple source into a tagged box (both-stage)
cg_widen_tagged_store only handled a tuple LITERAL (N_TUPLE / cast-of-
N_TUPLE) widened into a tagged box; any addressable non-literal tuple
source -- IDENT var, INDEX tbl[i], DEREF *p -- hit the `else fatal`
("tuple-typed source shape unwired"). Both stages loud-identical
(honest, no silent miscompile). This blocked indexing a const tuple
table into a union (regex charclass_map[i] -> charset union).

Add an addressable-tuple-source arm, both stages (cgen.c +
cgenutil.ww twin). It resolves the source address via the cgplaceaddr
place-spine (covering ident/index/deref -- one mechanism, so the trio
is family-closed) and block-copies the tuple's type-table ->size bytes
into the box payload (after the 8B tag), then stamps the variant tag.
No re-slotting: a tuple's in-memory layout uses the same eslot strides
(str=24B header, *fn=8B, ...) as the box payload the literal loop
fills, so source-layout == dest-layout. The existing narrow-pack and
tag-unresolved guards stay as the honest boundary; CALL/sret tuple
sources (different receive, #68-kin) stay loud.

align-BOTH: both stages were loud (no runtime reference), and byte-id
is structurally blind to an identical-wrong emission -- so correctness
is proven by a RUNTIME read-back pin (944_nonlit_tuple_widen_run, per
shape: match-extract + assert str header + call the fn-ptr elem with
distinct fns so a stale pointer is caught). 936's old reject row
graduates to a run row. Both stages byte-identical (990-997 green).
2026-06-06 19:27:28 +09:00
da30f10f70 wcc_ww/check: #47 gap-B tuple-with-tagged case-arm variant-match (align-up)
wwstage's checker rejected a `case let t: ((void|size),(void|size),
size) =>` arm against a (tuple|error) scrutinee ("case: not a variant
of scrutinee"), while cstage accepts and runs it. typeeqast's N_TTUPLE
arm recurses per-element, but a tagged element (void|size) is
N_TTAGGED -> fell to the conservative catch-all `return false`, so the
whole tuple-compare failed. typeeqast is the sole acceptance route
(casevariantpairmatch is N_TNAME-only).

Add an N_TTAGGED arm to typeeqast, sibling of N_TTUPLE, mirroring
cstage type.c:288-300 (type_eq TY_TAGGED): position-by-position
variant compare over the tagged node's .list (direct nodes, not
.lhs-wrapped). cstage's nullable-flag check is deliberately not ported
(resolved-Type property, no ww AST analogue; moot for case-match).

A spread variant (TK_ELLIPSIS) in the .list is loud-rejected rather
than compared: a naive streq would silently accept a `...ab` case that
cstage rejects (a new cs!=ww over-accept the bare arm introduced).
Flattening the spread is deferred (#115); until then it louds, matching
cstage.

ww-only (cstage already accepts); the gap-A cgen store landed in
6a5bb3e. wwstage now accepts the b1c match and runs the full shape
byte-identical to cstage -> #47 (both gaps) closed. The deferred
full-b1c row in 944_tuple_tagged_union_run is promoted to a both-stage
runtime row. Checker change is acceptance-only/additive -> bootstrap
byte-id neutral (990-997 green).
2026-06-06 17:36:10 +09:00
6a5bb3efc9 wcc/cgen: #47 gap-A tuple-in-union tagged-element store (both-stage)
A tuple containing a tagged-union element, used as a union member
(e.g. ((void|size),(void|size),size) | error), loud-stopped in the
cgen return-store: the tuple-in-union store walk had scalar/float/
str/slice element arms but no TY_TAGGED-element arm. A PLAIN
tuple-in-union already worked -- the blocker was the tagged element.

Add the recursive two-level widen arm at both stages
(cg_widen_tagged_store / cgwidentaggedstorebp): for each tagged
element, re-enter the tagged-box store (inner tag@slot+0,
payload@slot+8) at the element's tuple-payload offset, then stamp the
outer tuple tag. Slot strides come from the type table
(roundup8(eu->size)) -- the checker already sizes the shape correctly
(tuple->size measured 40, union box 48; check.c:715-720). The
recursion descends a finite type tree (a tagged element is never a
tuple literal, so it can't re-enter the tuple arm); unsupported
deeper nesting still louds via the existing size/tag guards.

Both stages get the same arm -> byte-id (990-997 green; additive,
bootstrap-neutral). cstage runs the full b1c shape
(construct+return+match-extract) as the runtime reference; wwstage's
store rides on byte-id until gap-B. gap-B (wwstage checker
match-acceptance of the tuple-with-tagged case pattern) is a separate
commit -- wwstage still louds the match honestly at the checker.
Pin 944_tuple_tagged_union_run.
2026-06-06 16:57:42 +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
cc896bd078 wcc_ww/cgen: #55 tagged-source arg-widen into wider tagged slot (align-up)
wwstage pushargsrev treated a narrower tagged-union argument widened into
a wider tagged param slot as a concrete variant: taggedvariantindex<0
clamped the tag to 0 and pushed word0 only (deref/index/dot silent-wrong;
ident ran correct only by prefix-union tag-index luck). cstage is correct
(cg_widen_tagged_push routes src_is_tagged unconditionally); align ww UP.

Three arms in cgenutil.ww, all mirroring cstage cgen.c:
 - slot-gate the ident aistagged short-circuit so a slot-differ tagged
   ident falls to the widen path instead of the raw 2-word push;
 - route a tagged source in the widensz>0 arm through @tagscr +
   cgwidentaggedstore + push high->low (cgen.c cg_widen_tagged_push);
 - cgwidentaggedstore cursor arm (<=32B INDEX/DOT) spills by source
   width, zero-pads, and tag-remaps (cgen.c 2698-2714) — was dst-slot
   spill of stale high regs with no pad and no remap.

Same-slot tagged->tagged is byte-id-neutral by construction (empty pad +
identity remap). cstage untouched; 4 legs x {aligned, misaligned-tag}
converge ww->cs byte-identical. Pin 944_tagged_widen_arg_run.
2026-06-06 15:15:13 +09:00