wcc,ww: bare-module fn mangles bare, not an imported same-leaf (M4 E2, #84)
A package-less primary's bare fn (module="") whose leaf collided with an imported module's same-leaf exported fn was mis-mangled to the imported qualified name (a user `fn run` emitted as `test.run`), producing a dead-duplicate symbol the linker silently shadowed -- a #263-class silent miscompile, gate-blind and symmetric across both stages. cgen now registers bare-module fns and resolves a bare-ident reference to its own bare leaf: mod_lookup_for_fn prefers the bare entry when the call carries no module hint and skips bare entries when it does, so the moduled-caller path stays byte-identical. The moduled `main` entry carve-out is an orthogonal rule (the linker entry is force-bared) and is retained. Prereq for the @test user-`run` coexist (#80). The bare non-fn (let/def/type) sibling is the same class but hint-less; deferred as #85, noted at the retained skip.
This commit is contained in:
11
Makefile
11
Makefile
@@ -560,6 +560,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
|||||||
$(BIN)/test_lib_byteid \
|
$(BIN)/test_lib_byteid \
|
||||||
$(BIN)/test_m2wwi_run \
|
$(BIN)/test_m2wwi_run \
|
||||||
$(BIN)/test_m3sep_run \
|
$(BIN)/test_m3sep_run \
|
||||||
|
$(BIN)/test_barefn_collide_run \
|
||||||
$(BIN)/test_sepbuild_run \
|
$(BIN)/test_sepbuild_run \
|
||||||
$(BIN)/test_sepdotpath_run \
|
$(BIN)/test_sepdotpath_run \
|
||||||
$(BIN)/test_seproot_export_run \
|
$(BIN)/test_seproot_export_run \
|
||||||
@@ -3133,6 +3134,16 @@ $(BIN)/test_sepbuild_run: test/wcc/989_sepbuild_run.c $(BIN)/ww $(BIN)/ww_ww \
|
|||||||
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# 989_barefn_collide_run — #84 cgen bare-module fn-leaf collision gate. A
|
||||||
|
# package-less root's bare `fn run` must stay BARE (distinct from an
|
||||||
|
# imported `aa.run`), not mis-mangle onto the import (a #263-class silent
|
||||||
|
# dead-dup). Drives BOTH driver stages on a real dir-package import, so it
|
||||||
|
# needs both driver + both compiler + both linker stages + libwwrt.
|
||||||
|
$(BIN)/test_barefn_collide_run: test/wcc/989_barefn_collide_run.c $(BIN)/ww $(BIN)/ww_ww \
|
||||||
|
$(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \
|
||||||
|
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
# 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package
|
# 989_sepdotpath_run — sep-build of a DOTTED-path (multi-component) package
|
||||||
# (#57): the producer must tag a primary body by its full dotted import path
|
# (#57): the producer must tag a primary body by its full dotted import path
|
||||||
# (`//ww:module-reset a.b`), not the leaf `package` clause, so definer ==
|
# (`//ww:module-reset a.b`), not the leaf `package` clause, so definer ==
|
||||||
|
|||||||
@@ -1458,18 +1458,31 @@ mod_collect(Cg *c, Node *file)
|
|||||||
* like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
* like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
||||||
* `MAX` that two packages could clash on under sep-compile. No
|
* `MAX` that two packages could clash on under sep-compile. No
|
||||||
* export skip: every decl with a module mangles identically. */
|
* export skip: every decl with a module mangles identically. */
|
||||||
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
|
int bare = (d->module == NULL || d->module[0] == '\0');
|
||||||
* marked `export`, it must keep its bare name so w6l can
|
/* #84: register bare-module (package-less `//ww:module-reset`)
|
||||||
* resolve `_start`'s `CALL main(SB)`. M1 #32: only the ROOT
|
* FNS too — previously ALL bare decls were skipped here, so a
|
||||||
* unit's main stays bare; an IMPORTED `fn main` mangles on its
|
* bare fn never entered mod_map and a bare-ident ref to it
|
||||||
* path (closes #31's dup-main by construction). */
|
* first-matched an imported module's same-leaf fn (mod_lookup_
|
||||||
|
* for_fn) → a #40 residual / #263-class silent dead-dup. With
|
||||||
|
* the bare entry present, mod_lookup_for_fn prefers it on a NULL
|
||||||
|
* hint. Non-fn bare decls keep the legacy skip (their collision
|
||||||
|
* class is outside #84's fn scope; pre-bind proven). */
|
||||||
|
/* #85 (deferred): the bare-NON-fn sibling (a package-less let/def/
|
||||||
|
* type leaf colliding with an imported same-kind export) needs a
|
||||||
|
* different fix — mod_lookup (non-fn) is hint-LESS — and has no
|
||||||
|
* corpus repro; retained skip until then (rule-11). */
|
||||||
|
if (bare && !isfn) continue;
|
||||||
|
/* `main` is the linker entry-point convention. A `package main`
|
||||||
|
* primary's decls are MODULED "main", so main is NOT bare here;
|
||||||
|
* skip it (cgfn force-emits the bare `main` label). M1 #32: only
|
||||||
|
* the ROOT main (imported==0) stays bare; an IMPORTED `fn main`
|
||||||
|
* mangles on its path (closes #31's dup-main by construction). */
|
||||||
if (d->str && strcmp(d->str, "main") == 0 && !d->imported)
|
if (d->str && strcmp(d->str, "main") == 0 && !d->imported)
|
||||||
continue;
|
continue;
|
||||||
Mod *m = amalloc(c->a, sizeof *m);
|
Mod *m = amalloc(c->a, sizeof *m);
|
||||||
m->name = d->str;
|
m->name = d->str;
|
||||||
m->module = d->module;
|
m->module = bare ? NULL : d->module;
|
||||||
m->next = mod_map;
|
m->next = mod_map;
|
||||||
mod_map = m;
|
mod_map = m;
|
||||||
}
|
}
|
||||||
@@ -1502,8 +1515,18 @@ mod_lookup_for_fn(const char *name, const char *hint)
|
|||||||
const char *first = NULL;
|
const char *first = NULL;
|
||||||
for (Mod *m = mod_map; m; m = m->next) {
|
for (Mod *m = mod_map; m; m = m->next) {
|
||||||
if (strcmp(m->name, name) != 0) continue;
|
if (strcmp(m->name, name) != 0) continue;
|
||||||
if (hint != NULL && m->module != NULL
|
if (m->module == NULL) {
|
||||||
&& strcmp(m->module, hint) == 0)
|
/* #84: a bare-module entry (now registered). A bare-ident
|
||||||
|
* ref (hint==NULL: the caller is itself in the bare/root
|
||||||
|
* module) resolves to it — bare wins over an imported
|
||||||
|
* same-leaf fn, by construction, order-independent. With a
|
||||||
|
* non-NULL hint the caller named a module, so a bare decl
|
||||||
|
* is irrelevant: skip it, leaving the legacy first-imported
|
||||||
|
* -match untouched (byte-id-neutral for moduled callers). */
|
||||||
|
if (hint == NULL) return NULL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hint != NULL && strcmp(m->module, hint) == 0)
|
||||||
return m->module;
|
return m->module;
|
||||||
if (first == NULL) first = m->module;
|
if (first == NULL) first = m->module;
|
||||||
}
|
}
|
||||||
@@ -14931,9 +14954,13 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
|||||||
/* Mangle the label using the fn's own module as the hint — picks
|
/* Mangle the label using the fn's own module as the hint — picks
|
||||||
* the right entry when multiple modules export the same leaf. M1 #32:
|
* the right entry when multiple modules export the same leaf. M1 #32:
|
||||||
* the ROOT-unit main (imported==0) is the bare `_start` entry — emit
|
* the ROOT-unit main (imported==0) is the bare `_start` entry — emit
|
||||||
* it bare directly, mirroring mod_collect's skip; without this its
|
* it bare directly, mirroring mod_collect's skip. This is irreducibly
|
||||||
* NULL cur_mod would fall through mafn's first-leaf match onto an
|
* name-based: a `package main` primary's decls are MODULED "main"
|
||||||
* IMPORTED package's now-registered `pkg.main`. */
|
* (the package clause sets cur_mod; cmd/ww/main.c:394), so main is
|
||||||
|
* NOT a bare-module decl — removing this carve-out mangles it to
|
||||||
|
* `main.main` (undefined `main`). #84's bare-module handling is the
|
||||||
|
* orthogonal axis (a package-LESS `//ww:module-reset` fn, cur_mod
|
||||||
|
* NULL), resolved in mod_lookup_for_fn, NOT here. */
|
||||||
if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported)
|
if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported)
|
||||||
text->to = asym("main");
|
text->to = asym("main");
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -41304,12 +41304,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
|
|||||||
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
||||||
|
|
||||||
// Emit the TEXT label via emitfnname so the def site picks up the
|
// Emit the TEXT label via emitfnname so the def site picks up the
|
||||||
// same skip rule (FFI / `main` / empty-module) and the same module
|
// same module hint (this fn's own module) the call sites use. M1 #32:
|
||||||
// hint (this fn's own module) that the call sites use. M1 #32: the
|
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
|
||||||
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
|
// it bare directly, mirroring collectmods' skip. Irreducibly name-
|
||||||
// bare directly, mirroring collectmods' skip; without this its
|
// based: a `package main` primary's decls are MODULED "main" (the
|
||||||
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
|
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
|
||||||
// onto an IMPORTED package's now-registered `pkg.main`.
|
// bare-module decl — dropping this carve-out mangles it to `main.main`
|
||||||
|
// (undefined `main`). #84's bare-module handling is the orthogonal
|
||||||
|
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
|
||||||
|
// in modlookupforfn, NOT here.
|
||||||
emitline("TEXT ");
|
emitline("TEXT ");
|
||||||
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
||||||
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
||||||
@@ -45224,23 +45227,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
if (d.kind == syntax.nkind.N_FNDECL) {
|
if (d.kind == syntax.nkind.N_FNDECL) {
|
||||||
// Fns mangle regardless of export status — covers
|
// Fns mangle regardless of export status — covers
|
||||||
// lib/os.read vs lib/io.read collision.
|
// lib/os.read vs lib/io.read collision.
|
||||||
if (d.nmod.len > 0) {
|
let isffi: bool = false;
|
||||||
let isffi: bool = false;
|
let a: *syntax.node = d.attr;
|
||||||
let a: *syntax.node = d.attr;
|
for (a != nil) {
|
||||||
for (a != nil) {
|
if (a.kind == syntax.nkind.N_ATTR) {
|
||||||
if (a.kind == syntax.nkind.N_ATTR) {
|
let an: str = a.str;
|
||||||
let an: str = a.str;
|
if (syntax.streq(an, "symbol")) { isffi = true; };
|
||||||
if (syntax.streq(an, "symbol")) { isffi = true; };
|
|
||||||
};
|
|
||||||
a = a.next;
|
|
||||||
};
|
};
|
||||||
if (!isffi) {
|
a = a.next;
|
||||||
// M1 #32: the ROOT main (imported==0) stays bare;
|
};
|
||||||
// an IMPORTED `fn main` mangles on its path.
|
if (!isffi) {
|
||||||
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
// #84: register bare-module (package-less `//ww:module-
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
// reset`) fns TOO (nmod.len==0) — previously the
|
||||||
c.mods = m;
|
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
|
||||||
};
|
// never entered c.mods and a bare-ident ref to it first-
|
||||||
|
// matched an imported same-leaf fn (modlookupforfn) → a
|
||||||
|
// #40 residual / #263-class silent dead-dup. With the bare
|
||||||
|
// entry present, modlookupforfn prefers it on an empty
|
||||||
|
// hint. main is the linker entry convention: a `package
|
||||||
|
// main` primary's decls are MODULED "main" so main is NOT
|
||||||
|
// bare here — skip it (cgfn force-emits the bare `main`
|
||||||
|
// label). M1 #32: only ROOT main (imported==0) stays bare;
|
||||||
|
// an IMPORTED `fn main` mangles on its path. Mirrors
|
||||||
|
// cstage cgen.c mod_collect.
|
||||||
|
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
||||||
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
|
c.mods = m;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -45248,6 +45260,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
||||||
// `MAX` two packages could clash on under sep-compile. No export
|
// `MAX` two packages could clash on under sep-compile. No export
|
||||||
// guard: every decl with a module mangles identically.
|
// guard: every decl with a module mangles identically.
|
||||||
|
// #85 (deferred): the `nmod.len > 0` gate below retains the
|
||||||
|
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
|
||||||
|
// type leaf colliding with an imported same-kind export) needs a
|
||||||
|
// different fix (modlookup is hint-LESS) and has no corpus repro;
|
||||||
|
// retained until then (rule-11).
|
||||||
if (d.kind == syntax.nkind.N_DEF) {
|
if (d.kind == syntax.nkind.N_DEF) {
|
||||||
if (d.nmod.len > 0) {
|
if (d.nmod.len > 0) {
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
@@ -45323,12 +45340,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
|
|||||||
first.len = 0;
|
first.len = 0;
|
||||||
for (m != nil) {
|
for (m != nil) {
|
||||||
if (syntax.streq(m.mname, name)) {
|
if (syntax.streq(m.mname, name)) {
|
||||||
if (hint.len > 0 && m.nmod.len > 0
|
if (m.nmod.len == 0) {
|
||||||
&& syntax.streq(m.nmod, hint)) {
|
// #84: a bare-module entry (now registered). A bare-ident
|
||||||
return m.nmod;
|
// ref (hint.len==0: the caller is itself in the bare/root
|
||||||
};
|
// module) resolves to it — bare wins over an imported
|
||||||
if (first.len == 0 && first.ptr == nil) {
|
// same-leaf fn, by construction, order-independent. A
|
||||||
first = m.nmod;
|
// non-empty hint named a module, so a bare decl is
|
||||||
|
// irrelevant: skip it, leaving the legacy first-imported-
|
||||||
|
// match untouched (byte-id-neutral for moduled callers).
|
||||||
|
if (hint.len == 0) {
|
||||||
|
let empty: str;
|
||||||
|
empty.ptr = nil;
|
||||||
|
empty.len = 0;
|
||||||
|
return empty;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
|
||||||
|
return m.nmod;
|
||||||
|
};
|
||||||
|
if (first.len == 0 && first.ptr == nil) {
|
||||||
|
first = m.nmod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
m = m.mnext;
|
m = m.mnext;
|
||||||
|
|||||||
@@ -3852,23 +3852,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
if (d.kind == syntax.nkind.N_FNDECL) {
|
if (d.kind == syntax.nkind.N_FNDECL) {
|
||||||
// Fns mangle regardless of export status — covers
|
// Fns mangle regardless of export status — covers
|
||||||
// lib/os.read vs lib/io.read collision.
|
// lib/os.read vs lib/io.read collision.
|
||||||
if (d.nmod.len > 0) {
|
let isffi: bool = false;
|
||||||
let isffi: bool = false;
|
let a: *syntax.node = d.attr;
|
||||||
let a: *syntax.node = d.attr;
|
for (a != nil) {
|
||||||
for (a != nil) {
|
if (a.kind == syntax.nkind.N_ATTR) {
|
||||||
if (a.kind == syntax.nkind.N_ATTR) {
|
let an: str = a.str;
|
||||||
let an: str = a.str;
|
if (syntax.streq(an, "symbol")) { isffi = true; };
|
||||||
if (syntax.streq(an, "symbol")) { isffi = true; };
|
|
||||||
};
|
|
||||||
a = a.next;
|
|
||||||
};
|
};
|
||||||
if (!isffi) {
|
a = a.next;
|
||||||
// M1 #32: the ROOT main (imported==0) stays bare;
|
};
|
||||||
// an IMPORTED `fn main` mangles on its path.
|
if (!isffi) {
|
||||||
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
// #84: register bare-module (package-less `//ww:module-
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
// reset`) fns TOO (nmod.len==0) — previously the
|
||||||
c.mods = m;
|
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
|
||||||
};
|
// never entered c.mods and a bare-ident ref to it first-
|
||||||
|
// matched an imported same-leaf fn (modlookupforfn) → a
|
||||||
|
// #40 residual / #263-class silent dead-dup. With the bare
|
||||||
|
// entry present, modlookupforfn prefers it on an empty
|
||||||
|
// hint. main is the linker entry convention: a `package
|
||||||
|
// main` primary's decls are MODULED "main" so main is NOT
|
||||||
|
// bare here — skip it (cgfn force-emits the bare `main`
|
||||||
|
// label). M1 #32: only ROOT main (imported==0) stays bare;
|
||||||
|
// an IMPORTED `fn main` mangles on its path. Mirrors
|
||||||
|
// cstage cgen.c mod_collect.
|
||||||
|
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
||||||
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
|
c.mods = m;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -3876,6 +3885,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
||||||
// `MAX` two packages could clash on under sep-compile. No export
|
// `MAX` two packages could clash on under sep-compile. No export
|
||||||
// guard: every decl with a module mangles identically.
|
// guard: every decl with a module mangles identically.
|
||||||
|
// #85 (deferred): the `nmod.len > 0` gate below retains the
|
||||||
|
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
|
||||||
|
// type leaf colliding with an imported same-kind export) needs a
|
||||||
|
// different fix (modlookup is hint-LESS) and has no corpus repro;
|
||||||
|
// retained until then (rule-11).
|
||||||
if (d.kind == syntax.nkind.N_DEF) {
|
if (d.kind == syntax.nkind.N_DEF) {
|
||||||
if (d.nmod.len > 0) {
|
if (d.nmod.len > 0) {
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
@@ -3951,12 +3965,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
|
|||||||
first.len = 0;
|
first.len = 0;
|
||||||
for (m != nil) {
|
for (m != nil) {
|
||||||
if (syntax.streq(m.mname, name)) {
|
if (syntax.streq(m.mname, name)) {
|
||||||
if (hint.len > 0 && m.nmod.len > 0
|
if (m.nmod.len == 0) {
|
||||||
&& syntax.streq(m.nmod, hint)) {
|
// #84: a bare-module entry (now registered). A bare-ident
|
||||||
return m.nmod;
|
// ref (hint.len==0: the caller is itself in the bare/root
|
||||||
};
|
// module) resolves to it — bare wins over an imported
|
||||||
if (first.len == 0 && first.ptr == nil) {
|
// same-leaf fn, by construction, order-independent. A
|
||||||
first = m.nmod;
|
// non-empty hint named a module, so a bare decl is
|
||||||
|
// irrelevant: skip it, leaving the legacy first-imported-
|
||||||
|
// match untouched (byte-id-neutral for moduled callers).
|
||||||
|
if (hint.len == 0) {
|
||||||
|
let empty: str;
|
||||||
|
empty.ptr = nil;
|
||||||
|
empty.len = 0;
|
||||||
|
return empty;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
|
||||||
|
return m.nmod;
|
||||||
|
};
|
||||||
|
if (first.len == 0 && first.ptr == nil) {
|
||||||
|
first = m.nmod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
m = m.mnext;
|
m = m.mnext;
|
||||||
|
|||||||
@@ -598,12 +598,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
|
|||||||
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
||||||
|
|
||||||
// Emit the TEXT label via emitfnname so the def site picks up the
|
// Emit the TEXT label via emitfnname so the def site picks up the
|
||||||
// same skip rule (FFI / `main` / empty-module) and the same module
|
// same module hint (this fn's own module) the call sites use. M1 #32:
|
||||||
// hint (this fn's own module) that the call sites use. M1 #32: the
|
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
|
||||||
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
|
// it bare directly, mirroring collectmods' skip. Irreducibly name-
|
||||||
// bare directly, mirroring collectmods' skip; without this its
|
// based: a `package main` primary's decls are MODULED "main" (the
|
||||||
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
|
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
|
||||||
// onto an IMPORTED package's now-registered `pkg.main`.
|
// bare-module decl — dropping this carve-out mangles it to `main.main`
|
||||||
|
// (undefined `main`). #84's bare-module handling is the orthogonal
|
||||||
|
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
|
||||||
|
// in modlookupforfn, NOT here.
|
||||||
emitline("TEXT ");
|
emitline("TEXT ");
|
||||||
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
||||||
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
||||||
|
|||||||
@@ -41304,12 +41304,15 @@ fn cgfn(c: *cgen, fn_: *syntax.node) void = {
|
|||||||
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
|
||||||
|
|
||||||
// Emit the TEXT label via emitfnname so the def site picks up the
|
// Emit the TEXT label via emitfnname so the def site picks up the
|
||||||
// same skip rule (FFI / `main` / empty-module) and the same module
|
// same module hint (this fn's own module) the call sites use. M1 #32:
|
||||||
// hint (this fn's own module) that the call sites use. M1 #32: the
|
// the ROOT-unit main (imported==0) is the bare `_start` entry — emit
|
||||||
// ROOT-unit main (imported==0) is the bare `_start` entry — emit it
|
// it bare directly, mirroring collectmods' skip. Irreducibly name-
|
||||||
// bare directly, mirroring collectmods' skip; without this its
|
// based: a `package main` primary's decls are MODULED "main" (the
|
||||||
// fn_.nmod hint would fall through modlookupforfn's first-leaf match
|
// package clause sets curmod; cmd/ww/main.c:394), so main is NOT a
|
||||||
// onto an IMPORTED package's now-registered `pkg.main`.
|
// bare-module decl — dropping this carve-out mangles it to `main.main`
|
||||||
|
// (undefined `main`). #84's bare-module handling is the orthogonal
|
||||||
|
// axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved
|
||||||
|
// in modlookupforfn, NOT here.
|
||||||
emitline("TEXT ");
|
emitline("TEXT ");
|
||||||
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
if (syntax.streq(fn_.str, "main") && fn_.imported == 0) {
|
||||||
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
emitbytes(fn_.str.ptr, fn_.str.len: u64);
|
||||||
@@ -45224,23 +45227,32 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
if (d.kind == syntax.nkind.N_FNDECL) {
|
if (d.kind == syntax.nkind.N_FNDECL) {
|
||||||
// Fns mangle regardless of export status — covers
|
// Fns mangle regardless of export status — covers
|
||||||
// lib/os.read vs lib/io.read collision.
|
// lib/os.read vs lib/io.read collision.
|
||||||
if (d.nmod.len > 0) {
|
let isffi: bool = false;
|
||||||
let isffi: bool = false;
|
let a: *syntax.node = d.attr;
|
||||||
let a: *syntax.node = d.attr;
|
for (a != nil) {
|
||||||
for (a != nil) {
|
if (a.kind == syntax.nkind.N_ATTR) {
|
||||||
if (a.kind == syntax.nkind.N_ATTR) {
|
let an: str = a.str;
|
||||||
let an: str = a.str;
|
if (syntax.streq(an, "symbol")) { isffi = true; };
|
||||||
if (syntax.streq(an, "symbol")) { isffi = true; };
|
|
||||||
};
|
|
||||||
a = a.next;
|
|
||||||
};
|
};
|
||||||
if (!isffi) {
|
a = a.next;
|
||||||
// M1 #32: the ROOT main (imported==0) stays bare;
|
};
|
||||||
// an IMPORTED `fn main` mangles on its path.
|
if (!isffi) {
|
||||||
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
// #84: register bare-module (package-less `//ww:module-
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
// reset`) fns TOO (nmod.len==0) — previously the
|
||||||
c.mods = m;
|
// `nmod.len > 0` gate skipped ALL bare fns, so a bare fn
|
||||||
};
|
// never entered c.mods and a bare-ident ref to it first-
|
||||||
|
// matched an imported same-leaf fn (modlookupforfn) → a
|
||||||
|
// #40 residual / #263-class silent dead-dup. With the bare
|
||||||
|
// entry present, modlookupforfn prefers it on an empty
|
||||||
|
// hint. main is the linker entry convention: a `package
|
||||||
|
// main` primary's decls are MODULED "main" so main is NOT
|
||||||
|
// bare here — skip it (cgfn force-emits the bare `main`
|
||||||
|
// label). M1 #32: only ROOT main (imported==0) stays bare;
|
||||||
|
// an IMPORTED `fn main` mangles on its path. Mirrors
|
||||||
|
// cstage cgen.c mod_collect.
|
||||||
|
if (!syntax.streq(d.str, "main") || d.imported != 0) {
|
||||||
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
|
c.mods = m;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -45248,6 +45260,11 @@ fn collectmods(c: *cgen, file: *syntax.node) void = {
|
|||||||
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
|
||||||
// `MAX` two packages could clash on under sep-compile. No export
|
// `MAX` two packages could clash on under sep-compile. No export
|
||||||
// guard: every decl with a module mangles identically.
|
// guard: every decl with a module mangles identically.
|
||||||
|
// #85 (deferred): the `nmod.len > 0` gate below retains the
|
||||||
|
// bare-NON-fn skip — the sibling of #84 (a package-less let/def/
|
||||||
|
// type leaf colliding with an imported same-kind export) needs a
|
||||||
|
// different fix (modlookup is hint-LESS) and has no corpus repro;
|
||||||
|
// retained until then (rule-11).
|
||||||
if (d.kind == syntax.nkind.N_DEF) {
|
if (d.kind == syntax.nkind.N_DEF) {
|
||||||
if (d.nmod.len > 0) {
|
if (d.nmod.len > 0) {
|
||||||
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
|
||||||
@@ -45323,12 +45340,27 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
|
|||||||
first.len = 0;
|
first.len = 0;
|
||||||
for (m != nil) {
|
for (m != nil) {
|
||||||
if (syntax.streq(m.mname, name)) {
|
if (syntax.streq(m.mname, name)) {
|
||||||
if (hint.len > 0 && m.nmod.len > 0
|
if (m.nmod.len == 0) {
|
||||||
&& syntax.streq(m.nmod, hint)) {
|
// #84: a bare-module entry (now registered). A bare-ident
|
||||||
return m.nmod;
|
// ref (hint.len==0: the caller is itself in the bare/root
|
||||||
};
|
// module) resolves to it — bare wins over an imported
|
||||||
if (first.len == 0 && first.ptr == nil) {
|
// same-leaf fn, by construction, order-independent. A
|
||||||
first = m.nmod;
|
// non-empty hint named a module, so a bare decl is
|
||||||
|
// irrelevant: skip it, leaving the legacy first-imported-
|
||||||
|
// match untouched (byte-id-neutral for moduled callers).
|
||||||
|
if (hint.len == 0) {
|
||||||
|
let empty: str;
|
||||||
|
empty.ptr = nil;
|
||||||
|
empty.len = 0;
|
||||||
|
return empty;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (hint.len > 0 && syntax.streq(m.nmod, hint)) {
|
||||||
|
return m.nmod;
|
||||||
|
};
|
||||||
|
if (first.len == 0 && first.ptr == nil) {
|
||||||
|
first = m.nmod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
m = m.mnext;
|
m = m.mnext;
|
||||||
|
|||||||
222
test/wcc/989_barefn_collide_run.c
Normal file
222
test/wcc/989_barefn_collide_run.c
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* 989_barefn_collide_run — #84 cgen bare-module fn-leaf collision gate.
|
||||||
|
*
|
||||||
|
* A bare-module fn (module==NULL — a package-LESS `//ww:module-reset`
|
||||||
|
* primary, e.g. a user test file) whose leaf collides with an IMPORTED
|
||||||
|
* module's same-leaf fn was MIS-MANGLED to the imported qualified name:
|
||||||
|
* mod_collect skipped bare decls, so the bare fn never entered mod_map,
|
||||||
|
* and at emission mod_lookup_for_fn(leaf, hint=NULL) first-matched the
|
||||||
|
* imported entry → emitted `<mod>.<leaf>` instead of bare `<leaf>`. That
|
||||||
|
* produced a DUPLICATE symbol with the imported fn (w6l-tolerated, #31-
|
||||||
|
* class) and the bare fn became silently dead — a #40 residual / #263-
|
||||||
|
* class silent miscompile, gate-blind and symmetric cs==ww. Surfaced by
|
||||||
|
* #80's `ww test` coexist (lib/test exports `run`).
|
||||||
|
*
|
||||||
|
* Fixture (the minimal real repro): a dir-package `aa` exporting `run`,
|
||||||
|
* imported by a PACKAGE-LESS root that defines its OWN bare `fn run` and
|
||||||
|
* calls it from `main`. Package-less is REQUIRED: a `package main` root's
|
||||||
|
* decls mangle `main.<leaf>` (the package clause sets the module), so its
|
||||||
|
* `run` would be `main.run` — no collision with `aa.run`, vacuous. Only a
|
||||||
|
* package-less primary yields a truly BARE `run`, the #84 trigger.
|
||||||
|
*
|
||||||
|
* Asserts (combined `ww build`, BOTH driver stages; the fix lives in
|
||||||
|
* mod_collect/mod_lookup_for_fn which run in every mode, so combined is
|
||||||
|
* the minimal repro — `--sep` reproduces identically):
|
||||||
|
* 1. Build + run, BOTH stages → exit 9 (the USER's bare `run`, return 9),
|
||||||
|
* never aa.run (return 5).
|
||||||
|
* 2. cs==ww (rule 10): the combined `.s` is byte-identical between the
|
||||||
|
* two driver stages.
|
||||||
|
* 3. NON-VACUITY + SOUNDNESS (the #84-specific signal): the `.s` has
|
||||||
|
* EXACTLY ONE `TEXT run` (the user's, bare) AND EXACTLY ONE `TEXT
|
||||||
|
* aa.run` (the import) — i.e. DISTINCT symbols, no dup. Pre-fix this
|
||||||
|
* row fails: two `TEXT aa.run` (the user's `run` mis-mangled onto the
|
||||||
|
* import) and zero bare `TEXT run`. (Exit alone is not a reliable
|
||||||
|
* signal: pre-fix the dup happened to bind the user's copy under this
|
||||||
|
* link order and still exited 9 — the label count is the proof.)
|
||||||
|
*
|
||||||
|
* Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are
|
||||||
|
* `-o`-redirected to /tmp, phase-parallel-safe. Models 989_sepbuild_run.c.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.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 const char *
|
||||||
|
absbin(void)
|
||||||
|
{
|
||||||
|
const char *b = getenv("BIN");
|
||||||
|
if (!b) b = "out/bin";
|
||||||
|
if (b[0] == '/') return b;
|
||||||
|
static char buf[2048];
|
||||||
|
char cwd[1024];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
||||||
|
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long n = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
if (n < 0) { fclose(f); return -1; }
|
||||||
|
char *b = malloc((size_t)n + 1);
|
||||||
|
if (!b) { fclose(f); return -1; }
|
||||||
|
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||||
|
b[n] = '\0';
|
||||||
|
fclose(f);
|
||||||
|
*outbuf = b;
|
||||||
|
*outlen = (size_t)n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
files_eq(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
char *ba = NULL, *bb = NULL;
|
||||||
|
size_t na = 0, nb = 0;
|
||||||
|
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
||||||
|
free(ba); free(bb);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
||||||
|
free(ba); free(bb);
|
||||||
|
return eq ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
write_file(const char *path, const char *body)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
fputs(body, f);
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Count occurrences of a line beginning with `^TEXT <sym>,` in the .s. */
|
||||||
|
static int
|
||||||
|
count_text_label(const char *sfile, const char *sym)
|
||||||
|
{
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t n = 0;
|
||||||
|
if (slurp(sfile, &buf, &n) < 0) return -1;
|
||||||
|
char needle[128];
|
||||||
|
snprintf(needle, sizeof needle, "TEXT %s,", sym);
|
||||||
|
size_t nl = strlen(needle);
|
||||||
|
int count = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
size_t j = i;
|
||||||
|
while (j < n && buf[j] != '\n') j++;
|
||||||
|
if (j - i >= nl && strncmp(buf + i, needle, nl) == 0)
|
||||||
|
count++;
|
||||||
|
i = j + 1;
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *aa_src =
|
||||||
|
"package aa;\n"
|
||||||
|
"export fn run() i32 = { return 5; };\n";
|
||||||
|
|
||||||
|
/* PACKAGE-LESS root (no `package` clause) → its `fn run` is bare (the #84
|
||||||
|
* trigger); a `package main` root would mangle it `main.run` (vacuous). */
|
||||||
|
static const char *root_src =
|
||||||
|
"import aa;\n"
|
||||||
|
"fn run() i32 = { return 9; };\n"
|
||||||
|
"export fn main() i32 = { return run(); };\n";
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
const char *bin = absbin();
|
||||||
|
if (!bin) { fprintf(stderr, "84 FAIL: getcwd\n"); return 1; }
|
||||||
|
|
||||||
|
char td[64], cmd[8192];
|
||||||
|
int fail = 0;
|
||||||
|
snprintf(td, sizeof td, "/tmp/wwbare84_%d", getpid());
|
||||||
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||||
|
runwait(cmd);
|
||||||
|
mkdir(td, 0755);
|
||||||
|
/* All paths derive from `td` (a small fixed 64-byte buffer) so the
|
||||||
|
* snprintfs are provably non-truncating (warning-clean). */
|
||||||
|
char aadir[1024], aaww[1024], rootww[1024];
|
||||||
|
snprintf(aadir, sizeof aadir, "%s/aa", td);
|
||||||
|
mkdir(aadir, 0755);
|
||||||
|
snprintf(aaww, sizeof aaww, "%s/aa/aa.ww", td);
|
||||||
|
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
|
||||||
|
if (write_file(aaww, aa_src) || write_file(rootww, root_src)) {
|
||||||
|
fprintf(stderr, "84 FAIL: write fixture\n");
|
||||||
|
fail++;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct { const char *drv, *tag; char prog[1024], sfile[1024]; }
|
||||||
|
stg[] = { { "ww", "cs", {0}, {0} }, { "ww_ww", "ww", {0}, {0} } };
|
||||||
|
|
||||||
|
for (int s = 0; s < 2; s++) {
|
||||||
|
snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag);
|
||||||
|
snprintf(stg[s].sfile, sizeof stg[s].sfile, "%s/prog.%s.s", td, stg[s].tag);
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"timeout 240 %s/%s build -o %s -I %s %s >/dev/null 2>&1",
|
||||||
|
bin, stg[s].drv, stg[s].prog, td, rootww);
|
||||||
|
if (runwait(cmd) != 0) {
|
||||||
|
fprintf(stderr, "84 FAIL: %s build\n", stg[s].drv);
|
||||||
|
fail++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* (1) the user's bare `run` (9) must win over imported aa.run (5). */
|
||||||
|
int rc = runwait(stg[s].prog);
|
||||||
|
if (rc != 9) {
|
||||||
|
fprintf(stderr, "84 FAIL: %s prog exit=%d expected 9 "
|
||||||
|
"(bare user run, not aa.run=5)\n", stg[s].drv, rc);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
/* (3) distinct symbols, no dup: exactly one bare `run` + one
|
||||||
|
* `aa.run`. Pre-fix: two `aa.run` (user run mis-mangled) + zero
|
||||||
|
* bare `run`. */
|
||||||
|
int nrun = count_text_label(stg[s].sfile, "run");
|
||||||
|
int naa = count_text_label(stg[s].sfile, "aa.run");
|
||||||
|
if (nrun != 1 || naa != 1) {
|
||||||
|
fprintf(stderr, "84 FAIL: %s labels TEXT run=%d aa.run=%d "
|
||||||
|
"(want 1/1 — bare user run distinct from import, no dup)\n",
|
||||||
|
stg[s].drv, nrun, naa);
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) cs==ww (rule 10): the combined .s is byte-identical. */
|
||||||
|
if (files_eq(stg[0].sfile, stg[1].sfile) != 0) {
|
||||||
|
fprintf(stderr, "84 FAIL: cs .s != ww .s (rule 10)\n");
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||||
|
runwait(cmd);
|
||||||
|
if (fail) {
|
||||||
|
fprintf(stderr, "84: %d check(s) failed\n", fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("barefn_collide: package-less root `fn run` (bare) coexists with "
|
||||||
|
"imported aa.run — distinct labels, no dup, user run wins (exit 9), "
|
||||||
|
"cs==ww, both driver stages (#84)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user