From 30a0856fe5bf6681aff1a15b31f71ed0d654e428 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 00:28:27 +0900 Subject: [PATCH] selfhost/cmd/wcc/cgenstmt+test: emit slice-form alloc let-init shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- selfhost/cmd/w6c/main.combined.ww | 85 ++++++++++++++++++++++++++++ selfhost/cmd/wcc/cgenstmt.ww | 85 ++++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 85 ++++++++++++++++++++++++++++ selfhost/test/tagged_ptr_ret.ww | 21 +++++++ 4 files changed, 276 insertions(+) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 15e516a7..beeacc49 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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, diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index d2d24a9e..ae9587d9 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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, diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 868d393d..1b67d579 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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, diff --git a/selfhost/test/tagged_ptr_ret.ww b/selfhost/test/tagged_ptr_ret.ww index 4cd5b547..8ca42ef9 100644 --- a/selfhost/test/tagged_ptr_ret.ww +++ b/selfhost/test/tagged_ptr_ret.ww @@ -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; };