742 lines
32 KiB
Zig
742 lines
32 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.
|
||
//!
|
||
//! 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,
|
||
};
|
||
|
||
/// 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` (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,
|
||
// 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;
|
||
}
|
||
|
||
// ── The owning resolution pass (Fork C S2.1a) ───────────────────────────────
|
||
//
|
||
// `resolve` turns this module from a raw author-collection FACADE into the
|
||
// OWNING pass: ONE exhaustive recursive walk of the resolved AST that populates a
|
||
// `ResolvedProgram` — node-keyed side tables binding each user spelling to its RAW
|
||
// author identity. ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old
|
||
// selectors, so this changes no generated byte. The walk switches over EVERY
|
||
// `ast.Node.Data` kind with NO `else` arm, so a newly added node kind is a compile
|
||
// error here rather than a silently unvisited subtree (it structurally cannot be
|
||
// "half-populated").
|
||
//
|
||
// S2.1a populates the three BARE-NAME domains (type / value-const / callable) via
|
||
// `collectVisibleAuthors`, and records generic-param references ($T, ..$Ts,
|
||
// $pack[i]) SYMBOLICALLY (template/pack ids, never TypeIds). The remaining seven
|
||
// tables are DECLARED but stay empty until S2.1b (namespace-qualified + the three
|
||
// head domains) and S2.1c (foreign-class / struct-const / UFCS) populate them.
|
||
|
||
/// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned
|
||
/// by the pass and indexing `ResolvedProgram.template_params`. Process-local.
|
||
pub const TemplateParamId = enum(u32) { _ };
|
||
|
||
/// A symbolic id for one enclosing type pack (`..$Ts`, referenced as `$Ts` /
|
||
/// `$Ts[i]`), assigned by the pass and indexing `ResolvedProgram.pack_params`.
|
||
pub const PackParamId = enum(u32) { _ };
|
||
|
||
/// One generic param, identified symbolically — NOT a TypeId (the concrete binding
|
||
/// is instantiation-time, owned by S2.2+).
|
||
pub const TemplateParamInfo = struct {
|
||
id: TemplateParamId,
|
||
name: []const u8,
|
||
/// The decl node that introduced the param (fn / struct / lambda / protocol /
|
||
/// impl) — the param's identity is its address, this is its scope owner.
|
||
owner: *const ast.Node,
|
||
/// `$N: u32` (value) vs `$T: Type` (type), read off the param's constraint.
|
||
is_value: bool,
|
||
};
|
||
|
||
/// One type pack, identified symbolically.
|
||
pub const PackParamInfo = struct {
|
||
id: PackParamId,
|
||
name: []const u8,
|
||
owner: *const ast.Node,
|
||
};
|
||
|
||
/// A reference to an enclosing pack — the whole pack (`$Ts`) or one element
|
||
/// (`$Ts[i]`). Symbolic.
|
||
pub const PackRef = struct {
|
||
id: PackParamId,
|
||
/// `$Ts[i]` literal index, or null for a whole-pack reference (`$Ts`).
|
||
index: ?u32 = null,
|
||
};
|
||
|
||
/// What ONE reference site resolves to — the S2.1 RAW form. `authors` carries the
|
||
/// collected author identity (own ∪ flat, diamond-deduped) with NO verdict:
|
||
/// own-wins / direct-flat ambiguity selection is S2.2. `template` / `pack` are
|
||
/// symbolic generic-param references.
|
||
pub const ResolvedRef = union(enum) {
|
||
authors: AuthorSet,
|
||
template: TemplateParamId,
|
||
pack: PackRef,
|
||
};
|
||
|
||
/// Node-keyed side table: an AST reference node → its `ResolvedRef`. Keyed by node
|
||
/// IDENTITY (the `*const ast.Node` pointer), so two textually-identical spellings
|
||
/// at different sites are distinct entries.
|
||
pub const NodeRefTable = std.AutoHashMap(*const ast.Node, ResolvedRef);
|
||
|
||
/// The output of the owning resolution pass: ten node-keyed side tables (one per
|
||
/// reference domain) plus the symbolic template/pack registries. OWNS every
|
||
/// allocation it holds — the maps, the registry lists, and each `AuthorSet.flat`
|
||
/// slice the collector returned — and frees them in `deinit`. Owned by
|
||
/// `Compilation`; borrowed by `ProgramIndex.resolved_program`.
|
||
pub const ResolvedProgram = struct {
|
||
alloc: std.mem.Allocator,
|
||
|
||
// ── bare-name domains (S2.1a populates these) ──
|
||
type_refs: NodeRefTable,
|
||
value_refs: NodeRefTable,
|
||
callable_refs: NodeRefTable,
|
||
// ── namespace-qualified + head domains (S2.1b) ──
|
||
namespace_refs: NodeRefTable,
|
||
generic_struct_heads: NodeRefTable,
|
||
type_fn_heads: NodeRefTable,
|
||
protocol_heads: NodeRefTable,
|
||
// ── foreign-class / struct-const / UFCS (S2.1c) ──
|
||
foreign_class_refs: NodeRefTable,
|
||
struct_const_refs: NodeRefTable,
|
||
ufcs_refs: NodeRefTable,
|
||
|
||
// ── symbolic generic-param registries ──
|
||
template_params: std.ArrayList(TemplateParamInfo) = .empty,
|
||
template_by_ptr: std.AutoHashMap(usize, TemplateParamId),
|
||
pack_params: std.ArrayList(PackParamInfo) = .empty,
|
||
pack_by_ptr: std.AutoHashMap(usize, PackParamId),
|
||
|
||
pub fn init(alloc: std.mem.Allocator) ResolvedProgram {
|
||
return .{
|
||
.alloc = alloc,
|
||
.type_refs = NodeRefTable.init(alloc),
|
||
.value_refs = NodeRefTable.init(alloc),
|
||
.callable_refs = NodeRefTable.init(alloc),
|
||
.namespace_refs = NodeRefTable.init(alloc),
|
||
.generic_struct_heads = NodeRefTable.init(alloc),
|
||
.type_fn_heads = NodeRefTable.init(alloc),
|
||
.protocol_heads = NodeRefTable.init(alloc),
|
||
.foreign_class_refs = NodeRefTable.init(alloc),
|
||
.struct_const_refs = NodeRefTable.init(alloc),
|
||
.ufcs_refs = NodeRefTable.init(alloc),
|
||
.template_by_ptr = std.AutoHashMap(usize, TemplateParamId).init(alloc),
|
||
.pack_by_ptr = std.AutoHashMap(usize, PackParamId).init(alloc),
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *ResolvedProgram) void {
|
||
for (self.allTables()) |t| {
|
||
var it = t.valueIterator();
|
||
while (it.next()) |ref| switch (ref.*) {
|
||
.authors => |a| if (a.flat.len > 0) self.alloc.free(a.flat),
|
||
.template, .pack => {},
|
||
};
|
||
t.deinit();
|
||
}
|
||
self.template_params.deinit(self.alloc);
|
||
self.template_by_ptr.deinit();
|
||
self.pack_params.deinit(self.alloc);
|
||
self.pack_by_ptr.deinit();
|
||
}
|
||
|
||
fn allTables(self: *ResolvedProgram) [10]*NodeRefTable {
|
||
return .{
|
||
&self.type_refs, &self.value_refs,
|
||
&self.callable_refs, &self.namespace_refs,
|
||
&self.generic_struct_heads, &self.type_fn_heads,
|
||
&self.protocol_heads, &self.foreign_class_refs,
|
||
&self.struct_const_refs, &self.ufcs_refs,
|
||
};
|
||
}
|
||
};
|
||
|
||
/// Run the owning resolution pass over `root` (the resolved program root), using
|
||
/// `index`'s borrowed import facts (`module_decls` / `flat_import_graph`) for
|
||
/// author collection. `main_file` is the ambient-source fallback for nodes that
|
||
/// carry no `source_file` stamp. Returns a fully-owned `ResolvedProgram` (the
|
||
/// caller stores it and calls `deinit`). One pass, no AST mutation, no diagnostics
|
||
/// — parallel/unconsumed, so generated output is unaffected.
|
||
pub fn resolve(
|
||
root: *const ast.Node,
|
||
index: *ProgramIndex,
|
||
main_file: []const u8,
|
||
alloc: std.mem.Allocator,
|
||
) ResolvedProgram {
|
||
var pass = ResolvePass{
|
||
.res = Resolver.init(index, alloc),
|
||
.out = ResolvedProgram.init(alloc),
|
||
};
|
||
pass.visit(root, .{ .source = main_file, .scope = null });
|
||
return pass.out;
|
||
}
|
||
|
||
/// One frame of generic params introduced by an enclosing decl (fn / struct /
|
||
/// lambda / protocol / impl). Lives on the Zig call stack (no allocation), chained
|
||
/// to its parent — a reference resolves a name against the NEAREST enclosing frame.
|
||
const Frame = struct {
|
||
params: []const ast.StructTypeParam,
|
||
owner: *const ast.Node,
|
||
parent: ?*const Frame,
|
||
};
|
||
|
||
/// Ambient walk context: the querying module's source path (`collectVisibleAuthors`'s
|
||
/// `from`) and the enclosing generic-param scope.
|
||
const Ctx = struct {
|
||
source: []const u8,
|
||
scope: ?*const Frame,
|
||
};
|
||
|
||
/// A resolved generic-param reference: the matched param (its address is its
|
||
/// identity) plus the scope owner that declared it.
|
||
const GenericMatch = struct {
|
||
param: *const ast.StructTypeParam,
|
||
owner: *const ast.Node,
|
||
};
|
||
|
||
/// `$N: u32` is a value param; `$T: Type` (or a variadic / non-type constraint) is
|
||
/// a type param. Read off the param's constraint type-expr name.
|
||
fn paramIsValue(p: ast.StructTypeParam) bool {
|
||
if (p.is_variadic) return false;
|
||
return switch (p.constraint.data) {
|
||
.type_expr => |te| !std.mem.eql(u8, te.name, "Type"),
|
||
else => false,
|
||
};
|
||
}
|
||
|
||
/// Nearest enclosing generic param named `name`, or null when the name is not a
|
||
/// generic in scope (→ it is an ordinary bare-name reference).
|
||
fn lookupGeneric(scope: ?*const Frame, name: []const u8) ?GenericMatch {
|
||
var cur = scope;
|
||
while (cur) |f| : (cur = f.parent) {
|
||
for (f.params) |*p| {
|
||
if (std.mem.eql(u8, p.name, name)) return .{ .param = p, .owner = f.owner };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// The single owning traversal. Holds the author collector + the `ResolvedProgram`
|
||
/// it populates; threads `Ctx` (ambient source + generic scope) down the tree.
|
||
const ResolvePass = struct {
|
||
res: Resolver,
|
||
out: ResolvedProgram,
|
||
|
||
/// Visit ONE node, then recurse into its children. A stamped
|
||
/// `node.source_file` (top-level decls, and cross-module fn bodies whose bare
|
||
/// names must resolve in their DEFINING module) overrides the ambient source
|
||
/// for this subtree; an unstamped node inherits its parent's.
|
||
fn visit(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||
const here = Ctx{ .source = node.source_file orelse ctx.source, .scope = ctx.scope };
|
||
switch (node.data) {
|
||
// ── declarations that open a generic-param scope ──
|
||
.fn_decl => |*fd| {
|
||
var frame = Frame{ .params = fd.type_params, .owner = node, .parent = here.scope };
|
||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||
self.visitTypeParamConstraints(fd.type_params, inner);
|
||
for (fd.params) |p| {
|
||
self.visit(p.type_expr, inner);
|
||
if (p.default_expr) |d| self.visit(d, inner);
|
||
}
|
||
if (fd.return_type) |rt| self.visit(rt, inner);
|
||
self.visit(fd.body, inner);
|
||
},
|
||
.lambda => |*l| {
|
||
var frame = Frame{ .params = l.type_params, .owner = node, .parent = here.scope };
|
||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||
self.visitTypeParamConstraints(l.type_params, inner);
|
||
for (l.params) |p| {
|
||
self.visit(p.type_expr, inner);
|
||
if (p.default_expr) |d| self.visit(d, inner);
|
||
}
|
||
if (l.return_type) |rt| self.visit(rt, inner);
|
||
self.visit(l.body, inner);
|
||
},
|
||
.struct_decl => |*sd| {
|
||
var frame = Frame{ .params = sd.type_params, .owner = node, .parent = here.scope };
|
||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||
self.visitTypeParamConstraints(sd.type_params, inner);
|
||
self.visitAll(sd.field_types, inner);
|
||
self.visitAllOpt(sd.field_defaults, inner);
|
||
self.visitAll(sd.methods, inner);
|
||
self.visitAll(sd.constants, inner);
|
||
},
|
||
.protocol_decl => |*pd| {
|
||
var frame = Frame{ .params = pd.type_params, .owner = node, .parent = here.scope };
|
||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||
self.visitTypeParamConstraints(pd.type_params, inner);
|
||
for (pd.methods) |m| {
|
||
self.visitAll(m.params, inner);
|
||
if (m.return_type) |rt| self.visit(rt, inner);
|
||
if (m.default_body) |b| self.visit(b, inner);
|
||
}
|
||
},
|
||
.impl_block => |*ib| {
|
||
var frame = Frame{ .params = ib.target_type_params, .owner = node, .parent = here.scope };
|
||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||
self.visitTypeParamConstraints(ib.target_type_params, inner);
|
||
if (ib.target_type_expr) |tt| self.visit(tt, inner);
|
||
self.visitAll(ib.protocol_type_args, inner);
|
||
self.visitAll(ib.methods, inner);
|
||
},
|
||
.foreign_class_decl => |*fc| {
|
||
for (fc.members) |m| switch (m) {
|
||
.method => |meth| {
|
||
self.visitAll(meth.params, here);
|
||
if (meth.return_type) |rt| self.visit(rt, here);
|
||
if (meth.body) |b| self.visit(b, here);
|
||
},
|
||
.field => |fld| self.visit(fld.field_type, here),
|
||
.extends, .implements => {},
|
||
};
|
||
},
|
||
|
||
// ── the three bare-name domains + symbolic generic refs ──
|
||
.type_expr => self.classifyType(node, here),
|
||
.identifier => self.classifyValue(node, here),
|
||
.call => |*c| {
|
||
if (c.callee.data == .identifier) {
|
||
// bare-name callable HEAD — recorded here, not re-walked as a
|
||
// value ref.
|
||
self.recordAuthors(&self.out.callable_refs, c.callee, c.callee.data.identifier.name, here.source);
|
||
} else {
|
||
self.visit(c.callee, here);
|
||
}
|
||
self.visitAll(c.args, here);
|
||
},
|
||
.field_access => |*fa| {
|
||
// namespace-qualified / struct-const / UFCS receivers are
|
||
// S2.1b/c — a BARE identifier receiver is left unclassified here;
|
||
// a compound receiver is recursed so its inner refs are collected.
|
||
if (fa.object.data != .identifier) self.visit(fa.object, here);
|
||
},
|
||
.pack_index_type_expr => |*p| self.recordPack(&self.out.type_refs, node, p.pack_name, p.index, here.scope),
|
||
.comptime_pack_ref => |*p| self.recordPack(&self.out.value_refs, node, p.pack_name, null, here.scope),
|
||
.error_type_expr => |*e| {
|
||
if (e.name) |name| self.recordAuthors(&self.out.type_refs, node, name, here.source);
|
||
},
|
||
.parameterized_type_expr => |*p| {
|
||
// the head (generic-struct / type-fn / protocol) is S2.1b; the
|
||
// type args are ordinary references, collected now.
|
||
self.visitAll(p.args, here);
|
||
},
|
||
|
||
// ── structural recursion (no classification of their own) ──
|
||
.root => |*r| {
|
||
// each top-level decl carries its own ambient source stamp.
|
||
self.visitAll(r.decls, here);
|
||
},
|
||
.block => |*b| self.visitAll(b.stmts, here),
|
||
.binary_op => |*b| {
|
||
self.visit(b.lhs, here);
|
||
self.visit(b.rhs, here);
|
||
},
|
||
.chained_comparison => |*c| self.visitAll(c.operands, here),
|
||
.unary_op => |*u| self.visit(u.operand, here),
|
||
.if_expr => |*e| {
|
||
self.visit(e.condition, here);
|
||
self.visit(e.then_branch, here);
|
||
if (e.else_branch) |b| self.visit(b, here);
|
||
},
|
||
.match_expr => |*e| {
|
||
self.visit(e.subject, here);
|
||
for (e.arms) |arm| {
|
||
if (arm.pattern) |pat| self.visit(pat, here);
|
||
self.visit(arm.body, here);
|
||
}
|
||
},
|
||
.match_arm => |*arm| {
|
||
if (arm.pattern) |pat| self.visit(pat, here);
|
||
self.visit(arm.body, here);
|
||
},
|
||
.const_decl => |*cd| {
|
||
if (cd.type_annotation) |ta| self.visit(ta, here);
|
||
self.visit(cd.value, here);
|
||
},
|
||
.var_decl => |*vd| {
|
||
if (vd.type_annotation) |ta| self.visit(ta, here);
|
||
if (vd.value) |v| self.visit(v, here);
|
||
},
|
||
.assignment => |*a| {
|
||
self.visit(a.target, here);
|
||
self.visit(a.value, here);
|
||
},
|
||
.multi_assign => |*m| {
|
||
self.visitAll(m.targets, here);
|
||
self.visitAll(m.values, here);
|
||
},
|
||
.destructure_decl => |*d| self.visit(d.value, here),
|
||
.enum_decl => |*ed| {
|
||
self.visitAllOpt(ed.variant_types, here);
|
||
self.visitAllOpt(ed.variant_values, here);
|
||
if (ed.backing_type) |bt| self.visit(bt, here);
|
||
},
|
||
.union_decl => |*ud| self.visitAll(ud.field_types, here),
|
||
.struct_literal => |*sl| {
|
||
if (sl.type_expr) |te| self.visit(te, here);
|
||
for (sl.field_inits) |fi| self.visit(fi.value, here);
|
||
if (sl.init_block) |ib| self.visit(ib, here);
|
||
},
|
||
.param => |*p| {
|
||
self.visit(p.type_expr, here);
|
||
if (p.default_expr) |d| self.visit(d, here);
|
||
},
|
||
.defer_stmt => |*d| self.visit(d.expr, here),
|
||
.push_stmt => |*p| {
|
||
self.visit(p.context_expr, here);
|
||
self.visit(p.body, here);
|
||
},
|
||
.comptime_expr => |*c| self.visit(c.expr, here),
|
||
.insert_expr => |*i| self.visit(i.expr, here),
|
||
.return_stmt => |*r| if (r.value) |v| self.visit(v, here),
|
||
.array_type_expr => |*a| {
|
||
self.visit(a.length, here);
|
||
self.visit(a.element_type, here);
|
||
},
|
||
.slice_type_expr => |*s| self.visit(s.element_type, here),
|
||
.array_literal => |*a| {
|
||
if (a.type_expr) |te| self.visit(te, here);
|
||
self.visitAll(a.elements, here);
|
||
},
|
||
.index_expr => |*i| {
|
||
self.visit(i.object, here);
|
||
self.visit(i.index, here);
|
||
},
|
||
.slice_expr => |*s| {
|
||
self.visit(s.object, here);
|
||
if (s.start) |st| self.visit(st, here);
|
||
if (s.end) |en| self.visit(en, here);
|
||
},
|
||
.pointer_type_expr => |*p| self.visit(p.pointee_type, here),
|
||
.many_pointer_type_expr => |*p| self.visit(p.element_type, here),
|
||
.optional_type_expr => |*o| self.visit(o.inner_type, here),
|
||
.raise_stmt => |*r| self.visit(r.tag, here),
|
||
.try_expr => |*t| self.visit(t.operand, here),
|
||
.catch_expr => |*c| {
|
||
self.visit(c.operand, here);
|
||
self.visit(c.body, here);
|
||
},
|
||
.onfail_stmt => |*o| self.visit(o.body, here),
|
||
.force_unwrap => |*f| self.visit(f.operand, here),
|
||
.null_coalesce => |*n| {
|
||
self.visit(n.lhs, here);
|
||
self.visit(n.rhs, here);
|
||
},
|
||
.deref_expr => |*d| self.visit(d.operand, here),
|
||
.while_expr => |*w| {
|
||
self.visit(w.condition, here);
|
||
self.visit(w.body, here);
|
||
},
|
||
.for_expr => |*f| {
|
||
self.visit(f.iterable, here);
|
||
if (f.range_end) |re| self.visit(re, here);
|
||
self.visit(f.body, here);
|
||
},
|
||
.spread_expr => |*s| self.visit(s.operand, here),
|
||
.function_type_expr => |*ft| {
|
||
self.visitAll(ft.param_types, here);
|
||
if (ft.return_type) |rt| self.visit(rt, here);
|
||
},
|
||
.closure_type_expr => |*ct| {
|
||
self.visitAll(ct.param_types, here);
|
||
if (ct.return_type) |rt| self.visit(rt, here);
|
||
},
|
||
.tuple_type_expr => |*tt| self.visitAll(tt.field_types, here),
|
||
.tuple_literal => |*tl| {
|
||
for (tl.elements) |el| self.visit(el.value, here);
|
||
},
|
||
.ffi_intrinsic_call => |*f| {
|
||
self.visit(f.return_type, here);
|
||
self.visitAll(f.args, here);
|
||
},
|
||
.jni_env_block => |*j| {
|
||
self.visit(j.env, here);
|
||
self.visit(j.body, here);
|
||
},
|
||
|
||
// ── leaves: no child node, no bare-name reference of their own ──
|
||
// `namespace_decl` is a leaf HERE: its members belong to another
|
||
// module and are reached via `collectNamespaceAuthors` in S2.1b, not
|
||
// re-walked through the importing root.
|
||
.int_literal,
|
||
.float_literal,
|
||
.bool_literal,
|
||
.string_literal,
|
||
.enum_literal,
|
||
.caller_location,
|
||
.null_literal,
|
||
.break_expr,
|
||
.continue_expr,
|
||
.undef_literal,
|
||
.inferred_type,
|
||
.builtin_expr,
|
||
.compiler_expr,
|
||
.import_decl,
|
||
.namespace_decl,
|
||
.error_set_decl,
|
||
.foreign_expr,
|
||
.library_decl,
|
||
.framework_decl,
|
||
.ufcs_alias,
|
||
.c_import_decl,
|
||
=> {},
|
||
}
|
||
}
|
||
|
||
fn visitAll(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
|
||
for (nodes) |n| self.visit(n, ctx);
|
||
}
|
||
|
||
fn visitAllOpt(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
|
||
for (nodes) |n| if (n) |nn| self.visit(nn, ctx);
|
||
}
|
||
|
||
fn visitTypeParamConstraints(self: *ResolvePass, params: []const ast.StructTypeParam, ctx: Ctx) void {
|
||
for (params) |p| self.visit(p.constraint, ctx);
|
||
}
|
||
|
||
/// A type-position reference: a generic param in scope → symbolic template ref;
|
||
/// otherwise a user type, collected RAW. Builtins / undeclared names collect to
|
||
/// an empty set and are simply not recorded.
|
||
fn classifyType(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||
const te = node.data.type_expr;
|
||
if (!te.is_raw) {
|
||
if (lookupGeneric(ctx.scope, te.name)) |m| {
|
||
self.recordTemplate(&self.out.type_refs, node, m);
|
||
return;
|
||
}
|
||
}
|
||
self.recordAuthors(&self.out.type_refs, node, te.name, ctx.source);
|
||
}
|
||
|
||
/// A value-position identifier: a generic value/type param in scope (shadowing)
|
||
/// → symbolic template ref; otherwise a module value/const, collected RAW.
|
||
fn classifyValue(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||
const id = node.data.identifier;
|
||
if (!id.is_raw) {
|
||
if (lookupGeneric(ctx.scope, id.name)) |m| {
|
||
self.recordTemplate(&self.out.value_refs, node, m);
|
||
return;
|
||
}
|
||
}
|
||
self.recordAuthors(&self.out.value_refs, node, id.name, ctx.source);
|
||
}
|
||
|
||
/// RAW author collection for a bare name. Only recorded when the name has ≥1
|
||
/// visible author (own or flat); a builtin / local / undeclared spelling has
|
||
/// none and is omitted — this is what keeps the tables to genuine authors.
|
||
fn recordAuthors(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, name: []const u8, from: []const u8) void {
|
||
const set = self.res.collectVisibleAuthors(name, from, .user_bare_flat);
|
||
if (set.distinctCount() == 0) return;
|
||
self.replaceRef(table, node, .{ .authors = set });
|
||
}
|
||
|
||
fn recordTemplate(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, m: GenericMatch) void {
|
||
self.replaceRef(table, node, .{ .template = self.internTemplate(m) });
|
||
}
|
||
|
||
fn recordPack(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, name: []const u8, index: ?u32, scope: ?*const Frame) void {
|
||
const m = lookupGeneric(scope, name) orelse return;
|
||
self.replaceRef(table, node, .{ .pack = .{ .id = self.internPack(m), .index = index } });
|
||
}
|
||
|
||
fn replaceRef(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, ref: ResolvedRef) void {
|
||
const entry = table.getOrPut(node) catch @panic("resolve: OOM");
|
||
if (entry.found_existing) self.releaseRef(entry.value_ptr.*);
|
||
entry.value_ptr.* = ref;
|
||
}
|
||
|
||
fn releaseRef(self: *ResolvePass, ref: ResolvedRef) void {
|
||
switch (ref) {
|
||
.authors => |a| if (a.flat.len > 0) self.out.alloc.free(a.flat),
|
||
.template, .pack => {},
|
||
}
|
||
}
|
||
|
||
fn internTemplate(self: *ResolvePass, m: GenericMatch) TemplateParamId {
|
||
const key = @intFromPtr(m.param);
|
||
if (self.out.template_by_ptr.get(key)) |id| return id;
|
||
const id: TemplateParamId = @enumFromInt(@as(u32, @intCast(self.out.template_params.items.len)));
|
||
self.out.template_params.append(self.out.alloc, .{
|
||
.id = id,
|
||
.name = m.param.name,
|
||
.owner = m.owner,
|
||
.is_value = paramIsValue(m.param.*),
|
||
}) catch @panic("resolve: OOM");
|
||
self.out.template_by_ptr.put(key, id) catch @panic("resolve: OOM");
|
||
return id;
|
||
}
|
||
|
||
fn internPack(self: *ResolvePass, m: GenericMatch) PackParamId {
|
||
const key = @intFromPtr(m.param);
|
||
if (self.out.pack_by_ptr.get(key)) |id| return id;
|
||
const id: PackParamId = @enumFromInt(@as(u32, @intCast(self.out.pack_params.items.len)));
|
||
self.out.pack_params.append(self.out.alloc, .{
|
||
.id = id,
|
||
.name = m.param.name,
|
||
.owner = m.owner,
|
||
}) catch @panic("resolve: OOM");
|
||
self.out.pack_by_ptr.put(key, id) catch @panic("resolve: OOM");
|
||
return id;
|
||
}
|
||
};
|