Commit Graph

423 Commits

Author SHA1 Message Date
b842f9337b lib/memio: add Option C parallel vstream API (#94 fold-e2)
Adds lib/memio/vstream.ww with three new constructors —
fixed_vstream / dynamic_vstream / dynamicfrom_vstream — that return
io.vstream (= *io.vtable, from lib/io/stream.ww) alongside the
pre-vtable memio.fixed / dynamic / dynamicfrom shape in memio.ww.

Hare's memio::fixed/dynamic return a `stream` whose first field IS
io::stream (= *vtable); ww mirrors that intrusively with fixed_ctx
+ dynamic_ctx structs whose first field is `vt: io.vtable`. A
heap-alloc'd *fixed_ctx is castable to vstream via `&c.vt`, and
callbacks recover the outer ctx via `s: *fixed_ctx` (same pattern
as lib/bufio.stream over io.stream and lib/log.stdlogger over
logger). ptr/len/cap kept flat (memio.ww:39 SOP) to dodge the
chained-dot-through-pointer-into-slice-subfield miscompile family.

Constructor flow: alloc with vt zero-initialised via a local, then
chained `c.vt.X = …` field-assigns through the *ctx pointer.
Struct-lit init via `vt = local_vt` (with local pre-set) silently
drops tagged-union slots past the first — sibling task filed,
workaround is the alloc-then-assign route (proven byte-id between
both stages). *ctx is a plain pointer-to-struct, not an aliased
pointer, so the chained-store path doesn't hit #195.

Cast workaround per #206 at each vtable-fn-ptr-slot init (8 sites
across the 3 constructors): bare `&fn_name` does not type-check as
`(*<alias> | void)`. Same `(&fn): *io.<role>` shape that
test/wcc/775_io_vtable_run.c uses. Drops out when #206 closes.

