diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index bc2f9e8..884d9ec 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -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()); +} diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index f936682..b904b9d 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -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) }); }