Commit Graph

110 Commits

Author SHA1 Message Date
1c184ee6aa examples: lisp — perm/trans split, promote-on-define, slice free
Two bump arenas. arena_reset_trans() runs between top-level forms;
top-level define / set! deep-copy the bound value graph into perm
via Cheney-style forwarding (pin = -1 + stashed fwd pointer in
.car/.val) so no perm cell ever points into trans. Args slice in
eval's apply path also gets explicit os.free per dispatch — without
that the rt_ensure page-per-call leak dominated and masked the
reset. test_huge peaks at ~2.6 MB under massif --pages-as-heap=yes,
down from ~525 MB pre-arena (~200x).
2026-05-12 23:27:04 +09:00
fa33357821 examples: lisp — chunked bump arena for value/env cells
Replaces rt_alloc-per-cell (one 4 KiB mmap each) with arena_alloc
over 64 KiB chunks. test_huge peak under massif --pages-as-heap=yes
drops from ~525 MB to ~253 MB. Same lifetime semantics; remaining
bulk is per-call append() in eval's arg slice (rt_ensure still
mmaps page-per-call).
2026-05-12 23:12:40 +09:00
78b1cbfb6a examples: lisp — proper tail calls in eval 2026-05-12 22:54:34 +09:00
ab173b095a examples: lisp — pure-ww Lisp interpreter, REPL, in-process tests
Demo program that lives entirely on lib/* and libwwrt.a — no @symbol
FFI of its own. The interpreter sits in lispcore.ww (exports for the
test driver); lisp.ww is a 3-line entry that calls lispcore.repl().

Language surface: integers, floats, symbols, strings, lists, lambdas
with closures, define / set! / if / quote / let / begin, recursion
(fact / fib / ackermann / gcd), map / filter / reduce as user code.

REPL is line-buffered: each read tries to parse one top-level form,
asks for more on "unterminated list", evaluates and prints, then
shifts consumed bytes off the front of the buffer. Lookahead-aware —
the parser primes one extra token so we shift to L.curstart, not
L.pos, otherwise the first byte of the next form gets eaten.

lisp_test.ww exec'd as a regular binary (ww test drops -I in single-
file mode); 66 probes cover arithmetic, lists, closures, recursion,
errors. test_*.lisp drive the live REPL through `make demo`.

The wwstage cgen still mis-lowers a handful of patterns at this
shape of program — top-level array indexing, global-ptr deref,
two-level field stores, f64 routing through *T, alloc(structlit{})
for f64/str fields, (slice | E) returns, xs[i].kind chains, f64
compound assigns. Each workaround is annotated at its use site;
the full taxonomy is in examples/lisp/CLAUDE.md.
2026-05-12 22:33:24 +09:00
b7893916a3 examples: cmatrix — ww + libncurses falling-glyph demo
Exercises match/yield/?/!/alloc/free/slice/tagged-union end-to-end:
the @symbol FFI binds initscr/mvaddch/init_pair/getch/napms; setup
returns (*void | initerr) propagated via `?`; main unwraps the clock
via `!`; dispatch is a nested match-as-expression that yields a
bool; key handlers fold into switch/enum (q/space) and (i32 |
speederr | void) for 1..4 + the '0' error overlay.

No definite or indirect leaks under valgrind (the only "possibly
lost" / "still reachable" bytes are libncurses's process-lifetime
terminfo caches, freed only with --with-leaks).
2026-05-12 20:41:22 +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
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
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
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
1ac9980f7e selfhost: mirror writable .data + R_X86_64_64 across the wwstage
Bring the wwstage toolchain to parity with C-side DATAW / DATAR /
.data / .rela.data support. With this, w6c_ww + w6a_ww + w6l_ww can
compile, assemble and link `let g: str = "lit";` (and the scalar /
str / slice / struct globals that landed earlier) end-to-end, with
output that's byte-identical to the C-side pipeline.

w6a (types.ww / parse.ww / asm.ww / obj.ww):
  - A_DATAW + A_DATAR opcodes; parser learns `name+disp(SB)`;
    A_DATAR records an R_X86_64_64 reloc in .data via the new
    addrelocdata helper; areloc gains a `section` flag and asym
    an `isdata` flag; obj.ww splits relocs into .rela.text /
    .rela.data, emits .data PROGBITS + .rela.data conditionally,
    and shuffles section indices the same way cmd/w6a/obj.c does
    so byte output stays identical when no DATAW/DATAR are used.

w6l (sym.ww / obj.ww / pass.ww / out.ww / dynout.ww / main.ww):
  - lrel grows `section`; lsym grows `indata`; lobj tracks
    dataoff / datasize; lnk grows combined .data buffer;
  - obj.ww loads .data and .rela.data, registers data symbols
    with indata=1 and val shifted by the input's data_off, and
    the archive scanner includes both .text and .data globals;
  - pass.ww adds R_X86_64_64 (patch 8 bytes in .text or .data
    with sym_va + addend); relocate's signature becomes
    (textva, datava);
  - out.ww emits a second PT_LOAD (R+W) when datalen > 0, with
    .data at the page-aligned offset after .text;
  - dynout.ww refuses .data + -l/-L cleanly (matches the C-side
    error message);
  - main.ww computes text_va / data_va and passes both to
    relocate.

wcc cgen (cgen.ww / cgendecl.ww):
  - letpreintern walks top-level str-lets and interns the strlit
    BEFORE emitdatasection emits its DATA row, so emitletdataw
    can later look up the same label;
  - emitletdataw's 16B branch detects non-empty strlit init and
    emits the 8-zero + 8-LE-len DATAW plus a DATAR slot+0,strlit
    reloc, mirroring cmd/w6c/cgen.c.

Verified: `wwdump_ww -c` byte-matches `w6c` on a `let g: str =
"hello world\n";` fixture; `w6a_ww` and `w6l_ww` produce a
binary byte-identical to the C-side pipeline that runs and
prints "hello world". Bootstrap fixed-point holds (ww2 == ww3 ==
ww4), 26/26 tests green.
2026-05-12 13:03:03 +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
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
38e0b6510a w6l: route writable globals into a second PT_LOAD
Second step toward top-level mutable `let`. The static path now loads
.data PROGBITS sections from input .o files, page-aligns them after
.text, and emits a second PT_LOAD (R+W) covering them. Relocations
targeting data symbols compute against the data VA; text→text
displacements still cancel the absolute VAs and stay correct.

Inputs without any .data keep the original single-PT_LOAD layout
byte-for-byte — 992 (selfhost w6l .o diff) and 995 (self-rebuild)
depend on that invariant.

Dynamic-link path (-l/-L) rejects .data for now with a clear error;
folding writable globals into the existing R+W segment alongside
.got.plt/.dynamic is a follow-up.
2026-05-12 11:42:30 +09:00
1b0955c97b w6a: DATAW directive for writable .data section
First step toward top-level mutable `let`. Adds a sibling directive to
DATA whose bytes land in a separate writable .data PROGBITS section
(SHF_ALLOC|SHF_WRITE, STT_OBJECT) instead of .text. The section is
emitted only when DATAW was used, so inputs without it produce a
byte-identical .o — tests 991 (selfhost .o diff) and 995 (self-rebuild)
keep passing unchanged.

w6l still treats data-resident syms as undefined; that's the next step.
2026-05-12 11:35:50 +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
d20674a5ad selfhost: graduate TY_* defs to tykind enum 2026-05-12 04:53:35 +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
fc49da44d8 os: graduate SYS_* defs to nr enum
`type nr = enum i64 { READ, WRITE, OPEN, ... }`. syscall0..4 take
`num: nr` so the wrong-arg-order trap is now a compile error
(`syscall1(0i64, ...)` no longer typechecks — it has to be
`syscall1(nr.READ, ...)`).

Internal-only (callers outside os.ww never touched the constants),
so no external API change. ABI is unchanged: nr's storage is i64
and rt_syscall's RDI is unchanged.

The selfhost combined.ww files regenerate as a side effect of
`make wwstage`.
2026-05-12 04:44:57 +09:00
b9443b1f33 os: graduate O_*, SEEK_* defs to flag and whence enums
Mirrors Hare's `fs::flag` and `io::whence`:

    export type flag = enum i32 {
            RDONLY  = 0,
            WRONLY  = 1,
            RDWR    = 2,
            CREATE  = 64,    // 0o100
            TRUNC   = 512,   // 0o1000
    };

    export type whence = enum i32 { SET = 0, CUR = 1, END = 2 };

open/tryopen/lseek signatures take the enum types (`flags: flag`,
`w: whence`) so callers get type-checked: `os.open(p, os.flag.RDONLY,
0)` is the correct shape, and `os.flag.WRONLY | os.flag.CREATE |
os.flag.TRUNC` typechecks as a `flag` via the same-named-type rule.

Callers in selfhost/cmd/{ww,w6c,w6a,w6l,wwdump} updated from
`os.O_RDONLY` etc. to `os.flag.RDONLY`. SYS_* syscall numbers kept
as `def` for now (internal-only, ABI surface, no Hare analogue in
this scope).

selfhost/test/smoke.ww keeps its standalone-compile property by
using a numeric literal (`0`, RDONLY's value) for the open flags
arg — probe 6 in 990_selfhost compiles smoke.ww with no `use`
expansion, so cross-module type refs like `os.flag.RDONLY` can't
resolve there. Untyped 0 → flag via type_isnum.
2026-05-12 04:41:50 +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
9227a07f91 gitignore: lib/**/*.combined.ww; drop stray lex.combined.ww
`ww build` on a lib/ module drops a .combined.ww snapshot next
to the source. Only the bootstrap-frozen copies under
selfhost/cmd/*/main.combined.ww are intentionally tracked; the
lib/ ones are transient. lex.combined.ww slipped in via an
ad-hoc `git add lib/`.
2026-05-12 04:22:37 +09:00
5bf30f209c selfhost: mirror enum tokens + AST + parsetype branch
Parses byte-identical to the C frontend on enum sources (verified
via `diff` of wwdump vs wwdump_ww -a on an enum-using fixture).
The selfhost side reserves the slot in the AST and TY_* enums so
later check.ww and cgen mirror work doesn't shift numeric IDs.

Codegen-side enum support (member-value folding in cgdot, enum↔int
pass-through in cgtypeassert) is deferred — current selfhost sources
don't use enum, so 990_selfhost / 995_self_rebuild stay green.

main.combined.ww in wwdump/ and w6c/ regenerated by `ww build` as
a side effect of `make wwstage`.
2026-05-12 04:20:59 +09:00
34817eedcd wcc: Hare-style enum [storage] { ... } type
`type Foo = enum [intT] { NAME [= expr], ... };`. Storage defaults
to i32; members auto-increment from 0 (or last+1) when `= expr` is
omitted, and value expressions can reference earlier siblings —
enough surface for io::mode-style flag enums (`RDWR = READ | WRITE`).

`Foo.MEMBER` folds to an N_INTLIT in the checker, typed as the
named enum. Binops on enum values yield the same enum (type_eq on
the named pointer), so `mode.R | mode.W` is a `mode`. Enum ↔ int
is a reinterpret-only `as` cast — same register, no tag wrap — so
`mode.RDWR as i32` and `1 as mode` both work without runtime ops.

`is`/`?`/`!` are still tagged-union-only. CSP runtime (chan/proc)
is unchanged; only the type-system slot is touched here.
2026-05-12 04:15:31 +09:00
22999cd3fa selfhost: mirror @test runner via ww_ww (997_at_test_ww)
@test parsing already works under ww_ww (parseattrs lives in the
shared lib/ww/parse/decl.ww, picked up by both Cstage and wwstage),
so this is the test-side parity: 997 is 910 with `ww run` swapped
for `ww_ww run`, exercising the selfhost driver+compiler+assembler+
linker end-to-end on the same attest_pass.ww fixture. Suite is now
22 tests.
2026-05-12 03:58:55 +09:00
cb78abf9e9 selfhost: is/as validity + let/return assignability checks
Three more structural checks from C check.c ported to selfhost,
at the AST level (no resolved tinfo).

is/as validity: e is T / e as T require e's declared type to be a
tagged union and T to name a variant. Mirrors the case-variant
check that just landed.

let init-type and return-type assignability: a new exprtype helper
infers an AST type-node for literal/ident/call/cast/?/as/is
expressions; isassignable approximates C type_assignable on the
shapes we can resolve — exact match, untyped numeric → typed
numeric, untyped nil → ptr/slice/chan/fn, variant inclusion, and
two-primitive-mismatch.

isassignable returns (ok, confident). When confident=false the
check emits no error — better to miss a real bug than fire a
false positive on a binary-op expression we can't infer. This
keeps existing selfhost code clean while still catching the
common typo cases (let x: bool = 42; return "hi" from i32 fn).

Naming: all new helpers follow Plan 9 run-together convention per
CLAUDE.md (`typeeqast`, `isassignable`, `exprtype`, ...). Earlier
work that used snake_case helpers (`case_variant_in`,
`check_match_exhaustive`, ...) got the same treatment — bulk
renamed in this commit.

Five new rows in 950_selfcheck exercise the new checks
(is-not-a-variant, two let mismatches, return mismatch, plus the
case-variant row already there).
2026-05-12 03:49:09 +09:00
751271a6bd wcc: case T => variant validity check (C + selfhost)
`match (u) { case T => ... }` where T isn't a variant of u was
silently accepted by both checkers. The cgen would emit a tag
comparison against an index that never appears, leaving the arm
unreachable — wasted code that's almost always a bug or typo.

C check.c now mirrors the existing is/as rule for match arms:
each `case T` and each alt of multi-pattern `case T1 | T2` is
checked against the scrutinee's variant list via variant_present.

selfhost check.ww gets the same shape with AST-level type_eq_ast
comparison. Both checks land in the same scope-aware pass that
already runs exhaustiveness and ? subset.

New test rows in 300_check (C side) and 950_selfcheck (selfhost
side) exercise both single-pattern and multi-pattern alt typos.
The 950 driver's err_present detector picks up the new
"case: not a variant" prefix.
2026-05-12 03:37:21 +09:00
68bd8197d5 selfhost: port match exhaustiveness, ?-subset, !-flag checks to check.ww
The selfhost checker did name resolution only — anything tagged-
union-shaped sailed through silently. The C check.c implements
three structural checks; this commit mirrors them at the AST level
in selfhost/cmd/wcc/check.ww:

1. Match exhaustiveness: every variant of the scrutinee's tagged
   union must be covered by a case arm (incl. multi-pattern alts)
   or a default arm. Operates on the scrutinee's declared type
   (N_TTAGGED via N_IDENT's sym.decl.lhs).

2. ? subset propagation: each error variant of the operand's type
   must be a variant of the enclosing fn's return type. Enclosing
   return must itself be a tagged union when the operand has any
   errors.

3. !-flag semantics: in flag-aware mode (any variant marked `!T`),
   error subset = flagged variants. Legacy mode (no flags) =
   everything-but-first. is_error_variant unifies both rules.

No tinfo / type-inference work: the checks read declared AST type
nodes directly. `resolvealias` chases N_TNAME → typedecl body to
handle aliased tagged unions. `type_eq_ast` does structural
comparison on the subset of type-expression shapes the checks
encounter (TNAME by string, TPTR/TSLICE/TCHAN recursive).

Folded into resolvewalk rather than a separate second pass, so the
checks see the same per-statement scope state as resolve. fnret is
threaded through resolvefnbody so ? can find the enclosing return.

New test/wcc/950_selfcheck.c — five rows exercising each error path
(missing variant, non-tagged enclosing, missing error subset
member, the flag-aware happy path, the flag-aware missing-error
case). Test suite now reports 21 ok.
2026-05-12 03:24:25 +09:00
906e17b128 wcc: @test marker attributes + runner
Hare-style `@test fn check_foo() void = { ... }` now parses. The
attribute is recognised by making the args list optional in
parseattrs: `@symbol("rt_syscall")` still requires the parens;
`@test` doesn't. Same change mirrored in lib/ww/parse/decl.ww.

The runner (test/wcc/910_at_test.c) scans a fixture for
`@test fn IDENT(`, synthesises a wrapper `main()` that calls each
test fn, builds it via `ww run`, and asserts exit 0. A failing
@test would either explicitly call abort or trip a runtime trap
(div-by-zero, etc.) and the whole driver exits non-zero.

The 910_at_test target sits alongside the existing C-side test
binaries; `make test` now runs 20 tests instead of 19.

Fixture: test/wcc/data/attest_pass.ww exercises two passing tests
(simple arithmetic and a match-with-yield).
2026-05-12 03:14:20 +09:00
404705b6fd selfhost: mirror defer; e2e tests for LIFO ordering
C cgen has carried defer for a while (defers[] global + reverse
walk on every return). Selfhost cgen now mirrors:

- cgen struct: deferbuf (**node, LIFO stack) + defertop counter.
- cgstmt N_DEFER: push n.lhs.
- cgreturn: rundefers() at entry — same as the C cgen pattern.
- cgfn fall-through return: rundefers() before zero-AX+RET.

DEFER_MAX = 16 matches C cgen.

Two new e2e rows: defer with an explicit `return acc;` (321 mod 256
= 65), and defer firing on an implicit void-fn fall-through (87).
Both rows verified via the wwstage cgen too.

Defer's semantics: queued exprs fire LIFO before the return expr
is evaluated, so a return that reads memory mutated by a deferred
call sees the post-defer state. Matches C cgen and Hare.
2026-05-12 03:11:13 +09:00
f267f99a2b wcc: match-as-expression with yield
`match (e) { ... }` can now sit in expression position, with each
arm using `yield expr;` to produce the match's value:

    let v = match (r) {
    case let n: i32 => yield n + 1;
    case let s: str => yield s.len: i32 + 100;
    };

TK_YIELD keyword + N_YIELD AST node, both appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff gates.

Checker: cexpr for N_MATCH walks each arm's body looking for the
first N_YIELD; the match's type is the unified yield type (or
ty_void if no yield, preserving the statement-form semantics).
Mismatched arm yields are flagged.

Cgen: a yield-target stack (separate from the loop break stack)
holds each enclosing match's end label. N_YIELD evaluates its
expression into AX (and BX for str) and JMPs to the topmost entry.
cgmatch pushes its end label on entry and pops on exit.

Selfhost mirror: lib/ww/lex/tok.ww kwtab+name, lib/ww/ast.ww
N_YIELD def+print, lib/ww/parse/stmt.ww yield-stmt; selfhost cgen
adds a yieldbuf to the cgen struct and a cgyield helper. Verified
end-to-end: a yield-using program compiled via the wwstage cgen
matches the C-cgen build's exit code.
2026-05-12 03:08:00 +09:00
67e27589fd selfhost: mirror nullable pointer folding for (*T | void)
C-cgen-side nullable folding landed in f4efaac. This commit catches
the selfhost cgen up so a wwstage-compiled binary produces the same
ABI for `(*T | void)`.

- cgenutil: isnullabletype(), nullableptrtag() helpers shaped to
  the selfhost cgen's AST-only world view (it doesn't carry a Type
  with a .nullable flag — it walks N_TTAGGED node lists). slotsize
  returns 8 for nullable.
- cgenexpr cgmatch: nullable arm uses pointer-vs-null discriminator
  and binds only the *T case (void has size 0).
- cgenstmt cglet: nullable target spills only AX (no tag word, no
  value-word DX/CX).
- cgenstmt cgreturn: nullable return passes AX through with no
  shuffle; bare `return;` emits AX = 0 (void encoding).

Verified end-to-end: a fn returning `(*i32 | void)` compiled by the
wwstage cgen produces the same exit code as the C-cgen build. The
995 fixed-point gate stays green — no selfhost source uses nullable
yet, so the existing tagged paths are still byte-identical.
2026-05-12 02:57:47 +09:00
f4efaac144 wcc: nullable pointer folding for (*T | void)
A tagged union with exactly one `*T` variant and one `void` variant
collapses to a single 8-byte pointer slot, where the null bit
pattern is the void variant and any non-null is the *T variant.
Mirrors Hare's `(*T | null)` ABI optimisation.

Detected in resolve_type when the post-flatten variant list has
exactly two entries of the right shape; Type.nullable = 1 and
size = 8. Codegen branches every tagged-handling site on the flag:

- match: discriminator = pointer-vs-zero, not slot+0 tag word.
  Binding for the *T case copies the same word (the pointer itself)
  rather than slot+8.
- is/as: same ptr-vs-zero discriminator.
- ?: null = error (propagate AX=0 to caller's matching null
  encoding); non-null = success (AX is already the pointer).
- !: null aborts; non-null falls through with AX = pointer.
- let-init / return: spill or set just AX (no tag/value pair).
- call-arg push: push only AX, not the now-unused DX/CX.

Prologue spill already pulled size/8 = 1 arg register via the
existing tagged-arg loop, so no change needed there.

Two existing helpers in cgen.c get nullable-aware spelling:
type_isnullable() and nullable_ptr_tag() (which variant index is
the *T side; the void side is the other one).

The Hare-style `(*T | null)` spelling isn't supported — `null` is
not a type keyword in ww. Callers use `void` instead, which is
already a real type. The result is the same bit-level layout.
2026-05-12 02:53:47 +09:00
dc8405429e selfhost: ?/! postfix in parser + cgen; use ! in lex.ww escape path
Selfhost parser (lib/ww/parse/expr.ww) recognises postfix `?` and
`!` at the same level as `as`/`is`/`:`. Selfhost cgen
(selfhost/cmd/wcc/cgenexpr.ww) emits matching code: cmp AX against
the success tag (0 in legacy mode), branch over the propagate /
abort path, then unwrap (DX → AX, CX → BX for str). Mirrors the C
cgen but without the tag-remap loop — none of the selfhost code
that uses `?` today needs cross-shape remapping.

lib/ww/lex/lex.ww \\x escape handling switched from 5-line match
blocks to one-liners: ascii.digitval(c: rune)!. Both digits are
already validated by isxdigit above; the void variant is
unreachable, so `!` collapses correctly. 995 fixed-point gate
verifies the selfhost cgen produces the same `!` codegen as C cgen.
2026-05-12 02:45:27 +09:00
d9041ab45e os: graduate tryopen/trywrite/tryread to (T | oserror)
Final piece of the os module graduation: the three try* wrappers
move off the (T | str) placeholder shape. `oserror` becomes a real
Hare-style error type (`!i64` instead of plain `i64`), so it's
picked up by ?-propagation as the error half without callers having
to name it.

tryread:  (i64 | oserror)   was (i64 | str)
trywrite: (i64 | oserror)   was (i64 | str)
tryopen:  (i32 | oserror)   was (i32 | str)

Callsites updated: wwdump uses tryopen; the e2e trywrite probe
matches on os.oserror and validates -EBADF for a bad fd (-9 instead
of the old "write failed" string length).

selfhost/test/smoke.ww switched to raw os.open(2) instead of
os.tryopen for probe 7 — same reason as the os.readall switch in
the prior commit: probe 6 in 990_selfhost compiles smoke.ww
standalone, and cross-module type refs like `os.oserror` don't
resolve in that mode.
2026-05-12 02:42:49 +09:00