From 3fe968c8a0b747eae07736a97e52b5986cfee513 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 18:51:07 +0900 Subject: [PATCH] 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. --- cmd/w6c/cgen.c | 11 ++++++--- cmd/wcc/check.c | 22 ++++++++++++++--- selfhost/cmd/w6c/main.combined.ww | 32 ++++++++++++++++++++++-- selfhost/cmd/wcc/cgen.ww | 19 ++++++++++++++ selfhost/cmd/wcc/cgenexpr.ww | 13 ++++++++-- selfhost/cmd/wwdump/main.combined.ww | 32 ++++++++++++++++++++++-- test/wcc/700_e2e.c | 37 +++++++++++++++++++++++----- 7 files changed, 148 insertions(+), 18 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 500eb0c4..9b67669a 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 3255adfd..a0dd281d 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7004e390..221db16d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14791,10 +14791,19 @@ fn cgcall(c: *cgen, n: *node) void = { // value's bytes. For struct literals, lower to rt_alloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // 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 (n.list != nil) { - cgalloc(c, n); - return; + if (!samemodfn(c, "alloc")) { + cgalloc(c, n); + return; + }; }; }; }; @@ -21273,6 +21282,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = { 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 // `mod.fn(...)` calls go through this so a leaf collision (multiple // modules export the same name, e.g. `os.read` and `io.read`) resolves diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 935437a9..1368f99f 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1703,6 +1703,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = { 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 // `mod.fn(...)` calls go through this so a leaf collision (multiple // modules export the same name, e.g. `os.read` and `io.read`) resolves diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e311fc3d..15dbcb1d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2938,10 +2938,19 @@ fn cgcall(c: *cgen, n: *node) void = { // value's bytes. For struct literals, lower to rt_alloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // 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 (n.list != nil) { - cgalloc(c, n); - return; + if (!samemodfn(c, "alloc")) { + cgalloc(c, n); + return; + }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 881f01a8..84df1d3c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14791,10 +14791,19 @@ fn cgcall(c: *cgen, n: *node) void = { // value's bytes. For struct literals, lower to rt_alloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // 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 (n.list != nil) { - cgalloc(c, n); - return; + if (!samemodfn(c, "alloc")) { + cgalloc(c, n); + return; + }; }; }; }; @@ -21273,6 +21282,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = { 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 // `mod.fn(...)` calls go through this so a leaf collision (multiple // modules export the same name, e.g. `os.read` and `io.read`) resolves diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 2beb9f2f..bdf0e57f 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -274,8 +274,12 @@ static const struct row rows[] = { " os.write(1, s.ptr, len(s): u64);\n" " return len(s);\n" "};", 4 }, - /* alloc() builtin: heap-allocate a struct, init from struct-lit */ - { "import os;\n" + /* 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. */ + { "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" @@ -291,13 +295,32 @@ static const struct row rows[] = { " for (let b .. s) { total += b: i32; };\n" " return total;\n" "};", 100 }, - /* alloc([], n): fresh empty slice with cap n */ - { "import os;\n" + /* alloc([], n): fresh empty slice with cap n. `package main;` for + * the same reason as the value-form test above (task #23 gate). */ + { "package main;\n" + "import os;\n" "fn main() i32 = {\n" " let s: []u8 = alloc([], 16);\n" " append(s, 72u8, 105u8);\n" " return s.cap;\n" "};", 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 */ { "import os;\n" "fn main() i32 = {\n" @@ -334,8 +357,10 @@ static const struct row rows[] = { " let t: (i64, i64) = pair();\n" " return (t.0 + t.1): i32;\n" "};", 42 }, - /* Hare-style abort/assert + free() builtin */ - { "import os;\n" + /* Hare-style abort/assert + free() builtin. `package main;` for + * the alloc-builtin gate (task #23). */ + { "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"