feat(resolver): source-keyed alias/const/global caches, write-side only [stdlib E0]

Phase E0 of the unified resolver (R5 §#4): add the source-partitioned
analogues of the global `type_alias_map` / `module_const_map` /
`global_names`, keyed `source path -> name -> X`, and POPULATE them from
the existing scan. Purely additive and behavior-preserving — the global
maps remain the ONLY readers; the read-side cutover to
`selectedAuthor.source` is E1.

ProgramIndex:
- type_aliases_by_source / module_consts_by_source / globals_by_source
  (StringHashMap of inner StringHashMap), owned + freed on deinit.
- put{TypeAlias,ModuleConst,Global}BySource + removeModuleConstBySource
  helpers; retain `module.alloc` to lazily create inner per-source maps.

lower.zig scan: every global `type_alias_map`/`module_const_map`/
`global_names` write (and each module_const_map.remove) now mirrors into
its by-source analogue, keyed by the registering decl's source
(decl.source_file / current_source_file), the analogue of module_fns.

Tests:
- program_index.test.zig: same alias/const/global name under two sources
  lands two distinct entries (not last-wins); compat globals stay
  single-keyed; removeModuleConstBySource scoped to its source.
- lower.test.zig: end-to-end two-source namespace fixture — the scan
  populates the by-source caches per declaring source while the global
  maps stay single-keyed by name.

Gate: zig build + zig build test (423, incl. 2 new) + run_examples
(477, byte-identical) + m3te ios-sim build, all exit 0.
This commit is contained in:
agra
2026-06-07 14:17:08 +03:00
parent c839c60233
commit 662142c388
4 changed files with 288 additions and 14 deletions

View File

@@ -1457,3 +1457,114 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
// A name no module authors (and no flat import provides) never routes.
try std.testing.expect(lowering.selectPlainCallableAuthor("nonexistent", b_path) == .none);
}
// E0 (R5 §#4): the scan populates the source-keyed caches partitioned by the
// registering decl's source. Two namespaced modules each author the SAME alias
// name `Color` AND the SAME const name `K`; the scan recurses into each
// namespace's decls (per-source). After lowering, the by-source maps hold TWO
// distinct entries under the two source keys (not last-wins), while the legacy
// global maps stay single-keyed by name — the compat readers are unchanged.
test "lower: scan populates source-keyed caches per declaring source (E0)" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const io = lowerTestIo();
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "Color :: *u8;\nK :: 5;\n" });
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "Color :: *u16;\nK :: 7;\n" });
const main_src =
\\na :: #import "a.sx";
\\nb :: #import "b.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 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_scopes = std.StringHashMap(std.StringHashMap(void)).init(alloc);
try module_scopes.put(main_path, mod.scope);
var cache_it = cache.iterator();
while (cache_it.next()) |entry| {
try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope);
}
var module_fns = imports.ModuleFns.init(alloc);
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
const resolved_root = try alloc.create(Node);
resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } };
var module = ir_mod.Module.init(alloc);
defer module.deinit();
var diagnostics = errors.DiagnosticList.init(alloc, main_source, main_path);
var lowering = Lowering.init(&module);
lowering.main_file = main_path;
lowering.resolved_root = resolved_root;
lowering.diagnostics = &diagnostics;
lowering.program_index.module_scopes = &module_scopes;
lowering.program_index.import_graph = &import_graph;
lowering.program_index.flat_import_graph = &flat_import_graph;
lowering.program_index.module_fns = &module_fns;
lowering.program_index.module_decls = &facts.decls;
lowering.lowerRoot(resolved_root);
try std.testing.expect(!diagnostics.hasErrors());
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 idx = &lowering.program_index;
// SAME alias name `Color` lands a DISTINCT entry under each source key.
const color_a = idx.type_aliases_by_source.get(a_path).?.get("Color").?;
const color_b = idx.type_aliases_by_source.get(b_path).?.get("Color").?;
try std.testing.expect(color_a != color_b); // *u8 vs *u16 — source-partitioned
// SAME const name `K` lands a DISTINCT entry (distinct value node) per source.
const k_a = idx.module_consts_by_source.get(a_path).?.get("K").?;
const k_b = idx.module_consts_by_source.get(b_path).?.get("K").?;
try std.testing.expect(k_a.value != k_b.value);
// Compat readers: the legacy global maps stay keyed by NAME alone — a
// hashmap key holds exactly one value, so a same-name author is last-wins
// there (one entry for `Color` / `K`), unchanged by the by-source writes.
// The single global `Color` is one of the two source-keyed authors (not a
// merged/duplicated value).
const global_color = idx.type_alias_map.get("Color").?;
try std.testing.expect(global_color == color_a or global_color == color_b);
const global_k = idx.module_const_map.get("K").?;
try std.testing.expect(global_k.value == k_a.value or global_k.value == k_b.value);
}