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

@@ -232,6 +232,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_nested_structlit \ $(BIN)/test_nested_structlit \
$(BIN)/test_dot_structlit \ $(BIN)/test_dot_structlit \
$(BIN)/test_nested_call_rhs \ $(BIN)/test_nested_call_rhs \
$(BIN)/test_fnlabel_mangle \
$(BIN)/test_use_promote_alias \ $(BIN)/test_use_promote_alias \
$(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
@@ -409,6 +410,12 @@ $(BIN)/test_nested_call_rhs: test/wcc/705_nested_call_rhs.c \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_fnlabel_mangle: test/wcc/706_fnlabel_mangle.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)

View File

@@ -451,10 +451,13 @@ ffi_collect(Cg *c, Node *file)
} }
} }
/* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Each /* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Every
* non-exported, non-FFI top-level decl is mangled to <module>.<name> * non-FFI top-level fn decl is mangled to <module>.<name> at emission
* at emission time so two modules can each privately define the same * time so two modules can each define the same fn leaf — including
* helper without colliding at link time. */ * 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; typedef struct Mod Mod;
struct Mod { struct Mod {
const char *name; const char *name;
@@ -601,16 +604,24 @@ decl_has_ffisym(Node *d)
return 0; 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 static void
mod_collect(Cg *c, Node *file) mod_collect(Cg *c, Node *file)
{ {
mod_map = NULL; mod_map = NULL;
if (file == NULL) return; if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) { 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); || (d->kind == N_DEF) || (d->kind == N_LET);
if (!track) continue; 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 (d->module == NULL || d->module[0] == '\0') continue;
if (decl_has_ffisym(d)) continue; if (decl_has_ffisym(d)) continue;
/* `main` is the linker entry-point convention. Even when not /* `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 /* 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 * static const char *
mod_lookup(const char *name) mod_lookup(const char *name)
{ {
@@ -635,6 +649,29 @@ mod_lookup(const char *name)
return NULL; 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 /* Collect every top-level `let` whose declared type we can store
* in a single .data slot. Names not in this map fall through to * in a single .data slot. Names not in this map fall through to
* the old "drop assignment" path; with a clear link-time * the old "drop assignment" path; with a clear link-time
@@ -664,6 +701,19 @@ let_islet(const char *name)
return 0; 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: /* Mangle an AST identifier into its asm linker symbol:
* - @symbol("...") binding wins (return mapped name). * - @symbol("...") binding wins (return mapped name).
* - module-private decl → <module>.<name>. * - module-private decl → <module>.<name>.
@@ -678,13 +728,22 @@ mod_mangle(Cg *c, const char *ident)
if (resolved != ident) return resolved; if (resolved != ident) return resolved;
const char *mod = mod_lookup(ident); const char *mod = mod_lookup(ident);
if (mod == NULL) return ident; if (mod == NULL) return ident;
size_t mn = strlen(mod), in = strlen(ident); return mod_join(c, mod, ident);
char *buf = amalloc(c->a, mn + 1 + in + 1); }
memcpy(buf, mod, mn);
buf[mn] = '.'; /* Fn-flavoured mangle: same shape as mod_mangle but consults
memcpy(buf + mn + 1, ident, in); * mod_lookup_for_fn so the right module wins when multiple modules
buf[mn + 1 + in] = '\0'; * register the same fn leaf. `hint` is the explicit module from a
return buf; * 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. */ /* 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)); 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 void
cg_init(Cg *c, Arena *a) 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 * of a body-less FFI binding yields the C
* symbol, not the ww-side ident. Hare emits * symbol, not the ww-side ident. Hare emits
* the same `$symname` for both call and * 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, ins2(c, A_LEAQ,
masym(c, n->str), areg(D_AX)); mafn(c, n->str, c->cur_mod), areg(D_AX));
break; break;
} }
for (Sdef *s = sdefs; s; s = s->next) { 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)); amem(D_BP, loff), areg(D_AX));
ins1(c, A_CALL, areg(D_AX)); ins1(c, A_CALL, areg(D_AX));
} else { } 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, 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 && } else if (n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->lhs->kind == N_IDENT) { 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. */ * function pointer — we load the field and indirect. */
Type *bt = n->lhs->lhs->type; Type *bt = n->lhs->lhs->type;
if (bt == NULL || bt == ty_err) { 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 { } else {
cgexpr(c, n->lhs, locals); /* AX = fn ptr */ cgexpr(c, n->lhs, locals); /* AX = fn ptr */
ins1(c, A_CALL, areg(D_AX)); ins1(c, A_CALL, areg(D_AX));
@@ -4759,8 +4838,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
Type *t = n->type; Type *t = n->type;
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t; Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
if (tu && tu->kind == TY_FN) { 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, ins2(c, A_LEAQ,
masym(c, n->str), areg(D_AX)); mafn(c, n->str, n->lhs->str), areg(D_AX));
break; break;
} }
for (Sdef *s = sdefs; s; s = s->next) { for (Sdef *s = sdefs; s; s = s->next) {
@@ -6470,6 +6551,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
/* fresh per-fn state */ /* fresh per-fn state */
c->head = c->tail = NULL; c->head = c->tail = NULL;
c->fnname = fn->str; c->fnname = fn->str;
c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL;
c->labelseq = 0; c->labelseq = 0;
cg_stack_arg_cursor = 0; cg_stack_arg_cursor = 0;
ndefers = 0; ndefers = 0;
@@ -6482,9 +6564,9 @@ cgfn(Cg *c, FILE *out, Node *fn)
/* TEXT directive comes first; framesize is filled at the end. */ /* TEXT directive comes first; framesize is filled at the end. */
Prog *text = newprog(c, A_TEXT); Prog *text = newprog(c, A_TEXT);
/* Mangle the label for non-exported, non-FFI decls; mod_mangle /* Mangle the label using the fn's own module as the hint — picks
* does the FFI/module lookup in one step. */ * the right entry when multiple modules export the same leaf. */
text->to = masym(c, fn->str); text->to = mafn(c, fn->str, c->cur_mod);
text->from.offset = 0; /* framesize patched below */ text->from.offset = 0; /* framesize patched below */
emit(c, text); emit(c, text);

View File

@@ -34,6 +34,13 @@ struct Cg {
Arena *a; Arena *a;
Prog *head, *tail; Prog *head, *tail;
const char *fnname; const char *fnname;
const char *cur_mod; /* current fn's `// MODULE: foo` directive,
* NULL when the fn lives in the primary
* file (no MODULE: stamp). Drives bare-IDENT
* call mangling — `frob()` from within
* lib/foo binds to `foo.frob` regardless
* of which other modules also export `frob`.
* Set by cgfn before walking the body. */
int framesize; /* bytes of locals; 16-byte aligned */ int framesize; /* bytes of locals; 16-byte aligned */
int curoff; /* current top of locals */ int curoff; /* current top of locals */
Scope *locals; /* (name → offset) tracked via Sym */ Scope *locals; /* (name → offset) tracked via Sym */

View File

@@ -9544,13 +9544,14 @@ fn cgident(c: *cgen, n: *node) void = {
}; };
// Fn-name used as a value (e.g. `let f = some_fn;` or // Fn-name used as a value (e.g. `let f = some_fn;` or
// `... = some_fn;`). LEAQ the symbol address into AX. The // `... = some_fn;`). LEAQ the symbol address into AX. The
// emitsymname helper handles ffiresolve and module-mangling // emitfnname helper handles ffiresolve and module-mangling
// in one go, so a body-less FFI binding emits the C symbol // in one go, so a body-less FFI binding emits the C symbol
// it was declared with via @symbol(), not the ww-side ident. // it was declared with via @symbol(), not the ww-side ident.
// Bare ident → same-module by ww's resolver, hint with c.curmod.
let rt: *node = fnretlookup(c, nm); let rt: *node = fnretlookup(c, nm);
if (rt != nil) { if (rt != nil) {
emitline("\tLEAQ\t"); emitline("\tLEAQ\t");
emitsymname(c, nm); emitfnname(c, nm, c.curmod);
emitline("(SB), AX\n"); emitline("(SB), AX\n");
return; return;
}; };
@@ -12142,11 +12143,23 @@ fn cgcall(c: *cgen, n: *node) void = {
emitline("\tCALL\t"); emitline("\tCALL\t");
if (callee != nil) { if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { if (callee.kind == nkind.N_IDENT) {
// Bare `f()` — same-module by ww's resolver,
// so c.curmod is the disambiguation hint.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); emitfnname(c, calleename, c.curmod);
} else { if (callee.kind == nkind.N_DOT) { } else { if (callee.kind == nkind.N_DOT) {
// `m.f()` — pass the explicit module bareword
// so cross-module same-leaf exports resolve.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); let hint: str;
hint.ptr = nil;
hint.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
hint = callee.lhs.str;
};
};
emitfnname(c, calleename, hint);
};}; };};
}; };
emitline("(SB)\n"); emitline("(SB)\n");
@@ -16363,33 +16376,16 @@ fn cgfnparams(c: *cgen, params: *node) void = {
fn cgfn(c: *cgen, fn_: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = {
cgeninit(c, c.a); cgeninit(c, c.a);
c.fnname = fn_.str; c.fnname = fn_.str;
c.curmod = fn_.module;
c.fnret = fn_.lhs; c.fnret = fn_.lhs;
// 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. Drops the
// `exported == 0` skip in the legacy inline form — exported fns
// now mangle too, so cross-module same-leaf exports coexist.
emitline("TEXT "); emitline("TEXT ");
if (fn_.exported == 0) { emitfnname(c, fn_.str, fn_.module);
if (fn_.module.len > 0) {
let isffi: bool = false;
let a: *node = fn_.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
// `main` is the linker entry-point convention; even
// when not marked `export`, it must keep its bare
// name so w6l's _start can resolve `CALL main(SB)`.
// Mirror of cmd/w6c/cgen.c collectmods exemption.
let isentry: bool = streq(fn_.str, "main");
if (!isffi && !isentry) {
os.write(1, fn_.module.ptr, fn_.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
};
let nm: str = fn_.str;
os.write(1, nm.ptr, nm.len: u64);
emitline(",$"); emitline(",$");
// Pre-scan total frame: only count params that land in a local // Pre-scan total frame: only count params that land in a local
@@ -16891,9 +16887,16 @@ type cgen = struct {
aliases: *aliasent, aliases: *aliasent,
structs: *structinfo, structs: *structinfo,
enums: *enumtype, enums: *enumtype,
mods: *modent, // non-exported decls → originating module mods: *modent, // fn (any export status) + non-exported
// let/def/type decls → originating module
lets: *letvar, // top-level mutable scalar `let` bindings lets: *letvar, // top-level mutable scalar `let` bindings
fnname: str, fnname: str,
curmod: str, // current fn's `// MODULE: foo` directive (len=0
// when the fn is in the primary file). Drives
// bare-IDENT call mangling — `frob()` from
// inside lib/foo binds to `foo.frob` even when
// other modules also export `frob`. Set in cgfn
// before walking the body.
fnret: *node, // declared return type of current fn (or nil) fnret: *node, // declared return type of current fn (or nil)
looptop: i32, looptop: i32,
loopendbuf: *str, // stack of end labels for break loopendbuf: *str, // stack of end labels for break
@@ -17998,11 +18001,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = {
// ---- module-private symbol map -------------------------------------- // ---- module-private symbol map --------------------------------------
// //
// Non-exported top-level decls live in their originating module's // Every non-FFI top-level fn decl lives in its module's namespace —
// namespace. cgen mangles those names to `<module>.<name>` at emission // cgen mangles the leaf to `<module>.<name>` at the def site (TEXT)
// time, both at the def site (TEXT/DATA) and at every call/load site, // and at every call/load site, so cross-module same-leaf fns (lib/os
// so two modules can each privately define `cstrlen` without colliding // `read` vs lib/io `read`, both exported) coexist at link time.
// at link time. Exported decls and FFI-bound decls keep their bare name. // Non-fn decls (let/def/type) stick to the older "non-exported only"
// rule: their export-side namespace is the user-facing data ABI and
// mangling them changes the surface. FFI-bound decls (@symbol) keep
// their explicit C symbol regardless of kind.
//
// 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.
type modent = struct { type modent = struct {
mname: str, // the bare ident as it appears in source mname: str, // the bare ident as it appears in source
@@ -18019,8 +18029,19 @@ fn collectmods(c: *cgen, file: *node) void = {
// branch). Earlier nested-if/early-return variants tickled a // branch). Earlier nested-if/early-return variants tickled a
// wwstage cgen bug that dropped most prepends. // wwstage cgen bug that dropped most prepends.
if (d.kind == nkind.N_FNDECL) { if (d.kind == nkind.N_FNDECL) {
if (d.exported == 0) { // Fns mangle regardless of export status — covers
if (d.module.len > 0) { // lib/os.read vs lib/io.read collision.
if (d.module.len > 0) {
let isffi: bool = false;
let a: *node = d.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
if (!streq(d.str, "main")) { if (!streq(d.str, "main")) {
let m: *modent = amalloc(c.a, 48u64): *modent; let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str; m.mname = d.str;
@@ -18080,10 +18101,38 @@ fn modlookup(c: *cgen, name: str) str = {
return empty; return empty;
}; };
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
// preferring entries where module matches `hint`; falls back to the
// first leaf-name match when nothing matches the hint (legacy single-
// owner shape, also covers lookups with hint.len==0). Needed because
// multiple modules can now register the same fn leaf — bare `lookup`
// would otherwise grab whichever module was prepended last.
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
let m: *modent = c.mods;
let first: str;
first.ptr = nil;
first.len = 0;
for (m != nil) {
if (streq(m.mname, name)) {
if (hint.len > 0 && m.module.len > 0
&& streq(m.module, hint)) {
return m.module;
};
if (first.len == 0 && first.ptr == nil) {
first = m.module;
};
};
m = m.mnext;
};
return first;
};
// emitsymname — write the asm symbol name for `ident`. Honours, in // emitsymname — write the asm symbol name for `ident`. Honours, in
// order: FFI mapping (@symbol), module mangling (private decls), bare // order: FFI mapping (@symbol), module mangling (private decls), bare
// name. Use everywhere a top-level name is emitted before `(SB)` or in // name. Use everywhere a top-level non-fn name is emitted before `(SB)`
// a `TEXT name,$N` header. // — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
// cross-module same-leaf fn exports.
fn emitsymname(c: *cgen, ident: str) void = { fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident); let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) { if (resolved.ptr != ident.ptr) {
@@ -18099,6 +18148,25 @@ fn emitsymname(c: *cgen, ident: str) void = {
os.write(1, ident.ptr, ident.len: u64); os.write(1, ident.ptr, ident.len: u64);
}; };
// emitfnname — write the asm symbol name for a fn `ident`, threading
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
// for bare-IDENT calls) through modlookupforfn. Same FFI override
// semantics as emitsymname; same dot-separator format. Use at every
// CALL / LEAQ-of-fn / TEXT-def site.
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
};
// ---- FFI map --------------------------------------------------------- // ---- FFI map ---------------------------------------------------------
fn fficollect(c: *cgen, file: *node) void = { fn fficollect(c: *cgen, file: *node) void = {

View File

@@ -367,9 +367,16 @@ type cgen = struct {
aliases: *aliasent, aliases: *aliasent,
structs: *structinfo, structs: *structinfo,
enums: *enumtype, enums: *enumtype,
mods: *modent, // non-exported decls → originating module mods: *modent, // fn (any export status) + non-exported
// let/def/type decls → originating module
lets: *letvar, // top-level mutable scalar `let` bindings lets: *letvar, // top-level mutable scalar `let` bindings
fnname: str, fnname: str,
curmod: str, // current fn's `// MODULE: foo` directive (len=0
// when the fn is in the primary file). Drives
// bare-IDENT call mangling — `frob()` from
// inside lib/foo binds to `foo.frob` even when
// other modules also export `frob`. Set in cgfn
// before walking the body.
fnret: *node, // declared return type of current fn (or nil) fnret: *node, // declared return type of current fn (or nil)
looptop: i32, looptop: i32,
loopendbuf: *str, // stack of end labels for break loopendbuf: *str, // stack of end labels for break
@@ -1474,11 +1481,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = {
// ---- module-private symbol map -------------------------------------- // ---- module-private symbol map --------------------------------------
// //
// Non-exported top-level decls live in their originating module's // Every non-FFI top-level fn decl lives in its module's namespace —
// namespace. cgen mangles those names to `<module>.<name>` at emission // cgen mangles the leaf to `<module>.<name>` at the def site (TEXT)
// time, both at the def site (TEXT/DATA) and at every call/load site, // and at every call/load site, so cross-module same-leaf fns (lib/os
// so two modules can each privately define `cstrlen` without colliding // `read` vs lib/io `read`, both exported) coexist at link time.
// at link time. Exported decls and FFI-bound decls keep their bare name. // Non-fn decls (let/def/type) stick to the older "non-exported only"
// rule: their export-side namespace is the user-facing data ABI and
// mangling them changes the surface. FFI-bound decls (@symbol) keep
// their explicit C symbol regardless of kind.
//
// 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.
type modent = struct { type modent = struct {
mname: str, // the bare ident as it appears in source mname: str, // the bare ident as it appears in source
@@ -1495,8 +1509,19 @@ fn collectmods(c: *cgen, file: *node) void = {
// branch). Earlier nested-if/early-return variants tickled a // branch). Earlier nested-if/early-return variants tickled a
// wwstage cgen bug that dropped most prepends. // wwstage cgen bug that dropped most prepends.
if (d.kind == nkind.N_FNDECL) { if (d.kind == nkind.N_FNDECL) {
if (d.exported == 0) { // Fns mangle regardless of export status — covers
if (d.module.len > 0) { // lib/os.read vs lib/io.read collision.
if (d.module.len > 0) {
let isffi: bool = false;
let a: *node = d.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
if (!streq(d.str, "main")) { if (!streq(d.str, "main")) {
let m: *modent = amalloc(c.a, 48u64): *modent; let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str; m.mname = d.str;
@@ -1556,10 +1581,38 @@ fn modlookup(c: *cgen, name: str) str = {
return empty; return empty;
}; };
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
// preferring entries where module matches `hint`; falls back to the
// first leaf-name match when nothing matches the hint (legacy single-
// owner shape, also covers lookups with hint.len==0). Needed because
// multiple modules can now register the same fn leaf — bare `lookup`
// would otherwise grab whichever module was prepended last.
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
let m: *modent = c.mods;
let first: str;
first.ptr = nil;
first.len = 0;
for (m != nil) {
if (streq(m.mname, name)) {
if (hint.len > 0 && m.module.len > 0
&& streq(m.module, hint)) {
return m.module;
};
if (first.len == 0 && first.ptr == nil) {
first = m.module;
};
};
m = m.mnext;
};
return first;
};
// emitsymname — write the asm symbol name for `ident`. Honours, in // emitsymname — write the asm symbol name for `ident`. Honours, in
// order: FFI mapping (@symbol), module mangling (private decls), bare // order: FFI mapping (@symbol), module mangling (private decls), bare
// name. Use everywhere a top-level name is emitted before `(SB)` or in // name. Use everywhere a top-level non-fn name is emitted before `(SB)`
// a `TEXT name,$N` header. // — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
// cross-module same-leaf fn exports.
fn emitsymname(c: *cgen, ident: str) void = { fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident); let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) { if (resolved.ptr != ident.ptr) {
@@ -1575,6 +1628,25 @@ fn emitsymname(c: *cgen, ident: str) void = {
os.write(1, ident.ptr, ident.len: u64); os.write(1, ident.ptr, ident.len: u64);
}; };
// emitfnname — write the asm symbol name for a fn `ident`, threading
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
// for bare-IDENT calls) through modlookupforfn. Same FFI override
// semantics as emitsymname; same dot-separator format. Use at every
// CALL / LEAQ-of-fn / TEXT-def site.
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
};
// ---- FFI map --------------------------------------------------------- // ---- FFI map ---------------------------------------------------------
fn fficollect(c: *cgen, file: *node) void = { fn fficollect(c: *cgen, file: *node) void = {

View File

@@ -711,33 +711,16 @@ fn cgfnparams(c: *cgen, params: *node) void = {
fn cgfn(c: *cgen, fn_: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = {
cgeninit(c, c.a); cgeninit(c, c.a);
c.fnname = fn_.str; c.fnname = fn_.str;
c.curmod = fn_.module;
c.fnret = fn_.lhs; c.fnret = fn_.lhs;
// 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. Drops the
// `exported == 0` skip in the legacy inline form — exported fns
// now mangle too, so cross-module same-leaf exports coexist.
emitline("TEXT "); emitline("TEXT ");
if (fn_.exported == 0) { emitfnname(c, fn_.str, fn_.module);
if (fn_.module.len > 0) {
let isffi: bool = false;
let a: *node = fn_.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
// `main` is the linker entry-point convention; even
// when not marked `export`, it must keep its bare
// name so w6l's _start can resolve `CALL main(SB)`.
// Mirror of cmd/w6c/cgen.c collectmods exemption.
let isentry: bool = streq(fn_.str, "main");
if (!isffi && !isentry) {
os.write(1, fn_.module.ptr, fn_.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
};
let nm: str = fn_.str;
os.write(1, nm.ptr, nm.len: u64);
emitline(",$"); emitline(",$");
// Pre-scan total frame: only count params that land in a local // Pre-scan total frame: only count params that land in a local

View File

@@ -528,13 +528,14 @@ fn cgident(c: *cgen, n: *node) void = {
}; };
// Fn-name used as a value (e.g. `let f = some_fn;` or // Fn-name used as a value (e.g. `let f = some_fn;` or
// `... = some_fn;`). LEAQ the symbol address into AX. The // `... = some_fn;`). LEAQ the symbol address into AX. The
// emitsymname helper handles ffiresolve and module-mangling // emitfnname helper handles ffiresolve and module-mangling
// in one go, so a body-less FFI binding emits the C symbol // in one go, so a body-less FFI binding emits the C symbol
// it was declared with via @symbol(), not the ww-side ident. // it was declared with via @symbol(), not the ww-side ident.
// Bare ident → same-module by ww's resolver, hint with c.curmod.
let rt: *node = fnretlookup(c, nm); let rt: *node = fnretlookup(c, nm);
if (rt != nil) { if (rt != nil) {
emitline("\tLEAQ\t"); emitline("\tLEAQ\t");
emitsymname(c, nm); emitfnname(c, nm, c.curmod);
emitline("(SB), AX\n"); emitline("(SB), AX\n");
return; return;
}; };
@@ -3126,11 +3127,23 @@ fn cgcall(c: *cgen, n: *node) void = {
emitline("\tCALL\t"); emitline("\tCALL\t");
if (callee != nil) { if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { if (callee.kind == nkind.N_IDENT) {
// Bare `f()` — same-module by ww's resolver,
// so c.curmod is the disambiguation hint.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); emitfnname(c, calleename, c.curmod);
} else { if (callee.kind == nkind.N_DOT) { } else { if (callee.kind == nkind.N_DOT) {
// `m.f()` — pass the explicit module bareword
// so cross-module same-leaf exports resolve.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); let hint: str;
hint.ptr = nil;
hint.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
hint = callee.lhs.str;
};
};
emitfnname(c, calleename, hint);
};}; };};
}; };
emitline("(SB)\n"); emitline("(SB)\n");

View File

@@ -9544,13 +9544,14 @@ fn cgident(c: *cgen, n: *node) void = {
}; };
// Fn-name used as a value (e.g. `let f = some_fn;` or // Fn-name used as a value (e.g. `let f = some_fn;` or
// `... = some_fn;`). LEAQ the symbol address into AX. The // `... = some_fn;`). LEAQ the symbol address into AX. The
// emitsymname helper handles ffiresolve and module-mangling // emitfnname helper handles ffiresolve and module-mangling
// in one go, so a body-less FFI binding emits the C symbol // in one go, so a body-less FFI binding emits the C symbol
// it was declared with via @symbol(), not the ww-side ident. // it was declared with via @symbol(), not the ww-side ident.
// Bare ident → same-module by ww's resolver, hint with c.curmod.
let rt: *node = fnretlookup(c, nm); let rt: *node = fnretlookup(c, nm);
if (rt != nil) { if (rt != nil) {
emitline("\tLEAQ\t"); emitline("\tLEAQ\t");
emitsymname(c, nm); emitfnname(c, nm, c.curmod);
emitline("(SB), AX\n"); emitline("(SB), AX\n");
return; return;
}; };
@@ -12142,11 +12143,23 @@ fn cgcall(c: *cgen, n: *node) void = {
emitline("\tCALL\t"); emitline("\tCALL\t");
if (callee != nil) { if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { if (callee.kind == nkind.N_IDENT) {
// Bare `f()` — same-module by ww's resolver,
// so c.curmod is the disambiguation hint.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); emitfnname(c, calleename, c.curmod);
} else { if (callee.kind == nkind.N_DOT) { } else { if (callee.kind == nkind.N_DOT) {
// `m.f()` — pass the explicit module bareword
// so cross-module same-leaf exports resolve.
calleename = callee.str; calleename = callee.str;
emitsymname(c, calleename); let hint: str;
hint.ptr = nil;
hint.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
hint = callee.lhs.str;
};
};
emitfnname(c, calleename, hint);
};}; };};
}; };
emitline("(SB)\n"); emitline("(SB)\n");
@@ -16363,33 +16376,16 @@ fn cgfnparams(c: *cgen, params: *node) void = {
fn cgfn(c: *cgen, fn_: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = {
cgeninit(c, c.a); cgeninit(c, c.a);
c.fnname = fn_.str; c.fnname = fn_.str;
c.curmod = fn_.module;
c.fnret = fn_.lhs; c.fnret = fn_.lhs;
// 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. Drops the
// `exported == 0` skip in the legacy inline form — exported fns
// now mangle too, so cross-module same-leaf exports coexist.
emitline("TEXT "); emitline("TEXT ");
if (fn_.exported == 0) { emitfnname(c, fn_.str, fn_.module);
if (fn_.module.len > 0) {
let isffi: bool = false;
let a: *node = fn_.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
// `main` is the linker entry-point convention; even
// when not marked `export`, it must keep its bare
// name so w6l's _start can resolve `CALL main(SB)`.
// Mirror of cmd/w6c/cgen.c collectmods exemption.
let isentry: bool = streq(fn_.str, "main");
if (!isffi && !isentry) {
os.write(1, fn_.module.ptr, fn_.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
};
let nm: str = fn_.str;
os.write(1, nm.ptr, nm.len: u64);
emitline(",$"); emitline(",$");
// Pre-scan total frame: only count params that land in a local // Pre-scan total frame: only count params that land in a local
@@ -16891,9 +16887,16 @@ type cgen = struct {
aliases: *aliasent, aliases: *aliasent,
structs: *structinfo, structs: *structinfo,
enums: *enumtype, enums: *enumtype,
mods: *modent, // non-exported decls → originating module mods: *modent, // fn (any export status) + non-exported
// let/def/type decls → originating module
lets: *letvar, // top-level mutable scalar `let` bindings lets: *letvar, // top-level mutable scalar `let` bindings
fnname: str, fnname: str,
curmod: str, // current fn's `// MODULE: foo` directive (len=0
// when the fn is in the primary file). Drives
// bare-IDENT call mangling — `frob()` from
// inside lib/foo binds to `foo.frob` even when
// other modules also export `frob`. Set in cgfn
// before walking the body.
fnret: *node, // declared return type of current fn (or nil) fnret: *node, // declared return type of current fn (or nil)
looptop: i32, looptop: i32,
loopendbuf: *str, // stack of end labels for break loopendbuf: *str, // stack of end labels for break
@@ -17998,11 +18001,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = {
// ---- module-private symbol map -------------------------------------- // ---- module-private symbol map --------------------------------------
// //
// Non-exported top-level decls live in their originating module's // Every non-FFI top-level fn decl lives in its module's namespace —
// namespace. cgen mangles those names to `<module>.<name>` at emission // cgen mangles the leaf to `<module>.<name>` at the def site (TEXT)
// time, both at the def site (TEXT/DATA) and at every call/load site, // and at every call/load site, so cross-module same-leaf fns (lib/os
// so two modules can each privately define `cstrlen` without colliding // `read` vs lib/io `read`, both exported) coexist at link time.
// at link time. Exported decls and FFI-bound decls keep their bare name. // Non-fn decls (let/def/type) stick to the older "non-exported only"
// rule: their export-side namespace is the user-facing data ABI and
// mangling them changes the surface. FFI-bound decls (@symbol) keep
// their explicit C symbol regardless of kind.
//
// 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.
type modent = struct { type modent = struct {
mname: str, // the bare ident as it appears in source mname: str, // the bare ident as it appears in source
@@ -18019,8 +18029,19 @@ fn collectmods(c: *cgen, file: *node) void = {
// branch). Earlier nested-if/early-return variants tickled a // branch). Earlier nested-if/early-return variants tickled a
// wwstage cgen bug that dropped most prepends. // wwstage cgen bug that dropped most prepends.
if (d.kind == nkind.N_FNDECL) { if (d.kind == nkind.N_FNDECL) {
if (d.exported == 0) { // Fns mangle regardless of export status — covers
if (d.module.len > 0) { // lib/os.read vs lib/io.read collision.
if (d.module.len > 0) {
let isffi: bool = false;
let a: *node = d.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
if (!streq(d.str, "main")) { if (!streq(d.str, "main")) {
let m: *modent = amalloc(c.a, 48u64): *modent; let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str; m.mname = d.str;
@@ -18080,10 +18101,38 @@ fn modlookup(c: *cgen, name: str) str = {
return empty; return empty;
}; };
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
// preferring entries where module matches `hint`; falls back to the
// first leaf-name match when nothing matches the hint (legacy single-
// owner shape, also covers lookups with hint.len==0). Needed because
// multiple modules can now register the same fn leaf — bare `lookup`
// would otherwise grab whichever module was prepended last.
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
let m: *modent = c.mods;
let first: str;
first.ptr = nil;
first.len = 0;
for (m != nil) {
if (streq(m.mname, name)) {
if (hint.len > 0 && m.module.len > 0
&& streq(m.module, hint)) {
return m.module;
};
if (first.len == 0 && first.ptr == nil) {
first = m.module;
};
};
m = m.mnext;
};
return first;
};
// emitsymname — write the asm symbol name for `ident`. Honours, in // emitsymname — write the asm symbol name for `ident`. Honours, in
// order: FFI mapping (@symbol), module mangling (private decls), bare // order: FFI mapping (@symbol), module mangling (private decls), bare
// name. Use everywhere a top-level name is emitted before `(SB)` or in // name. Use everywhere a top-level non-fn name is emitted before `(SB)`
// a `TEXT name,$N` header. // — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
// cross-module same-leaf fn exports.
fn emitsymname(c: *cgen, ident: str) void = { fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident); let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) { if (resolved.ptr != ident.ptr) {
@@ -18099,6 +18148,25 @@ fn emitsymname(c: *cgen, ident: str) void = {
os.write(1, ident.ptr, ident.len: u64); os.write(1, ident.ptr, ident.len: u64);
}; };
// emitfnname — write the asm symbol name for a fn `ident`, threading
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
// for bare-IDENT calls) through modlookupforfn. Same FFI override
// semantics as emitsymname; same dot-separator format. Use at every
// CALL / LEAQ-of-fn / TEXT-def site.
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
};
// ---- FFI map --------------------------------------------------------- // ---- FFI map ---------------------------------------------------------
fn fficollect(c: *cgen, file: *node) void = { fn fficollect(c: *cgen, file: *node) void = {

View File

@@ -0,0 +1,118 @@
/*
* 706_fnlabel_mangle — cgen mangles fn labels by module so two
* modules can each `export fn ping` (and each define a private
* `fn helper`) without colliding at link time.
*
* The fixture in test/wcc/data/fnlabelmangle/ pins three of the four
* label-emit sites #9 touches:
* mod{1,2}/mod{1,2}.ww
* export fn ping = bareval + helper() + fpi();
* fn helper — private, same-leaf across modules
* fn fpi — `let h = helper; h();` pins LEAQ N_IDENT
* pos.ww
* return mod1.ping() + mod2.ping(); // CALL N_DOT × 2
*
* mod1.ping = 3 + 11 + 11 = 25
* mod2.ping = 5 + 13 + 13 = 31
* total = 56
*
* Pre-fix (cgen emitted bare `TEXT ping` for both modules' exports;
* non-exported `helper` already mangled, so that half is incidental
* regression coverage), the linker collapsed both `ping` symbols and
* one dispatch landed on the wrong body. The bare-IDENT helper /
* `let h = helper` callsites would also pick whichever module the
* lookup found first, silently miscompiling fpi's intra-module call.
*
* Coverage notes:
* - LEAQ N_DOT for fn rvalue (`let p = mod1.ping`) is not exercised.
* wwstage cgdot has no working LEAQ-of-fn N_DOT branch — a row
* would diverge across stages. Tracked as a follow-up.
* - The skip rule's three remaining cases (@symbol / main / empty-
* module) are not pinned by a dedicated row because every passing
* run of `make test` already exercises them: the bootstrap and
* stdlib pull through `@symbol("rt_syscall")` bindings (lib/fmt,
* lib/log), every test program links a bare `main`, and the inline-
* source tests (700, 701, ...) compile fixtures with no module
* directive. A regression in any of those rules would cascade
* across the suite, not show up here.
*
* Both stages run the positive case — cstage and wwstage cgen must
* agree on the mangling rule for ww2/ww3/ww4 byte-identity to hold.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
static int
run_pos(const char *driver, const char *fixdir, const char *tag)
{
char cmd[2048];
snprintf(cmd, sizeof cmd,
"cd %s && %s build pos.ww >/dev/null 2>&1", fixdir, driver);
if (runwait(cmd) != 0) {
fprintf(stderr,
"fnlabel_mangle[%s]: pos.ww build failed\n", tag);
return 1;
}
char bin[2048];
snprintf(bin, sizeof bin, "%s/pos", fixdir);
int got = runwait(bin);
unlink(bin);
if (got != 56) {
fprintf(stderr,
"fnlabel_mangle[%s]: pos.ww exit=%d want=56 — "
"fn labels likely collapsed at link\n", tag, got);
return 1;
}
return 0;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
char fixdir[1024];
if (getcwd(fixdir, sizeof fixdir) == NULL) return 1;
size_t cwd_n = strlen(fixdir);
const char *rel = "/test/wcc/data/fnlabelmangle";
if (cwd_n + strlen(rel) + 1 >= sizeof fixdir) return 1;
memcpy(fixdir + cwd_n, rel, strlen(rel) + 1);
int fail = 0;
fail += run_pos(cdrv, fixdir, "cstage");
fail += run_pos(wdrv, fixdir, "wwstage");
if (fail) {
fprintf(stderr,
"fnlabel_mangle: %d case(s) failed\n", fail);
return 1;
}
printf("fnlabel_mangle: 2/2 ok\n");
return 0;
}

View File

@@ -0,0 +1,19 @@
// Two modules each `export fn ping` and each define a private
// `helper` with the same leaf. Together with mod2 they pin three of
// the four label-emit sites #9 touches:
// - CALL N_DOT : main calls mod1.ping / mod2.ping
// - CALL N_IDENT : ping bare-calls helper inside its own module
// - LEAQ N_IDENT : fpi takes `helper` by value, then calls it
//
// The LEAQ N_DOT path (`let p = mod1.ping`) is intentionally not
// exercised here — wwstage cgdot has no working LEAQ-of-fn N_DOT
// branch, so a row would diverge across stages.
fn helper() i32 = { return 11i32; };
fn fpi() i32 = {
let h: fn() i32 = helper;
return h();
};
export fn ping() i32 = { return 3i32 + helper() + fpi(); };

View File

@@ -0,0 +1,11 @@
// Sibling of mod1.ww — same leaves (`ping`, `helper`, `fpi`),
// distinct values. See mod1.ww for the coverage-rationale comment.
fn helper() i32 = { return 13i32; };
fn fpi() i32 = {
let h: fn() i32 = helper;
return h();
};
export fn ping() i32 = { return 5i32 + helper() + fpi(); };

View File

@@ -0,0 +1,18 @@
// Positive case: import both modules, dispatch through the explicit
// module qualifier. Each module's ping = bare-value + helper() (CALL
// N_IDENT) + fpi() (which exercises LEAQ N_IDENT via `let h = helper`).
//
// mod1.ping = 3 + 11 + 11 = 25
// mod2.ping = 5 + 13 + 13 = 31
// total = 56
//
// Pre-fix the four `helper` / `ping` symbols would collapse at link
// or the hint-less lookup would silently grab the wrong module's
// body — exit would land at 50/62/some other value, never 56.
use mod1;
use mod2;
fn main() i32 = {
return mod1.ping() + mod2.ping();
};