From 2025bb361b4c138c0a3eee4c86e34c61dc0deae7 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 05:36:22 +0300 Subject: [PATCH] =?UTF-8?q?issues:=20file=200114=20=E2=80=94=20namespace?= =?UTF-8?q?=20aliases=20leak=20transitively,=20collide=20first-wins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while probing the alias-carry design for the stdlib restructure (plan in current/PLAN-STDLIB.md): qualified members register globally with no per-importer gate, so an alias is usable any number of flat hops away, and same-name registrations silently first-win. The carry rule's one-level + ambiguity semantics fix both; repro and fix shape in the issue. --- ...4-namespace-alias-transitive-first-wins.md | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 issues/0114-namespace-alias-transitive-first-wins.md diff --git a/issues/0114-namespace-alias-transitive-first-wins.md b/issues/0114-namespace-alias-transitive-first-wins.md new file mode 100644 index 0000000..ae11c8b --- /dev/null +++ b/issues/0114-namespace-alias-transitive-first-wins.md @@ -0,0 +1,61 @@ +# 0114 — namespace aliases leak transitively and collide first-wins, silently + +**Symptom.** A namespace alias (`t :: #import "target.sx";`) declared in module +B is usable from ANY module whose import closure reaches B — at any depth, flat +or not — and when two modules register the same qualified name (`t.helper`), +the first registration silently wins (`registerQualifiedFn`: +`if contains return`). Expected (the approved carry design, session 72f): an +alias is visible one level deep — in the declaring module and in its DIRECT +flat importers — with own-wins / ambiguity-diagnostic collision semantics, +mirroring ordinary declarations. + +This is the alias-side sibling of issue 0106 (bare-name over-permissiveness), +plus a REJECTED-PATTERNS silent first-wins. + +## Reproduction + +```sx +// target.sx +helper :: () -> s64 { 7 } +``` +```sx +// facade.sx +t :: #import "target.sx"; +``` +```sx +// facade2.sx +#import "facade.sx"; +``` +```sx +// main.sx +#import "modules/std.sx"; +#import "facade2.sx"; // TWO flat hops from the alias declaration +main :: () { print("{}\n", t.helper()); } +``` + +- **Observed**: prints `7` — the alias rides two flat hops. +- **Expected**: `'t' is not visible` (one-level carry; `facade2.sx` would need + to re-alias or flat-import `facade.sx`'s surface deliberately). + +Collision face: two modules each declaring `t :: #import` of DIFFERENT +targets, both flat-imported by main — first-lowered wins silently. + +## Suspected area / fix shape + +Qualified members are registered GLOBALLY (`fn_ast_map["t.helper"]`, +`registerQualifiedFn` in src/ir/lower/decl.zig ~1912) with no per-importer +gate; `lowerCall`'s namespace path (src/ir/lower/call.zig ~687) and the +comptime field-access path consult only that global map. Meanwhile +`nominal.zig`'s `qualifiedStructTemplate`/`qualifiedMemberMissing` use STRICT +per-file `namespace_edges` — so carried aliases are inconsistently +over-visible for plain/comptime fns and under-visible for generic structs +(and `alias.Type.method` heads — see PLAN-STDLIB). + +Fix shape: one carry-aware resolver on Lowering — +`resolveNamespaceAlias(alias) → {own | carried(target) | ambiguous | none}` +walking own `namespace_edges[from]` first, then the DIRECT +`flat_import_graph[from]` targets' edges (one level; ≥2 distinct targets → +diagnostic). Route every `ns.member` consumer through it: the global +qualified-name paths gain the gate, the strict nominal/type paths gain the +carry. See `current/PLAN-STDLIB.md` for the full design and the std.sx +restructure that depends on it.