7 Commits

Author SHA1 Message Date
c7d9dc92de selfhost: migrate tool sources to directory packages
Build w6a and w6l from package-main directories and expose the wcc backend through a narrow package API so w6c and wwdump no longer import implementation files. Retarget the remaining load-bearing fixtures and example sources to directory packages; retain the one intentional flat compiler collision as an explicitly composed raw unit.
2026-08-12 17:12:03 +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
d9aba892f6 examples: lisp — drive wwstage by default; retire stale workarounds
Makefile sets WW_W6C=$(BIN)/w6c_ww so `make`, `make test`, and
`make demo` all use the ww-built backend. With the wwstage cgen
fixes in selfhost/cmd/wcc/ the demo no longer needs to dodge:

- bug #1+#2 (global addressing): symbol interner indexes
  sym_off / sym_len / sym_blob directly. No `let blob = sym_blob;`
  aliasing.
- bug #3 (chained non-pointer sub-struct field): not retired here
  (lexer.cur is still flattened) but the cgen now handles the
  shape; un-flattening is cosmetic.
- bug #4 (f64 through every boundary): vfloat writes p.fval = v
  directly; promote_v's FLOAT branch is one assign; to_f64 reads
  v.ival as f64 / v.fval directly. Drops fbuf, FVAL_OFF, copybytes.
- bug #7 (xs[i].field): builtins write xs[0].kind / xs[0].car
  directly; no `let p = xs[0];` first.

lisp_test still 101/101.

CLAUDE.md marks each historical bug as retired or still load-
bearing; #6 (slice-len in tagged-union return) and #8 (f64
compound assign) are the remaining shapes to avoid.
2026-05-13 03:07:01 +09:00
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