Commit Graph

83 Commits

Author SHA1 Message Date
98e166504f wcc: stamp N_IDENT-callee destructure bindings in checker (#121)
resolvewalk N_MLET arm distributes the N_IDENT-callee rhs return-tuple
element types onto unannotated bindings (the A-narrow slice). Byte-id-
neutral — cgen still classifies structurally, stamps inert until the
exprfloatkind collapse. N_DOT-callee destructure deferred to #16/#17.
Prereq for the #121 collapse (commits 2/3).
2026-05-26 17:27:44 +09:00
a1dff13ec1 wcc: stamp un-suffixed f32-context float literals (#104 fold-2)
fold-1 narrows a float literal at materialisation only when its node
already carries an f32 type — the `f32` suffix. The common un-suffixed
case `let x: f32 = 1.0` stays ty_untyped_float through the checker, so
the node is never f32-typed: the literal materialises as a 64-bit double
and the f32 consumer reads the low 4 bytes (0.0f for clean values).

Stamp such a literal f32 when an f32 target type is in context, the way
harec's lower_implicit_cast does (ref/harec/src/check.c:148): a float
literal's bit pattern is target-dependent, unlike a width-agnostic int
immediate, so the value-producing node must carry the type. Scoped to
untyped_float -> f32 only (f64 already works via cgen's double default).

coerce_floatlit (cstage clet + cstmt N_RETURN) / coercefloatlit (wwstage
resolvewalk's post-order N_LET / N_RETURN handler) are logically
identical. The wwstage stamp is placed AFTER the child re-walk: the
post-order exprtype dispatch re-stamps a bare N_FLOATLIT back to
untyped_float, so coercing earlier (checkletassign) would be undone.

Scope is let-init and return ONLY, aligned down to the leaner wwstage
(rule 10). The wwstage cgen's exprfloatkind hardcodes a float literal to
f64 and cgbin / the unary negate pick f32 off the operands, not the node
stamp — so a stamped literal in an arith-binop / behind a unary minus
narrows in cstage (ADDSS) but not wwstage (ADDSD), a byte-id break. The
wwstage checker also has no assign / param-typed call-arg / per-field
struct-lit site. binop, unary-minus, assign, call-arg, struct-field wait
on #120 (wwstage cgen + checker build-out).

965_f32stamp_run: cstage run + cs==ww byte-id over un-suffixed let-init
and return literals, the hole 964 left open. Regen w6c/wwdump
combined.ww embeds.
2026-05-26 12:06:47 +09:00
f4970d886c wcc: opaque use-guards — reject every unsized use (incl tuple/tagged, recursive) (#108)
#108 sub-fold (b): close the footgun #108(a) opened. opaque is abstract
and UNSIZED (size = align = SIZE_UNDEFINED = (u64)-1), legal only behind
indirection. Without guards a bare use would fabricate a (u64)-1-byte
slot — a silent miscompile (rule 7). opaque is illegal by-value in FOUR
aggregate positions (array element, struct field, tuple member, tagged-
union variant) + as a bare value, under size/align, and as a []opaque
element-index. LOUD guards, mirroring harec's scattered `size ==
SIZE_UNDEFINED` checks:

  1. bare value/local/param/return-by-value  (check.c clet, build_fn_type,
     top-level let; harec check.c:1524, :3931)
  2. opaque struct field                      (resolve_type N_TSTRUCT)
  3. [N]opaque array element                  (resolve_type N_TARRAY)
  3t. opaque tuple member                     (resolve_type N_TTUPLE;
      harec type_store.c:1147)
  3u. opaque tagged-union variant             (resolve_type N_TTAGGED;
      harec type_store.c:449)
  4. size(opaque) / align(opaque)             (size/align fold;
     harec check.c:2720)
  5. indexing []opaque                        (N_INDEX; harec check.c:384)

Detection is via the SIZE_UNDEFINED sentinel the guard consults, so the
sized forms `*opaque` (8B) and `[]opaque` (24B header) pass untouched.

Rule-10 per-guard stage placement:
  - Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an
    AST-level approximation with no binding-size computation (g1) and no
    type-decl field/element/member validation walk (g2/g3/3t/3u); its
    N_INDEX indexresult returns the element type without consulting its
    size and defers invalid-index rejection to the cstage (g5). Same
    cstage-only neg-case precedent as 712_redecl / 708_param_shadow_mod.
  - Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold
    (astsize/astalign would otherwise fold opaque to a bogus 0 — a silent
    miscompile); twinned via astunsized + deffolderr. Because the wwstage
    has NO per-construction guards, its fold alone must catch every
    opaque-containing type: astunsized is RECURSIVE — a type is unsized
    iff it is opaque OR an aggregate (array/struct/tuple/tagged) with a
    recursively-unsized member. This both reaches the tuple/tagged folds
    AND closes the leaf-only size([4]opaque)/size(struct{x:opaque})→0
    leak. The cstage size/align guard stays leaf — the cstage rejects
    unsized aggregates at construction, so its fold only ever sees a leaf.

opaque is unused by the bootstrap, so every guard is inert on the
selfhost corpus — 990-997 stay byte-identical. Regenerates the w6c/wwdump
combined.ww (check.ww embed). New compile-fail probe 961_opaque_guards
(14 build-fails rows incl tuple/tagged/nested + 2 *opaque/[]opaque
positive controls); 960 positive probe unchanged.
2026-05-26 09:52:13 +09:00
3a18d2cfe6 wcc: add the opaque abstract type (kind + UNDEFINED sentinel + name-binding) (#108)
#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.
2026-05-26 09:02:08 +09:00
9a265a31f5 wcc: name-bind the size type in type position (#85 fold-2)
Resolve `size` -> TY_SIZE at the type-name resolver (C lookup_builtin /
ww tinfofornode's N_TNAME chain), mirroring uintptr, both stages. This
makes `size` writable as a type (`let x: size`, struct field, etc.),
the prerequisite for lib/types SIZE_MAX.

Twins every NAME-keyed uintptr arm in the wwstage so it behaves like
the cstage's kind-keyed Type switches (already TY_SIZE-aware from
fold-1): primtypesize + astalign (8B/8-align), primsize + letscalarprim
(8B scalar slot), isinttypeast + isnumerictname (int/numeric). rule-10
symmetric; dead on the size-free selfhost corpus so 990-997 stay byte-id.

Coexists with the size(T) size-of operator (separate c.top SK_FN seed +
N_CALL fold, NOT a type path) and `.size` field access (N_DOT); neither
touched. No c.top SK_TYPE "size" seed (would collide with the operator
seed at check.ww:96). Regenerates w6c/wwdump combined.ww (checker
embedded). New probe 957_size_type_run exercises type-position `size`
and the operator in one scope.
2026-05-26 01:55:54 +09:00
bd7181ae1f wcc: add the size primitive type (TY_SIZE), classify as unsigned int (#85)
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).
2026-05-26 01:26:03 +09:00
0d1ae17dd0 check: def rhs const-fold resolves sibling/imported defs + casts (#88)
ww top-level def rhs const-fold was literal-only (fold_int_literal at the codegen emit-defs step), so a def referencing another def, an imported def, or a cast was inexpressible -- blocking faithful types/types::c/math/strconv ports whose defs cross-reference.

Fold at CHECK time: a recursive eval_def_const (pass-2 N_DEF arm, both stages) resolves N_IDENT/N_DOT via the checker's existing scope lookup to the target def's rhs, evaluates N_BIN through a shared fold_binop core (factored out of eval_enum_value so both compile-time-int-eval paths share one wrap/shift/divide table), strips identity/widening casts, and stamps rhs -> N_INTLIT. cgen is UNTOUCHED -- its existing literal-emit lays the DATA row. Gated to fire only when the plain literal fold fails, so existing defs keep their node and emitted asm is byte-identical (990-997 unperturbed by construction).

Guards (rule 7): recursion depth cap fails loud on a def cycle (same/cross-module); a narrowing cast (rhs outside target range) fails loud rather than silently truncating. Both stages' eval_def_const stamp identically (shared fold_binop semantics) so the substituted literal -- and byte-id -- holds across stages (rule 10, at the check pass).

a1 (same-module) + a2 (cross-module imported def) land together: the driver concatenates imports into one flat scope. Coverage: test/wcc/732_def_const_fold.
2026-05-25 11:27:00 +09:00
1140a590bf wcc: str -> 24B {ptr,len,cap}, 3-reg ABI -- parity with []u8 (both stages)
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).
2026-05-24 08:11:14 +09:00
f45caf1cd9 selfhost/cmd/wcc: index-base esz from n.type_.size + delete indexbaseesz (#72, A.6.3k)
The N_INDEX node carries the checker-stamped element tinfo (#60 arc); cgindex,
cgun TK_AMP, and cgassign now read the element stride off that .type_.size
instead of indexbaseesz's manual N_DOT-pseudo-field + structlookup walk,
retiring the helper (0 callers, ~96 LOC). Aligns down to cstage's
idx_eff(base->type)->sub->size (cmd/w6c/cgen.c:3517-18, natural element size) --
strictly more cstage-faithful than indexbaseesz's totsize/slotsize derivation
(byte-id held only because firing shapes have totsize==natural; cstage reads
natural and ww-old==cstage, so the flip is structural). Each site keeps its
nil->default-8 fallback; cgindex stays esz-only (signed_elem unset for the
N_DOT base, as before).

Closes the A.6.3 cgenutil-collapse arc: every type/size/offset query in cgen
now reads the checker-stamped tinfo, and the AST-walker / structinfo-walk
helpers it replaced (dotfieldtnode, indexvaluetnode, rhstargetname,
dotinnerstructptr, indexbaseesz) are retired. make test 134/134, byte-id
990-997 hold. Coverage: 713/741/755 + self-rebuild.
2026-05-24 03:16:14 +09:00
3036ba766d selfhost/cmd/wcc: type-key tagged variant match (#66, Phase-N step 3)
The user-ruled B-full semantic change: flip tagged-union variant matching
from surface-NAME to TYPE-identity (typeeq over tinfo.params), mirroring
cstage cg_variant_match (cmd/w6c/cgen.c:451). A cross-module `a.T` != `b.T`
and `type linerr=!str` != str are now distinguished by the per-decl TY_NAMED
pointer (Phase-N #64). ww has no type_assignable, so the untyped/loose arm
keeps the str/slice shape fallback (rule-10 align-down). The 5 helpers
(flatvariantidx, flatslicevariantidx, taggedvariantindex, cgtagvariantidx,
cgmatch dispatch) flip; nomem propagation (NAMED-name scan, no source value)
and the f64 widen arm (float-kind classification, no pattern node) are not
arm-by-value discrimination and stay name/kind-keyed.

The flip requires value nodes to carry nominal identity. exprtype's
N_STRUCTLIT arm stamped the flattened body, so `overflow{}` (overflow=!void)
got TY_VOID and missed its variant -- fixed to stamp the per-decl NAMED
(mktname(lhs.str) -> tinfofornode reuses the #64 NAMED build/cache, same ptr
the union variant resolved to), mirroring the N_CAST/N_IDENT arms + cstage.
Returns the body node unchanged (only e.type_ rides NAMED); struct-lit layout
is unaffected -- cgstructlitfill is structlookup(name)-keyed, never reads
NAMED.fields. The fix now hits all `T{}` stamps, kept byte-id by the #63/#65
structural-walker peels.

931_variant_typekey_run: table-driven, both stages, /tmp-isolated. Two rows
widen an alias-FIRST variant from a call (no surface name): `(linerr|str)`
str-via-call -> idx 1, `(ec|i32)` i32-via-call -> idx 1. Empirically
discriminating: FAILS pre-flip (wwstage falls to the leading-shape variant,
exit 10; cstage exit 0) and PASSES post-flip -- locking in the capability
byte-id can't reach (the corpus has no name-key/type-key-disagreeing
co-variant, which is why name-keying survived).

make test 134/134 (byte-id 990-997 green; 995 self-rebuild green).
2026-05-23 21:22:24 +09:00
0fde4beeb4 selfhost/cmd/wcc: peel NAMED in elemissignedc + refresh #63 comments (#65)
#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).
2026-05-23 19:54:00 +09:00
3866ea24f5 selfhost/cmd/wcc/check: build per-decl TY_NAMED in tinfofornode (#64, Phase-N step 2)
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).
2026-05-23 19:33:04 +09:00
ae6a59a355 selfhost/cmd/wcc: peel TY_NAMED in 5 structural walkers (#63, Phase-N step 1)
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.
2026-05-23 18:44:12 +09:00
e0c0f75b2a selfhost/cmd/wcc/check: flatten tagged spreads + iserror on tinfo.params (#61a)
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).
2026-05-23 17:36:39 +09:00
6c9a3b369e selfhost/cmd/wcc/check: populate tinfo.fields + .tupleelems (#57, A.6.3i-phase-1)
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.
2026-05-23 15:13:07 +09:00
26724feefb selfhost/cmd/wcc/check: populate TY_TAGGED.params (#50 phase 1, A.6.3f-a)
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.
2026-05-23 01:51:29 +09:00
3eef2828c6 selfhost/cmd/wcc: stamp typed-suffix N_FLOATLIT (#51b)
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.
2026-05-22 23:12:07 +09:00
1ce0f638bf selfhost/cmd/wcc: stamp typed-suffix N_INTLIT (#51, #52 precursor)
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.
2026-05-22 22:55:54 +09:00
e3237d1bcb selfhost/cmd/wcc/check: resolvealias prefer same-module (#53)
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.
2026-05-22 08:59:02 +09:00
045c49e398 selfhost/cmd/wcc: enable asserttyped + close A.6.2 (#4, #15)
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).
2026-05-22 04:10:24 +09:00
faba8b70dd selfhost/cmd/wcc: stamp inferred-let decl.lhs (#15 precursor, #43)
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.
2026-05-22 03:40:52 +09:00
45910ff966 selfhost/cmd/wcc: variadic param N_TSLICE wrap at installparams (cascade Commit 3, #25/#41)
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.
2026-05-22 03:09:22 +09:00
e662156574 selfhost/cmd/wcc: isassignable arm 6 → recursive (cascade Commit 1, #39)
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.
2026-05-22 02:37:00 +09:00
edd27101bf selfhost/cmd/wcc: N_CALL intercepts for len/append/free (A.6.2.1b)
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.
2026-05-22 00:52:53 +09:00
f8aac547b9 selfhost/cmd/wcc: extend enum fold to constexpr set (A.6.2.1a)
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).
2026-05-22 00:14:36 +09:00
6c70b46d5f selfhost/cmd/wcc: checkfile pass-2 walks d.attr (A.6.2.1-pre)
`@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).
2026-05-21 22:37:27 +09:00
009c4b35e2 selfhost/cmd/wcc: stamp e.type_ for N_MATCH + matchyieldtype port (A.6.2.0g)
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.
2026-05-21 21:27:56 +09:00
f0a8a37077 selfhost/cmd/wcc: stamp e.type_ for N_YIELD (A.6.2.0f)
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.
2026-05-21 21:18:25 +09:00
afd62a9187 selfhost/cmd/wcc: stamp e.type_ for N_SPREAD (A.6.2.0e)
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.
2026-05-21 21:14:08 +09:00
48c758bbe7 selfhost/cmd/wcc: stamp e.type_ for N_RECV (A.6.2.0d)
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.
2026-05-21 21:10:58 +09:00
4e75285a0b selfhost/cmd/wcc: drop dead N_ALLOC from post-dispatch list (A.6.2.0c)
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.
2026-05-21 21:05:50 +09:00
1131e7b68d selfhost/cmd/wcc: stamp e.type_ for N_TUPLE (A.6.2.0b)
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).
2026-05-21 21:01:57 +09:00
805c841f34 selfhost+lib/ww: N_TPARAM wrapper for tuple chains (A.6.2.0b-pre)
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.
2026-05-21 20:55:40 +09:00
c564d7d0fd selfhost/cmd/wcc: stamp e.type_ for N_SLICE (A.6.2.0a)
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.
2026-05-21 20:16:24 +09:00
0e1ab38586 selfhost/cmd/wcc: stamp e.type_ for N_ARRLIT (A.6.1.7)
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.
2026-05-21 19:53:31 +09:00
5f19015e16 selfhost/cmd/wcc: stamp e.type_ for N_STRUCTLIT (A.6.1.6)
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.
2026-05-21 19:28:45 +09:00
aa73e73013 selfhost/cmd/wcc: stamp N_DOT struct/pseudo/tuple in exprtype (A.6.1.5b)
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.
2026-05-21 19:08:02 +09:00
894d966707 selfhost/cmd/wcc: fold N_DOT module/enum in exprtype (A.6.1.5a)
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.
2026-05-21 18:46:34 +09:00
0715ec9662 selfhost/cmd/wcc: stamp e.type_ for N_INDEX (A.6.1.4)
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.
2026-05-21 16:08:48 +09:00
ada99973ee selfhost/cmd/wcc: stamp e.type_ for N_BIN + N_UN (A.6.1.3)
Adds unifyarith / binoptype / unoptype mirroring cstage
cmd/wcc/check.c:580-596, 598-640, 642-687. Two new exprtype
arms stamp e.type_ for N_BIN and N_UN via the same
tinfofornode pattern as A.6.1.1/.2.

unifyarith: untyped+untyped (prefer float), untyped+typed
(typed via isassignable), typed+typed (typeeqast), fallback ltn.
binoptype: ptr arith (ptr±int → ptr, ptr-ptr → i64), bitwise/
arith → unifyarith, comparison + logical → bool.
unoptype: deref (resolvealias+unwrapbang then N_TPTR.lhs),
addrof → *T with slice/str pseudo-field .len/.cap widening
to *i64 (mirrors check.c:672-682), unary +/-/^/!.

Wwstage stays silent on operator-type errors per existing
checker discipline; cstage flags the same shapes.

Verified 132/132 incl. 995_self_rebuild byte-identity.
2026-05-21 15:31:03 +09:00
19095e4b50 selfhost/cmd/wcc: stamp e.type_ for N_CALL + drop fold hook (A.6.1.2)
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.
2026-05-21 14:54:29 +09:00
c0fe38c101 selfhost/cmd/wcc: stamp e.type_ for CAST/TRY/TYPEASSERT/TYPETEST (A.6.1.1)
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.
2026-05-21 14:27:09 +09:00
321e7ca615 selfhost/cmd/wcc: plumb exprtype hint + resolvewalk dispatch (A.6.0)
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.
2026-05-21 13:58:25 +09:00
353dffb5e8 lib/ww + wcc + w6c + wwdump: strip *arena cascade (γ-6)
amalloc has 0 callers post-γ-2; the *arena threaded through
newnode/newscope/newtype/prim/typesinit/type{ptr,slice,array,chan,
named}/lexinit/parserinit/joindotted/checkinit/arenau64tos/cgeninit
and the scope.a / tctx.a / lex.a / parser.a / checker.a / cgen.a
fields are vestigial.

Drop `import mem;` from 15 files, remove six struct fields, strip
*arena from 14 signatures, update ~120 call sites across lib/ww +
wcc + w6c + wwdump. selfhost/test/sym_link.ww fixture drops the
newarena/freearena probe; still exits 42 on scopedefine/scopelookup.
Both main.combined.ww auto-regenerated.

Comments retidied: typ.ww "once per arena" → "once per program";
parse.ww drops "arena-build" qualifier on joindotted; sym.ww drops
mem-sibling-imports rationale.

Verified 132/132 incl. 994_w6c_ww + 995_self_rebuild byte-identity
(the primary symmetric-stages gate).
2026-05-21 13:01:57 +09:00
a3e4c6942f selfhost/cmd/wcc/check.ww: arenau64tos amalloc → alloc([], 24)! (α-9) 2026-05-21 03:26:23 +09:00
d68d3c7eb4 lib: extract rt module from os, sweep imports
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).
2026-05-20 20:39:52 +09:00
f80927201b tools/sizelint + CLAUDE.md rule 13: gate hardcoded size literals
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.
2026-05-20 15:22:21 +09:00
03b7336cae selfhost/cmd/wcc + lib/strings: restore SSoT routing for str/slice tinfo helpers
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.
2026-05-20 14:55:36 +09:00
9fd79cdc33 selfhost/cmd/wcc + lib/ww: tinfo.slotsize SSoT + module-name TNAME fallback (Phase A.5)
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.
2026-05-20 14:30:55 +09:00
e37b76710a selfhost/cmd/wcc: TARRAY struct-stride + cache-bind resolved body (Phase A.4)
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.
2026-05-20 13:42:15 +09:00