Files
ww/lib/ww/typ.ww
Hojun-Cho 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

529 lines
18 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,
};
// ---- 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,
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. Linked-list shape mirrors
// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars
// over hash-table cleverness, and Sym/Scope already pay the FNV cost
// for the resolver pass.
type tinfocacheent = struct {
key: *node,
val: *tinfo,
cnext: *tinfocacheent,
};
// ---- 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,
tyf32: *tinfo,
tyf64: *tinfo,
tystr: *tinfo,
tyerr: *tinfo,
tynever: *tinfo,
tyuntypedint: *tinfo,
tyuntypedfloat: *tinfo,
tyuntypedstr: *tinfo,
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *tinfo,
tinfocache: *tinfocacheent,
};
// ---- 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.tyf32 = prim(tykind.TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(tykind.TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(tykind.TY_STR, "str", 16u64, 8u64); // sizelint-ok: SSoT for tystr (#64)
c.tyerr = prim(tykind.TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64);
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);
};
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;
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.
export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
let e: *tinfocacheent = c.tinfocache;
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 e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfocache})!;
c.tinfocache = 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_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); };
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; };
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_RUNE){ return true; };
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; };
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; };
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; };
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; };
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; };
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; };
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_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
};