feat(stdlib/S1.1): DeclId for every declaration — additive DeclTable [additive]

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).
This commit is contained in:
agra
2026-06-09 11:25:04 +03:00
parent 864a14e42b
commit 8058be2538
5 changed files with 348 additions and 1 deletions

View File

@@ -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);
}