diff --git a/src/core.zig b/src/core.zig index fdc9cd1..9cc4229 100644 --- a/src/core.zig +++ b/src/core.zig @@ -26,6 +26,12 @@ pub const Compilation = struct { import_sources: std.StringHashMap([:0]const u8), module_scopes: std.StringHashMap(std.StringHashMap(void)), import_graph: std.StringHashMap(std.StringHashMap(void)), + /// Flat-only subset of `import_graph` (bare `#import` edges, no namespaced + /// `ns :: #import`). Borrowed by `ProgramIndex.flat_import_graph`. + flat_import_graph: std.StringHashMap(std.StringHashMap(void)), + /// Per-module authored-function index (`path → name → *const FnDecl`). + /// Borrowed by `ProgramIndex.module_fns`. + module_fns: imports.ModuleFns, ir_emitter: ?ir.LLVMEmitter = null, /// Lowered IR module, kept alive past `generateCode` so post-link /// callbacks can re-enter the interpreter to invoke sx functions @@ -52,6 +58,8 @@ pub const Compilation = struct { .import_sources = std.StringHashMap([:0]const u8).init(allocator), .module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator), .import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator), + .flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator), + .module_fns = imports.ModuleFns.init(allocator), .target_config = target_config, .stdlib_paths = stdlib_paths, }; @@ -101,6 +109,7 @@ pub const Compilation = struct { &self.diagnostics, self.stdlib_paths, &self.import_graph, + &self.flat_import_graph, self.comptimeContext(), ) catch return error.CompileError; @@ -111,6 +120,11 @@ pub const Compilation = struct { self.module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope) catch {}; } + // Per-module authored-function index, built from the SAME modules as + // `module_scopes` (main + every cache entry). Keyed by path; same-name + // cross-module authors stay distinct under their own paths. + imports.buildModuleFns(self.allocator, self.file_path, mod, &cache, &self.module_fns) catch {}; + // Store main file source in import_sources so error reporting can find it self.import_sources.put(self.file_path, self.source) catch {}; @@ -276,6 +290,8 @@ pub const Compilation = struct { lowering.diagnostics = &self.diagnostics; lowering.program_index.module_scopes = &self.module_scopes; lowering.program_index.import_graph = &self.import_graph; + lowering.program_index.flat_import_graph = &self.flat_import_graph; + lowering.program_index.module_fns = &self.module_fns; lowering.lowerRoot(root); if (self.diagnostics.hasErrors()) return error.CompileError; diff --git a/src/imports.test.zig b/src/imports.test.zig new file mode 100644 index 0000000..81f45a4 --- /dev/null +++ b/src/imports.test.zig @@ -0,0 +1,109 @@ +// Tests for imports.zig — flat-import name-resolution data retention (fix-0102a). + +const std = @import("std"); +const ast = @import("ast.zig"); +const parser = @import("parser.zig"); +const imports = @import("imports.zig"); + +var g_test_threaded: ?std.Io.Threaded = null; +fn testIo() std.Io { + if (g_test_threaded == null) { + g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{}); + } + return g_test_threaded.?.io(); +} + +// Two flat-imported modules each author `greet`; a third is namespaced. The +// step retains BOTH `greet` authors under their own paths in `module_fns`, and +// records the namespaced import in `import_graph` but NOT in `flat_import_graph`. +test "imports: module_fns retains same-name cross-module fns; flat_import_graph excludes namespaced edge" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = testIo(); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "nsmod.sx", .data = "helper :: () -> s64 { 3 }\n" }); + const main_src = + \\#import "a.sx"; + \\#import "b.sx"; + \\ns :: #import "nsmod.sx"; + \\main :: () -> s32 { 0 } + \\ + ; + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src }); + + var dirbuf: [4096]u8 = undefined; + const dirlen = try tmp.dir.realPath(io, &dirbuf); + const absdir = dirbuf[0..dirlen]; + + const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); + const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir}); + const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir}); + const ns_path = try std.fmt.allocPrint(alloc, "{s}/nsmod.sx", .{absdir}); + + const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20)); + const main_source = try alloc.dupeZ(u8, main_bytes); + var p = parser.Parser.init(alloc, main_source); + const root = p.parse() catch return error.ParseFailed; + + var chain = std.StringHashMap(void).init(alloc); + var cache = imports.ModuleCache.init(alloc); + var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); + var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); + const stdlib_paths = [_][]const u8{}; + + const mod = try imports.resolveImports( + alloc, + io, + root, + absdir, + main_path, + &chain, + &cache, + null, + null, + &stdlib_paths, + &import_graph, + &flat_import_graph, + .{}, + ); + + var module_fns = imports.ModuleFns.init(alloc); + try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns); + + // mergeFlat no longer first-wins-drops the second `greet`: the global flat + // decl list the lowering pass walks now carries BOTH authors. + var greet_count: usize = 0; + for (mod.decls) |decl| { + const name = decl.data.declName() orelse continue; + if (std.mem.eql(u8, name, "greet")) greet_count += 1; + } + try std.testing.expectEqual(@as(usize, 2), greet_count); + + // module_fns retains BOTH authors of `greet`, keyed by their own paths. + const a_fns = module_fns.get(a_path) orelse return error.MissingAFns; + const b_fns = module_fns.get(b_path) orelse return error.MissingBFns; + const a_greet = a_fns.get("greet") orelse return error.MissingAGreet; + const b_greet = b_fns.get("greet") orelse return error.MissingBGreet; + // Distinct authoring decls — not the same node deduped down to one. + try std.testing.expect(a_greet != b_greet); + + // flat_import_graph carries the two bare `#import` edges, NOT the + // namespaced `ns :: #import` edge. + const flat = flat_import_graph.get(main_path) orelse return error.MissingFlatEdges; + try std.testing.expect(flat.contains(a_path)); + try std.testing.expect(flat.contains(b_path)); + try std.testing.expect(!flat.contains(ns_path)); + + // The full import_graph DOES record the namespaced edge (the contrast that + // makes the flat-graph exclusion meaningful). + const full = import_graph.get(main_path) orelse return error.MissingFullEdges; + try std.testing.expect(full.contains(a_path)); + try std.testing.expect(full.contains(b_path)); + try std.testing.expect(full.contains(ns_path)); +} diff --git a/src/imports.zig b/src/imports.zig index 8517b4f..34e71cd 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -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); diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 6b6cc82..2612b18 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -1,5 +1,6 @@ const std = @import("std"); const ast = @import("../ast.zig"); +const imports = @import("../imports.zig"); const types = @import("types.zig"); const inst = @import("inst.zig"); const errors = @import("../errors.zig"); @@ -570,9 +571,10 @@ pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId }; /// `self.program_index.`; later phases hand collaborator modules a /// `*ProgramIndex` instead of `*Lowering`. /// -/// OWNS the declaration maps below. BORROWS `module_scopes` / `import_graph` -/// (pointers into maps owned by the compilation driver, `core.zig`) — those -/// are read-only views and are never freed here. +/// OWNS the declaration maps below. BORROWS `module_scopes` / `import_graph` / +/// `flat_import_graph` / `module_fns` (pointers into maps owned by the +/// compilation driver, `core.zig`) — those are read-only views and are never +/// freed here. /// /// Per-map allocators are preserved exactly as they were on `Lowering`: /// `import_flags` / `fn_ast_map` / `global_names` use the lowering allocator @@ -587,6 +589,16 @@ pub const ProgramIndex = struct { /// Module path → set of directly imported paths (param_impl visibility /// filter). Borrowed view. import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, + /// Module path → set of directly FLAT-imported paths — the subset of + /// `import_graph` edges from a bare `#import` (never a namespaced + /// `ns :: #import`). fix-0102c's bare-name disambiguation walks this to + /// decide which same-name authors a flat importer can reach. Borrowed view. + flat_import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, + /// Module path → (function name → authoring `*const FnDecl`), mirroring + /// `module_scopes`. Retains every same-name author under its own path so + /// fix-0102c can resolve a flat call to the right module's function. + /// Borrowed view. + module_fns: ?*imports.ModuleFns = null, // ── Declaration maps ── /// Function name → AST decl. @@ -627,7 +639,8 @@ pub const ProgramIndex = struct { } pub fn deinit(self: *ProgramIndex) void { - // Owned maps only — module_scopes / import_graph are borrowed. + // Owned maps only — module_scopes / import_graph / flat_import_graph / + // module_fns are borrowed. self.import_flags.deinit(); self.fn_ast_map.deinit(); self.qualified_fn_source.deinit(); diff --git a/src/root.zig b/src/root.zig index faf2280..767d19f 100644 --- a/src/root.zig +++ b/src/root.zig @@ -13,6 +13,7 @@ pub const trace_runtime_tests = @import("runtime_trace.test.zig"); pub const sema = @import("sema.zig"); pub const sema_tests = @import("sema.test.zig"); pub const imports = @import("imports.zig"); +pub const imports_tests = @import("imports.test.zig"); pub const core = @import("core.zig"); pub const c_import = @import("c_import.zig"); pub const ir = @import("ir/ir.zig");