cstage+selfhost+test: mangle fn labels by module (#9)

Both stages emitted fn TEXT labels by leaf only; lib/os and lib/io
exporting the same leaves (read, write, close) collided at link.
lib/fmt + lib/log worked around with @symbol("rt_syscall") stubs.

Drop d->export from the fn skip rule in mod_collect (both stages) so
exported fns mangle as <module>.<name>. Let/def/type keep current
behavior. Skip retained for {@symbol, main, empty-module}.

Add cur_mod thread through cgfn + mod_lookup_for_fn(name, hint) at
all 4 label-emit sites (TEXT def, LEAQ N_IDENT, CALL N_IDENT, CALL
N_DOT). Wwstage mirror: emitfnname + modlookupforfn + curmod.
Invariant comment pinned in both stages.

ww2 == ww3 == ww4 byte-identical at the new label format.
706_fnlabel_mangle covers same-leaf cross-module CALL + private-leaf
cur_mod disambiguation through a fn-pointer rvalue.

Wwstage LEAQ-of-fn N_DOT (`let p = mod.fn` rvalue) is a pre-existing
gap; deferred to a follow-up. fmt/log rt_syscall stubs untouched
here; cleanup follows.
This commit is contained in:
2026-05-15 23:41:01 +09:00
parent 98460e0220
commit f1440bf9e8
12 changed files with 602 additions and 136 deletions

View File

@@ -451,10 +451,13 @@ ffi_collect(Cg *c, Node *file)
}
}
/* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Each
* non-exported, non-FFI top-level decl is mangled to <module>.<name>
* at emission time so two modules can each privately define the same
* helper without colliding at link time. */
/* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Every
* non-FFI top-level fn decl is mangled to <module>.<name> at emission
* time so two modules can each define the same fn leaf — including
* exported ones (lib/os and lib/io both ship `read`/`write`/`close`)
* — without colliding at link time. Non-fn decls (let/def/type) keep
* the older "non-exported only" rule: their export-side namespace is
* the user-facing data ABI and mangling them changes the surface. */
typedef struct Mod Mod;
struct Mod {
const char *name;
@@ -601,16 +604,24 @@ decl_has_ffisym(Node *d)
return 0;
}
/* Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` for fns.
* Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */
static void
mod_collect(Cg *c, Node *file)
{
mod_map = NULL;
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
int track = (d->kind == N_FNDECL) || (d->kind == N_TYPEDECL)
int isfn = (d->kind == N_FNDECL);
int track = isfn || (d->kind == N_TYPEDECL)
|| (d->kind == N_DEF) || (d->kind == N_LET);
if (!track) continue;
if (d->export) continue;
/* Non-fn decls (let/def/type) still skip exported entries —
* their export-side namespace is the user-facing data ABI
* and mangling them changes the surface. Fns mangle
* unconditionally so cross-module same-leaf exports
* (os.read vs io.read) coexist at link time. */
if (!isfn && d->export) continue;
if (d->module == NULL || d->module[0] == '\0') continue;
if (decl_has_ffisym(d)) continue;
/* `main` is the linker entry-point convention. Even when not
@@ -626,7 +637,10 @@ mod_collect(Cg *c, Node *file)
}
/* Returns the originating module for a name, or NULL if the name
* isn't a registered private decl. */
* isn't a registered private decl. By-name only — works for non-fn
* refs (let/def/type) where the mod_collect skip rule keeps each leaf
* unique across the program. Fn refs go through mod_lookup_for_fn
* since multiple modules can now export the same fn leaf. */
static const char *
mod_lookup(const char *name)
{
@@ -635,6 +649,29 @@ mod_lookup(const char *name)
return NULL;
}
/* Hint-aware variant for fn names. Walks mod_map looking for a
* (name, hint) pair; returns NULL if there's no leaf-name match at
* all, the hinted module if a match exists, or the first leaf match
* when the caller had no hint. The hint comes from AST shape:
* - N_DOT call `m.fn(...)`: hint = the SK_USE module ident's str.
* - bare N_IDENT call `fn(...)`: hint = c->cur_mod (current fn's
* module — bare names resolve same-module by ww's rules).
* Falling back to the first leaf match preserves the legacy single-
* owner shape for callers that don't (yet) thread a hint. */
static const char *
mod_lookup_for_fn(const char *name, const char *hint)
{
const char *first = NULL;
for (Mod *m = mod_map; m; m = m->next) {
if (strcmp(m->name, name) != 0) continue;
if (hint != NULL && m->module != NULL
&& strcmp(m->module, hint) == 0)
return m->module;
if (first == NULL) first = m->module;
}
return first;
}
/* Collect every top-level `let` whose declared type we can store
* in a single .data slot. Names not in this map fall through to
* the old "drop assignment" path; with a clear link-time
@@ -664,6 +701,19 @@ let_islet(const char *name)
return 0;
}
/* Glue `<module>.<ident>` into a fresh arena buffer. */
static const char *
mod_join(Cg *c, const char *mod, const char *ident)
{
size_t mn = strlen(mod), in = strlen(ident);
char *buf = amalloc(c->a, mn + 1 + in + 1);
memcpy(buf, mod, mn);
buf[mn] = '.';
memcpy(buf + mn + 1, ident, in);
buf[mn + 1 + in] = '\0';
return buf;
}
/* Mangle an AST identifier into its asm linker symbol:
* - @symbol("...") binding wins (return mapped name).
* - module-private decl → <module>.<name>.
@@ -678,13 +728,22 @@ mod_mangle(Cg *c, const char *ident)
if (resolved != ident) return resolved;
const char *mod = mod_lookup(ident);
if (mod == NULL) return ident;
size_t mn = strlen(mod), in = strlen(ident);
char *buf = amalloc(c->a, mn + 1 + in + 1);
memcpy(buf, mod, mn);
buf[mn] = '.';
memcpy(buf + mn + 1, ident, in);
buf[mn + 1 + in] = '\0';
return buf;
return mod_join(c, mod, ident);
}
/* Fn-flavoured mangle: same shape as mod_mangle but consults
* mod_lookup_for_fn so the right module wins when multiple modules
* register the same fn leaf. `hint` is the explicit module from a
* N_DOT call site (or c->cur_mod for bare-ident calls); pass NULL
* to get the legacy first-match-wins behaviour. */
static const char *
mod_mangle_fn(Cg *c, const char *ident, const char *hint)
{
const char *resolved = ffi_resolve(ident);
if (resolved != ident) return resolved;
const char *mod = mod_lookup_for_fn(ident, hint);
if (mod == NULL) return ident;
return mod_join(c, mod, ident);
}
/* Forward decl — masym below depends on asym defined further down. */
@@ -696,6 +755,15 @@ masym(Cg *c, const char *ident)
return asym(mod_mangle(c, ident));
}
/* Fn-name address builder. Use at every CALL/LEAQ site whose target
* is a top-level fn — passes the hint so cross-module same-leaf
* exports resolve to the right module. */
static Adr
mafn(Cg *c, const char *ident, const char *hint)
{
return asym(mod_mangle_fn(c, ident, hint));
}
void
cg_init(Cg *c, Arena *a)
{
@@ -1496,9 +1564,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
* of a body-less FFI binding yields the C
* symbol, not the ww-side ident. Hare emits
* the same `$symname` for both call and
* address-of via QBE; here we mirror that. */
* address-of via QBE; here we mirror that.
* Bare ident → same-module by ww's resolver,
* so c->cur_mod is the right disambiguation
* hint. */
ins2(c, A_LEAQ,
masym(c, n->str), areg(D_AX));
mafn(c, n->str, c->cur_mod), areg(D_AX));
break;
}
for (Sdef *s = sdefs; s; s = s->next) {
@@ -4216,8 +4287,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BP, loff), areg(D_AX));
ins1(c, A_CALL, areg(D_AX));
} else {
/* Bare `f()` — same-module by ww's resolver
* rules. Hint with c->cur_mod so the right
* fn wins when the leaf collides with another
* module's exported same-leaf fn. */
ins1(c, A_CALL,
masym(c, n->lhs->str));
mafn(c, n->lhs->str, c->cur_mod));
}
} else if (n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->lhs->kind == N_IDENT) {
@@ -4227,7 +4302,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
* function pointer — we load the field and indirect. */
Type *bt = n->lhs->lhs->type;
if (bt == NULL || bt == ty_err) {
ins1(c, A_CALL, masym(c, n->lhs->str));
/* `m.fn()` — explicit module qualifier. Pass
* the bareword as the hint so cross-module
* same-leaf exports resolve correctly. */
ins1(c, A_CALL,
mafn(c, n->lhs->str, n->lhs->lhs->str));
} else {
cgexpr(c, n->lhs, locals); /* AX = fn ptr */
ins1(c, A_CALL, areg(D_AX));
@@ -4759,8 +4838,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
Type *t = n->type;
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
if (tu && tu->kind == TY_FN) {
/* `mod.fn` address-of via N_DOT — pass the
* module bareword as the disambiguation hint. */
ins2(c, A_LEAQ,
masym(c, n->str), areg(D_AX));
mafn(c, n->str, n->lhs->str), areg(D_AX));
break;
}
for (Sdef *s = sdefs; s; s = s->next) {
@@ -6470,6 +6551,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
/* fresh per-fn state */
c->head = c->tail = NULL;
c->fnname = fn->str;
c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL;
c->labelseq = 0;
cg_stack_arg_cursor = 0;
ndefers = 0;
@@ -6482,9 +6564,9 @@ cgfn(Cg *c, FILE *out, Node *fn)
/* TEXT directive comes first; framesize is filled at the end. */
Prog *text = newprog(c, A_TEXT);
/* Mangle the label for non-exported, non-FFI decls; mod_mangle
* does the FFI/module lookup in one step. */
text->to = masym(c, fn->str);
/* Mangle the label using the fn's own module as the hint — picks
* the right entry when multiple modules export the same leaf. */
text->to = mafn(c, fn->str, c->cur_mod);
text->from.offset = 0; /* framesize patched below */
emit(c, text);