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:
7
Makefile
7
Makefile
@@ -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)
|
||||||
|
|||||||
126
cmd/w6c/cgen.c
126
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
|
/* 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
118
test/wcc/706_fnlabel_mangle.c
Normal file
118
test/wcc/706_fnlabel_mangle.c
Normal 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;
|
||||||
|
}
|
||||||
19
test/wcc/data/fnlabelmangle/mod1/mod1.ww
Normal file
19
test/wcc/data/fnlabelmangle/mod1/mod1.ww
Normal 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(); };
|
||||||
11
test/wcc/data/fnlabelmangle/mod2/mod2.ww
Normal file
11
test/wcc/data/fnlabelmangle/mod2/mod2.ww
Normal 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(); };
|
||||||
18
test/wcc/data/fnlabelmangle/pos.ww
Normal file
18
test/wcc/data/fnlabelmangle/pos.ww
Normal 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();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user