refactor(ir): extract ProgramIndex, move low-fanout decl facts (A1.1a)

Architecture phase A1.1a. Introduce src/ir/program_index.zig as the single
storage owner for declaration-name / import / visibility facts, and move the
three low-fanout maps out of the Lowering state bag:

- import_flags     (owned by ProgramIndex)
- module_scopes    (borrowed pointer into a core.zig-owned map)
- import_graph     (borrowed pointer into a core.zig-owned map)

Lowering embeds one ProgramIndex by value and reaches every moved fact through
self.program_index.<field>; later phases hand collaborator modules a
*ProgramIndex instead of *Lowering. 8 call sites in lower.zig + 2 setters in
core.zig repointed. No duplicate storage, no fallback path; zig build enforces
no missed reference.

Mutation-heavy registration (registerStructDecl etc.) stays in Lowering and
now writes import_flags through the index. High-fanout maps are deferred to
A1.1b.

Adds src/ir/program_index.test.zig (init-empty, import_flags round-trip,
borrowed-view ownership) wired into the ir.zig barrel.

Behavior-preserving: zig build, zig build test, and bash tests/run_examples.sh
(350 passed, 0 failed) all green.
This commit is contained in:
agra
2026-06-02 12:04:31 +03:00
parent 795ce3dc7d
commit 90520eefeb
5 changed files with 96 additions and 14 deletions

View File

