feat(stdlib/S2.1c): foreign-class + struct-const + UFCS — final 3 domains on the owning pass [additive]

On the S2.1a owning traversal, populate the last three ResolvedProgram side
tables, closing planspec S2.1's full-population acceptance (all ten domains):

  - foreign_class_refs: a bare reference whose collected author is a
    foreign_class_decl is routed here (its own domain) instead of the bare
    type/value/callable table.
  - struct_const_refs: a Type.CONST field access whose base resolves to a
    struct author carrying that const member (mirrors lowering's struct_const_map).
  - ufcs_refs: a ufcs_alias decl (alias -> target author) plus its UFCS-rewrite
    call sites (alias(args), incl. pipe-desugared), via a global traversal-ordered
    alias map mirroring lowering's flat ufcs_alias_map.

Still PARALLEL / UNCONSUMED / RAW: lowering reads the OLD selectors, no consumer
cut over, ResolvedRef stays raw. Byte-identical vs baseline (540 examples).

Population proof extended: a new resolver.test fixture exercises all ten domains
at once and asserts each side table non-empty + node-keyed for the three new ones.
This commit is contained in:
agra
2026-06-09 14:32:27 +03:00
parent 2301b60bb4
commit 59681e0a09
2 changed files with 263 additions and 20 deletions

View File

@@ -187,8 +187,12 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool {
// $pack[i]) SYMBOLICALLY (template/pack ids, never TypeIds). S2.1b adds the
// namespace-qualified table (`alias.member` resolved via `collectNamespaceAuthors`)
// and the three HEAD tables (generic-struct / type-fn / protocol), binned by the
// resolved author's decl kind at `parameterized_type_expr` heads. The remaining
// three tables (foreign-class / struct-const / UFCS) stay empty until S2.1c.
// resolved author's decl kind at `parameterized_type_expr` heads. S2.1c closes the
// set with the final three: a bare reference whose author is a `foreign_class_decl`
// is routed to `foreign_class_refs`; a `Type.CONST` field access whose base author
// is a struct carrying that const member fills `struct_const_refs`; and a UFCS
// alias (`alias :: ufcs target`) plus its rewrite call sites fill `ufcs_refs`. All
// ten domains are now populated — still PARALLEL / UNCONSUMED / RAW.
/// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned
/// by the pass and indexing `ResolvedProgram.template_params`. Process-local.
@@ -327,7 +331,9 @@ pub fn resolve(
var pass = ResolvePass{
.res = Resolver.init(index, alloc),
.out = ResolvedProgram.init(alloc),
.ufcs_aliases = std.StringHashMap([]const u8).init(alloc),
};
defer pass.ufcs_aliases.deinit();
pass.visit(root, .{ .source = main_file, .scope = null });
return pass.out;
}
@@ -405,11 +411,56 @@ fn classifyHeadKind(raw: RawDeclRef, gs: *bool, tf: *bool, pr: *bool) void {
}
}
/// True when an author set resolves to a `foreign_class_decl` — the own author
/// decides when present, else any flat author. Such a reference is routed to
/// `foreign_class_refs` (its own domain) instead of the bare type/value table.
fn authorSetIsForeignClass(set: AuthorSet) bool {
if (set.own) |a| return std.meta.activeTag(a.raw) == .foreign_class_decl;
for (set.flat) |a| {
if (std.meta.activeTag(a.raw) == .foreign_class_decl) return true;
}
return false;
}
/// A struct author carrying a `const_decl` member named `field` — the RAW shape
/// `Type.CONST` field access resolves to. Mirrors the lowering `struct_const_map`
/// domain, which is struct-level constants only; enums / other decls carry no
/// const members, so only `struct_decl` matches.
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,
};
}
/// Any author in the set (own or flat) is a struct with a const member `field`.
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;
}
/// 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,
/// `alias name → target name` for every `ufcs_alias` seen so far on the walk.
/// Global (not block-scoped) and populated in traversal order, mirroring
/// lowering's flat `ufcs_alias_map`; lets a UFCS-rewrite call site resolve to
/// the alias target's author. Scratch — freed when `resolve` returns, NOT part
/// of the owned `ResolvedProgram`.
ufcs_aliases: std.StringHashMap([]const u8),
/// Visit ONE node, then recurse into its children. A stamped
/// `node.source_file` (top-level decls, and cross-module fn bodies whose bare
@@ -485,23 +536,35 @@ const ResolvePass = struct {
.identifier => self.classifyValue(node, here),
.call => |*c| {
if (c.callee.data == .identifier) {
// bare-name callable HEAD — recorded here, not re-walked as a
const cname = c.callee.data.identifier.name;
// a UFCS-alias callee (`alias(args)`, incl. the parser's
// pipe-desugared `x |> alias()`) resolves to the alias TARGET's
// author → ufcs_refs (S2.1c). Any other bare callee is an
// ordinary 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);
if (self.ufcs_aliases.get(cname)) |target| {
self.recordAuthorsInto(&self.out.ufcs_refs, c.callee, target, here.source);
} else {
self.recordAuthors(&self.out.callable_refs, c.callee, cname, here.source);
}
} else {
self.visit(c.callee, here);
}
self.visitAll(c.args, here);
},
.field_access => |*fa| {
// `alias.member` whose base alias is a namespace import edge of the
// `alias.member` whose base is a namespace import edge of the
// ambient source resolves via `collectNamespaceAuthors` into the
// namespace-qualified table (S2.1b). A non-alias bare receiver
// (struct-const / UFCS / local value) stays unclassified — S2.1c
// and is not walked as a value ref; a compound receiver is recursed
// so its inner refs are collected.
// namespace-qualified table (S2.1b). Otherwise `Type.CONST` — a base
// resolving to a struct author that carries the named const member
// fills `struct_const_refs` (S2.1c). A receiver that is neither (a
// local value / instance field access) records nothing and is not
// walked as a value ref; a compound receiver is recursed so its
// inner refs are collected.
if (fa.object.data == .identifier) {
self.classifyNamespaceQualified(node, fa.object.data.identifier.name, fa.field, here.source);
const base = fa.object.data.identifier.name;
if (!self.classifyNamespaceQualified(node, base, fa.field, here.source))
_ = self.classifyStructConst(node, base, fa.field, here.source);
} else {
self.visit(fa.object, here);
}
@@ -675,9 +738,17 @@ const ResolvePass = struct {
.foreign_expr,
.library_decl,
.framework_decl,
.ufcs_alias,
.c_import_decl,
=> {},
// `alias :: ufcs target` — record the alias→target binding (the
// target's RAW author) keyed by the decl node, and remember the alias
// name so its rewrite call sites resolve to the same target. The map is
// global / traversal-ordered, mirroring lowering's flat `ufcs_alias_map`.
.ufcs_alias => |ua| {
self.ufcs_aliases.put(ua.name, ua.target) catch @panic("resolve: OOM");
self.recordAuthorsInto(&self.out.ufcs_refs, node, ua.target, here.source);
},
}
}
@@ -722,8 +793,20 @@ const ResolvePass = struct {
/// 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.
/// none and is omitted — this is what keeps the tables to genuine authors. A
/// name whose author is a `foreign_class_decl` is routed to `foreign_class_refs`
/// (its own S2.1c domain) instead of the passed bare type/value/callable table.
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;
const dest = if (authorSetIsForeignClass(set)) &self.out.foreign_class_refs else table;
self.replaceRef(dest, node, .{ .authors = set });
}
/// RAW author collection into an explicit table, with NO foreign-class routing —
/// the destination domain is already chosen by the caller (UFCS rewrite sites
/// and alias decls, whose target is always a free function).
fn recordAuthorsInto(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 });
@@ -731,16 +814,34 @@ const ResolvePass = struct {
/// `alias.member`: when `alias` is a namespace import edge of `from`, resolve
/// `member` against that already-selected target via `collectNamespaceAuthors`
/// (NO graph walk) and record it into the namespace-qualified table. A base that
/// is not a namespace alias (struct-const / UFCS / local value — S2.1c) records
/// nothing here.
fn classifyNamespaceQualified(self: *ResolvePass, node: *const ast.Node, alias: []const u8, member: []const u8, from: []const u8) void {
const edges = self.res.index.namespace_edges orelse return;
const aliases = edges.get(from) orelse return;
const target = aliases.get(alias) orelse return;
/// (NO graph walk) and record it into the namespace-qualified table. Returns
/// whether it recorded — a base that is not a namespace alias records nothing
/// here and lets the caller try the struct-const path.
fn classifyNamespaceQualified(self: *ResolvePass, node: *const ast.Node, alias: []const u8, member: []const u8, from: []const u8) bool {
const edges = self.res.index.namespace_edges orelse return false;
const aliases = edges.get(from) orelse return false;
const target = aliases.get(alias) orelse return false;
const set = self.res.collectNamespaceAuthors(target, member);
if (set.distinctCount() == 0) return;
if (set.distinctCount() == 0) return false;
self.replaceRef(&self.out.namespace_refs, node, .{ .authors = set });
return true;
}
/// `Type.CONST`: when `base` resolves to a struct author carrying a const member
/// named `member`, record the base's RAW author set into `struct_const_refs`
/// (keyed by the field-access node) — the owning-type identity the constant
/// lives on. A base with authors but no matching const member, or no author at
/// all (a local value receiver), records nothing and releases its allocation.
/// Returns whether it recorded.
fn classifyStructConst(self: *ResolvePass, node: *const ast.Node, base: []const u8, member: []const u8, from: []const u8) bool {
const set = self.res.collectVisibleAuthors(base, from, .user_bare_flat);
if (set.distinctCount() == 0) return false;
if (!authorSetHasStructConst(set, member)) {
if (set.flat.len > 0) self.out.alloc.free(set.flat);
return false;
}
self.replaceRef(&self.out.struct_const_refs, node, .{ .authors = set });
return true;
}
/// A parameterized head (`Name(args)`) binned by its resolved author's decl