// 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, 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, 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, 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.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, "", 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); }; 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_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); }; 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_SIZE) { 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_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 };