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

@@ -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 `<module>.<name>` 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 `<module>.<name>` 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 = {

View File

@@ -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 `<module>.<name>` 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 `<module>.<name>` 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 = {

View File

@@ -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

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
// `... = 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");

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
// `... = 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 `<module>.<name>` 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 `<module>.<name>` 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 = {