Removes the S2.x pre-pass and its 10 NodeRefTable maps — 1934 net lines deleted. The Resolver gains two lazy functions: resolveBare(name, from, domain) and resolveQualified(target, name), each returning ResolvedAuthors (verdict + author set). verdictOver and authoredAsDomainAnywhere move from ResolvePass to Resolver as private methods. All domain-predicate helpers (eligibleKind, structDeclOf, fnDeclOf, etc.) are promoted to pub. Test file trimmed from 1352 to 396 lines; old pre-pass population tests replaced by focused resolveBare / resolveQualified verdict tests. 540/540 regression tests pass. Zero behavior change.
421 lines
18 KiB
Zig
421 lines
18 KiB
Zig
//! 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.
|
||
//!
|
||
//! 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.
|
||
//!
|
||
//! Two lazy resolution functions build on the collectors:
|
||
//! - `resolveBare(name, from, domain)` — collect bare authors + compute verdict.
|
||
//! The caller owns the returned `ResolvedAuthors.set.flat` slice.
|
||
//! - `resolveQualified(target, name)` — collect namespace authors + compute verdict.
|
||
//! Returns no allocation (namespace sets are always single-module, flat=&.{}).
|
||
//!
|
||
//! Both are RAW-author collectors returning verdicts: they say WHO authors a name
|
||
//! and whether that author wins outright, is ambiguous, not-visible, etc.
|
||
//! Per-domain lowering (R2+) decides what to do with the verdict.
|
||
//!
|
||
//! 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,
|
||
};
|
||
|
||
/// The selection verdict computed above a reference's collected author set —
|
||
/// the own-wins / single-flat-visible / ≥2-ambiguous layer. Evaluated over the
|
||
/// DOMAIN-ELIGIBLE subset of the author set (`eligibleKind`), so a same-name
|
||
/// VALUE never decides a TYPE reference — the type-vs-value `domain_filtered`
|
||
/// outcome.
|
||
pub const Verdict = enum {
|
||
/// The querying module's OWN author is eligible — it wins outright,
|
||
/// regardless of how many same-name flat authors exist.
|
||
own_wins,
|
||
/// Exactly ONE eligible flat-visible author, no own — the byte-identical
|
||
/// single-author path.
|
||
single,
|
||
/// ≥2 distinct eligible flat-visible authors, no own — a genuine collision
|
||
/// the source cannot disambiguate (the LOUD diagnostic at R2+).
|
||
ambiguous,
|
||
/// No eligible author is flat-visible, but the name IS authored for this
|
||
/// domain in some module — reachable only over a namespace edge ⇒ a
|
||
/// not-visible leak.
|
||
not_visible,
|
||
/// Visible same-name author(s) exist but NONE is eligible for this domain
|
||
/// (e.g. a same-name VALUE for a TYPE reference), or the name is authored
|
||
/// for this domain nowhere. The caller disambiguates by checking
|
||
/// `set.distinctCount()`: 0 = undeclared / builtin / local; >0 = wrong domain.
|
||
domain_filtered,
|
||
};
|
||
|
||
/// A collected author set paired with the verdict computed over it.
|
||
/// `set.flat` is owned by the caller and must be freed when no longer needed
|
||
/// (pass `self.alloc` to the same allocator used by the `Resolver`).
|
||
/// `resolveQualified` always returns `flat = &.{}` (no allocation).
|
||
pub const ResolvedAuthors = struct {
|
||
set: AuthorSet,
|
||
verdict: Verdict,
|
||
};
|
||
|
||
/// The reference domains a verdict is computed over. Each carries its own
|
||
/// eligibility filter (`eligibleKind`), so the own-wins / ambiguity count
|
||
/// surveys only the authors that can actually decide THIS kind of reference.
|
||
pub const Domain = enum {
|
||
bare_type,
|
||
value_const,
|
||
callable,
|
||
generic_struct_head,
|
||
type_fn_head,
|
||
protocol_head,
|
||
foreign_class,
|
||
struct_const,
|
||
namespace_member,
|
||
ufcs,
|
||
};
|
||
|
||
/// Whether `raw` is an author ELIGIBLE to decide a reference in `domain`.
|
||
/// `field` is the accessed member name (struct-const domain only; ignored
|
||
/// elsewhere). Mirrors the per-kind author predicates the lowering selectors
|
||
/// gate on (`isNamedTypeKind`, `isPlainFreeFn`, `typeFnAuthor`, etc.).
|
||
pub fn eligibleKind(domain: Domain, raw: RawDeclRef, field: ?[]const u8) bool {
|
||
return switch (domain) {
|
||
.bare_type => switch (raw) {
|
||
.struct_decl, .enum_decl, .union_decl, .error_set_decl,
|
||
.protocol_decl, .foreign_class_decl => true,
|
||
else => false,
|
||
},
|
||
.value_const => raw == .const_decl,
|
||
.callable => if (fnDeclOf(raw)) |fd| isPlainFreeFnDecl(fd) else false,
|
||
.generic_struct_head => if (structDeclOf(raw)) |sd| sd.type_params.len > 0 else false,
|
||
.type_fn_head => if (fnDeclOf(raw)) |fd| fd.type_params.len > 0 else false,
|
||
.protocol_head => raw == .protocol_decl,
|
||
.foreign_class => raw == .foreign_class_decl,
|
||
.struct_const => structHasConstMember(raw, field orelse return false),
|
||
.namespace_member => true,
|
||
.ufcs => fnDeclOf(raw) != null,
|
||
};
|
||
}
|
||
|
||
// ── Domain-predicate helpers ─────────────────────────────────────────────────
|
||
|
||
/// The `*StructDecl` a raw author wraps (bare or `Name :: struct(...)` const),
|
||
/// or null when the author is not a struct.
|
||
pub fn structDeclOf(raw: RawDeclRef) ?*const ast.StructDecl {
|
||
return switch (raw) {
|
||
.struct_decl => |sd| sd,
|
||
.const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null,
|
||
else => null,
|
||
};
|
||
}
|
||
|
||
/// The `*FnDecl` a raw author wraps (bare or `Name :: fn(...)` const), or null
|
||
/// when the author is not a function.
|
||
pub fn fnDeclOf(raw: RawDeclRef) ?*const ast.FnDecl {
|
||
return switch (raw) {
|
||
.fn_decl => |fd| fd,
|
||
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
|
||
else => null,
|
||
};
|
||
}
|
||
|
||
/// A PLAIN free function — no type params, an ordinary (non-`#foreign`/
|
||
/// `#builtin`/`#compiler`) body — the only callable kind the bare-call verdict
|
||
/// counts.
|
||
pub fn isPlainFreeFnDecl(fd: *const ast.FnDecl) bool {
|
||
if (fd.type_params.len > 0) return false;
|
||
return switch (fd.body.data) {
|
||
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
||
else => true,
|
||
};
|
||
}
|
||
|
||
/// True when the raw author is a struct carrying a const member named `field`.
|
||
pub fn structHasConstMember(raw: RawDeclRef, field: []const u8) bool {
|
||
return switch (raw) {
|
||
.struct_decl => |sd| blk: {
|
||
for (sd.constants) |c| {
|
||
if (c.data == .const_decl and std.mem.eql(u8, c.data.const_decl.name, field))
|
||
break :blk true;
|
||
}
|
||
break :blk false;
|
||
},
|
||
else => false,
|
||
};
|
||
}
|
||
|
||
/// True when any author in the set (own or flat) is a struct with const member
|
||
/// `field`. Used by resolveStructConst to short-circuit empty sets.
|
||
pub fn authorSetHasStructConst(set: AuthorSet, field: []const u8) bool {
|
||
if (set.own) |a| if (structHasConstMember(a.raw, field)) return true;
|
||
for (set.flat) |a| if (structHasConstMember(a.raw, field)) return true;
|
||
return false;
|
||
}
|
||
|
||
/// Bin ONE raw author by the head kind(s) it can author: generic struct, type
|
||
/// function, or protocol. Used by lowering's parameterized-head classification.
|
||
pub fn classifyHeadKind(raw: RawDeclRef, gs: *bool, tf: *bool, pr: *bool) void {
|
||
switch (raw) {
|
||
.struct_decl => |sd| if (sd.type_params.len > 0) { gs.* = true; },
|
||
.fn_decl => |fd| if (fd.type_params.len > 0) { tf.* = true; },
|
||
.const_decl => |cd| switch (cd.value.data) {
|
||
.struct_decl => |*sd| if (sd.type_params.len > 0) { gs.* = true; },
|
||
.fn_decl => |*fd| if (fd.type_params.len > 0) { tf.* = true; },
|
||
else => {},
|
||
},
|
||
.protocol_decl => { pr.* = true; },
|
||
else => {},
|
||
}
|
||
}
|
||
|
||
/// True when the bare-type verdict selected a foreign-class author
|
||
/// unambiguously. Used by lowering to route to the foreign-class path.
|
||
pub fn foreignClassWinsType(set: AuthorSet, verdict: Verdict) bool {
|
||
return switch (verdict) {
|
||
.own_wins => if (set.own) |a| std.meta.activeTag(a.raw) == .foreign_class_decl else false,
|
||
.single => blk: {
|
||
var selected: ?RawAuthor = null;
|
||
for (set.flat) |a| {
|
||
if (!eligibleKind(.bare_type, a.raw, null)) continue;
|
||
if (selected != null) break :blk false;
|
||
selected = a;
|
||
}
|
||
const a = selected orelse break :blk false;
|
||
break :blk std.meta.activeTag(a.raw) == .foreign_class_decl;
|
||
},
|
||
.ambiguous, .not_visible, .domain_filtered => false,
|
||
};
|
||
}
|
||
|
||
// ── Resolver ─────────────────────────────────────────────────────────────────
|
||
|
||
/// 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`.
|
||
/// `impl_transitive` and `lowering_internal` 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,
|
||
.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;
|
||
if (containsAuthor(flat.items, cand)) continue;
|
||
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 = &.{} };
|
||
}
|
||
|
||
/// Collect bare-name authors for `name` as seen from `from`, filter by
|
||
/// `domain` eligibility, and compute the selection verdict. The caller owns
|
||
/// the returned `ResolvedAuthors.set.flat` slice (allocator = `self.alloc`).
|
||
///
|
||
/// Returns `.domain_filtered` with an empty set when `name` has no author
|
||
/// anywhere in the domain (builtin / local variable / undeclared name).
|
||
pub fn resolveBare(
|
||
self: *Resolver,
|
||
name: []const u8,
|
||
from: []const u8,
|
||
domain: Domain,
|
||
) ResolvedAuthors {
|
||
const set = self.collectVisibleAuthors(name, from, .user_bare_flat);
|
||
const verdict = self.verdictOver(domain, name, set, null);
|
||
return .{ .set = set, .verdict = verdict };
|
||
}
|
||
|
||
/// Collect namespace-qualified authors for `target.member` and compute the
|
||
/// verdict. Namespace resolution has no own-wins / ambiguity — the target is
|
||
/// already selected, so "found" is `.single` and "not found" is
|
||
/// `.domain_filtered`. Returns no allocation (`flat = &.{}`).
|
||
pub fn resolveQualified(
|
||
self: *Resolver,
|
||
target: NamespaceTarget,
|
||
name: []const u8,
|
||
) ResolvedAuthors {
|
||
const set = self.collectNamespaceAuthors(target, name);
|
||
const verdict: Verdict = if (set.own != null) .single else .domain_filtered;
|
||
return .{ .set = set, .verdict = verdict };
|
||
}
|
||
|
||
// ── Private helpers ───────────────────────────────────────────────────────
|
||
|
||
/// Compute the verdict over a collected author set for `domain`: own-wins
|
||
/// when the querying module's own author is eligible; ≥2 distinct eligible
|
||
/// flat authors → ambiguous; exactly one → single; none eligible but authored
|
||
/// for this domain anywhere (non-flat-visible) → not_visible; otherwise
|
||
/// domain_filtered.
|
||
fn verdictOver(
|
||
self: *Resolver,
|
||
domain: Domain,
|
||
name: []const u8,
|
||
set: AuthorSet,
|
||
field: ?[]const u8,
|
||
) Verdict {
|
||
if (set.own) |o| {
|
||
if (eligibleKind(domain, o.raw, field)) return .own_wins;
|
||
}
|
||
var n: usize = 0;
|
||
for (set.flat) |fa| {
|
||
if (eligibleKind(domain, fa.raw, field)) {
|
||
n += 1;
|
||
if (n >= 2) return .ambiguous;
|
||
}
|
||
}
|
||
if (n == 1) return .single;
|
||
if (self.authoredAsDomainAnywhere(domain, name, field)) return .not_visible;
|
||
return .domain_filtered;
|
||
}
|
||
|
||
/// True iff `name` is authored for `domain` in ANY module's raw facts —
|
||
/// the not-visible leak detector. Reached only with zero eligible
|
||
/// flat-visible authors, so a hit means the author is reachable only over a
|
||
/// namespace edge.
|
||
fn authoredAsDomainAnywhere(
|
||
self: *Resolver,
|
||
domain: Domain,
|
||
name: []const u8,
|
||
field: ?[]const u8,
|
||
) bool {
|
||
const decls = self.index.module_decls orelse return false;
|
||
var it = decls.valueIterator();
|
||
while (it.next()) |m| {
|
||
if (m.names.get(name)) |ref| {
|
||
if (eligibleKind(domain, ref, field)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// ── Private identity helpers ─────────────────────────────────────────────────
|
||
|
||
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;
|
||
}
|