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:
@@ -288,6 +288,7 @@ test "resolver: visibility edge-walk — own + flat visible; namespaced-only onl
|
||||
const Resolved = struct {
|
||||
root: *ast.Node,
|
||||
decls: imports.ModuleDecls,
|
||||
ns_edges: imports.NamespaceEdges,
|
||||
flat_import_graph: Graph,
|
||||
import_graph: Graph,
|
||||
};
|
||||
@@ -329,6 +330,7 @@ fn buildResolved(alloc: std.mem.Allocator, io: std.Io, absdir: []const u8, main_
|
||||
return .{
|
||||
.root = root,
|
||||
.decls = facts.decls,
|
||||
.ns_edges = facts.ns_edges,
|
||||
.flat_import_graph = flat_import_graph,
|
||||
.import_graph = import_graph,
|
||||
};
|
||||
@@ -473,7 +475,10 @@ test "resolver: resolve — bare-name domains populated, keyed by node, symbolic
|
||||
try std.testing.expect(pack_ref == .pack);
|
||||
try std.testing.expectEqual(@as(?u32, 0), pack_ref.pack.index);
|
||||
|
||||
// (5) The seven domains S2.1b/c own stay EMPTY — S2.1a is parallel/unconsumed.
|
||||
// (5) This fixture exercises NONE of the S2.1b/c domains (no namespaced import,
|
||||
// no parameterized heads, no foreign/const/UFCS sites) — and the index has
|
||||
// no `namespace_edges` wired — so all seven stay EMPTY. The dedicated S2.1b
|
||||
// test below proves the four it owns populate when exercised.
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.namespace_refs.count());
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.generic_struct_heads.count());
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.type_fn_heads.count());
|
||||
@@ -535,3 +540,117 @@ test "resolver: resolve — generic constraints and named error sets are type re
|
||||
try std.testing.expect(err_ty.data == .error_type_expr);
|
||||
try expectTypeRefOwnTag(&rp, err_ty, error_tag);
|
||||
}
|
||||
|
||||
// ── the owning resolution pass — S2.1b namespace-qualified + head domains ──
|
||||
|
||||
// S2.1b populates four more domains on the SAME traversal, still RAW /
|
||||
// PARALLEL / UNCONSUMED. (1) A namespace-qualified `g.helper_fn` resolves via
|
||||
// collectNamespaceAuthors into namespace_refs, keyed by its field_access node.
|
||||
// (2..4) Parameterized heads are binned by the resolved author's decl kind: a
|
||||
// generic struct (`Box(s64)`) → generic_struct_heads, a type-function
|
||||
// (`Make(s64)`) → type_fn_heads, a parameterized protocol used as a value type
|
||||
// (`Cmp(s64)`) → protocol_heads — each keyed by its parameterized_type_expr node.
|
||||
test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protocol heads" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const io = testIo();
|
||||
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "lib.sx", .data =
|
||||
\\helper_fn :: () -> s64 { 7 }
|
||||
\\
|
||||
});
|
||||
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data =
|
||||
\\g :: #import "lib.sx";
|
||||
\\Box :: struct($T: Type) { value: T }
|
||||
\\Make :: ($T: Type) -> Type { return [3]T; }
|
||||
\\Cmp :: protocol(T: Type) { get :: () -> T; }
|
||||
\\use_box :: (b: Box(s64)) -> s64 { return 0; }
|
||||
\\use_make :: (m: Make(s64)) -> s64 { return 0; }
|
||||
\\use_cmp :: (c: Cmp(s64)) -> s64 { return 0; }
|
||||
\\read_ns :: () -> s64 { return g.helper_fn(); }
|
||||
\\main :: () -> s32 { return 0; }
|
||||
\\
|
||||
});
|
||||
|
||||
var dirbuf: [4096]u8 = undefined;
|
||||
const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)];
|
||||
const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir});
|
||||
|
||||
var prog = try buildResolved(alloc, io, absdir, main_path);
|
||||
|
||||
var idx = ProgramIndex.init(alloc);
|
||||
defer idx.deinit();
|
||||
idx.module_decls = &prog.decls;
|
||||
idx.namespace_edges = &prog.ns_edges;
|
||||
idx.flat_import_graph = &prog.flat_import_graph;
|
||||
idx.import_graph = &prog.import_graph;
|
||||
|
||||
var rp = resolver.resolve(prog.root, &idx, main_path, alloc);
|
||||
defer rp.deinit();
|
||||
|
||||
// (0) All four S2.1b tables are NON-EMPTY.
|
||||
try std.testing.expect(rp.namespace_refs.count() > 0);
|
||||
try std.testing.expect(rp.generic_struct_heads.count() > 0);
|
||||
try std.testing.expect(rp.type_fn_heads.count() > 0);
|
||||
try std.testing.expect(rp.protocol_heads.count() > 0);
|
||||
|
||||
const struct_tag = std.meta.Tag(resolver.RawDeclRef).struct_decl;
|
||||
const fn_tag = std.meta.Tag(resolver.RawDeclRef).fn_decl;
|
||||
const protocol_tag = std.meta.Tag(resolver.RawDeclRef).protocol_decl;
|
||||
|
||||
// (1) Namespace-qualified: keyed by a `field_access` node naming `helper_fn`,
|
||||
// resolved RAW to lib.sx's fn author (own of the namespace target).
|
||||
var saw_ns = false;
|
||||
var nit = rp.namespace_refs.iterator();
|
||||
while (nit.next()) |e| {
|
||||
const k = e.key_ptr.*;
|
||||
try std.testing.expect(k.data == .field_access); // namespace refs key the access node
|
||||
if (std.mem.eql(u8, k.data.field_access.field, "helper_fn")) {
|
||||
try std.testing.expect(e.value_ptr.* == .authors);
|
||||
try std.testing.expect(e.value_ptr.authors.own != null);
|
||||
try std.testing.expectEqual(fn_tag, std.meta.activeTag(e.value_ptr.authors.own.?.raw));
|
||||
saw_ns = true;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(saw_ns);
|
||||
|
||||
// (2) Generic-struct head: the `Box(s64)` param type-expr is keyed in
|
||||
// generic_struct_heads and resolves RAW to the own generic struct.
|
||||
const use_box = findFn(prog.root, "use_box") orelse return error.MissingFn;
|
||||
const box_head = use_box.data.fn_decl.params[0].type_expr;
|
||||
try std.testing.expect(box_head.data == .parameterized_type_expr);
|
||||
const box_ref = rp.generic_struct_heads.get(box_head) orelse return error.BoxHeadNotKeyed;
|
||||
try std.testing.expect(box_ref == .authors);
|
||||
try std.testing.expect(box_ref.authors.own != null);
|
||||
try std.testing.expectEqual(struct_tag, std.meta.activeTag(box_ref.authors.own.?.raw));
|
||||
// and it is NOT mis-binned into the other head tables.
|
||||
try std.testing.expect(rp.type_fn_heads.get(box_head) == null);
|
||||
try std.testing.expect(rp.protocol_heads.get(box_head) == null);
|
||||
|
||||
// (3) Type-function head: `Make(s64)` keyed in type_fn_heads, RAW fn author.
|
||||
const use_make = findFn(prog.root, "use_make") orelse return error.MissingFn;
|
||||
const make_head = use_make.data.fn_decl.params[0].type_expr;
|
||||
try std.testing.expect(make_head.data == .parameterized_type_expr);
|
||||
const make_ref = rp.type_fn_heads.get(make_head) orelse return error.MakeHeadNotKeyed;
|
||||
try std.testing.expect(make_ref == .authors);
|
||||
try std.testing.expect(make_ref.authors.own != null);
|
||||
try std.testing.expectEqual(fn_tag, std.meta.activeTag(make_ref.authors.own.?.raw));
|
||||
|
||||
// (4) Protocol head: the `Cmp(s64)` value-type param keyed in protocol_heads.
|
||||
const use_cmp = findFn(prog.root, "use_cmp") orelse return error.MissingFn;
|
||||
const cmp_head = use_cmp.data.fn_decl.params[0].type_expr;
|
||||
try std.testing.expect(cmp_head.data == .parameterized_type_expr);
|
||||
const cmp_ref = rp.protocol_heads.get(cmp_head) orelse return error.CmpHeadNotKeyed;
|
||||
try std.testing.expect(cmp_ref == .authors);
|
||||
try std.testing.expect(cmp_ref.authors.own != null);
|
||||
try std.testing.expectEqual(protocol_tag, std.meta.activeTag(cmp_ref.authors.own.?.raw));
|
||||
|
||||
// (5) The three S2.1c domains remain UNTOUCHED by S2.1b.
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.foreign_class_refs.count());
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count());
|
||||
try std.testing.expectEqual(@as(u32, 0), rp.ufcs_refs.count());
|
||||
}
|
||||
|
||||
@@ -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) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user