#64 flowed per-decl TY_NAMED wrappers, falsifying two #63-era assumptions
surfaced in the flip review.
elemissignedc read ti.sub for element signedness without peeling TY_NAMED;
a NAMED-of-indexable would read NAMED.sub (nil) instead of the underlying's.
Add the transitive peel ahead of the .sub read, mirroring cstage idx_eff's
type_unwrap (cmd/w6c/cgen.c:790) before eff->sub (:3518-3520). Byte-id-neutral:
every aliased indexable in-tree has a u8 element (typeissigned=false either
way). Independent .sub/.under inventory confirms elemissignedc was the sole
structural reader missing a peel (castsrcprim + TK_AMP already NAMED-guarded;
typeis* handle element NAMED via .under recursion; typeeq is nominal by
design and must not peel).
Refresh the 5 #63 peel-site comments (slotsize, fieldsize, nullableptrtag,
tupleelemslot, fieldslotsize): the peel now actively fires (#64 builds NAMED)
rather than being a no-op; byte-id holds because NAMED collapses to the
alias-invariant underlying.
make test 133/133 (byte-id 990-997 green).
tinfofornode's TNAME arm collapsed aliases to their underlying tinfo; flip
it to build a per-decl TY_NAMED wrapper cached on sym.type_, so every TNAME
resolving to the same decl yields one tinfo pointer -- ptr-identity =
nominal identity. Mirrors cstage's two-phase type_named (cmd/wcc/check.c:
1900-1929, resolve_typename :60-88): create the NAMED, pre-bind sym.type_
BEFORE resolving under (self-ref cycle-break, e.g. `type node = struct
{next: *node}`), then patch under + copy size/align/slotsize off the
immediate body. CHAINS not flatten (`type a=b` gives under=NAMED(b)),
matching resolve_typename returning the inner NAMED.
aliassym factored out of resolvealias for the one-level decl lookup;
resolvealias delegates and is behaviorally identical.
Semantically INERT until typeeq consumes nominal identity (step 3, #16) --
live type equality today is the AST-keyed typeeqast, and typeeq has no live
callers. The #63 structural-walker peels + cstage-mirrored single-if peels
keep all tinfo.kind sites correct with NAMED flowing -- byte-id 990-997
unchanged (133/133, independently re-confirmed on a quiescent tree).
Phase-N prerequisite (additive, byte-id unchanged). slotsize / fieldsize /
nullableptrtag (cgenutil) and tupleelemslot / fieldslotsize (check) read
size/slot/kind off a tinfo without peeling TY_NAMED. Once Phase-N step 2
(#64) makes tinfofornode build per-decl TY_NAMED wrappers, an unpeeled
reader would misbehave (fall through to 8 / take natural size not slot /
miss NAMED-of-tagged). Prepend a transitive `for (t != nil && t.kind ==
tykind.TY_NAMED) { t = t.under; }` peel + nil re-guard at each, mirroring
cstage's `while (t->kind == TY_NAMED) t = t->under` and the recursive
typeis* predicates.
Additive no-op today: tinfofornode still collapses aliases, so no NAMED is
ever built and the loop never executes. byte-id 990-997 unchanged (133/133).
Audit (worker + reviewer, independently, across all selfhost/cmd/wcc/*.ww
+ lib/ww/*.ww): these 5 are the ONLY non-peeling structural walkers. typeis*
recurse on .under; typeeq is nominal by design (ptr-identity, the step-3
goal); typeisuntyped cannot receive a NAMED; check.ww type constructors and
localloadop read only .size/.slotsize, which typenamed copies from .under so
they stay numerically correct on a NAMED without peeling.
A.6.3 #61 prerequisite (additive, no consumer changes). The tagged-variant
machinery (taggedvariantindex / flatvariant* / cgwidentagremap / cgmatch)
is AST-keyed -- it walks N_TTAGGED.list and spread-flattens `...inner` at
read time. To migrate it onto tinfo.params (#61b/c) the chain must first
carry the flattened variant set + per-variant error mark, matching cstage's
Type.params / Type.iserror.
tinfofornode's TTAGGED arm now splices `...inner` tagged spreads into
ti.params (dealias one NAMED level, require TY_TAGGED, inline its already-
flattened variants in declaration order) -- mirror of cstage check.c:366-389.
Each variant gets an iserror flag via varianterr (TBANG / `!`-aliased).
size/align stay accounted off the surface member so ti.size is byte-identical
to before; the flatten + iserror have zero readers this commit (the lone
TY_TAGGED params reader, nullableptrtag, only fires on 2-variant nullable
unions with no spreads).
iserror rides the shared tparam struct rather than a sidecar: a cstage-mirror
divergence from harec, which carries no per-variant flag (models `!T` as a
STORAGE_ERROR type node, ref/harec/include/types.h:144, src/types.c:151-159).
Faithful port filed as #62. Spread-only flatten (cstage check.c:373 also
flattens non-spread anonymous-nested unions) is a known symmetry gap, inert
in bootstrap, tracked for #61b.
make test 133/133 (quiescent tree, byte-id 990-997 green).
pushargsrev and cgindex re-derived an index element's type via
indexvaluetnode (a base->TPTR/SLICE/ARRAY .lhs AST walk). The checker
stamps the element type on the N_INDEX node itself (indexresult,
check.ww:1710; N_INDEX is in the asserttyped gate), so read n.type_
directly at the 3 sites that only need tagged-ness + element size/sign:
cgenutil pushargsrev L178/490: pass the N_INDEX arg to istaggedtype/
slotsize (arg.type_ is the element tinfo).
cgenexpr cgindex L726: esz = n.type_.size, signed = typeissigned(n.type_).
Polarity DOWN per rule 10: cstage has no indexvaluetnode -- it reads
base->type->sub->size directly (cmd/w6c/cgen.c:2070-2071, :3518). The new
path reads element natural size (tinfo.size == primsize: rune=4, u8=1,
str=16, slice=24), matching cstage; it also retires two latent elemsizeofc
divergences (elemsizeofc returned slotsize N*8 for *[N]i64 / 2D-array
elements where cstage uses sub->size=8) -- those shapes are absent from
self-compile, so byte-id stays green.
indexvaluetnode is NOT deleted: its remaining callers at cgenexpr
L3720/3729 feed cgwidentaggedstore -> resolvetagged -> resolvetype, still
node-keyed (nil for non-N_TNAME). Deleting it waits on the resolvetagged
-> tinfo migration (#11).
make test 133/133 (byte-id 990-997 green, independently re-confirmed on
quiescent tree).
exprprimresolved's N_DOT arm derived a struct field's prim width+sign by
walking the field's declared *node (dotfieldtnode + typenodeprimresolved
alias recursion). Read the checker-stamped tinfo instead, mirroring cstage
castsrcprim N_DOT (cmd/w6c/cgen.c:323-344): the base's n.lhs.type_ must
resolve to a struct (peel NAMED->PTR->NAMED, require TY_STRUCT) before the
field counts -- excluding pseudo-fields .len/.cap/.ptr (stamped i32/*T at
check.ww:1987-2004; their base is TY_SLICE/TY_STR/TY_ARRAY, never TY_STRUCT,
so the guard yields sz=0) and tuple positionals, which stay sz=0 to preserve
995 byte-id. The field's width/sign comes from the N_DOT's own n.type_
(check.ww:2012), one NAMED peel then typeisint?size:0. Bool exclusion is
now free via typeisint(bool)=false, dropping the old streq("bool") arm.
Polarity DOWN per rule 10: cstage castsrcprim already reads the stamped
type. typenamed() has zero callers in wwstage (tinfofornode collapses all
alias depth to the body), so no tinfo carries kind TY_NAMED -- the NAMED
peels are dead/inert, output matches cstage.
Removes one dotfieldtnode caller (3->2; remaining cgenutil 1073, 1871).
typenodeprimresolved retained (3 callers: cgenutil 1459/1465, cgenexpr 431).
Part of the dotfield* consumer-migration arc.
make test 133/133 (quiescent tree, byte-id 990-997 green). Coverage:
710_cast_enum_movl rows struct_field_rt + pseudo_field_clamp + bool_to_i8.
rhstaggedabicall's N_DOT arm re-derived the field type via dotfieldtnode
(a base-ident-only structlookup walk) then asked istaggedtype. The
checker now stamps the field's resolved type on the N_DOT node itself
(check.ww struct-field arm), so read src.type_ directly. typeistagged(nil)
is false, preserving the old ft==nil bail.
Polarity DOWN per rule 10: cstage's gate was already wide. cg_widen_tagged_
store reads src->type inline, NAMED-resolved, then tests TY_TAGGED
(cmd/w6c/cgen.c:1302-1305) -- no standalone helper. Pre-trim wwstage's
gate was NARROWER (dotfieldtnode required base==N_IDENT, firing only for
`ident.field`); reading src.type_ also fires for chained-dot bases
(`a.b.c`) since the checker stamps N_DOT.type_ at any depth. This aligns
wwstage UP to cstage's existing coverage. Byte-id 990-997 green confirms
the widened shape does not occur in bootstrap sources -- byte-id-neutral.
Drops one dotfieldtnode caller; the *node-returning callers
(typenodeprimresolved, indexvaluetnode, matchscrutt) stay until their
consumers migrate to *tinfo (the closing dotfield* deletion follows once
none need *node). First clean slice of that consumer migration; same
g/h-style stamp read as #55/#56.
Known pre-existing gap (filed, not introduced here): neither stage's
chained-DOT read path has a tagged-union leaf branch, so a chained-dot
tagged ABI source would store stale DX/CX/R8. Symmetric across stages
(byte-id stays green); the retained `cgdot loads AX=tag, DX=word0, ...`
comment is accurate only for ident.field bases.
make test 133/133 (quiescent tree, byte-id 990-997 green).
Phase 1 of A.6.3i: populate the field chain in tinfofornode's TSTRUCT
and TTUPLE arms so Phase 2/J/K (#58/#59/#60) can retire dotfieldtnode,
dotinnerstructptr, dotchainresolve, and indexbaseesz off their AST-keyed
structinfo walk and onto a tinfo read. Direct analog 26724fe (#50 phase
1, A.6.3f-a) for the head/tail append-list pattern.
TSTRUCT walks n.list's N_TFIELD chain in lockstep with the existing
natural-layout offset accumulator: alloc tfield {name, type_, offset,
tnext}, link head/tail, set r.fields after the loop. Mirrors cstage
cmd/wcc/check.c:468-527. Harec cite: ref/harec/include/types.h:109-115
struct_field and ref/harec/src/type_store.c:314-347 struct_init_from_atype.
Anonymous-embed promotion not populated here (#13 per the cstage cite
at check.ww:1263).
TTUPLE adds a new ttupleelem struct {type_, offset, tnext} on a new
tinfo.tupleelems slot, distinct from .fields per Rob's call: harec
splits struct_field vs type_tuple at types.h:109-115 vs :122-126
because tuples are positional/anonymous and struct members are named,
and the name="" idiom #50 reused for tagged-variants-on-tparam would
conflate two semantic axes. Diverges from cstage cmd/wcc/check.c:329-345
which stores tuple positionals on t->params (Tparam, no offset, consumer
recomputes by walking at cgen.c:5723-5750); storing the offset matches
the A.6 stamp-once-read-many arc Phase 2/J/K consume. Offset is raw-sum
(no per-element padding) matching cstage cgen.c:5723-5750, distinct
from harec's add_padding at type_store.c:561.
Purely additive: r.fields and r.tupleelems have zero readers today.
Phase 2/J/K consume. make test 133/133 (worker port); test-unit 124/124
post comment-only review trim.
nodeisslice + nodeisstr each walked base->struct->field on N_DOT,
gated on `base.kind == nkind.N_IDENT` (with dotinnerstructptr and
dotchainresolve fallbacks for chained / value-struct cases) --
duplicating cstage at the AST level while silently dropping
multi-level chains rooted at non-IDENT/DOT bases (e.g. `t.1.field`
for a tuple positional). After A.6.2 the checker stamps n.type_ on
every N_DOT (check.ww:1947-1996 pseudo-field, struct-field, and
tuple-positional arms), and A.6.3b landed typeisstr / typeisslice.
Both arms collapse to one tinfo read.
Polarity DOWN per rule 10: cstage was already aligned. cgen.c:168-170
node_isstr = type_isstr(n->type); cgen.c:182-184 node_isslice =
type_isslice(n->type). Wwstage was the laggard; this brings wwstage
to cstage's leaner shape, mirroring A.6.3g (8da1414).
Silent-false coverage gain: multi-level chains and tuple-positional
bases (`t.1.field` for a str element) now resolve via the checker
stamp instead of returning false. The "Not covered: tuple-positional"
bullet in nodeisstr's docstring is removed accordingly. dotchainresolve
itself is untouched -- still used by cgenexpr (cgdot read-path, &-of
address-of, structlit BP write-back). Byte-identity (994/995) is the
gate.
make test 133/133 ok. Net cgenutil.ww -110.
nodeisunsigned + exprfloatkind each manually walked base->struct->field
on N_DOT, gated on `base.kind == nkind.N_IDENT` -- duplicating cstage's
behaviour at the AST level while silently dropping the nested-N_DOT case
(`a.b.c` returned the conservative default). After A.6.2 the checker
stamps n.type_ on every N_DOT expression (check.ww:1973 struct-field
arm of exprtype), and A.6.3a/b landed the tinfo helpers (typeisunsigned,
isfloattype/isf32type) that the inner reads already use. Both arms
collapse to one tinfo read.
Polarity DOWN per rule 10: cstage was already aligned. cmd/w6c/cgen.c:
2330-2331 reads type_isunsigned(n->lhs->type) directly; cgen.c:152-156
node_isfloat = cg_isfloat(n->type); cgen.c:195-199 node_isf32 =
type_isf32(n->type). No exprfloatkind-equivalent walker exists in
cstage -- it is pre-A.6.2 wwstage scaffolding. Wwstage now reads the
same shape as cstage on N_DOT.
Nested N_DOT (a.b.c) now resolves to the field type instead of returning
the conservative default. Byte-identity (994/995) confirms the codegen
matches cstage on the test set -- cstage was already getting nested-dot
right via checker-stamped n->type, wwstage was the laggard.
make test 133/133 ok. Net cgenutil.ww -41 / +3.
Phase 2 of A.6.3f, closing A.6.3 entirely (a/b/c/d/e/f-a/f-b all
landed). With #50 phase 1 (26724fe) populating TY_TAGGED.params,
nullableptrtag retires its AST-keyed predecessor: walk ti.params,
strip TY_NAMED via .under on each variant, return idx of first
TY_PTR.
Mirrors cstage cmd/w6c/cgen.c:404-416 line-for-line. Source-order
semantics preserved by phase 1's append-tail head/tail
construction (head = first n.list variant). For a `(*T | void)`,
*T-first → returns 0; void-first → walks past void (vt.kind==
TY_VOID, no match), *T at idx 1 → returns 1.
Two paths the new tinfo-keyed body handles that the AST walk
missed (cstage parity, dead-in-bootstrap today, parallel to #49
TY_TUPLE / #51b typed-float corrections):
- Aliased *T variant: `type ip = *int; let x: (ip | void);` —
cstage cgen.c:415 strips TY_NAMED via .under; new wwstage
body mirrors. Bootstrap has zero aliased-ptr-variant
callsites today (`grep "^type [a-z]+ = \*"` yields only an
out-of-union test/uses.ww case).
- void-first ordering: cstage walks past TY_VOID and finds *T
at idx 1. Same in wwstage. No test/wcc fixture exercises
void-first today.
Outer-type TY_NAMED strip (cstage cgen.c:409 `if t->kind==TY_NAMED
t = t->under`) intentionally skipped: tinfofornode for N_TNAME
already resolves the alias and returns the underlying tinfo
unwrapped — `typenamed` is declared at lib/ww/typ.ww:238 but has
zero producers in selfhost today. The strip would be a no-op
under current invariants. Tracked as part of #13 (TTAGGED
normalization parity) for when wwstage starts producing
TY_NAMED.
Net +30 LOC across cgenutil.ww + two .combined.ww bundler regens
(new function body is ~14 lines vs ~4; new WHY comment shorter
than the pre-graduation note's 7).
Byte-identity (994/995) is the gate; full make test green at
133/133 confirms.
Phase 1 of Rob's two-phase A.6.3f pattern: populate the variant
chain in `tinfofornode`'s TTAGGED arm so phase 2 (#50b) can retire
`nullable_ptr_tag`'s AST-keyed walk in cgenutil onto a tinfo read.
Per b8e5a92 (A.6.3b) commit body: "nullableptrtag stays AST-keyed
for now — tinfofornode doesn't populate TY_TAGGED.params … so the
tinfo equivalent of cstage cgen.c:405 nullable_ptr_tag can't read
params today."
Restructure the TTAGGED arm into a single pre-pass: walk `n.list`,
resolve each variant via tinfofornode, alloc `tparam{name="",
type_=vt, tnext=nil}`, link head/tail, accumulate maxsz + al
inline. Set `r.params = head` after the loop. AST-level nullable
fold runs after, before size assignment — kept AST-keyed (not
ported to cstage's tinfo-level `kind==TY_VOID && !iserror` check)
because wwstage tinfo carries no `iserror` field; that's an honest
data-shape divergence (filed in passing as part of #13's TTAGGED
normalization arc).
Mirrors cstage cmd/wcc/check.c:347-435 — same head/tail append-
list construction, same Tparam reuse across struct-fields /
tuple-fields / fn-params / tagged-variants (sea-of-stars per
rule 12 — one record, four consumers, no per-kind variant of
the param node). Diverges from harec's array+id-sort at
ref/harec/include/types.h:128-132 / ref/harec/src/type_store.c:
431-432; rule 10 anchors wwstage byte-id to cstage, not harec.
Purely additive: ti.params has zero readers on TY_TAGGED today
(typeeq walks params only for TY_FN/TY_TUPLE; cgenutil's
nullableptrtag is still AST-keyed; #50b will consume). Byte-
identity (994/995) unchanged at 133/133 — pre-impl risk audit
by ken-thompson came back zero, confirmed by full make test.
A latent divergence wwstage doesn't cover (never-drop /
...spread / dedup / single-variant collapse — see cstage type
set normalization at check.c:393-432) is pre-existing and out
of #50's scope; tracked as #13. Phase 2 nullableptrtag is a
linear walk for the first TY_PTR variant in a 2-variant
nullable, indifferent to ordering and dedup, so it does not
need #13 closed first.
Net +12 LOC per file across check.ww + two .combined.ww
bundler regens.
fieldsize selected the slot-padded byte width of a struct field's
type-AST via the same TBANG/TNAME/TPTR/TSLICE/TARRAY/TTAGGED walker
shape slotsize used pre-#48 — with TNAME branching into
structlookup, enumlookup, and aliaslookup to follow the AST chain
back to a primitive size or struct totsize. After A.6.2 stamping
+ #48's slotsize collapse, the same data is reachable through
the populated tinfo:
TY_STRUCT / TY_ARRAY → ti.slotsize
TY_TAGGED / TY_SLICE / TY_STR → ti.size
TY_PTR / TY_FN / TY_CHAN → 8
primitives + TY_ENUM + TY_TUPLE → ti.size (catch-all > 0)
fallback → 8
cstage SSoT is `f->type->size` at cmd/w6c/cgen.c:1386, :1656, :2515,
:2535 — wwstage routes through ti.slotsize for composites (the
#48 verdict centralizes the slot-pad rule on tinfo) and through
ti.size where natural width and in-struct width coincide.
Two latent behavior fixes ride alongside the collapse, both
cstage-parity and dead-in-bootstrap (995 byte-id is the regression
gate, currently green at 133/133):
- TY_TUPLE-typed struct field: old walker had no TTUPLE arm and
fell through to 8; cstage `f->type->size` reads the natural
sum (e.g., 24 for `(i64, str)`). New code reads ti.size,
matching cstage.
- !T-typed struct field: old walker had no TBANG arm and fell
through to 8; cstage iserror-passthrough returns the inner
type's size. tinfofornode strips N_TBANG (check.ww:1154-1161)
so the new dispatch sees the inner ti directly.
No fixtures in selfhost exercise either case today; both fixes
are pre-correct for future code that does.
Body went from ~47 LOC to ~16 LOC. `c: *cgen` retained unused for
callsite stability (slotsize / localloadop precedent, a828c03 /
68219a1). Two callsites in cgenutil.ww:1870 + cgenexpr.ww:3594
untouched.
Dot-chain helpers (dotinnerstructptr, dotfieldtnode,
dotchainresolve) are split off as #49b — they walk a cgen-side
`structinfo` table keyed by field name + offset, and tinfo.fields
is not yet populated by check.ww's TSTRUCT/TTUPLE arms. Parallel
structure to #50's TY_TAGGED.params gap; treat as a separate
populate-then-port pair.
Net -48 LOC across cgenutil.ww + two .combined.ww bundler regens.
Full make test green at 133/133.
slotsize selected the slot-padded width for a frame slot via an AST
walker over TBANG / TPTR / TFN / TCHAN / TSLICE / TTUPLE / TTAGGED /
TNAME / TARRAY / TSTRUCT — duplicating the size + slot-pad math
tinfofornode already runs at check time. With A.6.2 stamping every
N_T* kind's n.type_, plus #61 A.5 splitting tinfo.slotsize from
tinfo.size, the dispatch collapses to a kind switch over the
populated tinfo:
- TY_VOID → 0
- TY_PTR/SLICE/CHAN/FN/STR/TAGGED → ti.size (size == slotsize for
these kinds)
- TY_STRUCT/TUPLE/ARRAY → ti.slotsize (slot-padded by check.ww's
TSTRUCT/TTUPLE/TARRAY arms with the same field-pad / stride
rules cgenutil's registerstruct uses)
- primitives → catch-all 8 (pad-to-8 lives at the read site, not
in ti.slotsize, so [N]i32 stride stays 4)
Ww-to-Hare divergence (deliberate, team consensus): Hare's struct
type carries only `size` (ref/harec/include/types.h:134-137); QBE
handles slot padding downstream. ww emits Plan 9 amd64 asm directly,
so slot-padding is part of the ww calling convention and belongs on
the type table. Cstage scatters pad-to-8 inline at ~30+ localoff
sites + the `(su->kind == TY_TAGGED) ? su->size : 16` tagged-spill
pattern at cmd/w6c/cgen.c:4850 / :5193 — that scattering would be a
rule-13 (no hardcoded size literals) violation in ww. Centralizing
on tinfo.slotsize IS the rule-13-compliant shape; #61 A.5 put it
there, and #48 just consumes it. Rule 9 is lib/-scoped; tinfo is
compiler-internal (already diverges via node.type_ as a checker
invariant Hare/harec lacks). Rule 10 covers inference power, not
data shape — byte-identity (994/995) holds at 133/133.
Body went from ~150 LOC AST walk to ~15 LOC tinfo dispatch. Two
nil guards (typn == nil, ti == nil) fall through to 8 — same outcome
as cstage's `(t && t->size > 0) ? sz : 8` defensive shape.
`c: *cgen` retained unused for callsite stability (localloadop
precedent, 68219a1).
Net -453 LOC across cgenutil.ww + two .combined.ww bundler regens.
Follow-up filed: grow cstage Type.slot_size symmetrically and
sizelint-ok-annotate the surviving inline 8s until ported.
localloadop selected MOVBQSX/MOVBQZX/MOVSWQ/MOVL/MOVQ for scalar
local/let loads via an AST walk down TBANG/TENUM/TNAME-alias chains,
re-consulting aliaslookup and ending in fieldsize + fieldissignedc.
Three precursors retire the walk: #53 (e3237d1, scopelookupprefer
fixes flat-alias collision in resolvealias), #51 (1ce0f63, INTLIT
tsuffix stamp), #52 (747279c, typenameisunsigned collapse onto
typeisunsigned). Plus the A.6.2 type-AST stamp invariant at
check.ww L426-436.
Body collapses to two nil guards + ti.size + size cascade +
typeissigned(ti). cstage cmd/w6c/cgen.c:357-362 is the SSoT;
wwstage now reads tinfo.size directly the same way cstage reads
t->size, with TBANG / TENUM / TNAME-alias chains pre-folded by
tinfofornode (check.ww:1102-1153 TNAME, 1154-1161 TBANG,
1196-1208 TENUM).
Defensive nil branches mirror cstage's `(t && t->size > 0) ? sz : 8`
fall-through: when type info is missing, sz fails the != 1/2/4
discriminator and the helper returns MOVQ. Both nil paths are
reachable: cgenexpr.ww callers (4 sites: 643, 664, 5380, 5471)
populate the *node via a search loop that may exit with nil.
Signature unchanged — `c: *cgen` is retained unused for callsite
stability, mirroring A.6.3a fieldissignedc(c, t) which followed the
same precedent. 5 callsites in cgenexpr.ww untouched (body-only
collapse).
Net -33 LOC across cgenutil.ww + two .combined.ww bundler regens.
Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
A.6.3a (#45) deferred typenameisunsigned because its two callers —
typenodeprimresolved and exprprimresolved — consumed a raw str:
TNAME.str / INTLIT.tsuffix. #51 (1ce0f63) stamped tsuffix-typed
N_INTLIT.type_ via tinfofornode; together with check.ww L426-436
type-AST stamping, both call sites now read a stamped n.type_
instead of a raw name.
typenodeprimresolved L1583 swaps typenameisunsigned(nm) for
typeisunsigned(cur.type_: *tinfo). cur is the walked N_TNAME; its
type_ is stamped at check time.
exprprimresolved L1620 swaps typenameisunsigned(s) for typeisunsigned
(n.type_: *tinfo). n is the N_INTLIT whose tsuffix is s; type_ is
stamped by the #51 arm. primsize(s) > 0 IS the tsuffix-resolves-to-
builtin gate, so the stamp is guaranteed at the read site.
typenameisunsigned (cgenutil.ww L850-866) deleted: -17 LOC of body
+ WHY block. Net change is three files (cgenutil.ww + two
.combined.ww bundler regens, same -17 each).
Mirrors cstage cmd/wcc/type.c:178 type_isunsigned plus cmd/w6c/cgen.c
castsrcprim routing INTLIT.tsuffix through n->type, then
type_isunsigned. Aligned, not richer.
Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
cstage cmd/wcc/check.c:702 cexpr N_FLOATLIT arm uses lookup_builtin
(n->tsuffix) with fall-through to ty_untyped_float. wwstage's
exprtype N_FLOATLIT arm at check.ww L1582 was unconditional
untyped_float — the symmetric-stage gap flagged at the tail of #51.
New arm: when e.tsuffix is non-empty, mktname+tinfofornode resolves
the builtin and stamps e.type_; on nil tinfo fall through to the
existing untyped_float path. Same shape as the #51 INTLIT arm one
block up.
Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
cstage cmd/wcc/check.c:694 cexpr N_INTLIT arm uses lookup_builtin
(n->tsuffix) with fall-through to ty_untyped_int. wwstage's exprtype
N_INTLIT arm at check.ww L1565 was unconditional untyped_int — a
symmetric-stage gap that left the last raw-str-typed reads of
TNAME.str / INTLIT.tsuffix alive in typenodeprimresolved /
exprprimresolved (the deferrals named at the end of A.6.3a, #45).
New arm: when e.tsuffix is non-empty, mktname+tinfofornode resolves
the builtin and stamps e.type_; on nil tinfo fall through to the
existing untyped_int path. Mirrors harec ref/harec/src/check.c
check_expr_literal routing typed ICONST through builtin_type_for_storage.
N_FLOATLIT at check.ww:1582 carries the same gap (cstage check.c:702
does the same lookup_builtin/untyped_float fall-through); filed as
#51b for a separate bisect-clean follow-up.
This is the additive stamp half; #52 collapses the two remaining
typenameisunsigned callers onto tinfo reads of the stamped type_.
Byte-identity (994/995) is the behavior gate; full make test green
at 133/133 confirms.
The bare-leaf TNAME lookup in resolvealias used flat scopelookup,
which bucket-walks all matching names and returns whichever entry
hashed in first. Two modules each declaring `type invalid = ...`
collided in the same flat scope: utf8.invalid (`!void`) and
strconv.invalid (`!i32`) resolved to whichever registered first.
That drove a localloadop divergence at the 994/995 byte-id gates —
MOVSXD vs MOVQ — depending on which alias the checker happened
to pick for a given site.
Switch to scopelookupprefer(c.cur, c.curmod, nm), mirroring cstage
cmd/wcc/check.c:66 (scope_lookup_prefer at sym.c:103): when the
current module matches the bucket entry's b.mod, prefer it; else
fall back to first-found. The SK_USE→scopelookuptype fallback for
the #61 A.5 bare-TNAME-vs-imported-module collision case is
unchanged.
Six remaining bare-leaf scopelookup sites in this file (exprtype
N_IDENT, N_DOT-callee leaf, varianterr, scruttype, exprtypeoftry
N_IDENT + N_CALL, walker N_IDENT) are punted to #55 — this commit
fixes the path the reproducer surfaced and leaves the rest behind
an explicit follow-up so the byte-id corpus stays the test for
each conversion.
The node-keyed kind helpers (typeis8byteprimitive, isstrtype/raw,
isslicetype/raw, istaggedtype/raw, isfloattype, isf32type/raw,
isf64typeraw, isnullabletype) each re-walked TNAME aliases via
aliaslookup and peeled TBANG by hand — duplicating cstage's single-
peel kind predicates at the AST level. After A.6.2 every type-AST
kind these read is tinfo-stamped at check.ww L426-436, and
tinfofornode collapses N_TBANG (check.ww:1145-1152) and the TY_NAMED
chain, so each predicate folds to one tinfo read.
Six new tinfo helpers in lib/ww/typ.ww mirror their cstage SSoT
verbatim:
typeisstr — cstage cgen.c:159 `type_isstr` (TY_STR / TY_UNTYPED_STR)
typeisslice — cstage cgen.c:174 `type_isslice`
typeistagged — cstage cgen.c:516 `type_istagged`
typeisf32 — cstage cgen.c:188 `type_isf32`
typeisnullable — cstage cgen.c:396 `type_isnullable` (reads tinfo.nullable
stamped at check.ww:1309-1318)
typeis8byteprim — cstage cgen.c N_LET sz==8 ladder (slot-pad set)
Rule 9 carve-out per the A.6.3a precedent: each helper has a named
cstage counterpart; the wwstage shape mirrors it directly. The five
dead AST-walking variants (isstrtyperaw, isslicetyperaw,
istaggedtyperaw, isf32typeraw, isf64typeraw) are deleted; the five
remaining callsites (cgenstmt cglet / cgmlet str-routing, cgenexpr
cgdot tuple-field) graduate to the alias-aware isstrtype(c, t).
nullableptrtag stays AST-keyed for now — tinfofornode doesn't
populate TY_TAGGED.params (check.ww:1287-1337 sets size / align /
nullable but not the variant chain), so the tinfo equivalent of
cstage cgen.c:405 `nullable_ptr_tag` can't read params today. WHY
comment at the site cites #50 / A.6.3f as the graduation point,
alongside the variant-index work and the tparam-population glue.
Byte-identity (994/995) is the behavior gate; full `make test` green
at 133/133 confirms.
The node-keyed signedness helpers (typenodeisunsigned,
typenodeisunsignedc, elemissigned, elemissignedc, fieldissignedc) each
re-walked TBANG / TENUM / TNAME chains and re-consulted alias / enum
registries — duplicating cstage's type_isunsigned (cmd/wcc/type.c:178)
and fld_issigned (cmd/w6c/cgen.c:240) at the AST level. After A.6.2
every type-AST kind we read here is tinfo-stamped at check.ww L426-436,
so the predicates collapse to a single tinfo read.
Two new arms close the wwstage divergence from cstage: typeisunsigned
gains TY_RUNE and TY_ENUM (recurse on .sub), matching type.c:178
verbatim. typeissigned is added as the cgen-facing predicate per
fld_issigned semantics (TY_BOOL excluded for sub-word storage —
0/1 → MOVZBQ — so it's not just !typeisunsigned). Rule 9 carve-out:
the helper exists in cstage; harec keeps the same pair.
elemissigned was fully dead (no callers); deleted. typenameissigned
was internal-only and dead post-collapse; deleted. typenameisunsigned
survives — two call sites (typenodeprimresolved, exprprimresolved)
hold only a raw `str` (TNAME.str / INTLIT.tsuffix). paramissigned in
cgenstmt.ww unchanged. Both deferrals close in A.6.3c (#47).
Byte-identity (994/995) is the behavior gate for the alias/enum
sites — full `make test` green at 133/133 confirms.
Final closer for the A.6.2 sequence. Mirror of harec's
`assert(expr->result)` at ref/harec/src/check.c:3810: every value-
producing nkind dispatched by resolvewalk (L475-488 + N_DOT at L391)
reaches a stamping arm in exprtype that sets e.type_ before
returning. asserttyped is the post-checker invariant gate; it walks
checkfile's decls in pass 3 (same curmod context exprtype saw in
pass 2) and writes a one-line stderr diagnostic for any dispatched
node whose type_ remained nil.
Three residual gates encode bails that aren't true gaps until #19
(Drew's δ: dedicated AST kinds for alloc/size/etc.) retires the
seeded-SK_FN-with-nil-decl + SK_USE-as-value shapes:
1. N_IDENT resolving to SK_USE (module ref like `os` in `os.write`)
2. N_IDENT whose sym.decl == nil (pseudo-builtin callee — len,
append, free, alloc, size, align, offset seeded at L86-98)
3. N_IDENT in LHS-of-N_DOT syntactic position (member-access
lookup target, not value-producing) — tracked via `indot` param
ZERO fires across all 5 selfhost combined.ww corpora (wcc, w6c, w6a,
w6l, wwdump). asserttyped IS the regression catch — future commits
that drop a type_ stamp will fire it during the 990_selfhost probes;
no standalone table-driven test is bundled.
Accreted folds:
- 5-lite-a (#33): dispatcher-invariant docstring at exprtype L1535.
- 5-lite-b (#34): WHY comments at 9 helper bail sites
(unifyarith/binoptype/unoptype/indexresult) classifying each as
unreachable-for-valid-input, propagation-from-callee, or
invalid-input (cstage errors at the matching cite). Cites
ref/harec/src/types.c type_promote on the function-doc updates.
- A.6.2.1c (#24): three propagation pointers re-cite the
inherent-IDENT bail at exprtype N_IDENT arm L1596-1599.
- unoptype TK_STAR dead `if (u == nil) { return nil; }` removed —
resolvealias(unwrapbang(non-nil)) is non-nil by parser invariant
(parsetype L148 always sets N_TBANG.lhs; resolvealias L514-569
every exit returns non-nil for non-nil input).
lib/ww/ast.ww: nkname becomes export so asserttyped's diagnostic can
format the offending node's kind without duplicating the table.
Closes#4 (A.6.2 umbrella) and #15 (A.6.2.1e).
checkletassign previously early-returned on `n.lhs == nil`, so an
inferred binding (`let r = expr;`) left decl.lhs unset; exprtype's
N_IDENT branch at check.ww:1550 reads `s.decl.lhs` and so the use
sites lost the inferred type.
Mirrors cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt)
t = type_default(initt);` and ref/harec/src/check.c:1422
check_expr_binding. cstage carries the let type on Sym.type;
wwstage carries it on decl.lhs — same observable result, byte-id
(rule 10) intact. Defaulting (untyped_int → i32) stays at use
sites in exprtype, not the binding site. Mutation layer matches
#41's installparams parser-decl-mutation precedent at check.ww:2747.
Table-driven test deferred to #44 per the speed-first cadence;
existing 990–997 byte-id corpus implicitly exercises inferred lets.
Context for the capture sweep: #36.
installparams normalises `T...` p.lhs to []T so N_IDENT lookups
(s.decl.lhs) and cgen's variadic-slot synthesis see the effective
slice — mirrors cstage check.c:455 `tp->type = type_slice(c->a, pt)`
and harec check_func_type. cgendecl/cgenexpr drop the on-the-fly
slicewrap and consume p.lhs directly; cgenexpr cgcall peels the
N_TSLICE wrap when reading the element predicates / esz (Ken's
gate: cstage cgen.c:4352 `vsu->kind == TY_SLICE`), and passes
`velem` — not the wrap — into cgwidentaggedstore (cstage cgen.c
:4382). Closes the 22 N_DOT + 9 N_INDEX fires from #36 with the
cascade absorbed by e662156 (#39 arm-6 recursion). Class B "case:
not a variant of scrutinee" did NOT fire post-#39, so Commit 2
(#40 tagged_select_subtype) was not needed.
The concrete→tagged arm walked variants with typeeqast-only, rejecting
widenings that aren't strict surface-eq: NAMED-aliased variants, nested
tagged inside a variant, and concrete → variant after the wrap-induced
exprtype reshape that Commit 3 (#41, variadic wrap re-land) introduces.
Replace the walk with a recursive isassignable call per variant, mirroring
harec tagged_select_subtype (ref/harec/src/types.c:702-739, recursive
type_is_assignable at :718; invoked from the TAGGED arm at :1110-1112)
and the byte-id cstage precedent at cmd/wcc/type.c:298-299. The recursive
call's leading typeeqast (check.ww:2257) preserves the #55 surface-nominal
fast path; the #57 bare-vs-qualified TNAME residual is unchanged.
No new test: the new widen path is dormant pre-Commit-3; existing
tagged-union tests cover the surface-eq fast path. Commit 3's fmttest
exercise will exercise the recursive widen for free.
Closes 39 cascade errors that fire once #41 lands.
Three Hare pseudo-builtins (len/append/free) were resolving via the
generic N_CALL path and leaving e.type_ nil, blocking #15's post-
checker assertion. Add inline intercepts in exprtype that stamp e.type_
to mktname("i32")/mktname("void") and return the same tnode, matching
the cstage shape at cmd/wcc/check.c:896-1011 (rule 10 stage byte-id).
No shadow guard: cstage's len/append/free intercepts have none either,
and L85-88 seeds the names into c.top so a same-module decl dup-
silences. No arg-walk: resolvewalk descends children before dispatching
the parent N_CALL (check.ww:404-417), so args are stamped before the
intercept fires.
insert/delete deferred — not implemented in either stage. The cstage
divergence from harec (len → i32 not size; append → void not tagged)
pre-dates this task; #19 (dedicated AST kinds) is the Hare-faithful
path. Behavior is transitively covered by the 990-997 byte-id corpus
(append used in progs 16, 19, 20); the stamp side gates on #15.
check.ww's N_DOT enum-fold (L1724-1810) walks the enum body to
resolve each `EnumT.MEMBER` access; the pre-#22 walker only accepted
N_INTLIT for a member's lhs and bailed on every richer shape via
`return nil`. Wwstage compensated at codegen time through cgen.ww's
enumevalmember (cgen.ww:158-227), so program semantics held; the
gap was visible only in check.ww's e.type_ stamp coverage, which
A.6.2.1e's post-checker assertion will land on.
Lift the literal-only branch into an `enumvalfold(body, until, e,
*u64) bool` helper alongside foldtointlit. The accepted set mirrors
cstage cmd/wcc/check.c:185-208 (fold_int_literal) + :210-284
(eval_enum_value) and harec's enum-resolve constexpr eval at
ref/harec/src/check.c:4419-4434: literal leaves
(INTLIT/RUNELIT/TRUE/FALSE/NIL), unary +/-/~, binary +/-/*//%
& | ^ << >>, and N_IDENT sibling backref bounded by `until` per
harec's lnext forward-only-ref discipline
(ref/harec/src/check.c:4436-4438). Both N_DOT call sites (inner
`EnumT.MEMBER`, outer `pkg.EnumT.MEMBER` via base-resolve) delegate
non-literal lhs to enumvalfold instead of bailing.
Closes#7. Lands on the A.6.2.1a slot per PLAN.md / Drew's 5-lite
plan; subsequent A.6.2.1b-d retire the remaining bail paths before
A.6.2.1e enables the assertion.
Add test/wcc/759_check_enum_fold.c — table-driven, modelled on
631_def_neg_global.c. 17 rows cover each new shape (INTLIT,
RUNELIT, sibling backref, unary +/-/~, all ten binops, chained
backref). Exit-code rows pin per-shape fold correctness through
both stages (cgen reads the mutated N_INTLIT, so a wrong fold
leaks into the constant); asm-byte-id rows pin the symmetric-emit
contract between cstage's eval_enum_value and wwstage's
enumvalfold.
`make sizelint` clean. `make test` green 133/133 (132 pre +
new 759).
`@symbol("rt_abort")` and similar attr arg literals (N_STRLIT inside
N_ATTR.list) never reach the post-order exprtype dispatch because the
top-level dispatch in checkfile pass-2 only enters per-decl via d.lhs
and d.body — d.attr was an oversight. Add a one-line resolvewalk
descent before the kind dispatch, mirroring resolvewalk L405 which
already descends n.attr on inner nodes.
Discovered while landing the A.6.2.1 assertion: the unstamped
N_STRLIT inside `@symbol("...")` would trip the assertion on every
selfhost source (lib/os pulls these in transitively). Fix lands as
its own prep commit so the next session can start clean on the
A.6.2.1 main work (bail-discipline refactor per Drew's 5-lite, then
δ+γ stamp completion).
A.6.2 status: 0a-g landed; 0b-pre N_TPARAM landed; assertion enable
(A.6.2.1e) deferred behind 5-lite + per-bail closure work — see
PLAN.md and tasks #20-#25.
`make sizelint` clean. `make test-unit` green; full `make test`
batched per option B (next session).
Port cstage's match_yield_type walker (cmd/wcc/check.c:110-135)
as `matchyieldtype` (Plan-9-cased) — recursive arm-body walker
that finds the first reachable yield's operand type, descends
N_BLOCK / N_IF / N_FOR / N_FORRANGE, and stops at nested N_MATCH
(each match opens its own yield scope). Sole structural
divergence from cstage: where cstage reads `body->lhs->type`,
wwstage calls `exprtype(c, body.lhs, nil)` — wwstage's AST is
untyped at parse time and the type lives in the tinfocache;
exprtype is the canonical reader (tinfocache-idempotent per
L467).
Port cstage's N_MATCH match-as-expression stamp (check.c:1316-
1330) into a new exprtype arm. Walks the first non-nil arm yield
via matchyieldtype, defaults to void if no arm yields. Closes
the consumer half of the match-as-expression contract that
A.6.2.0f opened on the producer side (N_YIELD).
One documented divergence from cstage (lenient, intentional):
the arm-yield-unification check (cstage L1322-1327) is skipped.
That's a checker-correctness concern; this arm only stamps.
Cstage's `match_yield_type` IS a helper there too — porting it
is structural fidelity per rule 10, not a new helper invention
under rule 9.
β scope per Drew (2026-05-21): α (stamp void unconditionally)
would bury a latent miscompile that A.6.2.1 assertion can't
catch (it sees nil, not wrong). Hare's `match_expr` AST has no
type field (ref/hare/hare/ast/expr.ha:341-348); harec/cstage
unify arms at check time — same architecture wwstage mirrors.
A.6.2 step 7 of 8 (γ order). The assertion closer (#15) is
next, which lands the invariant on a green tree.
`make test-unit` green; full `make test` batched per option B.
Documented richer-than-cstage divergence: cstage cmd/wcc/check.c
:1708 walks N_YIELD's lhs but does NOT stamp `n->type` — yield is
statement-shaped there. Wwstage's A.6.2 invariant requires every
post-dispatch kind have `type_` set, so this commit draws in the
leaner side. Yield's value type is the operand's type per Hare's
unified stmt/expr AST (ref/hare/hare/ast/expr.ha:449-461 —
`yield_expr` is on L459 inside the `expr` sum). Bare `yield;`
(no operand) stamps void.
Same nil-tolerant pass-through template as A.6.2.0e N_SPREAD,
plus the bare-yield void variant.
A.6.2 step 6 of 8 (γ order). N_MATCH-as-expression next, then
the assertion closer.
`make test-unit` green; full `make test` batched per option B.
Port head-only of cstage cmd/wcc/check.c:1212-1213 to exprtype's
new N_SPREAD arm. The spread expression `xs...` carries the
operand's type — pass-through stamp, no structural synthesis.
A.6.2 step 5 of 8 (γ order). The smallest stamp arm in the
series; mirrors cstage's one-liner directly.
`make test-unit` green; full `make test` batched per option B.
Port head-only of cstage cmd/wcc/check.c:1230-1236 to exprtype's
new N_RECV arm. Peel alias on the channel base; `chan T` → T.
N_TCHAN element lives in .lhs (cstage ww.h:279, ww typeeqast
L585).
One documented divergence: cstage L1234 errors on a non-chan
base; wwstage returns nil under the lenient-on-miss policy
(scruttype L656 / A.6.1.5b N_DOT precedent).
A.6.2 step 4 of 8 (γ order). Same shape as A.6.2.0a N_SLICE
scaffold — precedent-consistent.
`make test-unit` green; full `make test` batched per option B.
N_ALLOC is a vestigial nkind enum entry. Defined in cstage
cmd/wcc/ww.h:243 (comment: "lhs=expr, rhs=size-or-null") and
mirrored in lib/ww/ast.ww:47, but no parser produces it — the
alloc-call syntax routes via N_CALL with callee=alloc (check.ww
:1479). Cstage cexpr has no case N_ALLOC; wwstage's listing it
in the post-dispatch kind set at check.ww:482 was speculative.
Remove the disjunct. Enum entry stays — sync with ww.h is the
gate, not stamp-coverage. Broader dead-kind audit tracked as
task #18.
A.6.2 step 3 of 8 (γ order).
`make test-unit` green; full `make test` batched per option B.
Port head-only of cstage cmd/wcc/check.c:1437-1451 to exprtype's
new N_TUPLE arm. The arm walks e.list, types each element via
recursive exprtype, and assembles an N_TTUPLE whose .list chains
N_TPARAM wrappers (one per element) so shared element-type ASTs
(sym.decl.lhs, struct field's .lhs, another tuple's element) keep
their own .next untouched.
Foundation: A.6.2.0b-pre (805c841) introduced N_TPARAM as the
chain wrapper for N_TTUPLE.list, mirroring cstage's Tparam at the
AST layer (cstage keeps it at the Type layer; wwstage has no
separate type layer). This commit is the first checker-side
synthesizer to use it.
One documented divergence: empty list returns nil under the
lenient policy (cstage would synthesize empty TY_TUPLE; ww grammar
requires >= 2 elements per parse/expr.ww:117, so empty is
unreachable either way). Same shape as scruttype L656 / A.6.1.5b
N_DOT lenient-on-miss precedent.
α scope per Drew (no hint plumbing, no field-level walks, no
unify check). Hint param stays unused.
A.6.2 step 2 of 8 (γ order). N_ALLOC next (most likely a
parser-vestigial kind — A.6.2.0c worker audits first).
`make test-unit` green; full `make test` deferred to the batched
gate after the A.6.2.1 assertion commit (option B per user).
A.6.2.0b worker hit a real shared-`.next`-aliasing bug and stopped
per rule 7. Wwstage's N_TTUPLE chained element type ASTs via the
nodes' own `.next` field. `exprtype` routinely returns shared
nodes (sym.decl.lhs, struct field's `.lhs`, another N_TTUPLE's
`.list` element). Naive chain construction in the checker
corrupts source ASTs.
Introduce N_TPARAM = 67 as a chain wrapper for N_TTUPLE.list:
- `.lhs` holds the (possibly-shared) element type AST.
- `.next` chains within the parent N_TTUPLE.
- Other fields unused; never appears outside N_TTUPLE.list.
Mirrors cstage's Tparam at cmd/wcc/check.c:1437-1451. Cstage
keeps it at the Type layer; wwstage has no separate type layer
for tuple chains so the wrapper sits at the AST. Hare's design
intent at ref/hare/hare/ast/type.ha:117 uses `[]*_type` slice-of-
pointer — same principle, slice-flavored.
Migrations:
- lib/ww/ast.ww: kind + nkname + pr() unwrap (transparent for
the 990 -a astprint byte-diff).
- lib/ww/parse/parse.ww: parsetype N_TTUPLE construction wraps
each element in N_TPARAM (sole construction site).
- selfhost/cmd/wcc/check.ww: 4 readers (astalign, astsize,
tinfofornode TY_TUPLE, exprtype N_DOT-tuple-positional). The
last change retires the latent A.6.1.5b shared-`p` return.
- selfhost/cmd/wcc/cgenutil.ww: slotsize TY_TUPLE arm.
- selfhost/cmd/wcc/cgenexpr.ww: cgdot tuple-positional
(size/load op + str-check).
- selfhost/cmd/wcc/cgenstmt.ww: cglet TTUPLE init, cgmlet
call-return walk, cgforrange elem-size + bind-walk.
Out of scope: N_TFN params, N_TTAGGED variants, N_TSTRUCT fields.
N_TFIELD already wraps struct fields; N_TFN/N_TTAGGED aren't
currently chain-mutated by checker synthesis. If they ever are,
the same pattern applies.
Unblocks A.6.2.0b stamp on a clean foundation. Retires task #16.
Verified 132/132 incl. 990 AST byte-diff (astprint unwrap) + 995
self-rebuild byte-identity.
Port head-only of cstage cmd/wcc/check.c:1214-1228 to exprtype's
new N_SLICE arm. Four base shapes:
- N_TARRAY → synthesize N_TSLICE{lhs=elem} (cstage L1219).
- N_TSLICE → return basetn (pre-peel, matches cstage `base`
not `u`; L1221).
- N_TNAME("str") → mktname("str") (matches cstage's `ty_str`
canonical singleton, L1223).
- N_TPTR with non-nil sub → synthesize N_TSLICE{lhs=sub}
(L1225, Hare-faithful slice-of-elem from pointer-to-elem).
Slice bounds (e.rhs start, e.cond end) are already walked by
resolvewalk L406-408 + post-order dispatch L460-489 — typical
N_INTLIT/N_IDENT/N_BIN are in the dispatch list; this arm does
not double-walk.
One documented divergence from cstage: lenient on non-sliceable
base (cstage L1227 errs; wwstage returns nil under the established
scruttype L656 / A.6.1.5b N_DOT struct-miss precedent).
A.6.2 step 1 of 8 (γ scope per Drew, 2026-05-21) — gap-sweep
per-kind first, post-checker assertion last. Order by triviality:
N_SLICE → N_TUPLE → N_ALLOC → N_RECV → N_SPREAD → N_YIELD →
N_MATCH → assertion. Each commit holds invariant; cgen readers
migrate in A.6.3.
Verified 132/132 incl. 995_self_rebuild byte-identity.
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.