selfhost/cmd/wcc: collapse type-kind predicates onto n.type_ (A.6.3b, #46)

The node-keyed kind helpers (typeis8byteprimitive, isstrtype/raw,
isslicetype/raw, istaggedtype/raw, isfloattype, isf32type/raw,
isf64typeraw, isnullabletype) each re-walked TNAME aliases via
aliaslookup and peeled TBANG by hand — duplicating cstage's single-
peel kind predicates at the AST level. After A.6.2 every type-AST
kind these read is tinfo-stamped at check.ww L426-436, and
tinfofornode collapses N_TBANG (check.ww:1145-1152) and the TY_NAMED
chain, so each predicate folds to one tinfo read.

Six new tinfo helpers in lib/ww/typ.ww mirror their cstage SSoT
verbatim:

  typeisstr      — cstage cgen.c:159 `type_isstr`     (TY_STR / TY_UNTYPED_STR)
  typeisslice    — cstage cgen.c:174 `type_isslice`
  typeistagged   — cstage cgen.c:516 `type_istagged`
  typeisf32      — cstage cgen.c:188 `type_isf32`
  typeisnullable — cstage cgen.c:396 `type_isnullable` (reads tinfo.nullable
                                                       stamped at check.ww:1309-1318)
  typeis8byteprim — cstage cgen.c N_LET sz==8 ladder (slot-pad set)

Rule 9 carve-out per the A.6.3a precedent: each helper has a named
cstage counterpart; the wwstage shape mirrors it directly. The five
dead AST-walking variants (isstrtyperaw, isslicetyperaw,
istaggedtyperaw, isf32typeraw, isf64typeraw) are deleted; the five
remaining callsites (cgenstmt cglet / cgmlet str-routing, cgenexpr
cgdot tuple-field) graduate to the alias-aware isstrtype(c, t).

nullableptrtag stays AST-keyed for now — tinfofornode doesn't
populate TY_TAGGED.params (check.ww:1287-1337 sets size / align /
nullable but not the variant chain), so the tinfo equivalent of
cstage cgen.c:405 `nullable_ptr_tag` can't read params today. WHY
comment at the site cites #50 / A.6.3f as the graduation point,
alongside the variant-index work and the tparam-population glue.

Byte-identity (994/995) is the behavior gate; full `make test` green
at 133/133 confirms.
This commit is contained in:
2026-05-22 05:53:34 +09:00
parent 03e4718199
commit b8e5a921f8
6 changed files with 405 additions and 540 deletions

View File

@@ -867,72 +867,13 @@ fn typenameisunsigned(nm: str) bool = {
};
// typeis8byteprimitive — does this type take exactly one 8-byte
// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive
// padded up to 8) rather than a wider aggregate? Used by nkind.N_LET
// zero-init to mirror C cgen's "only zero if sz == 8 at the type
// level" rule. Strings (16), slices (24), tagged unions (>=16),
// tuples (16), structs (varies), arrays — all fall through to
// false here even when their *slot* rounds up to 8.
// slot rather than a wider aggregate? One-liner via typeis8byteprim
// (cstage N_LET sz==8 ladder SSoT). t.type_ is stamped at check.ww
// L426-436 for every type-AST kind callers reach. Collapsed onto the
// tinfo helper per A.6.3b (#46).
fn typeis8byteprimitive(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { return true; };
if (k == nkind.N_TFN) { return true; };
if (k == nkind.N_TCHAN) { return true; };
if (k == nkind.N_TSLICE) { return false; };
if (k == nkind.N_TARRAY) {
// C cgen (cmd/w6c/cgen.c:3317) zero-inits TY_ARRAY whenever
// its raw byte size is 8 — e.g. `[8]bool`, `[2]i32`, `[4]i16`,
// `[1]i64`. Mirror that here so the wwstage matches.
let lenn: *node = t.rhs;
let elemn: *node = t.lhs;
if (lenn == nil) { return false; };
if (lenn.kind != nkind.N_INTLIT) { return false; };
let elen: i64 = lenn.uval: i64;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
return (esz: i64 * elen) == 8i64;
};
if (k == nkind.N_TTUPLE) { return false; };
if (k == nkind.N_TTAGGED){ return false; };
// STATUS-3 #22: `!T` carries the error flag on T's underlying
// shape (cmd/wcc/check.c:290 resolve_type N_TBANG copies T's
// kind, just sets iserror). cstage's N_LET sizes off lu->kind,
// so `!void`/`!i32` land in the sz=8 default and `!str`/`!slice`
// keep their composite slot. Defer to the inner type so
// `let e: !void;` mirrors cstage's MOVQ $0 while `!str` falls
// through to the multi-word fill.
if (k == nkind.N_TBANG) { return typeis8byteprimitive(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return false; };
// Plain `void` slot: cstage sz=8 default → MOVQ $0. The let-
// decl is a phantom (a tagged-union variant tag carrier), but
// the slot is still 8B and zero-inits like any other prim.
if (streq(nm, "void")) { return true; };
// Struct alias: not a primitive even if the slot is 8B.
if (structlookup(c, nm) != nil) { return false; };
// Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...).
// All of these get slot-padded to 8 and zero-init in C.
if (primsize(nm) > 0) { return true; };
// STATUS-3 #22: alias to `!T` or to `void` (Hare-style error
// type / phantom variant). cstage resolves the alias and
// lands on sz=8 default. Follow through aliaslookup so
// `type invalid = !void;` and `type done = void;` zero-init.
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
return typeis8byteprimitive(c, aliased);
};
};
return false;
};
return false;
return typeis8byteprim(t.type_: *tinfo);
};
// elemissignedc — given an indexable type-AST (`*T`, `[]T`, `[N]T`),
@@ -2168,54 +2109,20 @@ fn collectstructs(c: *cgen, file: *node) void = {
};
};
// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the
// alias chain registered at file load.
fn isstrtyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return true; };
};
return false;
};
// isstrtype — alias-aware. Reads the stamped tinfo so `str`,
// `type alias = str`, `!str`, and chained aliases all route to the
// str-shaped slot. Cite cstage cgen.c:159 `type_isstr` SSoT.
// Collapsed onto typeisstr per A.6.3b (#46); the prior AST walker is
// reconstituted by typeisstr's TY_NAMED chase + tinfofornode's
// TBANG-unwrap (check.ww:1145).
fn isstrtype(c: *cgen, t: *node) bool = {
if (isstrtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isstrtyperaw(r)) { return true; };
// `parserr = !str` — `!T` aliases shouldn't hide their
// underlying type from str-routing. Unwrap and re-check.
if (r != nil) {
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (isstrtyperaw(inner)) { return true; };
if (inner != nil) {
let r2: *node = resolvetype(c, inner);
if (isstrtyperaw(r2)) { return true; };
};
};
};
return false;
};
fn isslicetyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TSLICE) { return true; };
return false;
return typeisstr(t.type_: *tinfo);
};
fn isslicetype(c: *cgen, t: *node) bool = {
if (isslicetyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isslicetyperaw(r);
};
fn istaggedtyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TTAGGED) { return true; };
return false;
return typeisslice(t.type_: *tinfo);
};
// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil
@@ -2336,64 +2243,31 @@ fn structparamsize(c: *cgen, t: *node) i32 = {
return si.totsize;
};
// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its
// underlying decl, then unwrap a leading N_TBANG so `type error =
// !(invalid | overflow);` is still recognised as tagged. Without the
// bang unwrap the prologue treats the param as scalar (8B), spilling
// only DI and losing the value-word SI; the match read of slot+8 then
// trails into saved BP.
// istaggedtype — alias-aware. Reads stamped tinfo so `T`,
// `type alias = (A|B)`, `type error = !(invalid|overflow)` all
// resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap
// (check.ww:1145) so we don't re-walk it here. Cite cstage cgen.c
// (`type_chase_named` + TY_TAGGED). Collapsed per A.6.3b (#46).
fn istaggedtype(c: *cgen, t: *node) bool = {
if (istaggedtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (istaggedtyperaw(r)) { return true; };
if (r != nil) {
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (istaggedtyperaw(inner)) { return true; };
if (inner != nil) {
let r2: *node = resolvetype(c, inner);
if (istaggedtyperaw(r2)) { return true; };
};
};
};
return false;
};
// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution.
fn isf32typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f32");
return typeistagged(t.type_: *tinfo);
};
fn isf64typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f64");
};
// isfloattype — f32 / f64 (and aliases of those). Used by cglet,
// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to
// dispatch the MOVSS/MOVSD-shaped paths.
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
// prologue. Collapsed per A.6.3b (#46).
export fn isfloattype(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (isf64typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isf32typeraw(r)) { return true; };
if (isf64typeraw(r)) { return true; };
return false;
if (t == nil) { return false; };
return typeisfloat(t.type_: *tinfo);
};
// isf32type — narrower predicate: true only for f32 (after alias
// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and
// the SS-variant arithmetic / cast opcodes.
// isf32type — narrower: true only for f32 (after alias chase). Cite
// cstage cgen.c:188 `type_isf32`. Picks MOVSS vs MOVSD and the SS-
// variant arithmetic / cast opcodes. Collapsed per A.6.3b (#46).
export fn isf32type(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isf32typeraw(r);
if (t == nil) { return false; };
return typeisf32(t.type_: *tinfo);
};
// exprfloatkind — classify an expression's value-class so callers can
@@ -2514,30 +2388,23 @@ export fn exprfloatkind(c: *cgen, n: *node) i32 = {
return 0;
};
// isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
// isnullabletype — `(*T | void)` one-word fold per Hare's
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
// two-variant shape matches. Collapsed per A.6.3b (#46).
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == nkind.N_TPTR);
let bptr: bool = (b.kind == nkind.N_TPTR);
let avoid: bool = (a.kind == nkind.N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == nkind.N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
return typeisnullable(t.type_: *tinfo);
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
// union. The void variant takes the other slot (0 or 1). AST-keyed
// for now: tinfofornode doesn't populate TY_TAGGED.params yet
// (check.ww:1287-1337 sets size/align/nullable but not the variant
// chain), so the tinfo equivalent of cstage cgen.c:405
// `nullable_ptr_tag` can't read params today. Graduates to a pure
// tinfo helper in A.6.3f (#50) alongside the variant-index work and
// the tparam-population glue.
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != nkind.N_TTAGGED) { return 0; };