Commit Graph

112 Commits

Author SHA1 Message Date
f1440bf9e8 cstage+selfhost+test: mangle fn labels by module (#9)
Both stages emitted fn TEXT labels by leaf only; lib/os and lib/io
exporting the same leaves (read, write, close) collided at link.
lib/fmt + lib/log worked around with @symbol("rt_syscall") stubs.

Drop d->export from the fn skip rule in mod_collect (both stages) so
exported fns mangle as <module>.<name>. Let/def/type keep current
behavior. Skip retained for {@symbol, main, empty-module}.

Add cur_mod thread through cgfn + mod_lookup_for_fn(name, hint) at
all 4 label-emit sites (TEXT def, LEAQ N_IDENT, CALL N_IDENT, CALL
N_DOT). Wwstage mirror: emitfnname + modlookupforfn + curmod.
Invariant comment pinned in both stages.

ww2 == ww3 == ww4 byte-identical at the new label format.
706_fnlabel_mangle covers same-leaf cross-module CALL + private-leaf
cur_mod disambiguation through a fn-pointer rvalue.

Wwstage LEAQ-of-fn N_DOT (`let p = mod.fn` rvalue) is a pre-existing
gap; deferred to a follow-up. fmt/log rt_syscall stubs untouched
here; cleanup follows.
2026-05-15 23:41:01 +09:00
98460e0220 cstage+selfhost+test: fix nested call-rhs silent zero in structlit fill (3rd of family)
Sister bug to #17 / #18. The structlit-fill helper handled nested
N_STRUCTLIT field values but a struct-typed field whose VALUE is an
N_CALL (call returning a struct, #4 cgreturn ABI) fell through to the
cgexpr-then-AX-store path — landing AX=first qword and silently
dropping DX/CX. For 16B/24B inner returns the trailing 8B/16B stayed
zero (whatever was in the destination slot beforehand).

Fix: a new N_CALL+struct branch in cg_structlit_fill / cgstructlitfill,
placed between the nested-N_STRUCTLIT recursion and the scalar
cgexpr fallthrough. Emits cgexpr -> BX reload (non-BP modes only) ->
MOVQ AX/DX/CX x full + sized tail (MOVL/MOVW/MOVB) per #4's receive
shape.

INVARIANT (commented inline both stages): between cgexpr(N_CALL) and
the AX/DX/CX stores below, no instruction may touch AX/DX/CX. Only
the BX reload (MOVQ srcoff(BP),BX or LEAQ name(SB),BX) is safe.

Sized-tail dispatch is {1->MOVB, 2->MOVW, 4->MOVL, else MOVQ}. Unlike
the scalar fallthrough — which still uses the {1/4/else MOVQ} shape
to stay byte-identical with cstage pending #13 — the new branch is
correctness-by-construction: MOVW for tail==2 only fires on call-rhs
shapes that didn't compile before, and both stages emit it
symmetrically (705's 10B inner row pins this).

Guard `fsz <= 24 && fsz%8 in {0,1,2,4}` mirrors #4's cgreturn ABI:
>24B falls through (sret deferred), and fsz%8 in {3,5,6,7} would need
shift-store — also unsupported by #4. Filed as task #21 (covers both
cgreturn and call-rhs's identical gap).

Two #15 sidesteps, both documented inline:

  1. wwstage's fi.fsz for an inner-struct field is slot-padded
     (8-rounded), not natural — using it would emit 2x MOVQ where
     cstage emits MOVQ+MOVL for a 12B inner. The new wwstage branch
     uses structnaturalsize(csi) to recover the natural size, matching
     cstage's fl->type->size (check.c hands the helper natural sizes).
     This sidesteps #15 without touching its scope.

  2. The outer struct's totsize diverges across stages when
     maxalign<8 (wwstage rounds to 8 universally; cstage to maxalign).
     The 705 test rows pin `x: i64` on the outer to force outer
     maxalign=8, keeping BP offsets stable across stages. Test-side
     sidestep only; also #15 territory.

Files:
  - cmd/w6c/cgen.c              cg_structlit_fill extended
  - selfhost/cmd/wcc/cgenutil.ww  cgstructlitfill mirror
  - selfhost/cmd/{w6c,wwdump}/main.combined.ww  auto-regen
  - test/wcc/705_nested_call_rhs.c  8 rows, table-driven; pins cstage
    exit + wwstage exit + .s byte-identity. Tail widths 0/4/2/1, dst
    modes DST_BP + DST_PTR_LOCAL, shallow + 3-deep.
  - Makefile  705 wiring

Test: 65/65 PASS. 994_w6c_ww + 995_self_rebuild PASS (byte-identity
holds — load-bearing).
2026-05-15 19:07:29 +09:00
99a68a6a57 cstage+selfhost+test: extend structlit-fill helper to N_ASSIGN N_DOT lhs (4 flavors)
Sister fix to #17. The BP-rel helper from #17 covered N_LET /
N_ASSIGN N_IDENT-lhs / N_RETURN; the four N_ASSIGN N_DOT-lhs
structlit walks still went through the inline `cgexpr(field.lhs);
store-AX-sized` shape and silently dropped trailing bytes when a
struct-typed field's value was itself an N_STRUCTLIT. Affected dot
flavors: single-dot via_ptr / global / BP-rel and the chained-dot
walker (depth >= 2, all three root flavors).

Extend `cg_structlit_fill_bp` / `cgstructlitfillbp` into
`cg_structlit_fill` / `cgstructlitfill` taking a destination mode
(DST_BP / DST_PTR_LOCAL / DST_GLOBAL = 0/1/2), srcoff (PTR_LOCAL),
srcname (GLOBAL), and disp accumulator. `disp` grows by foff on
descent; srcoff/srcname stay constant across the call tree. The
pre-#17 wrappers are preserved byte-identically by delegating with
mode=DST_BP — 995_self_rebuild byte-identity holds for the no-
nested-STRUCTLIT case that selfhost source actually uses.

The non-BP modes reload BX before the ELLIPSIS zero-fill loop AND
before every field store (tagged, scalar, and the cgexpr leaf).
This is correctness-by-construction — cgexpr clobbers BX between
fields, and the redundant reload only fires on shapes that didn't
compile before. The four dot-flavor sites in each stage now compute
their dst mode + disp and call the shared helper (reducing each
from ~80-130 inline lines to ~5-12 lines of dispatch).

Stage signature asymmetry: cstage threads Local** for cgexpr; ww-
stage takes explicit totsize because #15 (split totsize into
naturalsize + slotsize) is still pending and the dot sites need
structnaturalsize while the BP-rel sites need si.totsize. Both
asymmetries are documented in the helper docstrings.

704 covers 8 rows (24 checks: 8 cstage exits, 8 wwstage exits, 8
cstage-vs-wwstage .s byte-identity diffs): 6 dst-flavors (single-
dot local/ptr/global, chained-dot local/ptr/global) plus single-
local 3-deep and single-ptr 3-deep to pin disp threading through
the helper's recursion and through DST_PTR_LOCAL BX reloads.

The nested struct-typed CALL rhs in field-walks has the same shape
as the STRUCTLIT bug fixed here but the helper only handles
STRUCTLIT — tracked as task #20.
2026-05-15 18:45:21 +09:00
9d03e02881 cstage+selfhost+test: fix nested STRUCTLIT silent zero in BP-relative fills
Pre-existing landmine surfaced by #5. For a struct literal whose
field value is itself an N_STRUCTLIT of a struct-typed field, the
inline field-walk did `cgexpr(field.lhs); store-AX-sized`. cgexpr
has no whole-struct-in-register convention, so the nested literal
landed AX = first qword and the trailing bytes silently stayed zero
(or stack garbage). Three BP-relative sites in each stage hit it:
N_LET, N_ASSIGN N_IDENT-lhs, and N_RETURN N_STRUCTLIT.

Fix: shared cg_structlit_fill_bp (cstage) / cgstructlitfillbp
(wwstage) helper handles TK_ELLIPSIS autofill, tagged-field
widening, float vs scalar store dispatch, AND recurses on
struct-typed N_STRUCTLIT field values at bp_off + field_off. All 3
sites in each stage now call the helper instead of the inline walk.

Scalar store dispatch is the explicit {1->MOVB, 4->MOVL, else MOVQ}
shape (not fieldstoreop, which would emit MOVW for fsz==2) to stay
byte-identical with cstage pending task #13. Sister N_ASSIGN N_DOT
structlit walks (via_ptr / global / BP-relative-through-N_DOT) keep
their inline walk and still drop nested-STRUCTLIT silently — tracked
as task #18.

703 covers 6 rows: let_nested_i64, let_nested_3deep, let_nested_i32,
let_nested_middle (i64; switch to i32 once #15 lands),
assign_ident_nested, return_nested. 995_self_rebuild byte-identity
preserved.
2026-05-15 18:14:33 +09:00
c4347499ee cstage+selfhost+test: fix (*p).f silent drop in N_DOT lhs (read+write)
Pre-existing landmine surfaced by #5 (whole-STRUCT N_ASSIGN). Both
stages' N_DOT dispatch gated on `lhs->lhs->kind == N_IDENT`; the
parser produces N_UN(STAR, IDENT(p)) for `(*p).f`, so both sides fell
off:
  - Write side (cgassign N_DOT base): emitted nothing, store dropped.
  - Read side (case N_DOT pointer-auto-deref): cgexpr derefed the
    pointer as a scalar, AX = first qword of struct, field offset
    dropped.

Fix: retarget base / dot_lhs to the inner IDENT when shape is
N_UN(STAR, IDENT). The existing via_ptr branch fires identically to
`p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` (non-IDENT
pointer expression) is tracked separately as task #19.

702 covers 7 rows: write_i64/i32/str, read_i64/i32/str_len, roundtrip
2026-05-15 18:13:02 +09:00
846cfc5578 selfhost+test: regen combined.ww after os.mkdirs
Auto-regen of derived files; lib/os.mkdirs landed in 19aa66a but the
selfhost combined.ww snapshots that fold lib/os in were not regened
in that commit. Catching them up now so the next make doesn't fight
the tree.

No source change; make test 61/61.
2026-05-15 17:17:53 +09:00
f2643a846a cstage+selfhost+test: cgen N_ASSIGN whole-STRUCT (call+structlit, 5 sites)
Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of
size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15],
CX=[16..23], zero-padded to 24B; receive sites here read the regs and
write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized
tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT
mirror the sender's three uniform MOVQs, else trailing 1..7B chunks
overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable
under WW struct align rules (size%align==0) and fall through.

Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww +
cgenstmt.ww), call-result + structlit rhs at each:
  - N_LET   `let s: T = bar()` / `= T{...}`           cgenstmt cglet
  - N_ASSIGN N_IDENT-lhs   `s = bar()` / `= T{...}`   cgenexpr cgassign
  - N_ASSIGN single-DOT local-base   `o.f = ...`
  - N_ASSIGN single-DOT ptr-base auto-deref   `p.f = ...`
  - N_ASSIGN single-DOT global-base   `g.f = ...`
  - N_ASSIGN chained-DOT depth>=2   `o.m.in = ...`
(The four dot-flavors share one shape pattern, hence "5 sites".) Where
the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded
into BX after the call so CX stays as the third value word; for
structlit field-walks BX is reloaded before each store since cgexpr
clobbers AX/BX between fields.

wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww):
si.totsize is mis-named — it's slot-padded to 8 by registerstruct for
stack-slot use, while the receive ABI wants the type's natural size
(max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is
tracked as the wwstage struct sizing follow-up (task #15); until that
lands, the helper recovers the natural size at receive sites.

Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value,
wwstage value, asm byte-identity), wired in Makefile after 698. The
headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B
via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16.
A regression to a MOVQ tail there overruns 4B past the slot and
flips the exit-code check.

smoke.combined.ww is the auto-regen ride-along of strings.freeall
landing in 714d089 (worker-shlex).

Pre-existing gaps surfaced and tracked separately (not fixed here,
out of scope):
  - task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs.
  - task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN
    initializer — the field_chain and field_global test rows use
    explicit field writes (`o.m.t = 10i64;`) rather than nested
    literals as a fixture-level workaround.
  - task #9: module-name-mangle for fn labels avoided in the
    field_global_call fixture by `let g: outer;` (no init).

make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap
byte-identity is the load-bearing proof for this commit's scope.
2026-05-15 16:43:07 +09:00
aee8149754 cstage+selfhost+test: cgreturn TY_STRUCT <=24B via AX/DX/CX
Whole-struct return ABI for sizes <=24B. Both stages materialise rhs
into a zero-padded 24B @retscr scratch slot, then load AX=bytes[0..7],
DX=bytes[8..15], CX=bytes[16..23] unconditionally — three MOVQs
regardless of declared struct size, so the receive side (landing in
task #5) can read all three words and mask by the declared size. R8
stays reserved for the tagged-return 4th word; the uniform-MOVQ shape
is cheap over a size-conditional partial-load and keeps the producer
diff vs the existing tagged-return AX/DX/CX/R8 path minimal.

Two rhs shapes wired this pass: N_IDENT (word-copy from rhs local slot,
MOVQ pairs + MOVL/MOVB tail bounded by declared struct size) and
N_STRUCTLIT (field-walk; tagged fields delegate to the existing tagged
widening helper, float fields go through X0, int fields use MOVQ/MOVL/
MOVB by field size). Sizes >24B fall through to the existing scalar
path (only AX gets the first qword), pending sret in a future task.
N_CALL chain-return (`return otherfn()`) is deferred to task #5's
receive side — until that lands the call-result lives in caller regs.

The wwstage mirror in cgenstmt.ww matches cgen.c byte-for-byte on the
new branch; cgendecl.ww's scanlocals pre-reserves 24B for @retscr under
the same predicate (N_RETURN, fnret is N_TNAME, structlookup hit,
totsize<=24, rhs is N_IDENT|N_STRUCTLIT) since wwstage writes its
prologue SUBQ from the upfront frame total — cstage patches SUBQ at fn
end so it can allocate inline.

Latent fsz==2 MOVW divergence between stages (cstage structlit int-
branch only special-cases fsz 1/4, wwstage's fieldstoreop also returns
MOVW for fsz==2) tracked as task #13; not exercised by the new fixtures
or by any current selfhost <=24B struct return.

main.combined.ww files also pick up worker-checkfix's wwstage
architectural comment from 7f60ebb (auto-regen ran after that commit).
2026-05-15 15:19:38 +09:00
9d85aa4142 selfhost: structlookup/enumlookup mod-filter (mirror aliaslookup)
Tags structinfo/enumtype with originating module; exact-match first,
then split pkg.X and filter by smod/emod. Without this, two modules
with same-leaf-name struct/enum types collapsed to whichever entry
appeared first in the chain.

Wired into 696_modtype_leaf_collision via a wwstage run_pos using
ww_ww (negative case omitted: w6c_ww has no checkfile pass). Updated
the test's Makefile deps to include the wwstage binaries.

Audited the rest of the lookup family — fnretlookup, fnparamslookup,
deflookup don't need the same treatment: the parser emits N_DOT.str
(call/field name) as the leaf only, and fnparamslookup is only
invoked with N_IDENT.str. Dotted module-qualified function calls go
through the module-mangling path instead.
2026-05-15 14:04:44 +09:00
e6045f3ada w6c+selfhost: same-module preference for bare-leaf lookup 2026-05-15 11:24:33 +09:00
35b32c1304 selfhost: aliaslookup mod-filter for pkg.-qualified type refs 2026-05-15 10:54:58 +09:00
66d6408cbe w6c+selfhost: cross-module same-leaf type disambiguation via Sym.mod 2026-05-15 10:26:20 +09:00
df74d9c429 selfhost: cgmatch bsz for TY_STRUCT variant bind (closes #31)
Match-arm bind size in wwstage hardcoded str=16, []T=24, else=8 in
both cgmatch (emit) and scanlocals (frame pre-scan). A TY_STRUCT
variant fell into the 8B fallback: only the first quadword reached
the bind, and the prologue SUBQ underbooked the frame so the
emit-time localalloc(bsz=24+) wrote past SP.

Replace the hand-rolled table with slotsize(c, pat) at both sites.
slotsize already covers N_TNAME named structs (returns si.totsize),
str (16), []T (24), tuples, aliases, and primitives (8). Mirrors
cstage cgen.c cgmatch which falls through to bu->size for TY_STRUCT.

Cstage is correct; no mirror needed.

Test 695 covers seven shapes: the 3xi64 headline repro, a 4xi64
struct via let-init scrut (exercises >24B bind), a mixed-quadword
struct (i32+i32+i64+i64), str/slice/i32 negative controls, and a
direct let-init scrutinee variant. The wider 4xi64 row uses the
let-init shape because the N_DOT spill path in cgmatch tops out at
AX/DX/CX/R8 — a separate, unrelated gap from the bind size.
2026-05-15 02:14:00 +09:00
19fb3a3b3c selfhost: parser tsuffix plumb + cgen N_UN peel for tagged-store variant index (closes #32)
Typed-int literal assigned into a tagged-union slot (`h.e = 42i64;` where
e: (i32 | i64)) wrote tag = 0 (the i32 slot) instead of tag = 1 (the i64
slot). Cstage was correct: parse.c parseprimary copies tok.tsuffix onto
N_INTLIT, check.c stamps node.type = ty_i64, and cg_widen_tagged_store →
cg_tag_for_variant walks variants matching by structural type_eq —
ty_i64 lands at index 1. Wwstage had two gaps:

1. The parser (lib/ww/parse/expr.ww parseprimary) read p.curuval and
   p.curtext from the current token but never the tsuffix field. Token-
   side capture has been in place since the lexer's `i8/i16/.../u64/f32/
   f64` glue suffix landed (lib/ww/lex/lex.ww sets out.tsuffix); the
   parser side was missed. So an N_INTLIT for `42i64` carried tsuffix=""
   into cgen. Mirror of cmd/wcc/parse.c parseprimary's `n->tsuffix =
   t.tsuffix` line. Same plumb for N_FLOATLIT.

2. Wwstage has no checker stage to stamp N_UN's type from its inner
   expression's type. `-42i64` parses as N_UN(MINUS, N_INTLIT(42,
   tsuffix="i64")) and rhstargetname stopped at N_UN, returning "" and
   falling through to taggedvariantindex's "first non-str variant"
   fallback — which picked tag 0 (i32) for any numeric rhs in an
   (i32|i64) union. Cstage's cunop returns the inner type for
   TK_MINUS / TK_PLUS / TK_TILDE so the N_UN gets ty_i64 stamped
   naturally; wwstage gets the equivalent via an explicit peel in
   rhstargetname, recursing into rhs.lhs for these three ops. The
   recursion also covers nested unary (`- -42i64`), which parseunary
   builds as N_UN over N_UN over N_INTLIT.

The lib/ww/parse change is mirrored in selfhost/cmd/{w6c,wwdump}/
main.combined.ww so the bootstrap snapshot stays consistent with the
working frontend source. parser.curtsuffix is a new str field; refill
copies t.tsuffix into it; parseprimary TK_INT / TK_FLOAT copy it onto
the new node before advance.

Cstage handled both `42i64` and `-42i64` correctly already; no cstage
mirror needed.

Test 694_tagged_store_intlit — eleven rows running on both stages: i64
lit in (i32|i64); i32 lit (existing-working pin); i64 lit in
(i32|i64|str) with the str fallback at tail; u8 lit at head of
(u8|i32|i64); i64 lit at tail of (u8|i32|i64) with a +100 marker so
mis-binding into u8 can't masquerade as success; negative-i64 lit
(N_UN MINUS peel + sign extension through match-arm bind);
unary-plus i64 lit (N_UN PLUS peel); bitwise-not i64 lit (N_UN TILDE
peel; `~0i64 == -1i64`); nested unary `- -42i64` (recursion through
two N_UN levels); direct `let x: ev = 42i64;` (cglet's tagged-init
code path, separate write site from cgassign's field-write);
negative-control str field (pins the existing str-fallback path
through rhstargetname).

Pre-fix run on wwstage: 8/11 rows fail (every typed-i64 case including
all three unary operators, nested unary, and the direct let-init);
cstage 11/11 pass. Post-fix: 22/22 across both stages. make test
41/41. Bootstrap ww2 == ww3 == ww4 byte-identical.
2026-05-15 01:39:37 +09:00
bacbf4b845 w6c+selfhost: cgdot N_DOT tagged-field source ABI (closes #28)
cgdot of a tagged-union struct field previously dropped the AX/DX/CX/R8
payload-register convention used by tagged-union returns: cstage's
direct-struct branch stopped at CX (size > 16) and never loaded R8
(slice-payload variants, slot 32B); the via_ptr branch had no TY_TAGGED
handler at all, falling through to fldloadop and yielding only the tag
in AX. The N_DOT scrutinee fallback in N_MATCH similarly stored only AX
into the spill slot. Wwstage cgdot had no TY_TAGGED branch in any of
the direct, *struct, or top-level-global field-load paths, and cgmatch's
non-ident scrutinee branch didn't recognise N_DOT — dispatch always
computed want = 0 and the spill scratch was hardcoded 24B. The combined
effect: any code reading `s.taggedfield` and consuming more than one
quadword of the payload saw garbage in the upper halves.

Cstage: extended the direct-struct TY_TAGGED branch with an R8 load for
size > 24 (CX still loaded last so global LEAQ-into-CX rooting
survives), added a parallel TY_TAGGED handler to the via_ptr (TY_PTR
inner TY_STRUCT) field branch, and extended the N_DOT scrutinee spill
fallback in N_MATCH to write DX/CX/R8 alongside AX.

Wwstage: new cgloadtaggedfield helper emits the four-register load with
CX-last ordering, and dotfieldtnode resolves a field's declared type
node for a local-ident or *struct base. cgdot grew three TY_TAGGED
branches (direct local, *struct deref staging in BX, top-level global
through CX). cgmatch's non-ident-scrutinee branch grew an N_DOT type-
extraction path mirroring the N_CALL / N_INDEX shapes and now sizes the
@match_spill slot from slotsize(scrutt) so slice-payload variants don't
overflow the historical 24B alloc. rhstaggedabicall accepts N_DOT so
`let copy: ev = h.e;` and tagged-arg call sites pass through the
tagged-source spill branch of cgwidentaggedstore.

Out of scope for #28 and left as separate latents: wwstage's match-arm
bind for a TY_STRUCT-typed variant copies only 8B (cstage falls back
to bu->size; wwstage's bsz=8 default), and the variant-index lookup
for an i64 literal in (i32 | i64) picks the wrong tag on the write
side. Both surface in struct-payload tagged unions and merit their
own tasks; the new test rows steer clear so #28's fix verifies
end-to-end on scalar / str / slice payloads.

Test 693_dot_tagged_source — three variant shapes (16B i64, 24B str,
32B slice) read from direct local, *struct param, top-level global,
and let-init round-trip. The 32B-slice rows verify v.cap (R8 / +24)
so dropping the upper-word load isn't masked by len-only checks; the
top-level-global row routes the write through *p because the direct
global-LHS tagged store is a separate wwstage gap (followup). Three
negative controls (untagged i32 / str / slice fields) keep the new
TY_TAGGED guard from shadowing the existing field-load paths. Wired
into make test; 37 tests total. Bootstrap ww2 == ww3 == ww4
byte-identical.
2026-05-15 01:08:52 +09:00
6402d8deb7 w6c+selfhost: cgen N_DOT slice-field through *T root in call args (closes #29) 2026-05-14 23:45:14 +09:00
9706513e59 selfhost: nodeisstr chained-str through value-struct in call args (closes #30) 2026-05-14 23:26:49 +09:00
a5919ed8da w6c+selfhost: cg_widen_tagged_store basereg + N_ASSIGN tagged field (closes #26)
Extended cg_widen_tagged_store (cstage) / cgwidentaggedstore (wwstage)
to take a base_reg/basereg parameter so the primitive supports non-BP
destinations. Cstage extends body in-place via via_outer gate +
spill+scratch+copy-out; wwstage splits into wrapper (non-BP) +
cgwidentaggedstorebp (BP-only) to dodge the no-goto constraint. New
N_ASSIGN field TY_TAGGED branch routes through the primitive for all
rhs shapes.

Scope-adjacent: fieldsize recurses through N_TTAGGED via slotsize and
TNAME-aliased-to-tagged via aliaslookup. Needed for the test fixtures.

Wwstage read-side N_DOT-of-tagged-field source is filed as task #28;
test rows use mark-canary verification until that lands.
2026-05-14 19:12:20 +09:00
1726bcef18 w6c+selfhost: cgen N_ASSIGN TY_STRUCT field branch (closes #25)
Parallel to TY_STR/TY_SLICE branches at cgen.c:1939/1962. Word-copy
from src slot to field+k*8 via AX, MOVL/MOVB ragged tail. N_IDENT
rhs only — struct-call-result and struct-literal rhs are different
code paths, filed as task #27. Symmetric in N_ASSIGN field case AND
spine-walker terminal; mirrored in wwstage cgassign four sub-shapes
(*struct base, direct local, global, spine terminal).
2026-05-14 18:16:51 +09:00
eb0602d070 w6c+selfhost: cgen N_ASSIGN TY_SLICE field branch (closes #24)
Parallel to the existing TY_STR branch at cgen.c:1939. Stores AX/BX/CX
at field+0/+8/+16 across three sub-shapes (via_ptr, is_global, direct
local) — DX as addr scratch where needed so CX (cap) survives.
Symmetric in wwstage cgassign. Surfaces tasks #25 (whole-struct rhs)
and #26 (whole-tagged rhs) in the same locus class.
2026-05-14 03:18:49 +09:00
d66aef382a w6c+selfhost: cgen *T-rooted chained N_DOT (closes #22)
Spine walker now accepts *T root at the last hop (cur->lhs->kind ==
N_IDENT, pu->kind == TY_PTR), substitutes pointee struct, emits
MOVQ off(BP), CX before offset arithmetic. Symmetric in N_DOT and
N_ASSIGN. Distinct gate from existing mid-chain *T-field branch
(cgen.c:4615) — no shadow.

Unblocks task #18 (bufio writer first-field embed). Pre-existing
*T-field mid-chain path unchanged.
2026-05-14 02:44:57 +09:00
d2f4659305 w6c+selfhost: localloadop helper for sign-aware ident loads (closes #19)
Read-side fix dual to fldloadop: signed-narrow local/global ident loads
now MOVSXD/MOVSWQ/MOVSBQ from the slot instead of raw MOVQ. Deref-stores
(MOVL/MOVW/MOVB) no longer corrupt downstream i64 widens. Compound RMW
restructured to gate direct-mem ADDQ/SUBQ on load_op == MOVQ. Top-level
lets use LEAQ+indirect (w6a doesn't expose MOVSXD/MOVSWQ/MOVSBQ for
D_EXTERN).

dotchainresolve out-params restored to natural *i32 (workaround retired).
selfhost/CLAUDE.md graduated.
2026-05-14 01:58:52 +09:00
5f87c60e6c selfhost: retire dotchain bundle workaround (closes #17)
dotchain struct + bundle pre-declares retired. dotchainresolve now
takes 6 direct out-params; per-iter locals declared inline. Bootstrap
ww2==ww3==ww4 byte-identical — end-to-end validation of session's
N_DOT / N_INDEX / TK_AMP / fldloadop fixes.

i64 widths retained on out-params via task #19 workaround (i32 deref-
stores leave caller slot upper 4B stale, MOVQ reads zero-extend to
garbage). Documented in selfhost/CLAUDE.md active-workarounds.
2026-05-14 01:11:07 +09:00
f26c86f5b7 selfhost: cgfnparams variadic T... straddle stitch (closes #12)
Last reg/stack-straddle gap closed — variadic T... now stitches at
idx=5/regs_left=1/nw=3. Four classes (tagged/slice/str/variadic) at
cgendecl.ww 467/518/564/394 are now structurally symmetric. cgfn
pre-scan predicate widened to (istg || issl || isst || isvar).
cstage unchanged — type-promoted T... → []T already hits the slice
partial-fit branch.
2026-05-14 00:36:34 +09:00
6354f5267c w6c+selfhost: cgassign N_DOT N_INDEX-lhs branch (closes #16)
Symmetric write-side counterpart of #8, bundled across both stages.
cstage [N]Struct write was broken (1986 gated on TY_PTR); wwstage had
no N_DOT(N_INDEX) write branch at all. New branch covers both
[N]*Struct and [N]Struct via viaptr flag, uses fldstoreop for
scalar/sub-word, MOVSS/MOVSD for float, two-MOVQ for str rhs.
Compound (PLUSEQ etc.) wired for integer scalar.
2026-05-14 00:09:46 +09:00
dd92938eff selfhost: retire stk[i].str spill workaround (closes #14)
End-to-end validation of #8: removed the task-#8 spill in
dotchainresolve; bootstrap ww2==ww3==ww4 still holds.
2026-05-13 23:52:39 +09:00
9ef9bef340 w6c+selfhost: cgen N_DOT N_INDEX-lhs branch (closes #8)
cstage cmd/w6c/cgen.c gained the missing N_DOT N_INDEX-lhs branch.
Covers both [N]*Struct and [N]Struct via fldloadop. wwstage already
handled [N]*Struct since 7c75dd2; refactored to mirror cstage exactly
and added [N]Struct. The spill workaround in dotchainresolve stays
(Pike rule); task #14 retires it as a follow-up.

Wwstage cgassign N_DOT(N_INDEX,...) silent store-drop discovered in
scope, filed as task #16.
2026-05-13 23:45:35 +09:00
d053560b80 w6c+selfhost: TK_AMP through chained N_DOT (closes #9)
Three shapes handled: value-struct chains (&o.i.a), pointer-field
(&p.f), slice/str pseudo-fields (&s.len). cstage inlines #6's spine
walker; wwstage reuses dotchainresolve unchanged. Silent-drop fallback
preserved.

Slice-header width mismatch in *&s.len writes filed as task #13.
2026-05-13 23:17:15 +09:00
5b098c4a99 selfhost: cgfnparams slice/str straddle stitch (closes #11)
Three branches (tagged/slice/str) in cgendecl.ww now structurally
symmetric — partial-fit reg+stack stitching. cgfn pre-scan predicate
widened to (istg || issl || isst). Variadic T... at straddle is the
last open class, deferred to task #12.
2026-05-13 22:53:50 +09:00
5c8724845a selfhost: cgfn pre-scan SysV-class accounting (closes #7)
Mirrors cgen.c §5130-5223 / cgfnparams: reg-spill, tagged-partial-fit,
pure-stack. Pure-stack does not bump cursor (cstage semantics). Fixes
16B over-allocation on 7+ arg functions; bootstrap stays byte-identical.

Slice/str at reg/stack straddle is deferred to task #11 (cgfnparams
doesn't stitch them either); pre-scan stays symmetric until then.
2026-05-13 22:40:43 +09:00
c4b3aca5e4 w6c+selfhost: principled sub-word signedness (closes #5/#10)
type_isunsigned recurses TY_ENUM and includes TY_RUNE on both stages.
13 LOAD + 6 STORE ladder sites (cstage) plus 4 more wwstage stragglers
in cgindex/cgforrange collapsed to fldloadop/fldstoreop helpers. N_CAST
narrow gate symmetrised; task #1's literal-kind workaround retired.
bool kept out of type_isunsigned, special-cased in field helpers.

Retroactively fixes a u32 mis-sign-extend in deref-compound (sz=4
hardcoded MOVSXD), pinned by new 660_field_signed row.
2026-05-13 22:23:00 +09:00
461a448d5f w6c+selfhost: cgen chained N_DOT/N_ASSIGN spine walk
Loop-shaped spine walker for value-struct chains (o.i.a) and slice/str
pseudo-fields (s.buf.len), read+write, both stages. SB-fallback at the
catch-all preserved for unresolved module-qualified idents.

Follow-ups filed: tasks #7-#10 (wwstage >6-arg frame over-alloc, chained
array-elem field BX loss, & through chained DOT, signed sub-word field
loads zero-extend).
2026-05-13 21:44:50 +09:00
388ab8a707 w6c+selfhost: cgen N_CAST signed narrow + peel !T on unsigned narrow
TY_RUNE excluded — wwstage primsize skips it; task #5 will collapse
both gates back to symmetric once type_isunsigned recurses TY_ENUM.
2026-05-13 20:06:24 +09:00
5bfdc7b20d w6c+selfhost: cgen && and || short-circuit
Both stages were eagerly evaluating RHS regardless of LHS (eager
ANDQ/ORQ on the two results). Now: eval LHS into AX, CMPQ $0 +
JE/JNE to a per-call-site label, eval RHS into AX, fall through.
AX holds the LHS sentinel on the skipped path — typechecker
already enforces bool operands.

Surfaced by lib/getopt's nil-argv guard segfault. Six new rows in
test/wcc/700_e2e.c, three of which segfault pre-fix. lib/getopt
test comment relaxed; nested-if kept as regression marker.
2026-05-13 19:21:43 +09:00
fbe0df4e68 lib: add temp + os.mkdir/rmdir/EXCL
temp mirrors Hare's temp: file, named, dir. file() routes through
named() and discards the path (no O_TMPFILE yet). Path randomizer
uses inline SplitMix64 seeded from getpid + O_EXCL retry (Hare uses
crypto::random which we don't ship). named() takes out-pointers for
fd + path — return shape gated on tasks #5 and #11. Caller closes
and removes; no defer in ww.

os gains mkdir, rmdir, flag.EXCL — straight ports of ref/hare/os.
selfhost combined files cascade; 995_self_rebuild byte-identity
holds.
2026-05-13 17:18:59 +09:00
7a4f60b041 w6c+wcc+selfhost+lib: int-cast truncate + use_alias, 5 new modules
Two cgen/check bugs surfaced by new lib modules, plus the modules
themselves (crc64, siphash, random, base64, base32).

  1. `(big_u64): u32` (and `: u16`, `: u8`, `: bool`) didn't truncate.
     N_CAST emitted nothing for int↔int; the value stayed in AX with
     its upper bits intact and downstream CMPQ/DIVQ misread the slot.
     The TK_TILDE path already had clamp logic for the same reason —
     N_CAST was the missing case. Both stages now MOVL r,r for u32 and
     ANDQ $mask for u8/u16/bool. Signed-narrow (i8/i16/i32) stays
     no-op until w6a grows reg-reg MOVSBQ/MOVSWQ/MOVSXD. selfhost
     cgcast walks alias chains via aliaslookup before checking
     primsize/typenameisunsigned so `(u: random)` where
     `type random = u64` still bypasses the clamp.
     See cmd/w6c/cgen.c N_CAST and selfhost/cmd/wcc/cgenexpr.ww cgcast.

  2. `mod.mod` type refs (`random.random` when the imported module
     declares `export type random = u64;`) failed with "unknown type".
     The driver concatenates imports into one flat scope, so SK_USE
     `random` collided with SK_TYPE `random` and scope_define silently
     dropped the use. resolve_typename's leaf lookup required
     `kind == SK_USE` and gave up. Adds a `use_alias` flag to Sym; the
     pass-1 decl scan now marks colliding syms in both directions
     (use-after-type and type-after-use). resolve_typename and the
     N_DOT cexpr branch treat `use_alias` like SK_USE for qualified
     lookup. selfhost check.ww was already lenient on this path so no
     ww-side change was needed; bootstrap fixed point (990-995) holds.
     See cmd/wcc/check.c installdecl pass + N_DOT/resolve_typename and
     cmd/wcc/ww.h Sym.use_alias.

New modules under lib/, each with @test vectors in *_test.ww and wired
into test/wcc/900_stdlib.c (26 modules → all compile):

  - lib/hash/crc64       ECMA, ISO  (mirror of crc32 shape)
  - lib/hash/siphash     SipHash-2-4, buffer-based sum/sum24
  - lib/math/random      SplitMix64 (init, next, u32n, u64n)
  - lib/encoding/base64  RFC 4648 std + url-safe encode/decode + sizes
  - lib/encoding/base32  RFC 4648 std + base32hex encode/decode + sizes
2026-05-13 14:58:36 +09:00
cbcc0167ae w6c+w6a+selfhost+lib: cgen+asm bugs surfaced by hash modules
Seven fixes across the toolchain, plus three new lib/hash modules
(adler32, crc16, crc32) that surfaced them.

  1. `~x` on u8/u16/u32 left the upper bits set: NOTQ inverts the
     whole 64-bit register and nothing trimmed it back to type
     width, so a returned `u16` would compare 64-bit against a
     typed literal and disagree. Both stages now mask after NOTQ
     for narrow unsigned: AND $0xFF/0xFFFF for u8/u16, MOVL r,r for
     u32 (ANDQ $0xFFFFFFFF sign-extends imm32 and is a no-op).
     Signed narrows stay sign-extended and need no fix-up. See
     cmd/w6c/cgen.c N_UN TK_TILDE and selfhost cgenexpr.ww cgun
     TK_TILDE with new nodeprimwidth helper.

  2. w6a had no D_CONST immediate path for ANDQ / ORQ. cgen would
     emit `ANDQ $65535, AX` and the rr encoder silently wrote
     `21 /r` with garbage reg fields — the mask never happened.
     Added `81 /4` (AND) and `81 /1` (OR) imm32 paths in both
     cstage and selfhost w6a. The ~width fix above depends on this.

  3. `s: []u8` cast as a direct fn argument produced a 0-length
     slice. cgexpr for N_CAST left (AX=ptr, BX=len) from the str
     source but never set CX (cap), and the arg-push fallback only
     pushed AX. cgcast now synthesises CX=BX when target is slice
     and source is str; node_isslice / arg-push recognise
     cast-to-slice and emit the full (cap, len, ptr) triple. Both
     stages.

  4. `*[N]T` element-store used 8-byte stride + MOVQ regardless of
     T's width. Indexing `buf: *[4]u16` would step 8 bytes and
     write 8 bytes per element. Added idx_eff (drills *[N]T → T)
     in cstage and the matching pointer-array drill in selfhost
     elemsizeof. Also added MOVW / MOVZWQ / MOVSWQ to w6c, w6a,
     and selfhost mirrors so 2-byte element stores/loads use the
     right opcode (was falling through to MOVQ and trailing 6 bytes
     into the next slot).

  5. Slicing a top-level fixed array (`g[0:n]` where `g: [N]T` is
     a global) computed the base from BP instead of the symbol —
     localfind returned 0 and the cgen treated it as a local at
     offset 0. Both N_SLICE-as-expression (cgslice) and N_SLICE-
     as-call-arg paths now check let_islet / letvartnode and emit
     LEAQ name(SB) when the base is a global array (or MOVQ
     name(SB) for a global slice/pointer base). Both stages.

  6. Top-level `let arr: [N]T = [v0, v1, ...]` link-failed on
     cstage — emit_lets bailed when it saw N_ARRLIT init on an
     array type, and the sz==8 scalar path then misemitted any
     8-byte-sized array (e.g. [4]u16, [8]u8) as a single quad.
     emit_lets now walks N_ARRLIT, evaluates each element as an
     int/rune/bool/nil literal, packs per-element bytes
     little-endian, and honours the trailing `...` repeat marker.
     Selfhost already handled the literal-init path; fixed the
     parallel sz==8 duplicate-DATAW emit on its side (the array
     and the scalar paths both fired, last write winning at link
     but the duplicate broke cross-stage byte-identicality on user
     code with this shape).

  7. w6a's per-line input buffer was a 1KB stack `char buf[1024]`.
     A `DATAW` for a [256]u16 emits ~2080 bytes on one line, which
     truncated mid-escape; the assembler then re-parsed the
     remaining tail as garbage opcodes ("unknown opcode"). Bumped
     cstage w6a to a 32K static buffer (selfhost w6a already
     allocated per-line via amalloc).

  lib: lib/hash/adler32, lib/hash/crc16, lib/hash/crc32 — pure
  buffer-subset shape (matching lib/hash/fnv), with per-module
  *_test.ww runnable via `ww test lib/hash/<name>`. Adler-32 plus
  CRC-16 (CCITT/CMDA2000/DECT/ANSI) and CRC-32 (IEEE/Castagnoli/
  Koopman) cover Hare's reference vectors bit-for-bit. Wired into
  test/wcc/900_stdlib.c. .gitignore: lib/**/*.s,*.o so `ww test`
  droppings stay untracked.

`make test` (26/26), `make bootstrap` (ww2≡ww3≡ww4), and per-module
`ww test` all pass. cgen output is byte-identical across cstage and
selfhost for every repro that previously diverged.
2026-05-13 14:26:18 +09:00
b05968c7f4 w6c+selfhost: cgen *p OP= v (was silent no-op)
`*p += 1` and the rest of the compound-deref family (-= *= |= &= ^=
<<= >>=) fell through the N_ASSIGN switch in both stages and emitted
nothing. The `*p = v` block was gated on TK_ASSIGN, the IDENT-compound
block required N_IDENT, and there was no N_UN/TK_STAR compound branch
between them. Test 994/995/997 byte-identity hid it: both stages
mis-compiled identically, so the diffs were clean.

Surfaced via fmt.println("hello") segfaulting in wwstage builds.
findvariadicparam in cgenutil.ww does `*nfixed_out += 1`; the drop
left nfixed at 0, so `fprint(fd, args...)` mis-counted variadic args,
gathered fd as a formattable element, and segfaulted on tagged
dispatch.

Adds the missing branch in cmd/w6c/cgen.c N_ASSIGN and
selfhost/cmd/wcc/cgenexpr.ww cgassign: eval rhs → push, eval ptr →
BX, sized+extended load (BX) → AX (MOVZBQ for 1B, MOVSXD/MOVL for 4B
by signedness, MOVQ for 8B), pop rhs → CX, combine via
ADDQ/SUBQ/IMULQ/ANDQ/ORQ/XORQ/SHLQ/SHRQ on CX,AX, sized store back.
TK_SLASHEQ stays the rhs-only fallback, matching the IDENT path.
Float and aggregate deref compounds still fall through — uncommon.
2026-05-13 13:12:32 +09:00
956a20701b w6c+selfhost+lib: zero-init multi-word no-rhs lets
`let x: T;` for str/slice/tuple/struct/tagged previously left the slot
holding stack garbage — only 8B-primitive slots were zeroed. This bit
`expectbindname` in lib/ww/parse: `let empty: str; *into = empty;` was
copying stack bytes (often a recently-vacated str descriptor) into the
caller's `id`, so wwstage emitted `_` discard nodes carrying random
text instead of "". Both stages now zero the full slot on no-rhs lets;
`[N]T` arrays keep the per-index-write contract.

Also tightens the two known buggy sites: parse.ww `expectbindname`
writes `*into = ""` directly, expr.ww `_` primary returns the bare
newnode (amalloc already zeroes).
2026-05-13 12:46:02 +09:00
b6cf68f2b8 w6c+selfhost+lib: Hare-style variadic call sites
Param-decl `name: T...` (Tparam.variadic=1, type []T), call-site
gather of N args into a fresh `[N]T`, forward via `xs...`, full
selfhost mirror, and lib/fmt graduated to the Hare shape.

Frontend:
  - parse: `T...` after a param's type stamps Node.op=TK_ELLIPSIS
    and breaks out (variadic must be last).
  - check: resolve_type N_TFN / build_fn_type wrap the param type
    as []T and set tp->variadic. N_CALL accepts either a tail of
    args assignable to T (gather) or a single `xs...` spread of
    []T (forward); both bypass the "too many args" check on the
    variadic slot.
  - type: type_eq compares Tparam.variadic.

Cgen (cstage):
  - call site: when the callee has a variadic last param,
    materialise the tail args into a frame-resident `[N]T` via
    localoff, write a 24B slice descriptor (ptr,len,cap), and
    splice a synthesised N_IDENT into args[] so the downstream
    widen/eval/pop loops see one slice slot. Tagged-element types
    route each store through cg_widen_tagged_store. Forwarding
    skips gather: the N_SPREAD wrapper is replaced with its inner
    slice expression. Empty form writes {nil,0,0}. args[] / widen[]
    bump from 16 to 64 to accommodate Hare's mixed-arg printers.

Selfhost mirror:
  - lib/ww/parse: `T...` mark on N_PARAM.op.
  - cgen: varargseq counter on Cg; scanlocals reserves
    @vararg_d_N + @vararg_sl_N per variadic call (seq recorded on
    N_CALL.uval so cgcall picks the same names). cgcall does the
    same gather/forward and N_IDENT splice. cgfnparams treats
    variadic params as 24B slice slots via a synthesised TSLICE
    tnode. pushargsrev skips the tagged-widen detection for
    variadic params (effective type is []T, not tagged).
  - rhstargetname now recognises N_TRUE/N_FALSE/N_RUNELIT and
    typed N_INTLIT so the variant-tag lookup finds bool/rune/iN
    variants instead of falling through to "first non-str" (which
    misassigned tag 0 to bool in tagged unions like formattable).

lib/fmt graduated: print/println/fprint/fprintln/errorln/fatal
take `args: formattable...`. Bare `error` (no -ln) is skipped —
the leaf name collides with strconv's `type error = !(invalid |
overflow)` under the driver's flat namespace.

Tests: 5 new e2e rows (plain gather, zero-arg, tagged element,
forwarding, fmt.println end-to-end). lib/CLAUDE.md workaround
paragraph replaced with the Hare-shape description.
2026-05-13 08:56:01 +09:00
46edb8db4a w6c+selfhost+lib: cgen quality batch + lib Hare-shape graduation
Six fixes across the toolchain, surfaced by lib/lisp porting work.

  1. f64 compound assigns (`acc += d`, `-=`, `*=`, `/=`). Both stages
     load slot → X1, OP X0 into X1, store back (ADDSD/SUBSD/MULSD/
     DIVSD are reg-reg only). Previous MOVSD-overwrite dropped the
     OP. Locals and top-level lets.

  2. Top-level `[N]u8` arrays + `&arr[i]`. let_emit_size grows a
     TY_ARRAY branch so zero-init DATAW lands; cgindex / N_INDEX
     store / `&base[i]` all detect a global array base and use
     LEAQ name(SB) instead of LEAQ (BP). TK_AMP no longer pre-
     evaluates the operand as a value-load — `&base[i]` computes
     base + i*esz directly. Unblocks Hare's static-buffer pattern:
     strconv.{u64,i64,f64}tos graduate to module-level `*_buf`
     arrays and return owned views.

  3. Cross-module `pkg.Enum.MEMBER`. Nested N_DOT chains that
     don't fold to a known shape now emit `MOVQ <leaf>(SB), AX`
     (mirrors the bare-IDENT unresolved fallback), so isolation
     probes — and the test 990 cgen-match floor — stay consistent
     across stages. strconv exposes `base` as a real `enum i32`;
     callers updated. The `main` exemption (linker entry-point
     keeps bare name even when not exported) mirrors C-side
     collectmods into selfhost cgendecl.

  4. Sum-typed parameter ABI. lib/bytes.{index,rindex} take
     `(u8 | []u8)` needle; lib/strings.byteindex / rbyteindex take
     `(str | rune)` needle (Hare-shaped; the byte-wise misnomer
     `index` is dropped). tagged_arg_size cap bumps to 48 (6 int
     regs), with a new partial-fit branch on the callee: when an
     N-word tagged arg overflows remaining regs, fill what fits and
     stitch the rest from positive BP offsets. scanlocals MCASE
     handles slice binds (24B) and walks each arm with a saved /
     restored seenmark set so two arms naming the same local each
     get their own slot — matches cstage's per-arm scope reset.

  5. 4-reg tagged-return ABI (AX=tag, DX=word0, CX=word1, R8=word2),
     up from 3 regs. Slice-payload variants (`([]T | E)`, slot 32B)
     round-trip ptr/len/cap end-to-end. Every receive site updates:
     let-init via cgwidentaggedstore, match scrutinee spill, cgindex
     tagged-element load (both N_IDENT and fallback bases),
     pushargsrev tagged-ident arg (reads word count from slot size),
     cgreturn slice variant in the shuffle path.

  6. `expr: TaggedAlias` is a widening, not a re-interpret. C cgen +
     selfhost cgwidentaggedstore peel an N_CAST whose destination IS
     the union — so cgexpr's natural shape (str: AX=ptr, BX=len;
     slice: AX=ptr, BX=len, CX=cap) is consumed by the matching
     concrete-variant branch instead of being misread as a tagged
     AX/DX/CX triple. Inner casts to a concrete variant (`7: i32`)
     keep their type for proper tag lookup. `[N]Alias` arrays
     resolve element size via slotsize + aliaslookup, and aliaslookup
     strips a `pkg.` prefix so cross-module references work.

lib/fmt grows `formattable = (i64 | str | bool | rune)` plus
`printv` / `printlnv` taking an explicit `[]formattable` slice (the
receive side of Hare's `args: formattable...`). Call-site variadic
gather isn't wired — callers either hand-build the slice or compose
strconv.i64tos + strings.concat.

700_e2e: 114 → 123 rows (f64 compound, top-level u8 arrays + `&buf[i]`,
pkg.Enum.MEMBER, sum-typed (str|rune) and (u8|[]u8) params, 4-reg
slice-return ABI, formattable array). 26/26 tests, bootstrap stable
through ww4.
2026-05-13 08:05:01 +09:00
6fd0160c0f w6c+selfhost: tagged-arr element ABI + full selfhost mirror
Closes the remaining tagged-union gaps after the prior two commits:

  1. Tagged element in an array/slice (cstage). N_INDEX load now reads
     slot words into AX/DX/CX, matching the tagged-return ABI so match
     / call-arg / let-init paths consume `arr[i]` uniformly. N_INDEX
     store routes through a scratch slot + cg_widen_tagged_store +
     byte-copy to &arr[i], so the full widening machinery (scalar /
     str / struct payload / tagged subset / nullable fold) lights up
     for element writes too.

  2. Selfhost mirror — the cgen widen helpers (struct payload,
     tagged-subset, spread-flatten) C cgen has had for two commits
     finally land in selfhost:

       cgwidentaggedstore  — single writer for nullable / tagged ident /
                             tagged via AX:DX:CX / struct (lit + ident) /
                             str / scalar source shapes.
       cgwidentagremap     — CMPQ-chain tag remap for variant-subset.
       rhsstructpayload    — struct-name predicate; filters `!void` /
                             `!i32` aliases that share N_STRUCTLIT shape
                             but aren't structs.
       rhstaggedident,
       rhstaggedabicall    — source-shape predicates.
       flatvariantidx      — spread-aware variant index lookup. Walks
                             `(...inner | T)` entries by resolving the
                             alias and inlining the inner's variants so
                             wwstage's tag order matches the check.c
                             flattening cstage does at type resolution.

     cglet tagged init, cgassign tagged-ident reassign, cgreturn struct
     / subset payload, pushargsrev struct payload, cgindex tagged
     element load, cgassign N_INDEX tagged element store all delegate
     to these. cgmatch picks up scrutt from N_INDEX bases (element
     type) and uses flatvariantidx for case dispatch.

  3. Selfhost frame accounting: scanlocals reserves a 24B @tagscr slot
     when the body contains a tagged-arr store, a struct-payload
     tagged return, or a struct-payload call arg — dedup'd via
     scanseenmark so multiple sites share one slot. N_LET stubs now
     carry tnode so walk-time type checks see the array element type.
     slotsize TARRAY learned to size tagged / struct / ptr / aliased
     elements (was 8B-default for anything not N_TNAME-primitive,
     undersizing tagged-element arrays).

     Scalar / str call-arg widening keeps its direct-push fast path
     (no scratch), so wwstage's asm on selfhost source remains
     byte-identical to cstage's — 993/995 still pass.

700_e2e: 9 new rows — scalar/str/struct/subset/nullable variants in
arrays and slices, plus pass-arg / let-init / return / match shapes.
2026-05-13 06:29:52 +09:00
9133251269 w6c+wcc: widen struct/tagged-subset, parse ... spread
Three tagged-union gaps:

  1. Struct-payload widening was broken at every site (call, let,
     assign, return, struct-field init). cg_widen_tagged_store now
     materialises str / scalar / struct-lit / struct-ident / tagged
     payloads at slot+8+field_off and writes the tag last. Call sites
     route through cg_widen_tagged_push (scratch slot + push high→low).

  2. Tagged → wider tagged widening forwarded the source tag verbatim.
     cg_widen_tag_remap emits a CMPQ-chain switch that translates each
     source variant index to the destination's, then zero-pads to the
     wider slot. type_eq grew a TY_TAGGED arm (was returning 1 for any
     two unions); type_assignable now accepts variant-subset and
     rejects the rest.

  3. `(...inner | T)` spread parses (cmd/wcc/parse.c, lib/ww/parse).
     Marks Node.op = TK_ELLIPSIS; resolve_type unwraps NAMED + flattens
     when the spread bit is set so aliases inline like Hare's
     tagged_type unwrap flag.

Selfhost mirror: spread parser ported. Cgen widen helpers not yet
mirrored — wwstage stays byte-identical to cstage on the existing
test corpus, but will emit wrong asm if user code uses the new
patterns (probe sp2 shows the divergence).

700_e2e: 9 new rows covering call/let/assign/return × struct +
tagged subset, plus the spread-flatten case.
2026-05-13 05:30:20 +09:00
47d75d9b59 w6c+selfhost: widen concrete variant to tagged-union call arg
Tagged-union widening already fired for `let r: (str|rune) = "...";`,
`r = "...";`, and `return "..."` from a tagged-returning fn — but not
at call sites, so `fn f(x: (str|rune))` couldn't be called with a bare
str or rune. The arg was pushed as its own static type (2 words for
str, 1 for rune) while the callee's slot expected 3 (tag + payload).

C cgen: at the call boundary, look up the callee's declared param
type per arg. When the param is TY_TAGGED and the arg is a concrete
variant, materialise (tag, value-words, padding) sized to the param's
tagged_arg_size — then the existing pop-into-arg-regs logic picks it
up. Nullable `(*T | void)` collapses to a single 8B push.

selfhost: fnret now carries the params head alongside rtype (amalloc
bumped to 48); pushargsrev takes the matching param node and runs the
same widening sequence per arg. The pop drain in cgcall already
handled extra slot words, so no change needed on that side.

Verified with a smoke covering str/rune literals, typed locals,
pre-existing tagged-local pass-through, and nullable widening from a
raw pointer. Selfhost emits byte-identical asm to C cgen on the test.
2026-05-13 04:44:03 +09:00
6e7c9e0df4 selfhost: alias-aware istaggedtype for nested-union match
`type error = !(invalid | overflow)` miscompiled — istaggedtype
only matched N_TTAGGED directly, so an `e: error` param spilled
as 8B scalar and the match's slot+8 read trailed into saved BP.

Mirror isstrtype's alias+bang unwrap; add resolvetagged() for
is/as/match sites that need the inner N_TTAGGED. Frame scan
counts via slotsize so wwstage stays byte-identical to cstage.
Unblocks lib/strconv.strerror.
2026-05-13 04:23:31 +09:00
e16634baec lib: add missing Hare-stdlib functions (ascii/bytes/strings/path/endian)
ascii: valid, validstr, ispunct, isprint, iscntrl, isgraph, isblank,
strcasecmp.

bytes: hasprefix, hassuffix, rindex, rindexbyte, contains, reverse,
zero.

strings: rindex, sub, trimprefix, trimsuffix, ltrimbyte, rtrimbyte,
trimbyte. The byte-set trim is a single-byte subset of Hare's
`trim(input, exclude: rune...)`; no variadic ABI yet.

path: dirname, basename, extension, join. Owned-str returns where the
result isn't a borrowed view of the input (join).

endian: full Hare table — be/le get/put for u16/u32/u64 plus the
network-order htonu/ntohu pair extended to 32/64.

lib/CLAUDE.md rewritten to reflect the post-graduation policy
(tagged-union returns, owned-str returns, plan9 names, documented
deviations for non-graduating modules).

Makefile picks up lib/ascii and lib/fmt as wwdump_ww / w6c_ww deps so
lib-only edits regenerate the affected binaries.
2026-05-13 04:03:55 +09:00
be8a662f15 lib/strconv: graduate to owned-str returns with Hare-shape base param
i64tos / u64tos / f64tos return a fresh owned str (caller frees via
os.free) instead of writing into a caller-supplied [N]u8. Adds typed
variants (i32tos / i16tos / i8tos and u32 / u16 / u8) and the missing
base parameter on stoi64 / stou64 + typed parse wrappers.

Base values are exported as plain-i32 `def`s (strconv.DEC,
strconv.HEX_UPPER, ...) rather than a `base` enum: cross-module
`strconv.base.DEC` chains miscompile in the cstage cgen — it emits a
memory load through `base(SB)` rather than inlining the constant.
The Sdef path resolves correctly, so callers say `strconv.DEC` and
both cgens lower to an immediate.

Also renames strings.byteindex / rbyteindex to strings.indexbyte /
rindexbyte, matching bytes.indexbyte and reserving the Hare name
`byteindex` for the future `(str | rune)`-needle shape.

fmt drops printint / printlnint / fprintint — those were stand-ins
for variadic `fmt::println(42)`; with the owned-str graduation the
substitute is one call: `fmt.println(strconv.i64tos(42, strconv.DEC))`.

strerror is sketched in a comment but not shipped — match arms over
the wider `error = !(invalid | overflow)` union still expose a
cstage-vs-wwstage spill divergence.
2026-05-13 03:55:16 +09:00
9bc973dc96 lib: drop non-Hare extras (ascii.digitval, bytes.copy, fmt.errpos)
ascii.digitval/isidstart/isidpart are lexer-private, not Hare-stdlib.
Moved into lib/ww/lex/lex.ww as fn (renamed digitval to hexval since
its sole job is the \\xHH escape decoder).

bytes.copy and fmt.errpos have no Hare counterpart and no external
callers; gone.
2026-05-13 03:24:33 +09:00
f4743dc5d5 lib/strconv: add f64tos 2026-05-13 03:10:25 +09:00
73c0cf4c78 w6c+selfhost: match-arm scope/spill + alloc(structlit) sugar
Three gaps in the wwstage cgen relative to C w6c, plus a matching
C-side bug surfaced along the way.

cgmatch (selfhost) handles non-ident scrutinees: `match (foo())` now
spills the AX:DX:CX return triple into a 24B `@match_spill` slot
rather than reading garbage off BP+0. scanlocals counts the slot so
the prologue SUBQ stays in sync. For N_CALL we recover the return
type via fnretlookup so nullable dispatch picks the pointer-vs-null
discriminator. Mirrors @match_spill in cmd/w6c/cgen.c N_MATCH.

cgcall (selfhost) special-cases `alloc(structlit{...})`: lower to
rt_alloc(totsize) + per-field MOV* at the struct's field offsets,
mirroring cmd/w6c/cgen.c's existing path. Previously the structlit
fell into pushargsrev and produced wrong code.

check.ww's N_MCASE branch now pushes a fresh scope around each arm
body. Without this, `case let e: str` inside a fn with an outer
`let e: *T` collided with scopedefine's same-scope dedup, the inner
binding silently dropped, and references to `e` inside the arm
resolved through the outer type.

Both cgens save/restore the locals head around case bodies so arm
binds (and nested arm-body lets) don't leak past the arm — code
after the match resolves names back through the outer scope.

w6c gains `local_alloc`: same as `localoff` minus the dedup. N_MATCH
case-bind allocation switches to it. Previously `let e: *T` (8B)
shadowed by `case let e: str` (16B) reused the outer 8B slot and the
inner str.len store overflowed into the saved BP, segfaulting on
return.

Tests 26/26.
2026-05-12 18:40:41 +09:00