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).
This commit is contained in:
2026-05-22 04:10:24 +09:00
parent faba8b70dd
commit 045c49e398
4 changed files with 507 additions and 39 deletions

View File

@@ -149,7 +149,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = {
// ---- printer ----------------------------------------------------------
fn nkname(k: nkind) str = {
export fn nkname(k: nkind) str = {
if (k == nkind.N_NONE) { return "none"; };
if (k == nkind.N_INTLIT) { return "int"; };
if (k == nkind.N_FLOATLIT) { return "float"; };

View File

@@ -4569,7 +4569,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = {
// ---- printer ----------------------------------------------------------
fn nkname(k: nkind) str = {
export fn nkname(k: nkind) str = {
if (k == nkind.N_NONE) { return "none"; };
if (k == nkind.N_INTLIT) { return "int"; };
if (k == nkind.N_FLOATLIT) { return "float"; };
@@ -6985,6 +6985,7 @@ package wcc;
import os;
import tok;
import strconv;
type checker = struct {
tc: *tctx,
@@ -8310,10 +8311,17 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
};
// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith`. The
// trailing typeeq fallback covers same-typed operands; mismatched typed
// pairs silently return ltn (cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline).
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec
// ref/harec/src/types.c type_promote. Trailing `return ltn` covers
// mismatched typed pairs; cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline.
//
// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn
// and rtn can be nil when an operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-
// builtin callee with sym.decl == nil; #19 retires these as
// dedicated AST kinds). Propagation, not silent gap — asserttyped
// gates those idents at the consumer layer.
fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn);
let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn);
@@ -8331,6 +8339,9 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
if (isassignable(c, ltn, rtn, &conf)) { return ltn; };
};
if (typeeqast(ltn, rtn)) { return ltn; };
// Mismatched typed pair — return ltn so the binop stamps something;
// 5-lite-b: the trailing nil-on-mismatch shape was eliminated when
// the helper was split out of binoptype.
return ltn;
};
@@ -8372,13 +8383,18 @@ fn binoptype(c: *checker, e: *node) *node = {
op == tkind.TK_AND || op == tkind.TK_OR) {
return mktname(c, "bool");
};
// Unreachable for valid input: op is one of TK_PLUS/MINUS/STAR/
// SLASH/PERCENT/AMP/PIPE/CARET/LSHIFT/RSHIFT/EQ/NEQ/LT/LE/GT/GE/
// AND/OR per parser invariant (lib/ww/parse/expr.ww binary-op
// table); all are handled above. 5-lite-b #34.
return nil;
};
// unoptype — derive the result tnode of an N_UN unary expression. Mirrors
// cstage cmd/wcc/check.c:642-687 `cunop`. The slice/str .len/.cap
// pseudo-field address-of widening to *i64 mirrors check.c:672-682
// directly — its purpose is documented at the cstage site.
// cstage cmd/wcc/check.c:642-687 `cunop` and harec ref/harec/src/types.c
// type_promote. The slice/str .len/.cap pseudo-field address-of widening
// to *i64 mirrors check.c:672-682 directly — its purpose is documented
// at the cstage site.
fn unoptype(c: *checker, e: *node) *node = {
let op: tkind = e.op;
let opt: *node = exprtype(c, e.lhs, nil);
@@ -8386,9 +8402,18 @@ fn unoptype(c: *checker, e: *node) *node = {
if (op == tkind.TK_NOT) { return mktname(c, "bool"); };
if (op == tkind.TK_TILDE) { return opt; };
if (op == tkind.TK_STAR) {
// opt nil here means the operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE / pseudo-builtin
// sym.decl == nil — or another helper's nil propagation);
// 5-lite-b #34, A.6.2.1c #24. The `u == nil` post-resolvealias
// check was eliminated here — unwrapbang(non-nil) returns
// non-nil (parser invariant N_TBANG.lhs always set) and
// resolvealias passes through non-nil unchanged (L513
// `for (cur != nil)` only exits via `return cur` or `return n`).
if (opt == nil) { return nil; };
let u: *node = resolvealias(c, unwrapbang(opt));
if (u == nil) { return nil; };
// Invalid input (non-pointer dereference); cstage errors at
// cmd/wcc/check.c:660. 5-lite-b #34.
if (u.kind != nkind.N_TPTR) { return nil; };
return u.lhs;
};
@@ -8412,23 +8437,33 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
if (opt == nil) { return nil; };
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = opt;
return pp;
};
// Unreachable for valid input: op is one of TK_MINUS/PLUS/NOT/
// TILDE/STAR/AMP per parser invariant (lib/ww/parse/expr.ww unary
// op set); all are handled above. 5-lite-b #34.
return nil;
};
// indexresult — derive the result tnode of an N_INDEX expression.
// Mirrors cstage cmd/wcc/check.c:870-894. Slice/array → elem; str →
// u8; `*[N]T` decays to T (pointer-to-array); `*[]T` does NOT decay
// (yields []T via the generic *U → U fallback — Hare-faithful, a
// pointer-to-slice is a 1D array of slices, not of T); generic *T → T.
// Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c
// type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays
// to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the
// generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D
// array of slices, not of T); generic *T → T.
fn indexresult(c: *checker, e: *node) *node = {
let basetn: *node = exprtype(c, e.lhs, nil);
let _idx: *node = exprtype(c, e.rhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// basetn nil → propagation from inherent-IDENT bail at exprtype
// N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24).
// unwrapbang(nil)=nil and resolvealias(nil)=nil pass through.
if (u == nil) { return nil; };
if (u.kind == nkind.N_TSLICE) { return u.lhs; };
if (u.kind == nkind.N_TARRAY) { return u.lhs; };
@@ -8443,6 +8478,8 @@ fn indexresult(c: *checker, e: *node) *node = {
};
return inner;
};
// Invalid input (non-indexable base — cstage errors at
// cmd/wcc/check.c:893). 5-lite-b #34.
return nil;
};
@@ -8459,6 +8496,19 @@ fn indexresult(c: *checker, e: *node) *node = {
// follows doesn't ripple a fresh signature change. Mirrors harec's
// `check_expression(..., result_type, ...)` per
// `feedback_hare_frontend_reference.md`.
//
// Dispatcher invariant (5-lite-a #33): every value-producing nkind
// listed in resolvewalk's post-order dispatch (L474-489) reaches a
// stamping arm here that sets e.type_ before returning. No
// fall-through. Arms that return nil (binoptype trailing, unoptype
// TK_STAR opt-nil, indexresult u-nil, N_DOT outer fold-miss, N_SLICE
// non-sliceable base, etc.) are propagation from a callee's nil —
// not silent gaps. Mirror of harec's
// `assert(expr->result)` at ref/harec/src/check.c:3810. The
// asserttyped pass at L2871 enforces the invariant on every
// dispatched node post-checker, with gates for the residual
// inherent-IDENT bails (SK_USE, pseudo-builtin sym.decl==nil,
// N_DOT-LHS syntactic position) until #19 retires the bail shape.
fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
@@ -9753,6 +9803,101 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = {
c.cur = outer;
};
// asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the
// file tree and fires (writes a one-line diagnostic to stderr) for any
// node in resolvewalk's value-producing dispatch set (L474-489) whose
// n.type_ remained nil. Mirror of harec's `assert(expr->result)` at
// ref/harec/src/check.c:3810 — wwstage's checker is lenient (rule 7),
// so this is a soft assertion (diagnostic, not abort) used by the
// 990_selfhost probes to catch regressions in the stamping discipline.
//
// Gates (per Drew 2026-05-22 — "guards value-producing expression
// nodes; SK_USE refs and bare builtin callees are syntactic positions,
// gate them out with WHY pointing at #19"):
//
// 1. N_IDENT whose resolved sym kind is SK_USE — module references
// (`os` in `os.write`). Harec models these via EXPR_ACCESS whose
// lookup-target is an OBJ_USE directly; there is no intermediate
// "ident-as-value" expr. Until #19 ports that AST shape, skip.
// 2. N_IDENT whose resolved sym has decl == nil — pseudo-builtin
// callees (len/append/free/alloc/size/align/offset, seeded in
// checkinit L85-97 with decl=nil). Harec spells these as
// dedicated EXPR_* kinds (EXPR_LEN, EXPR_APPEND, EXPR_FREE,
// EXPR_ALLOC at ref/harec/src/check.c:2630/745/2443/...).
// Drew's δ (#19) retires the seeded-SK_FN-with-nil-decl hack.
// 3. N_IDENT at the LHS-of-N_DOT syntactic position — the bare
// name half of a member-access expr is a lookup target, not a
// value-producing sub-expression. Harec's EXPR_ACCESS stores the
// member as a string, not a node.
//
// `indot` tracks gate 3: true only when the immediate caller is an
// N_DOT recursing into its .lhs.
fn asserttyped(c: *checker, n: *node, indot: bool) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
let isexpr: bool =
k == nkind.N_INTLIT || k == nkind.N_FLOATLIT ||
k == nkind.N_STRLIT || k == nkind.N_RUNELIT ||
k == nkind.N_TRUE || k == nkind.N_FALSE ||
k == nkind.N_NIL || k == nkind.N_VOIDLIT ||
k == nkind.N_IDENT || k == nkind.N_BIN ||
k == nkind.N_UN || k == nkind.N_CALL ||
k == nkind.N_INDEX || k == nkind.N_CAST ||
k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT ||
k == nkind.N_RECV || k == nkind.N_DOT ||
k == nkind.N_SLICE || k == nkind.N_SPREAD ||
k == nkind.N_TUPLE || k == nkind.N_TRYPROP ||
k == nkind.N_TRYUNW || k == nkind.N_TYPETEST ||
k == nkind.N_TYPEASSERT || k == nkind.N_YIELD ||
k == nkind.N_MATCH;
let skip: bool = false;
if (isexpr && k == nkind.N_IDENT) {
if (indot) { skip = true; };
if (!skip) {
let s: *sym = scopelookup(c.cur, n.str);
if (s != nil) {
if (s.skind == skind.SK_USE) { skip = true; };
if (s.decl == nil) { skip = true; };
};
};
};
if (isexpr && !skip) {
if (n.type_ == nil) {
os.write(2, "asserttyped: ".ptr, 13u64);
let kn: str = nkname(k);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, " ".ptr, 1u64);
if (n.file.len > 0) {
os.write(2, n.file.ptr, n.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
};
if (n.str.len > 0) {
os.write(2, " '".ptr, 2u64);
os.write(2, n.str.ptr, n.str.len: u64);
os.write(2, "'".ptr, 1u64);
};
os.write(2, "\n".ptr, 1u64);
};
};
if (k == nkind.N_DOT) {
if (n.lhs != nil) { asserttyped(c, n.lhs, true); };
return;
};
if (n.attr != nil) { asserttyped(c, n.attr, false); };
if (n.lhs != nil) { asserttyped(c, n.lhs, false); };
if (n.rhs != nil) { asserttyped(c, n.rhs, false); };
if (n.cond != nil) { asserttyped(c, n.cond, false); };
if (n.body != nil) { asserttyped(c, n.body, false); };
if (n.els != nil) { asserttyped(c, n.els, false); };
let m: *node = n.list;
for (m != nil) {
asserttyped(c, m, false);
m = m.next;
};
};
export fn checkinit(c: *checker, tc: *tctx) void = {
c.tc = tc;
c.top = newscope(nil);
@@ -9808,6 +9953,17 @@ export fn checkfile(c: *checker, file: *node) void = {
};};};};
d = d.next;
};
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
// decl with its curmod set so asserttyped's gate lookups resolve
// against the same module context exprtype saw during pass 2.
d = file.list;
for (d != nil) {
c.curmod = declmod(file, d);
asserttyped(c, d, false);
d = d.next;
};
let empty: str;
c.curmod = empty;

