Commit Graph

60 Commits

Author SHA1 Message Date
2fb594748c cstage+selfhost+test: principled identity-cast skip (#33)
Generalizes b5632b1's single-site dst_is_enum gate. Skip the narrow-
clamp MOVL when src.width == dst.width && src.signed == dst.signed.
Closes #25's followup.

Both stages need symmetric source-type derivation for byte-id. cstage
deliberately throws away the checker's richer typed-AST and uses a
structural walker (castsrcprim) that mirrors wwstage's exprprimresolved
case-for-case. Otherwise cstage's `.len: i32` resolves to i32 (skip)
while wwstage's misses the pseudo-field (clamp) — bootstrap diverges.
Pseudo-fields, N_BIN, N_INDEX, N_CALL, match-bindings all yield sz=0
→ clamp emits defensively on both.

The N_TENUM walker now follows enum aliases in wwstage's
typenodeprimresolved (was the original lacuna behind #25), and bool
is excluded early in the same helper (mirrors cstage's
type_isint(TY_BOOL)=false). bool→bool keeps its dedicated is_bool
ANDQ $255 emit; bool→i8 / bool→u8 etc. fall through to the clamp on
both stages.

Walker shape (cstage castsrcprim / wwstage exprprimresolved):
  N_INTLIT     → tsuffix gated, untyped excluded
  N_IDENT      → trust local's resolved tnode
  N_CAST       → recurse on declared dst
  N_UN         → recurse on operand
  N_DOT        → real-struct only (TY_STRUCT or TY_PTR→TY_STRUCT)
  others       → sz=0 → identity false → clamp emits

Test 710 grew from 5 → 16 rows: 6 identity-width pins (u32/i32/u8/
i8/u16/i16 self), 1 sign-change pin (u32→i32 clamp MUST fire), 2
silent-miscompile exit-validating rows (truncate via divide), 1
pseudo-field defensive pin (`s.len: i32`), 1 bool-source pin
(`b: i8`). Asm byte-id asserted on every row.

Out of scope: redundant clamps remain for patterns wwstage can't
structurally derive (N_BIN, N_CALL, N_INDEX, pseudo-fields). A
sibling task extending wwstage's type inference closes those.
2026-05-16 12:20:56 +09:00
b8b32ab80c cstage+selfhost+test: refuse same-block let / param redecl (#32)
cmd/wcc/check.c silently accepted `let a; let a;` in the same block
and similar redecls. Pre-#27 the localoff dedup masked it; post-#27
last-write-wins via head-first localfind. Surfaced by worker-27
during the #27 review.

Cstage: 5 guard sites (check_scope_define-NULL → err) covering
N_LET block-bind, N_MLET tuple binders (incl. same-tuple
`let (a,a)`), N_FORRANGE tuple binders, top-level let, fn param.
Voice: "<kind> '<name>' redeclared in same scope" for inner;
"duplicate let %s" for top-let, matching the existing
"duplicate <kind>" idiom at 1812/1851/1872.

Wwstage: TODO(#11) comments at the 4 mirror sites (installdecl,
N_FORRANGE, N_LET, installparams). Full enforcement waits on the
checkfile pass per rob.

**Unmasked by #32 (worth flagging):** selfhost/cmd/wcc/cgenexpr.ww
cgcall had `let callee: *node = n.lhs;` twice at fn-body scope
(copy-paste, identical value). Pre-fix silent-redecl absorbed it;
post-fix the new guard rejects. Removed the second decl — outer
`callee` stays visible across the intermediate block.

Test 712 (redecl): 10 rows (6 neg + 4 pos), cstage-only per rob.
Negative rows cover all 5 guard sites + same-tuple-dup. Positive
rows pin the legal counter-shapes (cross-block, name-only bucket,
forrange body, mcase-per-arm).

Test 300 row 34 ("shadowing in inner scope; same scope flagged")
was incorrectly asserting the bug; flipped to expect "redeclared"
and added a sibling row pinning cross-block shadow stays ok. Test
709's `same_block_redecl_pin` canary (explicitly documented as
flipping under #32) removed; pointer to 712 left in its place.
2026-05-16 11:17:20 +09:00
10de5a5e0e selfhost+test: cgdot LEAQ-of-fn for module-qualified N_DOT (#12)
wwstage cgdot lacked a TY_FN branch for module-qualified N_DOT
rvalues. `let p = mod1.ping` fell through to the MOVQ/LEAQ-narrow
fallback, loading 8 prologue bytes from the fn's first instruction
instead of taking its address. Cstage cgdot already handled this
case (wired during #9, f1440bf).

Mirror cstage: gate on fnretlookup(c, fld) before the localloadop
fallback; emit `LEAQ <module>.<name>(SB), AX` via emitfnname with
the hint from lhs.str.

Extend 706_fnlabel_mangle: pos.ww now stores mod1.ping/mod2.ping
into local fn-pointer slots and dispatches through them in addition
to the existing direct calls. Expected exit 56 → 112. Regression
shape: without the new branch, MOVQ leaf(SB) loads the prologue
bytes; indirect call jumps into garbage → SIGSEGV.

ww2 == ww3 == ww4 byte-identical at the new emit.
2026-05-16 00:07:00 +09:00
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
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
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
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
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
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
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
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
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
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
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
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
7c75dd218a selfhost: fix several wwstage cgen miscompilations
Surfaced via examples/lisp, which had to work around the following in
source. Each lowering now matches cstage on the same shape.

- cgassign / cgdot: two-level field through a non-pointer sub-struct.
  `(*L).cur.kind = k` (cur a struct-by-value field of L) silently
  dropped the store; the corresponding read fell into the SB-symbol
  fallback and the linker reported `undefined reference to kind`. The
  two new branches resolve outer-field offset + inner-field offset
  and emit a single direct store/load at the combined slot, both for
  T-by-value and *T-base shapes.

- cgdot: `xs[i].field` chains the trailing field load through the
  N_INDEX result for [N]T / []T / *T element-of-struct-ptr. The
  cgforrange loop variable now carries the elem tnode so the same
  fast path covers `for (let x .. xs) { x.field }`.

- cgindex / cgassign: top-level `[N]T` array and `*T` pointer used
  as an index base. cgindex now emits LEAQ name(SB) (array) or
  MOVQ name(SB) (pointer) with the correct element scaling; without
  this the fallback emitted neither base and walked off the saved
  BP slot. Adds letvartnode() helper, an N_TARRAY branch to
  letemitsize so the array shows up in c.lets, and an N_TARRAY
  initialiser path in emitletdataw that lays the literal bytes into
  DATAW.

- cglet / scanlocals: infer the local's tnode for an unannotated
  `let x = f()` / `let x = f()?`. inferletcalltype() reads the
  callee's declared return; `?` and `!` strip to the success variant
  so a tagged-union let allocates the full 24B slot and the
  struct-field dispatch in cgdot/cgassign sees the right type.
  letslotsize now defers to slotsize on the inferred type.

- slotsize: follow type aliases for tagged-union variants. With
  `type parserr = !str;`, the variant slot was 8B instead of the
  required 16B; the tagged let stomped on the next slot at the
  AX/DX/CX spill.

- cgreturn: tagged-union return forwarding. `return f();` where f
  also returns a tagged union now passes the (tag, payload1,
  payload2) triple through unchanged instead of re-wrapping it.

- cgreturn / cglet / taggedvariantindex: dispatch by variant name
  with module-qualified-vs-bare matching, and recognise N_STRUCTLIT
  as the variant tag for `return eof{};`. cgexpr default emits
  `MOVQ $0, AX` so the surrounding return shuffle isn't left with
  a stale AX.

- isstrtype / nodeisstr: resolve through `!T` aliases. `parserr =
  !str` was not propagating the str-shape to the rhs check and the
  MOVQ BX,CX shuffle was being dropped from str-typed local
  returns.

- exprfloatkind: recognise `p.field` as f64/f32 when the struct
  field is so declared, so `v.fval: i64` lowers to CVTTSD2SI on X0.

- cgassign: str field on a direct struct local writes both halves.
  `L.src = s;` previously dropped s.len.

- cgcall: pop into the int reg window only up to 6 (DI..R9); rest
  stays on the stack and the caller emits ADDQ to clean up.
  cgfnparams accepts >6-arg signatures by registering the overflow
  params at positive BP offsets (16+8*k(BP)), no spill instruction
  emitted.

All 26 harness tests pass; bootstrap reaches a byte-stable fixed
point at ww3 == ww4.
2026-05-13 03:06:46 +09:00
da8d34e0d4 w6c+selfhost: route f64/f32 struct-field load/store through X0
cgexpr leaves float results in X0, not AX, but the struct-field paths
emitted MOVQ AX,off(BX) and MOVQ off(BX),AX — so every store wrote
garbage and every load read garbage, except by accidental register
survival across an unrelated call. examples/lisp only worked because
parsef's X0 happened to live across the broken MOVQ shuffle into
vfloat; any inserted f64 op between them would silently corrupt.

Wire MOVSD/MOVSS X0,… (and the matching loads) into eight field
sites on both compilers: alloc(T{...}), p.x = v through local/ptr/
global, chained r.sub.x = v, *p = v for *f64, let v: T = T{...},
base.x reads, *T.x reads, and chained a.b.c.x reads.

83/83 lisp_test probes still pass; bootstrap reaches a byte-stable
fixed point at ww3 == ww4.
2026-05-13 01:25:51 +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
67eaa9796a selfhost: fix 4B array load/store width + 8B uninit zero-init 2026-05-12 16:30:24 +09:00
ce5d66e18a selfhost: port append + spread — N_SPREAD parser + rt_ensure builtin 2026-05-12 16:04:17 +09:00
ab0976571b selfhost: port slice reassignment — N_SLICE cgexpr + N_ASSIGN triple store
Mirror of 548547a in the wwstage cgen.

cgexpr learns N_SLICE: `base[lo:hi]` leaves (AX=ptr, BX=len, CX=cap)
so callers (return, arg push, reassignment, let init for fn-returning-
slice / slice-ident) share one triple ABI. Without this, the existing
let-init that just forwards (AX,BX,CX) to the slot was silently storing
junk on a base[lo:hi] rhs.

cgassign gains a slice branch parallel to str: for a local slice ident
lhs, store all three halves to off+0/+8/+16; for a slice global, stash
CX into DI before LEAQ-ing the symbol address into CX (CX is both the
incoming cap and the LEAQ scratch), then store AX/BX/DI at +0/+8/+16.

N_IDENT slice-local triple load was already in place from the earlier
selfhost port; only the cgexpr and cgassign halves needed adding.

Byte-identical to C w6c on the corpus — make test 26/26, 994_w6c_ww
passes on 10 inputs, 990_selfhost + 995_self_rebuild reach fixed point.
2026-05-12 14:37:09 +09:00
5155ba55f3 selfhost: port float lex + expression cgen — feature parity with C
Lexer: `lexnum` now parses the digit/exponent tail into an f64 via a
new `parsef64` (decimal-only, integer-arith driver + pow-10 multiply,
no strtod). The IEEE bits are also stashed in tok.uval via pointer
reinterpret so cgen consumers stay integer-only.

Parser: TK_FLOAT → N_FLOATLIT, carrying both fval and uval. Parser
state grows curfval to plumb the lexer's f64 through refill.

cgen:
  - cgfloatlit reads n.uval and materialises X0 via the standard
    MOVQ-PUSHQ-MOVSD-ADDQ trampoline.
  - cglet, cgident, cgassign learn float-typed branches: MOVSS/MOVSD
    for locals; LEAQ-indirect MOVSS/MOVSD for globals.
  - cgbin handles ADDSD/SUBSD/MULSD/DIVSD (+ SS variants) and
    UCOMISD/UCOMISS-based comparisons. cgun handles float negate
    via the `0 - X0` shape C cgen uses.
  - cgcast routes int↔float and f32↔f64 through CVTSI2SD/CVTTSD2SI/
    CVTSD2SS/CVTSS2SD and their SS twins.
  - cgcall + pushargsrev push float args via SUBQ+MOVSD and pop into
    the X0..X7 stream, tracked by a per-class counter alongside the
    int DI..R9 stream. cgfnparams loads float params from the same
    stream.
  - emitletdataw bakes FLOATLIT init bits into DATAW (4B for f32,
    8B for f64).

Tests: smoke programs (literal init, reassign, arithmetic, fn args/
returns, casts) produce byte-identical asm through `w6c` and
`wwdump_ww -c`, and the resulting binary exits with the same value
whether compiled by the C or wwstage toolchain. Full `make test` is
26/26 and `make bootstrap` still reaches its byte-identical
ww2==ww3==ww4 fixed point.
2026-05-12 14:21:50 +09:00
00d1120441 w6c+selfhost: struct globals — zero-init DATAW + LEAQ-based field access
Extend top-level mutable `let` to cover structs. Same approach as
str / slice: take the field-access base through &name(SB) instead
of off(BP).

  - emit_lets / emitletdataw: emit `sizeof(T)` zero bytes for any
    struct global without a baked-in initialiser. Struct-literal
    init is skipped → undefined symbol at link if used;
  - cgdot read path: when the IDENT base's local lookup misses and
    the name is a struct let, LEAQ name(SB), CX and load the field
    at fi.foff(CX) with the width-aware op (MOVQ / MOVL /
    MOVZBQ / MOVSXD; MOVQ pair for str fields);
  - cgassign write path: parallel handling for plain `=` (incl. str
    fields) and the compound ops (+=, -=) via load → push → eval
    rhs → combine → store with a re-LEAQ between cgexpr clobbers.

Tagged-union fields on struct globals are unsupported in v1 — the
local path's tagged branch isn't generalised yet. Whole-struct
by-value flow through expressions remains NYI (matches the local
status). 630_let_global gains 3 fixtures (read/write, compound +=,
narrow u8 field); selfhost mirror keeps test 990 / 994 / 995 byte
identical; bootstrap fixed point holds.
2026-05-12 12:26:21 +09:00
97eb1fe20d w6c+selfhost: slice globals — 24B DATAW + (AX,BX,CX) load
Extend top-level mutable `let` to cover slices. Same shape as the
str work, with one more 8-byte field and the address holder CX
overwritten by the cap as the last load step:

  - emit_lets / emitletdataw: 24-byte zero DATAW for `let v: []u8;`
    (and the trivial `nil` init); no slice-literal syntax exists
    so the no-init path is the only supported shape;
  - cgident: LEAQ name(SB), CX → MOVQ (CX), AX → MOVQ 8(CX), BX →
    MOVQ 16(CX), CX, so the slice ABI triple lands in (AX, BX, CX);
  - cgdot: .cap delta 16 wired alongside .ptr / .len through the
    same &name(SB) base.

Slice reassignment (`v = some_slice;`) is still unsupported — slice
values don't yet flow as a full (AX, BX, CX) triple through general
expressions even for locals — so reads/`&` are the supported surface
today. Manual fill through `(&v): *u64` continues to work.

Tests 630 (10/10), 990, 994, 995 stay green; bootstrap fixed point
holds.
2026-05-12 12:16:52 +09:00
208bdd25df w6c+selfhost: str globals — 16B DATAW + (LEAQ, MOVQ, MOVQ) sequences
Extend top-level mutable `let` to cover str. The cgen now:

  - emits a 16-byte zero DATAW for `let s: str;` (and the trivial
    `nil` / `""` inits); a non-empty strlit init is skipped because
    a compile-time .data → .text reloc isn't supported yet, so the
    user gets a clean undefined-symbol error at link;
  - loads `s` as `(LEAQ s(SB), CX; MOVQ (CX), AX; MOVQ 8(CX), BX)`
    so the (AX=ptr, BX=len) pair convention is preserved;
  - stores via the same `&s` indirection for `s = expr;` and routes
    the `.ptr` / `.len` pseudo-field N_DOT branch through it; and
  - tracks the declared type on each LetVar so cgident / cgdot /
    cgassign pick the right load/store shape.

Selfhost cgen mirrors all four paths byte-for-byte; test 990
(cgen-match on err.ww) and tests 994/995 (self-rebuild) stay
green. 630_let_global gains two new fixtures (`let msg: str;` +
runtime assign, plus reassign from a helper).

Slice and struct globals still NYI — same scope deferred.
2026-05-12 12:10:23 +09:00
4bf1b56872 selfhost: cgident read path for top-level lets
Mirror the C cgen's N_IDENT load fallback. Reads of a top-level
scalar `let` now emit `MOVQ name(SB), AX` (RIP-relative) instead
of silently dropping. Truly undefined names still fall through to
the silent return, matching the pre-existing defensive behaviour;
the C side's broader unconditional fallback is intentionally not
mirrored here so typos surface as nothing-emitted rather than a
link-time stub.
2026-05-12 12:00:18 +09:00
3c812faa08 w6c+selfhost: codegen for top-level mutable let
Third step toward writable globals. The C cgen and its selfhost
mirror now:

  - emit DATAW <name>(SB),"<8 LE bytes>" for every top-level `let`
    whose type lands in the scalar set (i8..i64/u8..u64/bool/rune/
    int/uint/uintptr/ptr; floats and multi-word types deferred);
  - drop the "no writable .data" silent-drop guard at the N_IDENT
    store path, replacing it with a RIP-relative MOVQ for `=` and
    a load→combine→store sequence for the compound ops; and
  - route `&name` through LEAQ name(SB) instead of dropping it.

Type aliases resolve via aliaslookup so `type counter = i32; let c:
counter = 0;` still emits a DATAW slot. Non-literal initialisers
silently skip, which surfaces as a clean undefined-symbol error if
the binding is ever referenced.

The selfhost mirror lands in the same commit because test 990
diffs the C cgen against wwdump_ww -c on err.ww (which has
top-level `let nerrors: i32 = 0; ... nerrors += 1;`). Any drift
between the two cgens makes 990 fail. Bootstrap stays at a fixed
point: ww2 == ww3 == ww4 byte-identical.
2026-05-12 11:56:51 +09:00
922877309b ww+wcc: Hare-strict enum types — back out the int↔enum relaxation
Cascades the four enum kinds through every signature and local that
holds one of their values, then removes the type_assignable /
unify_arith relaxation that previously let bare i32 mix with the
named enum types.

Signature updates:
  - kwlookup() now returns `tkind` (not i32); tokname() takes `tkind`
  - accepttok / expecttok / bprec / isassignop take `tkind`
  - parsearglist's closekind is `tkind`
  - newtype / prim take `tykind`; scopedefine takes `skind`
  - newnode / nkname take `nkind`

Struct fields:
  - tok.kind is `tkind`; parser.curkind is `tkind`
  - node.kind is `nkind`; node.op is `tkind`
  - tinfo.kind is `tykind`; sym.skind is `skind`

Locals holding kinds across lex/parse/check/cgen are now typed with
their enum, including sentinel patterns like `let lkind: nkind =
nkind.N_NONE; if (...) lkind = tn.kind;`.

The selfhost cgen had a load-width bug exposed by this: fieldsize()
fell back to 8 bytes for any TNAME that wasn't a struct or primitive.
For a tkind-typed field that gave `MOVQ (BX), AX` instead of `MOVL`,
diverging from the C cgen on tok.kind / parser.curkind / etc. Two
fixes:
  - fieldsize now consults the enum registry and returns the storage
    type's size (4 for `enum i32`)
  - collectenums runs before collectstructs in cgfile so the registry
    is populated when registerstruct asks for field sizes

All 22 tests stay green; 990/993/995 byte-identity probes pass with
the strict typing in place.
2026-05-12 05:04:33 +09:00
3affe01705 selfhost: graduate N_* defs to nkind enum 2026-05-12 04:54:23 +09:00
077d0f95ed selfhost: graduate SK_* defs to skind enum 2026-05-12 04:52:42 +09:00
408ea2a322 ww+wcc: graduate selfhost TK_* defs to tkind enum
`type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, ... TK_LAST = 86 }`
replaces the 87-line `def TK_*: i32 = N` cluster in lib/ww/lex/tok.ww.
Numeric values explicit so 990_selfhost's byte-diff against the C-side
`Tkind` enum still passes.

All ~270 reference sites in lib/ww and selfhost/cmd/{wcc,wwdump}
sed-renamed `TK_X` → `tkind.TK_X`. Struct fields (`tok.kind`,
`parser.curkind`) intentionally kept as `i32` — making them `tkind`
shifted some byte-positions in the cgen output and broke 990/993/995
byte-identity probes without an obvious win.

To make the rename non-cascading on every signature, type_assignable
and unify_arith in cmd/wcc/check+type relax to allow enum ↔ int
mixing when storage matches (a `tkind` value flows into an `i32`
slot and vice versa, no explicit cast). This deviates from Hare's
strict enum semantics; doc'd as an explicit pragmatic relaxation
for the compiler's internal enum-shaped kinds. External user code
can still get the type-safety benefit if they declare their
parameters with the enum type.

combined.ww files regenerated by ww build.
2026-05-12 04:50:36 +09:00
5149d10618 wcc+selfhost: pkg-qualified enum access (os.whence.CUR)
Driver-side concatenation flattens module names, but enum member
lookup keyed off the exact lhs ident — so `whence.CUR` worked while
`os.whence.CUR` fell through to w6l with `undefined main.whence`.

C side: fold TY_ENUM members in the post-cexpr cascade too, not
just the early SK_TYPE shortcut. The recursive cexpr lands the
inner N_DOT(os, whence) on the named enum type; the outer access
then folds normally.

Selfhost: cgdot now treats `N_IDENT.MEMBER` and `N_DOT.MEMBER` the
same way, keying off the leaf name. enumlookup strips a trailing
`.`-prefix from the lookup key.

Adds e2e test 700: `use os; os.whence.CUR as i32 == 1`.
2026-05-12 04:30:20 +09:00
f597ce67f6 selfhost: cgen for enum (member fold + as pass-through)
w6c_ww now compiles enum end-to-end and emits byte-identical
asm to the C w6c on the new 994 corpus case (`type mode = enum u8
{ R, W, RW = R | W }; main() { return (mode.RW): i32 }`). Mechanism
mirrors the C side:

- collectenums walks every `type X = enum {...}` at file scope and
  pre-resolves each member's u64 value (auto-increment from prior,
  sibling-ref folding for `RDWR = READ | WRITE`).
- cgdot recognises `EnumName.MEMBER` before the local lookup and
  emits MOVQ $value, AX directly.
- cgtypeassert short-circuits when either side is enum: cgexpr on
  the LHS lands the value in AX with the right integer width; no
  tag/unwrap.

main.combined.ww (wwdump/ + w6c/) regenerated by ww build.
2026-05-12 04:25:28 +09:00