refactor(imports): retain dup same-name fn authors + build identity indexes [0102a]

First of four fix-0102 sub-steps. Purely additive: retains data that the
flat/directory merge currently first-wins-drops and builds two identity
indexes for later bare-name disambiguation (fix-0102c). No resolution
change — the existing first-wins bare path still wins; suite unchanged.

- mergeFlat + directory merge: stop dropping a same-name FUNCTION authored
  by a different module/file. Non-function decls keep first-wins dedup; node
  identity dedup is untouched.
- flat_import_graph: a flat-only subset of import_graph, recording an edge
  only for a bare `#import` (imp.name == null), never a namespaced
  `ns :: #import`. Threaded through resolveImports/resolveDirectoryImport
  and into ProgramIndex.
- module_fns (path -> name -> *const FnDecl): per-module authored-function
  index mirroring module_scopes, built in core.zig from the main module +
  cache. Same-name cross-module authors stay distinct under their own paths.
- imports.test.zig: asserts both a.sx/b.sx greet authors are retained in
  module_fns and in the global flat list, and that flat_import_graph
  excludes the namespaced edge while import_graph includes it.

Gate (this worktree): zig build, zig build test (398/398),
bash tests/run_examples.sh (457 passed) all green.
This commit is contained in:
agra
2026-06-06 11:27:11 +03:00
parent 792ed55068
commit ff9cb50079
5 changed files with 245 additions and 11 deletions

View File