View File

@@ -21,6 +21,7 @@ package wcc;
import os;
import tok;
import strconv;
type checker = struct {
tc: *tctx,
@@ -1346,10 +1347,17 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
};
// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith`. The
// trailing typeeq fallback covers same-typed operands; mismatched typed
// pairs silently return ltn (cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline).
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec
// ref/harec/src/types.c type_promote. Trailing `return ltn` covers
// mismatched typed pairs; cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline.
//
// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn
// and rtn can be nil when an operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-
// builtin callee with sym.decl == nil; #19 retires these as
// dedicated AST kinds). Propagation, not silent gap — asserttyped
// gates those idents at the consumer layer.
fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn);
let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn);
@@ -1367,6 +1375,9 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
if (isassignable(c, ltn, rtn, &conf)) { return ltn; };
};
if (typeeqast(ltn, rtn)) { return ltn; };
// Mismatched typed pair — return ltn so the binop stamps something;
// 5-lite-b: the trailing nil-on-mismatch shape was eliminated when
// the helper was split out of binoptype.
return ltn;
};
@@ -1408,13 +1419,18 @@ fn binoptype(c: *checker, e: *node) *node = {
op == tkind.TK_AND || op == tkind.TK_OR) {
return mktname(c, "bool");
};
// Unreachable for valid input: op is one of TK_PLUS/MINUS/STAR/
// SLASH/PERCENT/AMP/PIPE/CARET/LSHIFT/RSHIFT/EQ/NEQ/LT/LE/GT/GE/
// AND/OR per parser invariant (lib/ww/parse/expr.ww binary-op
// table); all are handled above. 5-lite-b #34.
return nil;
};
// unoptype — derive the result tnode of an N_UN unary expression. Mirrors
// cstage cmd/wcc/check.c:642-687 `cunop`. The slice/str .len/.cap
// pseudo-field address-of widening to *i64 mirrors check.c:672-682
// directly — its purpose is documented at the cstage site.
// cstage cmd/wcc/check.c:642-687 `cunop` and harec ref/harec/src/types.c
// type_promote. The slice/str .len/.cap pseudo-field address-of widening
// to *i64 mirrors check.c:672-682 directly — its purpose is documented
// at the cstage site.
fn unoptype(c: *checker, e: *node) *node = {
let op: tkind = e.op;
let opt: *node = exprtype(c, e.lhs, nil);
@@ -1422,9 +1438,18 @@ fn unoptype(c: *checker, e: *node) *node = {
if (op == tkind.TK_NOT) { return mktname(c, "bool"); };
if (op == tkind.TK_TILDE) { return opt; };
if (op == tkind.TK_STAR) {
// opt nil here means the operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE / pseudo-builtin
// sym.decl == nil — or another helper's nil propagation);
// 5-lite-b #34, A.6.2.1c #24. The `u == nil` post-resolvealias
// check was eliminated here — unwrapbang(non-nil) returns
// non-nil (parser invariant N_TBANG.lhs always set) and
// resolvealias passes through non-nil unchanged (L513
// `for (cur != nil)` only exits via `return cur` or `return n`).
if (opt == nil) { return nil; };
let u: *node = resolvealias(c, unwrapbang(opt));
if (u == nil) { return nil; };
// Invalid input (non-pointer dereference); cstage errors at
// cmd/wcc/check.c:660. 5-lite-b #34.
if (u.kind != nkind.N_TPTR) { return nil; };
return u.lhs;
};
@@ -1448,23 +1473,33 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
if (opt == nil) { return nil; };
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = opt;
return pp;
};
// Unreachable for valid input: op is one of TK_MINUS/PLUS/NOT/
// TILDE/STAR/AMP per parser invariant (lib/ww/parse/expr.ww unary
// op set); all are handled above. 5-lite-b #34.
return nil;
};
// indexresult — derive the result tnode of an N_INDEX expression.
// Mirrors cstage cmd/wcc/check.c:870-894. Slice/array → elem; str →
// u8; `*[N]T` decays to T (pointer-to-array); `*[]T` does NOT decay
// (yields []T via the generic *U → U fallback — Hare-faithful, a
// pointer-to-slice is a 1D array of slices, not of T); generic *T → T.
// Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c
// type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays
// to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the
// generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D
// array of slices, not of T); generic *T → T.
fn indexresult(c: *checker, e: *node) *node = {
let basetn: *node = exprtype(c, e.lhs, nil);
let _idx: *node = exprtype(c, e.rhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// basetn nil → propagation from inherent-IDENT bail at exprtype
// N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24).
// unwrapbang(nil)=nil and resolvealias(nil)=nil pass through.
if (u == nil) { return nil; };
if (u.kind == nkind.N_TSLICE) { return u.lhs; };
if (u.kind == nkind.N_TARRAY) { return u.lhs; };
@@ -1479,6 +1514,8 @@ fn indexresult(c: *checker, e: *node) *node = {
};
return inner;
};
// Invalid input (non-indexable base — cstage errors at
// cmd/wcc/check.c:893). 5-lite-b #34.
return nil;
};
@@ -1495,6 +1532,19 @@ fn indexresult(c: *checker, e: *node) *node = {
// follows doesn't ripple a fresh signature change. Mirrors harec's
// `check_expression(..., result_type, ...)` per
// `feedback_hare_frontend_reference.md`.
//
// Dispatcher invariant (5-lite-a #33): every value-producing nkind
// listed in resolvewalk's post-order dispatch (L474-489) reaches a
// stamping arm here that sets e.type_ before returning. No
// fall-through. Arms that return nil (binoptype trailing, unoptype
// TK_STAR opt-nil, indexresult u-nil, N_DOT outer fold-miss, N_SLICE
// non-sliceable base, etc.) are propagation from a callee's nil —
// not silent gaps. Mirror of harec's
// `assert(expr->result)` at ref/harec/src/check.c:3810. The
// asserttyped pass at L2871 enforces the invariant on every
// dispatched node post-checker, with gates for the residual
// inherent-IDENT bails (SK_USE, pseudo-builtin sym.decl==nil,
// N_DOT-LHS syntactic position) until #19 retires the bail shape.
fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
@@ -2789,6 +2839,101 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = {
c.cur = outer;
};
// asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the
// file tree and fires (writes a one-line diagnostic to stderr) for any
// node in resolvewalk's value-producing dispatch set (L474-489) whose
// n.type_ remained nil. Mirror of harec's `assert(expr->result)` at
// ref/harec/src/check.c:3810 — wwstage's checker is lenient (rule 7),
// so this is a soft assertion (diagnostic, not abort) used by the
// 990_selfhost probes to catch regressions in the stamping discipline.
//
// Gates (per Drew 2026-05-22 — "guards value-producing expression
// nodes; SK_USE refs and bare builtin callees are syntactic positions,
// gate them out with WHY pointing at #19"):
//
// 1. N_IDENT whose resolved sym kind is SK_USE — module references
// (`os` in `os.write`). Harec models these via EXPR_ACCESS whose
// lookup-target is an OBJ_USE directly; there is no intermediate
// "ident-as-value" expr. Until #19 ports that AST shape, skip.
// 2. N_IDENT whose resolved sym has decl == nil — pseudo-builtin
// callees (len/append/free/alloc/size/align/offset, seeded in
// checkinit L85-97 with decl=nil). Harec spells these as
// dedicated EXPR_* kinds (EXPR_LEN, EXPR_APPEND, EXPR_FREE,
// EXPR_ALLOC at ref/harec/src/check.c:2630/745/2443/...).
// Drew's δ (#19) retires the seeded-SK_FN-with-nil-decl hack.
// 3. N_IDENT at the LHS-of-N_DOT syntactic position — the bare
// name half of a member-access expr is a lookup target, not a
// value-producing sub-expression. Harec's EXPR_ACCESS stores the
// member as a string, not a node.
//
// `indot` tracks gate 3: true only when the immediate caller is an
// N_DOT recursing into its .lhs.
fn asserttyped(c: *checker, n: *node, indot: bool) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
let isexpr: bool =
k == nkind.N_INTLIT || k == nkind.N_FLOATLIT ||
k == nkind.N_STRLIT || k == nkind.N_RUNELIT ||
k == nkind.N_TRUE || k == nkind.N_FALSE ||
k == nkind.N_NIL || k == nkind.N_VOIDLIT ||
k == nkind.N_IDENT || k == nkind.N_BIN ||
k == nkind.N_UN || k == nkind.N_CALL ||
k == nkind.N_INDEX || k == nkind.N_CAST ||
k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT ||
k == nkind.N_RECV || k == nkind.N_DOT ||
k == nkind.N_SLICE || k == nkind.N_SPREAD ||
k == nkind.N_TUPLE || k == nkind.N_TRYPROP ||
k == nkind.N_TRYUNW || k == nkind.N_TYPETEST ||
k == nkind.N_TYPEASSERT || k == nkind.N_YIELD ||
k == nkind.N_MATCH;
let skip: bool = false;
if (isexpr && k == nkind.N_IDENT) {
if (indot) { skip = true; };
if (!skip) {
let s: *sym = scopelookup(c.cur, n.str);
if (s != nil) {
if (s.skind == skind.SK_USE) { skip = true; };
if (s.decl == nil) { skip = true; };
};
};
};
if (isexpr && !skip) {
if (n.type_ == nil) {
os.write(2, "asserttyped: ".ptr, 13u64);
let kn: str = nkname(k);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, " ".ptr, 1u64);
if (n.file.len > 0) {
os.write(2, n.file.ptr, n.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
};
if (n.str.len > 0) {
os.write(2, " '".ptr, 2u64);
os.write(2, n.str.ptr, n.str.len: u64);
os.write(2, "'".ptr, 1u64);
};
os.write(2, "\n".ptr, 1u64);
};
};
if (k == nkind.N_DOT) {
if (n.lhs != nil) { asserttyped(c, n.lhs, true); };
return;
};
if (n.attr != nil) { asserttyped(c, n.attr, false); };
if (n.lhs != nil) { asserttyped(c, n.lhs, false); };
if (n.rhs != nil) { asserttyped(c, n.rhs, false); };
if (n.cond != nil) { asserttyped(c, n.cond, false); };
if (n.body != nil) { asserttyped(c, n.body, false); };
if (n.els != nil) { asserttyped(c, n.els, false); };
let m: *node = n.list;
for (m != nil) {
asserttyped(c, m, false);
m = m.next;
};
};
export fn checkinit(c: *checker, tc: *tctx) void = {
c.tc = tc;
c.top = newscope(nil);
@@ -2844,6 +2989,17 @@ export fn checkfile(c: *checker, file: *node) void = {
};};};};
d = d.next;
};
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
// decl with its curmod set so asserttyped's gate lookups resolve
// against the same module context exprtype saw during pass 2.
d = file.list;
for (d != nil) {
c.curmod = declmod(file, d);
asserttyped(c, d, false);
d = d.next;
};
let empty: str;
c.curmod = empty;

