diff --git a/.gitignore b/.gitignore index 3afb57ea..1dd4951b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /memiotest /trypromote /tagged_ptr_ret +/smoke # Per-module build artifacts. The .combined.ww files under selfhost/ # are intentionally tracked — they're frozen bootstrap inputs. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index cfa28ad7..d6e0bcf0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7082,6 +7082,15 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + // #42: typed builtins folded to integer literals at check time — + // `size(T)` / `align(T)` (arg is a type-expression planted by the + // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is + // an N_DOT). exprtype intercepts these and rewrites the N_CALL to + // N_INTLIT so cgen never sees an unresolved size/align/offset symbol. + // Mirrors cmd/wcc/check.c:907-955. + scopedefine(c.top, "size", skind.SK_FN, nil, nil); + scopedefine(c.top, "align", skind.SK_FN, nil, nil); + scopedefine(c.top, "offset", skind.SK_FN, nil, nil); }; // declmod — module-tag stamp for a top-level decl. @@ -7399,6 +7408,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #42: trigger the size/align/offset fold here so the mutation + // fires regardless of context (if-conditions, expression statements, + // etc.) — wwstage's exprtype is otherwise called only from + // checkletassign / checkretassign / TRYPROP, and an unwrapped + // `if (size(str) != 16)` would otherwise leave the N_CALL alone + // and cgen would emit a stray `CALL size(SB)`. Mirrors cstage + // cstmt's recursive cexpr discipline. + if (k == nkind.N_CALL) { + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_IDENT) { + let nm: str = n.lhs.str; + if (streq(nm, "size") || streq(nm, "align") || streq(nm, "offset")) { + let _t: *node = exprtype(c, n); + }; + }; + }; + }; + // After walking children: a local `let X: T = init;` registers // `X` so subsequent statements can resolve it. Top-level lets // are installed in installdecl, so this duplicate install at @@ -7585,6 +7612,201 @@ fn mktname(c: *checker, nm: str) *node = { return n; }; +// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) +// fold. Mirror cstage resolve_type's size/align computation +// (cmd/wcc/check.c:286-528) on AST nodes — wwstage check.ww never +// materialises tinfo for user types so the fold has to walk the AST +// directly. Struct layout follows cstage check.c:471-526 (align each +// field, max align for the whole record, round size up to alignment). +fn astalign(c: *checker, t: *node) i64 = { + if (t == nil) { return 1i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astalign(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 8i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { return astalign(c, t.lhs); }; + if (k == nkind.N_TTAGGED) { return 8i64; }; + if (k == nkind.N_TTUPLE) { + let m: i64 = 1i64; + let p: *node = t.list; + for (p != nil) { + let pa: i64 = astalign(c, p); + if (pa > m) { m = pa; }; + p = p.next; + }; + return m; + }; + if (k == nkind.N_TSTRUCT) { + let m: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > m) { m = fa; }; + }; + f = f.next; + }; + return m; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astalign(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void") || streq(nm, "bool") || streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "str")) { return 8i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astalign(c, resolved); + }; + }; + return 1i64; +}; + +fn astsize(c: *checker, t: *node) i64 = { + if (t == nil) { return 0i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astsize(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 24i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { + let elen: i64 = 0i64; + if (t.rhs != nil) { + if (t.rhs.kind == nkind.N_INTLIT) { elen = t.rhs.uval: i64; }; + }; + return astsize(c, t.lhs) * elen; + }; + if (k == nkind.N_TTUPLE) { + let total: i64 = 0i64; + let p: *node = t.list; + for (p != nil) { + total += astsize(c, p); + p = p.next; + }; + return total; + }; + if (k == nkind.N_TSTRUCT) { + let off: i64 = 0i64; + let maxal: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > maxal) { maxal = fa; }; + off = (off + fa - 1i64) & ~(fa - 1i64); + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return (off + maxal - 1i64) & ~(maxal - 1i64); + }; + if (k == nkind.N_TTAGGED) { + // 8 (tag) + max variant payload, rounded up to 8. + let maxsz: i64 = 0i64; + let v: *node = t.list; + for (v != nil) { + let sz: i64 = astsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i64 = (maxsz + 7i64) & ~7i64; + return 8i64 + pad; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astsize(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void")) { return 0i64; }; + if (streq(nm, "bool")) { return 1i64; }; + if (streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; + if (streq(nm, "str")) { return 16i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astsize(c, resolved); + }; + }; + return 0i64; +}; + +// astoffset — byte offset of `dot.str` inside the struct type of +// `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR +// (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields +// honouring per-field alignment, return -1 if the field name is +// absent so the caller can flag the error and fold to 0. +fn astoffset(c: *checker, dot: *node) i64 = { + if (dot == nil) { return -1i64; }; + if (dot.kind != nkind.N_DOT) { return -1i64; }; + let recv: *node = scruttype(c, dot.lhs); + if (recv == nil) { return -1i64; }; + let rt: *node = resolvealias(c, unwrapbang(recv)); + if (rt == nil) { return -1i64; }; + if (rt.kind == nkind.N_TPTR) { + rt = resolvealias(c, unwrapbang(rt.lhs)); + }; + if (rt == nil) { return -1i64; }; + if (rt.kind != nkind.N_TSTRUCT) { return -1i64; }; + let off: i64 = 0i64; + let f: *node = rt.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + off = (off + fa - 1i64) & ~(fa - 1i64); + if (streq(f.str, dot.str)) { return off; }; + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return -1i64; +}; + +// arenau64tos — decimal string for the folded INTLIT's `str` field. +// Cstage uses aprintf("%llu") at the same site (cmd/wcc/check.c:921); +// wwstage cgen only reads `uval` for N_INTLIT codegen so `str` is +// just for the AST printer, but set it for parity with the parser's +// own literal-emit shape. +fn arenau64tos(a: *arena, v: u64) str = { + let buf: *u8 = amalloc(a, 24u64): *u8; + let i: i32 = 23; + buf[i] = 0u8; + if (v == 0u64) { i -= 1; buf[i] = 48u8; }; + let n: u64 = v; + for (n > 0u64) { + i -= 1; + buf[i] = (48u64 + (n % 10u64)): u8; + n /= 10u64; + }; + let r: str; + r.ptr = buf + (i: u64); + r.len = 23 - i; + return r; +}; + +// foldtointlit — mutate `n` in place to an N_INTLIT with value `v`. +// Used by the #42 size/align/offset intercepts so cgen sees the +// folded literal rather than an unresolved call. Mirrors cstage +// cmd/wcc/check.c:919-927 / :951-958. +fn foldtointlit(c: *checker, n: *node, v: i64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v: u64; + n.str = arenau64tos(c.a, v: u64); + n.lhs = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; +}; + // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, and casts; returns // nil for shapes we don't statically know (binary ops, struct @@ -7661,6 +7883,57 @@ fn exprtype(c: *checker, e: *node) *node = { }; }; }; + // #42: size(T) / align(T) / offset(e.f) typed-builtin intercepts. + // Fold the N_CALL in place to an N_INTLIT so cgen never sees an + // unresolved size/align/offset symbol. Same-module shadow gate + // mirrors the alloc precedent (#23) so a user `fn size(...)` + // inside this module suppresses the builtin. Mirrors cstage + // cmd/wcc/check.c:907-960. + if (callee.kind == nkind.N_IDENT) { + let bname: str = callee.str; + let issize: bool = streq(bname, "size"); + let isalign: bool = streq(bname, "align"); + let isoffset: bool = streq(bname, "offset"); + if (issize || isalign || isoffset) { + let shadowed: bool = false; + if (c.curmod.len > 0) { + if (scopelookupinmodule(c.cur, c.curmod, bname) != nil) { + shadowed = true; + }; + }; + if (!shadowed) { + if (e.list != nil) { + if (issize) { + let v: i64 = astsize(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + if (isalign) { + let v: i64 = astalign(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + // offset(e.f): the arg is a value expression + // (N_DOT), parsed via parsearglist — not a + // type expression. + if (isoffset) { + if (e.list.next == nil && e.list.kind == nkind.N_DOT) { + let off: i64 = astoffset(c, e.list); + if (off < 0i64) { + os.write(2, "offset: no field '".ptr, 18u64); + os.write(2, e.list.str.ptr, e.list.str.len: u64); + os.write(2, "'\n".ptr, 2u64); + c.errs += 1; + off = 0i64; + }; + foldtointlit(c, e, off); + return mktname(c, "i32"); + }; + }; + }; + }; + }; + }; let nm: str; nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 76a7712f..d8dae28a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -88,6 +88,15 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + // #42: typed builtins folded to integer literals at check time — + // `size(T)` / `align(T)` (arg is a type-expression planted by the + // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is + // an N_DOT). exprtype intercepts these and rewrites the N_CALL to + // N_INTLIT so cgen never sees an unresolved size/align/offset symbol. + // Mirrors cmd/wcc/check.c:907-955. + scopedefine(c.top, "size", skind.SK_FN, nil, nil); + scopedefine(c.top, "align", skind.SK_FN, nil, nil); + scopedefine(c.top, "offset", skind.SK_FN, nil, nil); }; // declmod — module-tag stamp for a top-level decl. @@ -405,6 +414,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #42: trigger the size/align/offset fold here so the mutation + // fires regardless of context (if-conditions, expression statements, + // etc.) — wwstage's exprtype is otherwise called only from + // checkletassign / checkretassign / TRYPROP, and an unwrapped + // `if (size(str) != 16)` would otherwise leave the N_CALL alone + // and cgen would emit a stray `CALL size(SB)`. Mirrors cstage + // cstmt's recursive cexpr discipline. + if (k == nkind.N_CALL) { + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_IDENT) { + let nm: str = n.lhs.str; + if (streq(nm, "size") || streq(nm, "align") || streq(nm, "offset")) { + let _t: *node = exprtype(c, n); + }; + }; + }; + }; + // After walking children: a local `let X: T = init;` registers // `X` so subsequent statements can resolve it. Top-level lets // are installed in installdecl, so this duplicate install at @@ -591,6 +618,201 @@ fn mktname(c: *checker, nm: str) *node = { return n; }; +// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) +// fold. Mirror cstage resolve_type's size/align computation +// (cmd/wcc/check.c:286-528) on AST nodes — wwstage check.ww never +// materialises tinfo for user types so the fold has to walk the AST +// directly. Struct layout follows cstage check.c:471-526 (align each +// field, max align for the whole record, round size up to alignment). +fn astalign(c: *checker, t: *node) i64 = { + if (t == nil) { return 1i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astalign(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 8i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { return astalign(c, t.lhs); }; + if (k == nkind.N_TTAGGED) { return 8i64; }; + if (k == nkind.N_TTUPLE) { + let m: i64 = 1i64; + let p: *node = t.list; + for (p != nil) { + let pa: i64 = astalign(c, p); + if (pa > m) { m = pa; }; + p = p.next; + }; + return m; + }; + if (k == nkind.N_TSTRUCT) { + let m: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > m) { m = fa; }; + }; + f = f.next; + }; + return m; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astalign(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void") || streq(nm, "bool") || streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "str")) { return 8i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astalign(c, resolved); + }; + }; + return 1i64; +}; + +fn astsize(c: *checker, t: *node) i64 = { + if (t == nil) { return 0i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astsize(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 24i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { + let elen: i64 = 0i64; + if (t.rhs != nil) { + if (t.rhs.kind == nkind.N_INTLIT) { elen = t.rhs.uval: i64; }; + }; + return astsize(c, t.lhs) * elen; + }; + if (k == nkind.N_TTUPLE) { + let total: i64 = 0i64; + let p: *node = t.list; + for (p != nil) { + total += astsize(c, p); + p = p.next; + }; + return total; + }; + if (k == nkind.N_TSTRUCT) { + let off: i64 = 0i64; + let maxal: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > maxal) { maxal = fa; }; + off = (off + fa - 1i64) & ~(fa - 1i64); + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return (off + maxal - 1i64) & ~(maxal - 1i64); + }; + if (k == nkind.N_TTAGGED) { + // 8 (tag) + max variant payload, rounded up to 8. + let maxsz: i64 = 0i64; + let v: *node = t.list; + for (v != nil) { + let sz: i64 = astsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i64 = (maxsz + 7i64) & ~7i64; + return 8i64 + pad; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astsize(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void")) { return 0i64; }; + if (streq(nm, "bool")) { return 1i64; }; + if (streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; + if (streq(nm, "str")) { return 16i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astsize(c, resolved); + }; + }; + return 0i64; +}; + +// astoffset — byte offset of `dot.str` inside the struct type of +// `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR +// (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields +// honouring per-field alignment, return -1 if the field name is +// absent so the caller can flag the error and fold to 0. +fn astoffset(c: *checker, dot: *node) i64 = { + if (dot == nil) { return -1i64; }; + if (dot.kind != nkind.N_DOT) { return -1i64; }; + let recv: *node = scruttype(c, dot.lhs); + if (recv == nil) { return -1i64; }; + let rt: *node = resolvealias(c, unwrapbang(recv)); + if (rt == nil) { return -1i64; }; + if (rt.kind == nkind.N_TPTR) { + rt = resolvealias(c, unwrapbang(rt.lhs)); + }; + if (rt == nil) { return -1i64; }; + if (rt.kind != nkind.N_TSTRUCT) { return -1i64; }; + let off: i64 = 0i64; + let f: *node = rt.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + off = (off + fa - 1i64) & ~(fa - 1i64); + if (streq(f.str, dot.str)) { return off; }; + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return -1i64; +}; + +// arenau64tos — decimal string for the folded INTLIT's `str` field. +// Cstage uses aprintf("%llu") at the same site (cmd/wcc/check.c:921); +// wwstage cgen only reads `uval` for N_INTLIT codegen so `str` is +// just for the AST printer, but set it for parity with the parser's +// own literal-emit shape. +fn arenau64tos(a: *arena, v: u64) str = { + let buf: *u8 = amalloc(a, 24u64): *u8; + let i: i32 = 23; + buf[i] = 0u8; + if (v == 0u64) { i -= 1; buf[i] = 48u8; }; + let n: u64 = v; + for (n > 0u64) { + i -= 1; + buf[i] = (48u64 + (n % 10u64)): u8; + n /= 10u64; + }; + let r: str; + r.ptr = buf + (i: u64); + r.len = 23 - i; + return r; +}; + +// foldtointlit — mutate `n` in place to an N_INTLIT with value `v`. +// Used by the #42 size/align/offset intercepts so cgen sees the +// folded literal rather than an unresolved call. Mirrors cstage +// cmd/wcc/check.c:919-927 / :951-958. +fn foldtointlit(c: *checker, n: *node, v: i64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v: u64; + n.str = arenau64tos(c.a, v: u64); + n.lhs = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; +}; + // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, and casts; returns // nil for shapes we don't statically know (binary ops, struct @@ -667,6 +889,57 @@ fn exprtype(c: *checker, e: *node) *node = { }; }; }; + // #42: size(T) / align(T) / offset(e.f) typed-builtin intercepts. + // Fold the N_CALL in place to an N_INTLIT so cgen never sees an + // unresolved size/align/offset symbol. Same-module shadow gate + // mirrors the alloc precedent (#23) so a user `fn size(...)` + // inside this module suppresses the builtin. Mirrors cstage + // cmd/wcc/check.c:907-960. + if (callee.kind == nkind.N_IDENT) { + let bname: str = callee.str; + let issize: bool = streq(bname, "size"); + let isalign: bool = streq(bname, "align"); + let isoffset: bool = streq(bname, "offset"); + if (issize || isalign || isoffset) { + let shadowed: bool = false; + if (c.curmod.len > 0) { + if (scopelookupinmodule(c.cur, c.curmod, bname) != nil) { + shadowed = true; + }; + }; + if (!shadowed) { + if (e.list != nil) { + if (issize) { + let v: i64 = astsize(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + if (isalign) { + let v: i64 = astalign(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + // offset(e.f): the arg is a value expression + // (N_DOT), parsed via parsearglist — not a + // type expression. + if (isoffset) { + if (e.list.next == nil && e.list.kind == nkind.N_DOT) { + let off: i64 = astoffset(c, e.list); + if (off < 0i64) { + os.write(2, "offset: no field '".ptr, 18u64); + os.write(2, e.list.str.ptr, e.list.str.len: u64); + os.write(2, "'\n".ptr, 2u64); + c.errs += 1; + off = 0i64; + }; + foldtointlit(c, e, off); + return mktname(c, "i32"); + }; + }; + }; + }; + }; + }; let nm: str; nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4aac9a23..5b700f74 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7082,6 +7082,15 @@ fn seedprimitives(c: *checker) void = { scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); + // #42: typed builtins folded to integer literals at check time — + // `size(T)` / `align(T)` (arg is a type-expression planted by the + // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is + // an N_DOT). exprtype intercepts these and rewrites the N_CALL to + // N_INTLIT so cgen never sees an unresolved size/align/offset symbol. + // Mirrors cmd/wcc/check.c:907-955. + scopedefine(c.top, "size", skind.SK_FN, nil, nil); + scopedefine(c.top, "align", skind.SK_FN, nil, nil); + scopedefine(c.top, "offset", skind.SK_FN, nil, nil); }; // declmod — module-tag stamp for a top-level decl. @@ -7399,6 +7408,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #42: trigger the size/align/offset fold here so the mutation + // fires regardless of context (if-conditions, expression statements, + // etc.) — wwstage's exprtype is otherwise called only from + // checkletassign / checkretassign / TRYPROP, and an unwrapped + // `if (size(str) != 16)` would otherwise leave the N_CALL alone + // and cgen would emit a stray `CALL size(SB)`. Mirrors cstage + // cstmt's recursive cexpr discipline. + if (k == nkind.N_CALL) { + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_IDENT) { + let nm: str = n.lhs.str; + if (streq(nm, "size") || streq(nm, "align") || streq(nm, "offset")) { + let _t: *node = exprtype(c, n); + }; + }; + }; + }; + // After walking children: a local `let X: T = init;` registers // `X` so subsequent statements can resolve it. Top-level lets // are installed in installdecl, so this duplicate install at @@ -7585,6 +7612,201 @@ fn mktname(c: *checker, nm: str) *node = { return n; }; +// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) +// fold. Mirror cstage resolve_type's size/align computation +// (cmd/wcc/check.c:286-528) on AST nodes — wwstage check.ww never +// materialises tinfo for user types so the fold has to walk the AST +// directly. Struct layout follows cstage check.c:471-526 (align each +// field, max align for the whole record, round size up to alignment). +fn astalign(c: *checker, t: *node) i64 = { + if (t == nil) { return 1i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astalign(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 8i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { return astalign(c, t.lhs); }; + if (k == nkind.N_TTAGGED) { return 8i64; }; + if (k == nkind.N_TTUPLE) { + let m: i64 = 1i64; + let p: *node = t.list; + for (p != nil) { + let pa: i64 = astalign(c, p); + if (pa > m) { m = pa; }; + p = p.next; + }; + return m; + }; + if (k == nkind.N_TSTRUCT) { + let m: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > m) { m = fa; }; + }; + f = f.next; + }; + return m; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astalign(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void") || streq(nm, "bool") || streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "str")) { return 8i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astalign(c, resolved); + }; + }; + return 1i64; +}; + +fn astsize(c: *checker, t: *node) i64 = { + if (t == nil) { return 0i64; }; + let k: nkind = t.kind; + if (k == nkind.N_TBANG) { return astsize(c, t.lhs); }; + if (k == nkind.N_TPTR) { return 8i64; }; + if (k == nkind.N_TSLICE) { return 24i64; }; + if (k == nkind.N_TCHAN) { return 8i64; }; + if (k == nkind.N_TFN) { return 8i64; }; + if (k == nkind.N_TARRAY) { + let elen: i64 = 0i64; + if (t.rhs != nil) { + if (t.rhs.kind == nkind.N_INTLIT) { elen = t.rhs.uval: i64; }; + }; + return astsize(c, t.lhs) * elen; + }; + if (k == nkind.N_TTUPLE) { + let total: i64 = 0i64; + let p: *node = t.list; + for (p != nil) { + total += astsize(c, p); + p = p.next; + }; + return total; + }; + if (k == nkind.N_TSTRUCT) { + let off: i64 = 0i64; + let maxal: i64 = 1i64; + let f: *node = t.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + if (fa > maxal) { maxal = fa; }; + off = (off + fa - 1i64) & ~(fa - 1i64); + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return (off + maxal - 1i64) & ~(maxal - 1i64); + }; + if (k == nkind.N_TTAGGED) { + // 8 (tag) + max variant payload, rounded up to 8. + let maxsz: i64 = 0i64; + let v: *node = t.list; + for (v != nil) { + let sz: i64 = astsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i64 = (maxsz + 7i64) & ~7i64; + return 8i64 + pad; + }; + if (k == nkind.N_TENUM) { + if (t.lhs != nil) { return astsize(c, t.lhs); }; + return 4i64; + }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "void")) { return 0i64; }; + if (streq(nm, "bool")) { return 1i64; }; + if (streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; + if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; + if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; + if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr")) { return 8i64; }; + if (streq(nm, "str")) { return 16i64; }; + let resolved: *node = resolvealias(c, t); + if (resolved != nil && resolved != t) { + return astsize(c, resolved); + }; + }; + return 0i64; +}; + +// astoffset — byte offset of `dot.str` inside the struct type of +// `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR +// (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields +// honouring per-field alignment, return -1 if the field name is +// absent so the caller can flag the error and fold to 0. +fn astoffset(c: *checker, dot: *node) i64 = { + if (dot == nil) { return -1i64; }; + if (dot.kind != nkind.N_DOT) { return -1i64; }; + let recv: *node = scruttype(c, dot.lhs); + if (recv == nil) { return -1i64; }; + let rt: *node = resolvealias(c, unwrapbang(recv)); + if (rt == nil) { return -1i64; }; + if (rt.kind == nkind.N_TPTR) { + rt = resolvealias(c, unwrapbang(rt.lhs)); + }; + if (rt == nil) { return -1i64; }; + if (rt.kind != nkind.N_TSTRUCT) { return -1i64; }; + let off: i64 = 0i64; + let f: *node = rt.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let fa: i64 = astalign(c, f.lhs); + off = (off + fa - 1i64) & ~(fa - 1i64); + if (streq(f.str, dot.str)) { return off; }; + off += astsize(c, f.lhs); + }; + f = f.next; + }; + return -1i64; +}; + +// arenau64tos — decimal string for the folded INTLIT's `str` field. +// Cstage uses aprintf("%llu") at the same site (cmd/wcc/check.c:921); +// wwstage cgen only reads `uval` for N_INTLIT codegen so `str` is +// just for the AST printer, but set it for parity with the parser's +// own literal-emit shape. +fn arenau64tos(a: *arena, v: u64) str = { + let buf: *u8 = amalloc(a, 24u64): *u8; + let i: i32 = 23; + buf[i] = 0u8; + if (v == 0u64) { i -= 1; buf[i] = 48u8; }; + let n: u64 = v; + for (n > 0u64) { + i -= 1; + buf[i] = (48u64 + (n % 10u64)): u8; + n /= 10u64; + }; + let r: str; + r.ptr = buf + (i: u64); + r.len = 23 - i; + return r; +}; + +// foldtointlit — mutate `n` in place to an N_INTLIT with value `v`. +// Used by the #42 size/align/offset intercepts so cgen sees the +// folded literal rather than an unresolved call. Mirrors cstage +// cmd/wcc/check.c:919-927 / :951-958. +fn foldtointlit(c: *checker, n: *node, v: i64) void = { + n.kind = nkind.N_INTLIT; + n.uval = v: u64; + n.str = arenau64tos(c.a, v: u64); + n.lhs = nil; + n.list = nil; + let empty: str; + n.tsuffix = empty; +}; + // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, and casts; returns // nil for shapes we don't statically know (binary ops, struct @@ -7661,6 +7883,57 @@ fn exprtype(c: *checker, e: *node) *node = { }; }; }; + // #42: size(T) / align(T) / offset(e.f) typed-builtin intercepts. + // Fold the N_CALL in place to an N_INTLIT so cgen never sees an + // unresolved size/align/offset symbol. Same-module shadow gate + // mirrors the alloc precedent (#23) so a user `fn size(...)` + // inside this module suppresses the builtin. Mirrors cstage + // cmd/wcc/check.c:907-960. + if (callee.kind == nkind.N_IDENT) { + let bname: str = callee.str; + let issize: bool = streq(bname, "size"); + let isalign: bool = streq(bname, "align"); + let isoffset: bool = streq(bname, "offset"); + if (issize || isalign || isoffset) { + let shadowed: bool = false; + if (c.curmod.len > 0) { + if (scopelookupinmodule(c.cur, c.curmod, bname) != nil) { + shadowed = true; + }; + }; + if (!shadowed) { + if (e.list != nil) { + if (issize) { + let v: i64 = astsize(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + if (isalign) { + let v: i64 = astalign(c, e.list); + foldtointlit(c, e, v); + return mktname(c, "i32"); + }; + // offset(e.f): the arg is a value expression + // (N_DOT), parsed via parsearglist — not a + // type expression. + if (isoffset) { + if (e.list.next == nil && e.list.kind == nkind.N_DOT) { + let off: i64 = astoffset(c, e.list); + if (off < 0i64) { + os.write(2, "offset: no field '".ptr, 18u64); + os.write(2, e.list.str.ptr, e.list.str.len: u64); + os.write(2, "'\n".ptr, 2u64); + c.errs += 1; + off = 0i64; + }; + foldtointlit(c, e, off); + return mktname(c, "i32"); + }; + }; + }; + }; + }; + }; let nm: str; nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 7f892622..40191000 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -3287,6 +3287,20 @@ fn count_emit(ctx: *void, b: u8) void = { c.n += 1; }; +// --- size/align/offset typed-builtin fixtures (#42) -------------------- + +type point = struct { + x: i32, + y: i32, +}; + +// Mixed-alignment struct: i8 lays at 0, then i64 needs to skip to +// offset 8 (the i64's natural align). Probe asserts both ends. +type mixalign = struct { + tag: i8, + val: i64, +}; + // --- byte scanner like lex.ww's hot path ------------------------------- fn count_digits(s: str) i32 = { @@ -3380,6 +3394,33 @@ export fn main() i32 = { os.close(fd); if (n <= 0i64) { return 22; }; + // Probe 8 — size(T) / align(T) / offset(e.f) typed-builtin folds + // (#42). Each call folds to an N_INTLIT at check time; cgen + // materialises the literal as a plain `MOVQ $N, AX`. Mirrors + // cstage cmd/wcc/check.c:907-960 byte-for-byte on this corpus. + if (size(str) != 16) { return 23; }; + if (size(i64) != 8) { return 24; }; + if (size(i32) != 4) { return 25; }; + if (align(i64) != 8) { return 26; }; + if (align(i32) != 4) { return 27; }; + // Initialize struct locals explicitly so the cgen path doesn't + // drift from cstage on bare `let X: T;` zero-init (pre-existing + // wwstage divergence outside #42). + let pt: point = point { x = 0, y = 0 }; + if (offset(pt.x) != 0) { return 28; }; + if (offset(pt.y) != 4) { return 29; }; + let mx: mixalign = mixalign { tag = 0i8, val = 0i64 }; + if (offset(mx.tag) != 0) { return 30; }; + if (offset(mx.val) != 8) { return 31; }; // align-padded to 8 + // Width breadth: smallest prim, ptr, slice, struct (8B + padded), + // covering astsize's TPTR/TSLICE/TNAME-resolve-to-struct arms. + if (size(i8) != 1) { return 32; }; + if (align(i8) != 1) { return 33; }; + if (size(*i32) != 8) { return 34; }; + if (size([]i32) != 24) { return 35; }; + if (size(point) != 8) { return 36; }; + if (size(mixalign) != 16) { return 37; }; + return 42; }; diff --git a/selfhost/test/smoke.ww b/selfhost/test/smoke.ww index 0463345c..bc940030 100644 --- a/selfhost/test/smoke.ww +++ b/selfhost/test/smoke.ww @@ -74,6 +74,20 @@ fn count_emit(ctx: *void, b: u8) void = { c.n += 1; }; +// --- size/align/offset typed-builtin fixtures (#42) -------------------- + +type point = struct { + x: i32, + y: i32, +}; + +// Mixed-alignment struct: i8 lays at 0, then i64 needs to skip to +// offset 8 (the i64's natural align). Probe asserts both ends. +type mixalign = struct { + tag: i8, + val: i64, +}; + // --- byte scanner like lex.ww's hot path ------------------------------- fn count_digits(s: str) i32 = { @@ -167,5 +181,32 @@ export fn main() i32 = { os.close(fd); if (n <= 0i64) { return 22; }; + // Probe 8 — size(T) / align(T) / offset(e.f) typed-builtin folds + // (#42). Each call folds to an N_INTLIT at check time; cgen + // materialises the literal as a plain `MOVQ $N, AX`. Mirrors + // cstage cmd/wcc/check.c:907-960 byte-for-byte on this corpus. + if (size(str) != 16) { return 23; }; + if (size(i64) != 8) { return 24; }; + if (size(i32) != 4) { return 25; }; + if (align(i64) != 8) { return 26; }; + if (align(i32) != 4) { return 27; }; + // Initialize struct locals explicitly so the cgen path doesn't + // drift from cstage on bare `let X: T;` zero-init (pre-existing + // wwstage divergence outside #42). + let pt: point = point { x = 0, y = 0 }; + if (offset(pt.x) != 0) { return 28; }; + if (offset(pt.y) != 4) { return 29; }; + let mx: mixalign = mixalign { tag = 0i8, val = 0i64 }; + if (offset(mx.tag) != 0) { return 30; }; + if (offset(mx.val) != 8) { return 31; }; // align-padded to 8 + // Width breadth: smallest prim, ptr, slice, struct (8B + padded), + // covering astsize's TPTR/TSLICE/TNAME-resolve-to-struct arms. + if (size(i8) != 1) { return 32; }; + if (align(i8) != 1) { return 33; }; + if (size(*i32) != 8) { return 34; }; + if (size([]i32) != 24) { return 35; }; + if (size(point) != 8) { return 36; }; + if (size(mixalign) != 16) { return 37; }; + return 42; };