diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index a9a9d34..956c7d5 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -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); +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f50dcd7..e1bcd29 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -765,6 +765,31 @@ pub const Lowering = struct { return ti.function.call_conv != .c; } + // ── Source-keyed cache writers (R5 §#4) ── + // Mirror each global `type_alias_map` / `module_const_map` / `global_names` + // write into its source-partitioned analogue, keyed by the registering + // decl's source. Behavior-preserving for now: the global maps stay the only + // readers; the per-source maps exist for the later read-side cutover. A null + // source (unreachable for a scanned top-level decl post-import-resolution) + // falls back to the main file; if even that is absent the write is skipped + // rather than recorded under a fabricated key. + fn recordTypeAliasBySource(self: *Lowering, source: ?[]const u8, name: []const u8, tid: TypeId) void { + const src = source orelse self.main_file orelse return; + self.program_index.putTypeAliasBySource(src, name, tid); + } + fn recordModuleConstBySource(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.ModuleConstInfo) void { + const src = source orelse self.main_file orelse return; + self.program_index.putModuleConstBySource(src, name, info); + } + fn recordGlobalBySource(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void { + const src = source orelse self.main_file orelse return; + self.program_index.putGlobalBySource(src, name, info); + } + fn dropModuleConstBySource(self: *Lowering, source: ?[]const u8, name: []const u8) void { + const src = source orelse self.main_file orelse return; + self.program_index.removeModuleConstBySource(src, name); + } + /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. fn scanDecls(self: *Lowering, decls: []const *const Node) void { // Pass 0: register every numeric-literal module const (`N :: 16` and the @@ -785,15 +810,27 @@ pub const Lowering = struct { if (decl.data != .const_decl) continue; const cd = decl.data.const_decl; switch (cd.value.data) { - .int_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {}, - .float_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {}, + .int_literal => { + const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 }; + self.program_index.module_const_map.put(cd.name, info) catch {}; + self.recordModuleConstBySource(decl.source_file, cd.name, info); + }, + .float_literal => { + const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .f64 }; + self.program_index.module_const_map.put(cd.name, info) catch {}; + self.recordModuleConstBySource(decl.source_file, cd.name, info); + }, // A const whose RHS is an integer EXPRESSION over other consts // (`M :: 2; N :: M + 1`) is itself a usable count: register it so // `moduleConstInt` can fold the RHS through `evalConstIntExpr` // (issue 0083). Placeholder `.s64` type — the count consumers read // only the value; if the expression doesn't fold (references a // non-const), `moduleConstInt` yields null and the use diagnoses. - .binary_op, .unary_op => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {}, + .binary_op, .unary_op => { + const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 }; + self.program_index.module_const_map.put(cd.name, info) catch {}; + self.recordModuleConstBySource(decl.source_file, cd.name, info); + }, else => {}, } } @@ -877,6 +914,7 @@ pub const Lowering = struct { } } self.program_index.type_alias_map.put(cd.name, target_ty) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, target_ty); } else if (cd.value.data == .identifier) { // Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide; // Chase through type_alias_map, then look up named types @@ -886,10 +924,12 @@ pub const Lowering = struct { const rhs_name = cd.value.data.identifier.name; if (self.program_index.type_alias_map.get(rhs_name)) |chained| { self.program_index.type_alias_map.put(cd.name, chained) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, chained); } else { const name_id = self.module.types.internString(rhs_name); if (self.module.types.findByName(name_id)) |tid| { self.program_index.type_alias_map.put(cd.name, tid) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, tid); } } } @@ -927,12 +967,14 @@ pub const Lowering = struct { const result_ty = self.resolveTypeCallWithBindings(call_data); if (result_ty != .void) { self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); } } else if (self.program_index.fn_ast_map.get(callee_name)) |fd| { // Type-returning function: Foo :: Complex(u32) if (fd.type_params.len > 0) { if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| { self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); } } } @@ -961,6 +1003,7 @@ pub const Lowering = struct { const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); if (result_ty != .void and result_ty != .unresolved) { self.program_index.type_alias_map.put(cd.name, result_ty) catch {}; + self.recordTypeAliasBySource(self.current_source_file, cd.name, result_ty); } } } @@ -974,17 +1017,19 @@ pub const Lowering = struct { // stay here (their type comes from the literal / inference). if (cd.type_annotation == null) { // Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;) - switch (cd.value.data) { - .string_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .string }) catch {}, - .int_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .s64 }) catch {}, - .float_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .f64 }) catch {}, - .bool_literal => self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = .bool }) catch {}, + const lit_ty: ?TypeId = switch (cd.value.data) { + .string_literal => .string, + .int_literal => .s64, + .float_literal => .f64, + .bool_literal => .bool, // Complex constant expressions (e.g. COLOR_WHITE :: Color.{ r = 255, ... }) - .struct_literal => { - const inferred_ty = self.inferExprType(cd.value); - self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = inferred_ty }) catch {}; - }, - else => {}, + .struct_literal => self.inferExprType(cd.value), + else => null, + }; + if (lit_ty) |ty| { + const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty }; + self.program_index.module_const_map.put(cd.name, info) catch {}; + self.recordModuleConstBySource(self.current_source_file, cd.name, info); } } }, @@ -1065,6 +1110,7 @@ pub const Lowering = struct { // placeholder behind as a usable const. if (ty == .unresolved) { _ = self.program_index.module_const_map.remove(cd.name); + self.dropModuleConstBySource(self.current_source_file, cd.name); return; } // Validate the initializer against the explicit annotation BY TYPE, so a @@ -1084,6 +1130,7 @@ pub const Lowering = struct { if (program_index_mod.evalConstFloatExpr(cd.value, self)) |fv| { self.diagNonIntegralNarrow(cd.value.span, fv, ty); _ = self.program_index.module_const_map.remove(cd.name); + self.dropModuleConstBySource(self.current_source_file, cd.name); return; } } @@ -1096,12 +1143,15 @@ pub const Lowering = struct { // `N : string : M + 2` are both pre-registered as `.s64` in scanDecls // pass 0); leaving it would let a count use still fold `N`. _ = self.program_index.module_const_map.remove(cd.name); + self.dropModuleConstBySource(self.current_source_file, cd.name); return; } // Reconcile the registration with the resolved annotation (pass 0 stored // a literal/expression placeholder type), so the const folds and emits at // its declared type — the same `put` the literal path always did. - self.program_index.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; + const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty }; + self.program_index.module_const_map.put(cd.name, info) catch {}; + self.recordModuleConstBySource(self.current_source_file, cd.name, info); } /// True iff a literal initializer of `value`'s kind is faithfully @@ -1218,6 +1268,7 @@ pub const Lowering = struct { .is_extern = vd.is_foreign, }); self.program_index.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {}; + self.recordGlobalBySource(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty }); } /// Serialize a top-level global's initializer into a static `ConstantValue`. @@ -1313,11 +1364,13 @@ pub const Lowering = struct { const rhs_name = cd.value.data.identifier.name; if (self.program_index.type_alias_map.get(rhs_name)) |chained| { self.program_index.type_alias_map.put(cd.name, chained) catch {}; + self.recordTypeAliasBySource(decl.source_file, cd.name, chained); progressed = true; } else { const name_id = self.module.types.internString(rhs_name); if (self.module.types.findByName(name_id)) |tid| { self.program_index.type_alias_map.put(cd.name, tid) catch {}; + self.recordTypeAliasBySource(decl.source_file, cd.name, tid); progressed = true; } } @@ -9533,6 +9586,7 @@ pub const Lowering = struct { // Register for runtime lookup: identifier resolution emits global_get self.program_index.global_names.put(name, .{ .id = gid, .ty = global_ty }) catch {}; + self.recordGlobalBySource(self.current_source_file, name, .{ .id = gid, .ty = global_ty }); } /// Lower a standalone `#run expr;` at the top level (side-effect only). @@ -14266,6 +14320,7 @@ pub const Lowering = struct { .is_const = true, }); self.program_index.global_names.put(global_name, .{ .id = gid, .ty = ctx_ty }) catch {}; + self.recordGlobalBySource(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty }); } /// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index edaa2eb..c2ced90 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -97,6 +97,54 @@ test "ProgramIndex declaration maps round-trip (A1.1b)" { try std.testing.expectEqualStrings("list_len", idx.ufcs_alias_map.get("len").?); } +// E0 (R5 §#4): the source-keyed caches partition by declaring source, so the +// SAME name authored in two different modules lands two DISTINCT entries under +// two source keys — never last-wins. The legacy global maps stay single-keyed +// by name (one entry per name), so the compat readers are untouched. +test "ProgramIndex source-keyed caches partition same-name authors by source" { + var idx = ProgramIndex.init(std.testing.allocator); + defer idx.deinit(); + + var blk_a = ast.Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{} } } }; + var blk_b = ast.Node{ .span = .{ .start = 1, .end = 1 }, .data = .{ .block = .{ .stmts = &.{} } } }; + + // SAME alias name `Foo` authored in two modules → two distinct TypeIds. + idx.putTypeAliasBySource("a.sx", "Foo", .s64); + idx.putTypeAliasBySource("b.sx", "Foo", .f64); + try std.testing.expectEqual(@as(?types.TypeId, .s64), idx.type_aliases_by_source.get("a.sx").?.get("Foo")); + try std.testing.expectEqual(@as(?types.TypeId, .f64), idx.type_aliases_by_source.get("b.sx").?.get("Foo")); + try std.testing.expectEqual(@as(u32, 2), idx.type_aliases_by_source.count()); + + // SAME const name `K` authored in two modules → two distinct ModuleConstInfos. + idx.putModuleConstBySource("a.sx", "K", .{ .value = &blk_a, .ty = .s32 }); + idx.putModuleConstBySource("b.sx", "K", .{ .value = &blk_b, .ty = .f32 }); + try std.testing.expect(idx.module_consts_by_source.get("a.sx").?.get("K").?.value == &blk_a); + try std.testing.expect(idx.module_consts_by_source.get("b.sx").?.get("K").?.value == &blk_b); + try std.testing.expectEqual(@as(?types.TypeId, .s32), idx.module_consts_by_source.get("a.sx").?.get("K").?.ty); + try std.testing.expectEqual(@as(?types.TypeId, .f32), idx.module_consts_by_source.get("b.sx").?.get("K").?.ty); + + // SAME global name `g` authored in two modules → two distinct GlobalInfos. + idx.putGlobalBySource("a.sx", "g", .{ .id = inst.GlobalId.fromIndex(0), .ty = .s64 }); + idx.putGlobalBySource("b.sx", "g", .{ .id = inst.GlobalId.fromIndex(1), .ty = .f64 }); + try std.testing.expect(idx.globals_by_source.get("a.sx").?.get("g").?.id == inst.GlobalId.fromIndex(0)); + try std.testing.expect(idx.globals_by_source.get("b.sx").?.get("g").?.id == inst.GlobalId.fromIndex(1)); + + // Compat readers: the legacy global maps stay keyed by NAME alone, so a + // same-name author is last-wins there — exactly ONE entry for `Foo` / `K`, + // unchanged by the source-keyed writes above. + idx.type_alias_map.put("Foo", .s64) catch unreachable; + idx.type_alias_map.put("Foo", .f64) catch unreachable; + try std.testing.expectEqual(@as(u32, 1), idx.type_alias_map.count()); + idx.module_const_map.put("K", .{ .value = &blk_a, .ty = .s32 }) catch unreachable; + idx.module_const_map.put("K", .{ .value = &blk_b, .ty = .f32 }) catch unreachable; + try std.testing.expectEqual(@as(u32, 1), idx.module_const_map.count()); + + // removeModuleConstBySource drops only the named entry under its source. + idx.removeModuleConstBySource("a.sx", "K"); + try std.testing.expect(idx.module_consts_by_source.get("a.sx").?.get("K") == null); + try std.testing.expect(idx.module_consts_by_source.get("b.sx").?.get("K").?.value == &blk_b); +} + /// Stand-in for the leaf-name lookup both array-dimension resolvers pass to the /// shared `evalConstIntExpr`: `M`/`N` resolve to integers, everything else is /// genuinely non-comptime. diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 6ae2ca7..5b14896 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -581,6 +581,11 @@ pub const GlobalInfo = struct { id: inst.GlobalId, ty: TypeId }; /// (set in `init`); the rest default to `page_allocator`. Written only by the /// declaration scan / registration code in `Lowering`; read everywhere else. pub const ProgramIndex = struct { + /// The lowering/compilation allocator (`module.alloc`), retained so the + /// source-keyed caches below can lazily create their inner per-source maps. + /// Lives for the whole compilation; the inner maps are freed in `deinit`. + alloc: std.mem.Allocator, + // ── Import / visibility ── /// Declaration name → is the function imported (declared `extern`)? import_flags: std.StringHashMap(bool), @@ -637,12 +642,30 @@ pub const ProgramIndex = struct { /// UFCS alias name → target function name. ufcs_alias_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), + // ── Source-keyed semantic caches (R5 §#4) ── + // The source-partitioned analogues of `type_alias_map` / `module_const_map` + // / `global_names`, keyed `source path → name → X`. Written by the same scan + // (`scanDecls` in lower.zig), keyed by the registering decl's source. The + // global maps above stay the ONLY readers for now; the read-side cutover to + // `selectedAuthor.source` lands in a later phase. These maps OWN their inner + // per-source maps and free them in `deinit`. + /// Type alias name → target TypeId, partitioned by declaring source. + type_aliases_by_source: std.StringHashMap(std.StringHashMap(TypeId)), + /// Module-level value const → info, partitioned by declaring source. + module_consts_by_source: std.StringHashMap(std.StringHashMap(ModuleConstInfo)), + /// `#run` / top-level global name → GlobalInfo, partitioned by declaring source. + globals_by_source: std.StringHashMap(std.StringHashMap(GlobalInfo)), + pub fn init(alloc: std.mem.Allocator) ProgramIndex { return .{ + .alloc = alloc, .import_flags = std.StringHashMap(bool).init(alloc), .fn_ast_map = std.StringHashMap(*const ast.FnDecl).init(alloc), .qualified_fn_source = std.StringHashMap([]const u8).init(alloc), .global_names = std.StringHashMap(GlobalInfo).init(alloc), + .type_aliases_by_source = std.StringHashMap(std.StringHashMap(TypeId)).init(alloc), + .module_consts_by_source = std.StringHashMap(std.StringHashMap(ModuleConstInfo)).init(alloc), + .globals_by_source = std.StringHashMap(std.StringHashMap(GlobalInfo)).init(alloc), }; } @@ -660,5 +683,42 @@ pub const ProgramIndex = struct { self.protocol_ast_map.deinit(); self.module_const_map.deinit(); self.ufcs_alias_map.deinit(); + deinitBySource(TypeId, &self.type_aliases_by_source); + deinitBySource(ModuleConstInfo, &self.module_consts_by_source); + deinitBySource(GlobalInfo, &self.globals_by_source); + } + + /// Free every inner per-source map, then the outer map. + fn deinitBySource(comptime V: type, outer: *std.StringHashMap(std.StringHashMap(V))) void { + var it = outer.valueIterator(); + while (it.next()) |inner| inner.deinit(); + outer.deinit(); + } + + /// Insert `name → value` into the per-source map for `source`, creating the + /// inner map on first use. OOM is swallowed to mirror the `catch {}` global + /// writes this shadows. + fn putBySource(comptime V: type, outer: *std.StringHashMap(std.StringHashMap(V)), alloc: std.mem.Allocator, source: []const u8, name: []const u8, value: V) void { + const gop = outer.getOrPut(source) catch return; + if (!gop.found_existing) gop.value_ptr.* = std.StringHashMap(V).init(alloc); + gop.value_ptr.put(name, value) catch {}; + } + + pub fn putTypeAliasBySource(self: *ProgramIndex, source: []const u8, name: []const u8, tid: TypeId) void { + putBySource(TypeId, &self.type_aliases_by_source, self.alloc, source, name, tid); + } + + pub fn putModuleConstBySource(self: *ProgramIndex, source: []const u8, name: []const u8, info: ModuleConstInfo) void { + putBySource(ModuleConstInfo, &self.module_consts_by_source, self.alloc, source, name, info); + } + + pub fn putGlobalBySource(self: *ProgramIndex, source: []const u8, name: []const u8, info: GlobalInfo) void { + putBySource(GlobalInfo, &self.globals_by_source, self.alloc, source, name, info); + } + + /// Mirror a `module_const_map.remove` into the per-source map: drop `name` + /// from `source`'s inner map (a no-op if the source/name is absent). + pub fn removeModuleConstBySource(self: *ProgramIndex, source: []const u8, name: []const u8) void { + if (self.module_consts_by_source.getPtr(source)) |inner| _ = inner.remove(name); } };