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

@@ -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