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; break;
} }
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && if (n->lhs && n->lhs->kind == N_IDENT &&
strcmp(n->lhs->str, "alloc") == 0 && n->list) { 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 /* 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; Node *v = n->list;
Type *t = v->type; Type *t = v->type;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t; 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; n->lhs->type = ty_err;
return n->type; 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 && if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 && 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 *t = cexpr(c, n->list);
Type *def = type_default(t); Type *def = type_default(t);
n->type = type_ptr(c->a, def ? def : ty_void); 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 /* alloc([], n) — Hare-style fresh slice with cap n. We pin
* the element type to u8 by default; the caller's declared * 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 && if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 && n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list && n->list->kind == N_ARRLIT && n->list && n->list->kind == N_ARRLIT &&
n->list->list == NULL && 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); (void)cexpr(c, n->list->next);
n->type = type_slice(c->a, ty_u8); n->type = type_slice(c->a, ty_u8);
n->lhs->type = ty_err; n->lhs->type = ty_err;

View File

@@ -14791,10 +14791,19 @@ fn cgcall(c: *cgen, n: *node) void = {
// value's bytes. For struct literals, lower to rt_alloc // value's bytes. For struct literals, lower to rt_alloc
// + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL
// alloc path. // alloc path.
//
// Same-module-scope guard: skip the builtin when a fn
// `alloc` is declared in the current module (lib/os and
// rt/ensure both shadow it). Mirrors cstage check.c's
// scope_lookup_prefer gating on the `abort` precedent;
// without it, the bare same-module call lands in the
// typed-builtin path and shadows the user decl. Task #23.
if (streq(callee.str, "alloc")) { if (streq(callee.str, "alloc")) {
if (n.list != nil) { if (n.list != nil) {
cgalloc(c, n); if (!samemodfn(c, "alloc")) {
return; cgalloc(c, n);
return;
};
}; };
}; };
}; };
@@ -21273,6 +21282,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil; return nil;
}; };
// samemodfn — true iff `name` is registered as a fn in c.curmod. Used
// by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`,
// future free/append/len audits) when the current module declares its
// own decl by that name. Mirrors cstage's same-module check at
// cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over
// the flat scope would also match `use os;`-imported decls in a primary,
// suppressing the builtin spuriously; the same-module-tag filter here
// (and `c.curmod && ...` on the cstage side) keeps the gate strict.
fn samemodfn(c: *cgen, name: str) bool = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return true; };
};
f = f.frnext;
};
return false;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified // fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple // `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves // modules export the same name, e.g. `os.read` and `io.read`) resolves

View File

@@ -1703,6 +1703,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil; return nil;
}; };
// samemodfn — true iff `name` is registered as a fn in c.curmod. Used
// by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`,
// future free/append/len audits) when the current module declares its
// own decl by that name. Mirrors cstage's same-module check at
// cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over
// the flat scope would also match `use os;`-imported decls in a primary,
// suppressing the builtin spuriously; the same-module-tag filter here
// (and `c.curmod && ...` on the cstage side) keeps the gate strict.
fn samemodfn(c: *cgen, name: str) bool = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return true; };
};
f = f.frnext;
};
return false;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified // fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple // `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves // modules export the same name, e.g. `os.read` and `io.read`) resolves

View File

@@ -2938,10 +2938,19 @@ fn cgcall(c: *cgen, n: *node) void = {
// value's bytes. For struct literals, lower to rt_alloc // value's bytes. For struct literals, lower to rt_alloc
// + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL
// alloc path. // alloc path.
//
// Same-module-scope guard: skip the builtin when a fn
// `alloc` is declared in the current module (lib/os and
// rt/ensure both shadow it). Mirrors cstage check.c's
// scope_lookup_prefer gating on the `abort` precedent;
// without it, the bare same-module call lands in the
// typed-builtin path and shadows the user decl. Task #23.
if (streq(callee.str, "alloc")) { if (streq(callee.str, "alloc")) {
if (n.list != nil) { if (n.list != nil) {
cgalloc(c, n); if (!samemodfn(c, "alloc")) {
return; cgalloc(c, n);
return;
};
}; };
}; };
}; };

View File

@@ -14791,10 +14791,19 @@ fn cgcall(c: *cgen, n: *node) void = {
// value's bytes. For struct literals, lower to rt_alloc // value's bytes. For struct literals, lower to rt_alloc
// + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL
// alloc path. // alloc path.
//
// Same-module-scope guard: skip the builtin when a fn
// `alloc` is declared in the current module (lib/os and
// rt/ensure both shadow it). Mirrors cstage check.c's
// scope_lookup_prefer gating on the `abort` precedent;
// without it, the bare same-module call lands in the
// typed-builtin path and shadows the user decl. Task #23.
if (streq(callee.str, "alloc")) { if (streq(callee.str, "alloc")) {
if (n.list != nil) { if (n.list != nil) {
cgalloc(c, n); if (!samemodfn(c, "alloc")) {
return; cgalloc(c, n);
return;
};
}; };
}; };
}; };
@@ -21273,6 +21282,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil; return nil;
}; };
// samemodfn — true iff `name` is registered as a fn in c.curmod. Used
// by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`,
// future free/append/len audits) when the current module declares its
// own decl by that name. Mirrors cstage's same-module check at
// cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over
// the flat scope would also match `use os;`-imported decls in a primary,
// suppressing the builtin spuriously; the same-module-tag filter here
// (and `c.curmod && ...` on the cstage side) keeps the gate strict.
fn samemodfn(c: *cgen, name: str) bool = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return true; };
};
f = f.frnext;
};
return false;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified // fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple // `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves // modules export the same name, e.g. `os.read` and `io.read`) resolves

View File

@@ -274,8 +274,12 @@ static const struct row rows[] = {
" os.write(1, s.ptr, len(s): u64);\n" " os.write(1, s.ptr, len(s): u64);\n"
" return len(s);\n" " return len(s);\n"
"};", 4 }, "};", 4 },
/* alloc() builtin: heap-allocate a struct, init from struct-lit */ /* alloc() builtin: heap-allocate a struct, init from struct-lit.
{ "import os;\n" * `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. */
{ "package main;\n"
"import os;\n"
"type point = struct { x: i32, y: i32 };\n" "type point = struct { x: i32, y: i32 };\n"
"fn main() 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"
@@ -291,13 +295,32 @@ static const struct row rows[] = {
" for (let b .. s) { total += b: i32; };\n" " for (let b .. s) { total += b: i32; };\n"
" return total;\n" " return total;\n"
"};", 100 }, "};", 100 },
/* alloc([], n): fresh empty slice with cap n */ /* alloc([], n): fresh empty slice with cap n. `package main;` for
{ "import os;\n" * the same reason as the value-form test above (task #23 gate). */
{ "package main;\n"
"import os;\n"
"fn main() i32 = {\n" "fn main() i32 = {\n"
" let s: []u8 = alloc([], 16);\n" " let s: []u8 = alloc([], 16);\n"
" append(s, 72u8, 105u8);\n" " append(s, 72u8, 105u8);\n"
" return s.cap;\n" " return s.cap;\n"
"};", 16 }, "};", 16 },
/* alloc-builtin shadow (task #23): a non-main package declares
* `fn alloc(n: i64) i64` and calls it bare. The same-module gate
* must suppress the builtin and dispatch to the user fn so the
* call returns n+100. Pre-gate this site lands in the typed-builtin
* path: arg is i64 → returns *i64 → init-type fails against the
* declared i64 (and would over-allocate against rt_alloc anyway).
* Inline multi-`package` mirrors driver-concatenated layout; the
* `import myos;` directive is silently skipped by locate_import
* (no external module by that name). */
{ "package myos;\n"
"fn alloc(n: i64) i64 = { return n + 100; };\n"
"fn run() i64 = { return alloc(7); };\n"
"package main;\n"
"import myos;\n"
"fn main() i32 = {\n"
" return myos.run(): i32;\n"
"};", 107 },
/* variadic spread: append(dst, src...) iterates src */ /* variadic spread: append(dst, src...) iterates src */
{ "import os;\n" { "import os;\n"
"fn main() i32 = {\n" "fn main() i32 = {\n"
@@ -334,8 +357,10 @@ static const struct row rows[] = {
" let t: (i64, i64) = pair();\n" " let t: (i64, i64) = pair();\n"
" return (t.0 + t.1): i32;\n" " return (t.0 + t.1): i32;\n"
"};", 42 }, "};", 42 },
/* Hare-style abort/assert + free() builtin */ /* Hare-style abort/assert + free() builtin. `package main;` for
{ "import os;\n" * the alloc-builtin gate (task #23). */
{ "package main;\n"
"import os;\n"
"type point = struct { x: i64, y: i64 };\n" "type point = struct { x: i64, y: i64 };\n"
"fn main() i32 = {\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"