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).
This commit is contained in:
2026-05-19 20:25:14 +09:00
parent d27411d833
commit 61705fb39e
9 changed files with 204 additions and 41 deletions

View File

@@ -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) {

View File

@@ -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;
}