selfhost/cmd/wcc/check+test: fold size(T)/align(T)/offset(e.f) at check time

Mirror cstage cmd/wcc/check.c:907-960. Three typed-builtin
intercepts that cstage already had:

- size(T) — folds to a literal integer at check time from a
  newly-introduced astsize walker over the type AST. Mirrors the
  size computation in cstage resolve_type at check.c:286-528.
- align(T) — same, via astalign.
- offset(e.f) — folds the byte offset of field f in e's struct
  type via astoffset. Peels exactly one N_TPTR for `p.field`.

seedprimitives registers the three names as SK_FN nil; exprtype's
N_CALL arm gates on a same-module shadow check (per #23 alloc
precedent) and consumes the parser-planted type-expression arg.
The fold is in-place — foldtointlit mutates N_CALL into N_INTLIT
so cgen sees a plain integer. resolvewalk's N_CALL trigger
invokes exprtype so the fold fires from non-let contexts too
(e.g. inside `if (size(T) != …)`).

selfhost/test/smoke.ww gains a probe-8 block: size/align/offset
assertions across str, primitive widths, ptrs, slices, and
two structs (`point`, `mixalign`) covering both no-padding and
i8+i64 natural-align padding cases.

Known divergences NOT in #42 scope:
- size((*T|void)) ≠ 8 on the cstage nullable-ptr fold (#13 family,
  unreachable through current grammar).
- 8B-struct bare-let zero-init wwstage skip vs cstage emit (#59).
- Same-module shadow gate added here, cstage has none — sibling
  shape to #26 (free/append/len gates).

Closes the original chain that started with the user's call to
fix the structural debt — six precondition fixes (#51, #52, #53,
#55, #56, #50) landed before this fold could safely live in the
check pass. Unblocks #43 (sweep literal 16s → size(str)) and #1
(str → 24B becomes one line).
This commit is contained in:
2026-05-20 05:22:57 +09:00
parent c58d3511e8
commit 3ec944a67f
6 changed files with 902 additions and 0 deletions

View File

@@ -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; };

View File

@@ -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; };

View File

@@ -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; };

View File

@@ -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;
};

View File

@@ -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;
};