Port head-only of cstage cmd/wcc/check.c:1198-1211 to exprtype's
new N_ARRLIT arm:
- Walk e.list, skip the N_FIELD repeat marker (lib/ww/parse/
expr.ww:100-107).
- First non-skipped element via recursive exprtype → elt.
- Count non-skipped elements.
- Empty list → mktname("i32") per cstage L1209.
- Synthesize N_TARRAY{lhs=elt, rhs=INTLIT{uval=count}}, stamp
tinfofornode, return.
One documented divergence from cstage: the inline type_default
lift (cstage L1206 — untyped_int → i32 etc) is skipped. Wwstage
uniformly returns the AST-level untyped name and defers defaulting
to the assignability sink; the N_INTLIT arm (check.ww:1388-1392)
and alloc-value-form (check.ww:1509) follow the same shape, so
defaulting here would be the outlier. No cgen consumer reads
e.type_ on N_ARRLIT today (verified via grep of cgen*.ww); A.6.3
will move the default to the read site or hoist a typedefault
helper — out of α scope.
α scope per Drew (ref/hare/hare/ast/expr.ha:215-218 — Hare's
array_literal is the uniform `{expand, values}` shape, no
named/anonymous split, so head-only is the natural cadence). No
hint plumbing, no unify check on mixed-type elements (cstage uses
first-element-wins).
Phase 1 A.6 step 7 of ~8 — closes the A.6.1 per-kind stamp series.
A.6.2 next: post-checker assertion that every expression node has
type_ set + fail-suite gap sweep.
Verified 132/132 incl. 995_self_rebuild byte-identity.
Port head-only of cstage cmd/wcc/check.c:1161-1197 to exprtype's
new N_STRUCTLIT arm:
- N_IDENT lhs: scopelookupprefer → SK_TYPE sym; stamp
tinfofornode(sym.decl.lhs), return that body. Mirrors cstage
L1166-1173.
- Synthetic type-expr lhs (e.g. `(*T){...}`): stamp
tinfofornode(e.lhs), return e.lhs directly. Mirrors cstage
L1174-1176.
- e.lhs == nil: bail (future Hare anonymous-lit shape ww
doesn't parse yet — lib/ww/parse/expr.ww:147-148 always
plants the TYPE_IDENT).
Field-level walk (cstage L1178-1194) stays parked behind #23 /
Phase 2; field-value exprs still get their own n.type_ via the
post-order dispatch at L460-489 (N_STRUCTLIT is in the kind
list since A.6.0).
One documented divergence from cstage: lenient on missing-
struct-type / non-SK_TYPE sym (cstage L1170 errors; wwstage
falls through to nil under the established scruttype L656 /
A.6.1.5b N_DOT struct-miss policy).
α scope per Drew (ref/hare/hare/ast/expr.ha:229-237 — Hare's
struct_literal AST distinguishes named/anonymous alias; ww
only parses the named form, so the hint param plumbed in A.6.0
stays unused for this arm). β/γ (hint plumbing into clet/
cassign; A.6.1.7 preempt) deferred per rule 11.
Phase 1 A.6 step 6 of ~8 — Hare struct_literal surface closed;
N_ARRLIT (A.6.1.7) is the genuine hint-consumer next.
Verified 132/132 incl. 995_self_rebuild byte-identity.
Port cstage cmd/wcc/check.c:833-866 stamp cases to exprtype's
N_DOT arm. Three sub-cases append to the A.6.1.5a basetn-walk
block, all pure type_ annotations — none mutate e.kind:
- Pseudo-fields .len / .cap / .ptr on slice / str / array
(cstage L833-842). `len` and `cap` stamp i32; `ptr` synthesises
an N_TPTR over the element (u8 for str, bu.lhs else).
- Struct field walk on N_TSTRUCT (cstage L843-849) — match
N_TFIELD by name, return f.lhs and stamp.
- Tuple positional access `t.0`, `t.1`, … on N_TTUPLE
(cstage L850-866). fldnumidx (cgenutil SSoT for decimal-only
parsing) yields the index; walk bu.list .next idx times.
Two divergences from cstage, documented inline:
- Wwstage carries str as N_TNAME("str") (no dedicated N_TSTR);
the pseudo-field guard tests the trio shape directly.
- Wwstage falls through to nil on struct/tuple miss instead
of erroring (lenient policy per scruttype L656).
Cumulative N_DOT arm now ~162 LOC, under Rob's 250-LOC tripwire.
No new helpers; fldnumidx reused. #56 mod.fn() parser-rep stays
parked.
Phase 1 A.6 step 5b of ~8 — closes Hare's access_field surface
(ref/hare/hare/ast/expr.ha:7-38). Cgenutil's paired walker still
derives the same shapes; A.6.3 collapses those onto these stamps.
Verified 132/132 incl. 995_self_rebuild byte-identity.
Port cstage cmd/wcc/check.c:740-832 fold cases to exprtype's new
N_DOT arm. Two paths land:
- Module-qualified ref (`pkg.x`): SK_USE leaf via
scopelookupinmodule, type returned from the resolved sym's
decl.lhs. Mirrors cstage L749-775.
- Enum member fold to N_INTLIT: bare `EnumT.MEMBER` (cstage
L780-803) and outer `pkg.EnumT.MEMBER` where the inner N_DOT
folded via case 1 (cstage L805-832, including TPTR peel at L808).
foldtointlit rewrites kind/uval/str/lhs/rhs; type_ stamps from
the enum body.
Stamp surface (struct field + pseudo-field .len/.cap/.ptr) lands
in A.6.1.5b. #56 mod.fn() parser-rep stays parked.
Two documented divergences from cstage:
- No use_alias gate — wwstage uses sym.mod disambiguation per
installdecl L195-207; SK_USE alone suffices.
- Cstage errors on unknown enum member; wwstage falls through
to nil under the lenient-check policy at scruttype L656.
Check-side enum eval is literal-only (N_INTLIT + nil-auto-
increment); cgen.ww's enumevalmember has the full const-folder
for cgen-side. Every selfhost+lib enum uses explicit literals
today (audited 2026-05-21). Tracked as task #7.
Phase 1 A.6 step 5a of ~8 — bisect-clean split per Hare's AST
access_identifier vs access_field axis
(ref/hare/hare/ast/expr.ha:7-38).
Verified 132/132 incl. 995_self_rebuild byte-identity.
Port cstage cmd/wcc/check.c:870-894 to exprtype. indexresult yields
the resolved element tnode for slice/array, u8 for str, T for *[N]T
(decay) and *T (generic), and []T for *[]T (no decay, mirrors the
Hare-faithful rule at check.c:883-885). The arm stamps e.type_ via
tinfofornode same shape as the A.6.1.3 BIN/UN pattern.
Phase 1 A.6 step 4 of 6 — slot the indexing result type into the
mandatory post-checker invariant. Cgenutil still derives index
stride from AST shape (indexbaseesz, elemsizeofc, indexvaluetnode);
A.6.3 will collapse those onto the now-stamped N_INDEX type_.
Verified 132/132 incl. 995_self_rebuild byte-identity.
Six N_CALL stamp sites in exprtype:
- alloc slice form: tt = ([]u8 | nomem)
- alloc value form: tt = (*T | nomem)
- size/align/offset fold sites: untyped_int (mirrors cstage
cmd/wcc/check.c:926/958 which sets n->type = ty_untyped_int
after the fold; the returned mktname("i32") is the assignability
target for callers, not the constant's own type)
- regular call: s.decl.lhs (mirrors cstage's build_fn_type ret)
Five error-path nil returns deliberately don't stamp.
Drop the size/align/offset trigger hook in resolvewalk: the A.6.0
end-of-fn general N_CALL dispatch already fires exprtype on every
N_CALL, making the targeted hook redundant. Pre-edit relied on
double-dispatch (hook → fold → re-dispatch → N_INTLIT stamp);
post-edit folds and stamps in one pass. Final n.type_ identical.
Hook removal + stamps bundled per CLAUDE.md rule 11: same-concern
(N_CALL handling) and the removal is what justifies the inline
fold-site stamps replacing the double-dispatch path.
Verified 132/132 incl. 995_self_rebuild byte-identity.
Phase 1 A.6.1 commit 1. Extends A.6.0's literal+ident type_
population to the CAST + TRY family + TYPEASSERT/TYPETEST arms in
exprtype. Mirrors cstage cmd/wcc/check.c:737 (N_CAST) and
1332-1435 (TYPETEST/TYPEASSERT/TRYPROP/TRYUNW) where n->type is
stamped on each value-returning path.
N_TRYPROP / N_TRYUNW only stamp the value-returning paths
(success-variant match inside the for-loop, no-error tail at
ou.list); the three nil-returning early-outs are intentionally
left unstamped.
Verified 132/132 incl. 995_self_rebuild byte-identity.
A.6.0: extend `exprtype(c, e)` → `exprtype(c, e, hint)` and dispatch
post-order on every expression-yielding node kind in resolvewalk.
hint is threaded but unused by every arm; A.6.1's STRUCTLIT/ARRLIT
arms consume it (harec's check_expression result_type shape per
feedback_hare_frontend_reference.md). Dispatch fires existing literal
+ ident stamps universally; per-kind stamp coverage lands in A.6.1+.
Cgen reads tnode.type_ (not expression-node type_), so byte-identity
holds.
Verified 132/132 incl. 995_self_rebuild.
mem.ww has 0 callers post-γ-6 — newarena/amalloc/grow/freearena/
roundup all unreferenced after the *arena cascade strip. Drop the
91-line module.
Makefile: remove mem.ww from 5 dep lists (wwdump_ww, w6c_ww,
w6a_ww, w6l_ww, ww_ww); drop `-I selfhost/cmd/wcc` from w6a_ww/
w6l_ww/ww_ww build invocations (wwdump_ww + w6c_ww still need it
for check.ww/cgen*.ww).
test/wcc/990_selfhost.c: drop 6 mem.ww entries from probe_codegen,
probe_dump_diff (×2), probe_resolve, probe_dump_stable, and
probe_cgen_match file lists.
lib/memio/memio.ww: dynamicgrow doc comment reframed as historical
context (collision source is gone, but task #9 keeps the
module-prefixed name conservative against future collisions).
Two dead `import mem;` lines remain in selfhost/cmd/w6a/asm.ww and
selfhost/test/uses.ww; tolerated silently by ww build, swept in
task #8.
main.combined.ww auto-regenerated for w6a/w6c/wwdump.
Verified 132/132 incl. 994_w6c_ww + 995_self_rebuild byte-identity.
Phase 0 closes.
Unreferenced after γ-5 *arena cascade. Removes the helper body
and its preceding doc comment; main.combined.ww auto-regenerated.
Verified 132/132 incl. 993_ww_ww + 995_self_rebuild byte-identity.
amalloc has 0 callers post-γ-2; the *arena threaded through ww
driver's importpathform / locatein / locateimport / enumeratedir /
peekpackage / arenadupcstr / builddirmodulepath / buildsearchpath /
resolvemodule and the expctx.a field are vestigial.
Drop `import mem;`, remove expctx.a, strip *arena from 9 signatures,
update 9 call sites. Drop 4 dead `let a: *arena = newarena();` in
buildone/dobuild/dorun/dotest. Comments retidied. main.combined.ww
auto-regenerated.
builddirmodulepath body intact — still 0 callers; γ-5b drops it.
arenadupcstr name is now a misnomer (task #10).
Verified 132/132 incl. 993_ww_ww + 995_self_rebuild byte-identity.
amalloc has 0 callers post-γ-2; the *arena threaded through w6l's
mklnk/resolvelib/cstrtostr/elfglobals/dcstrtostr and the lnk.a
field are vestigial.
Drop `import mem;` from sym/main/obj/dyn/dynout, remove lnk.a
struct field, strip *arena from the five signatures, update 13
call sites. Drop a dead `let a: *arena = l.a;` in dynout. Comments
at pass.ww/obj.ww/main.ww retidied to match post-strip reality.
main.combined.ww auto-regenerated.
Verified 132/132 incl. 992_w6l_ww + 996_dyn_ww + 995_self_rebuild
byte-identity.
amalloc has 0 callers post-γ-2; the *arena threaded through w6a's
init/dupstr/bufinit and the asm_.a / buf.a fields are vestigial.
Drop `import mem;` from opcodes/parse/obj/main, remove asm_.a and
buf.a struct fields, strip *arena from init/dupstr/bufinit
signatures, update 12 call sites. main.combined.ww auto-regenerated.
Comment at main.ww:36 retidied "argv/arena" → "argv-style" to match
post-strip reality.
Verified 132/132 incl. 991_w6a_ww + 995_self_rebuild byte-identity.
The final 2 astrndup callers in w6a (main.ww fname capture and the
dupstr wrapper in parse.ww) now use the uniform γ-1 shape:
let view: str;
view.ptr = src;
view.len = n: i32;
out = strings.dup(view);
With both call sites converted, wcc.astrndup is dead and removed
from selfhost/cmd/wcc/mem.ww. amalloc + arena bootstrap stay
(other callers; #7 Phase B/C territory).
The two `// astrndup until #11 (w6a types shadow) is fixed.`
WHY-pointers are obsolete (#11 landed in 6696e95) and dropped per
CLAUDE.md rule 8.
dupstr in parse.ww keeps its (*arena, *u8, u64) signature; the
vestigial *arena param is tracked by task #10.
Verified 132/132 incl. 991_w6a_ww + 995_self_rebuild byte-identity.
The file declares `package w6a;` (not a real `types` module), so
`import types;` was only the bundler file-key. The shadow against
lib/types/ blocked any w6a TU that wanted to pull lib/strings or
lib/bytes (both transitively reach `types.I64_MAX`); the w6a-local
shadow won the source-dir-first resolver, and `types.I64_MAX` came
back undefined → "ordered comparison on non-numeric".
Rename the file to its actual role — opcode + register enums
mirroring 6.out.h — and update the four import lines + Makefile
prereq. No symbol-call sites changed: every constant already
resolves unqualified inside the w6a package.
Unblocks #7 astrndup workaround comments in main.ww / parse.ww;
γ-2 (convert those two sites + delete astrndup export from
selfhost/cmd/wcc/mem.ww) becomes mechanical.
Verified 132/132 incl. 991_w6a_ww.
#7 Phase A first cut. 13 of 15 astrndup callers converted to the
explicit (*u8, n) → str view + strings.dup shape (ref/hare/strings/
dup.ha:7). astrndup export stays in selfhost/cmd/wcc/mem.ww — 2 w6a
sites blocked by the selfhost/cmd/w6a/types.ww shadow (filed as #11)
and carry an inline WHY pointer until the rename ships.
NUL-dependence audit: no consumer reads token text past `.len`. tok.text
flows through fputq (length-bounded) and parse.curtext → n.str (streq-
based dispatch across check/cgenutil); p.file is written via os.write
(ptr,len); selfhost/cmd/ww/main.ww's astrndup'd pathstr only flows into
visitseen/visitadd's manual byte-loop, while all OS calls in that file
use the unrelated `pathstr(*u8) str` view helper on the raw pointer.
Empty-str sites (lex.ww:579, 624, 640) collapse to strings.dup of an
empty view; strings.dup short-circuits len==0 (lib/strings/strings.ww:72)
and returns {nil, 0} — observationally identical to astrndup's prior
{arena_1byte, 0}.
Sites:
- lib/ww/lex/lex.ww (8: 450, 529, 554, 564, 579, 624, 640, 794)
- selfhost/cmd/w6c/main.ww:139
- selfhost/cmd/w6l/dyn.ww:111 (inside dcstrtostr)
- selfhost/cmd/w6l/obj.ww:185 (inside cstrtostr)
- selfhost/cmd/ww/main.ww:531
Wrappers (dupstr/cstrtostr/dcstrtostr) keep their bodies; deletion
deferred to #10.
Phase 0 last β-shape site. Two concerns in one commit because the
refactor surfaced the rename:
- selfhost/cmd/wcc/cgen.ww cgout buffer (cgoutbuf/cap/len + arena +
cgout_grow + CGOUT_INIT_CAP) → memio.state + io.stream behind a
one-shot lazy-init guard. cgout_enable drops its *arena param;
memio.reset in cgout_flush keeps the buffer sticky across fns so
the arena's amortisation survives — re-init per fn would abandon
the buffer and re-grow from 0 via the 8→…→65536 ladder for every
function (no io.close path → no os.free).
- lib/memio/memio.ww private fn grow → dynamicgrow. Symmetric with
dynamicwrite / dynamicclose; required because cstage bundles all
imported modules into a flat TU and resolves private fns by
unqualified name, so the new `import memio;` in wcc's bundle
collided with selfhost/cmd/wcc/mem.ww's arena `grow`. Module-aware
private-fn scoping in cstage is task #9.
@test fn dynamicgrow in memiotest.ww (same package as memio.ww)
renamed to dynamicgrowcases to free the name; new suffix mirrors the
file's existing fixedwritecases / borrowedreadcases convention.
Lazy-init guard cgoutinit. memio.dynamic runs once on first
cgout_enable; subsequent enables just set cgoutmode. Mirrors
lib/log/log.ww:124 ensureinit. Without it, ~14 mmap syscalls per fn
and ~100 MiB+ cumulative leak on a typical bootstrap.
io.write bare discard in emitbytes mirrors lib/log/log.ww:169 —
memio.dynamicwrite never returns io.closed (memio.ww:166).
Verified 132/132 incl. 995_self_rebuild byte-identity.
Phase 0 #9. cgen struct fields loopendbuf/loopcontbuf/yieldbuf/
deferbuf change from `*str`/`**node` over-allocated arena chunks
to `[]str`/`[]*node` slices. The 4 alloc sites in cgeninit drop
the byte-count form (`LOOP_MAX*24u64`, `DEFER_MAX*8u64`) for
element-count (`LOOP_MAX: u64`, `DEFER_MAX: u64`). 10 caller
sites in cgenstmt.ww/cgenexpr.ww use `[i]` indexing which works
identically for slice-shaped struct fields.
Two-line let-then-assign idiom for the 4 inits is a real checker
limitation: alloc's element-deferred `[]u8` → `[]T` retype only
fires in let-init (checkletassign N_TSLICE LHS), and cglet's
alloc-slice writeback shortcut (cgenstmt.ww:577) only fires in
let-init too. Direct `c.field = alloc([], N)!` would silently
emit a scalar alloc with a junk slice header. Filed #49 for the
checker enhancement; the let-then-assign is Hare-idiomatic in
the meantime.
Verified 132/132 + 995_self_rebuild byte-identity.
Phase 0 #8 third α/γ-batch. 33 sites total: 32 in selfhost/cmd/ww/
main.ww (driver) — 22 α `*u8` byte buffers + 10 γ `**u8` pointer
arrays — and 1 α in selfhost/cmd/wwdump/main.ww (file-slurp buffer,
previously amalloc).
Patterns:
- α: `let buf: []u8 = alloc([], N: u64)!; buf.len = N: i32;` then
`buf.ptr` to extract `*u8` for callees that still take raw pointer
(cstrinto/byteinto/readall/getdents64/...).
- γ: `let arr: []*u8 = alloc([], N)!; arr.len = N;` element-count
semantics (was bytes; ww slice alloc takes element count).
i32 .len cast: ww's slice.len is i32 so `.len = N` from a u64
source requires an explicit `: i32` cast or silent-zero results.
The previously-flagged `out = rt.malloc(PATH_MAX): *u8` reassignment
in dobuild migrates cleanly: locally allocate `outbuf: []u8`, then
`out = outbuf.ptr` to preserve the `*u8` shape for the else-branch
from defaultoutpath. No GC + process-exit reclaim makes the bare
.ptr lifetime-safe (no free path needed).
0 sites deferred. Verified make test 132/132 + 995_self_rebuild
byte-identity. Advances #44.
CLAUDE.md rule 9 amended with the explicit carve-out: ww is C/Plan-9-
lineage — no GC, no "safe" baseline to be unsafe relative to — so the
Hare `_unsafe` suffix flags an axis ww doesn't have. The convention
is dropped wholesale in lib/.
Concrete changes:
- lib/strings: `fromutf8_unsafe` → `frombytes` (pure reinterpret). The
validating sibling `fromutf8` is deleted entirely (28 lines, plus its
84-line fromutf8_cases test). Callers that need validation write the
two lines inline at the IO source: `utf8.validate(b)?;
let s = strings.frombytes(b);`. `fromutf8` name reserved for a future
true validating helper.
- lib/strings α-batch: concat/join/lpad/rpad migrate from
`rt.malloc(N): *u8` to `alloc([], N)!` + `buf.len = N;` +
`return frombytes(buf);`. Same dup-pilot pattern (4c07ef0). Task #41.
- lib/memio header comment trimmed: drops a stale reference to
"lib has no fromutf8 today"; cites the rule-9 carve-out instead.
- Caller renames across selfhost combined.ww files (auto-regen) +
cgenutil.ww comment ref.
Rule-11 disclosure on the bundle: the rename and the α-batch are
nominally separable concerns (symbol-naming policy vs amalloc→
alloc-slice migration), but they touch the same 4 functions in
lib/strings/strings.ww — the α-batch's first emission of `frombytes`
postdates the rename. The α-batch was applied on top of the rename
sweep mid-flight by the pre-commit reviewer; splitting them back
out is fiddly text surgery for marginal bisect value. The rename is
the primary concern; α-batch is one entry in #8's sized-slice
migration.
Verified: make test 132/132, 995_self_rebuild byte-identity holds.
Closes#42; advances #41.
Pilot for task #8 (runtime-N alloc API). `alloc([], n)!` yields a
slice with cap=n, len=0; explicit `buf.len = s.len;` lifts the len
before the fromutf8_unsafe reinterpret. Same shape as
ref/hare/strings/dup.ha:15 modulo ww not yet having `append`
(task #36) — open-coded byte loop in lieu of static-append.
Verified 132/132 + 995_self_rebuild byte-identity. Pattern is the
template for the next α-category sites (concat/join/lpad/rpad/...).
Hare's canonical runtime allocator is rt::malloc with linker symbol
rt.malloc (ref/hare/rt/malloc.ha:27,78). ww kept the dot→underscore
Plan 9 convention (CLAUDE.md rule 4) so the linker symbol becomes
rt_malloc; the lib/rt exported function name becomes malloc; ww
callers say rt.malloc(...).
The language builtin keyword stays `alloc(T)!` — unchanged from Hare
(ref/hare/hare/lex/token.ha:21 ltok::ALLOC, parse/expr.ha:398
builtin()). The rename only touches the lowered linker symbol and the
exported function name behind it; the user-facing syntax for
heap-allocation is identical to Hare.
Surface:
- rt/alloc.s: TEXT rt_alloc → TEXT rt_malloc, labels updated
- lib/rt/malloc.ww: @symbol("rt_malloc") fn malloc(...) (was rt_alloc/alloc)
- rt/ensure.ww: local FFI decl + call site updated to malloc; `!` dropped
on the direct FFI call (rt_malloc returns *void, not a tagged union)
- 18 .ww callers: rt.alloc(...) → rt.malloc(...)
- cstage cmd/wcc/check.c + wwstage selfhost/cmd/wcc/check.ww
alloc-builtin suppression gate routes through ffi_resolve("malloc")
for the lowering; the user-shadow check still keys on the BUILTIN
KEYWORD "alloc" since that is what `alloc(...)` parses as. Adding
"malloc" to the user-shadow check was unnecessary and was reverted
during pre-commit review.
- cstage cmd/w6c/cgen.c: 2× ffi_resolve("alloc") → ffi_resolve("malloc")
- wwstage cgenexpr/cgenstmt: 2× ffiresolve(c, "alloc") → ffiresolve(c, "malloc")
- Test fixtures (700_e2e, 758_cgalloc_str_field, 990_selfhost, 992_w6l_ww,
selfhost/test/tagged_ptr_ret.ww): updated inline ww sources to the new
decl + call form
This is commit 2 of 3 in the lib/rt extraction (#38). Commit 3 closes
the OOM contract — return type becomes nullable *void and the builtin
lowering null-checks + propagates nomem.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip identical) + make clean cold rebuild.
Hare puts runtime allocation in rt::, not os:: (ref/hare/rt/malloc.ha:27,
README). ww's `@symbol("rt_alloc") fn alloc(n: u64) *void;` lived at
lib/os/os.ww as a historical bootstrap shortcut; this commit relocates
it to a new lib/rt/malloc.ww and sweeps every site that depended on
`import os` for the alloc decl over to `import rt`.
This is commit 1 of 3 in the lib/rt extraction (#35):
1. (this) move decl, sweep imports — preserves shape
2. rename rt_alloc → rt_malloc (#38)
3. nullable return type + OOM-propagating builtin lowering (#39)
No rename here. Symbol stays rt_alloc, function stays `alloc`, return
stays *void. Behavior identical — same ffi resolution outcome, just
sourced from a different module file. The rt::ensure runtime helper at
selfhost/rt/ensure.ww is its own compilation unit with a local decl and
is untouched.
Side effect: every wcc cgen file used `rt` as a local *node variable
name for "return type." `import rt;` shadows the module, so each
selfhost/cmd/wcc/{check,cgenstmt,cgenexpr,cgenutil}.ww site renamed
to `rtyp`. Mechanical follow-through; only the wcc module-import was
forced to do this rename.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip byte-identical).
dobuild/dorun/dotest each allocated a 2-byte heap "." prefix buffer via
amalloc, set dot[0]='.'; dot[1]=0; passed dot as *u8 to a callee, then
let the arena chunk live forever. The dot pointer never escapes the
function — every callee chain (resolvemodule, cstrendswithlit,
rundirtests/runsingletest) byte-copies its input into a fresh arena
allocation before returning, never storing the original pointer.
Replace with `let dot: [2]u8 = ['.': u8, 0u8]; ... &dot[0]`. Both
stages allocate a fresh frame slot per let at function-frame entry
(localoff cstage / localadd wwstage), so the slot lives across the
synchronous callee.
Verified 132/132 + 995_self_rebuild byte-identity.
Phase 0 batch 4. Single typed-struct site in the driver. Retires the
32u64 over-size workaround on a 24-byte strnode (selfhost/CLAUDE.md
trap #1 — amalloc < struct corrupts the next slot).
14 other amalloc sites in ww/main.ww are runtime-N path/name buffers
(13 → task #8) and 3 fixed-max ".\0" prefix buffers (→ task #9).
wwdump/main.ww's 1 site is a runtime-N file-size buffer (→ task #8).
Verified 132/132 + 995_self_rebuild byte-identity.
Phase 0 batch 3a. structinfo registration + fieldinfo per-field in
registerstruct (cgenutil.ww). Both relied on amalloc-zero for fields=nil
and totsize=0 (structinfo) and finext=nil (fieldinfo); MAP_ANON-zero
covers the same slots.
check.ww:842 (arenau64tos 24B scratch) deferred to #11.
cgenutil.ww:108 (mkvarargname runtime-N) deferred to #8.
Verified 132/132 + 995_self_rebuild byte-identity.
Phase 0 batch 1. Six typed-struct allocations switch from
amalloc(arena, NNu64): *T over-sized byte counts to alloc(T{...})!
with partial struct literal initialization. MAP_ANON-zero from
rt_alloc covers any field the literal omits — same contract the
amalloc bump arena provided via its explicit zero loop, but
without the rule-13 size literal at the call site.
Converted:
- w6a/asm.ww addreloc, addrelocdata, addfixup (areloc, afixup)
- w6l/sym.ww intern (lsym)
- w6l/dyn.ww loadso (lso), lexport
lexport's `if (vernamecs == nil) { e.version.ptr = nil;
e.version.len = 0i32; }` branch dropped — MAP_ANON-zero provides
the empty-version slot for free; the inverted `if (vernamecs !=
nil)` only takes the dcstrtostr path.
w6l/sym.ww:20 comment updated from "amalloc-zeroing" to
"alloc-zeroing gives 0, not -1" so the documented mechanism
matches the call.
w6a/obj.ww's 2 remaining amalloc sites (bufinit + bufgrow) are
runtime-N *u8 byte buffers, deferred to #8 (runtime-N alloc API).
Verified via 991_w6a_ww + 992_w6l_ww + 995_self_rebuild +
996_dyn_ww byte-identity. make test 132/132.
Four ad-hoc emit sites in cgalloc's N_STRUCTLIT field-store loop
(cgenexpr.ww:2770-2802) wrote the displacement via
emitint(foff: i64); emitline("(REG)\n"), producing 0(REG) for
foff=0. cstage's txt.c:130-134 omits the zero displacement, so
ww2.s (cstage compiling wwstage) and ww3.s (wwstage compiling
wwstage) would diverge the moment any selfhost site migrates to
alloc(T{...})!. Dormant today only because selfhost source has
no alloc(T{...})! yet.
Route the four sites through emitdispreg (cgen.ww:786), the
existing SSoT that already omits zero displacement.
Extends test/wcc/758_cgalloc_str_field.c with 4 table-driven
asm_disp_rows pinning the displacement text for {str/int/f64
at offset 0, str at offset 8}. Internal subtest count: 16 → 20.
The 3 foff=0 rows fail without the fix.
Wwstage's cgalloc hardcoded `CALL rt_alloc(SB)` at cgenexpr.ww:2747 and
cgenstmt.ww:647. Cstage already routes through ffi_resolve("alloc")
at cmd/w6c/cgen.c:4149 — when a fixture lacks the @symbol("rt_alloc")
decl in scope, cstage falls back to `CALL alloc(SB)` while wwstage
still emits `CALL rt_alloc(SB)`. The divergence is dormant in
ww build (combined.ww always pulls lib/os/os.ww's decl) but activates
under direct `w6c file.ww` and any other single-file path.
Replace the hardcoded line with the ffiresolve(c, "alloc") pattern
already used for user-function calls. The @symbol decl in lib/os/os.ww
is unchanged and propagates via the combine step.
Extends test/wcc/758_cgalloc_str_field.c with 4 table-driven asm rows
that compile a fixture via direct w6c (no combine) and `cmp` the
CALL <sym>(SB) line between stages. The 3 noscope rows fail without
the fix and pass with it; the withsym row pins the positive ffi-hit
path. Test count internal: 12 → 16; total make test: 132/132.
wwstage cgalloc N_STRUCTLIT branch emitted MOVQ AX,foff(BX) for every
non-float field. For a str field the cgexpr result is (AX=ptr, BX=len)
and the single MOVQ clobbered BX with the heap pointer, dropping len.
Mirror cmd/w6c/cgen.c:4184-4190: isstrtype branch routes through CX
so BX=len survives. TY_STR only — slice/tagged/fn-pair have the same
gap on both stages (task #23, parked behind Phase 2).
New test/wcc/758_cgalloc_str_field.c is table-driven (6 rows), fails
without the fix under wwstage with predicted exit codes.
Drew's Hare-discipline framing: "no hardcoded size literals anywhere in
the compiler." This session spent 32 commits sweeping after-the-fact
and STILL kept introducing new bypass sites in our own structural
work (A.5's tupleelemslot/fieldslotsize most recently). The cure is a
gate that catches new violations at commit time, not a deeper sweep.
tools/sizelint (sh+gawk):
- Always-on: `.size = NN` / `->size = NN` / `prim(...,"name",NN,...)`.
- Context-gated literals (NN(u64|i64) and `return NN`) in files or fns
matching size|slot|elem|field|stride|paramfield|tinfo|primtype|
slotsize|letemit|tagged.
- Allow-list via `// sizelint-ok: <reason>` or `/* sizelint-ok: ... */`.
- Comment strip happens after allow-list match so prose mentions of
16/24 stay quiet.
Makefile: `test: all sizelint $(TESTS)` so the gate runs before any
binary builds.
CLAUDE.md rule 13 documents the discipline + escape hatch + optional
pre-commit-hook symlink.
Audit caught 3 real cstage bugs (cmd/wcc/check.c resolve_type:1002,
1079, 1531 hardcoded `tt->size = 16` / `= 32` for tagged-with-ptr and
tagged-with-slice payloads — should read `8 + sub.size`). Fixed
inline; behavioral no-op today (pt->size=16, st->size=24, sub.size=24
match the prior literals) but the SSoT seam carries forward through
#1/#34/#65.
8 SSoT-seed allow-lists added (cstage type.c ty_str/ty_slice prim
factories; wwstage primtypesize/tyslicesize; lib/ww/typ.ww tystr +
slice fields + their main.combined.ww mirrors). One amalloc-overalloc
allow-list at lib/ww/typ.ww:273 cites pending #36 (typed amalloc).
#66 filed for extending the filter once #65 routes lib/bytes +
lib/getopt's sizeof(slice) / sizeof(option) literals through SSoT —
naive line-pattern extension would false-positive on 22+ ELF wire-
format sites in dynout.ww.
131/131 + 994 + 995 + bootstrap green with `make sizelint` exit 0.
Phase A.5's tupleelemslot / fieldslotsize hardcoded 16u64 for TY_STR
and 24u64 for TY_SLICE — bypassing the tinfo.size SSoT seeded by
lib/ww/typ.ww:189 (the very pivot they were introduced to consult).
Route those four arms through pt.size / ft.size so #1 (str→24) and
#34 (slice graduation) land as a one-line bump at the seed.
lib/strings/stringstest.ww carried 12 `(cap: u64) * 16u64` strides
missed by #43's sweep over strings.ww + shlex.ww; convert to
`* size(str): u64` so the #42 fold owns the constant. Doc comments
in strings.ww (freeall + splitn) updated to the same SSoT form.
No-op at today's str.size=16 / slice=24: tinfo.size already matches
the literals these arms had baked in. Reviewer's pre/post asm-identity
probe (struct{i64,str,i64} + (i32,str,i32) tuple + bare str) shows
zero-byte diff. 131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) green.
Forward-link to #1 (str→24B bump) and #64 (sizelint pre-commit gate);
#65 filed for lib/bytes + lib/getopt sibling sites the reviewer
surfaced. Forward of #64 will catch any future regressions of this
class.
A.4 left 74 fallback hits, all TNAME-flavored — 71 TNAME → TY_STRUCT
(natural-align vs slot-padded mismatch) + 3 module-name TNAME quirks
(`let l: lex;` where lex is both struct and imported module).
tinfo gains a slotsize: u64 field (96 → 104 bytes; amalloc bumped
to 112B per rule-7). size(T) stays Hare-natural at the user level;
cgen's slot storage now reads ti.slotsize for kinds where the two
differ. tinfofornode populates both:
- TSTRUCT: existing natural-align walk for r.size; new size-derived
align walk (sz≥8→8, ≥4→4, ≥2→2) for r.slotsize, rounded to 8.
Mirrors cgenutil.ww:2192-2218 registerstruct exactly.
- TTUPLE: parallel via tupleelemslot helper (primitives→8, str=16,
slice=24, ptr/fn/chan/i64/u64/int/uint/uintptr/f64=8, composite
→pt.slotsize, void=0).
- TARRAY: typearray sets slotsize = sub.slotsize * n. [N]i32 stays
4N (natural); [N]Triplet lifts to 16N (slot-padded). Reverts
A.4's r.size override since slot-pad now lives in slotsize.
- TFN/TENUM/TTAGGED/nullable: explicit slotsize. Default trail
`if r.slotsize == 0 then r.slotsize = r.size` catches TBANG.
- New fieldslotsize(ft) helper mirrors registerstruct's per-field
rule (struct→ft.slotsize, array→ft.slotsize, primitive→ft.size,
tagged→ft.size).
slotsize fast-path (cgenutil.ww) reads ti.slotsize for TY_STRUCT,
TY_TUPLE, TY_ARRAY; ti.size stays correct for PTR/SLICE/CHAN/FN/
STR/TAGGED/VOID (size == slotsize for those). Narrow scalars still
pad-to-8 at the read site (moving into slotsize would break
[N]i32 stride).
lib/ww/sym.ww adds scopelookuptype(s, name) — same FNV bucket+parent
walk as scopelookup but filtered on skind==SK_TYPE. resolvealias
calls it when bare-leaf scopelookup returns non-TYPE (e.g., the
SK_USE/SK_MOD short-circuit case). Fixes `let l: lex;` (mod=leaf)
AND `let t: tok;` (mod≠leaf, tok lives in package lex).
Post-A.5 fallback: 0 across full bootstrap. Reviewer's stricter
metric (zero fast-path MISSES when tinfo IS stamped) also 0;
remaining FB_NIL hits are value-expression nodes the checker
doesn't yet stamp — A.6 candidate.
Ragged-tail probe `struct{inner=3*i32, mark:i32}`: ti.size=16
(natural), ti.slotsize=24 (slot-padded). Cstage emits [N]<ragged>
stride=16 on the same source — latent divergence filed as #63.
Not exercised by selfhost, so bootstrap byte-identity holds today.
131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) all green.
A.3 left wwstage slotsize at 134 fallback hits. Per-kind breakdown:
N_TARRAY 33 + N_TNAME 101 (of which 71 resolve to TY_STRUCT, 3 to
module-name quirks, 27 already had tinfo populated and were spurious
fallbacks via missed cache hits).
tinfofornode N_TNAME: existing arm already reached the resolved body
via aliaslookup → tinfofornode recursion (reviewer-61a3's "isn't
reaching body" hypothesis disproved by per-name instrumentation). A.4
binds the resolved-body node into the cache too — mirrors A.2's
TSTRUCT/TFN/TTUPLE/TTAGGED cycle-break pattern so future calls on
either node short-circuit.
tinfofornode N_TARRAY: when sub.kind == TY_STRUCT, round sub.size up
to 8 before stride. Mirrors registerstruct's slot-padded element
stride (cgenutil.ww:2156-2165 / :2233). Primitive elements stay
natural (slotsize's TARRAY walker also keeps them natural).
slotsize fast-path adds TY_VOID (size 0) and TY_ARRAY (gated on
alen > 0 so `[_]T` keeps routing through letslotsize). TY_STRUCT
deferred to A.5: tinfofornode TSTRUCT uses per-field natural-align
so size(T) stays natural at user level, but registerstruct uses
size-derived align with nested structs slot-padded — diverges on
ragged-tail shapes (`{inner=3*i32, mark: i32}` gives natural=16 vs
totsize=24). Proper A.5 design is a tinfo.slotsize SSoT distinct
from tinfo.size.
Module-name TNAME quirks (`let l: lex;` where lex is both a struct
and the imported module): resolvealias short-circuits on SK_MOD,
n.type_ stays nil, falls through to AST walker which structlookups
correctly. 3 hits in tree. A.5 work alongside TSTRUCT.
Post-A.4 fallback: wwdump 134→45, w6a 17→12, w6l 6→6, ww 12→11
(reviewer also measured w6c at 40). Total 169→74 across the corpus
(56% reduction). All 74 are TNAME → TY_STRUCT or module-name quirks.
131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) byte-identical.
A.2's slotsize fast-path covered PTR/SLICE/CHAN/FN/STR but bailed on
TAGGED (no nullable fold) and on primitives (cstage let_emit_size pads
to 8B for slot storage; tinfo.size is natural width). Fallback hit
count under wwdump build was 2187. A.3 closes both gaps.
tinfo gains a `nullable: i32` field (fits the existing 4B pad, struct
stays 96B). tinfofornode's N_TTAGGED arm detects `(*T | void)` (exactly
2 variants, one N_TPTR, one bare N_TNAME "void" — aliased or !void-
wrapped void don't match) and folds to size=8, align=8, nullable=1.
Mirrors cmd/wcc/check.c:412-426.
slotsize fast-path re-adds TY_TAGGED (safe now) and gains a primitive-
pad branch: BOOL/RUNE/I8-I64/U8-U64/INT/UINT/UINTPTR/ENUM/F32/F64 →
return 8. Padding lives at the read site; tinfo.size remains a faithful
natural-width SSoT. TUPLE/TSTRUCT/TARRAY deliberately stay on the
fallback because per-field stride is registerstruct.totsize, not
tinfo.size.
Post-A.3 fallback hit count: 134 (94% reduction from A.2's 2187).
Reviewer's per-kind breakdown: N_TNAME 101 (alias-to-struct chains)
+ N_TARRAY 33 (struct-element rounding) account for all remaining
hits. Both A.4 work.
Probes: `(*i32 | void)` byte-identical between stages with the
8B nullable encoding. `(*i32 | nomem)` correctly does NOT fold
(nomem ≠ bare void). `(*i32 | !void)` correctly does NOT fold
(N_TBANG isn't N_TNAME).
131/131 + 994 + 995 + bootstrap byte-identical (ww2==ww3==ww4).
tinfofornode (check.ww) covers six more kinds:
- N_TARRAY: typearray on recursed element, size = esz * elen.
- N_TFN: 8B/8B; recurse on ret.
- N_TENUM: storage size/align (default i32 → 4B). Mirrors cstage
check.c:531-542.
- N_TTUPLE: raw element sum + max-align. Mirrors check.c:329-345.
- N_TSTRUCT: per-field align, round total to maxalign. Mirrors
check.c:280-340 / :468-527.
- N_TTAGGED: 8B tag + (max(variant)+7)&~7, al ≥ 8. Mirrors
check.c:347-435.
Cycle-prone arms (TFN/TTUPLE/TSTRUCT/TTAGGED) pre-bind the in-progress
tinfo into the cache BEFORE recursing on subfields so self-referential
shapes (`type node = struct { next: *node, … }`) terminate. Pre-fix
wwdump_ww segfaulted on its own combined source.
More population sites in exprtype: every primitive literal arm
(N_FLOATLIT/N_STRLIT/N_RUNELIT/N_TRUE/N_FALSE/N_VOIDLIT/N_NIL —
A.1 only had N_INTLIT), N_IDENT (propagate from sym.decl.lhs.type_,
eagerly tinfofornode + cache if not yet visited), resolvewalk type-expr
stamping, and resolvefnbody now recurses into N_PARAM.lhs (pre-#61 the
param type-exprs were never walked — every param had nil type_).
slotsize (cgenutil.ww) gains a fast-path: when n.type_ is set AND the
kind is PTR / SLICE / CHAN / FN / STR, return ti.size: i32 directly.
The fallback walker stays alive for primitive scalars, enums, named
structs, inline composites, TARRAY — those need cstage's let_emit_size
slot-pad-to-8 contract (cmd/w6c/cgen.c:691-720) which tinfo doesn't
carry. A.3+ moves padding into the fast-path.
TY_TAGGED *not* in the fast-path (reviewer-61a2 caught this) —
tinfofornode's TTAGGED arm doesn't implement cstage's nullable-pointer
fold (check.c:412-426: `(*T | void) → 8B`). Self-host code happens not
to use that shape today, but the divergence would land latent. Pull
TAGGED until A.3 folds nullable into tinfofornode.
A.2 fallback-hit count under wwdump build: 1554 fast vs 2187 fallback —
partial graduation; expected. 131/131 + 994 + 995 + bootstrap
byte-identical (ww2==ww3==ww4).