@@ -288,8 +288,8 @@ pub const Compilation = struct {
lowering.resolved_root = root;
lowering.target_config = self.target_config;
lowering.diagnostics = &self.diagnostics;
lowering.module_scopes = &self.module_scopes;
lowering.import_graph = &self.import_graph;
lowering.program_index.module_scopes = &self.module_scopes;
lowering.program_index.import_graph = &self.import_graph;
lowering.lowerRoot(root);
if (self.diagnostics.hasErrors()) return error.CompileError;

View File

@@ -4,6 +4,7 @@ pub const module = @import("module.zig");
pub const print = @import("print.zig");
pub const interp = @import("interp.zig");
pub const lower = @import("lower.zig");
pub const program_index = @import("program_index.zig");
pub const TypeId = types.TypeId;
pub const TypeInfo = types.TypeInfo;
@@ -30,6 +31,7 @@ pub const printModule = print.printModule;
pub const Interpreter = interp.Interpreter;
pub const Value = interp.Value;
pub const Lowering = lower.Lowering;
pub const ProgramIndex = program_index.ProgramIndex;
pub const compiler_hooks = @import("compiler_hooks.zig");
pub const emit_llvm = @import("emit_llvm.zig");
@@ -48,6 +50,7 @@ pub const module_tests = @import("module.test.zig");
pub const print_tests = @import("print.test.zig");
pub const interp_tests = @import("interp.test.zig");
pub const lower_tests = @import("lower.test.zig");
pub const program_index_tests = @import("program_index.test.zig");
pub const type_bridge_tests = @import("type_bridge.test.zig");
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
pub const jni_descriptor_tests = @import("jni_descriptor.test.zig");

View File

@@ -11,6 +11,7 @@ const parser_mod = @import("../parser.zig");
const interp_mod = @import("interp.zig");
const errors = @import("../errors.zig");
const jni_descriptor = @import("jni_descriptor.zig");
const ProgramIndex = @import("program_index.zig").ProgramIndex;
const TypeId = types.TypeId;
const StringId = types.StringId;
@@ -104,9 +105,11 @@ pub const Lowering = struct {
target_type: ?TypeId = null, // target type for struct/enum literals without explicit names
lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered
local_fn_counter: u32 = 0, // unique counter for mangling local function names
import_flags: std.StringHashMap(bool), // tracks whether each function is imported
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
/// Declaration-name / import / visibility facts (architecture phase A1,
/// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` /
/// `import_graph` from the compilation driver. Reached via
/// `self.program_index.<field>`; populated by scan/registration code.
program_index: ProgramIndex,
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
// Implicit Context parameter machinery. When the program imports
// `std.sx` (and therefore declares `Context :: struct {...}`), every
@@ -312,7 +315,7 @@ pub const Lowering = struct {
.alloc = module.alloc,
.fn_ast_map = std.StringHashMap(*const ast.FnDecl).init(module.alloc),
.lowered_functions = std.StringHashMap(void).init(module.alloc),
.import_flags = std.StringHashMap(bool).init(module.alloc),
.program_index = ProgramIndex.init(module.alloc),
.global_names = std.StringHashMap(GlobalInfo).init(module.alloc),
};
}
@@ -1479,14 +1482,14 @@ pub const Lowering = struct {
switch (decl.data) {
.fn_decl => |fd| {
self.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
self.import_flags.put(fd.name, is_imported) catch {};
self.program_index.import_flags.put(fd.name, is_imported) catch {};
// Declare extern stub for all functions (bodies lowered lazily)
self.declareFunction(&fd, fd.name);
},
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
self.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {};
self.import_flags.put(cd.name, is_imported) catch {};
self.program_index.import_flags.put(cd.name, is_imported) catch {};
self.declareFunction(&cd.value.data.fn_decl, cd.name);
} else if (cd.value.data == .struct_decl) {
self.registerStructDecl(&cd.value.data.struct_decl);
@@ -1893,11 +1896,11 @@ pub const Lowering = struct {
/// responsible for restricting the call to names that ARE known
/// top-level decls; otherwise every local variable would be policed.
fn isNameVisible(self: *Lowering, name: []const u8) bool {
const scopes = self.module_scopes orelse return true;
const scopes = self.program_index.module_scopes orelse return true;
const source = self.current_source_file orelse return true;
const own_scope = scopes.get(source) orelse return true;
if (own_scope.contains(name)) return true;
const graph = self.import_graph orelse return true;
const graph = self.program_index.import_graph orelse return true;
const direct = graph.get(source) orelse return true;
var it = direct.iterator();
while (it.next()) |kv| {
@@ -11984,7 +11987,7 @@ pub const Lowering = struct {
out.appendSlice(self.alloc, entries) catch {};
return;
};
const graph = self.import_graph orelse {
const graph = self.program_index.import_graph orelse {
out.appendSlice(self.alloc, entries) catch {};
return;
};
@@ -14360,7 +14363,7 @@ pub const Lowering = struct {
const method_fd = &method_node.data.fn_decl;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue;
self.fn_ast_map.put(qualified, method_fd) catch {};
self.import_flags.put(qualified, is_imported) catch {};
self.program_index.import_flags.put(qualified, is_imported) catch {};
self.declareFunction(method_fd, qualified);
impl_methods.put(method_fd.name, {}) catch {};
}
@@ -14373,7 +14376,7 @@ pub const Lowering = struct {
const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type);
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue;
self.fn_ast_map.put(qualified, synth_fd) catch {};
self.import_flags.put(qualified, is_imported) catch {};
self.program_index.import_flags.put(qualified, is_imported) catch {};
self.declareFunction(synth_fd, qualified);
}
}
@@ -14480,7 +14483,7 @@ pub const Lowering = struct {
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue;
if (self.fn_ast_map.contains(q)) continue; // first impl wins
self.fn_ast_map.put(q, mfd) catch {};
self.import_flags.put(q, is_imported) catch {};
self.program_index.import_flags.put(q, is_imported) catch {};
if (!is_generic_src) self.declareFunction(mfd, q);
}
}

View File

@@ -0,0 +1,40 @@
const std = @import("std");
const ProgramIndex = @import("program_index.zig").ProgramIndex;
test "ProgramIndex.init starts empty with unset borrowed views" {
var idx = ProgramIndex.init(std.testing.allocator);
defer idx.deinit();
try std.testing.expectEqual(@as(u32, 0), idx.import_flags.count());
try std.testing.expect(idx.module_scopes == null);
try std.testing.expect(idx.import_graph == null);
}
test "ProgramIndex.import_flags round-trips imported vs local" {
var idx = ProgramIndex.init(std.testing.allocator);
defer idx.deinit();
try idx.import_flags.put("printf", true);
try idx.import_flags.put("main", false);
try std.testing.expectEqual(@as(?bool, true), idx.import_flags.get("printf"));
try std.testing.expectEqual(@as(?bool, false), idx.import_flags.get("main"));
try std.testing.expect(idx.import_flags.get("absent") == null);
}
test "ProgramIndex borrows module_scopes / import_graph without owning them" {
const ScopeSet = std.StringHashMap(std.StringHashMap(void));
var scopes = ScopeSet.init(std.testing.allocator);
defer scopes.deinit();
var graph = ScopeSet.init(std.testing.allocator);
defer graph.deinit();
var idx = ProgramIndex.init(std.testing.allocator);
defer idx.deinit();
idx.module_scopes = &scopes;
idx.import_graph = &graph;
// Reads go through the borrowed pointer; the backing stays caller-owned,
// so idx.deinit() must not free it (testing.allocator would flag a
// double-free / leak otherwise).
try std.testing.expect(idx.module_scopes.? == &scopes);
try std.testing.expect(idx.import_graph.? == &graph);
try std.testing.expectEqual(@as(u32, 0), idx.module_scopes.?.count());
}

36
src/ir/program_index.zig Normal file
View File

@@ -0,0 +1,36 @@
const std = @import("std");
/// Single storage owner for declaration-name facts: declarations, imports,
/// visibility, names, aliases, templates, protocols, foreign classes, and
/// module constants. The architecture stream (`current/PLAN-ARCH.md`, phase
/// A1) extracts these out of the `Lowering` state bag incrementally; this is
/// the A1.1a slice — the low-fanout import/visibility facts. `Lowering` embeds
/// one `ProgramIndex` by value and reaches every moved fact through it; later
/// phases hand collaborator modules a `*ProgramIndex` instead of `*Lowering`.
///
/// `import_flags` is owned here. `module_scopes` / `import_graph` are borrowed
/// views into maps owned by the compilation driver (`core.zig`); this index
/// only reads through them.
pub const ProgramIndex = struct {
/// Declaration name → is the function imported (declared `extern`)?
/// Populated by the declaration scan / registration code in `Lowering`.
import_flags: std.StringHashMap(bool),
/// Per-module visible names, keyed by source file (from import
/// resolution). Borrowed — the backing map lives in the compilation
/// driver, so this index does not free it.
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null,
/// Module path → set of directly imported paths; drives the
/// `param_impl_map` cross-module visibility filter. Borrowed — owned by
/// the compilation driver.
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null,
pub fn init(alloc: std.mem.Allocator) ProgramIndex {
return .{ .import_flags = std.StringHashMap(bool).init(alloc) };
}
pub fn deinit(self: *ProgramIndex) void {
self.import_flags.deinit();
}
};