View File

@@ -4569,7 +4569,7 @@ export fn newnode(k: nkind, file: str, line: i32, col: i32) *node = {
// ---- printer ----------------------------------------------------------
fn nkname(k: nkind) str = {
export fn nkname(k: nkind) str = {
if (k == nkind.N_NONE) { return "none"; };
if (k == nkind.N_INTLIT) { return "int"; };
if (k == nkind.N_FLOATLIT) { return "float"; };
@@ -6985,6 +6985,7 @@ package wcc;
import os;
import tok;
import strconv;
type checker = struct {
tc: *tctx,
@@ -8310,10 +8311,17 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
};
// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith`. The
// trailing typeeq fallback covers same-typed operands; mismatched typed
// pairs silently return ltn (cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline).
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec
// ref/harec/src/types.c type_promote. Trailing `return ltn` covers
// mismatched typed pairs; cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline.
//
// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn
// and rtn can be nil when an operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-
// builtin callee with sym.decl == nil; #19 retires these as
// dedicated AST kinds). Propagation, not silent gap — asserttyped
// gates those idents at the consumer layer.
fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn);
let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn);
@@ -8331,6 +8339,9 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
if (isassignable(c, ltn, rtn, &conf)) { return ltn; };
};
if (typeeqast(ltn, rtn)) { return ltn; };
// Mismatched typed pair — return ltn so the binop stamps something;
// 5-lite-b: the trailing nil-on-mismatch shape was eliminated when
// the helper was split out of binoptype.
return ltn;
};
@@ -8372,13 +8383,18 @@ fn binoptype(c: *checker, e: *node) *node = {
op == tkind.TK_AND || op == tkind.TK_OR) {
return mktname(c, "bool");
};
// Unreachable for valid input: op is one of TK_PLUS/MINUS/STAR/
// SLASH/PERCENT/AMP/PIPE/CARET/LSHIFT/RSHIFT/EQ/NEQ/LT/LE/GT/GE/
// AND/OR per parser invariant (lib/ww/parse/expr.ww binary-op
// table); all are handled above. 5-lite-b #34.
return nil;
};
// unoptype — derive the result tnode of an N_UN unary expression. Mirrors
// cstage cmd/wcc/check.c:642-687 `cunop`. The slice/str .len/.cap
// pseudo-field address-of widening to *i64 mirrors check.c:672-682
// directly — its purpose is documented at the cstage site.
// cstage cmd/wcc/check.c:642-687 `cunop` and harec ref/harec/src/types.c
// type_promote. The slice/str .len/.cap pseudo-field address-of widening
// to *i64 mirrors check.c:672-682 directly — its purpose is documented
// at the cstage site.
fn unoptype(c: *checker, e: *node) *node = {
let op: tkind = e.op;
let opt: *node = exprtype(c, e.lhs, nil);
@@ -8386,9 +8402,18 @@ fn unoptype(c: *checker, e: *node) *node = {
if (op == tkind.TK_NOT) { return mktname(c, "bool"); };
if (op == tkind.TK_TILDE) { return opt; };
if (op == tkind.TK_STAR) {
// opt nil here means the operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE / pseudo-builtin
// sym.decl == nil — or another helper's nil propagation);
// 5-lite-b #34, A.6.2.1c #24. The `u == nil` post-resolvealias
// check was eliminated here — unwrapbang(non-nil) returns
// non-nil (parser invariant N_TBANG.lhs always set) and
// resolvealias passes through non-nil unchanged (L513
// `for (cur != nil)` only exits via `return cur` or `return n`).
if (opt == nil) { return nil; };
let u: *node = resolvealias(c, unwrapbang(opt));
if (u == nil) { return nil; };
// Invalid input (non-pointer dereference); cstage errors at
// cmd/wcc/check.c:660. 5-lite-b #34.
if (u.kind != nkind.N_TPTR) { return nil; };
return u.lhs;
};
@@ -8412,23 +8437,33 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
if (opt == nil) { return nil; };
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = opt;
return pp;
};
// Unreachable for valid input: op is one of TK_MINUS/PLUS/NOT/
// TILDE/STAR/AMP per parser invariant (lib/ww/parse/expr.ww unary
// op set); all are handled above. 5-lite-b #34.
return nil;
};
// indexresult — derive the result tnode of an N_INDEX expression.
// Mirrors cstage cmd/wcc/check.c:870-894. Slice/array → elem; str →
// u8; `*[N]T` decays to T (pointer-to-array); `*[]T` does NOT decay
// (yields []T via the generic *U → U fallback — Hare-faithful, a
// pointer-to-slice is a 1D array of slices, not of T); generic *T → T.
// Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c
// type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays
// to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the
// generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D
// array of slices, not of T); generic *T → T.
fn indexresult(c: *checker, e: *node) *node = {
let basetn: *node = exprtype(c, e.lhs, nil);
let _idx: *node = exprtype(c, e.rhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// basetn nil → propagation from inherent-IDENT bail at exprtype
// N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24).
// unwrapbang(nil)=nil and resolvealias(nil)=nil pass through.
if (u == nil) { return nil; };
if (u.kind == nkind.N_TSLICE) { return u.lhs; };
if (u.kind == nkind.N_TARRAY) { return u.lhs; };
@@ -8443,6 +8478,8 @@ fn indexresult(c: *checker, e: *node) *node = {
};
return inner;
};
// Invalid input (non-indexable base — cstage errors at
// cmd/wcc/check.c:893). 5-lite-b #34.
return nil;
};
@@ -8459,6 +8496,19 @@ fn indexresult(c: *checker, e: *node) *node = {
// follows doesn't ripple a fresh signature change. Mirrors harec's
// `check_expression(..., result_type, ...)` per
// `feedback_hare_frontend_reference.md`.
//
// Dispatcher invariant (5-lite-a #33): every value-producing nkind
// listed in resolvewalk's post-order dispatch (L474-489) reaches a
// stamping arm here that sets e.type_ before returning. No
// fall-through. Arms that return nil (binoptype trailing, unoptype
// TK_STAR opt-nil, indexresult u-nil, N_DOT outer fold-miss, N_SLICE
// non-sliceable base, etc.) are propagation from a callee's nil —
// not silent gaps. Mirror of harec's
// `assert(expr->result)` at ref/harec/src/check.c:3810. The
// asserttyped pass at L2871 enforces the invariant on every
// dispatched node post-checker, with gates for the residual
// inherent-IDENT bails (SK_USE, pseudo-builtin sym.decl==nil,
// N_DOT-LHS syntactic position) until #19 retires the bail shape.
fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
@@ -9753,6 +9803,101 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = {
c.cur = outer;
};
// asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the
// file tree and fires (writes a one-line diagnostic to stderr) for any
// node in resolvewalk's value-producing dispatch set (L474-489) whose
// n.type_ remained nil. Mirror of harec's `assert(expr->result)` at
// ref/harec/src/check.c:3810 — wwstage's checker is lenient (rule 7),
// so this is a soft assertion (diagnostic, not abort) used by the
// 990_selfhost probes to catch regressions in the stamping discipline.
//
// Gates (per Drew 2026-05-22 — "guards value-producing expression
// nodes; SK_USE refs and bare builtin callees are syntactic positions,
// gate them out with WHY pointing at #19"):
//
// 1. N_IDENT whose resolved sym kind is SK_USE — module references
// (`os` in `os.write`). Harec models these via EXPR_ACCESS whose
// lookup-target is an OBJ_USE directly; there is no intermediate
// "ident-as-value" expr. Until #19 ports that AST shape, skip.
// 2. N_IDENT whose resolved sym has decl == nil — pseudo-builtin
// callees (len/append/free/alloc/size/align/offset, seeded in
// checkinit L85-97 with decl=nil). Harec spells these as
// dedicated EXPR_* kinds (EXPR_LEN, EXPR_APPEND, EXPR_FREE,
// EXPR_ALLOC at ref/harec/src/check.c:2630/745/2443/...).
// Drew's δ (#19) retires the seeded-SK_FN-with-nil-decl hack.
// 3. N_IDENT at the LHS-of-N_DOT syntactic position — the bare
// name half of a member-access expr is a lookup target, not a
// value-producing sub-expression. Harec's EXPR_ACCESS stores the
// member as a string, not a node.
//
// `indot` tracks gate 3: true only when the immediate caller is an
// N_DOT recursing into its .lhs.
fn asserttyped(c: *checker, n: *node, indot: bool) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
let isexpr: bool =
k == nkind.N_INTLIT || k == nkind.N_FLOATLIT ||
k == nkind.N_STRLIT || k == nkind.N_RUNELIT ||
k == nkind.N_TRUE || k == nkind.N_FALSE ||
k == nkind.N_NIL || k == nkind.N_VOIDLIT ||
k == nkind.N_IDENT || k == nkind.N_BIN ||
k == nkind.N_UN || k == nkind.N_CALL ||
k == nkind.N_INDEX || k == nkind.N_CAST ||
k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT ||
k == nkind.N_RECV || k == nkind.N_DOT ||
k == nkind.N_SLICE || k == nkind.N_SPREAD ||
k == nkind.N_TUPLE || k == nkind.N_TRYPROP ||
k == nkind.N_TRYUNW || k == nkind.N_TYPETEST ||
k == nkind.N_TYPEASSERT || k == nkind.N_YIELD ||
k == nkind.N_MATCH;
let skip: bool = false;
if (isexpr && k == nkind.N_IDENT) {
if (indot) { skip = true; };
if (!skip) {
let s: *sym = scopelookup(c.cur, n.str);
if (s != nil) {
if (s.skind == skind.SK_USE) { skip = true; };
if (s.decl == nil) { skip = true; };
};
};
};
if (isexpr && !skip) {
if (n.type_ == nil) {
os.write(2, "asserttyped: ".ptr, 13u64);
let kn: str = nkname(k);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, " ".ptr, 1u64);
if (n.file.len > 0) {
os.write(2, n.file.ptr, n.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
};
if (n.str.len > 0) {
os.write(2, " '".ptr, 2u64);
os.write(2, n.str.ptr, n.str.len: u64);
os.write(2, "'".ptr, 1u64);
};
os.write(2, "\n".ptr, 1u64);
};
};
if (k == nkind.N_DOT) {
if (n.lhs != nil) { asserttyped(c, n.lhs, true); };
return;
};
if (n.attr != nil) { asserttyped(c, n.attr, false); };
if (n.lhs != nil) { asserttyped(c, n.lhs, false); };
if (n.rhs != nil) { asserttyped(c, n.rhs, false); };
if (n.cond != nil) { asserttyped(c, n.cond, false); };
if (n.body != nil) { asserttyped(c, n.body, false); };
if (n.els != nil) { asserttyped(c, n.els, false); };
let m: *node = n.list;
for (m != nil) {
asserttyped(c, m, false);
m = m.next;
};
};
export fn checkinit(c: *checker, tc: *tctx) void = {
c.tc = tc;
c.top = newscope(nil);
@@ -9808,6 +9953,17 @@ export fn checkfile(c: *checker, file: *node) void = {
};};};};
d = d.next;
};
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
// decl with its curmod set so asserttyped's gate lookups resolve
// against the same module context exprtype saw during pass 2.
d = file.list;
for (d != nil) {
c.curmod = declmod(file, d);
asserttyped(c, d, false);
d = d.next;
};
let empty: str;
c.curmod = empty;