wip(resolver): collectors + unified predicate + tightened adapters [stdlib B, BLOCKED on 0106]
Collectors (resolver.zig: collectVisibleAuthors/collectNamespaceAuthors + AuthorSet + VisibilityMode, 4 unit tests) + unified visibility predicate + isNameVisible/ isCImportVisible adapters routed to flat modes. Tightening surfaces issue 0106 (stdlib comptime expansion relies on the over-permissive import_graph join), so run_examples is 467/471 here. attempt-2 folds in the coupled comptime-context fix.
This commit is contained in:
178
src/ir/resolver.zig
Normal file
178
src/ir/resolver.zig
Normal file
@@ -0,0 +1,178 @@
|
||||
//! The unified sx name/type resolver — the shared author-collection layer.
|
||||
//!
|
||||
//! A read-only facade over the borrowed Phase A import facts on a
|
||||
//! `*ProgramIndex` (`module_decls` / `namespace_edges`) and the existing
|
||||
//! `import_graph` / `flat_import_graph` views. It OWNS nothing import-derived;
|
||||
//! those maps live in `imports.zig`/`core.zig` and are borrowed here, exactly
|
||||
//! like `module_fns`.
|
||||
//!
|
||||
//! Two collectors sit on top of these facts (R5 §1 #1):
|
||||
//! - `collectVisibleAuthors` — own author ∪ the flat-import edge walk. THE one
|
||||
//! graph-walk; the permanent flat-import F-series root.
|
||||
//! - `collectNamespaceAuthors` — a single already-selected namespace target's
|
||||
//! members. NO graph walk.
|
||||
//!
|
||||
//! Both are RAW and verdict-free: they return who authors a name, not which
|
||||
//! author wins. Per-domain selectors (Phase C+) decide eligibility. Nothing
|
||||
//! routes resolution through these collectors yet.
|
||||
//!
|
||||
//! Falsifiable invariant (R5 §1 #1): there is EXACTLY ONE iterator over
|
||||
//! `flat_import_graph`/`import_graph` in this file — inside
|
||||
//! `collectVisibleAuthors`. `collectNamespaceAuthors` iterates one
|
||||
//! `NamespaceTarget.own_decls` slice and touches no graph. This is what keeps
|
||||
//! 0102 (callable) and 0105 (type) the SAME cross-module edge-walk.
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("../ast.zig");
|
||||
const imports = @import("../imports.zig");
|
||||
const program_index = @import("program_index.zig");
|
||||
const ProgramIndex = program_index.ProgramIndex;
|
||||
|
||||
// ── Raw-fact aliases (defined in imports.zig by buildImportFacts, Phase A) ──
|
||||
pub const RawDeclRef = imports.RawDeclRef;
|
||||
pub const RawAuthor = imports.RawAuthor;
|
||||
pub const NamespaceTarget = imports.NamespaceTarget;
|
||||
|
||||
/// Author multiplicity for ONE name as seen from ONE querying module: the
|
||||
/// own-module author (tier-2) plus the distinct flat-import authors (tier-3),
|
||||
/// diamond-deduped by author identity. RAW — no verdict, no domain, no pick.
|
||||
pub const AuthorSet = struct {
|
||||
/// The author declared in the querying module itself, if any.
|
||||
own: ?RawAuthor,
|
||||
/// Distinct flat-import authors. Diamond imports of the SAME author (same
|
||||
/// AST node reached over two edges, e.g. a directory aggregate and one of
|
||||
/// its member files) collapse to a single entry. Always disjoint from `own`.
|
||||
flat: []const RawAuthor,
|
||||
|
||||
/// own + flat, counted by author identity. `flat` is already deduped and
|
||||
/// disjoint from `own`, so this is a plain sum.
|
||||
pub fn distinctCount(self: AuthorSet) usize {
|
||||
return (if (self.own != null) @as(usize, 1) else 0) + self.flat.len;
|
||||
}
|
||||
};
|
||||
|
||||
/// How a name's cross-module visibility is computed. The author collector and
|
||||
/// the lowering-side visibility predicate (`Lowering.isVisible`) both switch on
|
||||
/// this single vocabulary.
|
||||
pub const VisibilityMode = enum {
|
||||
/// own scope ∪ `flat_import_graph`. The PERMANENT core for bare-name lookup
|
||||
/// under flat imports (Agra constraint) — never a transitional path.
|
||||
user_bare_flat,
|
||||
/// `user_bare_flat` plus the foreign-C gate (today's `isCImportVisible`):
|
||||
/// only C-import `fn_decl`s without a `library_ref` are policed; everything
|
||||
/// else is unconditionally visible.
|
||||
c_import_bare,
|
||||
/// own scope ∪ the TRANSITIVE import relation (specs.md:793-801). Owned by
|
||||
/// `ProtocolResolver.findVisibleImpls`; the single-hop author collector
|
||||
/// never serves it.
|
||||
impl_transitive,
|
||||
/// Registration / lazy lowering: falls open (visible), emits no user
|
||||
/// diagnostic, performs no graph walk.
|
||||
lowering_internal,
|
||||
/// own scope ∪ `import_graph` (flat AND namespaced edges) — an
|
||||
/// over-permissive set. QUARANTINE: reserved for sites PROVEN to be internal
|
||||
/// scans, never a user-facing lookup. Deleted in Phase K.
|
||||
legacy_direct_any,
|
||||
};
|
||||
|
||||
/// Read-only facade over the borrowed import facts. `alloc` backs the
|
||||
/// `AuthorSet.flat` slices the collectors return (the caller owns + frees them).
|
||||
pub const Resolver = struct {
|
||||
index: *ProgramIndex,
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
pub fn init(index: *ProgramIndex, alloc: std.mem.Allocator) Resolver {
|
||||
return .{ .index = index, .alloc = alloc };
|
||||
}
|
||||
|
||||
/// THE single graph-walk in this file (falsifiable invariant, R5 §1 #1):
|
||||
/// the own author declared in `from` ∪ the flat-import authors reachable
|
||||
/// over the edge set `vis` chooses. RAW — selectors decide eligibility, not
|
||||
/// this. `from` is the querying module's source path.
|
||||
///
|
||||
/// Edge set by mode: `flat_import_graph` for `user_bare_flat`/
|
||||
/// `c_import_bare`; `import_graph` for the quarantined `legacy_direct_any`.
|
||||
/// `impl_transitive` (a transitive closure owned by `findVisibleImpls`) and
|
||||
/// `lowering_internal` (no graph walk) are not single-hop author walks —
|
||||
/// reaching them here is a wiring bug, so we trip loudly.
|
||||
pub fn collectVisibleAuthors(
|
||||
self: *Resolver,
|
||||
name: []const u8,
|
||||
from: []const u8,
|
||||
vis: VisibilityMode,
|
||||
) AuthorSet {
|
||||
const decls = self.index.module_decls orelse return .{ .own = null, .flat = &.{} };
|
||||
|
||||
const own: ?RawAuthor = blk: {
|
||||
const mod = decls.get(from) orelse break :blk null;
|
||||
const ref = mod.names.get(name) orelse break :blk null;
|
||||
break :blk .{ .raw = ref, .source = mod.source };
|
||||
};
|
||||
|
||||
const graph = (switch (vis) {
|
||||
.user_bare_flat, .c_import_bare => self.index.flat_import_graph,
|
||||
.legacy_direct_any => self.index.import_graph,
|
||||
// findVisibleImpls owns transitive visibility; lowering_internal
|
||||
// performs no graph walk. Neither selects a single-hop edge set.
|
||||
.impl_transitive, .lowering_internal => @panic(
|
||||
"collectVisibleAuthors: vis mode performs no single-hop author walk",
|
||||
),
|
||||
}) orelse return .{ .own = own, .flat = &.{} };
|
||||
|
||||
const direct = graph.get(from) orelse return .{ .own = own, .flat = &.{} };
|
||||
|
||||
var flat = std.ArrayList(RawAuthor).empty;
|
||||
var it = direct.iterator(); // ← the one graph iterator (invariant)
|
||||
while (it.next()) |kv| {
|
||||
const dep = decls.get(kv.key_ptr.*) orelse continue;
|
||||
const ref = dep.names.get(name) orelse continue;
|
||||
const cand = RawAuthor{ .raw = ref, .source = dep.source };
|
||||
if (sameAuthor(own, cand)) continue; // keep flat disjoint from own
|
||||
if (containsAuthor(flat.items, cand)) continue; // diamond dedup
|
||||
flat.append(self.alloc, cand) catch @panic("collectVisibleAuthors: OOM");
|
||||
}
|
||||
return .{
|
||||
.own = own,
|
||||
.flat = flat.toOwnedSlice(self.alloc) catch @panic("collectVisibleAuthors: OOM"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Container collector for ONE already-selected namespace target. Iterates
|
||||
/// the target's `own_decls` and touches NO import graph (R5 §1 #1). A
|
||||
/// namespace's `own_decls` is name-deduped, so a name has at most one author
|
||||
/// here — returned as `own`, sourced to the target's module path.
|
||||
pub fn collectNamespaceAuthors(
|
||||
self: *Resolver,
|
||||
target: NamespaceTarget,
|
||||
name: []const u8,
|
||||
) AuthorSet {
|
||||
_ = self;
|
||||
for (target.own_decls) |decl| {
|
||||
const dn = decl.data.declName() orelse continue;
|
||||
if (!std.mem.eql(u8, dn, name)) continue;
|
||||
const ref = imports.rawDeclRefOf(decl) orelse continue;
|
||||
return .{ .own = .{ .raw = ref, .source = target.target_module_path }, .flat = &.{} };
|
||||
}
|
||||
return .{ .own = null, .flat = &.{} };
|
||||
}
|
||||
};
|
||||
|
||||
/// Author identity is the AST node pointer the `RawDeclRef` wraps; every variant
|
||||
/// holds a pointer, so a single `inline else` extracts it.
|
||||
fn authorNodePtr(ref: RawDeclRef) usize {
|
||||
return switch (ref) {
|
||||
inline else => |p| @intFromPtr(p),
|
||||
};
|
||||
}
|
||||
|
||||
fn sameAuthor(a: ?RawAuthor, b: RawAuthor) bool {
|
||||
const aa = a orelse return false;
|
||||
return authorNodePtr(aa.raw) == authorNodePtr(b.raw);
|
||||
}
|
||||
|
||||
fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool {
|
||||
for (list) |x| {
|
||||
if (authorNodePtr(x.raw) == authorNodePtr(b.raw)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user