Commit Graph

373 Commits

Author SHA1 Message Date
11e41ecea3 lib/ww/ast: nkname if-ladder -> switch (Wave-2 structural)
Fold the 69-arm `if (k == nkind.N_X) return "..."` ladder in nkname to a
single `switch (k)` with the terminal `return "?"` as the fall-past
default. The other ast.ww ladders stay: pr()'s kind dispatch is
side-effecting (emits output, recurses) and uses ||-grouped multi-kind
predicates, not a pure value->value mapping a switch can express.

Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/asttest.ww drives nkname over every nkind plus the out-of-band
"?" fallback, wired as 905_nkname_run (same `ww run` @test shape as
904_tok_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.

Regenerates the w6c + wwdump combined.ww amalgamations (nkname region
only).
2026-06-02 22:18:19 +09:00
e027d3eec4 lib/ww/lex: tokname if-ladder -> switch (Wave-2 structural)
Fold the ~88-arm `if (k == tkind.TK_X) return "..."` ladder in tokname
to a single `switch (k)` with the terminal `return "<?>"` as the
fall-past default. kwlookup stays an if-ladder: it dispatches on
streqn() string compares over distinct literals, which a value-switch
can't express.

Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/lex/toktest.ww drives tokname over every tkind plus the
out-of-band "<?>" fallback, and kwlookup over every keyword plus
non-keywords, wired as 904_tok_run (same `ww run` @test shape as
904_ascii_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.

Regenerates the w6c + wwdump combined.ww amalgamations (tokname region
only).
2026-06-02 21:58:39 +09:00
90479fed68 w6c+wwstage: reject untyped empty-[] alloc — require context, loud cannot-infer (#3 B', subsumes #5)
An empty `[]` carries no element type; ww gets it only from a let
annotation (the #45 retype). Both stages used to silently default the
element to u8, and in value-form positions (return / call-arg) the
lowering miscompiled — malloc(8) ignoring n, a 16B *u8|nomem where a 24B
slice was expected (#5). Now every empty alloc that isn't a
let-annotated binding fails to infer with a loud error, aligning ww DOWN
to harec (ref/harec/src/check.c:1801-1802).

Mechanism: clet / checkletassign flags the single alloc call node that a
`let x: []T =` rescues (save/restore around the init walk); the alloc
branch errors on any empty alloc that isn't that node. The #45 wide-T
retype path is kept. wwstage needs an extra not-yet-stamped guard because
resolvewalk re-types value nodes context-free after checkletassign.

Tests: negative cstage-driver 729 (table-driven: bare-let, return,
call-arg, assignment) + positive @test in attest_pass.ww exercising the
u8 and the wide-i32 (#45) paths at runtime. Both stages reject
symmetrically; byte-id verified on []u8 and []i32.
2026-06-02 18:54:35 +09:00
bec1e7d6b0 w6c+wwstage: #264 read cached operand tinfo for match-expr yield type
The wwstage checker rejected a match-bound binder used in a `yield` arm
of a match-AS-EXPRESSION (`let v = match (x) { case let p: *T => yield
*p; ... }`) with asserttyped:un/bin/index; cstage compiled it.

resolvewalk stamps the yield operand's type_ during the in-scope N_MCASE
arm walk. exprtype's N_MATCH arm then derived the match's type by
re-running exprtype on the same operand to recover a type NODE — but the
arm binder's scope is already popped, so the re-derive returned nil and
the N_UN/N_BIN/N_INDEX restamp arms overwrote the good in-scope stamp
with nil. cstage never re-runs: match_yield_type reads the operand's
cached ->type (cmd/wcc/check.c:121).

Root fix (align wwstage UP): matchyieldtype now returns a *tinfo and, at
the post-walk call, READS the operand's cached node.type_ instead of
re-running exprtype — so no operand shape can be clobbered by
construction (deref/bin/index all vanish, no per-arm guards). The
exprtype N_MATCH consumer stamps e.type_ from that tinfo directly (no
tinfofornode round-trip). The pre-walk call (checkletassign L302 /
checkretassign L303 run before the in-scope arm walk, so the operand is
nil there) keeps the nil-safe re-derive — benign and load-bearing: it
types the void-arm literal so let/return-assign has a usable node. The
re-derived node (or btype for the bare-binder idiom) is carried back via
an out-param for the assignability check and for the N_MLET/N_MASSIGN
tuple-destructure consumers (`let (a,b) = match { case let t => yield t
}`, test 945). cstage is single-pass so its else is dead; eliminating
the pre-walk call is #279.

Supersedes the narrow N_UN non-clobber guard (removed — its match
consumer is gone). @test check_match_ptr_deref extended to pin the whole
operand class (deref / bin / slice-index / deref-then-field), dual-stage
(910 + 997) with correct runtime + cs==ww byte-id. The *[N]T ptr-to-
array index variant is blocked separately by #278. Both compiler-
imported combined.ww regenerated. smoke + test-unit (242) + 994 w6c_ww
byte-id (18 corpus incl. selfhost combined.ww) green.
2026-06-02 16:22:22 +09:00
9d81ba77b7 w6c+wwstage: array global-aggregate-receive g = f() (#272 commit-2)
The caller-half of the global case: `g = mk()` into a GLOBAL array
stored only the first word — a ≤24B reg-return landed `MOVQ AX, g(SB)`
(8 of 24 bytes); a >24B sret-return hit the #220 sret-to-symbol gate
which was TY_STRUCT-only and fell through to the same truncation.

≤24B: the local aggregate-receive arm was `off != 0`-only, so a global
array fell to the scalar IDENT store. Add a global ARRAY arm — LEAQ
name(SB), DI then store the full+tail words from AX/DX/CX (an array is
never float-class, so AX/DX/CX is always the transport; no `g+8(SB)`
operand form exists). Mirrors the str/slice global arm.
>24B: add TY_ARRAY to the #220 sret-to-symbol gate (cg_sret_dest_sym /
sretdestnode) — the callee writes the whole array through RDI.

A ≤24B STRUCT global receive can be float-class (X0/X1, not AX/DX/CX),
so it is left at its pre-existing symmetric behaviour — no consumer.

949_aggret_source_run gains global_recv (c → 15) and global_recv_sret
(>24B → 22), both with per-row byte-id.
2026-06-02 15:00:55 +09:00
0d39129741 w6c+wwstage: aggregate return from any addressable source (#272 commit-1)
The N_RETURN aggregate arms gated the return source on N_IDENT ||
N_STRUCTLIT; every other aggregate rvalue (array literal, o.field N_DOT,
a[i] N_INDEX, *p deref) fell through to the scalar-AX default = a silent
8-byte truncation. Both stages emitted byte-IDENTICAL wrong asm, so the
byte-id gate could not catch it (#263 class) — the fix converges on the
runtime oracle.

Mirror the arg-side closure #271 landed: both arms (≤24B @retscr and
>24B sret) now funnel N_ARRLIT through the literal element fill and
N_DOT/N_INDEX/deref through aggarg_srcaddr + the #265/#268 whole-
aggregate copy. Type-agnostic, so struct AND array returns are closed.
A close-by-construction loud-stop (rule 7) guards any future unhandled
aggregate source from reaching the scalar default.

Closes the callee-half of (b)/(c) and the addressable siblings. The
g = mk() global-receive caller-half is commit-2.

949_aggret_source_run pins the class: array-literal / N_DOT / N_INDEX /
deref / named-ident control / >24B-sret-deref / struct-field / struct-
deref, each summing all members (full readback) with per-row byte-id.
2026-06-02 14:52:44 +09:00
42dd70dc0c w6c+wwstage: aggregate arg from any non-ident source via the closed addr machinery (#271) — close aggregate-arg family
Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).

The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).

The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.

#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.

949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
2026-06-02 14:01:03 +09:00
3c37b98164 w6c+wwstage: [N]struct literal element store (#270-1c)
`let x: [2]inner = [inner{..}, inner{..}]` left the array unpopulated:
the N_ARRLIT per-element store handled scalar/str/float ONLY, so a
struct/array/tuple element hit the multi-word-store gap and stored just
the first 8 bytes (cs0/ww0). Both stages symmetric-broken; converge on
the populated result (#263).

Fix: an aggregate element of an array literal fills each element slot
from its source — cg_structlit_fill_bp for an N_STRUCTLIT element,
word-copy for an N_IDENT element (reusing COMMIT 2's per-element copy
shape). esz is the element's natural size (cstage esub->size). cgen.c
N_ARRLIT arm + cgenstmt.ww cglet. An aggregate `...` repeat and other
element shapes hard-stop loud (rule-7).

949 rows: arrlit_structlit, arrlit_structident (8B struct, byteid=1,
full readback). All 96 pass; test-unit 241 green; smoke OK.
2026-06-02 12:54:01 +09:00
6f18f42a4a w6c+wwstage: &aggregate-array-element addressing + store/copy (#270-1)
The array-of-struct element store/copy family — one primitive (&(array
element) for an AGGREGATE element, used as address, never deref/truncate)
across three consumers. Both stages were symmetric-broken; converge on
the runtime-correct full-address/full-copy (#263).

(1a) `a[i].m[j] = v` (a:[N]struct) segfaulted: the `arr[i].field` arm
computed &a[i] then DEREF'd it (loaded the struct's first 8 bytes as a
value) for an `[N]T`-typed field → garbage base. Now an array-typed
field of an array element leaves the field ADDRESS (the #135 read-side,
applied to the array-element base). cgen.c arm + cgenexpr.ww cgdot
N_INDEX-lhs branch.

(1b) `a[i] = aggregateval` truncated the copy to an 8B MOVQ. New
aggregate (struct/array/tuple >8B) element-store branch word-copies the
element from the rhs source address (ident / N_DOT field / `*p` deref) —
the WRITE-twin of the #268 let-init loop. cgen.c N_INDEX store +
cgenexpr.ww cgassign.

(3a) `let c = x.arr[i]` (N_DOT base) / `let c = a[i][j]` (nested) dropped
the copy: the #268 let-init N_INDEX source-addr arm was N_IDENT-base-
gated. Now computes &base[idx] via cg_dotbase_addr (N_DOT field) or the
&abase[bidx] spine (nested N_IDENT-array base). cgen.c N_LET +
cgenstmt.ww cglet.

949 rows: elemfield_store, elem_struct_store, elem_arr_store,
letcopy_{dot,nest}_prim, letcopy_subarr (byteid=1); letcopy_{dot,nest}_
struct (byteid=0 — run-correct, byte-id blocked by the orthogonal
value-nested-struct frame divergence #254). All 94 pass; test-unit 241
green.
2026-06-02 12:49:25 +09:00
33bd2b1054 wwstage: nested-array outer-index esz = sub-array size (#270-2)
elemsizeofc drilled a 2D `[N][M]T` base's OUTER-index stride down to the
scalar T (the documented elemsizeof FOOTGUN: it bottoms out at the inner
prim size, 4 for [M]u32). The `direct != 8` short-circuit then returned
that scalar size, so wwstage emitted esz=$4 where cstage emits $12 (the
sub-array size, idx_eff(bt)->sub->size = sub.size*elen, type.c:121). The
runtime stayed self-consistent (write+read the same wrong stride) so it
masked until a CROSS-CELL access — a[0][j] and a[1][j] alias.

Fix: detect a nested-array element ([M]T inside [N][M]T) before the
short-circuit and return the element-array tinfo's natural .size, the
sub-array stride. wwstage-only; aligns up to cstage. w6c unchanged.

949 rows: nest2d_u32/u8/i32 (cross-cell write+readback, byte-id).
2026-06-02 12:24:43 +09:00
ebbc3f98c2 w6c+wwstage: array return-by-value via the struct-return ABI (#267 fold-2)
Wire TY_ARRAY into the existing struct-return gates so arrays ride the
same reg-class (<=24B in AX:DX:CX) / sret-class (>24B) path the struct
return ABI already emits byte-identically. No new ABI machinery.

Both stages, uniform gate-widen:
- cg_sret_retsize / sretretsize: +TY_ARRAY (natural size sub.size*len,
  the type table) -> auto-enables sret send/recv + the >24B sret N_IDENT
  word-copy + return-forward, all keyed on the shared sret SSoT.
- cgreturn <=24B reg-send: +TY_ARRAY (N_IDENT scratch word-copy ->
  AX/DX/CX). reg-class return-forward rides the default cgexpr passthrough.
- let-init / assign <=24B recv: +TY_ARRAY (AX/DX/CX sized stores).

struct_float_class stays struct-only: pure-int element arrays only; no
pure-float-array-return consumer exists today.

949 +11 rows: reg-class 8/16/24B + sret-class 32B, [N]u32 and [N]u8,
at let-init/assign/return-forward, full-member readback, + a struct-
return regression control. All cstage-run + cs==ww byte-id.
2026-06-02 12:00:58 +09:00
35b517ca3e w6c+wwstage: aggregate let-init copy from a struct-DEF global (#268 reviewer)
The fold-1b unified arm (bb2f4e1) added an N_IDENT addressable-rhs source
setup, but the two stages gated the GLOBAL case differently: cstage used
let_islet || def_isarraydef, wwstage used isletvar || deflookup (ANY def).
On a struct-typed `def` used as an aggregate-copy rhs (`let c: T = G`)
wwstage copied the whole value (correct) while cstage truncated to the 8B
scalar tail — a cs!=ww divergence (rule-10). A struct-LET global already
copies on both, so the def gap was also an internal cstage inconsistency.

Struct defs are first-class laid-out aggregates (DATA storage + field
load, #129 A.2/A.3), so converge on the correct full copy on both: add
def_isstructdef to cstage's predicate and replace wwstage's broad
deflookup with the def_is{array,struct}def pairing already held identical
in defisaddressable. 949 +2 rows (array-def + struct-def global, full
readback, byteid=1).
2026-06-02 11:37:34 +09:00
bb2f4e1dfe w6c+wwstage: aggregate let-init copy for ident-array/N_DOT/N_INDEX rhs (#268 fold-1b) — close addressable-rhs copy family
#265 fold-1 landed the deref-rhs aggregate copy as one slot→slot memcpy
loop fed from a source address in SI. fold-1b adds the remaining
addressable-rhs source-address setups, all routed into that SAME loop:

  - array IDENT `let c: [N]T = s`  — LEAQ the source slot into SI.
    Pre-fix both stages truncated to the 8B scalar tail.
  - N_DOT field `let c: A = o.i`   — cg_dotchain_addr / dotchainaddr
    (#253) lands &(o.i) in SI. Pre-fix truncated to 8B.
  - N_INDEX element `let c: A = a[i]` — the &base[i] spine (#252:
    scaled index + LEAQ base) lands the element address in SI. Pre-fix
    scalar-loaded the element address as a value → segfault.

Size (the #254 non-slot-padded ABI extent) comes from the declared let
type for every shape (lu->size / structabisize|tinfo.size), independent
of the rhs; only the per-rhs address setup differs. The deref arm
becomes one branch of the unified arm. Struct-IDENT keeps its own #32
slot-copy arm above (unchanged). With those, the whole addressable-rhs
let-init-copy family is closed by construction: struct-ident / array-
ident / deref / N_DOT / N_INDEX all full-copy, both stages byte-identical
(rule-10).

949 gains 9 full-readback rows (every member written distinct + summed,
so a partial copy fails): array-ident 16B/32B + 12B(MOVL)/11B(MOVW+MOVB)
tails; N_DOT struct-field 16B + array-field 32B + 11B-tail struct field;
N_INDEX struct element 16B/32B. The N_INDEX source array is populated
through a `*inner` to `&a[i]` (the #135/#252 store path) because the
array-of-struct element direct store (`a[i].m[j]=v` / `a[i]=s` / struct-
array literal) segfaults on a SEPARATE pre-existing bug, reported
alongside this fold. w6c+wwdump combined.ww regen (#110). 70/70 949,
test-unit 241, sizelint, smoke green.
2026-06-02 11:22:01 +09:00
8e9e28e357 lib/crypto/sha256: port SHA-256 over hash::hash; NIST test (989)
Port of ref/hare/crypto/sha256/sha256.ha — block-processed [64]u8
chunks, u32 modular arithmetic, hash::hash + io.writer surface. The
state embeds hash.hash (inline vtable at offset 0); the vtable + sum/
reset slots are wired post-construction (base64/memio convention).

u32 WRAPPING + vtable dispatch CONFIRMED CLEAN: all NIST vectors verify
byte-identical — empty, "abc", the 56-byte block-boundary case, and the
one-million-'a' multi-block stream (1000-byte chunks across many blocks,
stressing write()'s partial-block carry). cgen truncates u32 add/shift/
rotate to 32 bits correctly; no masking workaround needed.

Semantics-preserving spelling divergences (slice-copy as byte loops,
close()/digest loops) are noted at-site per CLAUDE.md rule 5/13.

ONE BEHAVIORAL DIVERGENCE, blocked on a cgen bug (flagged for ken/drew):
Hare's sum() snapshots the state (`let copy = *h`) so it is re-entrant.
That deref-copy of an array-containing struct miscompiles in ww cgen
(copied array fields come back zeroed). So sum() runs on the live state
and is SINGLE-SHOT until the cgen fix lands; every current caller does
one terminal sum(), so the digests are unaffected. Minimal repro:
  type t = struct { h: [4]u32 };
  let c: t = *(&s);   // c.h reads back wrong
A sibling bug (array return-by-value zeroes the result) was also found
and is avoided in the test's buffer-based helper. Both filed for ken.

The hash/crypto modules are dead-imported (no selfhost combined.ww
regen). 9xx test numbers are full, so the run-test shares the 989
prefix with siphash (distinct `short` name; 949_* multi-file precedent).
2026-06-02 09:44:38 +09:00
4d3f8467a8 w6c+wwstage: full-size aggregate copy for deref-rhs let-init (#265 fold-1)
A `let c: T = *p` (T a struct or array, >8B) copied no full aggregate:
cstage dropped the init entirely (c read garbage); wwstage emitted only
the scalar `MOVQ AX,off(BP)` tail (first 8 bytes). Both wrong, differently
— converge BOTH stages on a size-driven slot-to-slot memcpy: cgexpr the
deref operand to the source address in AX, MOVQ AX,SI, then a MOVQ run
plus a sized MOVL/MOVW/MOVB tail over the #254 non-slot-padded ABI extent
(lu->size / structabisize for a struct, tinfo.size for an array). Mirror
arms in cgen.c N_LET and cgenstmt.ww cglet, byte-identical (rule-10).

Unblocks sha256's faithful `let copy = *h`. The by-value aggregate RETURN
ABI (array/struct return truncates to AX) is fold-2 (#267, deferred).

949 gains 6 full-readback rows (every member written distinct + summed,
so a truncated copy fails): struct{[4]u32} 16B, struct{[8]u32} 32B via
both *(&s) and *p (sha256 shape), bare [4]u32, and non-8-mult tails
([3]u32 12B → MOVL, [11]u8 11B → MOVW+MOVB). w6c+wwdump combined.ww regen
(#110). 61/61 949, test-unit 240, sizelint, smoke green.
2026-06-02 09:31:44 +09:00
0afe4225cd w6c: materialize full tagged slot on N_IDENT-source return (#263) — cstage align-up to wwstage
cgreturn's passthrough predicate was TYPE-only (istagged && type-eq), with
no source-kind filter. It forwarded the source's AX/DX/CX unchanged, which
is correct ONLY when the source already materialized the full tagged slot
into registers — N_CALL / N_INDEX / N_DOT (the #261-broadened set). For a
tagged LOCAL ident, cgexpr loads only word0 (the tag) into AX, never the
payload into DX, so passthrough dropped the payload: `return v` of a
`(i32|void)=7i32` exited 0 instead of 7. wwstage was already correct — its
forwardtagged kind filter excludes N_IDENT, routing it through the
scratch-widen path. The runtime oracle (cstage 0, wwstage 7) proved cstage
is the bug; this aligns cstage UP.

Gate passthrough to {N_CALL,N_INDEX,N_DOT}; a tagged-ident return now falls
to the existing scratch-slot widen path (cg_widen_tagged_store tagged-subset
N_IDENT arm), byte-identical to wwstage's return scratch-widen. cstage-only
(no combined.ww regen — combined.ww embeds the unchanged wwstage source;
byte-id is blind here, the new 949 rows are the net).

test/949: tagged_ident_ret_i32 (7) + tagged_ident_ret_void (void tag
survives) + register-resident controls tagged_call_ret_ctrl /
tagged_dot_ret_ctrl (passthrough must still fire); INDEX control already
present. All dual-stage run + cs==ww byte-id.
2026-06-02 06:28:00 +09:00
16b7412003 test/949: pin nullable (*T|void) tagged-element read byte-id (#261 deviation) 2026-06-02 06:11:38 +09:00
0afc272f47 wwstage: copy full tagged-element slot for N_DOT/N_INDEX-base index read (#261)
The #259 store fix unmasked a pre-existing latent cs!=ww in the tagged-
element READ via an N_DOT base (`x.o[i]`) / chained N_INDEX base
(`m[i][j]`): wwstage materialized the element as a SCALAR one-word load +
zeroed tag where cstage copies the full tagged slot — silently dropping
the tag/payload-high word (wrong variant). Three sites all keyed off the
same N_IDENT-only gate; cstage classifies TY_TAGGED for ANY base off the
checker-stamped element type. Align wwstage UP:

- cgindex (cgenexpr.ww): the N_DOT/N_INDEX-base arm now sets
  elem_tagged/elem_slot_sz from n.type_ (the stamped element tinfo),
  mirroring cstage cgen.c:8101 — the full-slot copy arms then fire.
- rhstaggedabicall (cgenutil.ww): the N_INDEX branch reads
  typeistagged(src.type_) for any base instead of an N_IDENT-only
  structural lookup, mirroring cstage's src->type keying — fixes the
  let-init / call-arg widen-source spill.
- forwardtagged (cgenstmt.ww): the return-path passthrough gate now
  accepts N_INDEX/N_DOT tagged rhs (which cgexpr materializes into the
  tagged ABI), not just N_CALL — fixes `return x.o[i]`.

read + call-arg + return + chained 2D all close by construction (one
materialization path). cstage unchanged (pure wwstage-align-up). 949
gains 9 #261 rows (i32 + explicit-void variant per shape proves the tag
survives) and flips the two #259 read-back rows to byteid=1.
2026-06-02 06:02:56 +09:00
be23d7227a w6c+wwstage: source sub-8 value-struct ABI-size from tinfo.size at zero-init+DATAW (#254)
wwstage conflated SLOT-size (round-to-8, for frame) with ABI-size (true)
for a nested value-struct. A nested value-struct field is sized via
fieldsize() (TY_STRUCT -> ti.slotsize = 8), poisoning structabisize and
registerstruct si.totsize to 8 for a struct whose true ABI size is 4.
Two emission sites then over-sized, both SILENT cs!=ww divergences:

  D1 (local, cgenstmt.ww cglet): zsz = structabisize = 8 hit the
     `zsz == 8` zero arm (#213) -> a stray `MOVQ $0, off(BP)` cstage
     never emits (ABI 4 is sub-8 -> left uninit per the shared no-rhs
     zero-init policy).
  D2 (global, cgen.ww emitletdataw): the struct zero arm wrote
     letemitsize/si.totsize = 8 DATAW bytes; cstage cg_let_emit_size
     returns u->size = 4.

Fix sources the zero-init extent from the type table's tinfo.size
(peeling TY_NAMED) at both sites — the same value cstage reads
(cgen.c:8397 / :978). fieldsize / registerstruct / frame slot-padding
stay UNTOUCHED: moving the fix into the size helpers would shift
nested-struct field offsets and re-diverge other byte-id. Pure
wwstage-align-down; cstage cmd/w6c/cgen.c unchanged.

Test 949_valstruct_subsize_run: D1 local + D2 global over ABI sizes
1/2/4 (the whole sub-8 / non-8-multiple class), each cstage-run +
cs==ww .s byte-id; plus a >8 (16B) local+global NEGATIVE control
proving the fix didn't disable legitimate multi-word zero-init.

Regen w6c + wwdump main.combined.ww (cgen is compiler-imported, #110).
2026-06-02 05:41:34 +09:00
e92708ecda w6c+wwstage: implicit [N]T->[]T array-to-slice coercion via desugar (#258)
Hare admits an array with a defined length wherever its element slice is
expected (assign / return / call-arg / init) as a borrow; ww rejected it
everywhere (the #108(c) exclusion), so base64 worked around the gap with
explicit a[0:n] slices.

type_assignable / isassignable now admit array->slice on an exact element
match (mirror ref/harec/src/types.c:1080-1097, the SLICE-dst arm). The four
acceptance sites route through one shared helper (desugar_arrayslice /
desugararrayslice) that rewrites the array expr to the explicit full slice
arr[0:len(arr)] — an N_SLICE over the array base. cgen is untouched: the
existing slice lowering (#252/#257/#135 made array bases, incl struct-field
arrays, correct) materialises the borrow header {.ptr=&arr[0], .len=N,
.cap=N}, byte-identically in both stages.

wwstage runs no general call-arg / N_ASSIGN typecheck, so checkassign +
desugarcallargs are added solely to route those two contexts through the
shared desugar (rule-10). desugarcallargs additionally loud-rejects an
element-MISMATCH array into a []T param, scoped to that shape so wwstage's
broader call-arg leniency is untouched.

953_arraytoslice_run covers the four contexts + a borrow-alias proof + the
i32/u8 element axis (dual-stage run + cs==ww byte-id), plus mismatch-reject
rows asserting both stages refuse [4]i32 -> []u8. Regen'd w6c + wwdump
combined.ww (#110).
2026-06-02 05:31:24 +09:00
6bcb0929f8 w6c+wwstage: tagged-element indexed store via dotbaseaddr + align dotchainaddr guard (#259,#256)
#259: the tagged-union array-field indexed STORE arm computed &arr[i]
from a non-ident base (`x.o[1]=v` where o:[N](T|void)) with a plain
cgexpr(base) — the N_DOT array field auto-derefs (loads the field's
first 8 bytes AS a pointer) -> garbage dest -> SEGFAULT. Route the base
through the array-gated helper cg_dotbase_addr/dotbaseaddr (dst BX keeps
the scaled index live in AX; viaptr + chained handled by the shared
helper), mirroring #257. Symmetric both stages. This was the last
unrouted cgexpr(base) cell in the array-field-base-address family
(#135/#252/#253/#255/#257) — proof-grep of both stages now shows ZERO
unrouted base cells in the slice/decay/addr/index/store builders, so the
family is closed by construction. (The chained-ptr-field scalar/str/
float store sites at cgenexpr.ww:6489+ / cgen.c:4379+ correctly cgexpr
the pointer spine and are the #133 family, not array-field-address.)

#256: align wwstage dotchainaddr's N_IDENT non-local arm to carry
cstage cg_dotchain_addr's `let_islet || def_isstructdef` guard (here
isletvar || deflookup) instead of emitting LEAQ name(SB) unconditionally.
Unreachable on valid input (a struct-typed chain root is always local /
let-global / struct def) so zero divergent asm — never-silent ethos only.

Tests (949): store-only byte-id rows (tagged_store_own/_ptr) gate the
#259 store base-address emission cs==ww; store+readback rows
(tagged_store_*_rd) are run-only (cstage) proving the store wrote the
right slot (66/77) and no longer segfaults. byte-id on the readback rows
is blocked by an ORTHOGONAL newly-surfaced divergence in the N_DOT-base
tagged-element READ materialization (sibling of #255: wwstage loads one
word + zeroes the tag where cstage copies the full 16-byte slot) — the
store base is already byte-id; only the read-back diverges. Reported
separately for triage.

combined.ww regen'd (w6c + wwdump embed cgen).
2026-06-02 05:17:45 +09:00
4d0d3b58e6 lib/encoding/base64: Hare base64/base64url on io-streaming surface
Rewrite the buffer-based base64 placeholder as a faithful port of
ref/hare/encoding/base64/base64.ha over the just-landed io-streaming
surface (mirrors lib/encoding/hex).

Ships: std_encoding/url_encoding (module-level `def` consts; decmap
trailing 0xff run spelled out, no '...', to stay on #251 and avoid the
#250 repeat-fill sugar); the streaming encoder newencoder/encode/
encodeslice/encodestr with a padding closer wired into the inline
vtable; encodedsize/decodedsize; and decodestr as a direct in-memory
decode via decmap (the same divergence hex took for its direct path —
its return union carries errors.invalid, unconstrained by io.error).

Deferred (at-site notes): the streaming decoder newdecoder/decode_reader
(#247-sibling, blocked on #199b — io.error lacks errors.invalid).

clear() wipes the work buffers with explicit full-length slices
(`[0:len(...)]`) rather than Hare's bare-array decay (pending #258
[N]T->[]T coercion) to preserve the whole-array hygiene wipe.

base64 graduates off 900_stdlib (cross-module refs resolve only via
driver concatenation, as hex did); coverage at 984_base64_run over the
RFC 4648 §10 vectors for std and url.
2026-06-02 04:55:08 +09:00
ca8c78e97d test/949: slice-of-non-array-field call-arg guard rows (#257)
The #257 call-arg fix routes the N_SLICE base through the array-gated
cg_dotbase_addr/dotbaseaddr helper. Add the load-bearing deviation
guard: a slice of a []T field and of a str field passed straight as a
call arg must FALL THROUGH the gate to cgexpr (header .ptr load), not
take the field address. Both also exercise the N_DOT esz extension on
the fall-through arm (re-slice by element width). cs==ww byte-id.
2026-06-02 04:47:56 +09:00
0f2587d294 w6c+wwstage: struct-array-field slice as call-arg via dotbaseaddr (#257)
An inline slice of a struct `[N]T`-field passed DIRECTLY as a call
argument (`rd(x.o[lo:hi])`) materialized the slice .ptr from the field
VALUE, not its ADDRESS: the pushargs/pushargsrev N_SLICE inline builder's
non-ident else-arm did plain cgexpr(base), so the N_DOT field auto-derefs
(MOVL field,AX used as .ptr) -> callee derefs garbage -> SEGFAULT. The
let-init / assign-rhs / return / hoist-to-local contexts already routed
through the cgslice #252 choke-point; only this call-arg builder kept a
private duplicate. cs==ww both segfaulted identically pre-fix (gate-blind).

Fix (symmetric both stages):
  - route the else-arm through cg_dotbase_addr / dotbaseaddr (the cgslice
    #252 choke-point: array-field-gated, so `[]T`/str/`*T` fields fall
    through to cgexpr; chained inner `o.p.m` handled via its #253 arm);
  - extend the N_IDENT-only esz gate to N_DOT bases, taking the element
    width from the checker-stamped base->type (rule-13 type table), so
    non-u8 call-arg slices scale stride.

Before: `MOVL -8(BP),AX; PUSHQ AX` (field value as .ptr). After:
`LEAQ -8(BP),AX; PUSHQ AX` (field address). cs==ww byte-identical.

Helper note: used dotbaseaddr (not dotchainaddr as first scoped) — it is
the established cgslice choke-point and is array-field-gated, so a slice/
str-typed field base keeps the correct cgexpr header-ptr load; bare
dotchainaddr lacks that gate and would mis-emit the field address for
those. dotbaseaddr already handles the chained `o.p.m` inner via #253.

Tests: test/wcc/949 gains 6 call-arg rows (u8, i32-esz-stride, via-*struct,
chained, + hoist-to-local and bare-local-array controls), each run-
correctness AND cs==ww byte-id.

PROOF-GREP residual: the tagged-union-element indexed-STORE arm
(cgen.c:~4972 / cgenexpr.ww:~5024) is the same N_DOT-base auto-deref shape,
still unrouted in BOTH stages (symmetric, segfaults) — a distinct
consumption axis filed separately; NOT fixed here.
2026-06-02 04:38:41 +09:00
d8aaa54b41 wwstage: sign-extend signed-narrow struct-array-field element load via N_DOT base (#255)
The cgindex N_DOT-base arm set esz from the checker-stamped element
tinfo but skipped signedness, so loadopsz saw signed_elem=false and
emitted MOVL/MOVZ* (zero-extend) where cstage's fldloadop reads
signedness from the element type and emits MOVSXD/MOVSWQ/MOVSBQ. A
negative i8/i16/i32 read of `x.o[k]` (struct `[N]T` field) round-tripped
with the wrong upper bits — silent cs!=ww, byte-id-blind since bootstrap
never indexes signed-narrow struct array-fields.

Mirror the sibling N_INDEX-base arm: signed_elem = typeissigned(dt).
loadopsz already keys on (signed,sz), so this closes all three narrow
widths at once. Pure wwstage-up; cstage unchanged.

949 gains nload_i32/i16/i8 negative-read rows (run + cs==ww byte-id).
combined.ww regen'd for w6c + wwdump (the cgen embedders).
2026-06-02 04:07:42 +09:00
585ec50676 w6c+wwstage: chained-base array-field address via dotbaseaddr — close the family (#253)
cg_dotbase_addr / dotbaseaddr rejected a non-ident inner, so a chained
base (`o.p.m[i]` / `o.i.m[i]` / `o.a.b.m[i]`) fell to cgexpr(base) which
auto-derefs the array field's first 8 bytes AS a pointer -> garbage base
-> segfault (base64 fillobuf `s.enc.encmap[...]` blocker). Extend the one
helper per stage to accept a chained inner: a new cg_dotchain_addr /
dotchainaddr recovers the container base via the dot-chain spine (recurse
to &x, deref when x is a *struct, sum field offsets), keeping the same
no-AX/no-stack spill contract. dotbaseaddr then takes the pointer VALUE of
inner when viaptr, else its ADDRESS, and adds the field offset. One fix
closes every op (index r/w, addr-of, slice, compound) since all route
through the helper. Symmetric cs==ww byte-id.

test/949: +22 rows. Chained-PTR (rd/wr/addr/slice x2/compound), deeper
(value+ptr leaf links, triple-pointer exercising the internal deref),
non-u8 esz stride (i32 addr+slice), and single-level controls — all
byte-id. The chained VALUE-container arm (`o.i.m`) is run-only (byteid=0):
it needs a value nested-struct instance, which trips THREE orthogonal
pre-existing cs!=ww emission divergences (bare-let zero-init policy,
global DATAW byte count, i32 element-load opcode in the index fallback)
unrelated to #253. Run correctness proves the segfault is gone for that
cell; byte-id there awaits the separate wwstage value-nested-struct fix.
2026-06-02 03:44:49 +09:00
7e3271bf01 test/949: add non-u8 addr-of + slice-via-ptr rows (#252)
The 7-row table covered u8 addr-of (local + *struct param) and the
non-u8 stride only on the slice path. Two coverage gaps closed:

  addr_i32     &x.o[2] on a [4]i32 field, *p read -> 88. The addr-of
               complex-base arm scales the index by esz=sizeof(elem)
               independent of the base-address path; only u8 (esz=1)
               rows exercised it before. Proves IMULQ $4 stride
               composes with the dotbaseaddr LEAQ base.
  slice_ptr_u8 x.o[1:4] via a *e param. dotbaseaddr's viaptr arm
               (MOVQ (BP) deref) on the slice base was untested — all
               slice rows used a value-struct (LEAQ) base.

Both run-correct + cs==ww byte-identical.
2026-06-02 03:01:10 +09:00
5ebd9eb6db w6c+wwstage: addr-of/slice struct array-field via dotbaseaddr (#252)
Taking &x.o[i] (address-of) or slicing x.o[lo:hi] / x.o[lo:] of a
struct's [N]T-typed FIELD computed the field's VALUE as the base
address (MOVL off(BP),AX) instead of its ADDRESS (LEAQ off(BP),AX) ->
garbage pointer -> segfault. The index read/write path was fixed in
#135; this is the unwired addr-of + slice sibling — both base-address
paths fell to the generic cgexpr(base) auto-deref.

Wire the #135 cg_dotbase_addr / dotbaseaddr helper into the addr-of
N_INDEX complex-base arm and the N_SLICE base arm, symmetric on both
stages (guarded if(!dotbase) cgexpr(base)). Extend the slice element
stride (esz) and default-hi length to an N_DOT array-field base too,
read from the field's element tinfo / array length via the type table
(rule-13) — so non-u8 element slices scale correctly and s.obuf[lo:]
gets the array's element count.

cstage already derived default-hi via base->type (alen); only wwstage
needed the N_DOT default-hi arm. cs==ww byte-identical on every shape.

test/949_dotbase_addr_slice_run: 7 dual-stage rows (addr-of local +
*struct param, explicit + default-hi u8 slice, non-u8 [4]i32 stride,
bare-local control), run + cs==ww byte-id. Regen w6c/wwdump combined.ww.
2026-06-02 02:52:59 +09:00
8f85878bb2 test/951: add str->u8 reject rows at let + struct-field (#251)
The reject table exercised the non-foldable element-type branch only at
the def site; let and struct-field covered the foldable range branch
alone. Add let_str and struct_str so both reject branches (foldable
out-of-range int, non-foldable str) fire at all 3 wiring sites. A
rune>u8 over-range row stays unexpressible: the lexer caps rune escapes
at \xFF and does not decode multi-byte UTF-8 in a rune literal.
2026-06-02 02:16:13 +09:00
d56b7ca946 w6c+wwstage: narrow int/rune array-literal elements to the declared type (#251)
`let a:[4]u8=[65,66,67,68]`, `def D:[4]u8=['A',..]`, and `enc{m=[65,..]}`
rejected with "init [4]i32 not assignable to declared [4]u8": an array
literal's element type came from the elements via type_default (int-lit
-> i32, rune-lit -> rune) with no declared-element-type propagation. The
scalar path already narrows (`let c:u8='A'`); only array aggregation at
the let/def/struct-field sites #130 (test 920) left unwired did not.

Fix = the int/rune analogue of coerce_floatlit, realised as the EXISTING
#130 accept-if-fits range-check — NOT a node-type restamp. cgen drives
the array element WIDTH from the declared type at every site (cgen.c
local-let lu->sub, emit_array_data d->type), so a restamp would be dead
code (the array literal keeps its [N]i32/[N]rune node type; the cs==ww
byte-id gate confirms the bytes emit u8-wide regardless). Per element:
foldable int/rune literal -> defcastfits range-check vs declared T
(in-range accept, out-of-range REJECT loud, rule-7); non-foldable ->
type_assignable / isassignable.

cstage (check.c): wire arrlit_init_fits into clet (local let),
struct-field-init, and def-init — the three sites the #130 module-let
path already covered.

wwstage (check.ww): factor checkletassign's inline #130 block into
checkarrlitfits and call it from the let path, the def path, and a
TARGETED array-field walk in the N_STRUCTLIT arm. This also closes a
pre-existing rule-7 wwstage over-accept: the def path ran NO init
assignability check and the N_STRUCTLIT head-stamp parks field
assignability (#23), so out-of-range / str array elements silently
over-accepted (a truncating miscompile) at those two sites. The
struct-field walk is the array-field accept-if-fits ONLY — it reuses the
stable N_TSTRUCT field-list walk (astoffset precedent), isolated from
the broader parked #23 field-assignability walk.

Regenerated w6c + wwdump combined.ww (embed check.ww). New test 951
covers let/def/struct-field x int/rune accept (run + cs==ww byte-id) and
out-of-range/str reject (both stages). test-unit 237 + smoke green.
2026-06-02 01:53:57 +09:00
0fb4bae337 w6c+wwstage: store struct-literal array-field init (#249 BUG A)
A struct literal initialising an array-typed field as a local
(`e{ encmap = [..] }`) silently dropped the initializer: cg_structlit_fill
(cstage) / cgstructlitfill (wwstage) had no TY_ARRAY field arm, so the
array field fell to the generic scalar tail — cgexpr the N_ARRLIT (→ AX≈0)
then store one sized word — losing every element. cstage returned 0;
wwstage emitted byte-identical wrong code. (The GLOBAL literal-init path
is unaffected: it goes through emit_struct_lit_bytes, already correct via
#129 A.3.)

Both stages now element-wise store the N_ARRLIT at base+field_off+i*esz,
reusing the proven N_LET array-init shape (cgen.c:8467 / cgenstmt.ww:1393)
for int and float elements plus its `...` repeat fill; esz routes through
the type table (rule 13). str/slice/struct/tagged ELEMENT arrays are the
N_LET path's documented multi-word gap (cgen.c:8462) — converted from the
silent drop to a LOUD rule-7 error in both stages, not left silent.
Symmetric both stages (rule 10), byte-identical .s.

The `...` repeat in a struct-literal array field is checker-unreachable
today (the field type-check rejects `[v...]` length inference — a
separate checker gap); the arm mirrors N_LET's repeat for symmetry.

Test 949_structlit_arrfield_run: +local literal-init reads (idx 0 / last
element), cstage run + cs==ww byte-id.
2026-06-02 01:19:46 +09:00
16b519465a w6c+wwstage: read array field of a global struct (#249 BUG B)
Reading an array-typed field of a module-global struct value (`G.arr[i]`)
silently miscompiled: the N_INDEX fallback's cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) helper — the #135 sibling that computes &(s.field)
for a `[N]T` field — had no module-global-struct base arm. cstage emitted
`LEAQ (BP)` (localfind returns 0 for a global, so it read the stack frame
→ 0); wwstage's localfindnode returned nil and the fallback keyed on the
FIELD name, so it returned false and the caller's cgexpr(N_DOT) loaded the
field VALUE as a pointer → SEGFAULT. The .data was already correct
(emit_struct_lit_bytes #129 A.3); only the READ base address was wrong.

Both stages now emit `LEAQ name(SB) (+ ADDQ field_off)` for a global
value-struct base, mirroring the scalar global-field read (cgen.c:7532);
const globals resolve via def_isstructdef. Symmetric both stages (rule
10), byte-identical .s. Unblocks base64's `const std_encoding.encmap[i]`
reads (#22).

Test 949_structlit_arrfield_run: global `let`/`def` struct array-field
read, cstage run + cs==ww byte-id.
2026-06-02 01:12:40 +09:00
e3f49234f9 lib/encoding/hex: align to Hare io-streaming surface
The old buffer surface (encodedsize/decodedsize + encode(dst,src) i32 +
decode(dst,src) (i32|invalid)) does not exist in Hare — it predates the
#94 io vtable and mis-cited hex.ha:175 while implementing a different
signature. Replace it with Hare's real surface
(ref/hare/encoding/hex/hex.ha):

  - newencoder(out: io.handle) (:28) — write-only encoder stream.
  - encode(out: io.handle, in) (size | io.error) (:91).
  - encodestr(in) str (:68).
  - decodestr(s) ([]u8 | errors.invalid) (:175).

Divergences (documented at-site):

  - The streaming DECODER (newdecoder/decode_reader, :120,:129) is
    DEFERRED to #247, blocked on #199b: Hare's decode_reader returns
    errors::invalid, which fits Hare's io::error (spreads
    ...errors::error). ww's io.error (lib/io/types.ww:55-62) does not
    carry errors.invalid, and io.read's (size|eof|error) can't propagate
    it, so a hex decoder *stream* can't faithfully report invalid hex
    through io.read yet. decodestr ships as a direct transform meanwhile.
  - nomem dropped from encodestr/decodestr returns (ww memio.dynamic has
    no failure path — same memio.string rule-9 carve-out, memio.ww:208).
  - The local hex.invalid type is deleted in favor of errors.invalid
    (that was the original divergence).
  - encode uses a single io.write rather than Hare's io::writeall (ww has
    none — fmt.fprint:498-501: callers drive write-all over raw io.write;
    encode_writer is whole-slice so a single write is equivalent).
  - dump (:212) deferred: ww has no default-arg support and fmt's
    formattable lacks u64 (#209), so the address column can't be ported
    faithfully yet.

hex is now import-bearing, so it moves off the 900_stdlib standalone-
compile list (like fmt/os/strings/bufio/bytes/errors before it); coverage
stays at 979_hex_run.c. The stale "mirrors lib/encoding/hex.encode"
comments in lib/encoding/utf8/utf8.ww are updated, which regenerates the
6 selfhost combined.ww (5 cmd + test/smoke) (comment-only, byte-id-neutral).
2026-06-02 00:40:04 +09:00
dbc169a025 wwstage: reject error-type vs int comparison (#246)
cstage cbinop routes every comparison through unify_arith
(cmd/wcc/check.c:952), which loud-rejects an error-typed operand
paired with a differing type (e.g. strconv.invalid != i32). wwstage
binoptype returned bool for comparisons without any unify step, so it
silently accepted a program cstage rejects -- a rule-10 break (align
the leaner-but-leniner wwstage DOWN to cstage).

Scope the rejection to an error operand (varianterr) mismatched with
the other (typeeqast) so the broad differing-types diagnostic -- whose
typeeqast-vs-cstage-type_eq asymmetry risk could reject valid bootstrap
code -- stays out of wwstage. Covers the whole comparison family
(EQ/NEQ/LT/LE/GT/GE), all of which cstage routes through unify_arith.

Found by impl-strconv3 writing the strconv test. Gate-blind: the
bootstrap never compares an error type to an int, so byte-id stayed
green while the stages disagreed on what's a valid program.

test/wcc/949_errtype_compare.c: both drivers reject invalid !=/==/< i32
(K_BUILDERR); same-error-type and plain-int compares still accept on
both stages + cs==ww byte-id (K_RUN). 12/12.
2026-06-01 23:10:46 +09:00
6a5cdbd779 lib/strconv: parseint sign+overflow core; stoi64/stou64 fidelity (strconv-int fold-1 C1)
Port ref/hare/strconv/stou.ha:8-65 (rune_to_integer + parseint) and the
stoi64/stou64 fidelity rewrite (stoi.ha:9-17, stou.ha:70-76) over the old
digval loop. parseint is the shared sign + per-digit + multiply-overflow
core returning ((bool, u64) | invalid | overflow); stoi64/stou64 destructure
its `(sign, u)` tuple-in-union result — the shape unblocked by #242/#241.

Wins over the prior ad-hoc parse: leading '+' accepted, '-' on stou64 is
overflow (not silently dropped), wraparound overflow detection (n < old),
and the invalid payload carries the offending byte index per Hare.

Tests: lib/strconv/test/inttest.ww (run via test/wcc/922_strconv_int_run.c),
inline per-case checks mirroring Hare's assert sequences stoi.ha:56-86 /
stou.ha:116-138 (Hare's strconv int tests are flat sequences, not row
tables; feedback_test_match_hare_source). Covers valid dec/hex/oct/bin,
+/- sign, invalid+index, overflow, and U64_MAX / I64_MAX / I64_MIN
boundaries. The I64_MIN expectation is spelled -I64_MAX-1 (Hare's own
two's-complement identity) to isolate the test from #245 (wwstage mis-lexes
the literal 9223372036854775808 -> 0); the parse INPUT is unaffected and
yields the correct value on both stages.

combined.ww regen: strconv is compiler-imported (via fmt), so w6c +
wwdump main.combined.ww are regenerated.
2026-06-01 22:26:53 +09:00
5d023c0ef0 w6c+wwstage: cgexpr materializes tuple rvalues + unwrap-shift for tuple-payload destructure (#241)
cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:

  - an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
  - a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
    `let q = t`), leaving DX/CX stale;
  - the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
    stranding word1 in CX (the scalar/str success ABI).

Fix (both stages, byte-identical per rule 10):

  - cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
    cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
    N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
    (cg_tuple_slot_to_cursor / cgtupleslottocursor);
  - the ?/! unwrap shifts a tuple success payload down one integer reg past
    the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
    loud-stopping a float/slice/str payload element (the SysV per-eightbyte
    tagged-tuple-payload classification is #243);
  - wwstage's checker recovers the popped match-arm binder type for a
    `yield <binder>` operand (matchyieldtype's scope-free fallback to the
    arm's declared type), so the destructured binders stamp — cstage reads
    the operand's already-stamped ->type, wwstage caches only a tinfo.

Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.

NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.

Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
2026-06-01 21:27:49 +09:00
6fc85f9aaf w6c+wwstage: construct + bind tuple-in-union payload (#242)
A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):

(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
    shuffle, which ZEROED tag + payload — the operands were never packed.
    Route the tuple variant through the scratch-slot widen path; add a
    TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
    union payload at the register-ABI 8B stride + sets the variant tag.

(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
    bound union payload) left the un-annotated binders UNTYPED, so the bin
    node reading them was untyped -> asserttyped abort. The element-type
    distribution only fired for an N_CALL rhs. Consume the rhs tuple type
    for ANY rhs (mirror cstage check.c:2017).

(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
    left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
    IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
    Copy each element from the ident's slot at the 8B stride.

Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.

Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:

  - a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
    caught by the 8+payload > slot-size guard (the eightbyte tuple
    classification is #243);

  - a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
    `7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
    -1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
    so flatvariantidxt WOULD resolve the variant — a program cstage rejects
    but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
    mirrors cstage's CONDITION (a bare-literal element), not its -1
    mechanism, with an explicit guard that aligns the richer side DOWN. Lift
    BOTH guards together when #241 lands cstage literal typing -> symmetric
    accept.

Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
2026-06-01 20:24:43 +09:00
b79f005489 w6c+wwstage: agree on mixed-scalar tuple sret layout (#240)
An over-cap tuple mixing a scalar with slices/str (e.g. (int,[]u8,str),
56B) laid out differently in the two stages — gate-blind, since no
bootstrap path returns such a tuple. Two silent cs!=ww bugs, one per
ABI side:

  - callee SEND (cstage cgen.c N_RETURN over-cap-tuple arm): foff
    advanced by the LITERAL expression's type size. A bare int literal
    element is stamped TY_UNTYPED_INT (size 0), so `e->type->size`
    added 0 for a leading scalar — the next element clobbered it at
    offset 0 and every trailing element packed 8 bytes low. wwstage
    already sized from the return-type tuple (c.fnret.list), so the
    callee frames diverged. Fix: size foff from cg_ret_type's tuple
    params (rule-13 type table), aligning cstage to wwstage and to the
    t.N reader's f->offset.

  - caller RECEIVE (wwstage cgenstmt.ww cglet N_TTUPLE arm): the
    in-cap register tuple-receive branch had no capacity gate, so a
    56B over-cap tuple was received via AX/DX/CX/R8 (+ R8 fill)
    instead of from the sret dest the callee wrote. cstage gates the
    twin branch on `sz == 16 || sz == 32` and falls over-cap tuples
    through to the sret receive. Fix: add the same size gate to
    wwstage, aligning it to cstage.

Both stages now emit byte-identical asm and the value round-trips.
Regen w6c + wwdump combined.ww (cgenstmt embeds in both).

New 940_mixed_scalar_tuple_sret_run: leading/trailing/middle scalar
shapes, annotated + inferred let, each self-asserting every element
(scalar direct, slice/str via len) — both drivers exit 0 + cs==ww
byte-id (12/12).
2026-06-01 19:01:58 +09:00
6acddc3a82 w6c+wwstage: len() of tuple-element slice reads .len not .ptr (#235)
len() special-cased only a plain N_IDENT slice operand (load .len at
BP+off+8) and an array operand (fold $alen); every other shape fell back
to a bare cgexpr(operand), which for a slice leaves AX=.ptr. A tuple-
element read (t.N) loads only AX=.ptr, so len(t.N) on a slice/str tuple
element returned the slice's .ptr word AS its length — a silent
miscompile, gate-blind because the bootstrap never does len() on a
slice-typed tuple element (sibling of the #234/#237 tuple-sret cluster).

Both stages: detect a slice/str tuple-element len() operand and load the
element's .len word directly at BP + element_off + 8, mirroring the
N_IDENT slice arm and the tuple-field-offset walk (element_off sums
preceding element sizes through the type table). Byte-identical asm
(rule 10). The separate tuple-element-read full-header gap is #238; a
leading-scalar mixed-tuple has its own pre-existing sret-layout cs/ww
divergence, filed apart from #235.

Test 903_tuple_elem_slice_len_run: 4 slice/str-only tuple rows (two/
three slices, str+slice, slice+str; distinct lengths), build+run both
drivers + cs==ww byte-id. 12/12 ok.
2026-06-01 18:23:29 +09:00
20fe5419d2 w6c+wwstage: store over-cap tuple sret into local field/index (#234)
The STORE-twin of the Fold-B over-cap-tuple sret RECEIVE (a937d67). Fold B
wired single-var-let / destructure / reassign / return-forward to receive a
> 4-eightbyte (sret) tuple-returning call, but a FIELD or INDEXED-lvalue
dest stayed unwired: the store dropped the callee's sret body (a truncated
MOVQ through a stale RDI) — a silent miscompile, gate-blind because the
bootstrap never field-stores a wide tuple.

Per Rob's ruling A (one class, one commit): convert the silent miscompile
into either a CORRECT store or a LOUD stop, never a fall-through.

  - cstage cmd/w6c/cgen.c: the struct-field N_DOT store and the N_INDEX
    lvalue store each gain an arm keyed on cg_sret_retsize(dest) > 0 &&
    rhs == N_CALL. A LOCAL dest (BP-relative, not via_ptr / global) sets
    cg_sret_dest_off so the callee's hidden RDI writes the WHOLE tuple
    straight into the slot — field: boff + foff; indexed: boff + cidx*esz
    (a CONSTANT index into a local value array, the only indexed form whose
    dest is a static BP offset). Every other dest fatals "#234-tail".
  - wwstage selfhost/cmd/wcc/cgenexpr.ww: symmetric (rule 10). The direct
    struct-local field branch sets c.sretdestoff = lc.off + fi.foff; the
    via_ptr branch, the global branch, and the N_INDEX arm hard-stop loud
    with the same #234-tail diagnostic. The field branches key on
    sretretsize(fi.tnode) > 0 (fi.tnode is a real type-AST node). The
    N_INDEX arm keys its ENTRY on callsretsize(c, n.rhs) > 0 — the
    callee-return-type SSoT (cgenutil.ww) the receive sites use — NOT on
    sretretsize(elemtn): elemtn is only a type node for an N_IDENT base, a
    VALUE node for an N_DOT base (`s.arr[i]`) / chained (`a[i][k]`), which
    fell to sretretsize=0 and let those forms drop SILENTLY through to the
    truncating store. The callee return type equals the dest-element type
    (checker-guaranteed), so the verdict is byte-identical to cstage's
    cg_sret_retsize, and the base-shape split then loud-stops every
    non-local-array form, base-kind-independent.

Deferred (#234-tail): a via_ptr field (`p.f`), a global field (`g.f`), an
N_DOT-base index (`s.arr[i]`), a chained index (`a[i][k]`), and a runtime /
slice / pointer index all need a runtime RDI-pointer dest, which
cg_sret_dest_off (BP-relative only) can't express — they hard-error loud
(rule 7), never a truncating store.

Depends on #237 (committed first): the wwstage struct-field slot for a
tuple field is only correctly sized with that fix, so the struct-field arm
is byte-id-symmetric here.

Test 940: indexed-on-local and local-struct-field rows RUN on both stages
(exit 0) AND assert cs==ww byte-id; readback via a raw pointer
(`(&dest):*int; p[i]`) since a tuple-element read `dest.N` is a separate gap
(#238). Builderr rows assert the via_ptr / global / runtime-index /
N_DOT-base / chained-index forms loud-stop with #234-tail on BOTH drivers
(the N_DOT-base + chained rows are the regression witnesses for the wwstage
silent-store gap closed by the callsretsize re-key). The bootstrap exercises
no such store, so the w6c/wwdump combined amalgams regen with no asm change
(byte-id-neutral bootstrap; the new hard-error never fires self-compiling).
2026-06-01 17:52:42 +09:00
93d8ece740 wwstage: size struct tuple-field slot via fieldsize (#237)
The wwstage checker `fieldslotsize` (check.ww) summed each struct field's
SLOT width to stamp the enclosing struct's tinfo.slotsize, but had no
TY_TUPLE arm — a tuple-typed field fell through to the 8B default. So
`struct { f: ([]u8,[]u8) }` stamped slotsize=8 while size=48 (the natural
element sum, correct). A `let s: S` slot is allocated off ti.slotsize
(cgenutil.ww slotsize), so wwstage reserved an 8-byte frame slot for a
48-byte struct: a SILENT stack-corrupting miscompile.

cstage has no size/slotsize split — it sizes the field at f->type->size=48
throughout — so the stages diverged on the emitted frame ($16 wwstage vs
$64 cstage), invisible to a cstage-only check and caught only by cs==ww
byte-id (rule 10).

Add the TY_TUPLE arm (return the tuple's own slotsize, the per-element slot
sum already stamped at the N_TTUPLE arm with slices at 24 each). This
aligns the checker's field-slotsize with cgenutil.ww fieldsize, which
already returns the tuple's natural size (48). The stale comment claiming
"TY_TUPLE inside a struct currently defaults to 8 in cgenutil" is removed —
fieldsize stopped defaulting to 8 at the 2026-05-23 review.

Test 930 pins cs==ww .s byte-id for a struct with a tuple field (with and
without a leading scalar field, foff 0 and !=0); pure frame-size gate, no
runtime — the divergence is fully visible in the emitted assembly. No
selfhost source has a tuple-typed struct field, so the w6c/wwdump combined
amalgams regen with no asm change (byte-id-neutral bootstrap).
2026-06-01 17:34:23 +09:00
a6923a2873 test: parallelise Phase-2 byte-id gates — reader group + serial writer tail (#19)
Phase-2 (950/990-997) was blanket-serial only because ww_ww writes build intermediates next to source (#15), so gates race on selfhost/cmd/<tool>/main.* stems. Per-gate FS-footprint audit (ken): the reader group {990,991,992,994,996,997} writes only /tmp or disjoint tracked stems (smoke/mandelbrot) — parallelise race-free via the Phase-1 xargs -P machinery; the source-tree writers {993,995,950} stay a serial tail; combined_ww_fresh last. Verdict set identical (byte-id-neutral — parallelism alters scheduling, not emitted bytes); 2 stable green runs (237 each). make test ~459s -> ~390s.
2026-06-01 14:36:03 +09:00
befb042647 build+test: -j/ccache build, drop 990 dup compile, add make smoke (#19)
Test-speed "immediate wins" from task #19 (build/test-infra only, no
compiler/cgen change — byte-id-neutral; all 237 pass, 950/990-997 +
combined_ww_fresh unchanged).

#1 Parallel build + ccache. MAKEFLAGS += -j$(NPROC) by default: the
C-compile DAG and the five wwstage builds write disjoint outputs (each
.o distinct; each wwstage tool's side files land at its own
selfhost/cmd/<tool>/main.* stem), so -j is order-independent.
test/run's Phase-2 byte-id gates are a single serial recipe that -j
does not reach. CC is wrapped with ccache when present (content-
addressed, byte-identical to plain cc); falls back to bare $(CC).

#2 Kill 990's duplicate ww1->ww2 compile. probe_ww1_to_ww2 recompiled
main.combined.ww (~70s) to assert the ww2 binary is executable — but
probe_bootstrap_fixed_point already compiles ww1->ww2, assembles,
links, and *runs* ww2 to produce ww3, so executability is proven and
the byte-id assertions (ww2.s==ww3.s, ww2==ww3) are untouched. Drop
the redundant probe. make test ~8:30 -> 7:39.

Add `make smoke [FIXTURE=x.ww]`: inner-loop cross-stage byte-id check
(cstage w6c vs wwstage w6c_ww .s diff) on a small self-contained
fixture, seconds. Catches cs!=ww emission divergence per fold; NOT a
substitute for the full 990-997 gate before landing a cgen/ABI fold.
2026-06-01 14:05:32 +09:00
a937d67377 w6c+wwstage: receive over-cap tuple sret returns at the call site (#10 Fold B)
Fold A made the CALLEE emit an over-capacity tuple return (> 4 GP or > 2
SSE eightbytes) via sret, but every receive site stayed loud-stopped, so
such a fn was not yet usefully callable. Fold B wires the call/receive end
by aligning every receive gate UP to the shared cg_sret_retsize() /
callsretsize() > 0 predicate (never a kind), per Rob's (B) ruling:

  - single-var-let  `let t = f();`      cstage gate generalised from
        TY_STRUCT&&>24 to cg_sret_retsize(lt)>0; the let's slot IS the
        sret dest, the callee writes the whole tuple there, t.0/t.1 read
        by offset. wwstage already keyed callsretsize (verified).
  - N_ASSIGN-ident  `t = f();`          same generalisation; global arm
        kept TY_STRUCT-only (a tuple-global has no sret-to-symbol path in
        either stage). wwstage grows a tuple-local arm (rettupleof gates
        it apart from the >24B-struct recv, which keeps its own path).
  - destructure     `let (a,b) = f();` and `a,b = f();` — the genuinely
        new wiring: the callee sret's into the @sretscr discard slot, then
        a copy-out loop moves each element to its binding at the SAME
        packed offset the SEND wrote (foff += element size), each at its
        natural width (#169); a `_` binding skips its store but advances
        foff. Both stages, byte-identical.
  - return-forward  `return f();`        cstage forward gate generalised
        to the predicate, reusing cg_sret_forward verbatim. wwstage
        already keyed sretretsize (verified).

The escape boundary stays loud: arg-pass `g(f())` fatals identically in
both stages (tuple arg exceeds return-cursor ABI capacity).

Test 799 is the runtime net Fold A deferred (byte-id is blind to a
SEND/RECEIVE layout mismatch): the bytes.cut-shaped ([]u8,[]u8) round-trip
over destructure / single-var-let / reassign / return-forward, each both
RUN under cstage and asserted cs==ww byte-identical. Tests 945 (row F)
and 956 (f64x3) flip from asserting the old over-cap loud-stop to
asserting the now-working sret round-trip. combined.ww amalgams (w6c +
wwdump embed the wcc cgen) regenerated. Unblocks #4 bytes.cut/rcut.
2026-06-01 13:36:42 +09:00
19e6b68d03 w6c+wwstage: emit over-cap tuple return via sret callee-side (#10 Fold A)
A tuple return whose SysV register-return footprint exceeds the caps
(> 4 integer eightbytes or > 2 SSE eightbytes) previously LOUD-STOPPED
at the N_RETURN SEND. Fold A makes the CALLEE emit such a return through
the existing >24B-struct sret skeleton:

  - classifier (cg_sret_retsize / sretretsize) grows a TY_TUPLE arm:
    walk the element footprint over the SAME caps the SEND uses, and
    return the tuple's natural total size (type table) when over-cap,
    else 0. The gp/sse caps are factored to a single shared SSoT
    (TUPLE_GPCAP / TUPLE_SSECAP — cgen.c macros in cstage, cgen.ww defs
    in wwstage) consumed by the classifier AND every emit/receive site
    (the SEND, the destructure guards, the cgcall arg guard) — so
    classify and emit can't disagree in either stage.
  - the SEND replaces the loud-stop with a write-through: cgexpr each
    element, store it through *(@sretarg) at its packed layout offset
    (the t.0/t.1 positional layout), each at its natural width so a
    narrow tail stores MOVL/MOVB not an over-MOVQ (#169); the dest base
    reloads into DX each step since a wide element clobbers AX/BX/CX.
    Then the existing struct-sret epilogue (MOVQ @sretarg->AX; ret).
  - the prologue already wires @sretarg when the classifier is nonzero.

The CALL/receive side is deliberately untouched: the N_MLET/N_MASSIGN
destructure loud-stops stay, so an over-cap tuple return is not yet
usefully callable. The end-to-end round-trip arrives with Fold B (#10-B).

Symmetric cstage (cmd/w6c/cgen.c) + wwstage (cgen.ww / cgenstmt.ww /
cgenutil.ww); combined.ww amalgams regenerated. Test 798 asserts the
callee now COMPILES (no loud-stop) and w6c vs w6c_ww .s byte-identical
across all-wide, str, narrow-tail, and float-over-cap shapes; no runtime
row (uncallable until Fold B). All 236 pass incl. 990-997 byte-id.
2026-06-01 11:53:06 +09:00
fb62aa38f1 w6c+wwstage: emit global str/slice len via .len field load (#231)
len(str-or-slice-global) was wrong in both stages, differently. cstage's
len() arm did a BP-relative slot load; localfind returns 0 for a global,
so it emitted `MOVQ 8(BP),AX` — a bogus stack slot. wwstage's arm only
handled locals; a global fell through to cgexpr, which loads the whole
header and leaves AX=.ptr, not .len.

Both stages now emit the global .len load — LEAQ name(SB),CX; MOVQ
8(CX),AX (.len field; header is ptr@0/len@8/cap@16). The LEAQ symbol
routes through the post-#1 value mangle (cstage mahint c->cur_mod,
wwstage emitsymnamehint c.curmod), not a raw name, so a private
same-module same-leaf str global can't re-open the #1 collision.

The local-str case is unchanged (control). Slice-global rows wait on
#233 (cstage rejects `let g: []u8 = [...]` init); the str global proves
the path. Byte-id-blind, so a committed runtime + cs==ww test (797) is
the net.
2026-06-01 09:59:32 +09:00
80e7ab7cf3 w6c+wwstage: qualify N_DOT-base + addr-of value-global by dotted module (#229)
The cross-module dotted value-global read (`aa.v`) and addr-of (`&aa.v`)
still mangled their symbol via the non-preferring leaf lookup (cstage
masym / wwstage emitsymname), so they emitted `LEAQ main.v(SB)` — the
WRONG module's same-leaf global — returning 99 instead of 7. #1 fixed the
DATA def-site and the bare-ident load; these four dotted LOAD/addr sites
were the residual.

Thread the dotted module name (the `m` in `m.x`) — n->lhs->str /
opnd->lhs->str / lhs.str / basenm — into the existing value mangle
(cstage mahint, wwstage emitsymnamehint), the same polarity the TY_FN
branch beside each site already uses via mafn/emitfnname. The addr-of
spine-walk for a bare-root `&global.field` is a different shape and is
left untouched.

Byte-id-blind (the bootstrap has no colliding leaves), so a committed
runtime + cs==ww test (796) is the net.
2026-06-01 09:57:45 +09:00
07fed80fab lib/ascii: add strlower/strupper
Port ref/hare/ascii/string.ha strlower/strupper as the allocating entry
points: byte-wise ASCII case fold, equivalent to Hare's rune fold since
case-folding only touches bytes <0x80 and every UTF-8 multibyte byte is
>=0x80 (passes through unchanged, length-preserving). nomem arises only
from the allocation's `?`.

strlower_buf/strupper_buf are deferred: ww has no nomem-value form or
capacity-bounded static-append to express Hare's too-small-buffer path
(#230); restore the two-tier delegation when those land.

Divergence (rule 7): the empty-input fast path returns a nil/0 str
because ww's alloc([], 0) routes through nomem, whereas Hare allocs a
zero-length buffer and zero-loops; documented at the bypass site.

Test vectors mirror Hare's @test (ABC/abc/[[[/こ/empty/aB1z). Adds
lib/ascii/asciitest.ww + test/wcc/904_ascii_run.c (registered in the
Makefile TESTS list and a build rule). Regenerates the ascii-embedding
selfhost combined.ww amalgams (#110 freshness); the wwdump amalgam also
reorders the ascii block after strings to satisfy the new import edge.
2026-06-01 09:24:59 +09:00
8481a05c3a w6c+wwstage: qualify cross-module value-global by defining module (#1)
A bare cross-module value-global load mis-qualified its symbol: cgen
mangled it with curmod via a non-preferring leaf lookup, so an exported
`let v` in module aa emitted both its DATA storage AND its bare-load as
main.v, colliding with main's private v. aa.getv() returned 99, not 7.
Functions were already correct (they thread a cur_mod hint via mafn /
emitfnname); value-globals did not. Both stages emitted IDENTICAL wrong
asm, so the byte-id gate was blind to it; combined.ww (frontend) is clean
-- the bug is purely in cgen. This is the cgen residual of #55 (#1 cgen
value-global module-qualifier).

Fix, symmetric in cmd/w6c/cgen.c + selfhost/cmd/wcc/{cgen,cgenexpr}.ww:
reference-site mangle uses the resolved module (curmod-prefer for bare
idents); definition/DATA-site mangle uses the decl's own module
(d->module / d.nmod) -- threaded per-site the way fns already do, via
mahint / emitsymnamehint. The fn-mangle path is left byte-for-byte
untouched.

Deviation from the signed-off spec (ratified by rob-pike after this
finding): the spec prescribed reusing the fn lookup (mod_mangle_fn /
modlookupforfn), but its first-match fallback mis-fires for value-
globals -- mod_collect export-skips exported non-fn decls (cgen.c:1059)
to keep their bare-name data ABI, so an exported leaf is absent from the
module map and the fallback grabs another module's same-leaf private
global. The value path therefore uses a distinct exact-(name,module)-or-
bare lookup (mod_lookup_value / modlookupvalue): mangle only on an exact
match, else stay bare. Byte-id-neutral on all existing single-owner code;
exported globals stay bare (ABI preserved), private stay module-qualified.

Honest boundary (rule 7): if two modules BOTH export the same value leaf,
both stay bare and the linker sees a duplicate symbol -- a correct, loud,
link-time ABI clash (like C), NOT a silent miscompile; left to the
linker, not papered over with a cgen heuristic.

Test: test/wcc/795_xmod_valglobal_run.c -- runtime (the exported global
read returns its own value, not the colliding private one) + cs==ww
byte-id, across i32-let / def-const / f64-let. Sibling to the checker
test 794_xmod_ident_prefer, which deliberately omitted byte-id because
this cgen bug diverged the asm independently.
2026-06-01 09:01:56 +09:00
954badd28f wwstage: name vararg-gather slots via mklabel; graduate fmt 777/780/781 (#227)
wwstage named variadic-gather slots with mkvarargname off a separate
varargseq counter, never bumping the shared labelseq that names match
labels. cstage names them via mklabel (cmd/w6c/cgen.c:5427,5431), which
advances labelseq twice per gather. So by the time main.main reached its
`match (wr)`, wwstage's match-label counter ran two behind cstage's
(_4/_5/_6 vs _6/_7/_8) — a pure label-numbering divergence that kept fmt
cs/ww byte-id failing.

Drop varargseq and the mkvarargname helper; call the existing mklabel
for the two gather slots, matching cstage's order (vararg_d only when
nvar>0, vararg_sl always). The slot names are locals-table keys only —
they resolve to BP offsets and never reach the asm — so only the
labelseq advance is observable, which is exactly what realigns the
downstream match labels. cstage untouched (align wwstage up).

This was the match-label half of fmt's divergence; with the earlier
compound-assign fix it completes fmt byte-identity. Graduate
777/780/781 to STAGE_CS|STAGE_WW with byte_id, and drop the now-stale
(void)asm_byte_identical guard in 777.
2026-06-01 05:04:46 +09:00