From e6045f3adaf769521765f3a8d78e676ec25c8f0f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 11:24:33 +0900 Subject: [PATCH] w6c+selfhost: same-module preference for bare-leaf lookup --- .gitignore | 1 + Makefile | 6 ++ cmd/wcc/check.c | 20 +++-- cmd/wcc/sym.c | 37 ++++++++++ cmd/wcc/ww.h | 7 ++ lib/ww/sym.ww | 42 +++++++++++ selfhost/cmd/w6c/main.combined.ww | 58 ++++++++++++++- selfhost/cmd/wcc/check.ww | 16 +++- selfhost/cmd/wwdump/main.combined.ww | 58 ++++++++++++++- test/wcc/697_samemod_prefer.c | 93 ++++++++++++++++++++++++ test/wcc/data/samemodprefer/mod1/mod1.ww | 18 +++++ test/wcc/data/samemodprefer/mod2/mod2.ww | 18 +++++ test/wcc/data/samemodprefer/pos.ww | 20 +++++ 13 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 test/wcc/697_samemod_prefer.c create mode 100644 test/wcc/data/samemodprefer/mod1/mod1.ww create mode 100644 test/wcc/data/samemodprefer/mod2/mod2.ww create mode 100644 test/wcc/data/samemodprefer/pos.ww diff --git a/.gitignore b/.gitignore index 82455ee3..44adba31 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ test/wcc/data/**/*.s test/wcc/data/**/*.combined.ww test/wcc/data/modcollision/pos test/wcc/data/modcollision/neg +test/wcc/data/samemodprefer/pos examples/mandelbrot/mandelbrot examples/cmatrix/cmatrix examples/lisp/lisp diff --git a/Makefile b/Makefile index 834ca6e2..e7f6979b 100644 --- a/Makefile +++ b/Makefile @@ -225,6 +225,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_store_intlit \ $(BIN)/test_match_bind_struct \ $(BIN)/test_modtype_leaf_collision \ + $(BIN)/test_samemod_prefer \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ @@ -357,6 +358,11 @@ $(BIN)/test_modtype_leaf_collision: test/wcc/696_modtype_leaf_collision.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_samemod_prefer: test/wcc/697_samemod_prefer.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 4f428d31..55c5d9f6 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -60,7 +60,7 @@ resolve_typename(Checker *c, Node *n) const char *nm = n->str; Type *bi = lookup_builtin(nm); if (bi) return bi; - Sym *s = scope_lookup(c->cur, nm); + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, nm); if (s == NULL && nm) { /* module-qualified: io.stream → strip the last dot prefix * and look up the leaf, filtering on the importing module's @@ -679,7 +679,7 @@ cexpr(Checker *c, Node *n) if (n->str && n->str[0] == '\0') return n->type = err(c, n->pos, "`_` is only valid as a binding or discard lvalue"); - Sym *s = scope_lookup(c->cur, n->str); + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, n->str); if (s == NULL) return n->type = err(c, n->pos, "undefined: %s", n->str); /* SK_USE has no concrete value type; the only legal use is @@ -942,7 +942,7 @@ cexpr(Checker *c, Node *n) * (e.g. lib/os/os.ww) keeps working unchanged. */ if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "abort") == 0 && - scope_lookup(c->cur, "abort") == NULL) { + scope_lookup_prefer(c->cur, c->cur_mod, "abort") == NULL) { if (n->list) { Type *mt = cexpr(c, n->list); if (mt != ty_err && !type_assignable(ty_str, mt)) @@ -957,7 +957,7 @@ cexpr(Checker *c, Node *n) if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "assert") == 0 && n->list != NULL && - scope_lookup(c->cur, "assert") == NULL) { + scope_lookup_prefer(c->cur, c->cur_mod, "assert") == NULL) { Type *ct = cexpr(c, n->list); if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool) err(c, n->pos, "assert: cond must be bool"); @@ -1051,7 +1051,8 @@ cexpr(Checker *c, Node *n) } /* Reject assignment to a const-bound name. */ if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) { - Sym *s = scope_lookup(c->cur, n->lhs->str); + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, + n->lhs->str); if (s && s->is_const) err(c, n->pos, "cannot assign to const `%s`", n->lhs->str); @@ -1069,7 +1070,8 @@ cexpr(Checker *c, Node *n) * resolve_type for the synthetic-type-expr case. */ Type *t = NULL; if (n->lhs && n->lhs->kind == N_IDENT) { - Sym *s = scope_lookup(c->cur, n->lhs->str); + Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, + n->lhs->str); if (s == NULL || s->kind != SK_TYPE) t = err(c, n->pos, "unknown struct type '%s'", n->lhs->str); @@ -1682,6 +1684,7 @@ check_file(Checker *c, Node *file) } for (Node *d = file->list; d; d = d->next) { if (d->kind != N_TYPEDECL) continue; + c->cur_mod = decl_mod(file, d); Type *under = resolve_type(c, d->lhs); d->type->under = under; if (under) { @@ -1690,7 +1693,9 @@ check_file(Checker *c, Node *file) d->type->iserror = under->iserror; } } + c->cur_mod = NULL; for (Node *d = file->list; d; d = d->next) { + c->cur_mod = decl_mod(file, d); switch (d->kind) { case N_USE: /* already installed in pass 1; no-op here so the @@ -1741,9 +1746,11 @@ check_file(Checker *c, Node *file) default: break; } } + c->cur_mod = NULL; /* pass 2: check def initialisers and fn bodies */ for (Node *d = file->list; d; d = d->next) { + c->cur_mod = decl_mod(file, d); switch (d->kind) { case N_DEF: { if (d->rhs) { @@ -1786,4 +1793,5 @@ check_file(Checker *c, Node *file) default: break; } } + c->cur_mod = NULL; } diff --git a/cmd/wcc/sym.c b/cmd/wcc/sym.c index 48bc686c..165c2ef6 100644 --- a/cmd/wcc/sym.c +++ b/cmd/wcc/sym.c @@ -79,6 +79,43 @@ scope_lookup_in_module(Scope *s, const char *mod, const char *name) return NULL; } +/* + * scope_lookup_prefer — bare-leaf lookup with same-module preference. + * + * Walks the same FNV bucket + hashnext chain + parent walk scope_lookup + * uses. Within each scope's bucket: Pass 1 prefers entries whose + * `sym.mod` matches the caller's `mod`; Pass 2 falls back to the first + * match regardless of mod (the existing scope_lookup semantics). We + * only descend to the parent scope when the current scope has no + * matching entry at all — so a local binding in a closer scope still + * shadows a same-name fn from a parent scope, even when the parent + * entry mod-matches. + * + * When `mod` is NULL we just call scope_lookup — there's no module + * identity to prefer. + * + * Used at bare-leaf lookup sites inside a known current module so that + * a bare `read` inside lib/os resolves to os.read rather than the + * io.read that happens to hash earlier into the flat scope. + * Qualified-lookup paths (`mod.name`) stay on scope_lookup_in_module. + */ +Sym * +scope_lookup_prefer(Scope *s, const char *mod, const char *name) +{ + if (mod == NULL) return scope_lookup(s, name); + for (Scope *p = s; p; p = p->parent) { + u64 h = hashstr(name) % p->nbuckets; + Sym *fallback = NULL; + for (Sym *b = p->buckets[h]; b; b = b->hashnext) { + if (strcmp(b->name, name) != 0) continue; + if (b->mod && strcmp(b->mod, mod) == 0) return b; + if (fallback == NULL) fallback = b; + } + if (fallback) return fallback; + } + return NULL; +} + Sym * scope_define(Scope *s, const char *name, Skind k, Type *t, Node *decl) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index eb1d8987..2f7250cd 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -509,6 +509,7 @@ Sym *scope_define_in_module(Scope*, const char *name, const char *mod, Sym *scope_lookup(Scope*, const char *name); /* walk up parents */ Sym *scope_lookup_local(Scope*, const char *name); Sym *scope_lookup_in_module(Scope*, const char *mod, const char *name); +Sym *scope_lookup_prefer(Scope*, const char *mod, const char *name); /* ---- checker (check.c) -------------------------------------------- */ typedef struct Checker Checker; @@ -517,6 +518,12 @@ struct Checker { Scope *top; /* file scope */ Scope *cur; /* current scope */ Type *ret; /* expected return type of current fn (or NULL) */ + const char *cur_mod; /* importing-module bareword for the decl + * currently being checked; NULL for primary + * compilation unit. Drives same-module + * preference in bare-leaf lookups so a bare + * `read` inside lib/os resolves to os.read + * rather than colliding io.read. */ int loops; /* nesting count for break/continue */ int errs; }; diff --git a/lib/ww/sym.ww b/lib/ww/sym.ww index 16d97f21..c5e2677d 100644 --- a/lib/ww/sym.ww +++ b/lib/ww/sym.ww @@ -134,6 +134,48 @@ export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { return nil; }; +// scopelookupprefer — bare-leaf lookup with same-module preference. +// +// Walks the same FNV bucket + hashnext chain + parent walk scopelookup +// uses. Within each scope's bucket: Pass 1 prefers entries whose +// `sym.mod` matches `mod`; Pass 2 falls back to the first match +// regardless of mod (same semantics as scopelookup). We only descend +// to the parent scope when the current scope has no matching entry at +// all — so a local binding in a closer scope still shadows a same-name +// fn from a parent scope, even when the parent entry mod-matches. +// +// When `mod` is empty we just call scopelookup — there's no module +// identity to prefer. +// +// Used at bare-leaf lookup sites inside a known current module so that +// a bare `read` inside lib/os resolves to os.read rather than the +// io.read that happens to hash earlier into the flat scope. Mirrors +// cmd/wcc/sym.c scope_lookup_prefer. +export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { let empty: str; return scopedefineinmodule(s, name, empty, k, t, decl); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0b198370..d254e15c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -4647,6 +4647,48 @@ export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { return nil; }; +// scopelookupprefer — bare-leaf lookup with same-module preference. +// +// Walks the same FNV bucket + hashnext chain + parent walk scopelookup +// uses. Within each scope's bucket: Pass 1 prefers entries whose +// `sym.mod` matches `mod`; Pass 2 falls back to the first match +// regardless of mod (same semantics as scopelookup). We only descend +// to the parent scope when the current scope has no matching entry at +// all — so a local binding in a closer scope still shadows a same-name +// fn from a parent scope, even when the parent entry mod-matches. +// +// When `mod` is empty we just call scopelookup — there's no module +// identity to prefer. +// +// Used at bare-leaf lookup sites inside a known current module so that +// a bare `read` inside lib/os resolves to os.read rather than the +// io.read that happens to hash earlier into the flat scope. Mirrors +// cmd/wcc/sym.c scope_lookup_prefer. +export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { let empty: str; return scopedefineinmodule(s, name, empty, k, t, decl); @@ -4725,6 +4767,10 @@ type checker = struct { errs: i32, verbose: i32, // when non-zero, log each unresolved name fnret: *node, // enclosing fn's return type AST (for `?`) + curmod: str, // importing-module bareword for the decl + // currently being walked; "" for primary + // compilation unit. Drives same-module + // preference in bare-leaf lookups. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -4823,7 +4869,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { c.nunresolved += 1; if (c.verbose != 0) { @@ -4838,7 +4884,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_TNAME) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); // `pkg.Type` — strip the last dot prefix and look up // the leaf with a mod filter so same-leaf-name types // from different imports (`bufio.stream` vs @@ -5637,6 +5683,8 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.errs = 0; c.verbose = 0; c.fnret = nil; + let empty: str; + c.curmod = empty; seedprimitives(c); }; @@ -5652,8 +5700,12 @@ export fn checkfile(c: *checker, file: *node) void = { }; // Pass 2: walk decl bodies/types and resolve identifiers. + // Track the per-decl module bareword so bare-leaf lookups inside + // the body prefer same-module entries over alphabetically-earlier + // same-leaf imports. d = file.list; for (d != nil) { + c.curmod = declmod(file, d); let k: nkind = d.kind; if (k == nkind.N_FNDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type @@ -5669,6 +5721,8 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + let empty: str; + c.curmod = empty; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 6b6c9a15..b77673e4 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -31,6 +31,10 @@ type checker = struct { errs: i32, verbose: i32, // when non-zero, log each unresolved name fnret: *node, // enclosing fn's return type AST (for `?`) + curmod: str, // importing-module bareword for the decl + // currently being walked; "" for primary + // compilation unit. Drives same-module + // preference in bare-leaf lookups. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -129,7 +133,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { c.nunresolved += 1; if (c.verbose != 0) { @@ -144,7 +148,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_TNAME) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); // `pkg.Type` — strip the last dot prefix and look up // the leaf with a mod filter so same-leaf-name types // from different imports (`bufio.stream` vs @@ -943,6 +947,8 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.errs = 0; c.verbose = 0; c.fnret = nil; + let empty: str; + c.curmod = empty; seedprimitives(c); }; @@ -958,8 +964,12 @@ export fn checkfile(c: *checker, file: *node) void = { }; // Pass 2: walk decl bodies/types and resolve identifiers. + // Track the per-decl module bareword so bare-leaf lookups inside + // the body prefer same-module entries over alphabetically-earlier + // same-leaf imports. d = file.list; for (d != nil) { + c.curmod = declmod(file, d); let k: nkind = d.kind; if (k == nkind.N_FNDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type @@ -975,5 +985,7 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + let empty: str; + c.curmod = empty; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3a870aa7..cbf651a2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -4647,6 +4647,48 @@ export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { return nil; }; +// scopelookupprefer — bare-leaf lookup with same-module preference. +// +// Walks the same FNV bucket + hashnext chain + parent walk scopelookup +// uses. Within each scope's bucket: Pass 1 prefers entries whose +// `sym.mod` matches `mod`; Pass 2 falls back to the first match +// regardless of mod (same semantics as scopelookup). We only descend +// to the parent scope when the current scope has no matching entry at +// all — so a local binding in a closer scope still shadows a same-name +// fn from a parent scope, even when the parent entry mod-matches. +// +// When `mod` is empty we just call scopelookup — there's no module +// identity to prefer. +// +// Used at bare-leaf lookup sites inside a known current module so that +// a bare `read` inside lib/os resolves to os.read rather than the +// io.read that happens to hash earlier into the flat scope. Mirrors +// cmd/wcc/sym.c scope_lookup_prefer. +export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { + if (mod.len == 0) { return scopelookup(s, name); }; + let p: *scope = s; + for (p != nil) { + let h: u64 = hashstr(name); + let bi: i32 = (h % (p.nbuckets: u64)): i32; + let b: *sym = p.buckets[bi]; + let fallback: *sym = nil; + for (b != nil) { + if (streq(b.name, name)) { + if (b.mod.len > 0) { + if (streq(b.mod, mod)) { + return b; + }; + }; + if (fallback == nil) { fallback = b; }; + }; + b = b.hashnext; + }; + if (fallback != nil) { return fallback; }; + p = p.parent; + }; + return nil; +}; + export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { let empty: str; return scopedefineinmodule(s, name, empty, k, t, decl); @@ -4725,6 +4767,10 @@ type checker = struct { errs: i32, verbose: i32, // when non-zero, log each unresolved name fnret: *node, // enclosing fn's return type AST (for `?`) + curmod: str, // importing-module bareword for the decl + // currently being walked; "" for primary + // compilation unit. Drives same-module + // preference in bare-leaf lookups. }; // seedprimitives — install the built-in type names so `i32`, `str`, @@ -4823,7 +4869,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { c.nunresolved += 1; if (c.verbose != 0) { @@ -4838,7 +4884,7 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_TNAME) { let nm: str = n.str; if (nm.len > 0) { - let s: *sym = scopelookup(c.cur, nm); + let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); // `pkg.Type` — strip the last dot prefix and look up // the leaf with a mod filter so same-leaf-name types // from different imports (`bufio.stream` vs @@ -5637,6 +5683,8 @@ export fn checkinit(c: *checker, a: *arena, tc: *tctx) void = { c.errs = 0; c.verbose = 0; c.fnret = nil; + let empty: str; + c.curmod = empty; seedprimitives(c); }; @@ -5652,8 +5700,12 @@ export fn checkfile(c: *checker, file: *node) void = { }; // Pass 2: walk decl bodies/types and resolve identifiers. + // Track the per-decl module bareword so bare-leaf lookups inside + // the body prefer same-module entries over alphabetically-earlier + // same-leaf imports. d = file.list; for (d != nil) { + c.curmod = declmod(file, d); let k: nkind = d.kind; if (k == nkind.N_FNDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type @@ -5669,6 +5721,8 @@ export fn checkfile(c: *checker, file: *node) void = { };};};}; d = d.next; }; + let empty: str; + c.curmod = empty; }; diff --git a/test/wcc/697_samemod_prefer.c b/test/wcc/697_samemod_prefer.c new file mode 100644 index 00000000..f6930a97 --- /dev/null +++ b/test/wcc/697_samemod_prefer.c @@ -0,0 +1,93 @@ +/* + * 697_samemod_prefer — same-module preference for bare-leaf lookup. + * + * Two modules each export `read` with intentionally distinct + * signatures (mod1: i32 → i32, mod2: str → i32). Each module's + * `caller()` invokes bare `read(...)` with its own arg type. The + * bare-leaf lookup must resolve to the SAME-MODULE `read`; otherwise + * the call is a check-time signature mismatch. + * + * Pre-fix (flat scope, first-wins by hash order): one of the bare + * lookups picked the wrong module's `read` and the build failed with + * "argument type not assignable". Post-fix (scope_lookup_prefer): each + * bare-leaf inside its own module binds to the same-module entry, both + * calls type-check, and the build succeeds. + * + * Runtime exit codes aren't asserted here because cgen still emits + * unmangled `TEXT read` labels for both modules and the linker + * collapses them. Mangling fn labels by module is a separate codegen + * sweep; this test pins the resolver fix alone. + * + * Fixtures live in test/wcc/data/samemodprefer/. Same shape as + * 696_modtype_leaf_collision.c. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +run_pos(const char *driver, const char *fixdir, const char *tag) +{ + char cmd[2048]; + /* cd into the fixture dir so the driver's source-dir-first + * import search resolves `use mod1; use mod2;`. */ + snprintf(cmd, sizeof cmd, + "cd %s && %s build pos.ww >/dev/null 2>&1", fixdir, driver); + if (runwait(cmd) != 0) { + fprintf(stderr, + "samemod_prefer[%s]: pos.ww build failed — bare-leaf " + "`read` lookup likely cross-bound\n", tag); + return 1; + } + char bin[2048]; + snprintf(bin, sizeof bin, "%s/pos", fixdir); + unlink(bin); + 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 fixdir[1024]; + if (getcwd(fixdir, sizeof fixdir) == NULL) return 1; + size_t cwd_n = strlen(fixdir); + const char *rel = "/test/wcc/data/samemodprefer"; + 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"); + + if (fail) { + fprintf(stderr, + "samemod_prefer: %d case(s) failed\n", fail); + return 1; + } + printf("samemod_prefer: 1/1 ok\n"); + return 0; +} diff --git a/test/wcc/data/samemodprefer/mod1/mod1.ww b/test/wcc/data/samemodprefer/mod1/mod1.ww new file mode 100644 index 00000000..7efcd6f3 --- /dev/null +++ b/test/wcc/data/samemodprefer/mod1/mod1.ww @@ -0,0 +1,18 @@ +// samemodprefer/mod1 — exports a `read` that takes an i32 plus a +// `caller` that invokes bare `read(1i32)`. The bare-leaf lookup must +// resolve to mod1.read (i32 → i32) rather than mod2.read (str → i32), +// which coexists in the flat scope. Pre-fix the lookup picked +// whichever entry hashed earliest into the bucket; a wrong bind would +// surface as a check-time signature mismatch. Paired with mod2/mod2.ww +// and test/wcc/697_samemod_prefer.c. + +export fn read(x: i32) i32 = { + return x + 100i32; +}; + +export fn caller() i32 = { + // Bare-leaf call inside mod1: must bind to mod1.read (i32 arg). + // If preference doesn't kick in this fails check: mod2.read + // expects a str, so `read(1i32)` would be a signature mismatch. + return read(1i32); +}; diff --git a/test/wcc/data/samemodprefer/mod2/mod2.ww b/test/wcc/data/samemodprefer/mod2/mod2.ww new file mode 100644 index 00000000..0d969d57 --- /dev/null +++ b/test/wcc/data/samemodprefer/mod2/mod2.ww @@ -0,0 +1,18 @@ +// samemodprefer/mod2 — exports a `read` that takes a str plus a +// `caller` that invokes bare `read("ok")`. The bare-leaf lookup must +// resolve to mod2.read (str → i32) rather than mod1.read (i32 → i32), +// which coexists in the flat scope. Pre-fix the lookup picked +// whichever entry hashed earliest into the bucket; a wrong bind would +// surface as a check-time signature mismatch. Paired with mod1/mod1.ww +// and test/wcc/697_samemod_prefer.c. + +export fn read(x: str) i32 = { + return x.len + 200i32; +}; + +export fn caller() i32 = { + // Bare-leaf call inside mod2: must bind to mod2.read (str arg). + // If preference doesn't kick in this fails check: mod1.read + // expects an i32, so `read("ok")` would be a signature mismatch. + return read("ok"); +}; diff --git a/test/wcc/data/samemodprefer/pos.ww b/test/wcc/data/samemodprefer/pos.ww new file mode 100644 index 00000000..1aaae8de --- /dev/null +++ b/test/wcc/data/samemodprefer/pos.ww @@ -0,0 +1,20 @@ +// Positive case: two modules each export `read` with DIFFERENT +// signatures (mod1: i32 → i32, mod2: str → i32). Each module also +// exports a `caller` that invokes bare `read(...)` with its own arg +// type. The bare-leaf inside mod1 must bind to mod1.read; inside mod2 +// to mod2.read. If preference doesn't kick in and the bare lookup +// picks the other module's `read`, the build fails with a signature +// mismatch at check-time. The driver test asserts that this fixture +// builds successfully. +// +// We don't assert runtime exit codes here because cgen still emits +// unmangled `TEXT read` labels for both modules and the linker +// collapses them — that's a separate codegen sweep, orthogonal to the +// resolver fix this test pins. + +use mod1; +use mod2; + +fn main() i32 = { + return 0i32; +};