Files
ww/lib/ww/typ.ww
Hojun-Cho 4b118fa8f8 cgen: B7 emitter elem chases + tools/peellint gate — #5 alias-arc cs side closed by construction
The last four raw `->under` reads outside the whitelist were the
static-DATA emitters' ELEMENT-type single peels (the outer type already
chased): emit_array_lit_bytes:14356, emit_strarray_data:14574,
emit_slice_data:14788, let_pre_intern:15088 -> type_chase_named.
:15088 is the :14574 row's label-order leg and must flip in the same
commit or _S_ labels intern in emit order, not decl order (the in-tree
comment at the site); the strarr row's byte-id is the coupling proof.

Behavior moves (ken B7 first-position oracle + impl pre-state, all
pre-observed at 05f7af7):
- [N]alias-struct + [N]alias-str globals graduate cs link-ERR
  ("undefined reference") -> 0/0 BYTE-ID (cs emits ww's DATAW).
- zero-consumer latent silence closed: a never-referenced
  2-level-elem-alias global silently lacked DATA (no reference, no
  link error); now emits, pinned by the byte-id cell.
- []alias-str diagnostic routing: the alias escaped the 3-way
  slice-of-{str,slice,tagged} fatal onto the downstream "not a
  foldable constant" text — now the intended 3-way text (== control).
- []alias-tagged DESIGNED NARROWING: the alias dodged the 3-way fatal
  ENTIRELY — cs silently accepted + RAN WRONG for reachable consumer
  shapes (review-verified at base: a len+payload-read probe exits 1;
  the len-only row was luck-correct). Now loud with the 3-way text;
  widen what the gate SEES, never what it ACCEPTS (B6-c2 precedent).
- kb7_slc/slc0 scalar legs byte-NEUTRAL (the synthesized-array
  choke-point already handled them); full kb corpus sweep: movers are
  exactly the two graduation shapes, nothing else.

tools/peellint (sizelint clone, dep of test/test-unit): character-scan
strips comments and string/char literals, then matches the under-token
accessor-spelling-wide — `->under`/`.under` in C (deref-dot is the
same peel), `.under` in ww, optional whitespace after the operator,
and the line-split continuation (operator at EOL, `under` next line).
Scope cmd/wcc + cmd/w6c + selfhost/cmd/wcc + lib/ww (lib/ww/typ.ww
ruled IN — it is type.c's ww mirror, the accessor layer itself);
`peel-ok`/`peellint-ok` annotations exempt a 10-line window. Green at
this tip = zero unwhitelisted raw peels survive; the gate lands in the
commit that deletes the last raw read (the-funnel-completing-commit-
carries-the-gate; sizelint precedent). Whitelist, 27 entries:
  cmd/wcc/type.c    :78 :141 construction, :162 chase body,
                    :180 :193 :214 recursive chase
  cmd/wcc/check.c   :102 :2572 resolve-state probes, :2586 construction
  cmd/w6c/cgen.c    :731 probe-cleared scan peel (B5-c1),
                    :813/:814 :834/:835 peel-ok #218 variant-match
  lib/ww/typ.ww     :316 construction, :374 :385 :410 :437 :447 :463
                    :475 :488 :514 recursive chase
  selfhost/cmd/wcc/cgenutil.ww :1302 chase body (tichase),
                    :2759 probe-cleared peel
  selfhost/cmd/wcc/check.ww    :1815 construction (peellint-ok)

Negative validation wired into 944_peellint_gate (B4 precedent):
re-introduced raw peel (C and ww spellings) REDS the lint; corrupted
annotation (peel-okk-…, token-bounded matcher) REDS the lint; the
check.ww:3683 "io.underread" prose, a code read of a longer field, and
comment-quoted tokens are pinned green regression rows; real tree must
lint clean. 944_alias_emit_b7_run pins all four emit paths
table-driven (14 rows / 36 checks) incl. ken's ww observation cells
(ww checker rejects slice-literal globals, "let: not assignable" —
unmoved; plain []str louds at ww's own emitslicedata 3-way, pinned by
the shared needle).

REVIEW AMENDMENT (reviewer-B7, fix-what-you-find): the frozen tip's
regex matcher passed five compiling evasion spellings green — `t ->
under` spacing, `t->`/EOL + `under` next-line (both stages; ww parses
`t.`/EOL too), C deref-dot `(*t).under`, ww `t. under`, and a string
literal containing a block-comment opener that blinded the regex
comment-strip for the rest of the file. The matcher is now a
character scan (comments + string/char literals stripped before
matching) with the widened token rule above; all six spellings are
pinned RED rows in 944_peellint_gate (checks 10 -> 16). The 10-line
annotation window stays as designed (a peel within an annotation's
window is exempt by construction — the window IS the exemption
mechanism). Lint + test bytes only; zero compiler-source bytes moved
in review.

What this does NOT close, said out loud (f2-ruling): a consumer that
never spells `under` at all — a switch on t->kind that simply never
peels — has no token for the lint to see. The accessor+lint closes the
WRONG-PEEL class (single-peel where chase was needed) by construction;
the NO-PEEL class is closed only at sites where classification routes
through the internalized chasing helpers, and contained elsewhere by
the acceptance-commit-carries-tripwires doctrine, which stays standing
for every future acceptance widening. The gate does not make alias
bugs impossible; it makes the four-times-burned shape unwritable.

Rule-11 note: forced fuse — the four conversions ARE the last raw-read
deletions; peellint cannot be green one commit earlier (consumer-graph
-forces-the-fuse precedent, #61).

Invariants: cs asm byte-NEUTRAL on the whole bootstrap corpus (five
mains + smoke, base-input pre==post); five mains cs==ww byte-id at
tip; _ww binary quartet bit-identical to the W2 baseline (ww changes
are comment-only annotation bytes — codegen-inert, proven by the md5
hold); w6c_ww+wwdump main.combined.ww regen'd via make, idempotent;
989 lib ratchet zero flips (31 byte-id / 9 pinned-divergent / 3
pinned-wwreject across 43 units); sizelint 0; peellint 0;
make test-unit "all 294 tests passed" (292 + the two new suites).
2026-06-06 06:05:46 +09:00

616 lines
22 KiB
Plaintext

// lib/ww/typ.ww — port of cmd/wcc/type.c.
//
// Status: full structural port. The C version uses module-globals for
// the primitive types (tyvoid, tyi32, …); ww doesn't have writable
// global storage yet, so we bundle the primitives into a `tctx` that
// the checker passes around explicitly. typesinit fills the tctx
// once per program.
package ww;
import os;
// ---- TypeKind ---------------------------------------------------------
// Numeric values must stay aligned with cmd/wcc/ww.h TypeKind so the
// next diff signal (typed-AST printer / cgen) can compare across the
// two implementations.
// Mirror of the C `TypeKind` enum in cmd/wcc/ww.h. Numeric values
// are explicit and must stay in sync — the selfhost selfcheck and
// typed-AST printers depend on matching numeric layout.
type tykind = enum i32 {
TY_NONE = 0,
TY_VOID = 1,
TY_BOOL = 2,
TY_RUNE = 3,
TY_I8 = 4,
TY_I16 = 5,
TY_I32 = 6,
TY_I64 = 7,
TY_U8 = 8,
TY_U16 = 9,
TY_U32 = 10,
TY_U64 = 11,
TY_UINT = 12,
TY_INT = 13,
TY_UINTPTR = 14,
TY_F32 = 15,
TY_F64 = 16,
TY_STR = 17,
TY_PTR = 18,
TY_SLICE = 19,
TY_ARRAY = 20,
TY_STRUCT = 21,
TY_FN = 22,
TY_CHAN = 23,
TY_NAMED = 24,
TY_TUPLE = 25,
TY_TAGGED = 26,
TY_ERR = 27,
TY_NEVER = 28,
TY_UNTYPED_INT = 29,
TY_UNTYPED_FLOAT = 30,
TY_UNTYPED_STR = 31,
TY_UNTYPED_RUNE = 32,
TY_UNTYPED_BOOL = 33,
TY_UNTYPED_NIL = 34,
// Tail-appended values keep prior TY_* stable for the byte-diff
// against cmd/wcc/ww.h.
TY_ENUM = 35,
TY_SIZE = 36, // #85 fold-1; mirrors TY_UINTPTR (8/8 amd64)
TY_OPAQUE = 37, // #108(a); abstract + unsized, behind indirection only
};
// #108(a): unsized sentinel for abstract types (tinfo.size / .align).
// Mirrors harec SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h
// :58) and cstage cmd/wcc/ww.h; not 0, so a bare opaque local can't
// fabricate a 0-byte slot. Value == U64_MAX.
def SIZE_UNDEFINED: u64 = 18446744073709551615;
// ---- tinfo / tfield / tparam -----------------------------------------
type tfield = struct {
name: str,
type_: *tinfo,
offset: u64,
tnext: *tfield,
};
type tparam = struct {
name: str,
type_: *tinfo,
// #61a: per-variant `!T` error mark for TY_TAGGED variants.
// cstage-MIRROR divergence: harec carries no per-variant flag —
// it models `!T` as a distinct STORAGE_ERROR type node
// (ref/harec/include/types.h:144, src/types.c:151-159
// type_is_error). wwstage tinfo has no iserror field
// (check.ww TTAGGED arm), so the bit rides the shared param
// struct instead, matching cstage Type.iserror semantics.
// Faithful STORAGE_ERROR-node port filed as #62.
iserror: bool,
tnext: *tparam,
};
// #57 A.6.3i-phase-1: tuple positional element. Distinct from tfield
// (named, struct member) per harec's split at ref/harec/include/types.h
// :109-115 (struct_field) vs :122-126 (type_tuple) — tuple positionals
// carry no name (positional only) and a separate next-link. Rule-12
// sea-of-stars mirrors Hare's structural choice; the empty-name idiom
// from #50's TTAGGED-on-tparam would conflate two semantic axes
// (variants can be named; positionals never can).
type ttupleelem = struct {
type_: *tinfo,
offset: u64,
tnext: *ttupleelem,
};
type tinfo = struct {
kind: tykind,
size: u64,
align: u64,
sub: *tinfo, // ptr/slice/array/chan element
alen: u64,
fields: *tfield,
params: *tparam,
tupleelems: *ttupleelem, // #57 A.6.3i-phase-1: TY_TUPLE
// positional chain (harec types.h:122-126
// `struct type_tuple`). Distinct slot from
// .fields so struct-member vs tuple-
// positional stay axis-separated.
ret: *tinfo,
variadic: i32,
nullable: i32, // #61 A.3: TY_TAGGED `(*T | void)` fold collapses to
// 8B ptr slot (null is the void variant). Mirrors
// cstage Type.nullable (cmd/wcc/ww.h:430-433).
name: str,
under: *tinfo,
resolving: i32, // #62/#69: TY_NAMED demand-resolution cycle guard.
// Mirrors cstage Type.resolving (cmd/wcc/ww.h)
// and harec idecl->in_progress (ref/harec/src/
// check.c:4767): set while the alias body
// resolves; a VALUE-position read of an
// in-progress named is a true type cycle and
// loud-rejects. Pointer positions never read
// size, so legal self-refs stay accepted.
slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`.
// `size` stays natural (Hare-faithful);
// `slotsize` carries the slot-padded width
// cgen's let/struct-field layout demands.
// For primitives/ptr/slice/chan/fn/str/tagged
// `slotsize == size`; struct + tuple + array
// of struct diverge — see check.ww tinfo-
// fornode + cgenutil.ww registerstruct.
// Pad-to-8 of narrow primitives in let slots
// still lives at slotsize()'s read site;
// graduating it here would break `[N]i32`
// stride (4*N stays natural).
};
// #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes
// tinfofornode lookups keyed by AST pointer. perf #18: the original
// flat prepend-only list made the cache-MISS scan O(N) per call ->
// O(N2) over a compile (91% of all wwstage instructions on a 5k-line
// input). Now a node-ptr hash index, mirroring sym.ww scope.buckets
// (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch
// only one bucket -> O(1) amortized. Identical lookup results (same
// *tinfo for the same node), so emitted asm is byte-identical.
type tinfocacheent = struct {
key: *node,
val: *tinfo,
cnext: *tinfocacheent,
};
// Tuning knob, NOT a type size (rule-13 N/A): power-of-two so the
// bucket index is a MASK, not a mod. ~5400 nodes on a big input ->
// well under one entry/bucket. Mirror of sym.ww:38 NBUCKETS (16),
// scaled up — sym's 16 would give ~340-deep chains here.
def NBUCKETS_TINFO: u64 = 8192u64;
// ---- tctx — the box of primitive types -------------------------------
type tctx = struct {
tyvoid: *tinfo,
tybool: *tinfo,
tyrune: *tinfo,
tyi8: *tinfo,
tyi16: *tinfo,
tyi32: *tinfo,
tyi64: *tinfo,
tyu8: *tinfo,
tyu16: *tinfo,
tyu32: *tinfo,
tyu64: *tinfo,
tyint: *tinfo,
tyuint: *tinfo,
tyuintptr: *tinfo,
tysize: *tinfo,
tyopaque: *tinfo,
tyf32: *tinfo,
tyf64: *tinfo,
tystr: *tinfo,
tyerr: *tinfo,
tynever: *tinfo,
tyuntypedint: *tinfo,
tyuntypedfloat: *tinfo,
tyuntypedstr: *tinfo,
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *tinfo,
tinfobuckets: **tinfocacheent, // length NBUCKETS_TINFO; node-ptr hash index
};
// ---- constructors -----------------------------------------------------
export fn newtype(k: tykind) *tinfo = {
let t: *tinfo = alloc(tinfo{kind=k, size=0u64, align=0u64, sub=nil, alen=0u64, fields=nil, params=nil, tupleelems=nil, ret=nil, variadic=0, nullable=0, name="", under=nil, slotsize=0u64})!;
return t;
};
fn prim(k: tykind, nm: str, sz: u64, al: u64) *tinfo = {
let t: *tinfo = newtype(k);
t.name = nm;
t.size = sz;
if (al > 0u64) { t.align = al; } else { t.align = sz; };
t.slotsize = sz;
return t;
};
export fn typesinit(c: *tctx) void = {
c.tyvoid = prim(tykind.TY_VOID, "void", 0u64, 1u64);
c.tybool = prim(tykind.TY_BOOL, "bool", 1u64, 1u64);
c.tyrune = prim(tykind.TY_RUNE, "rune", 4u64, 4u64);
c.tyi8 = prim(tykind.TY_I8, "i8", 1u64, 1u64);
c.tyi16 = prim(tykind.TY_I16, "i16", 2u64, 2u64);
c.tyi32 = prim(tykind.TY_I32, "i32", 4u64, 4u64);
c.tyi64 = prim(tykind.TY_I64, "i64", 8u64, 8u64);
c.tyu8 = prim(tykind.TY_U8, "u8", 1u64, 1u64);
c.tyu16 = prim(tykind.TY_U16, "u16", 2u64, 2u64);
c.tyu32 = prim(tykind.TY_U32, "u32", 4u64, 4u64);
c.tyu64 = prim(tykind.TY_U64, "u64", 8u64, 8u64);
c.tyint = prim(tykind.TY_INT, "int", 8u64, 8u64);
c.tyuint = prim(tykind.TY_UINT, "uint", 8u64, 8u64);
c.tyuintptr= prim(tykind.TY_UINTPTR, "uintptr", 8u64, 8u64);
c.tysize = prim(tykind.TY_SIZE, "size", 8u64, 8u64); // #85
c.tyf32 = prim(tykind.TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(tykind.TY_F64, "f64", 8u64, 8u64);
// str IS []u8: { *u8, len, cap } — 24B, 3-reg ABI (#1/Phase 3).
// Size sourced from a u8-slice's size (typeslice SSoT) so str and
// []u8 can never drift; no second hardcoded 24. Mirrors cstage
// type.c `type_slice(a, ty_u8)->size`.
//
// The slice tinfo MUST land in a local first: the inline form
// `typeslice(c.tyu8).size` triggers a cgen bug — `call().field`
// where the call returns a *pointer* emits no deref (it uses the
// returned pointer AS the field value), so tystr.size would become
// a heap address → runaway slot-size loops. Filed as task #6
// (cstage cgen.c N_DOT base=N_CALL-returning-pointer + wwstage
// cgdot mirror); retained here as a local until that lands.
let u8slice: *tinfo = typeslice(c.tyu8);
c.tystr = prim(tykind.TY_STR, "str", u8slice.size, 8u64);
c.tystr.sub = c.tyu8; // str IS []u8: element is u8 (Phase 2 F1)
c.tyerr = prim(tykind.TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64);
// #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) blocks a bare
// `let x: opaque` 0-byte slot; legal only behind indirection.
// Mirrors cstage type.c ty_opaque (harec types.c:1446).
c.tyopaque = prim(tykind.TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED);
c.tyuntypedint = prim(tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64);
c.tyuntypedfloat = prim(tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64);
c.tyuntypedstr = prim(tykind.TY_UNTYPED_STR, "untyped_str", 0u64, 1u64);
c.tyuntypedrune = prim(tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64);
c.tyuntypedbool = prim(tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64);
c.tyuntypednil = prim(tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64);
let tib: []*tinfocacheent = alloc([], NBUCKETS_TINFO)!; // mirror sym.ww:63
c.tinfobuckets = tib.ptr;
};
export fn typeptr(sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(tykind.TY_PTR);
t.sub = sub;
t.size = 8u64;
t.align = 8u64;
t.slotsize = 8u64;
return t;
};
export fn typeslice(sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(tykind.TY_SLICE);
t.sub = sub;
t.size = 24u64; // sizelint-ok: SSoT for slice header (#64)
t.align = 8u64;
t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64)
return t;
};
export fn typearray(sub: *tinfo, n: u64) *tinfo = {
let t: *tinfo = newtype(tykind.TY_ARRAY);
t.sub = sub;
t.alen = n;
if (sub != nil) {
t.size = sub.size * n;
t.align = sub.align;
// #61 A.5: ti.slotsize = stride * elen using the element's
// slot-padded width. Primitives have slotsize == size so
// `[N]i32` stride stays 4 (natural); structs have padded
// slotsize so `[N]Triplet` stride lifts to 16.
t.slotsize = sub.slotsize * n;
} else {
t.align = 1u64;
};
return t;
};
export fn typechan(sub: *tinfo) *tinfo = {
let t: *tinfo = newtype(tykind.TY_CHAN);
t.sub = sub;
t.size = 8u64;
t.align = 8u64;
t.slotsize = 8u64;
return t;
};
export fn typenamed(name: str, under: *tinfo) *tinfo = {
let t: *tinfo = newtype(tykind.TY_NAMED);
t.name = name;
t.under = under; // peel-ok: construction
if (under != nil) {
t.size = under.size;
t.align = under.align;
t.slotsize = under.slotsize;
};
return t;
};
// #61 audit §1.8 — A.1 infrastructure: tinfocache lookup/bind. Keyed
// by AST node-pointer so two different N_TNAME("i32") nodes get
// independent entries that both resolve to c.tyi32. Used by
// tinfofornode in check.ww; cgen still reads sizes via primtypesize
// until A.2+ graduates each walker family.
// Node ptrs are 8+-aligned, so the low 3-4 bits are always zero —
// shift right 4 before masking or every 16th bucket would cluster.
fn tinfobucket(key: *node) u64 = {
return ((key: u64) >> 4u64) & (NBUCKETS_TINFO - 1u64);
};
export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
let bi: u64 = tinfobucket(key);
let e: *tinfocacheent = c.tinfobuckets[bi];
for (e != nil) {
if (e.key == key) { return e.val; };
e = e.cnext;
};
return nil;
};
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let bi: u64 = tinfobucket(key);
let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfobuckets[bi]})!;
c.tinfobuckets[bi] = e;
};
// ---- predicates -------------------------------------------------------
export fn typeisint(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_I8) { return true; };
if (k == tykind.TY_I16) { return true; };
if (k == tykind.TY_I32) { return true; };
if (k == tykind.TY_I64) { return true; };
if (k == tykind.TY_U8) { return true; };
if (k == tykind.TY_U16) { return true; };
if (k == tykind.TY_U32) { return true; };
if (k == tykind.TY_U64) { return true; };
if (k == tykind.TY_INT) { return true; };
if (k == tykind.TY_UINT){ return true; };
if (k == tykind.TY_UINTPTR) { return true; };
if (k == tykind.TY_SIZE) { return true; };
if (k == tykind.TY_RUNE){ return true; };
if (k == tykind.TY_UNTYPED_INT) { return true; };
if (k == tykind.TY_UNTYPED_RUNE) { return true; };
if (k == tykind.TY_ENUM) { return typeisint(t.sub); };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisint(t.under); };
return false;
};
export fn typeisfloat(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_F32) { return true; };
if (k == tykind.TY_F64) { return true; };
if (k == tykind.TY_UNTYPED_FLOAT) { return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisfloat(t.under); };
return false;
};
export fn typeisnum(t: *tinfo) bool = {
if (typeisint(t)) { return true; };
return typeisfloat(t);
};
// TY_RUNE is unsigned: Unicode codepoint (0..0x10FFFF) zero-extends on
// sub-word load (MOVL, not MOVSXD). TY_ENUM recurses on .sub so a
// `type k = enum u32 {…}` reads as unsigned. Cite cstage type.c:178
// `type_isunsigned`; rule 10 keeps wwstage aligned down to cstage.
export fn typeisunsigned(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_U8) { return true; };
if (k == tykind.TY_U16) { return true; };
if (k == tykind.TY_U32) { return true; };
if (k == tykind.TY_U64) { return true; };
if (k == tykind.TY_UINT){ return true; };
if (k == tykind.TY_UINTPTR) { return true; };
if (k == tykind.TY_SIZE) { return true; };
if (k == tykind.TY_RUNE){ return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); };
if (k == tykind.TY_ENUM) { return typeisunsigned(t.sub); };
return false;
};
// typeissigned — does this type need sign-extension on a sub-word
// (1/2/4B) load? Mirrors cstage cgen.c:240 `fld_issigned`. Cgen-facing
// predicate (TY_BOOL is unsigned for storage purposes — 0/1 → MOVZBQ),
// so it doesn't simply mirror `!typeisunsigned`. Pair-of-`is*`
// convention follows ref/hare/types/ helpers.
export fn typeissigned(t: *tinfo) bool = {
if (t == nil) { return false; };
if (t.kind == tykind.TY_BOOL) { return false; };
if (typeisunsigned(t)) { return false; };
return typeisint(t);
};
// typeisstr — TY_STR (and TY_UNTYPED_STR for literals pre-default).
// Cite cstage cgen.c:159 `type_isstr` — single TY_NAMED peel, accepts
// the same untyped form. ww walks the .under chain so alias-of-alias
// (`type s2 = s1; type s1 = str;`) lands the same way.
export fn typeisstr(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_STR) { return true; };
if (k == tykind.TY_UNTYPED_STR) { return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisstr(t.under); };
return false;
};
// typeisslice — TY_SLICE. Cite cstage cgen.c:174 `type_isslice`.
export fn typeisslice(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_SLICE) { return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisslice(t.under); };
return false;
};
// typeistagged — TY_TAGGED (alias-aware). Cite cstage cgen.c:516
// `type_istagged` — same single-peel shape. The node-keyed wwstage
// helper this replaces also unwrapped a leading N_TBANG so
// `type error = !(invalid | overflow);` registered as tagged. Post-
// A.6.2 the TBANG unwrap is handled by tinfofornode (check.ww:1145-
// 1152 returns the inner tinfo unchanged) so we recover the cstage
// semantics with the bare kind check + NAMED chase.
export fn typeistagged(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_TAGGED) { return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeistagged(t.under); };
return false;
};
// typeisf32 — narrower-than-typeisfloat: only TY_F32 (after alias
// chase). Cite cstage cgen.c:188 `type_isf32`. Used to pick MOVSS vs
// MOVSD and the SS-variant arithmetic / cast opcodes.
export fn typeisf32(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_F32) { return true; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisf32(t.under); };
return false;
};
// typeisnullable — TY_TAGGED with the `(*T | void)` one-word fold.
// Cite cstage cgen.c:396 `type_isnullable`. The .nullable flag is
// stamped by tinfofornode (check.ww:1309-1318) when the two-variant
// shape matches.
export fn typeisnullable(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_TAGGED) { return t.nullable != 0; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeisnullable(t.under); };
return false;
};
// typeis8byteprim — does this type take exactly one 8-byte stack
// slot (ptr / fn / chan / 64-bit int / scalar primitive padded up to
// 8 / `[N]T` whose natural width is 8) rather than a wider aggregate?
// Mirrors the ladder cstage's cgen.c N_LET zero-init takes on `sz==8`
// (cmd/wcc/check.c sizing + cgen.c N_LET). The node-keyed wwstage
// helper this replaces predates tinfo and AST-walked TBANG / TNAME
// alias chains; tinfofornode now collapses TBANG (check.ww:1145) and
// TY_NAMED.under carries the chain, so the tinfo walk handles every
// shape the AST walker did.
export fn typeis8byteprim(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_PTR) { return true; };
if (k == tykind.TY_FN) { return true; };
if (k == tykind.TY_CHAN) { return true; };
if (k == tykind.TY_SLICE) { return false; };
if (k == tykind.TY_TUPLE) { return false; };
if (k == tykind.TY_TAGGED) { return false; };
if (k == tykind.TY_STR) { return false; };
if (k == tykind.TY_STRUCT) { return false; };
if (k == tykind.TY_ARRAY) { return t.size == 8u64; };
// peel-ok: recursive chase
if (k == tykind.TY_NAMED) { return typeis8byteprim(t.under); };
// Remaining: primitives (i8/u8/.../i64/u64/bool/rune/f32/f64/
// int/uint/uintptr) and TY_VOID. All slot-pad to 8 and zero-init
// in cstage's `sz==8` branch.
return true;
};
export fn typeisuntyped(t: *tinfo) bool = {
if (t == nil) { return false; };
let k: tykind = t.kind;
if (k == tykind.TY_UNTYPED_INT) { return true; };
if (k == tykind.TY_UNTYPED_FLOAT) { return true; };
if (k == tykind.TY_UNTYPED_STR) { return true; };
if (k == tykind.TY_UNTYPED_RUNE) { return true; };
if (k == tykind.TY_UNTYPED_BOOL) { return true; };
if (k == tykind.TY_UNTYPED_NIL) { return true; };
return false;
};
// typeeq — structural equality. Named types compare nominally.
export fn typeeq(a: *tinfo, b: *tinfo) bool = {
if (a == b) { return true; };
if (a == nil) { return false; };
if (b == nil) { return false; };
if (a.kind != b.kind) { return false; };
let k: tykind = a.kind;
if (k == tykind.TY_PTR) { return typeeq(a.sub, b.sub); };
if (k == tykind.TY_SLICE) { return typeeq(a.sub, b.sub); };
if (k == tykind.TY_CHAN) { return typeeq(a.sub, b.sub); };
if (k == tykind.TY_ARRAY) {
if (a.alen != b.alen) { return false; };
return typeeq(a.sub, b.sub);
};
if (k == tykind.TY_FN) {
if (a.variadic != b.variadic) { return false; };
if (!typeeq(a.ret, b.ret)) { return false; };
let pa: *tparam = a.params;
let pb: *tparam = b.params;
for (true) {
if (pa == nil) { if (pb == nil) { return true; }; return false; };
if (pb == nil) { return false; };
if (!typeeq(pa.type_, pb.type_)) { return false; };
pa = pa.tnext;
pb = pb.tnext;
};
return true;
};
if (k == tykind.TY_STRUCT) {
let fa: *tfield = a.fields;
let fb: *tfield = b.fields;
for (true) {
if (fa == nil) { if (fb == nil) { return true; }; return false; };
if (fb == nil) { return false; };
let na: str = fa.name;
let nb: str = fb.name;
if (na.len != nb.len) { return false; };
let i: i32 = 0;
for (i < na.len) {
if (na[i] != nb[i]) { return false; };
i += 1;
};
if (!typeeq(fa.type_, fb.type_)) { return false; };
fa = fa.tnext;
fb = fb.tnext;
};
return true;
};
if (k == tykind.TY_NAMED) { return false; }; // nominal: only same ptr
if (k == tykind.TY_TAGGED) {
// Structural: variant lists match position-by-position, and
// the nullable `(*T|void)` fold is part of identity. Mirrors
// cstage type_eq's TY_TAGGED arm (cmd/wcc/type.c:271-284);
// the missing branch let any two tagged unions compare equal
// (fell through to the primitive `return true`), which
// #218's cgvariantmatch structural fallback was the first
// caller to exercise.
if (a.nullable != b.nullable) { return false; };
let pa: *tparam = a.params;
let pb: *tparam = b.params;
for (true) {
if (pa == nil) { if (pb == nil) { return true; }; return false; };
if (pb == nil) { return false; };
if (!typeeq(pa.type_, pb.type_)) { return false; };
pa = pa.tnext;
pb = pb.tnext;
};
return true;
};
if (k == tykind.TY_TUPLE) {
let pa: *tparam = a.params;
let pb: *tparam = b.params;
for (true) {
if (pa == nil) { if (pb == nil) { return true; }; return false; };
if (pb == nil) { return false; };
if (!typeeq(pa.type_, pb.type_)) { return false; };
pa = pa.tnext;
pb = pb.tnext;
};
return true;
};
return true; // primitives match by kind alone
};