feat(imports): buildImportFacts raw-fact store (ModuleRawDeclIndex + NamespaceEdges) [stdlib A]
Phase A of the unified resolver (R5 locked design). Additive infrastructure
with NO behavior change — builds the import-side raw-fact store; nothing
consumes it yet.
- imports.zig: add RawDeclRef / RawAuthor / ModuleRawDeclIndex / ModuleDecls /
NamespaceTarget / NamespaceEdges, plus buildImportFacts (mirrors
buildModuleFns) producing a scalar per-module name→RawDeclRef index + the
namespace edges. Callable without IR lowering (LSP reuses it later).
- ast.zig: NamespaceDecl gains target_module_path, captured at resolution time
(the resolved_path otherwise lost on the node) so the namespace edge records
the alias target.
- imports.zig: same-module duplicate top-level name is now DIAGNOSED
("duplicate top-level declaration 'X'") where addOwnDecl would silently drop
the second author — replaces the discarded `_ =` at the three call sites.
- program_index.zig: borrowed views module_decls / namespace_edges (like
module_fns); deinit does not free them.
- core.zig: build the facts alongside buildModuleFns and point the borrowed
views at them.
- imports.test.zig: index unit tests (flat / directory / namespaced file /
namespaced directory / C-import namespace / same-name fn / same-name struct /
value-vs-type same spelling / raw const_decl) + the duplicate-name diagnostic
regression (fails pre-fix, passes after).
Gate (worktree): zig build, zig build test (incl. LSP corpus sweep), and
run_examples (471, byte-identical) all green; m3te ios-sim build exits 0.
This commit is contained in:
149
src/imports.zig
149
src/imports.zig
@@ -378,6 +378,9 @@ pub const ResolvedModule = struct {
|
||||
// to (issue 0100). `decls` stays the full transitive list so
|
||||
// the lowering pass can still resolve transitive callees.
|
||||
.own_decls = other.own_decls,
|
||||
// The aliased module's resolved path (== the `resolved_path`
|
||||
// computed for this import). Retained for `buildImportFacts`.
|
||||
.target_module_path = other.path,
|
||||
// Carry the backtick raw escape from the `name :: #import …`
|
||||
// form so a reserved-name namespace is exempt from the decl
|
||||
// check, symmetric to every other decl site (issue 0089).
|
||||
@@ -441,6 +444,143 @@ pub fn buildModuleFns(allocator: std.mem.Allocator, main_path: []const u8, main_
|
||||
}
|
||||
}
|
||||
|
||||
// ── Raw import facts (the unified-resolver store; nothing consumes it yet) ──
|
||||
//
|
||||
// `buildImportFacts` produces two source-keyed views over the resolved program,
|
||||
// callable WITHOUT IR lowering (the LSP reuses it later): a scalar per-module
|
||||
// raw-decl index (`name → RawDeclRef`) and the namespace import edges
|
||||
// (`importer → alias → NamespaceTarget`). Both are built from each module's
|
||||
// `own_decls` — exactly the same modules `buildModuleFns` walks.
|
||||
|
||||
/// A named top-level declaration the resolver may select, kept as the raw AST
|
||||
/// node pointer (NOT pre-classified — a `const_decl` whose value is a function
|
||||
/// stays a `.const_decl`; classification is a later phase's job). `impl_block`
|
||||
/// is deliberately absent: it has no `declName` and is deduped by node identity
|
||||
/// (`mergeFlat`), so it never enters the scalar index.
|
||||
pub const RawDeclRef = union(enum) {
|
||||
fn_decl: *const ast.FnDecl,
|
||||
const_decl: *const ast.ConstDecl,
|
||||
struct_decl: *const ast.StructDecl,
|
||||
enum_decl: *const ast.EnumDecl,
|
||||
union_decl: *const ast.UnionDecl,
|
||||
error_set_decl: *const ast.ErrorSetDecl,
|
||||
protocol_decl: *const ast.ProtocolDecl,
|
||||
foreign_class_decl: *const ast.ForeignClassDecl,
|
||||
namespace_decl: *const ast.NamespaceDecl,
|
||||
};
|
||||
|
||||
/// A raw declaration paired with the source file that authors it.
|
||||
pub const RawAuthor = struct { raw: RawDeclRef, source: []const u8 };
|
||||
|
||||
/// One module's scalar raw-decl index: `name → ONE RawDeclRef`. Scalar because
|
||||
/// `addOwnDecl` refuses a same-module same-name second author (returns false),
|
||||
/// so a module's `own_decls` carries at most one author per name. Cross-module
|
||||
/// multiplicity lives one level up, keyed by path in `ModuleDecls`.
|
||||
pub const ModuleRawDeclIndex = struct { source: []const u8, names: std.StringHashMap(RawDeclRef) };
|
||||
|
||||
/// `path → ModuleRawDeclIndex`. Two modules each authoring `f` are retained
|
||||
/// under their own paths — the cross-module same-name authors the unified
|
||||
/// resolver's collector will surface.
|
||||
pub const ModuleDecls = std.StringHashMap(ModuleRawDeclIndex);
|
||||
|
||||
/// One namespace import edge: `alias :: #import "…"` (or `alias :: #import c …`).
|
||||
/// `target_module_path` is captured at resolution time (otherwise lost — it is
|
||||
/// not derivable from the namespace node alone). `is_pub` stays false until the
|
||||
/// `pub`-import phase lands the front-end form.
|
||||
pub const NamespaceTarget = struct {
|
||||
alias: []const u8,
|
||||
importer_source: []const u8,
|
||||
target_module_path: []const u8,
|
||||
own_decls: []const *Node,
|
||||
is_pub: bool = false,
|
||||
};
|
||||
|
||||
/// `importer_source → alias → NamespaceTarget`.
|
||||
pub const NamespaceEdges = std.StringHashMap(std.StringHashMap(NamespaceTarget));
|
||||
|
||||
/// The `RawDeclRef` a top-level node carries, or null when the node is not a
|
||||
/// selectable named declaration (e.g. `impl_block`, `var_decl`, `ufcs_alias`,
|
||||
/// a flat `c_import_decl`).
|
||||
fn rawDeclRefOf(decl: *const Node) ?RawDeclRef {
|
||||
return switch (decl.data) {
|
||||
.fn_decl => |*d| .{ .fn_decl = d },
|
||||
.const_decl => |*d| .{ .const_decl = d },
|
||||
.struct_decl => |*d| .{ .struct_decl = d },
|
||||
.enum_decl => |*d| .{ .enum_decl = d },
|
||||
.union_decl => |*d| .{ .union_decl = d },
|
||||
.error_set_decl => |*d| .{ .error_set_decl = d },
|
||||
.protocol_decl => |*d| .{ .protocol_decl = d },
|
||||
.foreign_class_decl => |*d| .{ .foreign_class_decl = d },
|
||||
.namespace_decl => |*d| .{ .namespace_decl = d },
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Index one module's authored decls (`own_decls`) into `decls[path]` and record
|
||||
/// any namespace aliases into `ns_edges[path]`. First-wins WITHIN a module
|
||||
/// mirrors `indexModuleFns`; `own_decls` is already name-deduped by `addOwnDecl`,
|
||||
/// so the first-wins guard never actually fires here.
|
||||
fn indexModuleDecls(
|
||||
allocator: std.mem.Allocator,
|
||||
decls: *ModuleDecls,
|
||||
ns_edges: *NamespaceEdges,
|
||||
path: []const u8,
|
||||
own_decls: []const *Node,
|
||||
) !void {
|
||||
const gop = try decls.getOrPut(path);
|
||||
if (!gop.found_existing) gop.value_ptr.* = .{ .source = path, .names = std.StringHashMap(RawDeclRef).init(allocator) };
|
||||
const index = gop.value_ptr;
|
||||
for (own_decls) |decl| {
|
||||
const ref = rawDeclRefOf(decl) orelse continue;
|
||||
const name = decl.data.declName() orelse continue;
|
||||
const name_gop = try index.names.getOrPut(name);
|
||||
if (!name_gop.found_existing) name_gop.value_ptr.* = ref;
|
||||
|
||||
if (decl.data == .namespace_decl) {
|
||||
const ns = &decl.data.namespace_decl;
|
||||
const edge_gop = try ns_edges.getOrPut(path);
|
||||
if (!edge_gop.found_existing) edge_gop.value_ptr.* = std.StringHashMap(NamespaceTarget).init(allocator);
|
||||
const tgt_gop = try edge_gop.value_ptr.getOrPut(ns.name);
|
||||
if (!tgt_gop.found_existing) tgt_gop.value_ptr.* = .{
|
||||
.alias = ns.name,
|
||||
.importer_source = path,
|
||||
.target_module_path = ns.target_module_path,
|
||||
.own_decls = ns.own_decls,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the raw import facts from a resolved program: the main module (keyed by
|
||||
/// `main_path`) plus every cached module (keyed by its own path). The same module
|
||||
/// set `buildModuleFns` walks. No IR lowering required.
|
||||
pub fn buildImportFacts(
|
||||
allocator: std.mem.Allocator,
|
||||
main_path: []const u8,
|
||||
main_mod: ResolvedModule,
|
||||
cache: *const ModuleCache,
|
||||
) !struct { decls: ModuleDecls, ns_edges: NamespaceEdges } {
|
||||
var decls = ModuleDecls.init(allocator);
|
||||
var ns_edges = NamespaceEdges.init(allocator);
|
||||
try indexModuleDecls(allocator, &decls, &ns_edges, main_path, main_mod.own_decls);
|
||||
var it = cache.iterator();
|
||||
while (it.next()) |entry| {
|
||||
try indexModuleDecls(allocator, &decls, &ns_edges, entry.key_ptr.*, entry.value_ptr.own_decls);
|
||||
}
|
||||
return .{ .decls = decls, .ns_edges = ns_edges };
|
||||
}
|
||||
|
||||
/// Surface a same-module duplicate top-level declaration as a hard error.
|
||||
/// `addOwnDecl` returns `false` when the name is already in this module's scope
|
||||
/// and drops the second author; without this the drop is silent, and the scalar
|
||||
/// `ModuleRawDeclIndex` would lose an authored name with no diagnostic.
|
||||
fn reportDuplicateDecl(diagnostics: ?*errors.DiagnosticList, added: bool, decl: *const Node) void {
|
||||
if (added) return;
|
||||
const diags = diagnostics orelse return;
|
||||
const name = decl.data.declName() orelse return;
|
||||
diags.addFmt(.err, decl.span, "duplicate top-level declaration '{s}'", .{name});
|
||||
}
|
||||
|
||||
pub fn resolveImports(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
@@ -559,6 +699,9 @@ pub fn resolveImports(
|
||||
// A C-import namespace authors exactly the wrapped fn
|
||||
// decls — they ARE its own decls (issue 0100).
|
||||
.own_decls = ns_slice,
|
||||
// No separate sx module: the synthesized members are
|
||||
// authored in THIS file. Record the importer's path.
|
||||
.target_module_path = file_path,
|
||||
.is_raw = ci.is_raw,
|
||||
} },
|
||||
};
|
||||
@@ -571,16 +714,16 @@ pub fn resolveImports(
|
||||
// Flat: add fn_decls directly + keep c_import_decl
|
||||
for (result.fn_decls) |fd| {
|
||||
fd.source_file = file_path;
|
||||
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd);
|
||||
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd), fd);
|
||||
}
|
||||
decl.source_file = file_path;
|
||||
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
|
||||
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl), decl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (decl.data != .import_decl) {
|
||||
decl.source_file = file_path;
|
||||
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
|
||||
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl), decl);
|
||||
continue;
|
||||
}
|
||||
const imp = decl.data.import_decl;
|
||||
|
||||
Reference in New Issue
Block a user