Commit Graph

622 Commits

Author SHA1 Message Date
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
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
497f1fa0d3 lib: fmt fprint family over io.handle; remove the fdsink workaround (#5)
Graduates the fmt fprint family (fprint/fprintf/fprintln/fprintfln + internal putbytes/writeone/format*) from io.stream to io.handle, so a file (fd) prints directly through io.write's file-arm (commit-1). Removes the fdsink placeholder -- the fake-stream-vtable-over-os.write shim that stood in for the missing handle. The 8 stdio wrappers route over os.STD{OUT,ERR}_FILENO (new i32 filenos in lib/os; os is the import floor, so it can't hold an io.file-typed handle like Hare's os::stdout_file -- consumers cast i32 to io.file). Migrates the fd-shim sentinel tests 777/780/781 to fprint-over-handle as their headers designed, cstage-only per the pre-existing #209 (fmt is wwstage-uncompilable). Regenerates the 6 os-embedding combined.ww.
2026-05-31 18:51:12 +09:00
06c00e0e1b lib: io.handle union + seeker + handle-typed read/write/close/seek dispatch (#5)
Ports ref/hare/io handle.ha: a handle is (file | *stream); ww stream is already *vtable (#94 collapse) so the payload is (file | stream). file=i32 (Hare int is 32-bit, ww int is 8B word -- width-faithful, USER-ruled). read/write/close/seek/tell match on the handle: file-arm to os.read/write/close/lseek (os plays Hare sys role), stream-arm to the unchanged st_* vtable bodies; seeker is the 4th vtable slot (copier deferred). On a file-arm syscall error the stub returns errors.unsupported with a #199b marker -- faithful errno to io.error needs io.error to spread ...errors.error (#204/#199b-blocked); only the error value is lossy until then, the type stays faithful. Regenerates w6c/wwdump combined.ww.
2026-05-31 17:30:55 +09:00
2e760d070d Revert "wwstage: add tinfo.module field, P0 of (module,name) NAMED interning (#10)"
This reverts commit f68a4c185b.
2026-05-31 15:39:13 +09:00
f68a4c185b wwstage: add tinfo.module field, P0 of (module,name) NAMED interning (#10)
Append module:str to tinfo and set "" in newtype (the sole tinfo
constructor); the field is set-not-keyed here — P1 keys typeeq's NAMED
arm on (module,name) to collapse cross-module same-nominal duplicates.

Byte-id neutral: only w6c/wwdump combined.ww regenerated (sole typ.ww
embedders); cs==ww and 990-997 hold; neutrality proven on the tinfo-free
corpus (w6a/w6l/ww/smoke).
2026-05-30 15:00:53 +09:00
c4e29df4e9 wwstage: zero-init 8B composite locals in bare-let to match cstage (#213)
wwstage's cglet no-rhs path zero-inited only 8B primitives (MOVQ) and >8B composites (XORQ run), so an 8B *composite* local (single-field struct/tagged, e.g. struct{src:*vtable}) declared bare (let b: box;) was left uninitialized -- reading an unassigned field returned stack garbage (a silent read-before-init), and it diverged from cstage which zero-inits any 8B local (cs!=ww byte-id, surfaced by #5's bufio box{src:io.stream}). Add the missing arm: a non-array composite of size 8 emits MOVQ $0, matching cstage's no-rhs sz==8 zeroing. cstage unchanged (already correct -- align wwstage UP). Scope is 8B-only: cstage does not zero-init sub-8 composites either (sub-8 falls through to nothing on both stages, already cs==ww), so zeroing sub-8 on wwstage would create a new divergence; the sub-8 read-before-init garbage is a separate shared-both-stages latent (#20). Adds test/wcc/790 (8B byte-id row + read-before-init correctness lock reading 0 on both stages). rule-10 align-up; closes the #213 8B-composite slice; unblocks post-eFinal #5.
2026-05-30 09:36:32 +09:00
45e5d74047 w6c+wwstage: tag a bare value widened into a NAMED-alias union variant via structural fallback (#15)
Variant selection (cg_tag_for_variant / flatvariantidxt) matched union variants by exact type only, so widening a bare value (e.g. *vtable) into a union with a NAMED ptr-alias variant (stream = *vtable, in handle = (file | stream)) found no match and the tag defaulted to 0 -- the wrong variant. In the compiler this hit emitbytes' io.write(&cgoutstream.vt) once io.write took a handle, writing the asm to a garbage fd -> empty .s -> w6c_ww miscompiled everything. Add a second selection pass: when the exact pass finds no variant, structurally compare the bare source against each NAMED-alias variant's unwrapped type; exact-match still wins in pass 1 (so a bare i64 stays the i64 variant, not oserror=!i64, which kept the os/errno union building). A >=2-structural-match collision guard (extending #218's) hard-errors LOUDLY on genuine nominal ambiguity (two ptr-aliases to the same struct) instead of silently first-picking, citing #199b/#10. Symmetric across cstage (cmd/w6c/cgen.c) and wwstage (selfhost/cmd/wcc/cgenutil.ww). One-level NAMED unwrap (chained ptr-aliases unmatched, unexercised -> #17). Adds test/wcc/789 (positive widen byte-id+runtime + degenerate-ambiguity reject guard, both stages). Unblocks post-eFinal #5's handle surface. rule-10 fix-up.
2026-05-30 09:04:33 +09:00
419c896ad0 wwstage: match cross-module union variants by (module, leaf) pair (#13)
wwstage's checker rejected matching an imported union's variants cross-module: casevariantin/casecovers' typeeqast did a raw streq, so a union's bare variant "unsupported" failed to match the dotted case pattern "errors.unsupported" (cstage compares resolved-Type identity, qualifier-agnostic). Add a (module, leaf)-pair fallback after typeeqast: reduce both the case pattern and each variant to (module, leaf) and match on pair equality -- a dotted name keeps its own qualifier, a bare name takes the union's defining module (taggeddefmod, via the aliassym hop chain). This closes BOTH directions: the false-reject of valid cross-module match AND a false-accept of a foreign same-leaf qualifier (case othermod.foo vs errors.error now rejected, matching cstage). Handles the nested errors.error-in-io.error case (the dotted variant keeps mod=errors, not the union's mod=io). typeeqast stays the first check so currently-valid code is byte-id-unchanged; the pair-match fires only on the previously-rejected qualified-vs-bare mix. Wired into casevariantin, casecovers, and the is/as caller. Adds test/wcc/787 (cross-module positive, exhaustiveness, foreign-qualifier reject-guard, dotted-variant body). Unblocks #5's cross-module io.error/errors.error decomposition. rule-10 fix-up; #10-family (wwstage cross-module resolution).
2026-05-30 06:47:44 +09:00
fc9b486fb4 lib: port errors.errno + os errno/strerror sys-layer (#6)
Hare-faithful port of errors::errno (ref/hare/errors/{rt,common,opaque}.ha): the 13 named common error conditions, opaque_data/opaque_ (the type-erased tail whose strerror fn-ptr defers to os.strerror), and errno(os.errno) error mapping the ~12 mapped errnos to named conditions and wrapping the unmapped tail in opaque_. The raw errno type (!i32, kernel-int width, distinct from oserror's !i64 negative raw return), the E* constants, and the strerror message table live in lib/os: ww folds Hare's sys role into os, so os is the import floor that lib/io and lib/errors build on -- documented in lib/CLAUDE.md (os never imports io or errors). errors.error is explicitly enumerated, matching Hare; the ...errors::error spread is only io.error's (blocked by #199b). Prereq for post-eFinal #5's faithful io error mapping; retires the nomem-collapse interim. Adds errnotest (mapping / opaque-tail / strerror) + test/wcc/902_errno_run. Landing required two wwstage cgen fixes (#9 struct-variant-large-union return, #11 deref-store alias narrow). Divergences cited at-site: bare-type-name return -> let+return; switch fall-through vs Hare's exhaustiveness-only default; opaque_ const dropped.
2026-05-30 05:58:06 +09:00
0eb3465919 wwstage: resolve deref-store width through type aliases so *(!i32-alias) narrows (#11)
The deref-store *p=v integer arm computed width by name-keying the pointee node (primsize(pe.str)), so a pointer to a !-flagged or otherwise non-primitive-named alias (os.errno = !i32) fell to the MOVQ default where cstage type-resolves to MOVL (cgen.c:4647-4652) -- cs!=ww and a latent 4-byte over-write. Add a primsize-first fallback to the existing typenodeprimresolved (peels N_TBANG/N_TENUM/N_TNAME alias chains to the underlying primitive) so *(!i32-alias) narrows to MOVL. primsize-first preserves *bool/*i32/*u8 byte-id (typenodeprimresolved excludes bool). Adds test/wcc/786 (store through *(!i32-alias) then read an adjacent field -- over-write guard -- plus a plain-*i32 control). The residual name-blind cases (non-ident pointers, str/float/bool aliases, size-2 i16/u16) are routed to #10/#12. Unblocks errno's opaque_ tail store. rule-10 fix-up: wwstage aligned up to cstage.
2026-05-30 05:29:58 +09:00
34c437fd63 wwstage: register error-structs in collectstructs so large-union struct-variant returns widen (#9)
collectstructs registered a struct only when the typedecl body is N_TSTRUCT, so an error-struct (type X = !struct{...}, whose body is N_TBANG{N_TSTRUCT}) never entered wwstage's c.structs table. The name-keyed structlookup then missed at the return-widen sites, and wwstage dropped the struct construction when returning a struct variant of a large (>4-eightbyte) union -- wrong runtime value and cs!=ww. cstage has no struct name-table (pure tinfo) and was correct. Peel the N_TBANG body in collectstructs so error-structs register; both existing cstage-mirrored widen arms then fire. Provably byte-id-inert: no committed source defines a !struct today. Adds test/wcc/785 (struct-variant return + named-void control, both-stage byte-id + runtime). The >4-eightbyte 5th-word truncation on return remains, symmetric (cs==ww) and unread by the tag/early-word path; #222's sret hidden-pointer cutover is the committed fix (table-retirement tracked as the wwstage->tinfo SSoT arc). Aligns wwstage up to cstage (rule-10).
2026-05-30 04:54:12 +09:00
7d39f6d623 lib: collapse the parallel vstream scaffold onto the single Hare io surface (#94 fold-eFinal)
The Option-C parallel _v vstream API was scaffolding to bring the io stack up alongside the old surface; carrying both permanently is a rule-9 divergence from ref/hare, which has exactly one io surface. Collapse onto that surface (stream = *vtable, ref/hare/io/stream.ha) and rename the _v symbols to their Hare names (io vstream->stream, fmt vfprint->fprint, bufio/memio/log surfaces, log.new). Deletes the 4 lib/*/vstream.ww scaffold files; regenerates w6c/wwdump combined.ww. cstage and wwstage stay byte-identical and combined_ww_fresh holds; all 220 tests pass.
2026-05-30 03:24:23 +09:00
80184a3acf wwstage: make the cgdot alias-peel struct-break module-aware (#223)
The wwstage cgdot #191 alias-peel loop broke on a name-keyed any-module
structlookup, so a receiver whose alias name collides with a struct of the
same name in ANOTHER module resolved to the foreign struct and fell through to
an undefined `name(SB)` global instead of the field load. The eFinal FLIP
renames io's `vstream` -> `stream`, which collides with memio's `stream`
struct, so io.read/io.write/io.close's `match (s.reader)` emitted
`MOVQ reader(SB), AX` (reader is also a type-alias) -> cs != ww (cstage chases
the nominal TY_NAMED.under pointer chain, module-correct). Gate-blind: on
master both stages emit the same wrong store so byte-id stays green; the FLIP
corpus is the first to put the io-alias/memio-struct collision in one build.

Fix (wwstage-only align-down; cstage is the authority and is untouched): make
the peel's struct-break MODULE-AWARE — break only on a same-module struct (a
genuine struct-value receiver); a same-module alias keeps peeling to its
underlying (io.stream -> *vtable -> the pointer field-load arm); a foreign leaf
keeps the prior any-module heuristic. New structsamemod / aliassamemod mirror
the same-module-first pass already in structlookup / aliaslookup. This is not a
naive alias-first reorder (which would reintroduce the mirror collision: a
same-module struct plus a foreign same-leaf alias). Peel-only — the direct-
struct arm's broader cross-module same-leaf-STRUCT name-keying is filed as #224.

#208-family (name-keyed resolution dropping to a wrong global) but in cgen, not
the checker; #213 is distinct (cosmetic local-struct-match divergence).

test/wcc/784_xmod_alias_struct_collide_run: collision (cross-module alias-vs-
struct, same leaf), symmetric (guards the same-module-struct break against a
naive reorder), and a no-collision control — branched callee. The discriminating
net is cs.s == ww.s (the path is gate-blind and cstage is correct, so byte-id
flips when wwstage is fixed); confirmed by source-revert. The FLIP's combined.ww
is now cs.s == ww.s byte-identical.
2026-05-30 02:00:07 +09:00
1175711021 w6c+wwstage: route sret dest to the global symbol on struct-return into a global (#220)
Assigning a >24B by-value struct-return into a GLOBAL lvalue dropped the
struct body: the sret dest was routed to a BP scratch temp and only the
8-byte return pointer was stored (`MOVQ AX, g(SB)`); the callee wrote the full
struct to the scratch, which never reached the global. A BP-relative dest
offset cannot name a global symbol. Pre-existing GATE-BLIND silent miscompile
— both stages emit the same broken store, so byte-id (990-997) stays green
while runtime is wrong — latent until the eFinal io surface put a global
`cgoutstream: memio.stream` (>24B) on the path, where it made cgen.ww's
self-built w6c_ww buffer every function body into a corrupt global (pos stayed
0) and emit prologue-only output.

Fix, both stages, byte-identical: route the sret dest pointer to the global
symbol so the callee writes the full struct through RDI straight into the
global. cstage adds cg_sret_dest_sym, mirroring the existing str/slice global
arm (skip the @sretscr scratch, emit `LEAQ masym(sym), DI`). wwstage carries
the lhs IDENT node (sretdestnode) and emits `LEAQ name(SB), DI` via emitsymname
— identical to cstage's symbol mangling, verified cs.s==ww.s on the probe and
across 990-997. #211-family (by-value struct + global/pointer), but a distinct
site: the cstage assignment-store into a global, not the wwstage call-return.

N_LET-global static-init (`let g: T = mk()` at top level) is a separate,
independently-broken path (#221) — link-fails for init-via-call, returns 0 for
constant init — not the sret-receive gap and not on the eFinal path; deferred.

test/wcc/940_global_sret_run: global assign (plus a branched callee to defeat
const-fold), through-pointer mutation (the io vtable-callback shape that
surfaced this), and local-init/assign regressions — runtime asserts on both
stages (the net, since byte-id is gate-blind here) plus cs.s==ww.s.
Discrimination confirmed by revert+rebuild: with the global arm disabled,
global_assign emits the truncated store and exits 1.
2026-05-30 00:46:57 +09:00
176904dffc w6c+wwstage: tag the outer widen of a nested multi-variant union (#218)
The outer widen of a NAMED multi-variant union value into an enclosing union
mis-tagged: the store took the tagged-subset path (inner value at slot+0 plus
a sub-variant remap, collapsing every inner sub-variant onto outer tag 0),
while the match-extract reads the nested layout (outer tag at +0, inner 16B
value at +8). Store and extract disagreed, so the match selected the first
arm. Pre-existing silent miscompile, latent because error-origination sites
(`let e: io.error = <leaf>; return e`) were gate-blind — no test discriminated
a freshly-originated error at a branched caller; the io vstream surface is the
first to do so.

Fix, both stages, byte-identical: cg_variant_match (cmd/w6c/cgen.c) and its
wwstage mirror cgvariantmatch (cgenutil.ww) fall back to structural equality
of the unwrapped tagged unions when the alias collapse loses nominal identity
(a NAMED outer variant vs an unwrapped-tagged source); the widen store now
writes the inner value at slot+8 and the outer tag at +0, matching the
extract. The inner union's build/payload/extract already worked (a destructure
through the outer round-trip recovers the inner payload) — only the
outer-widen store was wrong.

Collision guard (the fallback is unsound without it): structural matching
cannot disambiguate two nominally-distinct same-shape variants in one outer
union. That is unreachable under today's nominal-lossy collapse but inverts
the moment #199b lands the nominal layer, so if >=2 outer variants
structurally match the source we hard-error at compile time citing #199b —
both stages, an enforced invariant rather than a "rare, trust it" assumption.

Folds #219: the wwstage tinfo typeeq (lib/ww/typ.ww) had no TY_TAGGED branch
and fell through to `return true` (any two tagged unions compared equal);
cstage type_eq (type.c:269) has the structural branch. The structural fallback
above is the first and only caller to compare two bare tagged unions, so #219
is unexercised — and therefore ungateable — in isolation; it folds here per
the rule-11 couldn't-split carve-out (same structural reason as #206's
N_TTUPLE fold). The added branch mirrors cstage type_eq, tightening wwstage
into alignment.

test/wcc/925_nested_union_widen_run: outer-arm select, destructure-after-
propagation (payload survives the round-trip), destructure-let, single-variant
control, and the collision-guard compile-error, each with a cstage==wwstage
byte-id check (the path is gate-blind). Interim until #199b/B-full lands the
true nominal wrapped-slot layout.
2026-05-29 23:19:36 +09:00
10cb835f99 lib: complete the parallel vstream surface to Hare value-return shape (#94 fold-eFinal prep)
The #94 Option-C vstream surface was left incomplete and structurally
divergent from Hare: constructors heap-allocated and returned (X | nomem)
or used out-params instead of Hare's by-value stack ownership; memio lacked
reset/buffer/borrowedread; bufio's scanner was never ported to the vtable.
This is the additive half of the eFinal collapse — OLD surface stays fully
live; the destructive FLIP (delete OLD + drop _v + repoint) is the next
commit.

Reshape all constructors to VALUE-RETURN (field-by-field sret; the heap +
nomem was an unnecessary crutch — wide slice-bearing struct return-by-value
is byte-id-proven, cf 925_sret_struct_return_run). memio fixed/dynamic/
dynamicfrom, bufio init, log new now return the struct by value; the nomem
is gone with the alloc that forced it.

memio: unify the per-flavour ctx structs onto one `stream` (vt at offset 0);
collapse fixed_string + dynamic_string into a single string() over the common
header (bare-str return is the ratified rule-9 frombytes carve-out, cited at
the site per ref/hare/memio/stream.ha:81); port reset/buffer/borrowedread as
single fns over the header.

bufio: collapse the EXISTING scanner subset (newscannerbuf/scanbyte/scanbytes/
scanline/finish + setflush/flush/unread/isbuffered) onto the vtable, with src
now io.vstream so reads go through io.st_read. The Hare scanner functions ww
never implemented (scanrune/scanstring-arbitrary-delim/readtok/readline/
auto-grow newscanner) are out of scope and deferred to #217 — eFinal is a
collapse, not a feature expansion.

Keep the explicit (&fn): *io.T casts on vtable-slot stores (cgen-neutral;
avoids the #214 (X|void) over-acceptance surface; dropping the casts is a
deferred #206 payoff gated on #214).

Self-gate: 776 (memio) 18/18 and 778 (bufio) 27/27, every row carrying a
cs.s == ww.s byte-id check — bufio/fmt/log are not compiler-embedded, so
these rows are their only byte-id coverage. 779/781 stay STAGE_CS-only
pending #209. Regen w6c+wwdump combined.ww (io+memio are the embedded
modules).
2026-05-29 21:14:39 +09:00
f9f83faca7 wwstage: remap error tag on try-propagate across reordered unions (#173)
wwstage cgtryprop returned the operand union's RAW tag when propagating a
`c(s)?` error, while cstage (cmd/w6c/cgen.c:6161-6184) remaps it to the
enclosing return union's variant ordering. When the operand and return
unions differ in variant order, wwstage propagated the WRONG error variant
at runtime — gate-blind: byte-id (990-997) and cstage==wwstage asm both pass
because the bootstrap only ever tries same-order unions, while the
differing-order case is silently wrong.

Port cstage's remap loop into cgtryprop (iserror-only): for each error
variant whose return-union index differs, emit the CMPQ/JNE/MOVQ/JMP that
rewrites the tag in AX; the error payload words (DX/CX/R8) are untouched and
ride the RET. Mirror cstage's emission exactly — lazy tryprop_ret allocation
on the first remap, j==i skip, j<0 fallback, no dead label when empty,
identical label strings and operand order — so same-order emits zero extra
instructions (byte-id preserved) and differing-order is now byte-identical
cstage==wwstage.

Scope: error-variant remap only. wwstage's hardcoded success-tag=0 and
iserror-only error detection (vs cstage's cg_tagged_success_tag +
cg_variant_is_error legacy fallback) diverge for non-idx-0-success or
unmarked unions — also gate-blind, also latent — filed separately as #216.

test/wcc/925_tryprop_tag_remap_run: 5 rows (differing-order for both error
variants, success unwrap, same-order byte-id witness, and a multi-word !str
payload row asserting the payload bytes survive the remap), each with a
cstage==wwstage .s byte-id check.
2026-05-29 19:43:48 +09:00
f6ac7fb2f8 wcc: accept bare &fn into a fn-pointer-alias slot via a caller-site gate (#206)
A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)`
vtable field without an explicit cast: cstage type_eq on TY_NAMED is
pointer-identity, so a structural `*fn(...)` referent never matched the named
`*reader` variant; wwstage accepted it only via an accidental catch-all
leniency. harec accepts bare &fn through hint-directed alias adoption at the
address-of site (check.c:3594-3626) while keeping pointer assignability
strictly nominal (types.c:1039-1066), so a materialized `*fn` value never
launders across alias names.

Mirror that decision without threading a type hint through the bottom-up
cexpr: keep type_assignable / isassignable fully nominal, and add a
caller-site helper (assignable_addrfn) at the assignment boundaries
(let-init, struct-literal field-init, assign, return, call-arg, array
element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the
destination (or exactly one tagged variant) is a pointer-to-fn-alias whose
underlying fn signature structurally matches. A materialized `*fn` value, a
distinct same-signature alias, and an ambiguous multi-variant target all stay
rejected. Both stages share the rule; wwstage's lenient pointer-fn punt
becomes a confident reject. ww has no methods, so a `value.leaf` slot is only
ever a fn-pointer field and this never over-admits.

The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a
tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it,
newly rejecting a legitimate structural assign. Add the N_TTUPLE structural
case (rule-10), restoring test 766.

cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s
zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean
three-field-vtable dispatcher) and the negatives. Tagged-slot negatives
(ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate
`(X|void)` void-variant leniency (#214) still admits them; 783 pins them
cstage-only, to graduate when #214 closes (required before wwstage becomes
the authoritative selfhost checker).

Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures
(700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs
rt_malloc); identical with or without this change, so bisect-clean for #206.
2026-05-29 18:05:53 +09:00
4e1181fd8c wcc: resolve value-receiver field-call type via callee, not global leaf (#208)
wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an
N_DOT callee with a value or chained receiver, binding whatever same-named
global headed the scope bucket. Under a late-os combined.ww concat order
this resolved io's `s.read(...)` to os.read (i64) instead of the field's
fn type, so checkretassign confidently rejected a valid tagged return — an
import-order-sensitive false positive. cstage resolves a call result solely
from the callee expr's own type (check.c:1378-1433, mirroring harec
check_autodereference 1566-1581); drop the global-leaf else-arm so value
and chained receivers fall through to the existing fn-VALUE path at
check.ww:2455. SK_USE module-qualified calls are unchanged.

ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the
global hit was never legitimate. Zero .s delta across all 5 bootstrap tools
(the branch is dead in the bootstrap); test 776 graduates to both stages
(os-late order, byte-identical).

The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives
a call's return shape by name (fnretlookup), so a value-receiver field call
whose leaf collides with a same-named global of a different register shape
mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by
test/wcc/782 (graduates to STAGE_WW on #211 close).
2026-05-29 15:27:50 +09:00
df287846c5 lib/fmt: port V-side fprintln/fprintfln/bsprintf/asprintf (#94 fold-e7)
V had vfprint / vfprintf but no compositions over them, so callers
needing the newline / printf-newline / bounded-buffer / heap-grow
shapes still routed through the OLD io.stream-shaped fprintln /
fprintfln / bsprintf / asprintf. Port the four compositions into
vstream.ww as the v* twins: vfprintln + vfprintfln chain a
"\n" vputbytes after the underlying primitive; vbsprintf threads a
caller buffer through memio.fixed_vstream and returns the prefix view;
vasprintf grows through memio.dynamic_vstream and shrink-copies to a
tight allocation before io.st_close.

Bundles the two memio enablers (fixed_string / dynamic_string in
lib/memio/vstream.ww) that vbsprintf / vasprintf depend on directly,
per drew-approved exception to one-class-one-commit
(feedback_refactor_routing_same_class_drops applies — helpers are
direct prereqs, not unrelated churn; the bus-routing site lives in
v* fmt code, not in memio). They mirror OLD memio.string (memio.ww:
102) over the per-flavour *fixed_ctx / *dynamic_ctx intrusive cast,
same shape as the read/write callback split at memio/vstream.ww:144.

Mirror sites:
  vfprintln    fmt.ww:240 fprintln          ref/hare/fmt/wrappers.ha:48
  vfprintfln   fmt.ww:740 fprintfln         ref/hare/fmt/wrappers.ha:69
  vbsprintf    fmt.ww:839 bsprintf          ref/hare/fmt/wrappers.ha:42
  vasprintf    fmt.ww:873 asprintf          ref/hare/fmt/wrappers.ha:29

Divergence vs Hare on vbsprintf: Hare returns `(const str | nomem)`;
ww collapses to `(str | io.error)` so the underlying vfprintf io.error
arm stays uniform. The fixed_vstream nomem widens into io.error
explicitly (no `memio.fixed_vstream(buf)?`) because #173 (TRY-on-
tagged-return both-stages broken) is still open — same shape memio/
vstream.ww adopted at line 87-99 for fixed_vstream itself. vasprintf
keeps OLD's bare `str` return (no nomem variant on public surface).

ken cs==ww mechanical: additive only, both stages compile identically.
fmt is NOT embedded in any selfhost main.combined.ww (grep verified
pre-impl: zero `^package fmt;` hits in selfhost/cmd/*/main.combined.
ww). memio.vstream.ww IS embedded in w6c + wwdump combined.ww (lib/
ww/cgen.ww uses memio.dynamic for buffer growth); the two memio
helpers regen-and-commit via ww build per #110 SSoT.

test/wcc/781_fmt_vstream_compositions_run.c (cstage-only per #209): 4
rows pin all four V wrappers — fdprintln_v_run_basic (newline shape),
fdprintfln_v_run_fmt ({n}-placeholder + newline), bsprintf_v_basic
(fixed buffer + returned view + caller bytes), asprintf_v_basic
(owned heap str + os.free roundtrip). Mirror of 777/780 cstage carve-
out (#209 wwstage formattable match-arm bail). Byte-id graduates with
#209 close. 214 total tests green (was 213).
2026-05-29 12:28:12 +09:00
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