diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 0fdd1738..16d3512e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -398,9 +398,9 @@ struct LetVar { static LetVar *letvars; /* Slot size for a top-level `let` of type t, or 0 if the type isn't - * supported as a writable global yet. Floats (MOVSS/SD) and slice/ - * struct/tagged unions are deferred. enums route through their - * storage type. Keep this tight — extending it requires the matching + * supported as a writable global yet. Floats (MOVSS/SD) and struct/ + * tagged unions are deferred. enums route through their storage + * type. Keep this tight — extending it requires the matching * load/store code below. */ static int let_emit_size(Type *t) @@ -417,6 +417,8 @@ let_emit_size(Type *t) return 8; case TY_STR: return 16; /* {ptr, len}; literal-strlit init NYI. */ + case TY_SLICE: + return 24; /* {ptr, len, cap}; no init only. */ default: return 0; } @@ -433,6 +435,16 @@ let_isstr(Type *t) return u && u->kind == TY_STR; } +/* Is the unwrapped type a slice? Slice globals flow as the (AX, BX, + * CX) triple — same as the local ABI. */ +static int +let_isslice(Type *t) +{ + if (t == NULL) return 0; + Type *u = (t->kind == TY_NAMED) ? t->under : t; + return u && u->kind == TY_SLICE; +} + static int decl_has_ffisym(Node *d) { @@ -764,13 +776,20 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_BX)); goto ident_done; } - if (let_islet(n->str) && let_isstr(n->type)) { - /* Top-level str global: load both halves via - * its address (the asm has no `name+8(SB)` - * operand form). */ + if (let_islet(n->str) + && (let_isstr(n->type) || let_isslice(n->type))) { + /* Top-level str/slice global: load each half + * via its address (the asm has no `name+8(SB)` + * operand form). Slice has a third 8B (cap) + * — the address holder CX gets overwritten by + * the cap as the last step, after we no longer + * need it. */ + int is_slice = let_isslice(n->type); ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX)); + if (is_slice) + ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX)); goto ident_done; } ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); @@ -3662,7 +3681,10 @@ emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz) * str lets (16B): emit 16 zero bytes when there is no init (or * the init is `nil` / `""`). A non-empty strlit init would need a * compile-time .data → .text relocation (asm doesn't support that - * yet); we silently skip and let the link fail loudly. */ + * yet); we silently skip and let the link fail loudly. + * + * Slice lets (24B): no-init only — the slot is zero. There's no + * literal slice syntax to honour, so this is the natural shape. */ static void emit_lets(Cg *c, FILE *out, Node *file) { @@ -3687,10 +3709,11 @@ emit_lets(Cg *c, FILE *out, Node *file) emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); continue; } - /* Multi-word (str, 16B). Only zero-init shapes are - * supported: no rhs, or `nil`, or `""` (which interns to - * a strlit but we still emit a zero header — the program - * has to assign a real strlit at runtime to use it). */ + /* Multi-word (str=16, slice=24). Only zero-init shapes + * are supported: no rhs, or `nil`, or `""` (which interns + * to a strlit but we still emit a zero header — the + * program has to assign a real strlit/slice at runtime + * to use it). */ if (d->rhs != NULL) { Node *r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6e29a686..80f21ee8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6130,17 +6130,25 @@ fn cgident(c: *cgen, n: *node) void = { }; // Top-level mutable `let` — RIP-relative load from its DATAW // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for - // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str - // globals so both ptr and len land in (AX, BX). Names that - // aren't lets either (typos, never-defined) drop through to - // the silent return. + // scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence + // for str / slice globals so the ABI pair / triple lands in + // (AX, BX[, CX]). Names that aren't lets either (typos, + // never-defined) drop through to the silent return. if (isletvar(c, nm)) { - if (letvarisstr(c, nm)) { + let isstr: bool = letvarisstr(c, nm); + let issl: bool = letvarisslice(c, nm); + if (isstr || issl) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); + if (issl) { + // Overwrites the address holder with the + // cap as the last step — CX is no longer + // needed once both ptr/len are loaded. + emitline("\tMOVQ\t16(CX), CX\n"); + }; return; }; emitline("\tMOVQ\t"); @@ -6608,17 +6616,22 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // Top-level str global field access — load .ptr / .len via - // &name(SB) into CX, then MOVQ delta(CX), AX. Without this - // the module-qualified fallback below would mis-emit - // `MOVQ (SB), AX`. + // Top-level str/slice global field access — load .ptr / .len + // (and .cap for slices) via &name(SB) into CX, then MOVQ + // delta(CX), AX. Without this the module-qualified fallback + // below would mis-emit `MOVQ (SB), AX`. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (isletvar(c, lhs.str)) { - if (letvarisstr(c, lhs.str)) { + let isstr: bool = letvarisstr(c, lhs.str); + let issl: bool = letvarisslice(c, lhs.str); + if (isstr || issl) { let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; + if (issl) { + if (streq(fld, "cap")) { delta = 16; }; + }; if (delta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); @@ -9036,11 +9049,13 @@ fn letscalarprim(nm: str) bool = { // aliases so byte output matches C cgen, which resolves Type kinds. // 8 → scalar (literal init supported) // 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind == nkind.N_TSLICE) { return 24; }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -9106,6 +9121,24 @@ fn letvarisstr(c: *cgen, name: str) bool = { return false; }; +// letvarisslice — is the named top-level let a slice global? +// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the +// load sequence ends with `MOVQ 16(CX), CX` (overwrites the +// address holder with the cap). Mirrors C cgen's `let_isslice`. +fn letvarisslice(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + if (t == nil) { return false; }; + if (t.kind == nkind.N_TSLICE) { return true; }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -9215,6 +9248,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + if (sz == 24) { + // Slice: zero-init only (no slice-literal + // syntax to honour). Any rhs other than + // `nil` is skipped → undefined symbol at + // link. + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 24) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 57b5fecc..d42511a9 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -586,11 +586,13 @@ fn letscalarprim(nm: str) bool = { // aliases so byte output matches C cgen, which resolves Type kinds. // 8 → scalar (literal init supported) // 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind == nkind.N_TSLICE) { return 24; }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -656,6 +658,24 @@ fn letvarisstr(c: *cgen, name: str) bool = { return false; }; +// letvarisslice — is the named top-level let a slice global? +// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the +// load sequence ends with `MOVQ 16(CX), CX` (overwrites the +// address holder with the cap). Mirrors C cgen's `let_isslice`. +fn letvarisslice(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + if (t == nil) { return false; }; + if (t.kind == nkind.N_TSLICE) { return true; }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -765,6 +785,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + if (sz == 24) { + // Slice: zero-init only (no slice-literal + // syntax to honour). Any rhs other than + // `nil` is skipped → undefined symbol at + // link. + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 24) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index f287fd1e..45e972a4 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -402,17 +402,25 @@ fn cgident(c: *cgen, n: *node) void = { }; // Top-level mutable `let` — RIP-relative load from its DATAW // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for - // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str - // globals so both ptr and len land in (AX, BX). Names that - // aren't lets either (typos, never-defined) drop through to - // the silent return. + // scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence + // for str / slice globals so the ABI pair / triple lands in + // (AX, BX[, CX]). Names that aren't lets either (typos, + // never-defined) drop through to the silent return. if (isletvar(c, nm)) { - if (letvarisstr(c, nm)) { + let isstr: bool = letvarisstr(c, nm); + let issl: bool = letvarisslice(c, nm); + if (isstr || issl) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); + if (issl) { + // Overwrites the address holder with the + // cap as the last step — CX is no longer + // needed once both ptr/len are loaded. + emitline("\tMOVQ\t16(CX), CX\n"); + }; return; }; emitline("\tMOVQ\t"); @@ -880,17 +888,22 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // Top-level str global field access — load .ptr / .len via - // &name(SB) into CX, then MOVQ delta(CX), AX. Without this - // the module-qualified fallback below would mis-emit - // `MOVQ (SB), AX`. + // Top-level str/slice global field access — load .ptr / .len + // (and .cap for slices) via &name(SB) into CX, then MOVQ + // delta(CX), AX. Without this the module-qualified fallback + // below would mis-emit `MOVQ (SB), AX`. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (isletvar(c, lhs.str)) { - if (letvarisstr(c, lhs.str)) { + let isstr: bool = letvarisstr(c, lhs.str); + let issl: bool = letvarisslice(c, lhs.str); + if (isstr || issl) { let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; + if (issl) { + if (streq(fld, "cap")) { delta = 16; }; + }; if (delta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b84d1e49..48b31218 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6130,17 +6130,25 @@ fn cgident(c: *cgen, n: *node) void = { }; // Top-level mutable `let` — RIP-relative load from its DATAW // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for - // scalar lets, plus the (LEAQ, MOVQ, MOVQ) sequence for str - // globals so both ptr and len land in (AX, BX). Names that - // aren't lets either (typos, never-defined) drop through to - // the silent return. + // scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence + // for str / slice globals so the ABI pair / triple lands in + // (AX, BX[, CX]). Names that aren't lets either (typos, + // never-defined) drop through to the silent return. if (isletvar(c, nm)) { - if (letvarisstr(c, nm)) { + let isstr: bool = letvarisstr(c, nm); + let issl: bool = letvarisslice(c, nm); + if (isstr || issl) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); + if (issl) { + // Overwrites the address holder with the + // cap as the last step — CX is no longer + // needed once both ptr/len are loaded. + emitline("\tMOVQ\t16(CX), CX\n"); + }; return; }; emitline("\tMOVQ\t"); @@ -6608,17 +6616,22 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // Top-level str global field access — load .ptr / .len via - // &name(SB) into CX, then MOVQ delta(CX), AX. Without this - // the module-qualified fallback below would mis-emit - // `MOVQ (SB), AX`. + // Top-level str/slice global field access — load .ptr / .len + // (and .cap for slices) via &name(SB) into CX, then MOVQ + // delta(CX), AX. Without this the module-qualified fallback + // below would mis-emit `MOVQ (SB), AX`. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (isletvar(c, lhs.str)) { - if (letvarisstr(c, lhs.str)) { + let isstr: bool = letvarisstr(c, lhs.str); + let issl: bool = letvarisslice(c, lhs.str); + if (isstr || issl) { let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; + if (issl) { + if (streq(fld, "cap")) { delta = 16; }; + }; if (delta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); @@ -9036,11 +9049,13 @@ fn letscalarprim(nm: str) bool = { // aliases so byte output matches C cgen, which resolves Type kinds. // 8 → scalar (literal init supported) // 16 → str (only zero-init / nil / "" supported) +// 24 → slice (only zero-init supported) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; + if (t.kind == nkind.N_TSLICE) { return 24; }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -9106,6 +9121,24 @@ fn letvarisstr(c: *cgen, name: str) bool = { return false; }; +// letvarisslice — is the named top-level let a slice global? +// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the +// load sequence ends with `MOVQ 16(CX), CX` (overwrites the +// address holder with the cap). Mirrors C cgen's `let_isslice`. +fn letvarisslice(c: *cgen, name: str) bool = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + if (t == nil) { return false; }; + if (t.kind == nkind.N_TSLICE) { return true; }; + return false; + }; + lv = lv.lvnext; + }; + return false; +}; + // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { @@ -9215,6 +9248,35 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + if (sz == 24) { + // Slice: zero-init only (no slice-literal + // syntax to honour). Any rhs other than + // `nil` is skipped → undefined symbol at + // link. + let ok: bool = true; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_NIL) { ok = true; }; + }; + }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < 24) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c index 80aee2f2..e5280570 100644 --- a/test/wcc/630_let_global.c +++ b/test/wcc/630_let_global.c @@ -113,6 +113,31 @@ static const struct fixture fixtures[] = { "};\n", 7, }, + { + "slice-zeroinit", + /* Slice globals start as {nil, 0, 0}. .len and .cap both + * read zero; .ptr is nil but we don't dereference it. */ + "let buf: []u8;\n" + "fn main() i32 = {\n" + "\treturn (buf.len + buf.cap): i32;\n" + "};\n", + 0, + }, + { + "slice-cap-via-raw-pointer", + /* Drive the slice header through a raw u64 pointer cast + * — the language has no slice-reassignment expression + * yet, so this is the only way to fill the header from + * ww source today. .cap reads back as the value we + * wrote. */ + "let buf: []u8;\n" + "fn main() i32 = {\n" + "\tlet p: *u64 = (&buf): *u64;\n" + "\tp[2] = 99u64;\n" + "\treturn buf.cap: i32;\n" + "};\n", + 99, + }, { NULL, NULL, 0 } };