Commit Graph

87 Commits

Author SHA1 Message Date
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
49e39a4cf3 test: wire lib/getopt/getopttest.ww into make test
getopttest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 982_getopt_run runs it
and propagates exit; matches the 980_memio_run / 981_temp_run /
998_bufio_run pattern.
2026-05-15 00:28:59 +09:00
edb3350f54 test: wire lib/temp/temptest.ww into make test
temptest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 981_temp_run runs it
and propagates exit; matches the 980_memio_run / 998_bufio_run
pattern.
2026-05-15 00:27:38 +09:00
2cd88f1707 test: wire lib/memio/memiotest.ww into make test
memiotest.ww existed and passed under `out/bin/ww run` but no
test/wcc/*.c ran it under `make test`. New 980_memio_run runs it
and propagates exit; matches the 998_bufio_run pattern.

980-989 is the new sub-band for stdlib-run integration tests.
2026-05-15 00:26:10 +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
79c758c046 test: wire lib/bufio/bufiotest.ww into make test (closes #23)
Closes the coverage gap that hid task #22's pointer-rooted N_DOT bug:
test/wcc/900_stdlib.c claimed "Coverage lives at lib/bufio/bufiotest.ww"
but no test/wcc/*.c ran bufiotest. New 998_bufio_run runs it and
propagates exit; matches 910_at_test pattern.
2026-05-14 02:51: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
f2b47087a2 wcc: &s.len / &s.cap typed as *i64 (closes #13)
TK_AMP early-exit on slice/str .len/.cap pseudo-fields now returns
*i64 instead of legacy *i32. Slice ABI is 24B fixed; LEAQ at the slot
was already correct, only the pointer typing was wrong — store-width
flips from MOVL to MOVQ via the existing primsize-from-tnode path.
&s.ptr untouched (already **T).
2026-05-14 00:21:53 +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
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
036b2c851f lib+test: graduate bufio to Hare scanner on io.stream
Scanner subset: newscanner, finish, scanbyte, scanline, scantok,
overflow (named-void). Buffer caller-owned; EOF_DISCARD default.
Skips Hare's scanner-as-io.stream embed pending task #6 (chained
N_DOT-of-N_DOT miscompile).
2026-05-13 20:38:55 +09:00
2243849855 lib+test: unify errors to Hare named-void tagged-union
Five tags from ref/hare/errors/common.ha — invalid, noaccess, noentry,
exists, unsupported — all !void. lib/io keeps eof/closed as plain void
(no Hare analogue for ww's singleton-style done) and adds underread.
Drops errors.equal/isnil and the old str-sentinel surface.
2026-05-13 20:19:03 +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
09a336cb74 lib+test/wcc: add memio, rename io stream.ww→io.ww
Mirrors Hare's memio: fixed, dynamic, dynamicfrom, buffer, string,
reset, borrowedread. Caller owns the state + stream slots because
w6c lacks &x.field and 32B return-by-value. Tests table-driven via
parallel arrays. io.stream now exported.
2026-05-13 16:07:18 +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
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
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
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
67eaa9796a selfhost: fix 4B array load/store width + 8B uninit zero-init 2026-05-12 16:30:24 +09:00
e087c843e9 selfhost: port forrange — N_FORRANGE parser + cgen + tuple destructure 2026-05-12 16:21:13 +09:00
ce5d66e18a selfhost: port append + spread — N_SPREAD parser + rt_ensure builtin 2026-05-12 16:04:17 +09:00
f67c07cbae selfhost: port switch — N_SWITCH parser + cgen + scratch slot 2026-05-12 15:10:00 +09:00
a9b804935c w6l+selfhost: dynamic-link + .data — shared R+W segment
.data now lives at the end of the dyn-path R+W PT_LOAD, just after
.dynamic. The single segment covers .got.plt + .dynamic + .data; its
filesz drops trailing zeros (BSS) while memsz spans the full extent.

Relocation moves from main.c into each emit function so the static
and dynamic paths use their own data_va — text→data refs land on the
right VA regardless of path. Removes the early-error in dynout.c
that previously refused any .data with -l/-L.

Tests: 810_dyn gains two new dyn+.data fixtures (mutable read+write
of an i32, plus a zero-init i64 verifying the BSS scan still produces
a valid p_filesz<p_memsz under the shared segment).
2026-05-12 13:50:09 +09:00
2480f4c272 w6l+selfhost: BSS optimisation — trim trailing .data zeros from filesz
Scan the consolidated .data buffer (post-relocation) for trailing zero
bytes; set the R+W PT_LOAD's p_filesz to exclude them while p_memsz
covers the full region. The loader zero-fills the gap, so behaviour is
unchanged. Saves up to a page per binary on programs whose globals are
zero-init.

Mirrored in selfhost/cmd/w6l/out.ww so test 992's byte-identity diff
still holds. Dynamic-link path is untouched — it still errors on any
mutable global; that's the next feature.
2026-05-12 13:44:27 +09:00
548547a1d0 w6c: slice reassignment — full triple flow through N_IDENT/N_SLICE/N_ASSIGN
N_IDENT for a slice local now loads (AX=ptr, BX=len, CX=cap), matching
the existing global-slice load.

cgexpr learns N_SLICE: `base[lo:hi]` leaves the same triple in
registers, so callers (return, arg push, reassignment) all share
one shape. The let-init's pre-existing N_SLICE direct-store path
stays as a specialisation; the new generic slice let-init catches
fn-returning-slice and slice-ident initialisers.

N_ASSIGN gains a TY_SLICE branch parallel to TY_STR: store all
three halves to the local slot or, for globals, stash CX into DI
before LEAQ-ing the address (CX is both the new cap and the
address scratch).
2026-05-12 13:37:31 +09:00
328a53de5b w6c: tagged-union fields on struct globals — LEAQ-based read+write
Field write extends the existing TY_TAGGED branch with an is_global
arm: LEAQ name(SB),CX after cgexpr (no AX/BX clobber), then MOVQ
into slot+foff+0 (tag) and slot+foff+8 (value, plus +16 for str-
typed variants).

Field read now treats tagged fields specially — load AX=tag,
DX=val0, CX=val1 (when union >16B), mirroring the tagged-return
ABI that let-init and match dispatch already expect. Previously
the scalar-load path read 8B into AX and left DX/CX with junk,
which silently broke local tagged-field reads too.
2026-05-12 13:29:25 +09:00
6f04713601 w6c+selfhost: float globals — DATAW + LEAQ-indirect MOVSS/MOVSD
f32 → 4B slot, f64 → 8B. C cgen bakes the FLOATLIT bit pattern into
DATAW directly; selfhost emits zero-init only (its parser doesn't
lex N_FLOATLIT yet). Read/write goes LEAQ name(SB),CX + MOVSS/MOVSD
since w6a has no D_EXTERN operand form for SSE moves.
2026-05-12 13:23:45 +09:00
003f707618 w6c: emit DATAR for let s: str = "literal" initialisers
Use the new DATAR mechanism so str-literal init on a top-level
mutable `let` lands in .data and links cleanly.

emit_lets, when it sees `let s: str = "lit"` (non-empty strlit),
emits:

  DATAW s(SB),"<8 zero placeholder><8 LE bytes of len>"
  DATAR s+0(SB),<strlit_label>(SB)

The linker patches the placeholder with the strlit's runtime VA at
program load time, so `s.ptr` reads as the real pointer and `s.len`
as the literal length. A new let_pre_intern pass scans top-level
lets ahead of emit_data so the strlit gets a DATA row in the same
.s file; running emit_lets after emit_data instead would have
flipped the (DATA strlits, DATAW lets) section order in the .s and
broken byte-identity with the wwstage cgen.

The wwstage cgen still emits the zero-init shape for str lets,
which only matters if the wwstage is asked to compile source that
uses str-literal init. None of the selfhost combined sources do
that today, so test 994 / 990 stay green. The selfhost mirror for
DATAR + DATAW + this w6c branch is a follow-up.

630_let_global gains two fixtures: a length-readback and a first-
byte readback through the patched ptr.
2026-05-12 12:49:01 +09:00
d998425391 w6a+w6l: DATAR directive for absolute-address relocs in .data
Unblock literal initialisers for str/slice/struct globals by wiring
an R_X86_64_64 relocation kind through both assembler and static
linker.

w6a:
  - new A_DATAR directive, syntax `DATAR slot+off(SB),target(SB)`,
    records an R_X86_64_64 reloc at slot+off in .data pointing at
    target. The slot must be pre-defined by a prior DATAW;
  - parse_operand learned the `name+disp(SB)` shape so the slot's
    byte offset can be addressed explicitly;
  - Areloc carries a `section` flag (0=.text / 1=.data) and obj.c
    splits the reloc list into .rela.text and .rela.data, emitting
    the latter conditionally with sh_info pointing at .data.

w6l:
  - Lrel grows the same `section` flag; obj.c loads `.rela.data`
    sections into the global reloc list with offsets shifted by
    each input's data_off;
  - pass.c handles R_X86_64_64: target VA is data_va+sym.val for
    in_data symbols (else text_va+sym.val), addend is added, and
    the 8-byte slot is patched in l->data (or l->text).

Inputs without DATAR are unaffected — bootstrap, 991 (selfhost .o
diff) and 992 (selfhost exe diff) keep their byte-identical
output. 520_datar covers the new path: asm a DATAW+DATAR pair,
verify .rela.data has exactly one R_X86_64_64 entry, link, run,
confirm the relocated pointer feeds a 5-byte write that prints
"hello".

Selfhost mirror + w6c emission for str/slice/struct literal init
land in follow-ups.
2026-05-12 12:45:11 +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