diff --git a/Makefile b/Makefile index 14d356b0..3c7f1b52 100644 --- a/Makefile +++ b/Makefile @@ -232,6 +232,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_nested_structlit \ $(BIN)/test_dot_structlit \ $(BIN)/test_nested_call_rhs \ + $(BIN)/test_fnlabel_mangle \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(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) $(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)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 662eb865..4a9cea53 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 . - * 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 . 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 `.` 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 → .. @@ -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); diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index 7f61fe85..baceaa46 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -34,6 +34,13 @@ struct Cg { Arena *a; Prog *head, *tail; 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 curoff; /* current top of locals */ Scope *locals; /* (name → offset) tracked via Sym */ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3dcf8f77..91c5a112 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9544,13 +9544,14 @@ fn cgident(c: *cgen, n: *node) void = { }; // Fn-name used as a value (e.g. `let f = some_fn;` or // `... = 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 // 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); if (rt != nil) { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -12142,11 +12143,23 @@ fn cgcall(c: *cgen, n: *node) void = { emitline("\tCALL\t"); if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // Bare `f()` — same-module by ww's resolver, + // so c.curmod is the disambiguation hint. calleename = callee.str; - emitsymname(c, calleename); + emitfnname(c, calleename, c.curmod); } else { if (callee.kind == nkind.N_DOT) { + // `m.f()` — pass the explicit module bareword + // so cross-module same-leaf exports resolve. 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"); @@ -16363,33 +16376,16 @@ fn cgfnparams(c: *cgen, params: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = { cgeninit(c, c.a); c.fnname = fn_.str; + c.curmod = fn_.module; 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 "); - if (fn_.exported == 0) { - 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); + emitfnname(c, fn_.str, fn_.module); emitline(",$"); // Pre-scan total frame: only count params that land in a local @@ -16891,9 +16887,16 @@ type cgen = struct { aliases: *aliasent, structs: *structinfo, 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 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) looptop: i32, loopendbuf: *str, // stack of end labels for break @@ -17998,11 +18001,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = { // ---- module-private symbol map -------------------------------------- // -// Non-exported top-level decls live in their originating module's -// namespace. cgen mangles those names to `.` at emission -// time, both at the def site (TEXT/DATA) and at every call/load site, -// so two modules can each privately define `cstrlen` without colliding -// at link time. Exported decls and FFI-bound decls keep their bare name. +// Every non-FFI top-level fn decl lives in its module's namespace — +// cgen mangles the leaf to `.` at the def site (TEXT) +// and at every call/load site, so cross-module same-leaf fns (lib/os +// `read` vs lib/io `read`, both exported) coexist at link time. +// 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 { 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 // wwstage cgen bug that dropped most prepends. if (d.kind == nkind.N_FNDECL) { - if (d.exported == 0) { - if (d.module.len > 0) { + // Fns mangle regardless of export status — covers + // 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")) { let m: *modent = amalloc(c.a, 48u64): *modent; m.mname = d.str; @@ -18080,10 +18101,38 @@ fn modlookup(c: *cgen, name: str) str = { 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 // order: FFI mapping (@symbol), module mangling (private decls), bare -// name. Use everywhere a top-level name is emitted before `(SB)` or in -// a `TEXT name,$N` header. +// name. Use everywhere a top-level non-fn name is emitted before `(SB)` +// — 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 = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { @@ -18099,6 +18148,25 @@ fn emitsymname(c: *cgen, ident: str) void = { 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 --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 281a388f..9275992f 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -367,9 +367,16 @@ type cgen = struct { aliases: *aliasent, structs: *structinfo, 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 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) looptop: i32, loopendbuf: *str, // stack of end labels for break @@ -1474,11 +1481,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = { // ---- module-private symbol map -------------------------------------- // -// Non-exported top-level decls live in their originating module's -// namespace. cgen mangles those names to `.` at emission -// time, both at the def site (TEXT/DATA) and at every call/load site, -// so two modules can each privately define `cstrlen` without colliding -// at link time. Exported decls and FFI-bound decls keep their bare name. +// Every non-FFI top-level fn decl lives in its module's namespace — +// cgen mangles the leaf to `.` at the def site (TEXT) +// and at every call/load site, so cross-module same-leaf fns (lib/os +// `read` vs lib/io `read`, both exported) coexist at link time. +// 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 { 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 // wwstage cgen bug that dropped most prepends. if (d.kind == nkind.N_FNDECL) { - if (d.exported == 0) { - if (d.module.len > 0) { + // Fns mangle regardless of export status — covers + // 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")) { let m: *modent = amalloc(c.a, 48u64): *modent; m.mname = d.str; @@ -1556,10 +1581,38 @@ fn modlookup(c: *cgen, name: str) str = { 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 // order: FFI mapping (@symbol), module mangling (private decls), bare -// name. Use everywhere a top-level name is emitted before `(SB)` or in -// a `TEXT name,$N` header. +// name. Use everywhere a top-level non-fn name is emitted before `(SB)` +// — 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 = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { @@ -1575,6 +1628,25 @@ fn emitsymname(c: *cgen, ident: str) void = { 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 --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 65e78486..8e01b8d1 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -711,33 +711,16 @@ fn cgfnparams(c: *cgen, params: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = { cgeninit(c, c.a); c.fnname = fn_.str; + c.curmod = fn_.module; 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 "); - if (fn_.exported == 0) { - 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); + emitfnname(c, fn_.str, fn_.module); emitline(",$"); // Pre-scan total frame: only count params that land in a local diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1a015503..07607a0a 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -528,13 +528,14 @@ fn cgident(c: *cgen, n: *node) void = { }; // Fn-name used as a value (e.g. `let f = some_fn;` or // `... = 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 // 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); if (rt != nil) { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -3126,11 +3127,23 @@ fn cgcall(c: *cgen, n: *node) void = { emitline("\tCALL\t"); if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // Bare `f()` — same-module by ww's resolver, + // so c.curmod is the disambiguation hint. calleename = callee.str; - emitsymname(c, calleename); + emitfnname(c, calleename, c.curmod); } else { if (callee.kind == nkind.N_DOT) { + // `m.f()` — pass the explicit module bareword + // so cross-module same-leaf exports resolve. 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"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d0897870..73f8ddf8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9544,13 +9544,14 @@ fn cgident(c: *cgen, n: *node) void = { }; // Fn-name used as a value (e.g. `let f = some_fn;` or // `... = 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 // 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); if (rt != nil) { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -12142,11 +12143,23 @@ fn cgcall(c: *cgen, n: *node) void = { emitline("\tCALL\t"); if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // Bare `f()` — same-module by ww's resolver, + // so c.curmod is the disambiguation hint. calleename = callee.str; - emitsymname(c, calleename); + emitfnname(c, calleename, c.curmod); } else { if (callee.kind == nkind.N_DOT) { + // `m.f()` — pass the explicit module bareword + // so cross-module same-leaf exports resolve. 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"); @@ -16363,33 +16376,16 @@ fn cgfnparams(c: *cgen, params: *node) void = { fn cgfn(c: *cgen, fn_: *node) void = { cgeninit(c, c.a); c.fnname = fn_.str; + c.curmod = fn_.module; 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 "); - if (fn_.exported == 0) { - 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); + emitfnname(c, fn_.str, fn_.module); emitline(",$"); // Pre-scan total frame: only count params that land in a local @@ -16891,9 +16887,16 @@ type cgen = struct { aliases: *aliasent, structs: *structinfo, 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 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) looptop: i32, loopendbuf: *str, // stack of end labels for break @@ -17998,11 +18001,18 @@ fn deflookuprhs(c: *cgen, name: str) *node = { // ---- module-private symbol map -------------------------------------- // -// Non-exported top-level decls live in their originating module's -// namespace. cgen mangles those names to `.` at emission -// time, both at the def site (TEXT/DATA) and at every call/load site, -// so two modules can each privately define `cstrlen` without colliding -// at link time. Exported decls and FFI-bound decls keep their bare name. +// Every non-FFI top-level fn decl lives in its module's namespace — +// cgen mangles the leaf to `.` at the def site (TEXT) +// and at every call/load site, so cross-module same-leaf fns (lib/os +// `read` vs lib/io `read`, both exported) coexist at link time. +// 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 { 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 // wwstage cgen bug that dropped most prepends. if (d.kind == nkind.N_FNDECL) { - if (d.exported == 0) { - if (d.module.len > 0) { + // Fns mangle regardless of export status — covers + // 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")) { let m: *modent = amalloc(c.a, 48u64): *modent; m.mname = d.str; @@ -18080,10 +18101,38 @@ fn modlookup(c: *cgen, name: str) str = { 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 // order: FFI mapping (@symbol), module mangling (private decls), bare -// name. Use everywhere a top-level name is emitted before `(SB)` or in -// a `TEXT name,$N` header. +// name. Use everywhere a top-level non-fn name is emitted before `(SB)` +// — 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 = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { @@ -18099,6 +18148,25 @@ fn emitsymname(c: *cgen, ident: str) void = { 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 --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/test/wcc/706_fnlabel_mangle.c b/test/wcc/706_fnlabel_mangle.c new file mode 100644 index 00000000..b2702417 --- /dev/null +++ b/test/wcc/706_fnlabel_mangle.c @@ -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 +#include +#include +#include +#include +#include + +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; +} diff --git a/test/wcc/data/fnlabelmangle/mod1/mod1.ww b/test/wcc/data/fnlabelmangle/mod1/mod1.ww new file mode 100644 index 00000000..97801899 --- /dev/null +++ b/test/wcc/data/fnlabelmangle/mod1/mod1.ww @@ -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(); }; diff --git a/test/wcc/data/fnlabelmangle/mod2/mod2.ww b/test/wcc/data/fnlabelmangle/mod2/mod2.ww new file mode 100644 index 00000000..26e3752c --- /dev/null +++ b/test/wcc/data/fnlabelmangle/mod2/mod2.ww @@ -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(); }; diff --git a/test/wcc/data/fnlabelmangle/pos.ww b/test/wcc/data/fnlabelmangle/pos.ww new file mode 100644 index 00000000..bc5fd2ae --- /dev/null +++ b/test/wcc/data/fnlabelmangle/pos.ww @@ -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(); +};