Hare admits an array with a defined length wherever its element slice is
expected (assign / return / call-arg / init) as a borrow; ww rejected it
everywhere (the #108(c) exclusion), so base64 worked around the gap with
explicit a[0:n] slices.
type_assignable / isassignable now admit array->slice on an exact element
match (mirror ref/harec/src/types.c:1080-1097, the SLICE-dst arm). The four
acceptance sites route through one shared helper (desugar_arrayslice /
desugararrayslice) that rewrites the array expr to the explicit full slice
arr[0:len(arr)] — an N_SLICE over the array base. cgen is untouched: the
existing slice lowering (#252/#257/#135 made array bases, incl struct-field
arrays, correct) materialises the borrow header {.ptr=&arr[0], .len=N,
.cap=N}, byte-identically in both stages.
wwstage runs no general call-arg / N_ASSIGN typecheck, so checkassign +
desugarcallargs are added solely to route those two contexts through the
shared desugar (rule-10). desugarcallargs additionally loud-rejects an
element-MISMATCH array into a []T param, scoped to that shape so wwstage's
broader call-arg leniency is untouched.
953_arraytoslice_run covers the four contexts + a borrow-alias proof + the
i32/u8 element axis (dual-stage run + cs==ww byte-id), plus mismatch-reject
rows asserting both stages refuse [4]i32 -> []u8. Regen'd w6c + wwdump
combined.ww (#110).
The tagged→tagged subset arm walked src's leaves against dst's flat
variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper
REJECTED at cstage's checker — wrapper's leaves (unsupported, underread,
nomem) aren't direct variants of dst. Wwstage's permissive tail
accepted silently but cgen then miscompiled the tag (#199b layout-
extension family, deferred).
Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is
a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal
to src, accept by nominal identity BEFORE the subset loop. Wwstage's
isassignable mirrors the structural insertion before the existing
`*confident = false; return true;` tail (deferred-tightening per #202).
SSoT with `is`/`as` non-recursive variant lookup (#198 family).
Cgen's tag-remap for the wrapper-as-whole case still maps src variants
to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx,
dst.payload = src` is #199b future-work. Probe verifies checker-accept
+ runtime exit-clean only; does NOT inspect the resulting variant tag.
Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro,
nested-wrapper, pure-leaf subset (regression), concrete-unrelated
rejection (gate), branched callee. Two sibling cgen/checker bugs
surfaced (wwstage cgwidentaggedstorebp ssz<slot_sz pad gap; wwstage
isassignable !void-alias collapse) and documented inline at the
probe-row comment, kept in #202 family.
cgen has no wrapped-slot layout — the tagged-union slot is universally
[tag:8B][payload:up_to_24B], single level. The recursive walk admitted
let r: (size|io.eof|io.error) = u for u: io.underread (transitively
in io.error.params); cg_tag_for_variant + taggedvariantindext don't
recurse, returned -1, defaulted to tag=0, and the slot read back as
variant 0 = size at runtime.
Restores SSoT inside the checker pair: is / as / match variant
lookup is already non-recursive (#198 sibling), and the LET-init /
return / assign arms now agree. Aligns DOWN to the leaner side
(rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
drill at ref/harec/src/types.c:702-739 (#199b is the deferred
wrapped-slot layout port).
Pre-flight audit (drew mandate): zero transitive-widen sites in
lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant
(io.error, strconv.error, fmt.field) is used as a variant of a
wider union anywhere in bootstrap. Mechanical fix.
Escape hatch for callers: spread (...wrapper) inlines the wrapper's
flat variants into the parent set at parse time. Wwstage's gate
additionally preserves the recursive drill on op == TK_ELLIPSIS
because wwstage stays AST-keyed (cstage flattens at resolve_type).
771_widen_transitive: 5 rows (reject_transitive_widen,
spread_alt_widen, direct_flat_variant, branched_callee_widen,
wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on
spread-expanded variants is open-bug #190/#198.
#108 sub-fold (c): opaque as a type-erasure sink. Two implicit
assignability rules + the reinterpret casts sort's impl relies on.
rule 1 `*T -> *opaque` IMPLICIT — any pointer is the universal
void-pointer. harec type_is_assignable pointer arm
(ref/harec/src/types.c:1053: `case STORAGE_OPAQUE: break;`
— the referent need not match).
rule 2 `[]T -> []opaque` IMPLICIT — any slice is the erased slice;
{ptr,len,cap} header is normal, byte stride supplied at
runtime. harec slice arm (types.c:1094).
Both fire only when the destination element is opaque, so they are
inert on the opaque-free selfhost corpus.
Rule-10 (per-rule, empirical): rules 1 & 2 are CSTAGE-ONLY. cstage
type_assignable gains the sink; the wwstage check.ww isassignable is a
resolve-only AST approximation that returns "can't tell, stay quiet"
(confident=false) for a ptr/slice whose element it cannot match, so it
already ACCEPTS every form (let-init AND call-arg). Verified: w6c_ww
compiles each probe source exit 0, byte-identically to w6c. cstage
rejected these before this change; no ww twin is needed (same
align-down precedent as 960/961's cstage-only arms).
Casts: N_CAST is validation-free in BOTH stages (the checker never
checks cast legality), so `[]opaque -> *u8` / `*opaque -> *u8`/`*i32`
are already legal. The reinterpret CGEN needed NO change: cgexpr
leaves the pointer in AX for both a slice (so slice->ptr naturally
takes .ptr) and a pointer (ptr->ptr is a no-op). drew described the
Hare idiom as `*[*]u8`; ww has no unbounded-array `[*]`, so the
ww-faithful reinterpret target is `*u8` + uintptr stride arithmetic.
cs==ww byte-id proven on every probe row.
Array->[]opaque (harec array->slice decay, types.c:1080-1099) is
deliberately EXCLUDED: ww has no implicit array->slice for any element
type (`let s: []i32 = a` is rejected too — a slice is built only via
an explicit `a[0:n]`), so there is no array->slice-header cgen.
Accepting array->[]opaque alone would assign a fat array local into a
24-byte slot with no decay: a silent miscompile (rule 7). sort's
caller passes a slice, so slice->[]opaque suffices.
opaque is unused by the bootstrap → INERT → 990-997 stay
byte-identical; combined.ww unchanged (no embedded source touched).
New probe 962_opaque_assign_cast_run carries both dimensions per row
(cstage build+run asserting type-erasure round-trips, AND a w6c-vs-
w6c_ww .s byte-id gate — the 990-997 gates never exercise opaque, so
the test pins rule-10 symmetry itself): rule1_implicit_ptr,
rule2_implicit_slice, and sort_pattern (byte-swap via uintptr stride
through []opaque, read back through the *opaque path and the original
[]i32 view). Probe binds call results before comparing to dodge a
pre-existing inline-call-result-in-comparison cgen bug (#116 family,
reproduces with zero opaque) — same dodge 960 uses.
#108 sub-fold (a): TY_OPAQUE exists, is name-bindable, and carries an
UNDEFINED size sentinel. Mirrors the #85 `size` fold pattern at every
site, both stages (rule-10).
opaque is abstract + UNSIZED: prim()'d with size=align=SIZE_UNDEFINED
(NOT 0 — a 0 would let a bare `let x: opaque` fabricate a 0-byte local),
mirroring harec builtin_type_opaque (ref/harec/src/types.c:1446). ww had
no incomplete-size sentinel, so this fold ADDS one: cstage
`#define SIZE_UNDEFINED ((u64)-1)` (== harec types.h:58 (size_t)-1) and
wwstage `def SIZE_UNDEFINED: u64 = 18446744073709551615`.
Legal only behind indirection: `*opaque` (8B ptr) and `[]opaque` (24B
slice header) construct correctly because type_ptr/type_slice (and the
wwstage typeptr/typeslice) size themselves independent of the element.
opaque is deliberately absent from is-int/unsigned/num/float and from
the size-classification switches (let_emit_size / tupleelemslot /
fieldslotsize) on both stages — it only reaches those as TY_PTR/TY_SLICE.
The use-restriction GUARDS (reject bare opaque / size(opaque) / opaque
field / [N]opaque / []opaque-indexing), assignability, and cgen-verify
are the separate sub-folds (b)/(c)/(d) — NOT here.
opaque is unused by the bootstrap, so 990-997 stay byte-identical
(inert, like #85). Regenerates the w6c/wwdump combined.ww (typ.ww +
check.ww embedded). New probe 960_opaque_decl_run exercises `*opaque`
and `[]opaque` (.len/.ptr) behind indirection.
fold-1: type exists + classifies; mirrors TY_UINTPTR at every site, both stages. size(T)/len() return types UNCHANGED (fold-2). Regenerates the 5 combined.ww (lib/ww embedded).
str IS []u8 (#1 landed the 24B layout); F1 populates the element type
so the step-3 checker collapse can read str.sub instead of special-
casing TY_STR. No reader consumes str.sub yet, so this is byte-id-
neutral: every shared ->sub reader a TY_STR value can reach is
invariant under NULL->u8 -- u8 is unsigned + size-1, matching the
prior NULL-defaults (size->1, signed->0, isstr/istagged->false); the
only ->size derefs are guarded behind esz>1, which stays false for
str.
Verified inert: compiling a fixed source with the pre- and post-F1
compilers emits byte-identical asm on both stages; cross-stage
byte-id holds and full make test (135 tests incl. 990-997) is green.
cstage cmd/wcc/type.c, wwstage lib/ww/typ.ww; combined.ww regenerated
via the canonical make path.
A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.
Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
path already used by 32B slice-variant unions -- str-variant is now
structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.
Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.
Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
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.
Per Hare convention, `nomem` is a language-level error type — no
import required, in scope alongside void/done/rune/str. ref/hare uses
it bare at errors/string.ha:14, types/c/strings.ha:89, net/uri/parse.ha:17
with no `use`. Precondition for graduating the `alloc` builtin to
`(*T | nomem)` returns.
cstage: ty_nomem is NAMED{under=ty_void, iserror=1}, installed by
typesinit and surfaced via lookup_builtin. wwstage seeds the same
shape in both check.ww (scope) and cgen.ww (aliases) — separate
tables, both consulted; without the cgen seed wwstage drops the
zero-init for `let e: nomem;` locals and breaks byte-identity.
Tests: tagged_ptr_ret.ww and trypromote.ww drop their local
`type nomem = !void;` aliases. 990_selfhost.c adds a regression that
a value named `nomem` does not collide with the predeclared type.
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.
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.
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.
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.
`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.
`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.
A type prefixed with `!` is flagged as an error variant. When any
variant in a tagged union carries the flag, `?` propagation uses
those (and only those) as the error subset; the unflagged variant
is the success type. The legacy "first variant = success" rule still
applies when no `!`-flag is present, so existing code keeps working.
- TK_NOT in parsetype → N_TBANG wrapper (lhs = inner type expr).
Appended to Nkind tail for wwdump-diff byte stability.
- resolve_type N_TBANG: wraps primitives in a fresh Type copy so the
iserror bit doesn't taint shared globals like ty_str/ty_i32; flips
the bit in place on NAMED (already unique per alias decl).
- Type.iserror; type_named and typedecl inherit it from under.
- New check.c helpers: tagged_has_errflag, tagged_is_error_variant,
tagged_success_type. N_TRYPROP uses them to find the error subset
and verify each error variant is propagatable to the enclosing
return.
- cgen mirrors with cg_tagged_success_tag + cg_variant_is_error.
`?` compares AX against the success tag (no longer always 0) and
remaps each error variant's tag for the enclosing fn. `!` aborts
on any non-success tag.
strconv.invalid and strconv.overflow now use `!`-flagged shape
(`!i32` and `!void`) — visible signal in the API surface that they
are error types, matching Hare. The (i64 | invalid | overflow)
return shape and behavior are unchanged for callers; their match
arms still bind the same way.
Selfhost: lib/ww/parse/parse.ww recognises `!T` and emits N_TBANG.
The selfhost typechecker and cgen ignore the flag — none of the
selfhost sources use `!`, so byte-identity gates are unaffected.
The selfhost mirror catches up when there's a source using it.
Plan 9-style w-prefix on the per-arch tools, disambiguating from the
real Plan 9 6c/6a/6l in ref/plan9front/:
cmd/wwc/ → cmd/wcc/ libwwc.a → libwcc.a
cmd/6{c,a,l} → cmd/w6{c,a,l} binary names too
test/wwc/ → test/wcc/ 6 test files w/ w6 prefix
selfhost/cmd mirror in lockstep
bootstrap/amd64/{w6c,w6a,w6l} snapshot binaries (gitignored)
WW_6{C,A,L} → WW_W6{C,A,L} env-var overrides
Plan 9 source-tree refs ("Plan 9 6c shape", ref/plan9front/, etc.)
preserved. Hare-style driver, both C and ww sides:
ww test [path] discover *_test.ww in a directory module, run
each; single-file mode for `ww test foo.ww`
Module-by-name `ww build foo` resolves to foo.ww or foo/foo.ww
via search path (cwd : -I dirs : $WW_LIB)
Default-to-cwd `ww build` / `ww test` build the cwd module
Run pass-through `ww run path arg1 arg2` reaches the program
lib/os: getcwd (79) and getdents64 (217) syscalls power `.` resolution
and directory enumeration on the ww side.
Makefile: wwstage tool deps now include lib/os/os.ww (+ lib/strconv
for wwdump_ww) so lib/* edits force their rebuild instead of leaving
stale binaries — surfaced when test 995 first failed against a stale
w6c_ww built before the lib/os additions.
Test 993 byte-identical parity gate (C-side ww vs ww-side ww_ww on a
build corpus) stays green; all 19 tests pass.