wcc,ww: bare-module fn mangles bare, not an imported same-leaf (M4 E2, #84)

A package-less primary's bare fn (module="") whose leaf collided with an
imported module's same-leaf exported fn was mis-mangled to the imported
qualified name (a user `fn run` emitted as `test.run`), producing a
dead-duplicate symbol the linker silently shadowed -- a #263-class silent
miscompile, gate-blind and symmetric across both stages. cgen now registers
bare-module fns and resolves a bare-ident reference to its own bare leaf:
mod_lookup_for_fn prefers the bare entry when the call carries no module
hint and skips bare entries when it does, so the moduled-caller path stays
byte-identical. The moduled `main` entry carve-out is an orthogonal rule
(the linker entry is force-bared) and is retained. Prereq for the @test
user-`run` coexist (#80). The bare non-fn (let/def/type) sibling is the
same class but hint-less; deferred as #85, noted at the retained skip.
This commit is contained in:
2026-06-17 21:25:06 +09:00
parent e7cb9c9d83
commit 08cfb5b2dd
7 changed files with 452 additions and 96 deletions

View File

@@ -41304,12 +41304,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// Emit the TEXT label via emitfnname so the def site picks up the
// same skip rule (FFI / `main` / empty-module) and the same module
// hint (this fn's own module) that the call sites use. M1 #32: the
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
// bare directly, mirroring collectmods' skip; without this its
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
// onto an IMPORTED package's now-registered `pkg.main`.
// same module hint (this fn's own module) the call sites use. M1 #32:
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
// it bare directly, mirroring collectmods' skip. Irreducibly name-
// based: a `package main` primary's decls are MODULED "main" (the
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
// bare-module decl — dropping this carve-out mangles it to `main.main`
// (undefined `main`). #84's bare-module handling is the orthogonal
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
// in modlookupforfn, NOT here.
emitline("TEXT ");
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
emitbytes(fn_.str.ptr, fn_.str.len: u64);
@@ -45224,23 +45227,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
if (d.kind == syntax.nkind.N_FNDECL) {
// Fns mangle regardless of export status — covers
// lib/os.read vs lib/io.read collision.
if (d.nmod.len > 0) {
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
a = a.next;
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
if (!isffi) {
// M1 #32: the ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
a = a.next;
};
if (!isffi) {
// #84: register bare-module (package-less `//ww:module-
// reset`) fns TOO (nmod.len==0) — previously the
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
// never entered c.mods and a bare-ident ref to it first-
// matched an imported same-leaf fn (modlookupforfn) → a
// #40 residual / #263-class silent dead-dup. With the bare
// entry present, modlookupforfn prefers it on an empty
// hint. main is the linker entry convention: a `package
// main` primary's decls are MODULED "main" so main is NOT
// bare here — skip it (cgfn force-emits the bare `main`
// label). M1 #32: only ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path. Mirrors
// cstage cgen.c mod_collect.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
};
@@ -45248,6 +45260,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
// `MAX` two packages could clash on under sep-compile. No export
// guard: every decl with a module mangles identically.
// #85 (deferred): the `nmod.len > 0` gate below retains the
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
// type leaf colliding with an imported same-kind export) needs a
// different fix (modlookup is hint-LESS) and has no corpus repro;
// retained until then (rule-11).
if (d.kind == syntax.nkind.N_DEF) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
@@ -45323,12 +45340,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
first.len = 0;
for (m != nil) {
if (syntax.streq(m.mname, name)) {
if (hint.len > 0 && m.nmod.len > 0
&& syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
if (m.nmod.len == 0) {
// #84: a bare-module entry (now registered). A bare-ident
// ref (hint.len==0: the caller is itself in the bare/root
// module) resolves to it — bare wins over an imported
// same-leaf fn, by construction, order-independent. A
// non-empty hint named a module, so a bare decl is
// irrelevant: skip it, leaving the legacy first-imported-
// match untouched (byte-id-neutral for moduled callers).
if (hint.len == 0) {
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
} else {
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
};
};
};
m = m.mnext;

View File

@@ -3852,23 +3852,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
if (d.kind == syntax.nkind.N_FNDECL) {
// Fns mangle regardless of export status — covers
// lib/os.read vs lib/io.read collision.
if (d.nmod.len > 0) {
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
a = a.next;
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
if (!isffi) {
// M1 #32: the ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
a = a.next;
};
if (!isffi) {
// #84: register bare-module (package-less `//ww:module-
// reset`) fns TOO (nmod.len==0) — previously the
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
// never entered c.mods and a bare-ident ref to it first-
// matched an imported same-leaf fn (modlookupforfn) → a
// #40 residual / #263-class silent dead-dup. With the bare
// entry present, modlookupforfn prefers it on an empty
// hint. main is the linker entry convention: a `package
// main` primary's decls are MODULED "main" so main is NOT
// bare here — skip it (cgfn force-emits the bare `main`
// label). M1 #32: only ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path. Mirrors
// cstage cgen.c mod_collect.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
};
@@ -3876,6 +3885,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
// `MAX` two packages could clash on under sep-compile. No export
// guard: every decl with a module mangles identically.
// #85 (deferred): the `nmod.len > 0` gate below retains the
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
// type leaf colliding with an imported same-kind export) needs a
// different fix (modlookup is hint-LESS) and has no corpus repro;
// retained until then (rule-11).
if (d.kind == syntax.nkind.N_DEF) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
@@ -3951,12 +3965,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
first.len = 0;
for (m != nil) {
if (syntax.streq(m.mname, name)) {
if (hint.len > 0 && m.nmod.len > 0
&& syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
if (m.nmod.len == 0) {
// #84: a bare-module entry (now registered). A bare-ident
// ref (hint.len==0: the caller is itself in the bare/root
// module) resolves to it — bare wins over an imported
// same-leaf fn, by construction, order-independent. A
// non-empty hint named a module, so a bare decl is
// irrelevant: skip it, leaving the legacy first-imported-
// match untouched (byte-id-neutral for moduled callers).
if (hint.len == 0) {
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
} else {
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
};
};
};
m = m.mnext;

View File

@@ -598,12 +598,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// Emit the TEXT label via emitfnname so the def site picks up the
// same skip rule (FFI / `main` / empty-module) and the same module
// hint (this fn's own module) that the call sites use. M1 #32: the
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
// bare directly, mirroring collectmods' skip; without this its
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
// onto an IMPORTED package's now-registered `pkg.main`.
// same module hint (this fn's own module) the call sites use. M1 #32:
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
// it bare directly, mirroring collectmods' skip. Irreducibly name-
// based: a `package main` primary's decls are MODULED "main" (the
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
// bare-module decl — dropping this carve-out mangles it to `main.main`
// (undefined `main`). #84's bare-module handling is the orthogonal
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
// in modlookupforfn, NOT here.
emitline("TEXT ");
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
emitbytes(fn_.str.ptr, fn_.str.len: u64);

View File

@@ -41304,12 +41304,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// Emit the TEXT label via emitfnname so the def site picks up the
// same skip rule (FFI / `main` / empty-module) and the same module
// hint (this fn's own module) that the call sites use. M1 #32: the
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
// bare directly, mirroring collectmods' skip; without this its
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
// onto an IMPORTED package's now-registered `pkg.main`.
// same module hint (this fn's own module) the call sites use. M1 #32:
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
// it bare directly, mirroring collectmods' skip. Irreducibly name-
// based: a `package main` primary's decls are MODULED "main" (the
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
// bare-module decl — dropping this carve-out mangles it to `main.main`
// (undefined `main`). #84's bare-module handling is the orthogonal
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
// in modlookupforfn, NOT here.
emitline("TEXT ");
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
emitbytes(fn_.str.ptr, fn_.str.len: u64);
@@ -45224,23 +45227,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
if (d.kind == syntax.nkind.N_FNDECL) {
// Fns mangle regardless of export status — covers
// lib/os.read vs lib/io.read collision.
if (d.nmod.len > 0) {
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
a = a.next;
let isffi: bool = false;
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
if (!isffi) {
// M1 #32: the ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
a = a.next;
};
if (!isffi) {
// #84: register bare-module (package-less `//ww:module-
// reset`) fns TOO (nmod.len==0) — previously the
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
// never entered c.mods and a bare-ident ref to it first-
// matched an imported same-leaf fn (modlookupforfn) → a
// #40 residual / #263-class silent dead-dup. With the bare
// entry present, modlookupforfn prefers it on an empty
// hint. main is the linker entry convention: a `package
// main` primary's decls are MODULED "main" so main is NOT
// bare here — skip it (cgfn force-emits the bare `main`
// label). M1 #32: only ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path. Mirrors
// cstage cgen.c mod_collect.
if (!syntax.streq(d.str, "main") || d.imported != 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
};
@@ -45248,6 +45260,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
// `MAX` two packages could clash on under sep-compile. No export
// guard: every decl with a module mangles identically.
// #85 (deferred): the `nmod.len > 0` gate below retains the
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
// type leaf colliding with an imported same-kind export) needs a
// different fix (modlookup is hint-LESS) and has no corpus repro;
// retained until then (rule-11).
if (d.kind == syntax.nkind.N_DEF) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
@@ -45323,12 +45340,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
first.len = 0;
for (m != nil) {
if (syntax.streq(m.mname, name)) {
if (hint.len > 0 && m.nmod.len > 0
&& syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
if (m.nmod.len == 0) {
// #84: a bare-module entry (now registered). A bare-ident
// ref (hint.len==0: the caller is itself in the bare/root
// module) resolves to it — bare wins over an imported
// same-leaf fn, by construction, order-independent. A
// non-empty hint named a module, so a bare decl is
// irrelevant: skip it, leaving the legacy first-imported-
// match untouched (byte-id-neutral for moduled callers).
if (hint.len == 0) {
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
} else {
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
};
};
};
m = m.mnext;