selfhost/cmd/wcc/cgenstmt+test: emit slice-form alloc let-init shortcut

Cstage's cmd/w6c/cgen.c:6363-6411 special-cases `let s: []T =
alloc([], n)!;` to inline rt_alloc + null-check + exit(1) + slice
header build, avoiding a generic call-then-store path. Wwstage's
cglet had no mirror — pre-#31 the path was rejected at check, but
once #31 made the check side accept it, the cgen side would have
silently miscompiled. Mirror added at cgenstmt.ww cglet rhs head,
emitting byte-identical asm.

Element size goes through elemsizeofc so str (16), structs, and
tagged aliases all match cstage's lu->sub->size uniformly — the
defensive path matters because check today only allows []u8, but
relaxing that is its own task.

Test exercises the path: writes to s[0] and s[15], reads back. Would
SIGSEGV on a junk header. 994 + 995 byte-identity green.
This commit is contained in:
2026-05-20 00:28:27 +09:00
parent 6f10c832a4
commit 30a0856fe5
4 changed files with 276 additions and 0 deletions

View File

@@ -18353,6 +18353,91 @@ fn cglet(c: *cgen, n: *node) void = {
let off: i32 = localadd(c, nm, sz, tn);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// `let s: []T = alloc([], n)!;` shortcut (#32). Mirror of
// cstage cgen.c N_LET arrlit-empty + N_TRYUNW branch: allocate
// n*esz bytes via rt_alloc, exit(1) on null, then build the
// {ptr, 0, n} slice header in the let slot. The `!` wraps the
// builtin's `([]T | nomem)` return; walk into the N_TRYUNW to
// keep the direct-store fast path rather than falling through
// to cgalloc (which models scalar alloc and would land an 8B
// region and a junk slice header).
{
let scall: *node = nil;
let viatryunw: bool = false;
if (rhs.kind == nkind.N_TRYUNW) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == nkind.N_CALL) {
scall = rhs.lhs;
viatryunw = true;
};
};
};
let shapeok: bool = false;
if (scall != nil && tn != nil
&& tn.kind == nkind.N_TSLICE && sz == 24) {
let callee: *node = scall.lhs;
let a0: *node = scall.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil && a0 != nil && a1 != nil
&& a2 == nil) {
if (callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0.kind == nkind.N_ARRLIT
&& a0.list == nil) {
shapeok = true;
};
};
};
if (shapeok) {
// #32: cstage uses `lu->sub->size` (cgen.c:6387), so
// the element width must resolve struct/tagged/alias
// names too — not just primitives. elemsizeofc follows
// TNAME through structlookup/aliaslookup, matching the
// cstage path byte-identically. A bare primsize/slotsize
// fork would silently land esz=1 on `[]point` (today
// blocked at check.c, but the defensive cgen path must
// stay byte-identical with cstage for the moment check
// relaxes).
let esz: i32 = elemsizeofc(c, tn);
let count: *node = scall.list.next;
cgexpr(c, count);
emitline("\tPUSHQ\tAX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", BX\n");
emitline("\tIMULQ\tBX, AX\n");
};
emitline("\tMOVQ\tAX, DI\n");
emitline("\tCALL\trt_alloc(SB)\n");
if (viatryunw) {
let okl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(okl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n");
emitline("\tMOVQ\t$60, AX\n");
emitline("\tSYSCALL\n");
emitlabel(okl);
};
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t$0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
};
// Tagged-union init: delegate to cgwidentaggedstore, which
// handles nullable fold, tagged source (ident or AX/DX/CX
// ABI call), struct payload (literal/ident), str payload,

View File

@@ -574,6 +574,91 @@ fn cglet(c: *cgen, n: *node) void = {
let off: i32 = localadd(c, nm, sz, tn);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// `let s: []T = alloc([], n)!;` shortcut (#32). Mirror of
// cstage cgen.c N_LET arrlit-empty + N_TRYUNW branch: allocate
// n*esz bytes via rt_alloc, exit(1) on null, then build the
// {ptr, 0, n} slice header in the let slot. The `!` wraps the
// builtin's `([]T | nomem)` return; walk into the N_TRYUNW to
// keep the direct-store fast path rather than falling through
// to cgalloc (which models scalar alloc and would land an 8B
// region and a junk slice header).
{
let scall: *node = nil;
let viatryunw: bool = false;
if (rhs.kind == nkind.N_TRYUNW) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == nkind.N_CALL) {
scall = rhs.lhs;
viatryunw = true;
};
};
};
let shapeok: bool = false;
if (scall != nil && tn != nil
&& tn.kind == nkind.N_TSLICE && sz == 24) {
let callee: *node = scall.lhs;
let a0: *node = scall.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil && a0 != nil && a1 != nil
&& a2 == nil) {
if (callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0.kind == nkind.N_ARRLIT
&& a0.list == nil) {
shapeok = true;
};
};
};
if (shapeok) {
// #32: cstage uses `lu->sub->size` (cgen.c:6387), so
// the element width must resolve struct/tagged/alias
// names too — not just primitives. elemsizeofc follows
// TNAME through structlookup/aliaslookup, matching the
// cstage path byte-identically. A bare primsize/slotsize
// fork would silently land esz=1 on `[]point` (today
// blocked at check.c, but the defensive cgen path must
// stay byte-identical with cstage for the moment check
// relaxes).
let esz: i32 = elemsizeofc(c, tn);
let count: *node = scall.list.next;
cgexpr(c, count);
emitline("\tPUSHQ\tAX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", BX\n");
emitline("\tIMULQ\tBX, AX\n");
};
emitline("\tMOVQ\tAX, DI\n");
emitline("\tCALL\trt_alloc(SB)\n");
if (viatryunw) {
let okl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(okl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n");
emitline("\tMOVQ\t$60, AX\n");
emitline("\tSYSCALL\n");
emitlabel(okl);
};
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t$0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
};
// Tagged-union init: delegate to cgwidentaggedstore, which
// handles nullable fold, tagged source (ident or AX/DX/CX
// ABI call), struct payload (literal/ident), str payload,

View File

@@ -18353,6 +18353,91 @@ fn cglet(c: *cgen, n: *node) void = {
let off: i32 = localadd(c, nm, sz, tn);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// `let s: []T = alloc([], n)!;` shortcut (#32). Mirror of
// cstage cgen.c N_LET arrlit-empty + N_TRYUNW branch: allocate
// n*esz bytes via rt_alloc, exit(1) on null, then build the
// {ptr, 0, n} slice header in the let slot. The `!` wraps the
// builtin's `([]T | nomem)` return; walk into the N_TRYUNW to
// keep the direct-store fast path rather than falling through
// to cgalloc (which models scalar alloc and would land an 8B
// region and a junk slice header).
{
let scall: *node = nil;
let viatryunw: bool = false;
if (rhs.kind == nkind.N_TRYUNW) {
if (rhs.lhs != nil) {
if (rhs.lhs.kind == nkind.N_CALL) {
scall = rhs.lhs;
viatryunw = true;
};
};
};
let shapeok: bool = false;
if (scall != nil && tn != nil
&& tn.kind == nkind.N_TSLICE && sz == 24) {
let callee: *node = scall.lhs;
let a0: *node = scall.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil && a0 != nil && a1 != nil
&& a2 == nil) {
if (callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0.kind == nkind.N_ARRLIT
&& a0.list == nil) {
shapeok = true;
};
};
};
if (shapeok) {
// #32: cstage uses `lu->sub->size` (cgen.c:6387), so
// the element width must resolve struct/tagged/alias
// names too — not just primitives. elemsizeofc follows
// TNAME through structlookup/aliaslookup, matching the
// cstage path byte-identically. A bare primsize/slotsize
// fork would silently land esz=1 on `[]point` (today
// blocked at check.c, but the defensive cgen path must
// stay byte-identical with cstage for the moment check
// relaxes).
let esz: i32 = elemsizeofc(c, tn);
let count: *node = scall.list.next;
cgexpr(c, count);
emitline("\tPUSHQ\tAX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", BX\n");
emitline("\tIMULQ\tBX, AX\n");
};
emitline("\tMOVQ\tAX, DI\n");
emitline("\tCALL\trt_alloc(SB)\n");
if (viatryunw) {
let okl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(okl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n");
emitline("\tMOVQ\t$60, AX\n");
emitline("\tSYSCALL\n");
emitlabel(okl);
};
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t$0, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
};
// Tagged-union init: delegate to cgwidentaggedstore, which
// handles nullable fold, tagged source (ident or AX/DX/CX
// ABI call), struct payload (literal/ident), str payload,

View File

@@ -43,6 +43,25 @@ fn allocbox() (*point | nomem) = {
return p;
};
// Task #32: slice-form `let s: []T = alloc([], n)!;` shortcut. Both
// stages must lower to `n*esz` bytes via rt_alloc, abort on null, and
// build a {ptr, 0, n} header in the let slot. Pre-#32 wwstage fell
// through to cgalloc, allocating 8B and dropping the slice header
// entirely — silent miscompile. Cap-only would pass on a junk header
// pointing to dead memory; write-then-read on s[0]/s[cap-1] proves
// the ptr field is a real rt_alloc'd region (would SIGSEGV otherwise).
// IMULQ esz path is currently unreachable from user code — check.c
// pins the alloc shape to []u8 (cstage check.c:1052-1082) — so this
// row only exercises esz=1; the cgen elemsizeofc resolution stays
// defensive against a future check.c relaxation.
fn sliceshort() i32 = {
let s: []u8 = alloc([], 16)!;
if (s.cap != 16) { return -1i32; };
s[0] = 42u8;
s[15] = 99u8;
return (s[0]: i32) + (s[15]: i32);
};
export fn main() i32 = {
let rc: i32 = 0;
match (caller(0i64)) {
@@ -74,5 +93,7 @@ export fn main() i32 = {
rc = 5;
};
};
if (sliceshort() != 141i32) { rc = 6; }
else { fmt.println("sliceshort ok"); };
return rc;
};