OLD memio surface is UNCHANGED. fold-eFinal (task #50) atomically
flips the package shape: deletes OLD constructors + callbacks and
renames `_vstream` suffix off.

Probe test/wcc/776_memio_vstream_run.c: 4 rows
(fixed_read_5 / dynamic_write_grow / dynamicfrom_alt_rw /
branched_fixed) exercise both vtable flavours through the
io.st_read / st_write / st_close dispatchers. Each row drives
cs runtime + ww runtime + cs.s == ww.s byte-id — 12 fixtures
total, all green.

Pre-existing wwstage gap surfaced + documented inline: ww_ww's
combined.ww concat order trips the wwstage checker on os.tryread
/ trywrite / tryopen's bare `return r;` over `(int | oserror)`
when os is checked after rt/io. Each probe row places `import os;`
FIRST to match the ordering selfhost uses (time → os → rt → …)
where the checker resolves cleanly. Sibling task; resolves the
ordering-sensitivity in the wwstage checker drops the workaround.
2026-05-29 08:42:36 +09:00
90ed913160 lib/io: port ref/hare/io/stream.ha vtable surface (#94 fold-e1)
Additive: keeps lib/io/io.ww's pre-vtable `stream` struct +
`read`/`write`/`close` wrappers (fold-e2-eN migrates the legacy
surface to vtable-backed implementations and retires it).

lib/io/stream.ww — vtable struct (reader/writer/closer slots,
spelled `(*T | void)` per #192 — ww parser rejects `nullable *T`),
vstream = *vtable, and the st_read/st_write/st_close dispatchers
per ref/hare/io/stream.ha:33-68. Void-arm `return e;` chains two
direct widens: concrete `errors.unsupported` → `error` (#199 α)
then `error` → (size|eof|error) (#205 NAMED-variant nominal at
tagged→tagged subset). Hare's `?`-propagating st_close collapses
to a direct `return (*c)(s);` because #173 is still open; the
surface stays Hare-shaped.

lib/io/types.ww — retarget reader/writer/closer fn-aliases from
*stream to vstream. Extend `error` union to include
`errors.unsupported` explicitly (no spread — per ken's #204-block
the wrapper-vs-flatten layout asymmetry would mis-widen; the
deferred fix is filed as #199b layout-extension).

test/wcc/775_io_vtable_run.c — 7-row sentinel: reader/writer/
closer × {set, void} happy paths + branched-callee runtime.
Rows verify the call runs + the constant exit; the void-arm
rows do NOT inspect the resulting variant tag (deferred #199b
wrapped-slot tag-remap mis-routes to dst tag 0). Cstage and
wwstage emit byte-identical asm on every row.

test/wcc/768_io_types_run.c — track the alias retarget; rows
now build a vtable, pass `&vt` (= vstream), and call through
the fn-VALUE param shape.

Combined.ww regen for w6c + wwdump per #110: lib/errors lands
transitively via the new `import errors;` in types.ww.

Sibling filed inline (NOT fixed): the checker rejects bare
`&fn_name` / `let p: *alias = &fn` assignment to a
`(*alias | void)` field — the structural `*fn(...)` value isn't
accepted as the `*alias` NAMED variant. Both stages reject.
Probes route around via explicit `(&fn): *io.reader` cast at
each vtable-field assignment.
2026-05-29 07:28:59 +09:00
487cf91f12 wcc: accept NAMED-variant nominal match at tagged→tagged subset (#205)
The tagged→tagged subset arm walked src's leaves against dst's flat
variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper
REJECTED at cstage's checker — wrapper's leaves (unsupported, underread,
nomem) aren't direct variants of dst. Wwstage's permissive tail
accepted silently but cgen then miscompiled the tag (#199b layout-
extension family, deferred).

Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is
a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal
to src, accept by nominal identity BEFORE the subset loop. Wwstage's
isassignable mirrors the structural insertion before the existing
`*confident = false; return true;` tail (deferred-tightening per #202).
SSoT with `is`/`as` non-recursive variant lookup (#198 family).

Cgen's tag-remap for the wrapper-as-whole case still maps src variants
to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx,
dst.payload = src` is #199b future-work. Probe verifies checker-accept
+ runtime exit-clean only; does NOT inspect the resulting variant tag.

Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro,
nested-wrapper, pure-leaf subset (regression), concrete-unrelated
rejection (gate), branched callee. Two sibling cgen/checker bugs
surfaced (wwstage cgwidentaggedstorebp ssz<slot_sz pad gap; wwstage
isassignable !void-alias collapse) and documented inline at the
probe-row comment, kept in #202 family.
2026-05-29 06:18:27 +09:00
3f4eeff7ff wcc: route is/as variant lookup through tinfo.params (#198)
checkisas walked the unflattened AST u.list via casevariantin (typeeqast
streq), so any variant introduced via a `...inner` spread was invisible
and rejected as "is/as: not a variant of operand". Repro:

    type rsh = (size | io.eof | ...io.error);
    let r: rsh = 42: size;
    if (r is io.underread) ...    -- pre-fix wwstage REJECTS

io.underread is in io.error.params, which tinfofornode splices into the
parent at L1827-1836, but the AST u.list still holds the single
`...io.error` entry that streq("io.underread", "io.error") rejects.

Route through flatvariantidxt — the same Phase-N helper #179 cgmatch
and #66 cgtagvariantidx already key off. Mirrors cstage cmd/wcc/check.c
:1662-1675 u->params + type_eq. Falls back to casevariantin AST walk
when tinfo isn't available (defensive — non-#198 path stays as-is).

project_tinfo_lossy_nominal: name-keying was the pre-Phase-N workaround
for tinfo lossy on nominal identity; typeeq inside flatvariantidxt now
handles NAMED ptr-id (#64), so the checker pair aligns with cgen on the
flattened-variant axis.

Closes the cgen-drain mini-cluster (#201 -> #199 -> #200 -> #198).

773_isas_spread_variant: 5 rows (spread_is_inline_variant,
direct_cross_mod_tagged, cross_mod_named_void, same_module_variant,
spread_as_inline_payload). Rows 2-4 byte-id; rows 1/5 skip byte-id due
to layout-asymmetry on `...wrapper` (cstage flattens at resolve_type,
wwstage computes maxsz off vt.size of the un-spliced alias) — sibling
not blocking the checker correctness fix.
2026-05-29 04:51:44 +09:00
6ce292b157 wwstage: spill non-IDENT scrutinee at cgtypeassert (#200)
cgtypeassert kept scrutoff=0 when the scrutinee wasn't an N_IDENT
(direct call result, arr[i], p.field, ?, paren-wrap of any of those),
so the tag-load fell on (BP) — the saved-BP word — and the payload-
load on +8(BP) — the return address. The wwstage repro returned 220
(garbage from RIP) where cstage returned 42 (impl-e1-resume sibling
of #199/#201).

Mirror cstage cmd/w6c/cgen.c:6300-6316 N_TYPEASSERT non-IDENT arm.
Add an `else` branch after the existing N_IDENT path that resolves
the tagged type via matchscrutt, alloc an @asrt_spill slot via
matchspillsz/localalloc, cgexpr the LHS, then spill the AX/DX/CX
tagged-return-ABI words: AX→+0 (tag), DX→+8 (word0), CX→+16
(word1, guarded on spill > 16). Subsequent tag-check + payload load
indexes off the spill like the IDENT path. Helpers reused from
cgmatch (cgenexpr.ww:1422-1460).

cstage's cgtypeassert omits the cgmatch 4-word R8→+24 spill (rule-10
stage symmetry: rather than diverge into a 32B-payload case the test
suite doesn't exercise, mirror cstage exactly and file the cstage
omission inline). Filed inline: cstage cgtypeassert needs the same
R8→+24 path cgmatch already has (drew's design rationale, blocked
by the rule-10 floor today).

772_typeassert_nonident: 7 rows (call_as_size — the repro, call_as_str
— CX→+16 spill + BX post-load, call_as_namedvoid — void-variant
tag-check fires, payload load is a 0-byte no-op, call_as_fnptr —
fn-ptr variant 8B word0, call_as_u8 / call_as_i16 — narrow scalar
round-trip via MOVQ + MOVQ confirms no truncation, branched_call_as
— runtime-chosen tag). Each row gated on cstage runtime + wwstage
runtime + cs.s == ww.s byte-identity.
2026-05-29 03:52:04 +09:00
4d44242363 wcc: reject transitive nested-tagged widen at type_assignable (#199 α)
cgen has no wrapped-slot layout — the tagged-union slot is universally
[tag:8B][payload:up_to_24B], single level. The recursive walk admitted
let r: (size|io.eof|io.error) = u for u: io.underread (transitively
in io.error.params); cg_tag_for_variant + taggedvariantindext don't
recurse, returned -1, defaulted to tag=0, and the slot read back as
variant 0 = size at runtime.

Restores SSoT inside the checker pair: is / as / match variant
lookup is already non-recursive (#198 sibling), and the LET-init /
return / assign arms now agree. Aligns DOWN to the leaner side
(rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
drill at ref/harec/src/types.c:702-739 (#199b is the deferred
wrapped-slot layout port).

Pre-flight audit (drew mandate): zero transitive-widen sites in
lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant
(io.error, strconv.error, fmt.field) is used as a variant of a
wider union anywhere in bootstrap. Mechanical fix.

Escape hatch for callers: spread (...wrapper) inlines the wrapper's
flat variants into the parent set at parse time. Wwstage's gate
additionally preserves the recursive drill on op == TK_ELLIPSIS
because wwstage stays AST-keyed (cstage flattens at resolve_type).

771_widen_transitive: 5 rows (reject_transitive_widen,
spread_alt_widen, direct_flat_variant, branched_callee_widen,
wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on
spread-expanded variants is open-bug #190/#198.
2026-05-29 03:04:18 +09:00
fb53b4798b wcc: TYPE-based forward-tagged predicate in wwstage cgreturn (#201)
cgreturn's forwardtagged detection was keyed on the CALLEE NAME
(N_IDENT/N_DOT only via fnretlookupmod), so any other callee shape
fell through to the variant-tag synthesis path — clobbering the
just-returned AX/DX/CX/R8 tagged-ABI words. The deref-call case
`(*r)(...)` (impl-e1-resume STOP, 994 w6c_ww byte-id red) was the
proximate trigger.

Replace with a TYPE-BASED predicate over the checker-stamped tinfos
(rhs.type_ vs c.fnret.type_), mirroring cstage cgen.c:8007 passthrough.
Peel TY_NAMED on both sides then identity-check the underlying
TY_TAGGED — sufficient for the NAMED case because tinfocache memoizes
per typedecl (#191 lineage). Variant-pointer fallback walks the
params chain when identity fails so anonymous unions like the
cross-module (i32 | void) shared between strings.byteindex and
bytes.index still forward correctly; full recursive tinfo
structural-eq is gated by #178 (typeeqast's TY_TAGGED arm
conservatively returns false today).

Probe 770_return_tagged_forward covers 6 rows — IDENT forward, widen
non-matching, deref-call (the bug), scalar (sanity), nested call,
cross-module forward — each gated on cstage runtime + wwstage runtime
+ cs.s == ww.s byte-identity.
2026-05-29 01:00:02 +09:00
b2ac8cbf81 wcc: peel N_TNAME alias chain on cgdot receiver before lkind decision (#191)
`type vs = *vt; fn(s: vs) s.field` linked-failed in wwstage with
`undefined reference to field' — cgdot read lc.tnode.kind without first
walking N_TNAME aliases, so lkind stayed N_TNAME (not the underlying
N_TPTR), structlookupchain missed (`vs` isn't a struct alias), and the
lookup fell through to the SB-global fallback that emits `MOVQ
<field>(SB), AX`. Mirror cstage type_chase_named (cmd/w6c/cgen.c:144-155)
via an aliaslookup loop, stopping at struct aliases so the existing
direct-struct N_TNAME arm stays byte-id with pre-fix #22 callers. LOOP
(not single-peel) — Phase-N builds N_TNAME chains
(project_tinfo_lossy_nominal), depth-2+ aliases require iteration. Inner
peel on the pointee is unnecessary: the existing structlookupchain
already walks N_TNAME chains via aliaslookup (cgenutil.ww:1266-1276); row
4 of probe 769 proves the inner-chain depth-3 path stays green without
an explicit inner peel.

Probe test/wcc/769_dot_aliased_ptr.c covers 4 rows (fn-param read,
let-binding read, double-alias receiver, pointee-alias chain), per-row
runtime + byte-id gates. Files inline two sibling bugs surfaced during
impl (cgassign write-side silent-drop, chained-N_DOT spine link-fail)
plus a cstage checker assignability gap on chain-depth-2 aliases — all
out-of-scope per rule 11 split.
2026-05-28 23:17:22 +09:00
635429bef8 lib/io: port mode/whence/error + reader/writer/closer fn-aliases (#94 fold-d)
New lib/io/types.ww mirrors ref/hare/io/types.ha — the surrounding
port that lives alongside the existing lib/io/io.ww (pre-vtable
stream + eof + underread). Hare splits the same way (stream.ha +
types.ha share `module io`); ww does the equivalent via dir-enum.

Each type cites Hare per CLAUDE.md rule 9:
  - mode    (enum u8)  — ref/hare/io/types.ha:29-34. RDWR=3 (not
                         Hare's `READ | WRITE`) because ww enum-value
                         positions don't fold expressions; bitfield
                         value SSoT preserved, divergence inline.
  - whence  (enum i32) — ref/hare/io/types.ha:37-41. Hare leaves the
                         underlying implicit; ww requires one. i32
                         matches the `off` type fold-e wires in.
  - error              — ref/hare/io/types.ha:11. Hare spreads
                         `errors::error`; lib/errors not ported, so
                         the union carries the two tags observable
                         in this fold: underread (from io.ww) and
                         the predeclared `nomem` (#29, type.c:72 /
                         check.ww:78). NOT redefined here.
  - reader/writer/closer — ref/hare/io/types.ha:46/51/55. EOF=eof
                           (not Hare's `done` singleton) per #93
                           and the io.ww:8 rationale. `*stream`
                           forward-refs the existing pre-vtable
                           struct in io.ww; same cross-file pattern
                           Hare uses.

Drew signoff (this fold only): seeker, copier, strerror, and the
EOF=done singleton DEFERRED to fold-e — they need the `handle` sum
and #93's done landing. Hare's `_unsafe` carve-out unaffected.

eof / underread / stream re-used from io.ww (NOT redefined); io.ww
keeps the pre-vtable struct unchanged, ditto its WHY-comments.

Combined.ww regen (#110): selfhost/cmd/{w6c,wwdump}/main.combined.ww
auto-pulled the new types.ww via dir-enum (+52 lines each, same
package io). Makefile dep lines for wwdump_ww + w6c_ww add the new
source so editing it triggers rebuild.

Probe: test/wcc/768_io_types_run.c — 5 rows × 2 stages = 10
invocations. Pins enum value/underlying + the three fn-type aliases
at the param slot. Both siblings filed inline in the probe header:

  - #189 wwstage `let r: io.reader = fn_name;` bails "let: not
    assignable". cstage accepts. Param + struct-field paths work
    in both stages, so io vtable port is unblocked. Probe uses
    the alias only at the param slot.
  - #190 wwstage match-arm on cross-module variant tag bails
    "case: not a variant of scrutinee (io.eof | io.error)". Likely
    same family as #178. cstage accepts. Probe uses `is` instead
    of `match` for the variant gate.

make test: 201/201 (was 200; +1 from 768). 990-997 byte-id +
combined_ww_fresh + sizelint all green.
2026-05-28 21:58:06 +09:00
c07f96fc35 wcc: kind-agnostic typeeq dispatch in cgmatch non-nullable arm (#179)
cgmatch's non-nullable variant-tag synthesis gated flatvariantidx on
pat.kind == N_TNAME (with N_TSLICE else-branch for #19's untyped-elem
fallback). N_TPTR / N_TFN / N_TPTR(N_TFN) case-patterns fell through
both, leaving r=-1 → want=0 so every variant past 0 silently
collapsed to tag 0 — runtime-passes only when the value happens to
sit on variant 0 (zero-coincidence miscompile).

Cstage cg_tag_for_variant works on resolved Type and is kind-
agnostic; harec stores `_case->type = ctype` (ref/harec check.c:2527).
#66 Phase-N already flipped match dispatch to typeeq; #179 is the
last site still keyed on AST kind. Memory: project_tinfo_lossy_nominal
+ feedback "Hare = resolved-type-only match dispatch".

Route through flatvariantidxt(scrutt.type_, pat.type_) directly,
guarded by istaggedtype + typeisslice(pattype) for the slice axis.
No new helper — existing flatvariantidxt / flatslicevariantidx wire
up unchanged.

767 probe locks the fix across 5 rows: (1) nullable *fn branched
store (ken's verify gate — proves the nullable arm at line 1484
isn't perturbed); (2) *i32|*i64 storing &i64 — pre-fix wwstage
emitted CMPQ $0 for *i64 arm, post-fix CMPQ $1; (3) *fn(i32)|*fn(i64)
via intermediate local; (4) branched runtime variant choice defeats
const-fold; (5) aliased ptr variants (typeeq through TY_NAMED).
Per row: cs runtime, ww runtime, cs.s == ww.s byte-id.

Sibling filed inline: widening `&fn` INLINE into a fn-ptr-only
tagged union picks tag 0 in wwstage's cgwidentaggedstorebp
(out-of-scope; row 3 dodges via intermediate ident store).
2026-05-28 20:55:42 +09:00
595577f606 cgen: route non-named callee to indirect CALL AX (wwstage #181-cgen)
Discovered while verifying #181's checker fix end-to-end: wwstage's
cgcall pre-computes `isfnptrcall` only for N_IDENT (local) and
N_DOT (struct fn-ptr field) callees. For a non-named callee — the
deref-call `(*f)(...)` shape (N_UN TK_STAR) most prominently — the
flag stayed false, the IDENT/DOT name-emit branches both missed,
and the emit produced `CALL (SB)` with an empty symbol.

Cstage handles this naturally via its default-fallthrough at
cmd/w6c/cgen.c:5918-5921 — `else { cgexpr(c, n->lhs, locals);
ins1(c, A_CALL, areg(D_AX)); }` catches every callee shape that
isn't bare-IDENT module-fn or N_DOT module-qualified call. The
fix here mirrors that fallthrough: any callee whose kind is
neither N_IDENT nor N_DOT sets isfnptrcall = true, routing
through the existing cgexpr-into-AX + CALL AX path.

Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main
.combined.ww per #110 freshness gate.

Lands as a follow-up to the #181 checker bail-lift: the checker
now stamps the deref-call N_CALL (so cgen runs), and with this
fix the wwstage cgen lowers it correctly. test/wcc/766's wwstage
+byte-id rows go green; runtime symmetry with cstage holds.
2026-05-28 20:13:07 +09:00
ba044e206d wcc: route deref-call N_CALL to fn-VALUE fallback (#181)
Pre-fix the wwstage checker bailed asserttyped on the N_CALL whose
callee was N_UN TK_STAR over a *fn — selfhost/cmd/wcc/check.ww
exprtype's N_CALL arm only resolved IDENT/DOT-named callees and
early-returned nil for any other shape, leaving e.type_ unstamped
so the post-checker invariant fired. cstage worked because cexpr
recurses on the callee — TK_STAR's unop arm returns t->sub which
IS the TY_FN, no name path needed.

Fix: replace the `if (nm.len == 0) return nil` early-bail with
`if (nm.len > 0) { name-lookup }`, so non-named callees fall
through to the existing fn-VALUE fallback below (peel TPTR /
dealias to TFN / stamp the result type). Mirrors harec
check_autodereference at ref/harec/src/check.c:1566. cgen post
-#180+#185 already lowers the deref-call correctly, so lifting
the asserttyped bail is silent-SIGSEGV-safe per drew + ken.

Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main
.combined.ww per #110 freshness gate.

Probe: test/wcc/766_star_fn_deref_call.c, 5 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn
-tuple-return. Gate flip from 765: every row now gates BOTH
stages — cstage runtime, wwstage runtime, AND cs.s == ww.s byte
-id. This is the runtime coverage 765 deferred plus the symmetry
gate that proves both stages emit identical asm for the deref
-call shape. Closes the full c-cluster (#180 + #185 + #181 all 3
commits working together end-to-end).
2026-05-28 20:01:14 +09:00
b86b9d76e2 cgen: deref of *fn skips MOVQ load — pointer IS fn-addr (#185)
Pre-fix the N_UN TK_STAR arm applied the generic pointer-load
`MOVQ (AX), AX` to a *fn operand. cgexpr on the operand already
left AX = fn-addr (post-#180 LEAQ); the spurious second load
read the first instruction word, and the subsequent CALL AX
jumped through that junk address and segfaulted.

Cstage: cmd/w6c/cgen.c N_UN TK_STAR opens with a TY_NAMED-peel
+ TY_FN early-break — leave AX as the fn-addr cgexpr produced.
Wwstage twin in selfhost/cmd/wcc/cgenexpr.ww cgun TK_STAR walks
the n.type_ tinfo chain the same way (TY_NAMED peel then TY_FN
check) and returns before the generic load. Mirrors
ref/harec/src/check.c expr_call's STORAGE_POINTER→STORAGE_FUNCTION
path (harec skips the deref since the pointer IS the address).

Both stages must land together per rule-10 (cstage-only would
break 990-997 byte-id gates — same lesson as #180).

Probe: test/wcc/765_star_fn_deref.c, 5 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args /
fn-tuple-return. Every row is cstage-only via stage_mask
because wwstage's checker bails asserttyped on `(*f)(...)`
(filed as #181 — N_CALL type_ stamp gap on deref-call); #181's
own probe will lock the wwstage runtime once the bail lifts.
Gate-blind risk (ken's note): byte-id alone cannot catch this
class because both stages drop the SAME instruction
symmetrically, so cs.s == ww.s holds either way. Runtime
exit-code is the only correctness net here.

Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main.
combined.ww per #110 freshness gate.
2026-05-28 19:25:53 +09:00
5478695922 cgen: address-of fn name emits LEAQ via mafn (#180)
Pre-fix the N_UN TK_AMP arm fell through silently when the operand
was an N_IDENT naming a top-level function — the let/def cascade
had no TY_FN branch, so the store at the assign site picked up
whatever AX held from prior code (commonly a stale arg register).
A subsequent (*f)(...) jumped through that junk and segfaulted.

Cstage: cmd/w6c/cgen.c N_UN TK_AMP IDENT adds a TY_FN arm before
the let/def cascade, mirror of the read-arm at line 2330 — same
mafn(opnd->str, c->cur_mod) shape. Wwstage twin in selfhost/cmd/
wcc/cgenexpr.ww cgun TK_AMP IDENT uses the analogous predicate
fnretlookup(c, nm) != nil + emitfnname(c, nm, c.curmod), matching
the cstage emit on byte-id. Both stages must land together per
rule-10 (cstage-only breaks 990-997 byte-id gates).

Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main.combined
.ww per #110 freshness gate.

Probe: test/wcc/764_amp_fn_ident.c, 6 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn-tuple
-return / cross-module. Rows 1-5 gate both stages (run + .s LEAQ
check + cs.s == ww.s byte-id); row 6 cross-module is cstage-only
because wwstage bails asserttyped on `&mod.fn` (sibling project
#184, filed). Per drew option (b) the probe exercises the address
-of without (*f)(7) — deref-call runtime coverage stays with
project #181's probe once the wwstage asserttyped bail on
N_CALL(*f) is lifted.

Phase 1 cross-mod verdict = FINE for cstage (LEAQ emits via the
already-present N_DOT TK_AMP branch at cgen.c:2477-2493); WWSTAGE
fails asserttyped on the same shape → project #184.
2026-05-28 18:50:10 +09:00
7031e0d713 wcc: typeeqast TY_FN compares result/params/variadic at AST layer (#94 fold-b)
Pre-fix master left N_TFN under typeeqast's conservative
"anything else fails" tail (selfhost/cmd/wcc/check.ww:748-751).
case-patterns spelled with a raw `*fn(...)` head — the io vtable
use case — tripped casevariantin / casecovers on every variant
compare, so a well-typed `match (v: tagged-of-fn-ptr) { case
*fn(...) => ... }` would not compile under wwstage.

Adds a TY_FN arm that mirrors harec STORAGE_FUNCTION
(ref/harec/src/types.c:589-615): recurse on the result type
(.lhs), iterate the param chain (.list of N_PARAM, descend each
.lhs), require the variadic flag (.op == TK_ELLIPSIS) to match
position-by-position, and require both chains to terminate
together. Param NAMES do not participate (harec analog), and the
C-variadic terminal sentinel (N_PARAM with .str == "...") is
handled defensively even though wwstage's parseparams doesn't
currently produce it. Attributes + default-param values are NOT
checked (drew-pre-approved, harec doesn't either).

cstage type.c:239's type_eq walks the same shape on the resolved
Type. typeeqast lives one layer below — a documented divergence
filed as project #178 for the harmonization fold; the in-source
comment cites #178.

Probe 763_typeeq_fn_ast.c locks 7 rows covering identical /
diff-return / diff-arity / diff-param-type / variadic / param-
name-only / io-vtable shapes across cstage + wwstage (14
fixtures). Pre-fix wwstage red-errors every row at the checker
("case: not a variant of scrutinee" + "match: variant not
handled"); post-fix all 14 compile and the tag-0 arm fires
(exit 7). Per-row .s byte-id is intentionally NOT gated — see
the probe header for the cgmatch N_TPTR-not-routed-to-
flatvariantidx sibling bug that drives the divergence on rows
b/c/d/e/g; orthogonal to this AST-layer typeeqast fold and not
swept per the brief's "do not sweep" instruction.
2026-05-28 17:44:24 +09:00
66a91c8969 wcc: converge let-IDENT memcpy, IDENT-assign recv, nested struct call-recv onto structabisize
Three more wwstage cgen sites still used unrounded structnaturalsize where
cstage rounds via lu->size — pre-existing gate-blind cs!=ww latents the #169b
reviewer surfaced: cgenstmt N_LET struct-IDENT memcpy (let p2: T = p1; twin
cgen.c:7869), cgenexpr N_ASSIGN N_IDENT-lhs register RECV (s = mk(); twin
cgen.c:4700-4737), and cgenutil's nested struct N_CALL recv inside
cgstructlitfill (twin cgen.c:2121).

Converge all three onto structabisize, completing the same-class closure
started by #169 and continued by #169b. Also corrected the inline comment at
cgenutil.ww:3273-3286 that wrongly claimed fl->type->size was natural
(check.c:760 sets ABI). sretretsize at cgenutil.ww:1301 is gate-equivalent
natural and is left alone.

Probe 698 +3 rows (one per converged site) with cs==ww .s byte-cmp and a
pre-fix-rebuild discriminator. 990-997 byte-id hold.
2026-05-28 14:07:58 +09:00
39f9267bc9 wcc: converge DOT-recv, structlit-fill, and bare-let zero-init onto structabisize
Three wwstage cgen sites still used the unrounded structnaturalsize where cstage
rounds via lu->size (check.c:760), pre-existing gate-blind cs!=ww latents
flagged in #169's reviewer notes: cgenexpr DOT register-RECV for obj.f = mk()
(~5175/5393/5628/6164); cgstructlitfill's TK_ELLIPSIS zero-fill branch
(cgenutil); and cglet bare 'let z: T;' zero-init of a maxalign<8 struct
(cgenstmt). Each produced MOVQ-vs-MOVL or wider-write divergence vs cstage on
the trailing word of a sub-eightbyte tail.

Converge all three onto the maxalign-rounded structabisize the #169 work
established at the register-ABI sites (cite cstage cgen.c:7720 RECV twin +
cgen.c:2085 cg_structlit_fill). cgstructlitfill's signature drops the external
totsize parameter in favor of one internal source; the field-walk path is
untouched, only the ELLIPSIS zero-fill uses the ABI size. cglet's slot
allocation stays on the frame size; only the zero-fill extent uses ABI.

Gate-blind (the bootstrap exercises none of these shapes); covered by 5 new
rows in probe 698 with cs==ww .s byte-cmp and a pre-fix-rebuild proving the
exact MOVQ-vs-MOVL discrimination. 990-997 byte-id hold.
2026-05-28 13:02:47 +09:00
d92c199d25 wcc: retire exprfloatkind, read the checker stamp directly
exprfloatkind was wwstage cgen's structural float-classifier — a workaround for
the checker stamp being untrustworthy. With the previous commit arming the
asserttyped bail, every checked value-node is now stamped (or cited-exempt),
so its job collapses to a 2-liner reading n.type_ — the same path cstage cgen
has always taken. Retire it: inline the stamp-read at its eight sites (cgcast,
cgun, cgbin lhs+rhs, cgcall pop, pushargsrev, cgwidentaggedstorebp, cgreturn
x2 collapsed), delete the wrapper, and delete the two residual
sibling-evidence loud-aborts (cgbin float-arith, cgwidentaggedstorebp
float-arm) — their operands are real source value-exprs the armed bail now
stamps, so the guards can never fire.

One synth-post-checker value-node remained outside the bail's reach: the
variadic-slice descriptor pushed in pushargsrev/cgcall (cgenexpr.ww). Stamp
it at synthesis with the variadic param's []T slice tinfo so the inlined
reads see a stamped node, no nil special-case. Byte-id-neutral by design
(slice tinfo and nil both read non-float); 990-997 confirm.

Closes the bail-rearm arc — wwstage now reads the same float-class SSoT
cstage does, the gate-blind float-classification family is closed, and the
build+test corpus is asserttyped-clean by construction.
2026-05-28 11:35:45 +09:00
719893743e wcc: arm asserttyped bail — a nil-typed value node is now fatal
The wwstage asserttyped pass only WARNED on a checked value-node with no
result type_, a check-bail-discipline regression that let gate-blind nil-stamp
miscompiles ship green (the whole #6 arc: tuple/struct/fn-ptr/enum/binding
nil-stamps were all invisible to the byte-id gates). With every nil-gap class
now stamped (module-qual calls, fn-ptr-field calls, computed enum value-exprs,
for-range/massign binds) the bail can finally arm: warn -> os.exit(1).

Exempt exactly the two legitimately-no-type value classes, each a positive
cited assertion (never a residual warn): the EXPR_ASSERT family (abort/assert,
guarded against a user shadow; harec check.c:877,893) and seeded pseudo-builtin
callees (len/append/free/alloc/size — a structural nil-decl-SK_FN predicate,
not a name-list). The pre-existing module-ref and dot-lhs filters stay: they
identify access-path components that aren't value exprs (harec EXPR_ACCESS),
not exemptions.

Verified clean over the broadest net — the armed checker over all five
self-build combined units (the full selfhost source) plus the 901 gap corpus —
zero out-of-class bail; fails-loud confirmed (undeclared call, abort's args, a
nil dot-base all bail). Checker-only: 990-997 byte-id hold.
2026-05-28 10:40:08 +09:00
f892a91223 wcc: stamp for-range tuple-binds and massign blank via shared helper
For-range tuple-destructure binders (for (let (k,v) .. s)) and the tuple
massign discard _ were left nil-typed: the for-range binders are N_IDENT
use-sites and the _ slot, though unbound, has a real element type. Add a
shared stamptuplebinds helper — one lockstep walk distributing an N_TTUPLE's
per-element types onto a binder chain — refactoring the existing N_MLET
destructure loop into it (behavior identical) and adding N_FORRANGE and
N_MASSIGN call-sites. _ is STAMPED with its slot's element type (unbound is
not untyped), not exempted. Mirrors harec create_unpack_bindings
(ref/harec/src/check.c:1354-1419), the routine harec shares between let-unpack
and the for-each header (:2308-2317).

A prerequisite for arming the wwstage asserttyped bail. byte-id holds (cgen
derives binder/elem widths structurally, never off type_; 990-997 green).
Extends the 901 gap-corpus with 901_forrange_tuple.ww + 901_massign_blank.ww.
2026-05-28 09:06:54 +09:00
3252bd1709 wcc: stamp computed enum-member value-exprs
A computed enum member — B = A + 4, RW = R | W, sibling/chained backref —
left its value-expr node nil-typed: enumvalfold folds the constant but never
stamps the expr, and since enum members are not installed as scope idents the
sibling backref resolves to nothing, so BOTH the N_BIN/N_UN wrapper and the
backref N_IDENT go nil (literal members are fine). Stamp the value-expr subtree
(only-nil) to the enum's underlying storage type via a new
stampenumvals/stampnilexpr pass on the N_TENUM branch. Mirrors harec checking
each member value-expr at the underlying type (ref/harec/src/check.c:4419).

A prerequisite for arming the wwstage asserttyped bail. Checker-only — the
value folds to a constant at every use site and in cgen, so the node's type_
is never read by codegen; 990-997 byte-id hold. Extends the 901 gap-corpus
with 901_enum_corpus.ww.
2026-05-28 08:07:19 +09:00
360b58b267 wcc: stamp fn-ptr-value call results from the callee fn-type
A call whose callee is a fn-VALUE (a fn-pointer struct field like w.emit(...),
or a local/param) had no free SK_FN entry, so exprtype's name lookup missed and
the N_CALL went nil-stamped (the fn-ptr-field class of the asserttyped gap
audit — 3 warns at smoke). When the name lookup misses, fall back to harec's
check_expr_call shape: read the result off the checked callee node's own type —
autodereference + dealias to the TY_FN, take its result (ref/harec/src/check.c
:1566-1581). Name lookup stays primary: a fn-NAME callee node carries its
return type, not its fn-type, so an N_TFN check first would mis-yield void for
a fn-returning-fn; only genuine fn-value callees reach the fallback.

Drives the 901 gap-corpus B count to 0 — with A/D already closed, only the
legitimate abort exemption (C) remains before the bail can arm. Byte-id holds
(cgen's fn-ptr detection is structural, independent of the stamp; the lib/io
return-forwarding site is cs==ww on both bootstrap combined.ww).
2026-05-28 06:28:59 +09:00
884dbb402b wcc: dot-lhs prefers SK_USE module over same-leaf type/fn name
The wwstage checker resolved a module-qualified call/access mod.x by the
same-module preference in scopelookupprefer, so when the importing package's
name collides with a type/fn of the same leaf (package fnmatch with fn fnmatch;
package random with type random), the dot-lhs mod resolved to the same-leaf
SK_TYPE/SK_FN instead of the coexisting SK_USE import — the N_DOT module-qual
arm never fired and the call went nil-stamped (the D class of the asserttyped
gap audit: fnmatch 2, random 16). cstage resolves this via Sym.use_alias; this
ports the equivalent to wwstage.

Add scopelookupuselocal (a single-scope SK_USE lookup, twin of scopelookuptype)
and prefer SK_USE for a dot-lhs in exprtype's N_CALL and N_DOT arms, keyed on
the scope where scopelookupprefer landed so a local binding sharing a module's
leaf keeps value semantics. Scope-layer only — no type-identity touch (cstage
use_alias never reaches type_eq).

Drives the 901 gap-corpus D count to 0 (random_test now byte-id cs==ww).
Compiler binary unchanged (no such collision in its own source); 990-997 hold.
The separate fnmatch bare-enum-member cgen cs!=ww is unrelated (filed).
2026-05-28 05:17:28 +09:00
ea1579a6fd wcc: exprtype resolves SK_USE module-qual N_DOT call results (#16, #17)
Re-arming the wwstage asserttyped bail surfaced 94 nil-stamp warns in the
checked corpus: let (a,b) = mod.fn() left its destructure bindings (and every
use) unstamped because exprtype's N_CALL arm resolved an N_DOT callee by bare
leaf — the gap its own comment flagged (#16/#17). Fix at the root: when an
N_DOT callee's lhs resolves to SK_USE, resolve the result via
scopelookupinmodule (mirror cstage cexpr check.c:1035 + cgen fnretlookupmod
cgen.ww:2263). The N_MLET backfill then just consumes the resolved tuple,
matching harec create_unpack_bindings (check.c:1354-1419), which does no callee
resolution — single path, no third copy.

The SK_USE gate leaves the module-leaf==type/fn-name collision cases
(random/fnmatch) on bare lookup — that nominal-resolution gap is a separate
fold. Beyond destructure, the root fix also closes a latent cs!=ww divergence
on non-destructure cross-module same-leaf calls (a head-ordered shadow was
mis-sizing the receive slot).

asserttyped is ww-stage only, so the live ww-driver suite can't see this — the
net is the warn count (checked 94->0, collision cases unchanged) + cs==ww .s
(probe 956). Compiler binary unchanged; 990-997 byte-id hold.
2026-05-28 03:55:22 +09:00
c882bcf27c wcc: struct-return float fields via SSE return regs (#171a)
The RETURN twin of #165: a qualifying float-struct was returned GP-only
(struct{f64,f64} in AX/DX instead of X0/X1) — value-correct via GP transport
but not SysV register-class conformant. Route each float eightbyte through the
SSE return cursor (X0/X1) and each integer eightbyte through GP (AX/DX) via
independent cursors, at the struct-return SEND and RECV, both stages, reusing
struct_float_class verbatim. Closes the temporary tuple-SSE/struct-GP
divergence opened across #164/#165.

A qualifying struct has >=1 lone f64 so maxalign is 8 and the ABI slot is an
8-multiple — no sub-8 tail — so #169's sized tail is unreachable here and the
integer eightbyte uses a full MOVQ (cstage agrees, proven by the f64i32
cs==ww byte-id). f32 / multi-float-per-eightbyte stays GP (deferred #171b);
>16B stays sret.

Gate-blind and value-correct, so the discriminator is the SEND/RECV register
class (MOVSD X0/X1 vs MOVQ AX/DX) — covered by probe 946_structret_run.
2026-05-28 01:50:28 +09:00
d4e500f61c wcc: struct ABI size maxalign-rounded via structabisize (#169)
wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where
cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign
8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the
return path — unpacked with a different trailing-word width (MOVL vs MOVQ)
between stages. Value-correct either way, but a cs!=ww asm divergence.

Add a dedicated structabisize = round(natural, maxalign) used only at the two
register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and
memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural
size, so the two are different sizes — rounding the shared metric breaks 995.
maxalign derives from each field's tinfo.align (mirrors cstage check.c:708),
not an fsz ladder (a ladder over-rounds composite [N]u8 fields).

Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the
discriminator is the cs==ww .s byte-cmp; covered by probe 698.
2026-05-28 01:02:41 +09:00
a917533fbf wcc: struct-param float fields via SSE arg regs (#165)
struct params were passed GP-only, so a struct{f64,f64} argument landed in
DI/SI instead of X0/X1 — value-correct for internal ww calls (the bits
round-trip) but not SysV register-class conformant. Add a per-eightbyte
classifier (struct_float_class) routing a qualifying struct's float eightbytes
through the SSE arg cursor, reusing #163's dual-cursor plumbing and #164's
field classification. A struct qualifies only when every eightbyte is
pure-integer or a lone f64 exactly filling it (and >=1 f64); anything else —
any f32, multiple floats per eightbyte, a straddling or aggregate field —
falls back to the unchanged GP path (f32 sub-eightbyte packing deferred #165b).
Both stages' predicates are alias-aware and identical in coverage.

Gate-blind and value-correct either way, so the discriminator is the callee's
receive instruction (MOVSD vs MOVQ), scoped per-function — covered by probe
946.
2026-05-27 23:31:19 +09:00
0465c423c1 wcc: tuple-param ABI via SSE/GP arg cursors (#163)
Tuples were unhandled as parameters — no tuple arm in arg-push, arg-pop, or
callee-recv in either stage — so a tuple param fell to the 1-GP-word else and
dropped all but its first element (integer tuple params too; floats doubly
lost). Add tuple-param arms (SEND push+pop, callee RECV) across both stages,
reusing #164's per-element SysV classify with the 6-GP (DI,SI,DX,CX,R8,R9) +
8-SSE (X0-X7) arg cursors. A frame slot @tupargscr decouples the producing
call's return cursor from the overlapping arg cursor (capture-before-clobber).
Overflow (>6 GP / >8 SSE) fails loud (rule 7). Scoped to the N_CALL producer;
first-class tuple values (ident/literal) remain a separate unimplemented gap.

Gate-blind (the bootstrap passes no tuple params) — covered by table-driven
probe 905, which proves pre-fix element-drop and the loud-stop.
2026-05-27 22:13:46 +09:00
153c7b3b46 wcc: multi-float tuple return via SSE cursor (#164, #107)
A multi-float tuple return mis-routed: SEND pushed a stale AX leaving the
float stranded in X0, while RECV (#105) read every float from X0 — so a
(f64,f64) return collided both floats. Add an SSE cursor [X0,X1] parallel to
the GP cursor [AX,DX,CX,R8], placing each element by its SysV class +
within-class index (ref/qbe/amd64/sysv.c retr), symmetric send/recv across
both stages, via a generic tuple_store/tupstore+tupsse helper that #171 will
reuse for struct-return convergence. (f64,f64,f64) = 3 SSE eightbytes exceeds
the 2-register cap and now fails loud (rule 7) rather than colliding.

Unifying the 16B and 32B whole-tuple-single-var branches onto the dual cursor
was required for f64+str coexistence; it also fixes a latent str-first
single-var bug (the old 32B branch read .ptr from DX while the send placed it
in AX). No str-first or 32B tuple exists in-tree, so integer paths stay
byte-identical (990-997 green).
2026-05-27 21:01:25 +09:00
028109513e lib/strconv: retire workarounds in f32todecf32 (#168)
The two local-binds were #168 dodges: a CSE before `% 10u32` to avoid the
signed-IDIVQ-on-call-result shape that #168 has now fixed. Inline to the
natural form, faithful to ftos_ryu.ha:418-421,444-445 — this exercises #168
in real ported code. The dividends are zero-extended u32 (always positive as
64-bit), so IDIVQ and DIVQ agree on the value; the fix is a cs==ww byte-id
shape correction, not a value change. strconv is compiler-imported, so this
regenerates the w6c/wwdump/smoke amalgamations.
2026-05-27 19:29:46 +09:00
393ac1a882 wcc: nodeisunsigned N_CALL reads stamped type_ (#168)
Twin of #134 (N_INDEX arm): the wwstage signedness classifier did not
consult the checker-stamped type_ for an N_CALL result, so an
unsigned-returning call got signed IDIVQ/SARQ instead of DIVQ/SHRQ.
cstage was already correct (reads the stamped operand type; check.c:1433),
so this is a wwstage-only arm — symmetric outcome both stages.

Gate-blind (the bootstrap lacks the shape) — covered by table-driven
runtime probe 906, which also asserts w6c==w6c_ww .s byte-identity.
2026-05-27 18:43:08 +09:00
0e72556120 lib/strconv: f32tos Ryū shortest float→string (#106 fold-5b)
The f32 coda of Drew's strconv 5-fold plan — f64tos shipped in fold-5a
(0e66073); this completes the plan. Re-lands the f32-exclusive Ryū path
that fold-5a removed under the dead-code rule (it was #143-blocked):
pow5fac32/pow5multiple32/pow2multiple32, mulshift32, mulpow5inv_divpow2/
mulpow5_divpow2, decf32, f32todecf32 (ftos_ryu.ha), and the f32tos driver
(ftos.ha:448). Plus F32_POW5_*_BITCOUNT in ftos_data.ww.

The f32 path REUSES the shared u64 core (mulshiftall64/u128mul/u128rshift/
log*) and f64computeinvpow5/f64computepow5 — and thus the f64 SPLIT2
tables — exactly as ftos_ryu.ha does; there are no separate f32 tables.

Unblocked by #143 (aff7725): f32tos calls math.f32bits(n), passing an f32
arg, which now spills MOVSS (4B) in both stages. Verified: f32tos's arg
push/pop is MOVSS, w6c vs w6c_ww 0-diff on the strconv-embedding
combined.ww (w6c/wwdump/smoke regenerated).

Dodges (cgen bugs still deferred, each cited at-site): decf32.exponent:i64
sidesteps the #169 narrow-second-field struct-return unpack (byte-id
gate-confirmed, not an ABI guarantee); #168 div/mod local-bind on the two
`%10` sites; *decimal pointer field reads (#170); [32]u8 buffer reuses
f64tos's byte-id-clean band over Hare's [14] (#43). mulshift32's U32_MAX
bound inlines the literal — ww's types.U32_MAX is package-private (#172).

Test: ftostest.ww gains f32 vectors — the tcs G/void rows (shared f32/f64
shortest), the f32-exclusive tcsf32 extremes (1e-45 / 1.1754944e-38 /
3.4028235e38, full 24-bit mantissa), specials, a negative-normal, and
33554432 (the sole e2>=0/q<=9 runtime cover). make test 187/187 incl
908_ftos_run + 990-997 byte-id + combined_ww_fresh.
2026-05-27 17:18:27 +09:00
0e66073e32 lib/strconv: f64tos Ryū shortest float→string (#106 fold-5a)
Graduate f64tos from the lossy fixed-point placeholder to Ryū shortest-
round-trippable (ftos.ha:432 + ftos_ryu.ha). f64tos(n:f64) str, G-format
(void/NONE) — the faithful documented subset (full parametric fftosf is
dead code for G/void/NONE → deferred #64). Ryū core decomposed: struct-
RETURN + scalar params (Hare's r128 idiom; avoids tuple-ABI #163-166).
f64 powers tables [15][2]/[13][2]u64 (2D #156). Zero float literals.
Both E+F encode paths reachable+tested, no dead code.

Graduation (lib-note "don't keep both"): old lossy f64tos deleted; fmt
fprintf_f64_huge "huge"→"9.5e18" (improvement). 5 value-faithful filed-
bug dodges (byte-id, documented): #167/#169/#170/#43/#144. Test 908.
Make test 186/186 incl 990-997 byte-id + combined_ww_fresh.

Drew's strconv 5-fold plan — primary completion (f64tos). f32tos coda
#67 (behind #143); parametric ftosf #64.
2026-05-27 15:39:03 +09:00
81796cd533 lib/strconv: stof.ha port — Eisel-Lemire string→float (#106 fold-4)
stof64/stof32 (f64|f32 | invalid | overflow) via Eisel-Lemire fast-path
(powers_of_ten[596][2]u64 + eisel_lemire 128-bit multiply) + decimal
slow-path fallback (decimal.ww, fold-3). 16 fns + faithful powers_of_ten
(byte-identical to Hare). u128 via pure-u64 64×64→128 (ftos_ryu.ha).
Consumes &math.f64info (γ-cleanup), tagged-float-return (PREREQ-2 #157),
2D double-index (PREREQ-1 #156).

13 documented spelling-divergences (rule-9, each cites stof.ha): #155
(po10 double-index + per-field struct-copy), #161 (compound-assign explicit
form), #144 (-0.0 via 1u64<<63), #158, #138, test-only #143/parsef64.
Test 909 (DEC+hex+NaN/Inf/invalid/overflow, bit-exact, cstage ww run).
Make test 185/185 incl 990-997 byte-id + combined_ww_fresh. Makefile:
stof.ww added to w6c_ww/wwdump_ww deps (freshness, fold-3 precedent).

Drew's strconv 5-fold plan 4/5. Followup #162 (wwstage lexer parsef64
1-ULP — could adopt stof64).
2026-05-27 13:55:56 +09:00
4a91bdc8db wcc: tagged float-variant return packs float bits via X0-spill (#157)
The N_RETURN tagged-pack scalar-variant arm did MOVQ AX,DX, but a float
variant's value is in X0 not AX -> packed stale int (broke stof64/stof32
return (f64|invalid|overflow)). Fix: float variant bridges X0->DX via a
stack slot (SUBQ $8,SP; MOVQ $0,(SP); MOVSS|MOVSD X0,(SP); MOVQ (SP),DX;
ADDQ $8,SP), gated type_isfloat/exprfloatkind. No MOVQ-xmm->gp form
exists, hence the spill (715-class, cgreturn-register-pack twin of 715's
store-to-slot). Zero-slot-first -> deterministic f32 high-4. AX-independent
-> also resolves the multi-variant cs!=ww. Bootstrap-NEUTRAL (compiler has
no float-tagged-return). Test 707 +3 rows (f64/f32/multi, slot+8 bit-exact;
f32 no-f32-arg to isolate #143). Make test 184/184 incl 990-997 byte-id.
2026-05-27 13:04:02 +09:00
cbeffea7d8 wcc: 2D array [N][M]T static-init + double-index read (#156, A.3 capstone)
Close A.3's deferred shape-14 (nested array). (a) emit_array_lit_bytes
gains a TY_ARRAY-element arm (mechanical clone of the TY_STRUCT-element
arm — recurses; esz=etype->size, rule-13; ...-nested loud-reject). (b)
double-index read tbl[i][j]: when the indexed element is TY_ARRAY, leave
the sub-array ADDRESS in AX instead of dereferencing (sister of #135's
N_DOT-base fix, on the N_INDEX path) — new elemisarrayc/tinfoisarray
helpers, both stages. Storage + read = one 2D-end-to-end concern (A.2/A.3
storage+LOAD precedent).

Unblocks strconv fold-4's powers_of_ten[596][2]u64 (direct double-index
access). Bootstrap-NEUTRAL (new arms gate on TY_ARRAY-element; 1D
consumers byte-identical, 990-997 green). Test 919 +2D rows + 3D +
...-nested-reject. Deferred siblings: #155 (sub-array bind / whole-
aggregate copy), #160 (global-struct-field index base).
2026-05-27 12:21:15 +09:00
db7523e0e7 wcc: address-of symbol-bearing def + cross-module module-qual (#149)
Widen cgaddr / N_UN TK_AMP for addressable globals — the address-of
twin of A.2/A.3's value-LOAD widening (which covered cgexpr N_DOT /
cgindex but never the &-path, so &<mod>.<def> emitted uninitialized-AX
garbage). Two shapes, one class:
- Shape 1 (&G, N_IDENT def): LEAQ masym(G) for struct/array/scalar
  defs. Scalar gate = emit_defs/emit_floatlit_data eligibility exactly
  (fold_int_literal OR rhs-peels-to-N_FLOATLIT) so the addressable set
  equals the symbol-bearing set — never LEAQs a missing symbol. Reuses
  DefStruct/DefArray registries (A.2/A.3); new def_isscalardef/DefAny.
- Shape 2 (&mod.G, N_DOT module-qual): LEAQ leaf (TY_FN -> mafn),
  mirroring the value-READ resolution (cgen.c:6043). Kind-agnostic —
  fixes &mod.def, &mod.let, &mod.scalar, &mod.func. This is fold-4's
  &math.f64info pattern.
Non-addressable def (str-def, computed-float-def) -> loud error both
stages; upgrades #147 &NAN(0.0/0.0) from silent wild-deref to loud
compile error. Computed-float-def symbol emission split to #147
(needs float-const-fold; not needed by gamma/fold-4). wwstage Shape-2
gate carries !deflookup so a def base routes to silent-drop matching
cstage's type-gate (rule-10 parity).

Unblocks gamma-cleanup (#40) + strconv fold-4 (&math.f64info).
Bootstrap-NEUTRAL (no &global in lib/selfhost; only &local). Test 921
(12 rows: same-pkg struct/array/scalar-int/scalar-float + cross-pkg
def_struct/let_struct/scalar_let/func + regression &let/&arr[i] +
2 loud-reject). Make test: 184/184 incl 990-997 byte-id +
combined_ww_fresh.

Followups: #147 (computed-float-def symbol), #152/#153 (pre-existing
ptr-param element read divergences, dodged by 921), #154 (address-of
def subparts &def.field/&def_array[i]).
2026-05-27 10:27:55 +09:00
0546bda6c4 wcc: array-init accept-if-fits coercion (#130, merges #146)
Align bare-int array-init assignability to Hare's literal-fits rule
(ref/harec/src/types.c promote_flexible): accept untyped-int array
elements that FIT the element type, reject out-of-range loud. cstage
(rejected all bare-int arrays, over-strict) and wwstage (accepted +
silently truncated out-of-range, over-loose) converge to the same
accept-if-fits rule. Per-element: foldable int literal range-checked
against element type [min,max] via def_cast_fits (rule-13 type-table
widths); non-foldable element falls back to type_assignable.

cstage: new arrlit_init_fits, N_LET decl-check fallback after whole-
array type_assignable fails. wwstage: checkletassign array branch +
route top-level lets through checkletassign (were unchecked — only
function-body lets ran assignability; closes #146 wwstage str->u8
over-accept).

Scalar-init range-check (let X:u8=300 truncates, both stages,
pre-existing) deferred to #148 — language-wide, needs bootstrap audit
+ explicit-cast conversion. def-array accept-if-fits deferred to #151
(def constfold machinery, different risk). Both bootstrap-NEUTRAL.

Test 920 (14 rows — accept: in-range u8/u32/u64/i32 + u8/i8 boundary +
typed regression + non-foldable-body; reject: over-range + over-256 +
i8-over + neg-for-unsigned + str->u8 + non-foldable-wider). Non-
foldable else-branch cs==ww verified (matching-type accept + byte-id;
wider-runtime-int reject both stages). Make test: 183/183 incl 990-997
byte-id + combined_ww_fresh.
2026-05-27 09:18:03 +09:00
9e3bc4ea37 wcc: array static-init let/def DATA emit via SSoT helper (#129 A.3)
Extract emit_array_data + emit_array_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_ARRLIT initializer or no-rhs
zero-init. Two-pass validate-then-emit: validate pass walks elements
and fails atomically on any non-foldable element (no partial-byte
emit on failure); emit pass writes element bytes after success.
Element-kind dispatch: integer via fold_int_literal byte-for-byte
preserved from pre-A.3 inline arm (bootstrap NEUTRAL — 6 live consumers
in lib/os/bufio/strings/encoding-utf8/strconv-stof_data), float via
inline bitcast + sign-XOR byte-loop (A.1 shape, no INT64_MIN — sibling
#144), struct via recursion into emit_struct_lit_bytes (A.2 helper).
Out-of-scope element kinds (ptr-elem, nested-array) rule-7 fatal.

emit_struct_lit_bytes gains TY_ARRAY field arm calling emit_array_lit_
bytes recursively — closes A.2 parked shape-15 (array-in-struct
`def D: dt = dt{tag=42, buf=[1u8,2u8,3u8,4u8]};`).

LOAD-side widened symmetric to A.2 precedent: cstage cgindex N_INDEX
direct-ident isglobal gate widened via new DefArray registry
(def_isarraydef populated in let_collect parallel to DefStruct);
wwstage cgindex N_INDEX falls through to defvartnode on letvartnode nil
(reads defent.dtnode field added in A.2). Both stages materialise
array-def via LEAQ name(SB) same as array-let.

Mid-impl rule-7 stop: refactor initially routed only rhs==N_ARRLIT
through emitarraydata, leaving nil-rhs zero-init arrays (e.g.
`let f64tos_buf: [64]u8;` in lib/strconv) silently SKIPPED → undef-ref
at link of wwstage-rebuilt selfhost binaries. Caught on first gate run
via bootstrap 994/995 RED. Fixed by adding nil-rhs branch to
emitarraydata (zero-fills arrt.size bytes) + widening wwstage caller
to route both N_ARRLIT and nil through helper. Same-class-lower-stratum
pattern (recurring across A.1 N_UN-peel, A.2 sz==8-short-circuit, A.3
nil-rhs-drop); banked as feedback memory.

Test 919 (11 rows: int-elem 1B/4B/8B + signed-N_UN-peel + float-elem
f64/f32 + def-int / def-float / struct-with-array-field shape-15 +
explicit-zero + single-elem-regression) registered. Make test:
182/182 incl. 990-997 byte-id + combined_ww_fresh.

Followups filed:
- #43 — wwstage emitletdataw str/slice-size arms lack !isarr guards;
  hypothetical no-rhs [16/24]u8 triple-emits (NOT A.3-introduced;
  no live consumer; 2-line parity fix)
2026-05-27 05:56:24 +09:00
0ed0b3933c wcc: struct-composite let/def DATA emit via SSoT helper (#129 A.2)
Extract emit_struct_data + emit_struct_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_STRUCTLIT initializer. Walks
Tfield linked-list in declaration order, zero-fills padding via per-
field offset (rule 13, no hardcoded sizes), dispatches per field kind:
integer via fold_int_literal, float via inline bitcast + sign-XOR byte-
loop (A.1 shape, no INT64_MIN materialised — sibling #144), nested
struct via recursion (#145 inner-field-name-leak gates the test row).
Out-of-scope field kinds (str/slice/ptr/array) fatal loud per rule 7.

LOAD-side widened symmetric to A.1 precedent: cstage cgexpr N_DOT
direct-struct-ident + chained-N_DOT widened via new DefStruct registry
(def_isstructdef populated in let_collect); wwstage cgdot direct-struct-
global falls through to defvarstructinfo on letvarstructinfo nil
(defent.dtnode field added, populated in collectdefs). Both stages
materialise struct-def via LEAQ name(SB) same as struct-let.

Pre-existing cstage scalar 8B short-circuit at emit_lets caused silent
fold-fail-continue on 8B struct lits (`struct{i32,i32}`); gate now
excludes let_isstruct so 8B struct lits route through emit_struct_data.
Wwstage's `!issg` gate was already correct; symmetric ordering restored.

Closes (all bootstrap-NEUTRAL pre-impl; γ-cleanup #40 first consumer):
- emit_lets `is_struct continue` skip → struct lets emitted no DATA
- emit_defs no struct arm → struct defs emitted no DATA
- cstage cgexpr N_DOT for struct-def emitted MOVSXD (BP), AX (broken
  stack-frame read)
- cstage emit_lets sz==8 short-circuit silently skipped 8B struct lits

Test 918 (7 rows: let_int_struct / def_int_struct / let_float_field /
def_float_field / let_empty_struct / let_int_struct_8b / let_norhs_
struct_regression) registered. Make test: 181/181 incl. 990-997 byte-id
+ combined_ww_fresh.

Followups filed:
- #145 (task #41) — nested struct-lit inner field-name leaks as extern
- #42 — wwstage dotchainresolve missing defvarstructinfo lookup (A.2-
  scope-clean today; surfaces post-#145 nested-struct shapes)
- A.3 (task #39) — array static-init audit (parks shape-4 array-in-struct)
- γ-cleanup (task #40) — lib/math const-floatinfo re-fold, blocked-by A.2
2026-05-27 05:04:05 +09:00
1f8fcdc0ee wcc: float-typed def/let DATA emit via SSoT helper (#129 A.1)
Extract emit_floatlit_data helper for def/let with float-typed top-level
initializer; replaces inline emit_lets float arm and adds previously-
absent emit_defs float arm. Helper peels N_CAST then N_UN(±, N_FLOATLIT),
bit-casts magnitude (f32 via union narrow), emits in little-endian byte
order, applies sign-XOR to top byte inside the loop (bit 31 for f32, bit
63 for f64). The byte-loop XOR avoids materialising 2^63, sidestepping
the strconv.i64tos INT64_MIN bug (#144 / task #37) on the wwstage self-
build path. Mirrored cstage (cgen.c) and wwstage (cgen.ww). Both stages'
float-typed def materialisation (cgexpr N_IDENT / cgident def-branch)
widened to route through the same LEAQ+MOVSS/MOVSD shape as float-typed
let.

Closes 3 latent bugs (all bootstrap-NEUTRAL, no current consumer):
- def: f64 = literal silently emitted undefined ref
- let: f64 = -literal silently emitted undefined ref (N_UN peel absent)
- wwstage let: f32 = literal silently truncated to low 4 of f64 bits

Test 917 (7 rows: f64_def_pos / f64_def_neg / f32_def_pos / f32_def_neg
matrix-closure / f64_let_neg / f64_let_pos / f32_let_pos) registered.
Make test: 180/180 incl. 990-997 byte-id + combined_ww_fresh + 995_self_
rebuild.

#144 (strconv.i64tos INT64_MIN two's-complement-overflow root) filed
separately as task #37 for its own fold.
2026-05-27 04:19:37 +09:00
61e6aab384 wcc: float arr[i]= spills X0 across index eval (#125)
Fix value-loss bug introduced as a #122 boundary in the float
arr[i]=v store: when the index sub-expr clobbers X0 (e.g.
`a[geti()]=1.5f32`), the value is lost. Mirror the scalar-deref
X0-spill template (cstage cgen.c:4187; line shifted from the brief's
stale :3859 cite by intervening #133/#135/#138 commits): for float
element only, replace PUSHQ AX (junk for floats — value is in X0)
with SUBQ $8,SP + MOVSS/MOVSD X0,(SP) before the idx/base eval;
mirror replace POPQ AX with MOVSS/MOVSD (SP),X0 + ADDQ $8,SP after.
Wwstage parallel. Non-float keeps PUSHQ/POPQ AX so the str/slice
3-word {ptr,len,cap} pop order at the end of the branch is preserved.
#122 trailing-store comment updated from "Deferred to #125" to a
positive cite.

Test 916: 5 rows — f64_call_index + f32_call_index canonical repros
(geti's body clobbers X0; pre-fix exit=2 from post-call residue,
post-fix exit=1 from the spilled 1.5) + f64_lit_index / _localvar /
_arith control rows for non-X0-clobbering index paths. f32_call_index
uses an int-arg call to dodge the sibling cs/ww f32-arg-push
MOVSD-vs-MOVSS divergence (#143, task #36 — orthogonal, filed).

Bootstrap NEUTRAL (zero current float arr[i]= callers in lib; only
[N]u8 byte-buffers like f64tos_buf). cs==ww byte-identical both
stages (990-997 + 916 inline cmp). Closes the #122 boundary-doc
loose end; completes the #122 family.
2026-05-27 03:30:15 +09:00
1de1b9a8a9 wcc: module-imported array indexed-load via cg_dotbase_addr (#128b)
Fix segfault-class memory corruption on `module.array[i]` indexed-read
where both stages emitted MOVQ-not-LEAQ on the module-qualified base
plus wrong stride. Extends the #135 cg_dotbase_addr/dotbaseaddr helper
to handle the SK_USE module-ident-base case: when bt is NULL/ty_err
and let_islet(base.str) resolves to TY_ARRAY, emit LEAQ base(SB),dst
instead of MOVQ. Wwstage parallel via letvartnode/N_TARRAY check.
Stride fix via let_var_type fallback in cgindex when n.lhs.kind==N_DOT.

Use-site fix per #135 precedent (Option B); preserves cgdot's MOVQ
semantics for the whole-array-assign defensive case (zero current
consumers). Test 915 carries 3 module-u16 indexed-read rows
(strconv.left_shift_table[0/2/4]) + 2 local-array controls; the
strconv.left_shift_table[2]:u32 probe segfaulted (exit 139) pre-fix
and exits cleanly post-fix. Broader width-variation rows (u8/u32/i32
module-imported) deferred as informational enhancement. Test 915
skips its inline cs==ww .s cmp on needs_import rows (line 217-222)
since `ww build` only drives cstage; reviewer externally verified
byte-id on /tmp/k128probe.combined.ww (driver-expanded form, no
imports). Future enhancement: 915 could read the driver-emitted
combined.ww and add a cmp leg there.

Bootstrap NEUTRAL (zero current module.array[i] consumers; strconv
decimal.ww uses IDENT-base from within package). 178/178 incl.
990-997 + combined_ww_fresh green. Sibling bugs #137 (chained N_DOT)
/ #141 (variadic-gather esz==2) / #142 (wwstage primsize-on-alias)
properly deferred to backlog.
2026-05-27 03:07:49 +09:00
684f59c48c lib/strconv: decimal.ww header documents i_sz/lowbit_lit hoist sub-cases (#32 c2)
Add a sub-bullet to the 8-spelling-divergences list documenting two
implementation sub-cases reviewer-fold3 surfaced during the 07e57ff
decimal.ha port: (1) `i_sz` per-iteration size-cast hoist inside
leftshift_newdigits' for-loop (decimal.ww:93); (2) `lowbit_lit`
stepwise boolean decomposition in should_round_up (decimal.ww:242)
dodging ww parser precedence on Hare's `(nd > 0 && d.digits[nd - 1]
& 1 != 0)` (ref/hare/strconv/decimal.ha:158). Both are in-file
instances of the documented hoist+restructure patterns — rule-9
doc-completeness, not new divergence. Combined.ww regen for
lib/strconv (compiler-imported into w6c + wwdump + smoke) uses the
build's include paths (`-I lib/ww -I lib/ww/lex -I lib/ww/parse
-I selfhost/cmd/wcc`) for transitive import closure; bare `ww build`
without these flags produces truncated output (reviewer-32c2 +
reviewer-fold3 both hit this).
2026-05-27 02:03:04 +09:00
bdfb5cda58 wcc: route DATA-emit through emitsymname mangler (#127)
wwstage cgen.ww emitdefconstants now uses the same emitsymname
mangler that LOAD/CALL sites use, replacing 8 lines of duplicate
`d.exported`/`d.nmod` logic. Rule-12 sea-of-stars consolidation —
one path, not two parallel paths that can desynchronise.

Cstage twin: cmd/w6c/cgen.c:8510 (mod_mangle in emit_defs). Bootstrap-
neutral post-90d31c5 (the duplicate PATH_MAX def that motivated the
divergence was cleaned up in drew's source-hygiene fold); all 5 tool
combined.ww emit cs==ww byte-identical asm post-fix. New test 913
(4 rows: exported i64 def, main-local i64 def, u64 width, multi-def
sequence) pins the simple-shape invariant forward — a future caller
introducing a colliding name produces the same symbol from both
stages by construction.

Drew's (a) ruling. Reviewer-127 noted the new path additionally
consults FFI (ffiresolve) which the old d.exported/d.nmod block did
not — incidental improvement to cs/ww symmetry beyond the mod-mangle
consolidation.
2026-05-27 01:51:12 +09:00
13441c5e2e w6a, wcc: add A_SARQ for signed arithmetic right-shift (#136)
Add SAR/SARQ to both assemblers' opcode tables (cstage cmd/w6a +
wwstage selfhost/cmd/w6a) — REX.W + D3 /7, parallel to SHR's D3 /5.
Encoding is the unary-on-CL form (SAR r/m64, CL), the only variant
the cgen emits today. cstage cgen + wwstage cgen sweep all 12 SHRQ
emission sites (6 per stage) so signed RSHIFT and signed RSHIFTEQ
route through SARQ (arithmetic, sign-extends MSB) instead of SHRQ
(logical, zero-fill). Pre-fix `let i: i32 = -200; i >>= 2;`
produced 0x3FFFFFCE (1073741774) instead of -50; cs==ww held because
BOTH stages emitted SHRQ, so the 990-997 byte-id gates were
gate-blind to this silent miscompile.

Sites covered (per stage 6, same shape in both):
  - plain TK_RSHIFT (cgbin / N_BIN ordered binop) — derives unsignd
    from operand types via type_isunsigned / nodeisunsigned, picks
    SHRQ vs SARQ at emit
  - chained-ptr-field compound RSHIFTEQ (cgen.c:3281-3317 area)
  - N_INDEX-lhs compound RSHIFTEQ (#133-expanded N_INDEX site)
  - deref-target compound RSHIFTEQ
  - top-level let compound RSHIFTEQ
  - IDENT-local compound RSHIFTEQ
All sites reuse the in-scope unsignd variable from the surrounding
SLASHEQ block (or derive one locally when not available). LSHIFTEQ
unchanged — SHL == SAL at the encoder, no signedness dispatch needed.

912_sar_shr_run: 5 rows. i32_neg_rshifteq (lead's repro, was wrong
1073741774 → now -50), i64_neg_rshifteq (wider type), i32_pos_
rshifteq (positive control, SARQ ≡ SHRQ on positives, no regression),
u32_rshifteq (unsigned control, still SHRQ), i32_neg_rshift_binop
(plain >> not compound, cgbin TK_RSHIFT site). Exit codes use small
absolute values with u8 wrap (-50 = 206) per Unix 8-bit exit.

Bootstrap-NEUTRAL — `grep -rE '>>=|>>\b'` in lib/+selfhost/ (excl.
combined.ww) returned zero callers of signed RSHIFT today; the only
asm shifts are on previously-broken paths. 990-997 + combined_ww_
fresh stay green. Closes the silent-misbehavior class on signed
right-shift across all 12 cgen emission paths in one fold per
rule-11. Foundation for Eisel-Lemire (strconv fold-4) big-int signed
shifts.
2026-05-27 01:29:24 +09:00
90d31c5b41 cmd/ww: drop duplicate PATH_MAX def; use os.PATH_MAX
main.ww was declaring `def PATH_MAX: u64 = 4096u64;` alongside the
imported os module's `export def PATH_MAX: i32 = 4096;` (lib/os/os.ww:91).
The duplicate triggered the cstage mod_map last-write-wins attribution
bug (#127): cstage emitted `main.PATH_MAX` for os's DATA entry too,
producing two 4096-byte slots and a wrong-module symbol. Both stages
shipped working binaries because LOADs all routed to main.PATH_MAX(SB),
but the divergence was real.

Removing the duplicate closes the only known #127 consumer per rule-7;
the defensive cstage cgen fix stays filed (task #11) until a new
duplicate-def collision surfaces. PATH_MAX use-sites in main.ww cast
`os.PATH_MAX: u64` at point-of-use for alloc/arithmetic, and use the
bare i32 form directly for slice .len assigns (which expect i32).

No cgen surface touched; byte-id-neutral on the 990-997 gates.
2026-05-27 01:16:16 +09:00
07e57ff9a6 lib/strconv: decimal arbitrary-precision arithmetic (#106 fold-3)
Port ref/hare/strconv/decimal.ha (~202 LOC Hare) → 314 LOC
lib/strconv/decimal.ww — decimal struct + 11 fns (trim,
decimal_shift, leftshift, leftshift_newdigits, rightshift, round,
decimal_round, helpers). 1:1 mechanical Hare-fidelity with 8
documented spelling-divergences. Shared engine for stof (fold-4) +
ftos (fold-5). Built atop 5 wwstage cgen prereqs
(#131/#133-expanded/#134/#135/#138) that closed gate-blind silent
miscompiles surfaced by the port. Test 922_decimal_run +
lib/strconv/test/decimaltest.ww (6 @test fns covering all 11 impl
fns).
2026-05-27 00:44:16 +09:00
d960971c6e wcc: continue runs post-step in 3-clause for and range form (#138)
`for (init; cond; post) { ... continue; ... }` and `for (let i .. xs)
{ ... continue; ... }` now emit a `post` (3-clause) or `rpost` (range)
label between the body and the JMP back to the cond-test. `continue`
jumps to that label, runs the post-step, then re-tests the loop
condition — mirrors C/Go/Hare semantics. Pre-fix both stages emitted
`JMP loop_top` for continue, SKIPPING the post-step → the value that
triggered continue never advanced → silent infinite loop on the first
matching iteration. Found by impl-strconv-fold2 during the fold-3
decimal.ha port: `leftshift_newdigits`'s `for (... i+=1) { ... else
if (d.digits[i]==p5[i]) continue; ... }` would infinite-loop at the
first equal digit.

BOTH stages were identically buggy → 990-997 cs==ww byte-id held →
gate-blind. Bootstrap audit (`grep -rE 'for \(let .*\.\.' lib/
selfhost/`) confirmed zero existing callers with continue in either
the 3-clause or range form; bootstrap-NEUTRAL.

Sites: cmd/w6c/cgen.c N_FOR + N_FORRANGE; selfhost/cmd/wcc/
cgenstmt.ww cgfor + cgforrange. 1-clause `for (cond)` byte-id
preserved (cont_target stays = loop_top when n.rhs == nil). Rule-11
carve-out: 3-clause and range share the lowered structure; fixing
one without the other would leave the same silent miscompile in
N_FORRANGE — one-class closure on the continue-skips-post bug, same
precedent as #133-expanded.

911_continue_run: 4 rows. for3_skip_one (lead's repro, was infinite
loop, now 4), for3_skip_two (nested continues, 30), range_skip
(Hare-range continue, was infinite loop, now 120), for1_continue_
byteid (1-clause regression assertion — bootstrap shape unchanged).
Pre-existing parser-side divergences (cstage silently drops post in
the never-used 2-clause `for (cond; post)`; wwstage doesn't support
infinite `for {}`) deferred to #139 — not in decimal.ha, no shared
class with the cgen continue-skips-post.
2026-05-27 00:12:39 +09:00
ade6840610 wcc: N_DOT-base address arithmetic for [N]T-field index (#135)
Strategy (a) use-site fix: new helper cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) detects `base.kind == N_DOT` whose field type
is TY_ARRAY and emits the field's address inline — LEAQ inner_off+
field_off(BP) for a value-struct inner, MOVQ inner_off(BP),reg +
ADDQ field_off,reg for a *struct inner. The TY_ARRAY-only gate (after
TY_NAMED peel) keeps the helper INERT on TY_PTR/TY_SLICE/TY_STR/
TY_TAGGED field kinds where the existing cgexpr(base) path is
correct (loads pointer/header value, then adds scaled index). Wired
at 6 sites: cstage cgassign N_INDEX-lhs plain ASSIGN + #133 compound
arm + cgindex N_INDEX read fallback; wwstage twin × 3. Closes the
silent-segfault on `(*struct).array_field[i]` reads and writes —
pre-fix cgexpr on the N_DOT base auto-derefed and loaded the field's
first 8 bytes as if they were a pointer, faulting on packed [N]u8
arrays (small u64 → unmapped page).

Bootstrap-NEUTRAL: zero working callers in either direction pre-fix
(symmetric READ + WRITE segfault evidence). All corpus + 990-997
byte-id + combined_ww_fresh stay green post-fix.

949_dotbase_arr_run: 3 rows direct runtime + cs==ww byte-id (READ
u8, plain WRITE u8, compound WRITE u8). Wider element widths and
value-struct base / pointer-field-control rows deferred — blocked by
orthogonal pre-existing wwstage divergences (i32-return ABI MOVSXD
vs MOVL, uninit-struct-let zero-init asymmetry) documented in the
test body. The TY_ARRAY-gate no-over-fire is implicitly verified by
994/995 (corpus exercises thousands of struct.pointerfield[i]
shapes; any over-fire would shift bytes).

Chained N_DOT (`outer.inner.array[i]` depth ≥2) deferred to #137 —
confirmed not in ref/hare/strconv/decimal.ha or sibling strconv/.
Not a fold-3 blocker; helper bails (returns false) on chained shape,
caller falls back to existing cgexpr path.
2026-05-26 22:56:00 +09:00