Commit Graph

624 Commits

Author SHA1 Message Date
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
6d8434002d lib/encoding/base64: clear() via array-decay now that #258 lands; drop stale comment 2026-06-02 06:17:59 +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
a2659c7942 test/base64: table-driven decode_invalid; cover len%4==1/3, excess '=', mid-quad '=' 2026-06-02 05:01:02 +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
9d383288d2 lib/ww: hash-index the tinfo cache, kills O(n2) compile (perf)
tinfocachelookup walked a flat prepend-only association list on every
cache miss -> O(N) scan x O(N) calls = O(N2) (91% of all wwstage
instructions on a 5k-line input; w6c_ww ~265x slower than its C twin).

Replace the single list head with a node-ptr hash index, mirroring
sym.ww scope.buckets (rule-12): NBUCKETS_TINFO=8192 power-of-two
buckets, ptr hashed via (key>>4)&(N-1) (>>4 drops the always-zero
aligned low bits so buckets don't cluster), cnext now chains within a
bucket. First-match-in-bucket preserves the old most-recent-bind-wins
order -> identical *tinfo per node -> byte-identical asm.

cstage (cmd/wcc C) has no such cache, so this is wwstage-internal:
no emitted-asm change, no cstage-symmetry obligation. Verified
byte-identical output (baseline vs new binary, same 32k-line input)
and 52.6s -> 0.54s (~97x). combined.ww regenerated for w6c + wwdump
(only tools embedding typ.ww). test-unit (235) + smoke green.
2026-06-02 00:08:52 +09:00
db5c5b6149 lib/strconv: add itos/utos integer format (strconv-int fold-2)
Graduates the integer FORMAT side to verbatim Hare ports, completing the
round-trip whose parse half landed in fold-1, and adds the machine-word
entry points.

  - u64tos: ref/hare/strconv/utos.ha:10-42. Replaces the pre-graduation
    basedigit() helper with Hare's rune LUT (lut_upper/lut_lower), single
    static buffer + bytes.reverse, and strings.frombytes for the
    `*(&s: *str)` reinterpret (rule-9 carve-out; ww's lib/types has no
    `string` struct). basedigit deleted (now dead).
  - i64tos: ref/hare/strconv/itos.ha:10-32. Now `if (i >= 0) u64tos(i)`
    else negate-and-prefix via `u64tos((-i): u64)`. This fixes the
    i64tos-on-I64_MIN bug (cgen.ww #144): the old `n = -n; for (n > 0)`
    left n at the I64_MIN bit pattern (still negative), emitting just
    "-". The `(-i): u64` two's-complement reinterpret yields the true
    magnitude 9223372036854775808.
  - itos/utos/ztos/uptrtos: int/uint/size/uintptr 8B machine-word
    wrappers (itos.ha:52, utos.ha:62/67/72), parallel to fold-1's
    stoi/stou/stoz. The existing iN/uN width wrappers are unchanged.

Divergences documented at-site: no static assert; LUT-select + base
normalize via the existing basenum() (ww has no if-expression); explicit
copy loop for Hare's slice-assign.

Probes (drew PROBE-BEFORE-COMMIT, all green on BOTH stages):
  - i64tos(I64_MIN) == "-9223372036854775808": cstage `ww run` exit 0 +
    wwstage-compiled binary exit 0; cs==ww .s byte-identical on the real
    combined (30190 lines).
  - static `[0...]` fill + rune LUT static-init emit byte-identically
    cross-stage (isolated smoke probe + the combined byte-id).
  - frombytes (not a types::string mirror) per rule 9.

Tests: extend inttest.ww with test_u64tos[_bases] / test_i64tos[_bases]
(verbatim utos.ha:74-103 / itos.ha:54-87, flat assert sequences;
feedback_test_match_hare_source) + test_word_wrappers. I64_MIN inputs
spelled -I64_MAX-1 (proj #245: wwstage mis-lexes the 2^63 literal).

combined.ww regen: strconv is compiler-imported via fmt, so w6c +
wwdump main.combined.ww + smoke.combined.ww are regenerated.
2026-06-01 23:36:36 +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
a11785273a lib/strconv: stoi/stou/stoz machine-word int parse (strconv-int fold-1 C2)
Add the int/uint/size entry points (ref/hare/strconv/stoi.ha:53,
stou.ha:107,113). Hare clamps to types::INT_MIN/MAX, UINT_MAX, SIZE_MAX
via stoiminmax/stoumax; ww's int/uint/size are 8B machine words
(INT/UINT/SIZE limits == I64/U64 per lib/types/types.ww:30-37), so the
clamp is a no-op — the full i64/u64 range parses with no spurious
overflow. Documented at-site (the bound consts are package-private, so
inlining them would just re-encode I64/U64_MAX).

Tests: extend inttest.ww with test_stoi_stou_stoz — value path, sign,
overflow pass-through, and the no-clamp fidelity (I64_MAX/U64_MAX parse
without overflow) plus hex/bin bases through the shared parseint core.

combined.ww regen: w6c + wwdump main.combined.ww.
2026-06-01 22:27:00 +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
38a906cd9b lib/strings: add cut and rcut 2026-06-01 16:33:45 +09:00
2b893b9353 lib/ascii: add strlower_buf/strupper_buf (#11)
Restore Hare's two-tier delegation: strlower/strupper alloc a buffer
then delegate to strlower_buf/strupper_buf, which fold ASCII case
into a caller-provided buffer. Too-small buffer returns nomem via the
`let nm: nomem` value form. ref/hare/ascii/string.ha:21,43.

Regen w6c/wwdump/smoke combined.ww — all three embed lib/ascii.
2026-06-01 16:12:05 +09:00
cdb74e8a49 lib/bytes: add cut and rcut (#4)
Port bytes::cut / bytes::rcut from ref/hare/bytes/tokenize.ha:392,413.
Both return borrowed (before, after) views split on the first / last
delimiter instance; void-case yields (whole input, empty). Needle order
is ww's (u8 | []u8), matching index/rindex (bytes.ww:57/91) rather than
Hare's ([]u8 | u8).

Unblocked by #10 (wide tuple-return / sret): ([]u8, []u8) is 48B,
over-cap, returned via sret and received by the call-site destructure
the tests exercise. combined.ww amalgamations regenerated (bytes is
compiler-imported via strings).
2026-06-01 15:41:10 +09:00
d0a1cb1ca3 build: track strings/bytes/utf8 deps for w6a_ww/w6l_ww/ww_ww
w6a/w6l/ww transitively import strings (-> bytes, encoding.utf8) yet
their _ww targets listed only os/rt/time, so `make` left their canonical
binaries stale when a transitive lib source changed. w6c_ww/wwdump_ww
already list the full closure; mirror it. Surfaced by 995_self_rebuild
diverging on a bytes.ww edit (canonical not rebuilt, live rebuild was).
2026-06-01 15:02:02 +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
9cf1560392 wwstage: load-combine-store local-field compound assign (#227)
A compound assign (`-=`/`+=`) on a local field silently dropped the
operator in wwstage, storing the bare rhs. Two same-class sites in
cgenexpr.ww lacked the `n.op != TK_ASSIGN` load-combine-store guard that
the pointer-to-struct path already had: the local str/slice pseudo-field
fall-through (`view.len -= 1` stored 1) and the direct struct-local
scalar field (`p.x -= 4` stored 4). Both now load the field, push, eval
rhs, pop, combine (ADDQ/SUBQ), and store — mirroring cstage
cmd/w6c/cgen.c:3235-3264 and :3477-3502. cstage was already correct;
this aligns wwstage up. PLUSEQ/MINUSEQ only, matching cstage's switch.

This is the missing-SUBQ half of fmt's cs/ww divergence (fmt's
view.len-=1). The remaining match-label-counter offset is separate, so
777/780/781 stay STAGE_CS until that lands.

test/wcc/data/attest_pass.ww: @test check_local_field_compound covers
both sites (str pseudo-field + struct scalar), run by 910_at_test
(cstage) and 997_at_test_ww (wwstage); pre-fix the dropped op aborts via
the 1/0 idiom.
2026-06-01 03:52:06 +09:00
f8aebc045d wwstage: prefer curmod for bare-leaf value-ident in exprtype (#55, fixes #226)
exprtype's N_IDENT branch resolved a bare value-ident through the
flat-scope scopelookup, which bucket-walks and returns whichever
same-leaf symbol heads the bucket (the last-registered one). Under a
foreign curmod that binds a same-named symbol from the wrong module
and drags in its declaration's type: resolving `read` to io.read while
checking os pulled io.read's (size|eof|error) return node, whose bare
`error` then bound strconv.error instead of io.error. The mistyped
union variant made the tagged-tag remap's flatvariantidxt return -1
(correctly: the union held the wrong type), collapsing the tag to 0 —
the #226 fmt cs/ww asm divergence.

Resolve through scopelookupprefer(c.cur, c.curmod, e.str), preferring
the current module, mirroring cstage cmd/wcc/check.c:66
scope_lookup_prefer. Sibling bare-leaf sites already migrated: #56
(N_CALL callee), #53 (bare TNAME).

#226 is thereby an instance of #55, not a nominal-identity gap:
io.error is already a sound sym-cached singleton. The remaining
bare-leaf sites (N_DOT-callee leaf, varianterr, scruttype) and the
cgen-side cgident analogue are tracked separately. fmt's 777/780/781
stay STAGE_CS pending a separate spread-union residual.

test/wcc/794: cross-module bare-leaf value-ident, reject->accept
polarity (w6c_ww must accept the cstage-emitted combined); no byte-id
assertion as the minimal value-ident also trips the open cgen-side
cgident bug.
2026-06-01 02:07:59 +09:00
db9edb7c39 wwstage: classify tagged call-source by stamped result type, not callee leaf name (#211)
rhstaggedabicall keyed the tagged-vs-scalar call-source decision off the
callee result type looked up by leaf NAME (fnretlookupmod), with the
receiver variable used as the "module". A value-receiver fn-ptr field
call s.f(...) whose leaf collides with a same-named global fn then
mis-bound the global's register shape, so the source was misclassified
as scalar and widened wrong: silent cs/ww asm divergence and wrong
runtime. Read the checker-stamped N_CALL result type (src.type_)
instead, mirroring the N_DOT sister branch. cstage already reads u->ret
off the typed callee (cmd/wcc/check.c:1490) and harec selects by
interned type id, not name (ref/harec/src/types.c:714).

Graduates test/wcc/782 to STAGE_WW + byte_id.
2026-05-31 23:34:16 +09:00
1995d8e43a w6c+wwstage: zero high pad words on scalar/float widen into a >16B tagged union (#227)
cg_widen_tagged_store (cmd/w6c/cgen.c) and the wwstage twin cgwidentaggedstorebp (selfhost/cmd/wcc/cgenutil.ww) wrote only the tag (slot+0) and value (slot+8) in their scalar and float arms, leaving the high pad words (slot+16..sz) as stack garbage on the BP/let/assign/return-scratch path, which never pre-zeroes. A passthrough return or u8-reinterpret of a narrow scalar/float widened into a >16B union (fmt's field = (...formattable | *mods) is 32B via the str variant) then read that garbage. Both stages were wrong identically, so the byte-id gates stayed green while the runtime truncated; fmt's spread-union scalar widen is the first real consumer. Both arms now tail-zero slot+16..sz (gated size>16), mirroring the tagged-subset/struct tail-zeros and keeping the stages byte-identical (rule 10). Adds runtime test 793; regenerates w6c/wwdump combined.ww. fmt byte-id graduation still awaits the other residual, #226 (io.read nominal-remap).
2026-05-31 22:20:53 +09:00
3d44f742a0 wwstage: expand spread variants in match check + size them off flattened members (#209)
The wwstage checker walked a match's raw AST variant list and never expanded a ...inner spread variant, so it rejected fmt's match over field = (...formattable | *mods) ('not a variant of scrutinee'). cstage's resolve_type flattens the spread at type-build. Mirror that in the AST exhaustiveness walk (casevariantin + a recursive checkvariantcovered): when a variant resolves to N_TTAGGED via a spread, recurse into its members. Additive + spread-gated -- typeeqast / casevariantpairmatch (#13) / casecovers untouched, so non-spread matches and 990-997 byte-id are unaffected. Also size a spread N_TTAGGED off each flattened member (mirror cstage check.c), dropping the inner union tag word (field 40B to 32B). Closes the #209 CHECKER reject; full fmt-byte-id still awaits cgen cluster #226 (io.read nominal-remap) + #227 (spread-widen ABI), so fmt tests stay cstage-only with retargeted comments. Adds test 792; regenerates w6c/wwdump combined.ww.
2026-05-31 20:31:40 +09:00