fix(imports): diagnose namespace-alias dup + propagate buildImportFacts errors [stdlib A attempt-2]

Two defects from the Phase A attempt-1 review.

F1 — duplicate-name diagnostic missed NAMESPACE ALIASES (silent first-win).
`addNamespace` unconditionally put the alias into scope/own_decls, so a
same-module collision between an authored decl and a `dup :: #import "…"`
alias compiled clean in the fn-then-alias order (the scalar
ModuleRawDeclIndex silently first-won). Now `addNamespace` returns a bool
and refuses a same-module duplicate (mirroring addOwnDecl); the call site
surfaces it via the new `reportDuplicateName` (the import_decl node has no
declName, so the alias name is passed explicitly). The C-import namespace
site gets the same guard. Both orders now emit "duplicate top-level
declaration 'X'" and exit nonzero (alias-then-fn was already caught by
addOwnDecl seeing the alias in scope).

F2 — buildImportFacts errors were swallowed by `else |_| {}` in core.zig
(REJECTED-PATTERN catch-all leaving the borrowed store silently empty).
`resolveImports` returns !void, so the call is now a plain `try` and a
build failure propagates instead of producing a stale/empty store.

Tests: extend the dup-name regression with fn-vs-namespace-alias
collisions in both orders. No resolution behavior change (no lower.zig
edits; run_examples 471 byte-identical); m3te ios-sim builds via the
worktree binary.
This commit is contained in:
agra
2026-06-06 23:54:51 +03:00
parent b5ec121645
commit 5f06b6504f
3 changed files with 95 additions and 17 deletions

View File

@@ -357,6 +357,13 @@ pub const ResolvedModule = struct {
/// Add another module as a namespaced import. The alias `name` becomes
/// part of this module's own decls (so a flat-importer of this module
/// sees the alias one hop out — matching authored names).
///
/// Returns `false` (and adds nothing) when `name` already names a decl
/// authored in THIS module — symmetric to `addOwnDecl`, so a namespace
/// alias colliding with a prior same-module name is dropped rather than
/// shadowing it. The caller surfaces the drop via `reportDuplicateName`;
/// the reverse order (alias first, then a same-name authored decl) is
/// caught by `addOwnDecl` seeing the alias already in `scope`.
pub fn addNamespace(
self: *ResolvedModule,
allocator: std.mem.Allocator,
@@ -367,7 +374,8 @@ pub const ResolvedModule = struct {
other: ResolvedModule,
span: ast.Span,
is_raw: bool,
) !void {
) !bool {
if (self.scope.contains(name)) return false;
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = span,
@@ -391,6 +399,7 @@ pub const ResolvedModule = struct {
try seen_list.put(name, {});
try list.append(allocator, ns_node);
try own_list.append(allocator, ns_node);
return true;
}
pub fn finalize(
@@ -570,15 +579,24 @@ pub fn buildImportFacts(
return .{ .decls = decls, .ns_edges = ns_edges };
}
/// Surface a same-module duplicate top-level declaration as a hard error.
/// `addOwnDecl` returns `false` when the name is already in this module's scope
/// and drops the second author; without this the drop is silent, and the scalar
/// `ModuleRawDeclIndex` would lose an authored name with no diagnostic.
fn reportDuplicateDecl(diagnostics: ?*errors.DiagnosticList, added: bool, decl: *const Node) void {
/// Surface a same-module duplicate top-level declaration as a hard error at an
/// explicit name + span. `addOwnDecl` / `addNamespace` return `false` when the
/// name is already in this module's scope and drop the second author; without
/// this the drop is silent, and the scalar `ModuleRawDeclIndex` would lose an
/// authored name with no diagnostic.
fn reportDuplicateName(diagnostics: ?*errors.DiagnosticList, added: bool, name: []const u8, span: ast.Span) void {
if (added) return;
const diags = diagnostics orelse return;
diags.addFmt(.err, span, "duplicate top-level declaration '{s}'", .{name});
}
/// `reportDuplicateName` keyed off a node whose `declName()` carries the name
/// (the regular authored-decl sites; an `import_decl` has no `declName`, so a
/// namespace alias must use `reportDuplicateName` with the alias directly).
fn reportDuplicateDecl(diagnostics: ?*errors.DiagnosticList, added: bool, decl: *const Node) void {
if (added) return;
const name = decl.data.declName() orelse return;
diags.addFmt(.err, decl.span, "duplicate top-level declaration '{s}'", .{name});
reportDuplicateName(diagnostics, added, name, decl.span);
}
pub fn resolveImports(
@@ -706,10 +724,14 @@ pub fn resolveImports(
} },
};
ns_node.source_file = file_path;
try mod.scope.put(ns_name, {});
try seen_in_list.put(ns_name, {});
try decl_list.append(allocator, ns_node);
try own_decl_list.append(allocator, ns_node);
if (mod.scope.contains(ns_name)) {
reportDuplicateName(diagnostics, false, ns_name, decl.span);
} else {
try mod.scope.put(ns_name, {});
try seen_in_list.put(ns_name, {});
try decl_list.append(allocator, ns_node);
try own_decl_list.append(allocator, ns_node);
}
} else {
// Flat: add fn_decls directly + keep c_import_decl
for (result.fn_decls) |fd| {
@@ -796,7 +818,8 @@ pub fn resolveImports(
};
if (imp.name) |ns_name| {
try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span, imp.is_raw);
const added = try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span, imp.is_raw);
reportDuplicateName(diagnostics, added, ns_name, decl.span);
} else {
try mod.mergeFlat(allocator, &decl_list, &seen_in_list, &seen_nodes, imported_mod);
}