From 61705fb39e771bd66293b88960edc48037aaf512 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 20:25:14 +0900 Subject: [PATCH] cmd+rt+selfhost+test: graduate alloc to (*T | nomem) / ([]T | nomem) Per Hare convention, alloc is a typed builtin that returns a tagged union carrying nomem as the OOM variant. Callers spell their policy: `alloc(T)!` aborts on OOM (the old behavior), `alloc(T)?` propagates when the enclosing fn already returns nomem. cstage: check builds TY_TAGGED{*T | nomem} (or {[]T | nomem}); cgen emits AX=tag, DX=ptr per the general tagged-return ABI (the (*T|!void) nullable-ptr fold gated in ea76ee4 keeps this clean). wwstage cgalloc mirrors. rt/alloc.s zeroes AX on syscall error so the builtin's null check sees a clean 0 instead of mmap's -errno leaking through as a poisoned pointer. Migration: 3 `!` sites in test/wcc/700_e2e.c, 1 `!` site in rt/ensure.ww (preserves the pre-existing sizeof bug tracked by #27), 1 `?` site in selfhost/test/tagged_ptr_ret.ww (allocbox exercises real `?` propagation against a (*T | nomem) return). 130/130 tests green, 994_w6c_ww + 995_self_rebuild stage byte-identity preserved. Follow-ups #31 (wwstage checkletassign leniency), #32 (wwstage slice-form gap), #33 (tagged_ptr_ret.ww make-test wiring). --- cmd/w6c/cgen.c | 89 ++++++++++++++++++++-------- cmd/wcc/check.c | 34 ++++++++++- rt/alloc.s | 10 ++++ rt/ensure.ww | 7 ++- selfhost/cmd/w6c/main.combined.ww | 21 ++++++- selfhost/cmd/wcc/cgenexpr.ww | 21 ++++++- selfhost/cmd/wwdump/main.combined.ww | 21 ++++++- selfhost/test/tagged_ptr_ret.ww | 26 +++++++- test/wcc/700_e2e.c | 16 +++-- 9 files changed, 204 insertions(+), 41 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 9b67669a..9927ea47 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4130,16 +4130,30 @@ cgexpr(Cg *c, Node *n, Local *locals) * bytes. Size comes from the value's static type. * `n->lhs->type == ty_err` gate (mirrors assert above) * — check.c only stamps ty_err when no user-scoped - * `alloc` shadows the builtin (task #23). */ + * `alloc` shadows the builtin (task #23). + * + * Result is the graduated `(*T | nomem)` tagged-pointer + * ABI (AX=tag, DX=ptr) — task #30. rt_alloc returns 0 + * on OOM (rt/alloc.s); we branch on AX, building tag=1 + * (nomem, DX=0) on null and tag=0 (success, DX=ptr) + * after the value-init stores complete. Callers wrap + * with `!` / `?` to consume the union. */ Node *v = n->list; Type *t = v->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; Type *def = type_default(t); int sz = def ? (int)def->size : 8; if (sz == 0) sz = 8; - /* call os.alloc(sz) */ + char *alloc_ok = mklabel(c, "alloc_ok"); + char *alloc_done = mklabel(c, "alloc_done"); ins2(c, A_MOVQ, aimm(sz), areg(D_DI)); ins1(c, A_CALL, asym(ffi_resolve("alloc"))); + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + ins1(c, A_JNE, abranch(alloc_ok)); + ins2(c, A_MOVQ, aimm(1), areg(D_AX)); + ins2(c, A_MOVQ, aimm(0), areg(D_DX)); + ins1(c, A_JMP, abranch(alloc_done)); + label(c, alloc_ok); ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */ if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) { for (Node *f = v->list; f; f = f->next) { @@ -4190,7 +4204,9 @@ cgexpr(Cg *c, Node *n, Local *locals) else if (sz == 4) op = A_MOVL; ins2(c, op, areg(D_AX), amem(D_BX, 0)); } - ins1(c, A_POPQ, areg(D_AX)); /* return the ptr */ + ins1(c, A_POPQ, areg(D_DX)); /* DX = success ptr */ + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + label(c, alloc_done); break; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && @@ -6346,29 +6362,52 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int isf32 = type_isf32(lt); /* alloc([], n) initialiser for a slice local: allocate * n*esize bytes, build the {ptr, 0, n} header in the slot. - * Element size comes from the declared slice type. */ - if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24 - && n->rhs->kind == N_CALL && n->rhs->lhs - && n->rhs->lhs->kind == N_IDENT - && strcmp(n->rhs->lhs->str, "alloc") == 0 - && n->rhs->list && n->rhs->list->kind == N_ARRLIT - && n->rhs->list->list == NULL - && n->rhs->list->next && n->rhs->list->next->next == NULL) { - Node *count = n->rhs->list->next; - int esz = (lu->sub) ? (int)lu->sub->size : 1; - cgexpr(c, count, *locals); /* AX = n */ - ins1(c, A_PUSHQ, areg(D_AX)); /* save count */ - if (esz > 1) { - ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); - ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); + * Element size comes from the declared slice type. + * + * Task #30 graduated the builtin to `([]T | nomem)`. The let + * declares a bare `[]T`, so the canonical idiom wraps in `!` + * to abort on OOM; walk into the N_TRYUNW to keep the + * direct-store fast path. */ + { + Node *call = NULL; + int via_tryunw = 0; + if (n->rhs && n->rhs->kind == N_TRYUNW && n->rhs->lhs + && n->rhs->lhs->kind == N_CALL) { + call = n->rhs->lhs; + via_tryunw = 1; + } + if (call && lu && lu->kind == TY_SLICE && sz == 24 + && call->lhs && call->lhs->kind == N_IDENT + && strcmp(call->lhs->str, "alloc") == 0 + && call->list && call->list->kind == N_ARRLIT + && call->list->list == NULL + && call->list->next + && call->list->next->next == NULL) { + Node *count = call->list->next; + int esz = (lu->sub) ? (int)lu->sub->size : 1; + cgexpr(c, count, *locals); /* AX = n */ + ins1(c, A_PUSHQ, areg(D_AX)); /* save count */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); + ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); + } + ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); + ins1(c, A_CALL, asym(ffi_resolve("alloc"))); + if (via_tryunw) { + char *ok = mklabel(c, "tryunw_ok"); + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + ins1(c, A_JNE, abranch(ok)); + ins2(c, A_MOVQ, aimm(1), areg(D_DI)); + ins2(c, A_MOVQ, aimm(60), areg(D_AX)); + ins0(c, A_SYSCALL); + label(c, ok); + } + ins1(c, A_POPQ, areg(D_BX)); /* count */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); + break; } - ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); - ins1(c, A_CALL, asym(ffi_resolve("alloc"))); - ins1(c, A_POPQ, areg(D_BX)); /* count */ - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); - ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8)); - ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); - break; } /* str initialiser: cgexpr produces (AX=ptr, BX=len). */ if (n->rhs && type_isstr(lt) && sz == 16) { diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index a67be34d..fd7713d4 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -985,7 +985,22 @@ cexpr(Checker *c, Node *n) scope_lookup_in_module(c->cur, c->cur_mod, "alloc"))) { Type *t = cexpr(c, n->list); Type *def = type_default(t); - n->type = type_ptr(c->a, def ? def : ty_void); + Type *pt = type_ptr(c->a, def ? def : ty_void); + /* Task #30 — graduate to Hare's `(*T | nomem)` shape; + * the cgen branches on rt_alloc's null return to emit + * the nomem variant. Two-variant union with TY_PTR + + * TY_NAMED(nomem) does not trip the nullable-pointer + * fold (#25), so the result rides the general AX=tag, + * DX=ptr ABI both stages already share. */ + Type *tt = newtype(c->a, TY_TAGGED); + Tparam *vp = amalloc(c->a, sizeof *vp); + Tparam *ve = amalloc(c->a, sizeof *ve); + vp->type = pt; vp->next = ve; + ve->type = ty_nomem; ve->next = NULL; + tt->params = vp; + tt->size = 16; + tt->align = 8; + n->type = tt; n->lhs->type = ty_err; return n->type; } @@ -1046,7 +1061,22 @@ cexpr(Checker *c, Node *n) !(c->cur_mod && scope_lookup_in_module(c->cur, c->cur_mod, "alloc"))) { (void)cexpr(c, n->list->next); - n->type = type_slice(c->a, ty_u8); + Type *st = type_slice(c->a, ty_u8); + /* Task #30 — slice form graduates the same way: + * `alloc([], n)` now returns `([]T | nomem)`. Slot is + * 8 (tag) + 24 (slice payload) = 32B. The element type + * defaults to u8 here; the let-init shortcut in + * cmd/w6c/cgen.c N_LET drives the real element size + * from the declared slice type. */ + Type *tt = newtype(c->a, TY_TAGGED); + Tparam *vs = amalloc(c->a, sizeof *vs); + Tparam *ve = amalloc(c->a, sizeof *ve); + vs->type = st; vs->next = ve; + ve->type = ty_nomem; ve->next = NULL; + tt->params = vs; + tt->size = 32; + tt->align = 8; + n->type = tt; n->lhs->type = ty_err; return n->type; } diff --git a/rt/alloc.s b/rt/alloc.s index a1c3de1c..290e1dfa 100644 --- a/rt/alloc.s +++ b/rt/alloc.s @@ -5,6 +5,12 @@ // // We pin to PROT_READ|PROT_WRITE and MAP_PRIVATE|MAP_ANONYMOUS so // callers never have to plumb file descriptors through. +// +// On mmap failure the raw syscall returns -errno (negative). Task #30 +// graduated the `alloc` builtin to a fallible `(*T | nomem)` / +// `([]T | nomem)` signature whose cgen branches on a null return, so +// the failure path here returns 0 instead of a poisoned pointer. The +// builtin's caller is expected to `!`/`?` the result. TEXT rt_alloc,$0 MOVQ DI, SI // arg 1: length = caller's n @@ -15,6 +21,10 @@ TEXT rt_alloc,$0 MOVQ $0, R9 // arg 5: offset = 0 MOVQ $9, AX // syscall: mmap SYSCALL + CMPQ $0, AX + JGE rt_alloc_ok + XORQ AX, AX +rt_alloc_ok: RET TEXT rt_free,$0 diff --git a/rt/ensure.ww b/rt/ensure.ww index 685e55ce..5b2409b9 100644 --- a/rt/ensure.ww +++ b/rt/ensure.ww @@ -37,7 +37,12 @@ export fn rt_ensure(s: *slice, membsz: u64) void = { let nc: i64 = s.cap * 2i64; if (nc < 8i64) { nc = 8i64; }; for (nc < s.len) { nc *= 2i64; }; - let np: *u8 = alloc((nc: u64) * membsz): *u8; + // Task #30: the alloc builtin returns `(*T | nomem)`; `!` aborts + // on OOM. The longstanding sizeof-only allocation bug (task #27) + // stays unfixed here — rt_ensure presumes a same-module `fn + // alloc(n)` resolution that the bare cur_mod=NULL gate at + // cmd/wcc/check.c:984 doesn't honour. + let np: *u8 = alloc((nc: u64) * membsz)!: *u8; let n: u64 = (s.cap: u64) * membsz; let i: u64 = 0u64; for (i < n) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f16600f7..9decbca4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14581,7 +14581,14 @@ fn cgbin(c: *cgen, n: *node) void = { // region. For an N_STRUCTLIT arg, allocate the struct's totsize and // emit per-field stores at each field's offset. For a scalar/ptr, // allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's -// alloc-special branch in N_CALL. Returns the heap ptr in AX. +// alloc-special branch in N_CALL. +// +// Task #30: result is the graduated `(*T | nomem)` tagged-pointer +// pair (AX=tag, DX=ptr). rt_alloc now returns 0 on OOM +// (rt/alloc.s); branch on AX to emit the nomem variant (tag=1, +// DX=0) or the success variant (tag=0, DX=ptr) after the +// value-init stores complete. Callers wrap with `!` / `?` to +// consume the union. fn cgalloc(c: *cgen, n: *node) void = { let v: *node = n.list; let sz: i32 = 8; @@ -14597,10 +14604,18 @@ fn cgalloc(c: *cgen, n: *node) void = { si = structlookup(c, sname); if (si != nil) { sz = si.totsize; }; }; + let okl: str = mklabel(c, "alloc_ok"); + let donel: str = mklabel(c, "alloc_done"); emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); emitline(okl); emitline("\n"); + emitline("\tMOVQ\t$1, AX\n"); + emitline("\tMOVQ\t$0, DX\n"); + emitline("\tJMP\t"); emitline(donel); emitline("\n"); + emitlabel(okl); emitline("\tPUSHQ\tAX\n"); if (v.kind == nkind.N_STRUCTLIT) { if (si != nil) { @@ -14653,7 +14668,9 @@ fn cgalloc(c: *cgen, n: *node) void = { emitline(sop); emitline("\tAX, (BX)\n"); }; - emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tDX\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(donel); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 15dbcb1d..0db26d0a 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2711,7 +2711,14 @@ fn cgbin(c: *cgen, n: *node) void = { // region. For an N_STRUCTLIT arg, allocate the struct's totsize and // emit per-field stores at each field's offset. For a scalar/ptr, // allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's -// alloc-special branch in N_CALL. Returns the heap ptr in AX. +// alloc-special branch in N_CALL. +// +// Task #30: result is the graduated `(*T | nomem)` tagged-pointer +// pair (AX=tag, DX=ptr). rt_alloc now returns 0 on OOM +// (rt/alloc.s); branch on AX to emit the nomem variant (tag=1, +// DX=0) or the success variant (tag=0, DX=ptr) after the +// value-init stores complete. Callers wrap with `!` / `?` to +// consume the union. fn cgalloc(c: *cgen, n: *node) void = { let v: *node = n.list; let sz: i32 = 8; @@ -2727,10 +2734,18 @@ fn cgalloc(c: *cgen, n: *node) void = { si = structlookup(c, sname); if (si != nil) { sz = si.totsize; }; }; + let okl: str = mklabel(c, "alloc_ok"); + let donel: str = mklabel(c, "alloc_done"); emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); emitline(okl); emitline("\n"); + emitline("\tMOVQ\t$1, AX\n"); + emitline("\tMOVQ\t$0, DX\n"); + emitline("\tJMP\t"); emitline(donel); emitline("\n"); + emitlabel(okl); emitline("\tPUSHQ\tAX\n"); if (v.kind == nkind.N_STRUCTLIT) { if (si != nil) { @@ -2783,7 +2798,9 @@ fn cgalloc(c: *cgen, n: *node) void = { emitline(sop); emitline("\tAX, (BX)\n"); }; - emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tDX\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(donel); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c7016c0d..2e53c64c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14581,7 +14581,14 @@ fn cgbin(c: *cgen, n: *node) void = { // region. For an N_STRUCTLIT arg, allocate the struct's totsize and // emit per-field stores at each field's offset. For a scalar/ptr, // allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's -// alloc-special branch in N_CALL. Returns the heap ptr in AX. +// alloc-special branch in N_CALL. +// +// Task #30: result is the graduated `(*T | nomem)` tagged-pointer +// pair (AX=tag, DX=ptr). rt_alloc now returns 0 on OOM +// (rt/alloc.s); branch on AX to emit the nomem variant (tag=1, +// DX=0) or the success variant (tag=0, DX=ptr) after the +// value-init stores complete. Callers wrap with `!` / `?` to +// consume the union. fn cgalloc(c: *cgen, n: *node) void = { let v: *node = n.list; let sz: i32 = 8; @@ -14597,10 +14604,18 @@ fn cgalloc(c: *cgen, n: *node) void = { si = structlookup(c, sname); if (si != nil) { sz = si.totsize; }; }; + let okl: str = mklabel(c, "alloc_ok"); + let donel: str = mklabel(c, "alloc_done"); emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\trt_alloc(SB)\n"); + emitline("\tCMPQ\t$0, AX\n"); + emitline("\tJNE\t"); emitline(okl); emitline("\n"); + emitline("\tMOVQ\t$1, AX\n"); + emitline("\tMOVQ\t$0, DX\n"); + emitline("\tJMP\t"); emitline(donel); emitline("\n"); + emitlabel(okl); emitline("\tPUSHQ\tAX\n"); if (v.kind == nkind.N_STRUCTLIT) { if (si != nil) { @@ -14653,7 +14668,9 @@ fn cgalloc(c: *cgen, n: *node) void = { emitline(sop); emitline("\tAX, (BX)\n"); }; - emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tDX\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(donel); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. diff --git a/selfhost/test/tagged_ptr_ret.ww b/selfhost/test/tagged_ptr_ret.ww index f09b49b2..4cd5b547 100644 --- a/selfhost/test/tagged_ptr_ret.ww +++ b/selfhost/test/tagged_ptr_ret.ww @@ -13,10 +13,19 @@ // Two match arms cover both runtime outcomes — success unwrap (tag=0, // ptr payload in DX) and error propagation (tag=1) — exercising the // same AX/DX ABI both stages must agree on. +// +// Task #30: the `alloc` builtin itself now returns `(*T | nomem)`. The +// allocbox arm below propagates the builtin's tagged return through +// the enclosing fn via `?` against a same-shape `(*point | nomem)` +// — matching the team-lead spec's "exercise `alloc(T)?` against a +// real function returning `(T | nomem)`". -package test; +package main; import fmt; +import os; + +type point = struct { x: i32, y: i32 }; fn alloc1(fail: i64) (*u8 | nomem) = { if (fail != 0i64) { let e: nomem; return e; }; @@ -29,6 +38,11 @@ fn caller(fail: i64) (*u8 | nomem) = { return p; }; +fn allocbox() (*point | nomem) = { + let p: *point = alloc(point { x = 3, y = 4 })?; + return p; +}; + export fn main() i32 = { let rc: i32 = 0; match (caller(0i64)) { @@ -50,5 +64,15 @@ export fn main() i32 = { fmt.println("nomem as expected"); }; }; + match (allocbox()) { + case let p: *point => { + fmt.println("allocbox ok"); + if (p.x * p.x + p.y * p.y != 25) { rc = 4; }; + }; + case nomem => { + fmt.println("allocbox unexpected nomem"); + rc = 5; + }; + }; return rc; }; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index bdf0e57f..23e2447e 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -277,12 +277,13 @@ static const struct row rows[] = { /* alloc() builtin: heap-allocate a struct, init from struct-lit. * `package main;` is required so the bare-alloc-builtin gate * (task #23) sees c->cur_mod=="main" and doesn't suppress the - * builtin via the inherited os.alloc decl. */ + * builtin via the inherited os.alloc decl. Task #30 graduated + * the builtin to `(*T | nomem)`; the `!` aborts on OOM. */ { "package main;\n" "import os;\n" "type point = struct { x: i32, y: i32 };\n" "fn main() i32 = {\n" - " let p: *point = alloc(point { x = 3, y = 4 });\n" + " let p: *point = alloc(point { x = 3, y = 4 })!;\n" " return p.x * p.x + p.y * p.y;\n" "};", 25 }, /* Hare-style range loop: for (let x .. slice) iterates elements */ @@ -296,11 +297,13 @@ static const struct row rows[] = { " return total;\n" "};", 100 }, /* alloc([], n): fresh empty slice with cap n. `package main;` for - * the same reason as the value-form test above (task #23 gate). */ + * the same reason as the value-form test above (task #23 gate). + * Task #30 graduated the slice form to `([]T | nomem)`; the `!` + * aborts on OOM. */ { "package main;\n" "import os;\n" "fn main() i32 = {\n" - " let s: []u8 = alloc([], 16);\n" + " let s: []u8 = alloc([], 16)!;\n" " append(s, 72u8, 105u8);\n" " return s.cap;\n" "};", 16 }, @@ -358,12 +361,13 @@ static const struct row rows[] = { " return (t.0 + t.1): i32;\n" "};", 42 }, /* Hare-style abort/assert + free() builtin. `package main;` for - * the alloc-builtin gate (task #23). */ + * the alloc-builtin gate (task #23). Task #30 graduated the + * builtin to a fallible signature; the `!` aborts on OOM. */ { "package main;\n" "import os;\n" "type point = struct { x: i64, y: i64 };\n" "fn main() i32 = {\n" - " let p: *point = alloc(point { x = 7, y = 35 });\n" + " let p: *point = alloc(point { x = 7, y = 35 })!;\n" " let r: i64 = p.x + p.y;\n" " free(p);\n" " os.assert(r == 42, \"sum mismatch\\n\");\n"