From 8058be2538bd29fa964d63b8dbff2bcbeefd1b71 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 9 Jun 2026 11:25:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(stdlib/S1.1):=20DeclId=20for=20every=20dec?= =?UTF-8?q?laration=20=E2=80=94=20additive=20DeclTable=20[additive]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build a DeclTable in parallel with the import facts: every RawDeclRef (source / imported / namespaced / C-imported) gets a stable DeclId carrying source path, display name, AST node identity, span, and DeclKind. Namespace targets record their members' DeclIds (NamespaceTarget.member_ids). A generic struct's template is keyed by DeclId in a parallel struct_template_by_decl store, written alongside the live name-keyed struct_template_map. A Debug-only round-trip cross-check (RawDeclRef -> DeclId -> AST node ptr) asserts the table identifies the same node across the corpus, run from buildDeclTable and pinned by a unit test. Additive (S0.1 class: mirror): the old maps stay active and lowering still consumes them; nothing reads the DeclTable / struct_template_by_decl for selection yet (the S4 cutover does). Generated IR + output bytes are unchanged by construction. Gate over the baseline-green corpus: zig build, zig build test (424/424), bash tests/run_examples.sh (540 passed) — all exit 0; single-author output byte-identical (37 .ir snapshots unchanged). --- src/core.zig | 12 +++ src/imports.test.zig | 100 ++++++++++++++++++ src/imports.zig | 214 +++++++++++++++++++++++++++++++++++++++ src/ir/lower.zig | 11 ++ src/ir/program_index.zig | 12 ++- 5 files changed, 348 insertions(+), 1 deletion(-) diff --git a/src/core.zig b/src/core.zig index f73c141..fd0b117 100644 --- a/src/core.zig +++ b/src/core.zig @@ -38,6 +38,10 @@ pub const Compilation = struct { /// Namespace import edges (`importer → alias → NamespaceTarget`), built by /// `imports.buildImportFacts`. Borrowed by `ProgramIndex.namespace_edges`. namespace_edges: imports.NamespaceEdges, + /// Stable `DeclId` for every declaration (Fork C S1), built by + /// `imports.buildDeclTable` in parallel with the import facts. Borrowed by + /// `ProgramIndex.decl_table`. + decl_table: imports.DeclTable, 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 @@ -68,6 +72,7 @@ pub const Compilation = struct { .module_fns = imports.ModuleFns.init(allocator), .module_decls = imports.ModuleDecls.init(allocator), .namespace_edges = imports.NamespaceEdges.init(allocator), + .decl_table = imports.DeclTable.init(allocator), .target_config = target_config, .stdlib_paths = stdlib_paths, }; @@ -143,6 +148,12 @@ pub const Compilation = struct { self.module_decls = facts.decls; self.namespace_edges = facts.ns_edges; + // DeclTable (Fork C S1): a stable DeclId for every declaration, built in + // parallel from the SAME modules. Additive — nothing consumes it for + // selection yet, so generated IR + bytes are unchanged. Updates + // `namespace_edges` in place to record each target's member ids. + self.decl_table = try imports.buildDeclTable(self.allocator, self.file_path, mod, &cache, &self.module_decls, &self.namespace_edges); + // Store main file source in import_sources so error reporting can find it self.import_sources.put(self.file_path, self.source) catch {}; @@ -312,6 +323,7 @@ pub const Compilation = struct { lowering.program_index.module_fns = &self.module_fns; lowering.program_index.module_decls = &self.module_decls; lowering.program_index.namespace_edges = &self.namespace_edges; + lowering.program_index.decl_table = &self.decl_table; lowering.lowerRoot(root); if (self.diagnostics.hasErrors()) return error.CompileError; diff --git a/src/imports.test.zig b/src/imports.test.zig index 99f06f6..9739f79 100644 --- a/src/imports.test.zig +++ b/src/imports.test.zig @@ -503,3 +503,103 @@ test "buildImportFacts: namespace-alias-then-fn same-module collision is diagnos const m_idx = facts.decls.get(main_path) orelse return error.MissingMainIndex; try expectTag(m_idx.names.get("dup") orelse return error.MissingDup, .namespace_decl); } + +// ── DeclTable unit tests (Fork C S1.1) ── + +// Every source / imported / namespaced declaration gets a stable DeclId; the +// RawDeclRef → DeclId → AST node round-trip holds; a generic struct is keyable +// by DeclId; and the namespace target records its members' ids. The OLD facts +// (`module_decls` / `ns_edges`) are untouched — the table is built in parallel. +test "buildDeclTable: stable DeclId per decl, round-trip, struct keying, namespace member ids" { + 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 = "lib.sx", .data = "helper :: () -> s64 { 9 }\nBox :: struct($T: Type) { v: T; }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "geom.sx", .data = "Point :: struct { x: s64 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = "#import \"lib.sx\";\ng :: #import \"geom.sx\";\nmain :: () -> s32 { 0 }\n" }); + + var dirbuf: [4096]u8 = undefined; + const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)]; + const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); + const lib_path = try std.fmt.allocPrint(alloc, "{s}/lib.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 diags = errors.DiagnosticList.init(alloc, main_source, main_path); + 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, + &diags, + &stdlib_paths, + &import_graph, + &flat_import_graph, + .{}, + ); + + var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache); + var table = try imports.buildDeclTable(alloc, main_path, mod, &cache, &facts.decls, &facts.ns_edges); + defer table.deinit(); + + // Every module author resolves to a DeclId that round-trips to the same node + // and carries the matching name + source. (verifyRoundTrip also asserts this + // in Debug; this pins the public lookup API too.) + var mit = facts.decls.iterator(); + var seen: usize = 0; + while (mit.next()) |m| { + var nit = m.value_ptr.names.iterator(); + while (nit.next()) |kv| { + const ref = kv.value_ptr.*; + const id = table.declIdForRef(ref) orelse return error.MissingDeclId; + const info = table.get(id); + try std.testing.expectEqual(imports.authorNodePtrOf(ref), imports.authorNodePtrOf(info.ref)); + try std.testing.expectEqualStrings(kv.key_ptr.*, info.name); + try std.testing.expectEqualStrings(m.value_ptr.source, info.source); + seen += 1; + } + } + try std.testing.expect(seen > 0); + + // The generic struct `Box` (authored in lib.sx) is keyable by DeclId via its + // inner *StructDecl, and the id reports DeclKind.@"struct" + name "Box". + const lib_idx = facts.decls.get(lib_path) orelse return error.MissingLibIndex; + const box_ref = lib_idx.names.get("Box") orelse return error.MissingBox; + const box_sd = switch (box_ref) { + .struct_decl => |sd| sd, + .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else return error.BoxNotStruct, + else => return error.BoxNotStruct, + }; + const box_id = table.declIdForStructDecl(box_sd) orelse return error.BoxNoDeclId; + try std.testing.expectEqual(imports.DeclKind.@"struct", table.get(box_id).kind); + try std.testing.expectEqualStrings("Box", table.get(box_id).name); + + // The namespaced `geom.sx` target records its members' DeclIds (here: Point), + // each round-tripping to a DeclInfo named "Point". + const aliases = facts.ns_edges.get(main_path) orelse return error.MissingNsEdges; + const target = aliases.get("g") orelse return error.MissingAlias; + try std.testing.expect(target.member_ids.len >= 1); + var found_point = false; + for (target.member_ids) |id| { + if (std.mem.eql(u8, table.get(id).name, "Point")) found_point = true; + } + try std.testing.expect(found_point); +} diff --git a/src/imports.zig b/src/imports.zig index 71d334a..668da29 100644 --- a/src/imports.zig +++ b/src/imports.zig @@ -537,6 +537,10 @@ pub const NamespaceTarget = struct { target_module_path: []const u8, own_decls: []const *Node, is_pub: bool = false, + /// The `DeclId` of each member in `own_decls`, in slice order. Filled by + /// `buildDeclTable` (empty until then). Lets a member be addressed by stable + /// id without re-deriving it from the node pointer. + member_ids: []const DeclId = &.{}, }; /// `importer_source → alias → NamespaceTarget`. @@ -615,6 +619,216 @@ pub fn buildImportFacts( return .{ .decls = decls, .ns_edges = ns_edges }; } +// ── DeclTable: a stable DeclId for every declaration (Fork C S1, additive) ── +// +// `buildDeclTable` lifts every `RawDeclRef` the import facts hold into a stable +// `DeclId` carrying source + name + AST node identity + span + `DeclKind`. It is +// built in PARALLEL with the old maps and nothing in lowering consumes it for +// selection yet (S4 makes it the fact-store key), so generated IR + bytes are +// unchanged by construction. + +/// The taxonomy of a declaration, mirroring the `RawDeclRef` variants so a +/// `DeclTable` row carries its kind without re-switching on the AST node. +pub const DeclKind = enum { + function, + constant, + @"struct", + @"enum", + @"union", + error_set, + protocol, + foreign_class, + namespace, +}; + +fn declKindOf(ref: RawDeclRef) DeclKind { + return switch (ref) { + .fn_decl => .function, + .const_decl => .constant, + .struct_decl => .@"struct", + .enum_decl => .@"enum", + .union_decl => .@"union", + .error_set_decl => .error_set, + .protocol_decl => .protocol, + .foreign_class_decl => .foreign_class, + .namespace_decl => .namespace, + }; +} + +/// The AST node identity a `RawDeclRef` wraps — the inner decl pointer every +/// variant holds (the same identity `resolver.zig` selects authors by). This is +/// the key the `DeclTable` indexes and round-trips on. +pub fn authorNodePtrOf(ref: RawDeclRef) usize { + return switch (ref) { + inline else => |p| @intFromPtr(p), + }; +} + +/// The `*const ast.StructDecl` a top-level decl node carries, or null when it is +/// not a struct — a bare `struct_decl` or a `const_decl` whose value is one, +/// both unwrapping to the same inner decl (mirrors lower's `structDeclOfRaw`). +fn structDeclPtrOf(decl: *const Node) ?*const ast.StructDecl { + return switch (decl.data) { + .struct_decl => &decl.data.struct_decl, + .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null, + else => null, + }; +} + +/// A stable identifier for one declaration, assigned by `DeclTable` in module- +/// walk order. Process-local: it indexes the table's `entries` (S5 stabilizes it +/// to `(source, index)` for the LSP, per the deep-dive's R5). +pub const DeclId = enum(u32) { _ }; + +/// One `DeclTable` row: a `RawDeclRef` lifted to a stable `DeclId`, with its +/// authoring source path, display name, AST span, and `DeclKind`. `ref` is the +/// same raw author the import facts hold (its AST node identity is `id`'s key). +pub const DeclInfo = struct { + id: DeclId, + source: []const u8, + name: []const u8, + ref: RawDeclRef, + span: ast.Span, + kind: DeclKind, +}; + +/// Stable `DeclId` for every source / namespaced / imported / C-imported decl. +/// `entries` is indexed by `DeclId`; `by_node` reverse-maps the AST node +/// identity (`authorNodePtrOf`) to its id; `by_struct` maps a generic struct's +/// inner `*StructDecl` to its id (so a template registered during lowering can +/// be keyed by `DeclId`). Borrowed by `ProgramIndex.decl_table`. +pub const DeclTable = struct { + alloc: std.mem.Allocator, + entries: std.ArrayList(DeclInfo) = .empty, + by_node: std.AutoHashMap(usize, DeclId), + by_struct: std.AutoHashMap(usize, DeclId), + + pub fn init(alloc: std.mem.Allocator) DeclTable { + return .{ + .alloc = alloc, + .by_node = std.AutoHashMap(usize, DeclId).init(alloc), + .by_struct = std.AutoHashMap(usize, DeclId).init(alloc), + }; + } + + pub fn deinit(self: *DeclTable) void { + self.entries.deinit(self.alloc); + self.by_node.deinit(); + self.by_struct.deinit(); + } + + pub fn get(self: *const DeclTable, id: DeclId) DeclInfo { + return self.entries.items[@intFromEnum(id)]; + } + + /// The `DeclId` for an AST node (by its `RawDeclRef` identity), or null when + /// the node never entered the table. + pub fn declIdForRef(self: *const DeclTable, ref: RawDeclRef) ?DeclId { + return self.by_node.get(authorNodePtrOf(ref)); + } + + /// The `DeclId` for a generic struct template's inner `*StructDecl`, or null. + pub fn declIdForStructDecl(self: *const DeclTable, sd: *const ast.StructDecl) ?DeclId { + return self.by_struct.get(@intFromPtr(sd)); + } + + /// Intern one top-level decl node, returning its (possibly pre-existing) + /// `DeclId`. First-wins / diamond dedup by node identity, matching how the + /// scalar import facts dedup. The caller guarantees `rawDeclRefOf(decl)` is + /// non-null (so `declName` is too). + fn intern(self: *DeclTable, source: []const u8, decl: *const Node) !DeclId { + const ref = rawDeclRefOf(decl).?; + const key = authorNodePtrOf(ref); + if (self.by_node.get(key)) |existing| return existing; + const id: DeclId = @enumFromInt(@as(u32, @intCast(self.entries.items.len))); + try self.entries.append(self.alloc, .{ + .id = id, + .source = source, + .name = decl.data.declName().?, + .ref = ref, + .span = decl.span, + .kind = declKindOf(ref), + }); + try self.by_node.put(key, id); + if (structDeclPtrOf(decl)) |sd| try self.by_struct.put(@intFromPtr(sd), id); + return id; + } + + fn internModule(self: *DeclTable, source: []const u8, own_decls: []const *Node) !void { + for (own_decls) |decl| { + if (rawDeclRefOf(decl) == null) continue; + _ = try self.intern(source, decl); + } + } + + /// Debug cross-check (S1.1 acceptance): every `RawDeclRef` the import facts + /// hold round-trips `RawDeclRef → DeclId → AST node ptr` back to the same + /// node, with matching name. Asserts; call only under `builtin.mode == .Debug`. + pub fn verifyRoundTrip(self: *const DeclTable, decls: *const ModuleDecls, ns_edges: *const NamespaceEdges) void { + var mit = decls.iterator(); + while (mit.next()) |m| { + var nit = m.value_ptr.names.iterator(); + while (nit.next()) |kv| { + const ref = kv.value_ptr.*; + const id = self.declIdForRef(ref) orelse @panic("DeclTable round-trip: module ref has no DeclId"); + const info = self.get(id); + std.debug.assert(authorNodePtrOf(info.ref) == authorNodePtrOf(ref)); + std.debug.assert(std.mem.eql(u8, info.name, kv.key_ptr.*)); + } + } + var nsit = ns_edges.iterator(); + while (nsit.next()) |imp| { + var ait = imp.value_ptr.valueIterator(); + while (ait.next()) |target| { + for (target.own_decls) |decl| { + const ref = rawDeclRefOf(decl) orelse continue; + const id = self.declIdForRef(ref) orelse @panic("DeclTable round-trip: ns member has no DeclId"); + std.debug.assert(authorNodePtrOf(self.get(id).ref) == authorNodePtrOf(ref)); + } + } + } + } +}; + +/// Build the `DeclTable` from the resolved program + the import facts: every +/// module author (main + cache) interned first, then every namespace member +/// (reusing the module author's id when it is also a module decl, minting a new +/// id for a synthetic C-import member). `ns_edges` is updated in place so each +/// `NamespaceTarget.member_ids` lists its members' ids. Built from the SAME +/// modules `buildImportFacts` walks; no IR lowering required. +pub fn buildDeclTable( + allocator: std.mem.Allocator, + main_path: []const u8, + main_mod: ResolvedModule, + cache: *const ModuleCache, + decls: *const ModuleDecls, + ns_edges: *NamespaceEdges, +) !DeclTable { + var table = DeclTable.init(allocator); + try table.internModule(main_path, main_mod.own_decls); + var it = cache.iterator(); + while (it.next()) |entry| { + try table.internModule(entry.key_ptr.*, entry.value_ptr.own_decls); + } + + var nsit = ns_edges.iterator(); + while (nsit.next()) |imp| { + var ait = imp.value_ptr.valueIterator(); + while (ait.next()) |target| { + var ids = std.ArrayList(DeclId).empty; + for (target.own_decls) |decl| { + if (rawDeclRefOf(decl) == null) continue; + const id = try table.intern(target.target_module_path, decl); + try ids.append(allocator, id); + } + target.member_ids = try ids.toOwnedSlice(allocator); + } + } + + if (builtin.mode == .Debug) table.verifyRoundTrip(decls, ns_edges); + return table; +} + /// Surface a same-module duplicate top-level declaration as a hard error at an /// explicit name + span. `addOwnDecl` / `addNamespace` return `false` when the /// name is already in this module's scope and drop the second author; without diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4a54cf9..ec77257 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -15487,6 +15487,17 @@ pub const Lowering = struct { const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return; self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {}; + // S1.1 (additive): key the template by DeclId in parallel. Nothing + // reads this for selection yet; `struct_template_map` stays the live + // consumer. A template whose decl is not in the table (comptime / + // block-local registration with facts unwired) keeps only the + // name-keyed entry. + if (self.program_index.decl_table) |dt| { + if (dt.declIdForStructDecl(sd)) |id| { + self.program_index.struct_template_by_decl.put(id, tmpl) catch {}; + } + } + // Register methods under "TemplateName.method" in fn_ast_map for (sd.methods) |method_node| { if (method_node.data == .fn_decl) { diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 933e389..6734472 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -628,6 +628,10 @@ pub const ProgramIndex = struct { /// `imports.buildImportFacts`, carrying each alias's resolved target path. /// Borrowed view. namespace_edges: ?*imports.NamespaceEdges = null, + /// Stable `DeclId` for every declaration, built by `imports.buildDeclTable` + /// in parallel with the import facts. Borrowed view; nothing in lowering + /// consumes it for selection yet (additive — S4 makes it the fact-store key). + decl_table: ?*imports.DeclTable = null, // ── Declaration maps ── /// Function name → AST decl. @@ -649,6 +653,11 @@ pub const ProgramIndex = struct { type_alias_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), /// Generic struct name → template. struct_template_map: std.StringHashMap(StructTemplate) = std.StringHashMap(StructTemplate).init(std.heap.page_allocator), + /// `DeclId` → generic struct template — the DeclId-keyed analogue of + /// `struct_template_map`, built in parallel during `registerStructDecl`. + /// Nothing reads it for selection yet; `struct_template_map` stays the live + /// consumer until the S4 cutover. + struct_template_by_decl: std.AutoHashMap(imports.DeclId, StructTemplate) = std.AutoHashMap(imports.DeclId, StructTemplate).init(std.heap.page_allocator), /// Protocol name → protocol info. protocol_decl_map: std.StringHashMap(ProtocolDeclInfo) = std.StringHashMap(ProtocolDeclInfo).init(std.heap.page_allocator), /// Protocol name → AST node. @@ -687,7 +696,7 @@ pub const ProgramIndex = struct { pub fn deinit(self: *ProgramIndex) void { // Owned maps only — module_scopes / import_graph / flat_import_graph / - // module_fns / module_decls / namespace_edges are borrowed. + // module_fns / module_decls / namespace_edges / decl_table are borrowed. self.import_flags.deinit(); self.fn_ast_map.deinit(); self.qualified_fn_source.deinit(); @@ -695,6 +704,7 @@ pub const ProgramIndex = struct { self.global_names.deinit(); self.type_alias_map.deinit(); self.struct_template_map.deinit(); + self.struct_template_by_decl.deinit(); self.protocol_decl_map.deinit(); self.protocol_ast_map.deinit(); self.module_const_map.deinit();