@@ -5,6 +5,32 @@ const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const Node = ast.Node;
/// True iff `decl` authors a top-level FUNCTION — either a bare `fn_decl`
/// (`f :: (…) -> T { … }`) or a `const_decl` whose value is a function
/// (`f :: (…) { … }` parsed as a const-bound fn). Used by the flat / directory
/// merge to retain a same-name function authored by a DIFFERENT module instead
/// of first-wins dropping it (fix-0102a): a flat importer that pulls two modules
/// each authoring `f` needs BOTH decls reachable downstream. Non-function decls
/// keep first-wins dedup.
fn declAuthorsFn(decl: *const Node) bool {
return switch (decl.data) {
.fn_decl => true,
.const_decl => |cd| cd.value.data == .fn_decl,
else => false,
};
}
/// The `*const ast.FnDecl` a function-authoring decl carries, or null when the
/// decl is not a function (mirrors `declAuthorsFn`). Drives the per-module
/// `module_fns` identity index (fix-0102a).
fn fnDeclOf(decl: *const Node) ?*const ast.FnDecl {
return switch (decl.data) {
.fn_decl => &decl.data.fn_decl,
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
else => null,
};
}
/// Comptime evaluation context for the inline-if hoisting pass below.
/// Mirrors the values `injectComptimeConstants` will later push into the
/// lowering's `comptime_constants` map (OS / ARCH / POINTER_SIZE), but
@@ -334,8 +360,13 @@ pub const ResolvedModule = struct {
for (other.decls) |decl| {
if (seen_nodes.contains(decl)) continue;
if (decl.data.declName()) |name| {
if (seen_list.contains(name)) continue;
try seen_list.put(name, {});
if (seen_list.contains(name)) {
// First-wins dedup for non-functions; retain a same-name
// FUNCTION authored by a different module (fix-0102a).
if (!declAuthorsFn(decl)) continue;
} else {
try seen_list.put(name, {});
}
}
try seen_nodes.put(decl, {});
try list.append(allocator, decl);
@@ -392,6 +423,43 @@ pub const ResolvedModule = struct {
/// Module cache: maps resolved file paths to their ResolvedModules.
pub const ModuleCache = std.StringHashMap(ResolvedModule);
/// Per-module function identity index: function NAME → the `*const FnDecl` that
/// module AUTHORS. Mirrors a single module's slice of `module_scopes`.
pub const FnIndex = std.StringHashMap(*const ast.FnDecl);
/// `path → name → *const FnDecl`, mirroring `module_scopes`. One entry per
/// resolved module keyed by its path (a directory's combined module keyed by
/// `dir_path`); each entry indexes only what that module AUTHORS. Two modules
/// each authoring `f` are retained under their own paths — the identity index
/// fix-0102c's bare-name disambiguation consults to bind a flat call to the
/// right author.
pub const ModuleFns = std.StringHashMap(FnIndex);
/// Index a single module's authored functions (`own_decls`) into `out[path]`.
/// First-wins WITHIN a module mirrors the scan pass; cross-module same-name
/// authors live under their own `path` keys.
fn indexModuleFns(allocator: std.mem.Allocator, out: *ModuleFns, path: []const u8, own_decls: []const *Node) !void {
const gop = try out.getOrPut(path);
if (!gop.found_existing) gop.value_ptr.* = FnIndex.init(allocator);
for (own_decls) |decl| {
const fd = fnDeclOf(decl) orelse continue;
const name = decl.data.declName() orelse continue;
if (gop.value_ptr.contains(name)) continue;
try gop.value_ptr.put(name, fd);
}
}
/// Build the per-module function index from a resolved program: the main module
/// (keyed by `main_path`) plus every cached module (keyed by its own path).
/// Mirrors how `core.zig` fills `module_scopes` from `mod.scope` + the cache.
pub fn buildModuleFns(allocator: std.mem.Allocator, main_path: []const u8, main_mod: ResolvedModule, cache: *const ModuleCache, out: *ModuleFns) !void {
try indexModuleFns(allocator, out, main_path, main_mod.own_decls);
var it = cache.iterator();
while (it.next()) |entry| {
try indexModuleFns(allocator, out, entry.key_ptr.*, entry.value_ptr.own_decls);
}
}
pub fn resolveImports(
allocator: std.mem.Allocator,
io: std.Io,
@@ -404,6 +472,7 @@ pub fn resolveImports(
diagnostics: ?*errors.DiagnosticList,
stdlib_paths: []const []const u8,
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
comptime_ctx: ComptimeContext,
) !ResolvedModule {
// Record this file's edge set so `param_impl_map` lookups can filter
@@ -414,6 +483,15 @@ pub fn resolveImports(
try g.put(file_path, std.StringHashMap(void).init(allocator));
}
}
// FLAT-only edge set: identical to `import_graph` but records ONLY bare
// `#import "…"` edges (`imp.name == null`), never a namespaced
// `ns :: #import "…"`. fix-0102c's bare-name disambiguation walks this to
// decide which same-name authors a flat importer can actually reach.
if (flat_import_graph) |g| {
if (!g.contains(file_path)) {
try g.put(file_path, std.StringHashMap(void).init(allocator));
}
}
var mod = ResolvedModule{
.path = file_path,
.decls = &.{},
@@ -536,6 +614,17 @@ pub fn resolveImports(
set.put(resolved_path, {}) catch {};
}
}
// The same edge, FLAT-only: recorded only for a bare `#import`
// (`imp.name == null`), excluding a namespaced `ns :: #import`. Covers
// both a flat file import and a flat directory import (`resolved_path`
// is the directory in the latter case).
if (imp.name == null) {
if (flat_import_graph) |g| {
if (g.getPtr(file_path)) |set| {
set.put(resolved_path, {}) catch {};
}
}
}
// Circular import check — only along the current chain
if (chain.contains(resolved_path)) continue;
@@ -563,7 +652,7 @@ pub fn resolveImports(
// Push onto chain before recursing, pop after
try chain.put(resolved_path, {});
const imp_dir = dirName(resolved_path);
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, flat_import_graph, comptime_ctx);
_ = chain.remove(resolved_path);
// Cache
@@ -571,7 +660,7 @@ pub fn resolveImports(
break :blk result;
} else |_| {
// File read failed — try as directory import
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, comptime_ctx) catch {
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, flat_import_graph, comptime_ctx) catch {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
}
@@ -605,6 +694,7 @@ fn resolveDirectoryImport(
span: ast.Span,
stdlib_paths: []const []const u8,
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
comptime_ctx: ComptimeContext,
) anyerror!ResolvedModule {
// Open the directory with iteration capability
@@ -679,7 +769,7 @@ fn resolveDirectoryImport(
};
try chain.put(file_path, {});
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, flat_import_graph, comptime_ctx);
_ = chain.remove(file_path);
try cache.put(file_path, result);
@@ -697,8 +787,13 @@ fn resolveDirectoryImport(
for (file_mod.decls) |decl| {
if (seen_nodes.contains(decl)) continue;
if (decl.data.declName()) |name| {
if (seen_in_list.contains(name)) continue;
try seen_in_list.put(name, {});
if (seen_in_list.contains(name)) {
// First-wins dedup for non-functions; retain a same-name
// FUNCTION authored by a different file (fix-0102a).
if (!declAuthorsFn(decl)) continue;
} else {
try seen_in_list.put(name, {});
}
}
try seen_nodes.put(decl, {});
try decl_list.append(allocator, decl);