feat(stdlib/S2.1b): namespace-qualified + 3 head domains on the owning pass [additive]

On the S2.1a exhaustive traversal, populate four more ResolvedProgram side
tables, still RAW / PARALLEL / UNCONSUMED:

- namespace-qualified references: an `alias.member` field_access whose base
  alias is a NamespaceEdges[ambient_source] target resolves via
  collectNamespaceAuthors into namespace_refs, keyed by the access node.
- the three HEAD domains at parameterized_type_expr heads, binned by the
  resolved author's decl kind: a struct with type params -> generic_struct_heads,
  a fn/const-wrapped fn with type params -> type_fn_heads, a protocol ->
  protocol_heads. RAW: the whole author set is recorded with no winner picked;
  a name authored as >1 head kind lands a distinct entry in every matching table.

Lowering still reads the old selectors and resolved_program has no consumer, so
generated output is byte-identical. ResolvedRef stays RAW (selection is S2.2);
generics stay symbolic. S2.1c (foreign-class / struct-const / UFCS) owns the
remaining three tables.

Extends the population proof: a resolver unit test asserting all four tables are
non-empty + node-keyed with the expected RAW authors.

Gate (all exit 0): zig build; zig build test (All 427 mod + exe + LSP sweep 574);
tests/run_examples.sh (540 passed, byte-identical); tests/resolver-target
(18 xfail, 0 leaked); m3te ios-sim via the main sx binary.
This commit is contained in:
agra
2026-06-09 13:38:59 +03:00
parent 51ab730b74
commit 6b41d113f2
2 changed files with 245 additions and 10 deletions

View File

@@ -184,9 +184,11 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool {
//
// 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.
// $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.
/// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned
/// by the pass and indexing `ResolvedProgram.template_params`. Process-local.
@@ -375,6 +377,34 @@ fn lookupGeneric(scope: ?*const Frame, name: []const u8) ?GenericMatch {
return null;
}
/// Bin ONE raw author by the head kind it can author: a struct with type params (a
/// generic-struct head), a fn / const-wrapped fn with type params (a type-function
/// head), or a protocol. The `type_params.len > 0` gate is the head test — a
/// non-generic struct or a zero-type-param fn authors no head kind and sets
/// nothing. The `const_decl` arm unwraps a `Name :: struct/fn(...)` const exactly
/// as `structDeclOfRaw` / `fnDeclOfRaw` do.
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 => {},
}
}
/// The single owning traversal. Holds the author collector + the `ResolvedProgram`
/// it populates; threads `Ctx` (ambient source + generic scope) down the tree.
const ResolvePass = struct {
@@ -464,10 +494,17 @@ const ResolvePass = struct {
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);
// `alias.member` whose base alias 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.
if (fa.object.data == .identifier) {
self.classifyNamespaceQualified(node, fa.object.data.identifier.name, fa.field, here.source);
} else {
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),
@@ -475,8 +512,10 @@ const ResolvePass = struct {
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.
// the head (`Name(args)`) is binned by its resolved author's decl
// kind into the generic-struct / type-fn / protocol head tables
// (S2.1b); the type args are ordinary references, collected now.
self.classifyHead(node, p.name, p.is_raw, here);
self.visitAll(p.args, here);
},
@@ -690,6 +729,83 @@ const ResolvePass = struct {
self.replaceRef(table, node, .{ .authors = set });
}
/// `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;
const set = self.res.collectNamespaceAuthors(target, member);
if (set.distinctCount() == 0) return;
self.replaceRef(&self.out.namespace_refs, node, .{ .authors = set });
}
/// A parameterized head (`Name(args)`) binned by its resolved author's decl
/// kind: a generic struct (struct with type params) → `generic_struct_heads`;
/// a type-function (fn / const-wrapped fn with type params) → `type_fn_heads`; a
/// protocol → `protocol_heads`. RAW — the whole author set is recorded with no
/// winner picked, so a name authored as more than one head kind across modules
/// lands a distinct entry in every matching table. A head naming a generic param
/// in scope is symbolic (not an author); a name with no user author (builtins
/// like `Vector`, undeclared) or only non-head authors is omitted.
fn classifyHead(self: *ResolvePass, node: *const ast.Node, name: []const u8, is_raw: bool, ctx: Ctx) void {
if (!is_raw and lookupGeneric(ctx.scope, name) != null) return;
const set = self.res.collectVisibleAuthors(name, ctx.source, .user_bare_flat);
if (set.distinctCount() == 0) return;
var gs = false;
var tf = false;
var pr = false;
if (set.own) |a| classifyHeadKind(a.raw, &gs, &tf, &pr);
for (set.flat) |a| classifyHeadKind(a.raw, &gs, &tf, &pr);
var tables: [3]*NodeRefTable = undefined;
var n: usize = 0;
if (gs) {
tables[n] = &self.out.generic_struct_heads;
n += 1;
}
if (tf) {
tables[n] = &self.out.type_fn_heads;
n += 1;
}
if (pr) {
tables[n] = &self.out.protocol_heads;
n += 1;
}
if (n == 0) {
// an author exists but is not a head kind (e.g. a non-generic struct or
// a zero-type-param fn) — own this set's allocation, then drop it.
if (set.flat.len > 0) self.out.alloc.free(set.flat);
return;
}
// each table OWNS its `AuthorSet.flat`; give the first match the collected
// slice and a fresh copy to every subsequent table so `deinit` frees each
// exactly once.
self.replaceRef(tables[0], node, .{ .authors = set });
var i: usize = 1;
while (i < n) : (i += 1) {
self.replaceRef(tables[i], node, .{ .authors = self.dupAuthorSet(set) });
}
}
/// A shallow copy of an `AuthorSet` with its own freshly-allocated `flat` slice
/// (the `RawAuthor` elements are borrowed AST pointers + source strings, so the
/// copy is shallow). Lets one head reference be recorded into several head
/// tables without aliasing the owned slice.
fn dupAuthorSet(self: *ResolvePass, set: AuthorSet) AuthorSet {
return .{
.own = set.own,
.flat = if (set.flat.len > 0)
(self.out.alloc.dupe(RawAuthor, set.flat) catch @panic("resolve: OOM"))
else
&.{},
};
}
fn recordTemplate(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, m: GenericMatch) void {
self.replaceRef(table, node, .{ .template = self.internTemplate(m) });
}