cmd+selfhost+test: gate alloc builtin behind same-module fn alloc

Mirrors the existing abort/assert gates in cstage check.c (strict
same-module lookup rather than scope_lookup_prefer, since lib/os.alloc
under a `use os;` import must not suppress the bare-alloc builtin in
client code). cgen.c shadows the resolution: only fire the rt_alloc
path when the typer left N_CALL.lhs->type == ty_err. wwstage gets a
new samemodfn helper for the matching gate.

Test fixtures: package-main repair for the 3 alloc rows in 700_e2e.c
that the parser was inheriting curmod="os" from the concat'd os.ww;
new shadow-test row asserts a same-module `fn alloc(n: i64) i64`
beats the builtin in cgen.
This commit is contained in:
2026-05-19 18:51:07 +09:00
parent 58e6d349a2
commit 3fe968c8a0
7 changed files with 148 additions and 18 deletions

View File

@@ -4122,10 +4122,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
break;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "alloc") == 0 && n->list) {
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->type == ty_err &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list) {
/* alloc(value): heap-init a fresh *T with the value's
* bytes. Size comes from the value's static type. */
* 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). */
Node *v = n->list;
Type *t = v->type;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;

View File

@@ -963,9 +963,22 @@ cexpr(Checker *c, Node *n)
n->lhs->type = ty_err;
return n->type;
}
/* `alloc(value)` Hare-style builtin — suppressed when the
* current module declares its own `alloc` (lib/os/os.ww,
* rt/ensure.ww). Without the gate, the bare same-module call
* lands in the typed-builtin path and silently allocates
* sizeof(arg-type) bytes against rt_alloc, shadowing the
* user decl. Strict same-module check (not scope_lookup_prefer):
* `use os;` in a primary brings os.alloc into the flat scope
* as a fallback match — that's what the `abort` precedent
* sidesteps by leaving os.abort un-exported, but os.alloc IS
* exported. c->cur_mod==NULL is the primary unit; gate only
* fires when a same-module decl is registered. Task #23. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list != NULL && n->list->next == NULL) {
n->list != NULL && n->list->next == NULL &&
!(c->cur_mod &&
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);
@@ -1019,12 +1032,15 @@ cexpr(Checker *c, Node *n)
}
/* alloc([], n) — Hare-style fresh slice with cap n. We pin
* the element type to u8 by default; the caller's declared
* slice type drives the actual element size at codegen. */
* slice type drives the actual element size at codegen.
* Same scope_lookup_in_module gate as the value-form (#23). */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list && n->list->kind == N_ARRLIT &&
n->list->list == NULL &&
n->list->next && n->list->next->next == NULL) {
n->list->next && n->list->next->next == NULL &&
!(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);
n->lhs->type = ty_err;