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:
agra
2026-06-07 04:52:56 +03:00
parent 35457cb614
commit 7158337c73
6 changed files with 668 additions and 35 deletions

178
src/ir/resolver.zig Normal file
View 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;
}