From ebcc8f2d09e7d0a4d699a38c7afd3e1c10849c96 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 15:39:39 +0900 Subject: [PATCH] =?UTF-8?q?wcc/cgen:=20lift=20helpers=E2=86=92cgen=5Futil,?= =?UTF-8?q?=20fn/file=E2=86=92cgen=5Fdecl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 6 +- selfhost/cmd/w6c/main.combined.ww | 2204 +++++++++++++------------- selfhost/cmd/wcc/cgen.ww | 1084 +------------ selfhost/cmd/wcc/cgen_decl.ww | 252 +++ selfhost/cmd/wcc/cgen_util.ww | 864 ++++++++++ selfhost/cmd/wwdump/main.combined.ww | 2204 +++++++++++++------------- 6 files changed, 3372 insertions(+), 3242 deletions(-) create mode 100644 selfhost/cmd/wcc/cgen_decl.ww create mode 100644 selfhost/cmd/wcc/cgen_util.ww diff --git a/Makefile b/Makefile index 81f3da4d..e3580483 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,8 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ selfhost/cmd/wcc/parse.ww selfhost/cmd/wcc/typ.ww \ selfhost/cmd/wcc/sym.ww selfhost/cmd/wcc/check.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgen_expr.ww \ - selfhost/cmd/wcc/cgen_stmt.ww \ + selfhost/cmd/wcc/cgen_stmt.ww selfhost/cmd/wcc/cgen_util.ww \ + selfhost/cmd/wcc/cgen_decl.ww \ lib/os/os.ww lib/strconv/strconv.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @@ -127,7 +128,8 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ selfhost/cmd/wcc/parse.ww selfhost/cmd/wcc/typ.ww \ selfhost/cmd/wcc/sym.ww selfhost/cmd/wcc/check.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgen_expr.ww \ - selfhost/cmd/wcc/cgen_stmt.ww \ + selfhost/cmd/wcc/cgen_stmt.ww selfhost/cmd/wcc/cgen_util.ww \ + selfhost/cmd/wcc/cgen_decl.ww \ lib/os/os.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b2a6734e..c9707f71 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -3637,6 +3637,872 @@ export fn checkfile(c: *checker, file: *node) void = { }; }; +// MODULE: wcc +// selfhost/cmd/wcc/cgen_util.ww — split out of cgen.ww. +// +// General helpers used across cgen_expr / cgen_stmt / cgen_decl: +// - pushargsrev: per-call arg pushing +// - type predicates: isstr*/isslice*/istagged*/nodeis* families +// - field ops: fieldloadop, fieldstoreop +// - index helpers: indexbaseesz, dotinnerstructptr, elemsizeof +// - slot sizing: structlookup, primsize, slotsize, fieldsize, +// registerstruct, collectstructs +// - rhs helpers: rhstargetname, taggedvariantindex +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_util;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- expression cgen ------------------------------------------------- + +// pushargsrev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn pushargsrev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = pushargsrev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let baselocal: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + }; + }; + // base address → push + if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emitline("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let lenn: *node = tn.rhs; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { + emitline("\tMOVQ\t$"); + emituint(lenn.uval); + emitline(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + }; + };};}; + }; + } else { + emitline("\tMOVQ\t$0, AX\n"); + };}; + emitline("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emitline("\tMOVQ\t$0, AX\n"); }; + emitline("\tPOPQ\tBX\n"); // hi + emitline("\tPOPQ\tCX\n"); // base + emitline("\tMOVQ\tBX, DX\n"); // DX = hi + emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emitline("\tPUSHQ\tDX\n"); // cap + emitline("\tPUSHQ\tDX\n"); // len + emitline("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + emitline("\tMOVQ\t"); + emitoff((off + 16): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (nodeisslice(c, arg)) { + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (nodeisstr(c, arg)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emitline("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn nodeisslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return isslicetype(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// nodeisstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn nodeisstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnretlookup(c, cnm); + return isstrtype(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + // Chained dot (`p.foo.bar`): use dotinnerstructptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return isstrtype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return isstrtype(c, n.rhs); + }; + return false; +}; + +// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn typenameisunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn typenodeisunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; + return false; +}; + +// typeis8byteprimitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn typeis8byteprimitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (structlookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (primsize(nm) > 0) { return true; }; + return false; + }; + return false; +}; + +// typenameissigned — true for i8/i16/i32/i64/int/rune. +fn typenameissigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// fieldloadop — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn fieldloadop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (typenameissigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// fieldstoreop — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn fieldstoreop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// indexbaseesz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn indexbaseesz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = localfindnode(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let innert: *node = tn; + if (tn.kind == N_TPTR) { innert = tn.lhs; }; + if (innert == nil) { return 8; }; + if (innert.kind == N_TNAME) { + if (streq(innert.str, "str")) { return 1; }; + }; + if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return 8; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = primsize(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dotinnerstructptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let baset: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { baset = tn; }; + if (tn.kind == N_TPTR) { baset = tn.lhs; }; + } else { if (base.kind == N_DOT) { + baset = dotinnerstructptr(c, base); + };}; + if (baset == nil) { return nil; }; + if (baset.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *structinfo = structlookup(c, baset.str); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elemsizeof — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elemsizeof(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). primsize returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// nodeisunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn nodeisunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return typenodeisunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = localfindnode(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return typenodeisunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; + if (k == N_BIN) { + if (nodeisunsigned(c, n.lhs)) { return true; }; + return nodeisunsigned(c, n.rhs); + }; + if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C w6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return typenodeisunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; + return false; +}; +// ---- type-driven slot sizing ---------------------------------------- + +fn structlookup(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// primsize — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn primsize(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slotsize(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let lenn: *node = typ_n.rhs; + let elemn: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == N_TNAME) { + let en: str = elemn.str; + let ps: i32 = primsize(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slotsize(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// registerstruct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slotsize logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn fieldsize(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slotsize's TARRAY branch. + let lenn: *node = tnode.rhs; + let elemn: *node = tnode.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = fieldsize(c, elemn); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { + let si: *structinfo = amalloc(c.a, 64u64): *structinfo; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *fieldinfo = nil; + let tail: *fieldinfo = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = fieldsize(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collectstructs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + registerstruct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn isstrtyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn isstrtype(c: *cgen, t: *node) bool = { + if (isstrtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isstrtyperaw(r); +}; + +fn isslicetyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn isslicetype(c: *cgen, t: *node) bool = { + if (isslicetyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isslicetyperaw(r); +}; + +fn istaggedtype(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhstargetname — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhstargetname(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// taggedvariantindex — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let wantname: str = rhstargetname(c, rhs); + if (wantname.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, wantname)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let wantstr: bool = nodeisstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let visstr: bool = false; + if (v.kind == N_TNAME) { + if (isstrtype(c, v)) { visstr = true; }; + }; + if (visstr == wantstr) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgen_expr.ww — split out of cgen.ww. // @@ -5071,6 +5937,260 @@ fn cgstmt(c: *cgen, n: *node) void = { }; +// MODULE: wcc +// selfhost/cmd/wcc/cgen_decl.ww — split out of cgen.ww. +// +// Houses the top-level emission glue: +// - scanlocals: frame pre-scan that counts each local `let` +// - cgfnparams: parameter spilling per SysV +// - cgfn: fn prologue + body + epilogue +// - cgfile: file-level entry (the exported driver) +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_decl;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scanlocals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match localadd's rounding: < 8 bumps to 8, then 8-align. + // scanlocals must agree with localadd or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. Same-name re-declarations share the first + // slot (see scanseenmark / localadd). + if (!scanseenmark(c, n.str)) { + let sz: i32 = slotsize(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + }; + // Match-arm binding (`case let v: T => ...`) gets a slot too. + // Crucially we do NOT dedup these against c.locals: C cgen + // handles a match as an expression with a by-value locals copy, + // so two separate matches in the same function each allocate + // their `v`/`e` slots fresh. Treating these as deduped would + // shrink the frame below what localadd then bumps it to. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (isstrtype(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; + if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; + if (n.cond != nil) { total += scanlocals(c, n.cond); }; + if (n.body != nil) { total += scanlocals(c, n.body); }; + if (n.els != nil) { total += scanlocals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scanlocals(c, m); + m = m.next; + }; + }; + return total; +}; + + +// ---- function-level cgen --------------------------------------------- + +fn cgfnparams(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (istaggedtype(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isslicetype(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isstrtype(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = localadd(c, nm, 16, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + } else { + let off: i32 = localadd(c, nm, 8, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgeninit(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emitline("TEXT "); + if (fn_.exported == 0) { + if (fn_.module.len > 0) { + let isffi: bool = false; + let a: *node = fn_.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let an: str = a.str; + if (streq(an, "symbol")) { isffi = true; }; + }; + a = a.next; + }; + if (!isffi) { + os.write(1, fn_.module.ptr, fn_.module.len: u64); + os.write(1, ".".ptr, 1u64); + }; + }; + }; + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emitline(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scanlocals. + // Seed c.locals with param-name stubs so scanlocals dedups a + // re-declared `let ` in the body against the param's + // slot (matches C cgen). Stubs get cleared before emission. + let scanp: *node = fn_.list; + let frame: i32 = 0; + for (scanp != nil) { + if (scanp.kind == N_PARAM) { + if (istaggedtype(scanp.lhs)) { frame += 24; } + else { if (isslicetype(c, scanp.lhs)) { frame += 24; } + else { if (isstrtype(c, scanp.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + scanseenmark(c, scanp.str); + }; + scanp = scanp.next; + }; + if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + // Drop the stubs so emission rebuilds c.locals with real offsets. + c.locals = nil; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emitint(frame: i64); + emitline("\n"); + + emitline("\tPUSHQ\tBP\n"); + emitline("\tMOVQ\tSP, BP\n"); + emitline("\tSUBQ\t$"); + emitint(frame: i64); + emitline(", SP\n"); + + cgfnparams(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cgfile(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collectaliases(c, file); + collectstructs(c, file); + collectdefs(c, file); + collectfnrets(c, file); + fficollect(c, file); + collectmods(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emitdatasection(c); + emitdefconstants(c, file); +}; + // MODULE: wcc // selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c. // @@ -5104,9 +6224,12 @@ use typ; use sym; use strconv; // Split files. Bundler pulls these in transitively so consumers only -// need `use cgen;`. +// need `use cgen;`. Order matters for the flat-bundle concat — utils +// first so cgen_expr/stmt/decl can reference helpers defined here. +use cgen_util; use cgen_expr; use cgen_stmt; +use cgen_decl; // ---- typedef alias registry ----------------------------------------- // @@ -5808,1085 +6931,6 @@ fn argregname(i: i32) str = { return "?"; }; -// ---- expression cgen ------------------------------------------------- - -// pushargsrev — recursively walks the arg list, evaluates rightmost -// first, and pushes. str args take two slots (ptr in AX, len in BX); -// the order on the stack so a left-to-right pop into argregs lands -// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the -// pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { - if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); - // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header - // on the stack matching C cgen's sequence — push base, push hi, - // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). - if (arg.kind == N_SLICE) { - let base: *node = arg.lhs; - let lo: *node = arg.rhs; - let hi: *node = arg.cond; - let baselocal: *local = nil; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - baselocal = localfindnode(c, bn); - }; - }; - // base address → push - if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - emitline("\tLEAQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - cgexpr(c, base); - }; - emitline("\tPUSHQ\tAX\n"); - // hi (default base length) → push - if (hi != nil) { - cgexpr(c, hi); - } else { if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - let lenn: *node = tn.rhs; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { - emitline("\tMOVQ\t$"); - emituint(lenn.uval); - emitline(", AX\n"); - }; - }; - } else { if (tn.kind == N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - } else { if (tn.kind == N_TNAME) { - if (streq(tn.str, "str")) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - }; - };};}; - }; - } else { - emitline("\tMOVQ\t$0, AX\n"); - };}; - emitline("\tPUSHQ\tAX\n"); - // lo (default 0) → AX - if (lo != nil) { cgexpr(c, lo); } - else { emitline("\tMOVQ\t$0, AX\n"); }; - emitline("\tPOPQ\tBX\n"); // hi - emitline("\tPOPQ\tCX\n"); // base - emitline("\tMOVQ\tBX, DX\n"); // DX = hi - emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len - emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr - emitline("\tPUSHQ\tDX\n"); // cap - emitline("\tPUSHQ\tDX\n"); // len - emitline("\tPUSHQ\tCX\n"); // ptr (top) - return rest + 3; - }; - // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in - // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop - // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). - if (arg.kind == N_IDENT) { - let nm: str = arg.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { - emitline("\tMOVQ\t"); - emitoff((off + 16): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff((off + 8): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff(off: i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - }; - }; - cgexpr(c, arg); - if (nodeisslice(c, arg)) { - emitline("\tPUSHQ\tCX\n"); - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - if (nodeisstr(c, arg)) { - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 2; - }; - emitline("\tPUSHQ\tAX\n"); - return rest + 1; -}; - -fn nodeisslice(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return isslicetype(c, lc.tnode); }; - return false; - }; - if (k == N_SLICE) { return true; }; - return false; -}; - -// nodeisstr — best-effort surface check: does this expression -// evaluate to a str value? Used to drive the call-arg push convention -// (str args take two slots: ptr + len). -fn nodeisstr(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_STRLIT) { return true; }; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { - let tnm: str = tn.str; - if (streq(tnm, "str")) { return true; }; - }; - }; - }; - return false; - }; - if (k == N_CALL) { - let callee: *node = n.lhs; - if (callee != nil) { - if (callee.kind == N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); - return isstrtype(c, rt); - }; - }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - // `.ptr` is *u8 not str; `.len` is i32 not str. - if (streq(fld, "ptr")) { return false; }; - if (streq(fld, "len")) { return false; }; - if (streq(fld, "cap")) { return false; }; - if (base != nil) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - }; - }; - // Chained dot (`p.foo.bar`): use dotinnerstructptr - // to resolve the inner chain to the *struct it lands - // on, then look up `fld` in that struct. - if (base.kind == N_DOT) { - let innert: *node = dotinnerstructptr(c, base); - if (innert != nil) { - if (innert.kind == N_TNAME) { sname = innert.str; }; - }; - }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return isstrtype(c, fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { - return isstrtype(c, n.rhs); - }; - return false; -}; - -// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. -fn typenameisunsigned(nm: str) bool = { - if (streq(nm, "u8")) { return true; }; - if (streq(nm, "u16")) { return true; }; - if (streq(nm, "u32")) { return true; }; - if (streq(nm, "u64")) { return true; }; - if (streq(nm, "uint")) { return true; }; - if (streq(nm, "uintptr")) { return true; }; - return false; -}; - -// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. -fn typenodeisunsigned(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; - return false; -}; - -// typeis8byteprimitive — does this type take exactly one 8-byte -// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive -// padded up to 8) rather than a wider aggregate? Used by N_LET -// zero-init to mirror C cgen's "only zero if sz == 8 at the type -// level" rule. Strings (16), slices (24), tagged unions (>=16), -// tuples (16), structs (varies), arrays — all fall through to -// false here even when their *slot* rounds up to 8. -fn typeis8byteprimitive(c: *cgen, t: *node) bool = { - if (t == nil) { return false; }; - let k: i32 = t.kind; - if (k == N_TPTR) { return true; }; - if (k == N_TFN) { return true; }; - if (k == N_TCHAN) { return true; }; - if (k == N_TSLICE) { return false; }; - if (k == N_TARRAY) { return false; }; - if (k == N_TTUPLE) { return false; }; - if (k == N_TTAGGED){ return false; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return false; }; - // Struct alias: not a primitive even if the slot is 8B. - if (structlookup(c, nm) != nil) { return false; }; - // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). - // All of these get slot-padded to 8 and zero-init in C. - if (primsize(nm) > 0) { return true; }; - return false; - }; - return false; -}; - -// typenameissigned — true for i8/i16/i32/i64/int/rune. -fn typenameissigned(nm: str) bool = { - if (streq(nm, "i8")) { return true; }; - if (streq(nm, "i16")) { return true; }; - if (streq(nm, "i32")) { return true; }; - if (streq(nm, "i64")) { return true; }; - if (streq(nm, "int")) { return true; }; - if (streq(nm, "rune")) { return true; }; - return false; -}; - -// fieldloadop — pick the load instruction for a non-str struct -// field by its declared size + signedness. Mirrors the C cgen op -// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ -// for 8-byte). f might be nil for fields outside our struct registry. -fn fieldloadop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVZBQ"; }; - if (sz == 4) { - let t: *node = f.tnode; - if (t != nil) { - if (t.kind == N_TNAME) { - if (typenameissigned(t.str)) { return "MOVSXD"; }; - }; - }; - return "MOVL"; - }; - return "MOVQ"; -}; - -// fieldstoreop — pick the store instruction for a non-str struct -// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. -fn fieldstoreop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVB"; }; - if (sz == 4) { return "MOVL"; }; - return "MOVQ"; -}; - -// indexbaseesz — element size for `arr[i]` where the base is a -// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). -// For str the element is one byte; for `[]T` / `*[]T` we drill into -// the slice element type. -fn indexbaseesz(c: *cgen, base: *node) i32 = { - if (base == nil) { return 8; }; - if (base.kind != N_DOT) { return 8; }; - let fld: str = base.str; - let inner: *node = base.lhs; - if (inner == nil) { return 8; }; - if (inner.kind != N_IDENT) { return 8; }; - let nm: str = inner.str; - let lc: *local = localfindnode(c, nm); - if (lc == nil) { return 8; }; - let tn: *node = lc.tnode; - if (tn == nil) { return 8; }; - - // `.ptr` pseudo-field on str/slice → element of the str/slice. - if (streq(fld, "ptr")) { - let innert: *node = tn; - if (tn.kind == N_TPTR) { innert = tn.lhs; }; - if (innert == nil) { return 8; }; - if (innert.kind == N_TNAME) { - if (streq(innert.str, "str")) { return 1; }; - }; - if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; - return 8; - }; - - // Generic struct field: if it's *T, element size is T's size. - let lkind: i32 = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let pinner: *node = tn.lhs; - if (pinner != nil) { - if (pinner.kind == N_TNAME) { sname = pinner.str; }; - }; - }; - if (sname.len == 0) { return 8; }; - let si: *structinfo = structlookup(c, sname); - if (si == nil) { return 8; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return 8; }; - if (ft.kind == N_TPTR) { - let elem: *node = ft.lhs; - if (elem != nil) { - if (elem.kind == N_TNAME) { - if (streq(elem.str, "str")) { return 16; }; - let ps: i32 = primsize(elem.str); - if (ps > 0) { return ps; }; - }; - }; - return 8; - }; - if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; - // str-typed field: indexing yields one byte - // (`n.s[i]` where .s is str — matches C cgen's - // MOVZBQ for byte indexing). - if (ft.kind == N_TNAME) { - if (streq(ft.str, "str")) { return 1; }; - }; - return 8; - }; - fi = fi.finext; - }; - return 8; -}; - -// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots -// or an N_IDENT, walk the chain and return the N_TNAME tnode of the -// struct that the chain dereferences to (i.e., for `r.sym` where -// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain -// doesn't resolve to a *struct. -// -// Used by the chained-DOT cgen path so `r.sym.val` knows the outer -// is a field of `lsym`. -fn dotinnerstructptr(c: *cgen, n: *node) *node = { - if (n == nil) { return nil; }; - if (n.kind != N_DOT) { return nil; }; - let base: *node = n.lhs; - let fld: str = n.str; - if (base == nil) { return nil; }; - - // Resolve base's struct tnode. - let baset: *node = nil; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc == nil) { return nil; }; - let tn: *node = lc.tnode; - if (tn == nil) { return nil; }; - // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). - if (tn.kind == N_TNAME) { baset = tn; }; - if (tn.kind == N_TPTR) { baset = tn.lhs; }; - } else { if (base.kind == N_DOT) { - baset = dotinnerstructptr(c, base); - };}; - if (baset == nil) { return nil; }; - if (baset.kind != N_TNAME) { return nil; }; - - // Look up the struct, find the field, return the field's *struct. - let si: *structinfo = structlookup(c, baset.str); - if (si == nil) { return nil; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return nil; }; - if (ft.kind != N_TPTR) { return nil; }; - let inner: *node = ft.lhs; - if (inner == nil) { return nil; }; - if (inner.kind != N_TNAME) { return nil; }; - return inner; - }; - fi = fi.finext; - }; - return nil; -}; - -// elemsizeof — given the type node of an indexable (`*T`, `[]T`, -// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ -// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). -fn elemsizeof(t: *node) i32 = { - if (t == nil) { return 1; }; - let k: i32 = t.kind; - let elem: *node = nil; - if (k == N_TPTR) { elem = t.lhs; }; - if (k == N_TSLICE) { elem = t.lhs; }; - if (k == N_TARRAY) { elem = t.lhs; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return 1; }; - // Indexing a primitive name (rare): element size = the prim. - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - return 1; - }; - if (elem == nil) { return 1; }; - if (elem.kind == N_TNAME) { - let nm: str = elem.str; - // str element is 16B (ptr+len). primsize returns 0 for it. - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - }; - return 8; -}; - -// nodeisunsigned — best-effort cgen-time inference from the AST. We -// don't have a typed AST yet, so we walk surface nodes: -// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) -// N_IDENT — look up the local's declared type -// N_DOT — look up the field's declared type via struct reg -// N_BIN / N_UN — recurse: unsigned if either operand is unsigned -// N_CAST — use the cast target type -// -// Conservative: if we can't tell, return false (signed). The cost of -// being wrong here is byte-different asm vs C, not bad runtime. -fn nodeisunsigned(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return typenodeisunsigned(lc.tnode); }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return typenodeisunsigned(fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; - if (k == N_BIN) { - if (nodeisunsigned(c, n.lhs)) { return true; }; - return nodeisunsigned(c, n.rhs); - }; - if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; - // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. - // Walks the base local's declared type and pulls the element - // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the - // compare-codegen for `p[i] >= 48u8` falls back to signed JGE - // instead of JAE, diverging from C w6c on byte indexing. - if (k == N_INDEX) { - let base: *node = n.lhs; - if (base != nil) { - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - let elem: *node = nil; - if (tn.kind == N_TPTR) { elem = tn.lhs; }; - if (tn.kind == N_TARRAY) { elem = tn.lhs; }; - if (tn.kind == N_TSLICE) { elem = tn.lhs; }; - if (elem != nil) { - return typenodeisunsigned(elem); - }; - }; - }; - }; - }; - return false; - }; - return false; -}; - -// ---- type-driven slot sizing ---------------------------------------- - -fn structlookup(c: *cgen, name: str) *structinfo = { - let s: *structinfo = c.structs; - for (s != nil) { - let sn: str = s.sname; - if (streq(sn, name)) { return s; }; - s = s.sinext; - }; - return nil; -}; - -// primsize — size in bytes of a primitive type name (or 0 if not -// recognised as a primitive — the caller falls back to other paths). -fn primsize(name: str) i32 = { - if (streq(name, "u8")) { return 1; }; - if (streq(name, "i8")) { return 1; }; - if (streq(name, "bool")) { return 1; }; - if (streq(name, "u16")) { return 2; }; - if (streq(name, "i16")) { return 2; }; - if (streq(name, "u32")) { return 4; }; - if (streq(name, "i32")) { return 4; }; - if (streq(name, "f32")) { return 4; }; - if (streq(name, "u64")) { return 8; }; - if (streq(name, "i64")) { return 8; }; - if (streq(name, "uint")) { return 8; }; - if (streq(name, "int")) { return 8; }; - if (streq(name, "uintptr")) { return 8; }; - if (streq(name, "f64")) { return 8; }; - if (streq(name, "rune")) { return 4; }; - if (streq(name, "void")) { return 0; }; - return 0; -}; - -fn slotsize(c: *cgen, typ_n: *node) i32 = { - if (typ_n == nil) { return 8; }; - let k: i32 = typ_n.kind; - if (k == N_TPTR) { return 8; }; - if (k == N_TFN) { return 8; }; - if (k == N_TCHAN) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TTUPLE) { return 16; }; - if (k == N_TTAGGED){ return 24; }; - if (k == N_TNAME) { - let nm: str = typ_n.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { - // Pad to 8 for stack slots — matches C cgen which spills - // every primitive into an 8-byte slot. - return 8; - }; - // Named struct lookup. - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TARRAY) { - let lenn: *node = typ_n.rhs; - let elemn: *node = typ_n.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = 8; - if (elemn != nil) { - if (elemn.kind == N_TNAME) { - let en: str = elemn.str; - let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; - }; - return (esz: i64 * elen): i32; - }; - if (k == N_TSTRUCT) { - // Inline anonymous struct — sum of field sizes. - let f: *node = typ_n.list; - let total: i32 = 0; - for (f != nil) { - if (f.kind == N_TFIELD) { - total += slotsize(c, f.lhs); - }; - f = f.next; - }; - return total; - }; - return 8; -}; - -// registerstruct — compute field offsets + total size for a struct -// type-decl, store in c.structs. Field type sizes use the same -// slotsize logic (with primitives kept at their natural width — we -// only round to 8 for stack slots, not struct interiors). -fn fieldsize(c: *cgen, tnode: *node) i32 = { - if (tnode == nil) { return 8; }; - let k: i32 = tnode.kind; - if (k == N_TNAME) { - let nm: str = tnode.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TPTR) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TARRAY) { - // Same shape as slotsize's TARRAY branch. - let lenn: *node = tnode.rhs; - let elemn: *node = tnode.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = fieldsize(c, elemn); - return (esz: i64 * elen): i32; - }; - return 8; -}; - -fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { - let si: *structinfo = amalloc(c.a, 64u64): *structinfo; - si.sname = name; - si.fields = nil; - si.tot_size = 0; - let head: *fieldinfo = nil; - let tail: *fieldinfo = nil; - let off: i32 = 0; - let f: *node = tstruct.list; - for (f != nil) { - if (f.kind == N_TFIELD) { - let sz: i32 = fieldsize(c, f.lhs); - // Align to 8 for any field >= 4 bytes (matches our other - // cgen choices). i8/u8/bool may sit on odd byte offsets; - // the C cgen does similar best-effort packing. - let aln: i32 = 1; - if (sz >= 8) { aln = 8; } - else { if (sz >= 4) { aln = 4; } - else { if (sz >= 2) { aln = 2; }; }; }; - if ((off & (aln - 1)) != 0) { - off = (off + aln - 1) & ~(aln - 1); - }; - let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; - fi.fname = f.str; - fi.foff = off; - fi.fsz = sz; - fi.tnode = f.lhs; - if (head == nil) { head = fi; tail = fi; } - else { tail.finext = fi; tail = fi; }; - off += sz; - }; - f = f.next; - }; - // Round total to 8 for stack-slot use. - if ((off & 7) != 0) { off = (off + 7) & ~7; }; - si.fields = head; - si.tot_size = off; - si.sinext = c.structs; - c.structs = si; -}; - -fn collectstructs(c: *cgen, file: *node) void = { - c.structs = nil; - if (file == nil) { return; }; - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_TYPEDECL) { - let body: *node = d.lhs; - if (body != nil) { - if (body.kind == N_TSTRUCT) { - registerstruct(c, d.str, body); - }; - }; - }; - d = d.next; - }; -}; - -// ---- frame pre-scan -------------------------------------------------- -// -// Recursively walks the body to count every local `let`. Each gets a -// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- -// init lets count too. Params are added by the cgfn driver. - -fn scanlocals(c: *cgen, n: *node) i32 = { - if (n == nil) { return 0; }; - let total: i32 = 0; - if (n.kind == N_LET) { - // Match localadd's rounding: < 8 bumps to 8, then 8-align. - // scanlocals must agree with localadd or the prologue - // SUBQ undersizes the frame and lets overflow into the - // caller's stack — corrupting whatever's at -frameSize..-1 - // of the caller. Same-name re-declarations share the first - // slot (see scanseenmark / localadd). - if (!scanseenmark(c, n.str)) { - let sz: i32 = slotsize(c, n.lhs); - if (sz < 8) { sz = 8; }; - if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; - total += sz; - }; - }; - // Match-arm binding (`case let v: T => ...`) gets a slot too. - // Crucially we do NOT dedup these against c.locals: C cgen - // handles a match as an expression with a by-value locals copy, - // so two separate matches in the same function each allocate - // their `v`/`e` slots fresh. Treating these as deduped would - // shrink the frame below what localadd then bumps it to. - if (n.kind == N_MCASE) { - let bn: str = n.str; - if (bn.len > 0) { - let pat: *node = n.lhs; - if (pat != nil) { - if (isstrtype(c, pat)) { total += 16; } - else { total += 8; }; - }; - }; - }; - if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; - if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; - if (n.cond != nil) { total += scanlocals(c, n.cond); }; - if (n.body != nil) { total += scanlocals(c, n.body); }; - if (n.els != nil) { total += scanlocals(c, n.els); }; - if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - total += scanlocals(c, m); - m = m.next; - }; - }; - return total; -}; - -// ---- function-level cgen --------------------------------------------- - -// isstrtype — true when the type expr resolves (through any -// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the -// alias chain registered at file load. -fn isstrtyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return true; }; - }; - return false; -}; - -fn isstrtype(c: *cgen, t: *node) bool = { - if (isstrtyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isstrtyperaw(r); -}; - -fn isslicetyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TSLICE) { return true; }; - return false; -}; - -fn isslicetype(c: *cgen, t: *node) bool = { - if (isslicetyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isslicetyperaw(r); -}; - -fn istaggedtype(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TTAGGED) { return true; }; - return false; -}; - -// rhstargetname — for a returned value, what's its declared (or -// surface-inferred) type name? `expr: T` casts dictate T directly; -// bare strlit/intlit fall back to a primitive name. -fn rhstargetname(c: *cgen, rhs: *node) str = { - let nm: str; - nm.ptr = nil; nm.len = 0; - if (rhs == nil) { return nm; }; - if (rhs.kind == N_CAST) { - let t: *node = rhs.rhs; - if (t != nil) { - if (t.kind == N_TNAME) { return t.str; }; - }; - return nm; - }; - if (rhs.kind == N_STRLIT) { return "str"; }; - if (rhs.kind == N_IDENT) { - let lc: *local = localfindnode(c, rhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { return tn.str; }; - }; - }; - }; - return nm; -}; - -// taggedvariantindex — given the tagged-union type expr and the -// returned value's surface type, find the matching variant's 0-based -// index. Compare by exact type name first; if no match, fall back to -// "any str-shape variant matches an str-typed value". -fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { - if (tagged == nil) { return -1; }; - if (rhs == nil) { return -1; }; - let wantname: str = rhstargetname(c, rhs); - if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == N_TNAME) { - if (streq(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - }; - // Fallback: by str-shape (resolves aliases). - let wantstr: bool = nodeisstr(c, rhs); - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - let visstr: bool = false; - if (v.kind == N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; - v = v.next; - idx += 1; - }; - return -1; -}; - -fn cgfnparams(c: *cgen, params: *node) void = { - let p: *node = params; - let idx: i32 = 0; - for (p != nil) { - if (p.kind == N_PARAM) { - let nm: str = p.str; - if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isslicetype(c, p.lhs)) { - // slice param: 3 regs (ptr, len, cap), 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isstrtype(c, p.lhs)) { - // str param: passed in two regs (ptr, len). - // Slot is 16 bytes; ptr at off+0, len at off+8. - let off: i32 = localadd(c, nm, 16, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - } else { - let off: i32 = localadd(c, nm, 8, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - };};}; - }; - p = p.next; - }; -}; - -fn cgfn(c: *cgen, fn_: *node) void = { - cgeninit(c, c.a); - c.fn_name = fn_.str; - c.fn_ret = fn_.lhs; - - emitline("TEXT "); - if (fn_.exported == 0) { - if (fn_.module.len > 0) { - let isffi: bool = false; - let a: *node = fn_.attr; - for (a != nil) { - if (a.kind == N_ATTR) { - let an: str = a.str; - if (streq(an, "symbol")) { isffi = true; }; - }; - a = a.next; - }; - if (!isffi) { - os.write(1, fn_.module.ptr, fn_.module.len: u64); - os.write(1, ".".ptr, 1u64); - }; - }; - }; - let nm: str = fn_.str; - os.write(1, nm.ptr, nm.len: u64); - emitline(",$"); - - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. - let scanp: *node = fn_.list; - let frame: i32 = 0; - for (scanp != nil) { - if (scanp.kind == N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; - scanseenmark(c, scanp.str); - }; - scanp = scanp.next; - }; - if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; - // Drop the stubs so emission rebuilds c.locals with real offsets. - c.locals = nil; - if ((frame & 15) != 0) { - frame = (frame + 15) & ~15; - }; - emitint(frame: i64); - emitline("\n"); - - emitline("\tPUSHQ\tBP\n"); - emitline("\tMOVQ\tSP, BP\n"); - emitline("\tSUBQ\t$"); - emitint(frame: i64); - emitline(", SP\n"); - - cgfnparams(c, fn_.list); - c.last_was_return = 0; - if (fn_.body != nil) { cgstmt(c, fn_.body); }; - - if (c.last_was_return == 0) { - // Zero AX before the fall-through return — matches C cgen, - // which always emits this so void-returning fns don't leak - // a stale callee value to their caller. - emitline("\tMOVQ\t$0, AX\n"); - emitline("\tMOVQ\tBP, SP\n"); - emitline("\tPOPQ\tBP\n"); - emitline("\tRET\n"); - }; -}; - -// ---- file-level entry ------------------------------------------------ - -export fn cgfile(c: *cgen, file: *node) void = { - if (file == nil) { return; }; - c.strlits = nil; - c.strlit_seq = 0; - collectaliases(c, file); - collectstructs(c, file); - collectdefs(c, file); - collectfnrets(c, file); - fficollect(c, file); - collectmods(c, file); - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); - }; - }; - d = d.next; - }; - emitdatasection(c); - emitdefconstants(c, file); -}; - // MODULE: w6c // selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c. // diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 8ded959d..565561b9 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -30,9 +30,12 @@ use typ; use sym; use strconv; // Split files. Bundler pulls these in transitively so consumers only -// need `use cgen;`. +// need `use cgen;`. Order matters for the flat-bundle concat — utils +// first so cgen_expr/stmt/decl can reference helpers defined here. +use cgen_util; use cgen_expr; use cgen_stmt; +use cgen_decl; // ---- typedef alias registry ----------------------------------------- // @@ -733,1082 +736,3 @@ fn argregname(i: i32) str = { if (i == 5) { return "R9"; }; return "?"; }; - -// ---- expression cgen ------------------------------------------------- - -// pushargsrev — recursively walks the arg list, evaluates rightmost -// first, and pushes. str args take two slots (ptr in AX, len in BX); -// the order on the stack so a left-to-right pop into argregs lands -// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the -// pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { - if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); - // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header - // on the stack matching C cgen's sequence — push base, push hi, - // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). - if (arg.kind == N_SLICE) { - let base: *node = arg.lhs; - let lo: *node = arg.rhs; - let hi: *node = arg.cond; - let baselocal: *local = nil; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - baselocal = localfindnode(c, bn); - }; - }; - // base address → push - if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - emitline("\tLEAQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - cgexpr(c, base); - }; - emitline("\tPUSHQ\tAX\n"); - // hi (default base length) → push - if (hi != nil) { - cgexpr(c, hi); - } else { if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - let lenn: *node = tn.rhs; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { - emitline("\tMOVQ\t$"); - emituint(lenn.uval); - emitline(", AX\n"); - }; - }; - } else { if (tn.kind == N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - } else { if (tn.kind == N_TNAME) { - if (streq(tn.str, "str")) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - }; - };};}; - }; - } else { - emitline("\tMOVQ\t$0, AX\n"); - };}; - emitline("\tPUSHQ\tAX\n"); - // lo (default 0) → AX - if (lo != nil) { cgexpr(c, lo); } - else { emitline("\tMOVQ\t$0, AX\n"); }; - emitline("\tPOPQ\tBX\n"); // hi - emitline("\tPOPQ\tCX\n"); // base - emitline("\tMOVQ\tBX, DX\n"); // DX = hi - emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len - emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr - emitline("\tPUSHQ\tDX\n"); // cap - emitline("\tPUSHQ\tDX\n"); // len - emitline("\tPUSHQ\tCX\n"); // ptr (top) - return rest + 3; - }; - // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in - // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop - // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). - if (arg.kind == N_IDENT) { - let nm: str = arg.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { - emitline("\tMOVQ\t"); - emitoff((off + 16): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff((off + 8): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff(off: i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - }; - }; - cgexpr(c, arg); - if (nodeisslice(c, arg)) { - emitline("\tPUSHQ\tCX\n"); - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - if (nodeisstr(c, arg)) { - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 2; - }; - emitline("\tPUSHQ\tAX\n"); - return rest + 1; -}; - -fn nodeisslice(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return isslicetype(c, lc.tnode); }; - return false; - }; - if (k == N_SLICE) { return true; }; - return false; -}; - -// nodeisstr — best-effort surface check: does this expression -// evaluate to a str value? Used to drive the call-arg push convention -// (str args take two slots: ptr + len). -fn nodeisstr(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_STRLIT) { return true; }; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { - let tnm: str = tn.str; - if (streq(tnm, "str")) { return true; }; - }; - }; - }; - return false; - }; - if (k == N_CALL) { - let callee: *node = n.lhs; - if (callee != nil) { - if (callee.kind == N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); - return isstrtype(c, rt); - }; - }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - // `.ptr` is *u8 not str; `.len` is i32 not str. - if (streq(fld, "ptr")) { return false; }; - if (streq(fld, "len")) { return false; }; - if (streq(fld, "cap")) { return false; }; - if (base != nil) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - }; - }; - // Chained dot (`p.foo.bar`): use dotinnerstructptr - // to resolve the inner chain to the *struct it lands - // on, then look up `fld` in that struct. - if (base.kind == N_DOT) { - let innert: *node = dotinnerstructptr(c, base); - if (innert != nil) { - if (innert.kind == N_TNAME) { sname = innert.str; }; - }; - }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return isstrtype(c, fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { - return isstrtype(c, n.rhs); - }; - return false; -}; - -// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. -fn typenameisunsigned(nm: str) bool = { - if (streq(nm, "u8")) { return true; }; - if (streq(nm, "u16")) { return true; }; - if (streq(nm, "u32")) { return true; }; - if (streq(nm, "u64")) { return true; }; - if (streq(nm, "uint")) { return true; }; - if (streq(nm, "uintptr")) { return true; }; - return false; -}; - -// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. -fn typenodeisunsigned(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; - return false; -}; - -// typeis8byteprimitive — does this type take exactly one 8-byte -// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive -// padded up to 8) rather than a wider aggregate? Used by N_LET -// zero-init to mirror C cgen's "only zero if sz == 8 at the type -// level" rule. Strings (16), slices (24), tagged unions (>=16), -// tuples (16), structs (varies), arrays — all fall through to -// false here even when their *slot* rounds up to 8. -fn typeis8byteprimitive(c: *cgen, t: *node) bool = { - if (t == nil) { return false; }; - let k: i32 = t.kind; - if (k == N_TPTR) { return true; }; - if (k == N_TFN) { return true; }; - if (k == N_TCHAN) { return true; }; - if (k == N_TSLICE) { return false; }; - if (k == N_TARRAY) { return false; }; - if (k == N_TTUPLE) { return false; }; - if (k == N_TTAGGED){ return false; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return false; }; - // Struct alias: not a primitive even if the slot is 8B. - if (structlookup(c, nm) != nil) { return false; }; - // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). - // All of these get slot-padded to 8 and zero-init in C. - if (primsize(nm) > 0) { return true; }; - return false; - }; - return false; -}; - -// typenameissigned — true for i8/i16/i32/i64/int/rune. -fn typenameissigned(nm: str) bool = { - if (streq(nm, "i8")) { return true; }; - if (streq(nm, "i16")) { return true; }; - if (streq(nm, "i32")) { return true; }; - if (streq(nm, "i64")) { return true; }; - if (streq(nm, "int")) { return true; }; - if (streq(nm, "rune")) { return true; }; - return false; -}; - -// fieldloadop — pick the load instruction for a non-str struct -// field by its declared size + signedness. Mirrors the C cgen op -// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ -// for 8-byte). f might be nil for fields outside our struct registry. -fn fieldloadop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVZBQ"; }; - if (sz == 4) { - let t: *node = f.tnode; - if (t != nil) { - if (t.kind == N_TNAME) { - if (typenameissigned(t.str)) { return "MOVSXD"; }; - }; - }; - return "MOVL"; - }; - return "MOVQ"; -}; - -// fieldstoreop — pick the store instruction for a non-str struct -// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. -fn fieldstoreop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVB"; }; - if (sz == 4) { return "MOVL"; }; - return "MOVQ"; -}; - -// indexbaseesz — element size for `arr[i]` where the base is a -// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). -// For str the element is one byte; for `[]T` / `*[]T` we drill into -// the slice element type. -fn indexbaseesz(c: *cgen, base: *node) i32 = { - if (base == nil) { return 8; }; - if (base.kind != N_DOT) { return 8; }; - let fld: str = base.str; - let inner: *node = base.lhs; - if (inner == nil) { return 8; }; - if (inner.kind != N_IDENT) { return 8; }; - let nm: str = inner.str; - let lc: *local = localfindnode(c, nm); - if (lc == nil) { return 8; }; - let tn: *node = lc.tnode; - if (tn == nil) { return 8; }; - - // `.ptr` pseudo-field on str/slice → element of the str/slice. - if (streq(fld, "ptr")) { - let innert: *node = tn; - if (tn.kind == N_TPTR) { innert = tn.lhs; }; - if (innert == nil) { return 8; }; - if (innert.kind == N_TNAME) { - if (streq(innert.str, "str")) { return 1; }; - }; - if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; - return 8; - }; - - // Generic struct field: if it's *T, element size is T's size. - let lkind: i32 = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let pinner: *node = tn.lhs; - if (pinner != nil) { - if (pinner.kind == N_TNAME) { sname = pinner.str; }; - }; - }; - if (sname.len == 0) { return 8; }; - let si: *structinfo = structlookup(c, sname); - if (si == nil) { return 8; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return 8; }; - if (ft.kind == N_TPTR) { - let elem: *node = ft.lhs; - if (elem != nil) { - if (elem.kind == N_TNAME) { - if (streq(elem.str, "str")) { return 16; }; - let ps: i32 = primsize(elem.str); - if (ps > 0) { return ps; }; - }; - }; - return 8; - }; - if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; - // str-typed field: indexing yields one byte - // (`n.s[i]` where .s is str — matches C cgen's - // MOVZBQ for byte indexing). - if (ft.kind == N_TNAME) { - if (streq(ft.str, "str")) { return 1; }; - }; - return 8; - }; - fi = fi.finext; - }; - return 8; -}; - -// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots -// or an N_IDENT, walk the chain and return the N_TNAME tnode of the -// struct that the chain dereferences to (i.e., for `r.sym` where -// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain -// doesn't resolve to a *struct. -// -// Used by the chained-DOT cgen path so `r.sym.val` knows the outer -// is a field of `lsym`. -fn dotinnerstructptr(c: *cgen, n: *node) *node = { - if (n == nil) { return nil; }; - if (n.kind != N_DOT) { return nil; }; - let base: *node = n.lhs; - let fld: str = n.str; - if (base == nil) { return nil; }; - - // Resolve base's struct tnode. - let baset: *node = nil; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc == nil) { return nil; }; - let tn: *node = lc.tnode; - if (tn == nil) { return nil; }; - // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). - if (tn.kind == N_TNAME) { baset = tn; }; - if (tn.kind == N_TPTR) { baset = tn.lhs; }; - } else { if (base.kind == N_DOT) { - baset = dotinnerstructptr(c, base); - };}; - if (baset == nil) { return nil; }; - if (baset.kind != N_TNAME) { return nil; }; - - // Look up the struct, find the field, return the field's *struct. - let si: *structinfo = structlookup(c, baset.str); - if (si == nil) { return nil; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return nil; }; - if (ft.kind != N_TPTR) { return nil; }; - let inner: *node = ft.lhs; - if (inner == nil) { return nil; }; - if (inner.kind != N_TNAME) { return nil; }; - return inner; - }; - fi = fi.finext; - }; - return nil; -}; - -// elemsizeof — given the type node of an indexable (`*T`, `[]T`, -// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ -// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). -fn elemsizeof(t: *node) i32 = { - if (t == nil) { return 1; }; - let k: i32 = t.kind; - let elem: *node = nil; - if (k == N_TPTR) { elem = t.lhs; }; - if (k == N_TSLICE) { elem = t.lhs; }; - if (k == N_TARRAY) { elem = t.lhs; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return 1; }; - // Indexing a primitive name (rare): element size = the prim. - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - return 1; - }; - if (elem == nil) { return 1; }; - if (elem.kind == N_TNAME) { - let nm: str = elem.str; - // str element is 16B (ptr+len). primsize returns 0 for it. - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - }; - return 8; -}; - -// nodeisunsigned — best-effort cgen-time inference from the AST. We -// don't have a typed AST yet, so we walk surface nodes: -// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) -// N_IDENT — look up the local's declared type -// N_DOT — look up the field's declared type via struct reg -// N_BIN / N_UN — recurse: unsigned if either operand is unsigned -// N_CAST — use the cast target type -// -// Conservative: if we can't tell, return false (signed). The cost of -// being wrong here is byte-different asm vs C, not bad runtime. -fn nodeisunsigned(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return typenodeisunsigned(lc.tnode); }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return typenodeisunsigned(fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; - if (k == N_BIN) { - if (nodeisunsigned(c, n.lhs)) { return true; }; - return nodeisunsigned(c, n.rhs); - }; - if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; - // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. - // Walks the base local's declared type and pulls the element - // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the - // compare-codegen for `p[i] >= 48u8` falls back to signed JGE - // instead of JAE, diverging from C w6c on byte indexing. - if (k == N_INDEX) { - let base: *node = n.lhs; - if (base != nil) { - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - let elem: *node = nil; - if (tn.kind == N_TPTR) { elem = tn.lhs; }; - if (tn.kind == N_TARRAY) { elem = tn.lhs; }; - if (tn.kind == N_TSLICE) { elem = tn.lhs; }; - if (elem != nil) { - return typenodeisunsigned(elem); - }; - }; - }; - }; - }; - return false; - }; - return false; -}; - -// ---- type-driven slot sizing ---------------------------------------- - -fn structlookup(c: *cgen, name: str) *structinfo = { - let s: *structinfo = c.structs; - for (s != nil) { - let sn: str = s.sname; - if (streq(sn, name)) { return s; }; - s = s.sinext; - }; - return nil; -}; - -// primsize — size in bytes of a primitive type name (or 0 if not -// recognised as a primitive — the caller falls back to other paths). -fn primsize(name: str) i32 = { - if (streq(name, "u8")) { return 1; }; - if (streq(name, "i8")) { return 1; }; - if (streq(name, "bool")) { return 1; }; - if (streq(name, "u16")) { return 2; }; - if (streq(name, "i16")) { return 2; }; - if (streq(name, "u32")) { return 4; }; - if (streq(name, "i32")) { return 4; }; - if (streq(name, "f32")) { return 4; }; - if (streq(name, "u64")) { return 8; }; - if (streq(name, "i64")) { return 8; }; - if (streq(name, "uint")) { return 8; }; - if (streq(name, "int")) { return 8; }; - if (streq(name, "uintptr")) { return 8; }; - if (streq(name, "f64")) { return 8; }; - if (streq(name, "rune")) { return 4; }; - if (streq(name, "void")) { return 0; }; - return 0; -}; - -fn slotsize(c: *cgen, typ_n: *node) i32 = { - if (typ_n == nil) { return 8; }; - let k: i32 = typ_n.kind; - if (k == N_TPTR) { return 8; }; - if (k == N_TFN) { return 8; }; - if (k == N_TCHAN) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TTUPLE) { return 16; }; - if (k == N_TTAGGED){ return 24; }; - if (k == N_TNAME) { - let nm: str = typ_n.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { - // Pad to 8 for stack slots — matches C cgen which spills - // every primitive into an 8-byte slot. - return 8; - }; - // Named struct lookup. - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TARRAY) { - let lenn: *node = typ_n.rhs; - let elemn: *node = typ_n.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = 8; - if (elemn != nil) { - if (elemn.kind == N_TNAME) { - let en: str = elemn.str; - let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; - }; - return (esz: i64 * elen): i32; - }; - if (k == N_TSTRUCT) { - // Inline anonymous struct — sum of field sizes. - let f: *node = typ_n.list; - let total: i32 = 0; - for (f != nil) { - if (f.kind == N_TFIELD) { - total += slotsize(c, f.lhs); - }; - f = f.next; - }; - return total; - }; - return 8; -}; - -// registerstruct — compute field offsets + total size for a struct -// type-decl, store in c.structs. Field type sizes use the same -// slotsize logic (with primitives kept at their natural width — we -// only round to 8 for stack slots, not struct interiors). -fn fieldsize(c: *cgen, tnode: *node) i32 = { - if (tnode == nil) { return 8; }; - let k: i32 = tnode.kind; - if (k == N_TNAME) { - let nm: str = tnode.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TPTR) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TARRAY) { - // Same shape as slotsize's TARRAY branch. - let lenn: *node = tnode.rhs; - let elemn: *node = tnode.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = fieldsize(c, elemn); - return (esz: i64 * elen): i32; - }; - return 8; -}; - -fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { - let si: *structinfo = amalloc(c.a, 64u64): *structinfo; - si.sname = name; - si.fields = nil; - si.tot_size = 0; - let head: *fieldinfo = nil; - let tail: *fieldinfo = nil; - let off: i32 = 0; - let f: *node = tstruct.list; - for (f != nil) { - if (f.kind == N_TFIELD) { - let sz: i32 = fieldsize(c, f.lhs); - // Align to 8 for any field >= 4 bytes (matches our other - // cgen choices). i8/u8/bool may sit on odd byte offsets; - // the C cgen does similar best-effort packing. - let aln: i32 = 1; - if (sz >= 8) { aln = 8; } - else { if (sz >= 4) { aln = 4; } - else { if (sz >= 2) { aln = 2; }; }; }; - if ((off & (aln - 1)) != 0) { - off = (off + aln - 1) & ~(aln - 1); - }; - let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; - fi.fname = f.str; - fi.foff = off; - fi.fsz = sz; - fi.tnode = f.lhs; - if (head == nil) { head = fi; tail = fi; } - else { tail.finext = fi; tail = fi; }; - off += sz; - }; - f = f.next; - }; - // Round total to 8 for stack-slot use. - if ((off & 7) != 0) { off = (off + 7) & ~7; }; - si.fields = head; - si.tot_size = off; - si.sinext = c.structs; - c.structs = si; -}; - -fn collectstructs(c: *cgen, file: *node) void = { - c.structs = nil; - if (file == nil) { return; }; - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_TYPEDECL) { - let body: *node = d.lhs; - if (body != nil) { - if (body.kind == N_TSTRUCT) { - registerstruct(c, d.str, body); - }; - }; - }; - d = d.next; - }; -}; - -// ---- frame pre-scan -------------------------------------------------- -// -// Recursively walks the body to count every local `let`. Each gets a -// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- -// init lets count too. Params are added by the cgfn driver. - -fn scanlocals(c: *cgen, n: *node) i32 = { - if (n == nil) { return 0; }; - let total: i32 = 0; - if (n.kind == N_LET) { - // Match localadd's rounding: < 8 bumps to 8, then 8-align. - // scanlocals must agree with localadd or the prologue - // SUBQ undersizes the frame and lets overflow into the - // caller's stack — corrupting whatever's at -frameSize..-1 - // of the caller. Same-name re-declarations share the first - // slot (see scanseenmark / localadd). - if (!scanseenmark(c, n.str)) { - let sz: i32 = slotsize(c, n.lhs); - if (sz < 8) { sz = 8; }; - if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; - total += sz; - }; - }; - // Match-arm binding (`case let v: T => ...`) gets a slot too. - // Crucially we do NOT dedup these against c.locals: C cgen - // handles a match as an expression with a by-value locals copy, - // so two separate matches in the same function each allocate - // their `v`/`e` slots fresh. Treating these as deduped would - // shrink the frame below what localadd then bumps it to. - if (n.kind == N_MCASE) { - let bn: str = n.str; - if (bn.len > 0) { - let pat: *node = n.lhs; - if (pat != nil) { - if (isstrtype(c, pat)) { total += 16; } - else { total += 8; }; - }; - }; - }; - if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; - if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; - if (n.cond != nil) { total += scanlocals(c, n.cond); }; - if (n.body != nil) { total += scanlocals(c, n.body); }; - if (n.els != nil) { total += scanlocals(c, n.els); }; - if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - total += scanlocals(c, m); - m = m.next; - }; - }; - return total; -}; - -// ---- function-level cgen --------------------------------------------- - -// isstrtype — true when the type expr resolves (through any -// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the -// alias chain registered at file load. -fn isstrtyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return true; }; - }; - return false; -}; - -fn isstrtype(c: *cgen, t: *node) bool = { - if (isstrtyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isstrtyperaw(r); -}; - -fn isslicetyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TSLICE) { return true; }; - return false; -}; - -fn isslicetype(c: *cgen, t: *node) bool = { - if (isslicetyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isslicetyperaw(r); -}; - -fn istaggedtype(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TTAGGED) { return true; }; - return false; -}; - -// rhstargetname — for a returned value, what's its declared (or -// surface-inferred) type name? `expr: T` casts dictate T directly; -// bare strlit/intlit fall back to a primitive name. -fn rhstargetname(c: *cgen, rhs: *node) str = { - let nm: str; - nm.ptr = nil; nm.len = 0; - if (rhs == nil) { return nm; }; - if (rhs.kind == N_CAST) { - let t: *node = rhs.rhs; - if (t != nil) { - if (t.kind == N_TNAME) { return t.str; }; - }; - return nm; - }; - if (rhs.kind == N_STRLIT) { return "str"; }; - if (rhs.kind == N_IDENT) { - let lc: *local = localfindnode(c, rhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { return tn.str; }; - }; - }; - }; - return nm; -}; - -// taggedvariantindex — given the tagged-union type expr and the -// returned value's surface type, find the matching variant's 0-based -// index. Compare by exact type name first; if no match, fall back to -// "any str-shape variant matches an str-typed value". -fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { - if (tagged == nil) { return -1; }; - if (rhs == nil) { return -1; }; - let wantname: str = rhstargetname(c, rhs); - if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == N_TNAME) { - if (streq(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - }; - // Fallback: by str-shape (resolves aliases). - let wantstr: bool = nodeisstr(c, rhs); - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - let visstr: bool = false; - if (v.kind == N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; - v = v.next; - idx += 1; - }; - return -1; -}; - -fn cgfnparams(c: *cgen, params: *node) void = { - let p: *node = params; - let idx: i32 = 0; - for (p != nil) { - if (p.kind == N_PARAM) { - let nm: str = p.str; - if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isslicetype(c, p.lhs)) { - // slice param: 3 regs (ptr, len, cap), 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isstrtype(c, p.lhs)) { - // str param: passed in two regs (ptr, len). - // Slot is 16 bytes; ptr at off+0, len at off+8. - let off: i32 = localadd(c, nm, 16, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - } else { - let off: i32 = localadd(c, nm, 8, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - };};}; - }; - p = p.next; - }; -}; - -fn cgfn(c: *cgen, fn_: *node) void = { - cgeninit(c, c.a); - c.fn_name = fn_.str; - c.fn_ret = fn_.lhs; - - emitline("TEXT "); - if (fn_.exported == 0) { - if (fn_.module.len > 0) { - let isffi: bool = false; - let a: *node = fn_.attr; - for (a != nil) { - if (a.kind == N_ATTR) { - let an: str = a.str; - if (streq(an, "symbol")) { isffi = true; }; - }; - a = a.next; - }; - if (!isffi) { - os.write(1, fn_.module.ptr, fn_.module.len: u64); - os.write(1, ".".ptr, 1u64); - }; - }; - }; - let nm: str = fn_.str; - os.write(1, nm.ptr, nm.len: u64); - emitline(",$"); - - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. - let scanp: *node = fn_.list; - let frame: i32 = 0; - for (scanp != nil) { - if (scanp.kind == N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; - scanseenmark(c, scanp.str); - }; - scanp = scanp.next; - }; - if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; - // Drop the stubs so emission rebuilds c.locals with real offsets. - c.locals = nil; - if ((frame & 15) != 0) { - frame = (frame + 15) & ~15; - }; - emitint(frame: i64); - emitline("\n"); - - emitline("\tPUSHQ\tBP\n"); - emitline("\tMOVQ\tSP, BP\n"); - emitline("\tSUBQ\t$"); - emitint(frame: i64); - emitline(", SP\n"); - - cgfnparams(c, fn_.list); - c.last_was_return = 0; - if (fn_.body != nil) { cgstmt(c, fn_.body); }; - - if (c.last_was_return == 0) { - // Zero AX before the fall-through return — matches C cgen, - // which always emits this so void-returning fns don't leak - // a stale callee value to their caller. - emitline("\tMOVQ\t$0, AX\n"); - emitline("\tMOVQ\tBP, SP\n"); - emitline("\tPOPQ\tBP\n"); - emitline("\tRET\n"); - }; -}; - -// ---- file-level entry ------------------------------------------------ - -export fn cgfile(c: *cgen, file: *node) void = { - if (file == nil) { return; }; - c.strlits = nil; - c.strlit_seq = 0; - collectaliases(c, file); - collectstructs(c, file); - collectdefs(c, file); - collectfnrets(c, file); - fficollect(c, file); - collectmods(c, file); - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); - }; - }; - d = d.next; - }; - emitdatasection(c); - emitdefconstants(c, file); -}; diff --git a/selfhost/cmd/wcc/cgen_decl.ww b/selfhost/cmd/wcc/cgen_decl.ww new file mode 100644 index 00000000..c4b58498 --- /dev/null +++ b/selfhost/cmd/wcc/cgen_decl.ww @@ -0,0 +1,252 @@ +// selfhost/cmd/wcc/cgen_decl.ww — split out of cgen.ww. +// +// Houses the top-level emission glue: +// - scanlocals: frame pre-scan that counts each local `let` +// - cgfnparams: parameter spilling per SysV +// - cgfn: fn prologue + body + epilogue +// - cgfile: file-level entry (the exported driver) +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_decl;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scanlocals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match localadd's rounding: < 8 bumps to 8, then 8-align. + // scanlocals must agree with localadd or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. Same-name re-declarations share the first + // slot (see scanseenmark / localadd). + if (!scanseenmark(c, n.str)) { + let sz: i32 = slotsize(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + }; + // Match-arm binding (`case let v: T => ...`) gets a slot too. + // Crucially we do NOT dedup these against c.locals: C cgen + // handles a match as an expression with a by-value locals copy, + // so two separate matches in the same function each allocate + // their `v`/`e` slots fresh. Treating these as deduped would + // shrink the frame below what localadd then bumps it to. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (isstrtype(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; + if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; + if (n.cond != nil) { total += scanlocals(c, n.cond); }; + if (n.body != nil) { total += scanlocals(c, n.body); }; + if (n.els != nil) { total += scanlocals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scanlocals(c, m); + m = m.next; + }; + }; + return total; +}; + + +// ---- function-level cgen --------------------------------------------- + +fn cgfnparams(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (istaggedtype(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isslicetype(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isstrtype(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = localadd(c, nm, 16, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + } else { + let off: i32 = localadd(c, nm, 8, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgeninit(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emitline("TEXT "); + if (fn_.exported == 0) { + if (fn_.module.len > 0) { + let isffi: bool = false; + let a: *node = fn_.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let an: str = a.str; + if (streq(an, "symbol")) { isffi = true; }; + }; + a = a.next; + }; + if (!isffi) { + os.write(1, fn_.module.ptr, fn_.module.len: u64); + os.write(1, ".".ptr, 1u64); + }; + }; + }; + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emitline(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scanlocals. + // Seed c.locals with param-name stubs so scanlocals dedups a + // re-declared `let ` in the body against the param's + // slot (matches C cgen). Stubs get cleared before emission. + let scanp: *node = fn_.list; + let frame: i32 = 0; + for (scanp != nil) { + if (scanp.kind == N_PARAM) { + if (istaggedtype(scanp.lhs)) { frame += 24; } + else { if (isslicetype(c, scanp.lhs)) { frame += 24; } + else { if (isstrtype(c, scanp.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + scanseenmark(c, scanp.str); + }; + scanp = scanp.next; + }; + if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + // Drop the stubs so emission rebuilds c.locals with real offsets. + c.locals = nil; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emitint(frame: i64); + emitline("\n"); + + emitline("\tPUSHQ\tBP\n"); + emitline("\tMOVQ\tSP, BP\n"); + emitline("\tSUBQ\t$"); + emitint(frame: i64); + emitline(", SP\n"); + + cgfnparams(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cgfile(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collectaliases(c, file); + collectstructs(c, file); + collectdefs(c, file); + collectfnrets(c, file); + fficollect(c, file); + collectmods(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emitdatasection(c); + emitdefconstants(c, file); +}; diff --git a/selfhost/cmd/wcc/cgen_util.ww b/selfhost/cmd/wcc/cgen_util.ww new file mode 100644 index 00000000..6257d608 --- /dev/null +++ b/selfhost/cmd/wcc/cgen_util.ww @@ -0,0 +1,864 @@ +// selfhost/cmd/wcc/cgen_util.ww — split out of cgen.ww. +// +// General helpers used across cgen_expr / cgen_stmt / cgen_decl: +// - pushargsrev: per-call arg pushing +// - type predicates: isstr*/isslice*/istagged*/nodeis* families +// - field ops: fieldloadop, fieldstoreop +// - index helpers: indexbaseesz, dotinnerstructptr, elemsizeof +// - slot sizing: structlookup, primsize, slotsize, fieldsize, +// registerstruct, collectstructs +// - rhs helpers: rhstargetname, taggedvariantindex +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_util;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- expression cgen ------------------------------------------------- + +// pushargsrev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn pushargsrev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = pushargsrev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let baselocal: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + }; + }; + // base address → push + if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emitline("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let lenn: *node = tn.rhs; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { + emitline("\tMOVQ\t$"); + emituint(lenn.uval); + emitline(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + }; + };};}; + }; + } else { + emitline("\tMOVQ\t$0, AX\n"); + };}; + emitline("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emitline("\tMOVQ\t$0, AX\n"); }; + emitline("\tPOPQ\tBX\n"); // hi + emitline("\tPOPQ\tCX\n"); // base + emitline("\tMOVQ\tBX, DX\n"); // DX = hi + emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emitline("\tPUSHQ\tDX\n"); // cap + emitline("\tPUSHQ\tDX\n"); // len + emitline("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + emitline("\tMOVQ\t"); + emitoff((off + 16): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (nodeisslice(c, arg)) { + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (nodeisstr(c, arg)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emitline("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn nodeisslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return isslicetype(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// nodeisstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn nodeisstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnretlookup(c, cnm); + return isstrtype(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + // Chained dot (`p.foo.bar`): use dotinnerstructptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return isstrtype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return isstrtype(c, n.rhs); + }; + return false; +}; + +// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn typenameisunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn typenodeisunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; + return false; +}; + +// typeis8byteprimitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn typeis8byteprimitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (structlookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (primsize(nm) > 0) { return true; }; + return false; + }; + return false; +}; + +// typenameissigned — true for i8/i16/i32/i64/int/rune. +fn typenameissigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// fieldloadop — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn fieldloadop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (typenameissigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// fieldstoreop — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn fieldstoreop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// indexbaseesz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn indexbaseesz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = localfindnode(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let innert: *node = tn; + if (tn.kind == N_TPTR) { innert = tn.lhs; }; + if (innert == nil) { return 8; }; + if (innert.kind == N_TNAME) { + if (streq(innert.str, "str")) { return 1; }; + }; + if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return 8; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = primsize(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dotinnerstructptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let baset: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { baset = tn; }; + if (tn.kind == N_TPTR) { baset = tn.lhs; }; + } else { if (base.kind == N_DOT) { + baset = dotinnerstructptr(c, base); + };}; + if (baset == nil) { return nil; }; + if (baset.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *structinfo = structlookup(c, baset.str); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elemsizeof — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elemsizeof(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). primsize returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// nodeisunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn nodeisunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return typenodeisunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = localfindnode(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return typenodeisunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; + if (k == N_BIN) { + if (nodeisunsigned(c, n.lhs)) { return true; }; + return nodeisunsigned(c, n.rhs); + }; + if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C w6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return typenodeisunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; + return false; +}; +// ---- type-driven slot sizing ---------------------------------------- + +fn structlookup(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// primsize — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn primsize(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slotsize(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let lenn: *node = typ_n.rhs; + let elemn: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == N_TNAME) { + let en: str = elemn.str; + let ps: i32 = primsize(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slotsize(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// registerstruct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slotsize logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn fieldsize(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slotsize's TARRAY branch. + let lenn: *node = tnode.rhs; + let elemn: *node = tnode.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = fieldsize(c, elemn); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { + let si: *structinfo = amalloc(c.a, 64u64): *structinfo; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *fieldinfo = nil; + let tail: *fieldinfo = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = fieldsize(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collectstructs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + registerstruct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn isstrtyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn isstrtype(c: *cgen, t: *node) bool = { + if (isstrtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isstrtyperaw(r); +}; + +fn isslicetyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn isslicetype(c: *cgen, t: *node) bool = { + if (isslicetyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isslicetyperaw(r); +}; + +fn istaggedtype(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhstargetname — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhstargetname(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// taggedvariantindex — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let wantname: str = rhstargetname(c, rhs); + if (wantname.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, wantname)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let wantstr: bool = nodeisstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let visstr: bool = false; + if (v.kind == N_TNAME) { + if (isstrtype(c, v)) { visstr = true; }; + }; + if (visstr == wantstr) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c04153ad..de9f3857 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -3637,6 +3637,872 @@ export fn checkfile(c: *checker, file: *node) void = { }; }; +// MODULE: wcc +// selfhost/cmd/wcc/cgen_util.ww — split out of cgen.ww. +// +// General helpers used across cgen_expr / cgen_stmt / cgen_decl: +// - pushargsrev: per-call arg pushing +// - type predicates: isstr*/isslice*/istagged*/nodeis* families +// - field ops: fieldloadop, fieldstoreop +// - index helpers: indexbaseesz, dotinnerstructptr, elemsizeof +// - slot sizing: structlookup, primsize, slotsize, fieldsize, +// registerstruct, collectstructs +// - rhs helpers: rhstargetname, taggedvariantindex +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_util;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- expression cgen ------------------------------------------------- + +// pushargsrev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn pushargsrev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = pushargsrev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let baselocal: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + }; + }; + // base address → push + if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emitline("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let lenn: *node = tn.rhs; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { + emitline("\tMOVQ\t$"); + emituint(lenn.uval); + emitline(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emitline("\tMOVQ\t"); + emitoff((baselocal.off + 8): i64); + emitline("(BP), AX\n"); + }; + };};}; + }; + } else { + emitline("\tMOVQ\t$0, AX\n"); + };}; + emitline("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emitline("\tMOVQ\t$0, AX\n"); }; + emitline("\tPOPQ\tBX\n"); // hi + emitline("\tPOPQ\tCX\n"); // base + emitline("\tMOVQ\tBX, DX\n"); // DX = hi + emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emitline("\tPUSHQ\tDX\n"); // cap + emitline("\tPUSHQ\tDX\n"); // len + emitline("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + emitline("\tMOVQ\t"); + emitoff((off + 16): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (nodeisslice(c, arg)) { + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (nodeisstr(c, arg)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emitline("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn nodeisslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return isslicetype(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// nodeisstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn nodeisstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnretlookup(c, cnm); + return isstrtype(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + // Chained dot (`p.foo.bar`): use dotinnerstructptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let innert: *node = dotinnerstructptr(c, base); + if (innert != nil) { + if (innert.kind == N_TNAME) { sname = innert.str; }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return isstrtype(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return isstrtype(c, n.rhs); + }; + return false; +}; + +// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn typenameisunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn typenodeisunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; + return false; +}; + +// typeis8byteprimitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn typeis8byteprimitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (structlookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (primsize(nm) > 0) { return true; }; + return false; + }; + return false; +}; + +// typenameissigned — true for i8/i16/i32/i64/int/rune. +fn typenameissigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// fieldloadop — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn fieldloadop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (typenameissigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// fieldstoreop — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn fieldstoreop(f: *fieldinfo) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// indexbaseesz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn indexbaseesz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = localfindnode(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let innert: *node = tn; + if (tn.kind == N_TPTR) { innert = tn.lhs; }; + if (innert == nil) { return 8; }; + if (innert.kind == N_TNAME) { + if (streq(innert.str, "str")) { return 1; }; + }; + if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return 8; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = primsize(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dotinnerstructptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let baset: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { baset = tn; }; + if (tn.kind == N_TPTR) { baset = tn.lhs; }; + } else { if (base.kind == N_DOT) { + baset = dotinnerstructptr(c, base); + };}; + if (baset == nil) { return nil; }; + if (baset.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *structinfo = structlookup(c, baset.str); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elemsizeof — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elemsizeof(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). primsize returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// nodeisunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn nodeisunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = localfindnode(c, nm); + if (lc != nil) { return typenodeisunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = localfindnode(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return typenodeisunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; + if (k == N_BIN) { + if (nodeisunsigned(c, n.lhs)) { return true; }; + return nodeisunsigned(c, n.rhs); + }; + if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C w6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return typenodeisunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; + return false; +}; +// ---- type-driven slot sizing ---------------------------------------- + +fn structlookup(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// primsize — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn primsize(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slotsize(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let lenn: *node = typ_n.rhs; + let elemn: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == N_TNAME) { + let en: str = elemn.str; + let ps: i32 = primsize(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slotsize(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// registerstruct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slotsize logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn fieldsize(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + let si: *structinfo = structlookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slotsize's TARRAY branch. + let lenn: *node = tnode.rhs; + let elemn: *node = tnode.lhs; + let elen: i64 = 1i64; + if (lenn != nil) { + if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; + }; + let esz: i32 = fieldsize(c, elemn); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { + let si: *structinfo = amalloc(c.a, 64u64): *structinfo; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *fieldinfo = nil; + let tail: *fieldinfo = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = fieldsize(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collectstructs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + registerstruct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn isstrtyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn isstrtype(c: *cgen, t: *node) bool = { + if (isstrtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isstrtyperaw(r); +}; + +fn isslicetyperaw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn isslicetype(c: *cgen, t: *node) bool = { + if (isslicetyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isslicetyperaw(r); +}; + +fn istaggedtype(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhstargetname — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhstargetname(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// taggedvariantindex — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let wantname: str = rhstargetname(c, rhs); + if (wantname.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, wantname)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let wantstr: bool = nodeisstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let visstr: bool = false; + if (v.kind == N_TNAME) { + if (isstrtype(c, v)) { visstr = true; }; + }; + if (visstr == wantstr) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; + // MODULE: wcc // selfhost/cmd/wcc/cgen_expr.ww — split out of cgen.ww. // @@ -5071,6 +5937,260 @@ fn cgstmt(c: *cgen, n: *node) void = { }; +// MODULE: wcc +// selfhost/cmd/wcc/cgen_decl.ww — split out of cgen.ww. +// +// Houses the top-level emission glue: +// - scanlocals: frame pre-scan that counts each local `let` +// - cgfnparams: parameter spilling per SysV +// - cgfn: fn prologue + body + epilogue +// - cgfile: file-level entry (the exported driver) +// +// Bundler pulls this in transitively via cgen.ww; consumers don't +// need to `use cgen_decl;` directly. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scanlocals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match localadd's rounding: < 8 bumps to 8, then 8-align. + // scanlocals must agree with localadd or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. Same-name re-declarations share the first + // slot (see scanseenmark / localadd). + if (!scanseenmark(c, n.str)) { + let sz: i32 = slotsize(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + }; + // Match-arm binding (`case let v: T => ...`) gets a slot too. + // Crucially we do NOT dedup these against c.locals: C cgen + // handles a match as an expression with a by-value locals copy, + // so two separate matches in the same function each allocate + // their `v`/`e` slots fresh. Treating these as deduped would + // shrink the frame below what localadd then bumps it to. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (isstrtype(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; + if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; + if (n.cond != nil) { total += scanlocals(c, n.cond); }; + if (n.body != nil) { total += scanlocals(c, n.body); }; + if (n.els != nil) { total += scanlocals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scanlocals(c, m); + m = m.next; + }; + }; + return total; +}; + + +// ---- function-level cgen --------------------------------------------- + +fn cgfnparams(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (istaggedtype(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isslicetype(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = localadd(c, nm, 24, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + idx += 1; + } else { if (isstrtype(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = localadd(c, nm, 16, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + } else { + let off: i32 = localadd(c, nm, 8, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgeninit(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emitline("TEXT "); + if (fn_.exported == 0) { + if (fn_.module.len > 0) { + let isffi: bool = false; + let a: *node = fn_.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let an: str = a.str; + if (streq(an, "symbol")) { isffi = true; }; + }; + a = a.next; + }; + if (!isffi) { + os.write(1, fn_.module.ptr, fn_.module.len: u64); + os.write(1, ".".ptr, 1u64); + }; + }; + }; + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emitline(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scanlocals. + // Seed c.locals with param-name stubs so scanlocals dedups a + // re-declared `let ` in the body against the param's + // slot (matches C cgen). Stubs get cleared before emission. + let scanp: *node = fn_.list; + let frame: i32 = 0; + for (scanp != nil) { + if (scanp.kind == N_PARAM) { + if (istaggedtype(scanp.lhs)) { frame += 24; } + else { if (isslicetype(c, scanp.lhs)) { frame += 24; } + else { if (isstrtype(c, scanp.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + scanseenmark(c, scanp.str); + }; + scanp = scanp.next; + }; + if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; + // Drop the stubs so emission rebuilds c.locals with real offsets. + c.locals = nil; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emitint(frame: i64); + emitline("\n"); + + emitline("\tPUSHQ\tBP\n"); + emitline("\tMOVQ\tSP, BP\n"); + emitline("\tSUBQ\t$"); + emitint(frame: i64); + emitline(", SP\n"); + + cgfnparams(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cgfile(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collectaliases(c, file); + collectstructs(c, file); + collectdefs(c, file); + collectfnrets(c, file); + fficollect(c, file); + collectmods(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emitdatasection(c); + emitdefconstants(c, file); +}; + // MODULE: wcc // selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c. // @@ -5104,9 +6224,12 @@ use typ; use sym; use strconv; // Split files. Bundler pulls these in transitively so consumers only -// need `use cgen;`. +// need `use cgen;`. Order matters for the flat-bundle concat — utils +// first so cgen_expr/stmt/decl can reference helpers defined here. +use cgen_util; use cgen_expr; use cgen_stmt; +use cgen_decl; // ---- typedef alias registry ----------------------------------------- // @@ -5808,1085 +6931,6 @@ fn argregname(i: i32) str = { return "?"; }; -// ---- expression cgen ------------------------------------------------- - -// pushargsrev — recursively walks the arg list, evaluates rightmost -// first, and pushes. str args take two slots (ptr in AX, len in BX); -// the order on the stack so a left-to-right pop into argregs lands -// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the -// pop sequence then yields AX, then BX. -fn pushargsrev(c: *cgen, arg: *node) i32 = { - if (arg == nil) { return 0; }; - let rest: i32 = pushargsrev(c, arg.next); - // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header - // on the stack matching C cgen's sequence — push base, push hi, - // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). - if (arg.kind == N_SLICE) { - let base: *node = arg.lhs; - let lo: *node = arg.rhs; - let hi: *node = arg.cond; - let baselocal: *local = nil; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - baselocal = localfindnode(c, bn); - }; - }; - // base address → push - if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - emitline("\tLEAQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - emitline("\tMOVQ\t"); - emitoff(baselocal.off: i64); - emitline("(BP), AX\n"); - }; - } else { - cgexpr(c, base); - }; - emitline("\tPUSHQ\tAX\n"); - // hi (default base length) → push - if (hi != nil) { - cgexpr(c, hi); - } else { if (baselocal != nil) { - let tn: *node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == N_TARRAY) { - let lenn: *node = tn.rhs; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { - emitline("\tMOVQ\t$"); - emituint(lenn.uval); - emitline(", AX\n"); - }; - }; - } else { if (tn.kind == N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - } else { if (tn.kind == N_TNAME) { - if (streq(tn.str, "str")) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - }; - };};}; - }; - } else { - emitline("\tMOVQ\t$0, AX\n"); - };}; - emitline("\tPUSHQ\tAX\n"); - // lo (default 0) → AX - if (lo != nil) { cgexpr(c, lo); } - else { emitline("\tMOVQ\t$0, AX\n"); }; - emitline("\tPOPQ\tBX\n"); // hi - emitline("\tPOPQ\tCX\n"); // base - emitline("\tMOVQ\tBX, DX\n"); // DX = hi - emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len - emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr - emitline("\tPUSHQ\tDX\n"); // cap - emitline("\tPUSHQ\tDX\n"); // len - emitline("\tPUSHQ\tCX\n"); // ptr (top) - return rest + 3; - }; - // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in - // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop - // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). - if (arg.kind == N_IDENT) { - let nm: str = arg.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { - emitline("\tMOVQ\t"); - emitoff((off + 16): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff((off + 8): i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVQ\t"); - emitoff(off: i64); - emitline("(BP), AX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - }; - }; - cgexpr(c, arg); - if (nodeisslice(c, arg)) { - emitline("\tPUSHQ\tCX\n"); - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 3; - }; - if (nodeisstr(c, arg)) { - emitline("\tPUSHQ\tBX\n"); - emitline("\tPUSHQ\tAX\n"); - return rest + 2; - }; - emitline("\tPUSHQ\tAX\n"); - return rest + 1; -}; - -fn nodeisslice(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return isslicetype(c, lc.tnode); }; - return false; - }; - if (k == N_SLICE) { return true; }; - return false; -}; - -// nodeisstr — best-effort surface check: does this expression -// evaluate to a str value? Used to drive the call-arg push convention -// (str args take two slots: ptr + len). -fn nodeisstr(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_STRLIT) { return true; }; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { - let tnm: str = tn.str; - if (streq(tnm, "str")) { return true; }; - }; - }; - }; - return false; - }; - if (k == N_CALL) { - let callee: *node = n.lhs; - if (callee != nil) { - if (callee.kind == N_IDENT) { - let cnm: str = callee.str; - let rt: *node = fnretlookup(c, cnm); - return isstrtype(c, rt); - }; - }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - // `.ptr` is *u8 not str; `.len` is i32 not str. - if (streq(fld, "ptr")) { return false; }; - if (streq(fld, "len")) { return false; }; - if (streq(fld, "cap")) { return false; }; - if (base != nil) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - }; - }; - // Chained dot (`p.foo.bar`): use dotinnerstructptr - // to resolve the inner chain to the *struct it lands - // on, then look up `fld` in that struct. - if (base.kind == N_DOT) { - let innert: *node = dotinnerstructptr(c, base); - if (innert != nil) { - if (innert.kind == N_TNAME) { sname = innert.str; }; - }; - }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return isstrtype(c, fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { - return isstrtype(c, n.rhs); - }; - return false; -}; - -// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr. -fn typenameisunsigned(nm: str) bool = { - if (streq(nm, "u8")) { return true; }; - if (streq(nm, "u16")) { return true; }; - if (streq(nm, "u32")) { return true; }; - if (streq(nm, "u64")) { return true; }; - if (streq(nm, "uint")) { return true; }; - if (streq(nm, "uintptr")) { return true; }; - return false; -}; - -// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc. -fn typenodeisunsigned(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { return typenameisunsigned(t.str); }; - return false; -}; - -// typeis8byteprimitive — does this type take exactly one 8-byte -// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive -// padded up to 8) rather than a wider aggregate? Used by N_LET -// zero-init to mirror C cgen's "only zero if sz == 8 at the type -// level" rule. Strings (16), slices (24), tagged unions (>=16), -// tuples (16), structs (varies), arrays — all fall through to -// false here even when their *slot* rounds up to 8. -fn typeis8byteprimitive(c: *cgen, t: *node) bool = { - if (t == nil) { return false; }; - let k: i32 = t.kind; - if (k == N_TPTR) { return true; }; - if (k == N_TFN) { return true; }; - if (k == N_TCHAN) { return true; }; - if (k == N_TSLICE) { return false; }; - if (k == N_TARRAY) { return false; }; - if (k == N_TTUPLE) { return false; }; - if (k == N_TTAGGED){ return false; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return false; }; - // Struct alias: not a primitive even if the slot is 8B. - if (structlookup(c, nm) != nil) { return false; }; - // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). - // All of these get slot-padded to 8 and zero-init in C. - if (primsize(nm) > 0) { return true; }; - return false; - }; - return false; -}; - -// typenameissigned — true for i8/i16/i32/i64/int/rune. -fn typenameissigned(nm: str) bool = { - if (streq(nm, "i8")) { return true; }; - if (streq(nm, "i16")) { return true; }; - if (streq(nm, "i32")) { return true; }; - if (streq(nm, "i64")) { return true; }; - if (streq(nm, "int")) { return true; }; - if (streq(nm, "rune")) { return true; }; - return false; -}; - -// fieldloadop — pick the load instruction for a non-str struct -// field by its declared size + signedness. Mirrors the C cgen op -// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ -// for 8-byte). f might be nil for fields outside our struct registry. -fn fieldloadop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVZBQ"; }; - if (sz == 4) { - let t: *node = f.tnode; - if (t != nil) { - if (t.kind == N_TNAME) { - if (typenameissigned(t.str)) { return "MOVSXD"; }; - }; - }; - return "MOVL"; - }; - return "MOVQ"; -}; - -// fieldstoreop — pick the store instruction for a non-str struct -// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. -fn fieldstoreop(f: *fieldinfo) str = { - if (f == nil) { return "MOVQ"; }; - let sz: i32 = f.fsz; - if (sz == 1) { return "MOVB"; }; - if (sz == 4) { return "MOVL"; }; - return "MOVQ"; -}; - -// indexbaseesz — element size for `arr[i]` where the base is a -// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). -// For str the element is one byte; for `[]T` / `*[]T` we drill into -// the slice element type. -fn indexbaseesz(c: *cgen, base: *node) i32 = { - if (base == nil) { return 8; }; - if (base.kind != N_DOT) { return 8; }; - let fld: str = base.str; - let inner: *node = base.lhs; - if (inner == nil) { return 8; }; - if (inner.kind != N_IDENT) { return 8; }; - let nm: str = inner.str; - let lc: *local = localfindnode(c, nm); - if (lc == nil) { return 8; }; - let tn: *node = lc.tnode; - if (tn == nil) { return 8; }; - - // `.ptr` pseudo-field on str/slice → element of the str/slice. - if (streq(fld, "ptr")) { - let innert: *node = tn; - if (tn.kind == N_TPTR) { innert = tn.lhs; }; - if (innert == nil) { return 8; }; - if (innert.kind == N_TNAME) { - if (streq(innert.str, "str")) { return 1; }; - }; - if (innert.kind == N_TSLICE) { return elemsizeof(innert); }; - return 8; - }; - - // Generic struct field: if it's *T, element size is T's size. - let lkind: i32 = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TNAME) { sname = tn.str; }; - if (lkind == N_TPTR) { - let pinner: *node = tn.lhs; - if (pinner != nil) { - if (pinner.kind == N_TNAME) { sname = pinner.str; }; - }; - }; - if (sname.len == 0) { return 8; }; - let si: *structinfo = structlookup(c, sname); - if (si == nil) { return 8; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return 8; }; - if (ft.kind == N_TPTR) { - let elem: *node = ft.lhs; - if (elem != nil) { - if (elem.kind == N_TNAME) { - if (streq(elem.str, "str")) { return 16; }; - let ps: i32 = primsize(elem.str); - if (ps > 0) { return ps; }; - }; - }; - return 8; - }; - if (ft.kind == N_TSLICE) { return elemsizeof(ft); }; - // str-typed field: indexing yields one byte - // (`n.s[i]` where .s is str — matches C cgen's - // MOVZBQ for byte indexing). - if (ft.kind == N_TNAME) { - if (streq(ft.str, "str")) { return 1; }; - }; - return 8; - }; - fi = fi.finext; - }; - return 8; -}; - -// dotinnerstructptr — for an N_DOT whose lhs is a chain of dots -// or an N_IDENT, walk the chain and return the N_TNAME tnode of the -// struct that the chain dereferences to (i.e., for `r.sym` where -// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain -// doesn't resolve to a *struct. -// -// Used by the chained-DOT cgen path so `r.sym.val` knows the outer -// is a field of `lsym`. -fn dotinnerstructptr(c: *cgen, n: *node) *node = { - if (n == nil) { return nil; }; - if (n.kind != N_DOT) { return nil; }; - let base: *node = n.lhs; - let fld: str = n.str; - if (base == nil) { return nil; }; - - // Resolve base's struct tnode. - let baset: *node = nil; - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc == nil) { return nil; }; - let tn: *node = lc.tnode; - if (tn == nil) { return nil; }; - // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). - if (tn.kind == N_TNAME) { baset = tn; }; - if (tn.kind == N_TPTR) { baset = tn.lhs; }; - } else { if (base.kind == N_DOT) { - baset = dotinnerstructptr(c, base); - };}; - if (baset == nil) { return nil; }; - if (baset.kind != N_TNAME) { return nil; }; - - // Look up the struct, find the field, return the field's *struct. - let si: *structinfo = structlookup(c, baset.str); - if (si == nil) { return nil; }; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - let ft: *node = fi.tnode; - if (ft == nil) { return nil; }; - if (ft.kind != N_TPTR) { return nil; }; - let inner: *node = ft.lhs; - if (inner == nil) { return nil; }; - if (inner.kind != N_TNAME) { return nil; }; - return inner; - }; - fi = fi.finext; - }; - return nil; -}; - -// elemsizeof — given the type node of an indexable (`*T`, `[]T`, -// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ -// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). -fn elemsizeof(t: *node) i32 = { - if (t == nil) { return 1; }; - let k: i32 = t.kind; - let elem: *node = nil; - if (k == N_TPTR) { elem = t.lhs; }; - if (k == N_TSLICE) { elem = t.lhs; }; - if (k == N_TARRAY) { elem = t.lhs; }; - if (k == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return 1; }; - // Indexing a primitive name (rare): element size = the prim. - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - return 1; - }; - if (elem == nil) { return 1; }; - if (elem.kind == N_TNAME) { - let nm: str = elem.str; - // str element is 16B (ptr+len). primsize returns 0 for it. - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - }; - return 8; -}; - -// nodeisunsigned — best-effort cgen-time inference from the AST. We -// don't have a typed AST yet, so we walk surface nodes: -// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) -// N_IDENT — look up the local's declared type -// N_DOT — look up the field's declared type via struct reg -// N_BIN / N_UN — recurse: unsigned if either operand is unsigned -// N_CAST — use the cast target type -// -// Conservative: if we can't tell, return false (signed). The cost of -// being wrong here is byte-different asm vs C, not bad runtime. -fn nodeisunsigned(c: *cgen, n: *node) bool = { - if (n == nil) { return false; }; - let k: i32 = n.kind; - if (k == N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { return typenodeisunsigned(lc.tnode); }; - return false; - }; - if (k == N_DOT) { - let base: *node = n.lhs; - let fld: str = n.str; - if (base != nil) { - if (base.kind == N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *node = lc.tnode; - let lkind: i32 = -1; - if (tn != nil) { lkind = tn.kind; }; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == N_TPTR) { - let inner: *node = tn.lhs; - if (inner != nil) { - if (inner.kind == N_TNAME) { sname = inner.str; }; - }; - }; - if (lkind == N_TNAME) { sname = tn.str; }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return typenodeisunsigned(fi.tnode); - }; - fi = fi.finext; - }; - }; - }; - }; - }; - }; - return false; - }; - if (k == N_CAST) { return typenodeisunsigned(n.rhs); }; - if (k == N_BIN) { - if (nodeisunsigned(c, n.lhs)) { return true; }; - return nodeisunsigned(c, n.rhs); - }; - if (k == N_UN) { return nodeisunsigned(c, n.lhs); }; - // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. - // Walks the base local's declared type and pulls the element - // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the - // compare-codegen for `p[i] >= 48u8` falls back to signed JGE - // instead of JAE, diverging from C w6c on byte indexing. - if (k == N_INDEX) { - let base: *node = n.lhs; - if (base != nil) { - if (base.kind == N_IDENT) { - let lc: *local = localfindnode(c, base.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - let elem: *node = nil; - if (tn.kind == N_TPTR) { elem = tn.lhs; }; - if (tn.kind == N_TARRAY) { elem = tn.lhs; }; - if (tn.kind == N_TSLICE) { elem = tn.lhs; }; - if (elem != nil) { - return typenodeisunsigned(elem); - }; - }; - }; - }; - }; - return false; - }; - return false; -}; - -// ---- type-driven slot sizing ---------------------------------------- - -fn structlookup(c: *cgen, name: str) *structinfo = { - let s: *structinfo = c.structs; - for (s != nil) { - let sn: str = s.sname; - if (streq(sn, name)) { return s; }; - s = s.sinext; - }; - return nil; -}; - -// primsize — size in bytes of a primitive type name (or 0 if not -// recognised as a primitive — the caller falls back to other paths). -fn primsize(name: str) i32 = { - if (streq(name, "u8")) { return 1; }; - if (streq(name, "i8")) { return 1; }; - if (streq(name, "bool")) { return 1; }; - if (streq(name, "u16")) { return 2; }; - if (streq(name, "i16")) { return 2; }; - if (streq(name, "u32")) { return 4; }; - if (streq(name, "i32")) { return 4; }; - if (streq(name, "f32")) { return 4; }; - if (streq(name, "u64")) { return 8; }; - if (streq(name, "i64")) { return 8; }; - if (streq(name, "uint")) { return 8; }; - if (streq(name, "int")) { return 8; }; - if (streq(name, "uintptr")) { return 8; }; - if (streq(name, "f64")) { return 8; }; - if (streq(name, "rune")) { return 4; }; - if (streq(name, "void")) { return 0; }; - return 0; -}; - -fn slotsize(c: *cgen, typ_n: *node) i32 = { - if (typ_n == nil) { return 8; }; - let k: i32 = typ_n.kind; - if (k == N_TPTR) { return 8; }; - if (k == N_TFN) { return 8; }; - if (k == N_TCHAN) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TTUPLE) { return 16; }; - if (k == N_TTAGGED){ return 24; }; - if (k == N_TNAME) { - let nm: str = typ_n.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { - // Pad to 8 for stack slots — matches C cgen which spills - // every primitive into an 8-byte slot. - return 8; - }; - // Named struct lookup. - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TARRAY) { - let lenn: *node = typ_n.rhs; - let elemn: *node = typ_n.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = 8; - if (elemn != nil) { - if (elemn.kind == N_TNAME) { - let en: str = elemn.str; - let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; }; - }; - }; - return (esz: i64 * elen): i32; - }; - if (k == N_TSTRUCT) { - // Inline anonymous struct — sum of field sizes. - let f: *node = typ_n.list; - let total: i32 = 0; - for (f != nil) { - if (f.kind == N_TFIELD) { - total += slotsize(c, f.lhs); - }; - f = f.next; - }; - return total; - }; - return 8; -}; - -// registerstruct — compute field offsets + total size for a struct -// type-decl, store in c.structs. Field type sizes use the same -// slotsize logic (with primitives kept at their natural width — we -// only round to 8 for stack slots, not struct interiors). -fn fieldsize(c: *cgen, tnode: *node) i32 = { - if (tnode == nil) { return 8; }; - let k: i32 = tnode.kind; - if (k == N_TNAME) { - let nm: str = tnode.str; - if (streq(nm, "str")) { return 16; }; - let ps: i32 = primsize(nm); - if (ps > 0) { return ps; }; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.tot_size; }; - return 8; - }; - if (k == N_TPTR) { return 8; }; - if (k == N_TSLICE) { return 24; }; - if (k == N_TARRAY) { - // Same shape as slotsize's TARRAY branch. - let lenn: *node = tnode.rhs; - let elemn: *node = tnode.lhs; - let elen: i64 = 1i64; - if (lenn != nil) { - if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; }; - }; - let esz: i32 = fieldsize(c, elemn); - return (esz: i64 * elen): i32; - }; - return 8; -}; - -fn registerstruct(c: *cgen, name: str, tstruct: *node) void = { - let si: *structinfo = amalloc(c.a, 64u64): *structinfo; - si.sname = name; - si.fields = nil; - si.tot_size = 0; - let head: *fieldinfo = nil; - let tail: *fieldinfo = nil; - let off: i32 = 0; - let f: *node = tstruct.list; - for (f != nil) { - if (f.kind == N_TFIELD) { - let sz: i32 = fieldsize(c, f.lhs); - // Align to 8 for any field >= 4 bytes (matches our other - // cgen choices). i8/u8/bool may sit on odd byte offsets; - // the C cgen does similar best-effort packing. - let aln: i32 = 1; - if (sz >= 8) { aln = 8; } - else { if (sz >= 4) { aln = 4; } - else { if (sz >= 2) { aln = 2; }; }; }; - if ((off & (aln - 1)) != 0) { - off = (off + aln - 1) & ~(aln - 1); - }; - let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo; - fi.fname = f.str; - fi.foff = off; - fi.fsz = sz; - fi.tnode = f.lhs; - if (head == nil) { head = fi; tail = fi; } - else { tail.finext = fi; tail = fi; }; - off += sz; - }; - f = f.next; - }; - // Round total to 8 for stack-slot use. - if ((off & 7) != 0) { off = (off + 7) & ~7; }; - si.fields = head; - si.tot_size = off; - si.sinext = c.structs; - c.structs = si; -}; - -fn collectstructs(c: *cgen, file: *node) void = { - c.structs = nil; - if (file == nil) { return; }; - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_TYPEDECL) { - let body: *node = d.lhs; - if (body != nil) { - if (body.kind == N_TSTRUCT) { - registerstruct(c, d.str, body); - }; - }; - }; - d = d.next; - }; -}; - -// ---- frame pre-scan -------------------------------------------------- -// -// Recursively walks the body to count every local `let`. Each gets a -// slot sized by slotsize(typ); 8-byte default. Match-bindings + for- -// init lets count too. Params are added by the cgfn driver. - -fn scanlocals(c: *cgen, n: *node) i32 = { - if (n == nil) { return 0; }; - let total: i32 = 0; - if (n.kind == N_LET) { - // Match localadd's rounding: < 8 bumps to 8, then 8-align. - // scanlocals must agree with localadd or the prologue - // SUBQ undersizes the frame and lets overflow into the - // caller's stack — corrupting whatever's at -frameSize..-1 - // of the caller. Same-name re-declarations share the first - // slot (see scanseenmark / localadd). - if (!scanseenmark(c, n.str)) { - let sz: i32 = slotsize(c, n.lhs); - if (sz < 8) { sz = 8; }; - if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; - total += sz; - }; - }; - // Match-arm binding (`case let v: T => ...`) gets a slot too. - // Crucially we do NOT dedup these against c.locals: C cgen - // handles a match as an expression with a by-value locals copy, - // so two separate matches in the same function each allocate - // their `v`/`e` slots fresh. Treating these as deduped would - // shrink the frame below what localadd then bumps it to. - if (n.kind == N_MCASE) { - let bn: str = n.str; - if (bn.len > 0) { - let pat: *node = n.lhs; - if (pat != nil) { - if (isstrtype(c, pat)) { total += 16; } - else { total += 8; }; - }; - }; - }; - if (n.lhs != nil) { total += scanlocals(c, n.lhs); }; - if (n.rhs != nil) { total += scanlocals(c, n.rhs); }; - if (n.cond != nil) { total += scanlocals(c, n.cond); }; - if (n.body != nil) { total += scanlocals(c, n.body); }; - if (n.els != nil) { total += scanlocals(c, n.els); }; - if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - total += scanlocals(c, m); - m = m.next; - }; - }; - return total; -}; - -// ---- function-level cgen --------------------------------------------- - -// isstrtype — true when the type expr resolves (through any -// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the -// alias chain registered at file load. -fn isstrtyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TNAME) { - let nm: str = t.str; - if (streq(nm, "str")) { return true; }; - }; - return false; -}; - -fn isstrtype(c: *cgen, t: *node) bool = { - if (isstrtyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isstrtyperaw(r); -}; - -fn isslicetyperaw(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TSLICE) { return true; }; - return false; -}; - -fn isslicetype(c: *cgen, t: *node) bool = { - if (isslicetyperaw(t)) { return true; }; - if (c == nil) { return false; }; - let r: *node = resolvetype(c, t); - return isslicetyperaw(r); -}; - -fn istaggedtype(t: *node) bool = { - if (t == nil) { return false; }; - if (t.kind == N_TTAGGED) { return true; }; - return false; -}; - -// rhstargetname — for a returned value, what's its declared (or -// surface-inferred) type name? `expr: T` casts dictate T directly; -// bare strlit/intlit fall back to a primitive name. -fn rhstargetname(c: *cgen, rhs: *node) str = { - let nm: str; - nm.ptr = nil; nm.len = 0; - if (rhs == nil) { return nm; }; - if (rhs.kind == N_CAST) { - let t: *node = rhs.rhs; - if (t != nil) { - if (t.kind == N_TNAME) { return t.str; }; - }; - return nm; - }; - if (rhs.kind == N_STRLIT) { return "str"; }; - if (rhs.kind == N_IDENT) { - let lc: *local = localfindnode(c, rhs.str); - if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == N_TNAME) { return tn.str; }; - }; - }; - }; - return nm; -}; - -// taggedvariantindex — given the tagged-union type expr and the -// returned value's surface type, find the matching variant's 0-based -// index. Compare by exact type name first; if no match, fall back to -// "any str-shape variant matches an str-typed value". -fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { - if (tagged == nil) { return -1; }; - if (rhs == nil) { return -1; }; - let wantname: str = rhstargetname(c, rhs); - if (wantname.len > 0) { - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - if (v.kind == N_TNAME) { - if (streq(v.str, wantname)) { return idx; }; - }; - v = v.next; - idx += 1; - }; - }; - // Fallback: by str-shape (resolves aliases). - let wantstr: bool = nodeisstr(c, rhs); - let v: *node = tagged.list; - let idx: i32 = 0; - for (v != nil) { - let visstr: bool = false; - if (v.kind == N_TNAME) { - if (isstrtype(c, v)) { visstr = true; }; - }; - if (visstr == wantstr) { return idx; }; - v = v.next; - idx += 1; - }; - return -1; -}; - -fn cgfnparams(c: *cgen, params: *node) void = { - let p: *node = params; - let idx: i32 = 0; - for (p != nil) { - if (p.kind == N_PARAM) { - let nm: str = p.str; - if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isslicetype(c, p.lhs)) { - // slice param: 3 regs (ptr, len, cap), 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; - } else { if (isstrtype(c, p.lhs)) { - // str param: passed in two regs (ptr, len). - // Slot is 16 bytes; ptr at off+0, len at off+8. - let off: i32 = localadd(c, nm, 16, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - } else { - let off: i32 = localadd(c, nm, 8, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - };};}; - }; - p = p.next; - }; -}; - -fn cgfn(c: *cgen, fn_: *node) void = { - cgeninit(c, c.a); - c.fn_name = fn_.str; - c.fn_ret = fn_.lhs; - - emitline("TEXT "); - if (fn_.exported == 0) { - if (fn_.module.len > 0) { - let isffi: bool = false; - let a: *node = fn_.attr; - for (a != nil) { - if (a.kind == N_ATTR) { - let an: str = a.str; - if (streq(an, "symbol")) { isffi = true; }; - }; - a = a.next; - }; - if (!isffi) { - os.write(1, fn_.module.ptr, fn_.module.len: u64); - os.write(1, ".".ptr, 1u64); - }; - }; - }; - let nm: str = fn_.str; - os.write(1, nm.ptr, nm.len: u64); - emitline(",$"); - - // Pre-scan total frame: 24 bytes per slice param, 16 per str - // param, 8 per other param, plus per-let from scanlocals. - // Seed c.locals with param-name stubs so scanlocals dedups a - // re-declared `let ` in the body against the param's - // slot (matches C cgen). Stubs get cleared before emission. - let scanp: *node = fn_.list; - let frame: i32 = 0; - for (scanp != nil) { - if (scanp.kind == N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } - else { if (isslicetype(c, scanp.lhs)) { frame += 24; } - else { if (isstrtype(c, scanp.lhs)) { frame += 16; } - else { frame += 8; }; }; }; - scanseenmark(c, scanp.str); - }; - scanp = scanp.next; - }; - if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; - // Drop the stubs so emission rebuilds c.locals with real offsets. - c.locals = nil; - if ((frame & 15) != 0) { - frame = (frame + 15) & ~15; - }; - emitint(frame: i64); - emitline("\n"); - - emitline("\tPUSHQ\tBP\n"); - emitline("\tMOVQ\tSP, BP\n"); - emitline("\tSUBQ\t$"); - emitint(frame: i64); - emitline(", SP\n"); - - cgfnparams(c, fn_.list); - c.last_was_return = 0; - if (fn_.body != nil) { cgstmt(c, fn_.body); }; - - if (c.last_was_return == 0) { - // Zero AX before the fall-through return — matches C cgen, - // which always emits this so void-returning fns don't leak - // a stale callee value to their caller. - emitline("\tMOVQ\t$0, AX\n"); - emitline("\tMOVQ\tBP, SP\n"); - emitline("\tPOPQ\tBP\n"); - emitline("\tRET\n"); - }; -}; - -// ---- file-level entry ------------------------------------------------ - -export fn cgfile(c: *cgen, file: *node) void = { - if (file == nil) { return; }; - c.strlits = nil; - c.strlit_seq = 0; - collectaliases(c, file); - collectstructs(c, file); - collectdefs(c, file); - collectfnrets(c, file); - fficollect(c, file); - collectmods(c, file); - let d: *node = file.list; - for (d != nil) { - if (d.kind == N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); - }; - }; - d = d.next; - }; - emitdatasection(c); - emitdefconstants(c, file); -}; - // MODULE: wwdump // selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. //