From 9568f7689f2b2e29ae1214087a48127743f61b70 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 11:02:08 +0300 Subject: [PATCH] feat(resolver): route plain bare-call author through Phase B collector via SelectedFunc [stdlib C] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C of the unified resolver (R5 §C, §#3). Re-base the plain bare-name call author onto the Phase B collector behind one shared SelectedFunc, so every call-path consumer reads ONE author and they can no longer disagree (fix-0102 F2). Behavior-preserving: 0722-0735 byte-identical, run_examples stays at 475. - SelectedFunc {decl, source, materialized?} replaces ResolvedAuthor in BareCallee.func; CallPlan.Target gains a `selected` arm (calls.zig). - selectPlainCallableAuthor: resolveBareCallee's body verbatim over resolver.collectVisibleAuthors (.user_bare_flat) — the ONE graph-walk. fnDeclOfRaw mirrors imports.fnDeclOf so the collector's all-domain authors reproduce module_fns' fn-only view; every byte of the negative space is preserved (own==winner → .none; non-plain-free → .none; filter-before-count; ≥2 distinct → .ambiguous). No eager materialization. - selectedFuncId materializes the FuncId on demand (shadow-only), caching into materialized — null until a site needs it (0102d: a shadow taken as a value never lowers the winner). - Six consumers route through the one selector: lowerCall variadic packing, free-fn UFCS, fn-value, closure(fn), resolveCallParamTypes, and expandCallDefaults (decl-only, no materialization). plan() produces the SelectedFunc as `.selected`. Generic/comptime/foreign/builtin stay legacy. - lower.test.zig: wire module_decls; selectPlainCallableAuthor verdicts (own-winner → .none; ≥2 flat → .ambiguous; own-shadow → decl+source, fid round-trips, materialized null). Gate: zig build + zig build test (412 ok) + run_examples (475, byte-identical) + m3te ios-sim build exit 0. --- src/ir/calls.zig | 29 ++++++ src/ir/lower.test.zig | 33 +++++-- src/ir/lower.zig | 223 ++++++++++++++++++++++++++++-------------- 3 files changed, 199 insertions(+), 86 deletions(-) diff --git a/src/ir/calls.zig b/src/ir/calls.zig index e906c13..bcc71a6 100644 --- a/src/ir/calls.zig +++ b/src/ir/calls.zig @@ -72,6 +72,13 @@ pub const CallPlan = struct { /// A callee carried by name — reflection builtin, generic / lazy fn, /// closure / fn-pointer binding, or a not-yet-lowered namespace fn. named: []const u8, + /// The single bare-call author `selectPlainCallableAuthor` selected for a + /// genuine flat same-name collision (R5 §#3). Carries the resolved + /// `*FnDecl` + source so `plan` and the lowering call-path read ONE + /// author and can no longer disagree (fix-0102 F2); the FuncId is + /// materialized on demand. Only set when the bare name reroutes away from + /// the first-wins winner; the common path still uses `func` / `named`. + selected: Lowering.SelectedFunc, /// Protocol method, by index in the protocol's method table. protocol_method: u32, /// Foreign-class method (Obj-C / JNI), with its static-ness. @@ -149,6 +156,28 @@ pub const CallResolver = struct { if (std.mem.eql(u8, bare_name, "type_is_unsigned")) return refl(bare_name, .bool); if (std.mem.eql(u8, bare_name, "type_of")) return refl(bare_name, .any); if (std.mem.eql(u8, bare_name, "field_value")) return refl(bare_name, .any); + // Plain bare same-name flat collision (R5 §C): route through the ONE + // selector so `plan` reads the SAME author the lowering call-path + // binds — they can no longer disagree (fix-0102 F2). The gate mirrors + // `lowerCall`'s: a plain top-level identifier with no scope-mangle / + // local shadow. A generic / foreign / builtin author is not plain-free + // so the selector returns `.none`; `.ambiguous` / `.none` fall through + // to the first-wins path below, byte-for-byte. + if (std.mem.eql(u8, name, bare_name) and + (if (self.l.scope) |scope| scope.lookup(bare_name) == null else true)) + { + if (self.l.current_source_file) |caller_file| { + switch (self.l.selectPlainCallableAuthor(bare_name, caller_file)) { + .func => |sf| return .{ + .kind = .direct_fn, + .return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void, + .target = .{ .selected = sf }, + .expands_defaults = defaultsFor(sf.decl, c.args.len), + }, + .ambiguous, .none => {}, + } + } + } // Generic function — infer return type via type bindings. if (self.l.program_index.fn_ast_map.get(name)) |fd| { if (fd.type_params.len > 0) { diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index d827be3..a9a9d34 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -1361,6 +1361,10 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102 var module_fns = imports.ModuleFns.init(alloc); try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns); + // Phase A raw facts: `selectPlainCallableAuthor` (Phase C) collects authors + // over `module_decls`, not `module_fns`. Wired exactly as `core.zig` does. + var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache); + const resolved_root = try alloc.create(Node); resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } }; @@ -1375,6 +1379,7 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102 lowering.program_index.import_graph = &import_graph; lowering.program_index.flat_import_graph = &flat_import_graph; lowering.program_index.module_fns = &module_fns; + lowering.program_index.module_decls = &facts.decls; lowering.lowerRoot(resolved_root); try std.testing.expect(!diagnostics.hasErrors()); @@ -1428,19 +1433,27 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102 try std.testing.expect(shadow_fid != null); try std.testing.expect(shadow_fid.? != winner_fid.?); - // fix-0102c: THE bare-name resolver routes per caller file. `main` flat- - // imports two `greet` authors and is its own author of neither → a bare - // `greet()` from `main` is ambiguous. a.sx authors the WINNER, so its bare - // `greet` resolves through the existing path (`.none`). b.sx authors the - // SHADOW, so own-author-wins binds b.sx's distinct FuncId — not first-wins. + // fix-0102c / Phase C: THE bare-name selector routes per caller file over the + // Phase A author collector. `main` flat-imports two `greet` authors and is its + // own author of neither → a bare `greet()` from `main` is ambiguous. a.sx + // authors the WINNER, so its bare `greet` resolves through the existing path + // (`.none`). b.sx authors the SHADOW, so own-author-wins selects b.sx's + // author — its `*FnDecl` + source, NOT first-wins. The selector does NOT + // eagerly materialize: it returns the decl, and the FuncId still round-trips + // to the shadow slot via the identity map (`fn_decl_fids`). const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir}); const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir}); - try std.testing.expect(lowering.resolveBareCallee("greet", main_path) == .ambiguous); - try std.testing.expect(lowering.resolveBareCallee("greet", a_path) == .none); - switch (lowering.resolveBareCallee("greet", b_path)) { - .func => |resolved| try std.testing.expectEqual(shadow_fid.?, resolved.fid), + try std.testing.expect(lowering.selectPlainCallableAuthor("greet", main_path) == .ambiguous); + try std.testing.expect(lowering.selectPlainCallableAuthor("greet", a_path) == .none); + switch (lowering.selectPlainCallableAuthor("greet", b_path)) { + .func => |sf| { + try std.testing.expectEqual(shadow_fd.?, sf.decl); + try std.testing.expectEqualStrings(b_path, sf.source); + try std.testing.expect(sf.materialized == null); + try std.testing.expectEqual(shadow_fid.?, lowering.fn_decl_fids.get(sf.decl).?); + }, else => return error.TestUnexpectedResult, } // A name no module authors (and no flat import provides) never routes. - try std.testing.expect(lowering.resolveBareCallee("nonexistent", b_path) == .none); + try std.testing.expect(lowering.selectPlainCallableAuthor("nonexistent", b_path) == .none); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4d0a488..bc8cc93 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1461,7 +1461,7 @@ pub const Lowering = struct { // A `#run` body lowers in its OWN module's source context (fix-0102d // site 4): `NAME :: #run f()` written in an imported module must // resolve a bare `f` from that module's flat imports, not the main - // file's. Without this, `resolveBareCallee` runs with the main + // file's. Without this, `selectPlainCallableAuthor` runs with the main // file's perspective and reports a genuine per-source author as // ambiguous. Mirrors `scanDecls` / `lowerDecls`, which already set // the source file per decl. @@ -1552,14 +1552,16 @@ pub const Lowering = struct { } } - /// Result of bare-call disambiguation (fix-0102c). + /// Result of bare-call disambiguation (fix-0102c, now over the Phase B + /// author collector). pub const BareCallee = union(enum) { - /// Bind the call to this specific author — its identity-addressable - /// FuncId (fix-0102b's `bareAuthorFuncId`) AND its `*FnDecl`. The decl - /// travels with the FuncId so every callee-signature decision in the - /// call path (variadic packing, …) reads the RESOLVED author, never a - /// first-wins re-lookup by name (fix-0102c F1). - func: ResolvedAuthor, + /// Bind the call to this specific author, carried as the shared + /// `SelectedFunc` (R5 §#3): its `*FnDecl` + authoring source, FuncId + /// materialized on demand. Every callee-signature decision in the call + /// path (variadic packing, param typing, default expansion) reads the + /// RESOLVED author from this one object — never a first-wins re-lookup + /// by name (fix-0102c F1). + func: SelectedFunc, /// ≥2 distinct flat authors are reachable from the caller and none is /// the caller's own — the bare call can't pick one; require a qualifier. ambiguous, @@ -1568,78 +1570,122 @@ pub const Lowering = struct { none, }; - /// A resolved bare-call author: its FuncId and the `*FnDecl` that defined - /// it, kept together so the call path has ONE source of truth for the - /// callee (no re-fetch by name after resolution). - pub const ResolvedAuthor = struct { fid: FuncId, decl: *const ast.FnDecl }; + /// The single bare-call author object (R5 §#3): the `*FnDecl` that defines + /// the call and the SOURCE file that authors it, kept together so the call + /// path has ONE source of truth for the callee. `materialized` holds the + /// author's FuncId once a site needs it; it is filled on demand by + /// `selectedFuncId` (→ `bareAuthorFuncId`), NOT during selection — so a + /// selection that only needs the decl (default-arg expansion), or a shadow + /// taken purely as a value, never lowers the first-wins winner (0102d). + pub const SelectedFunc = struct { + decl: *const ast.FnDecl, + source: []const u8, + materialized: ?FuncId = null, + }; - /// THE bare-name call resolver (fix-0102c). One canonical traversal over - /// fix-0102a's `module_fns` + `flat_import_graph` that routes a bare - /// identifier call `name` from `caller_file` to the right same-name author - /// when flat imports introduce a genuine collision. Every single-author / - /// local / parameter / std / qualified name resolves through the EXISTING - /// path unchanged: the resolver returns `.none` whenever the outcome would - /// match first-wins, so nothing on the common path is perturbed. + /// THE plain bare-name call selector (fix-0102c, R5 §C). `resolveBareCallee`'s + /// body verbatim, now over the Phase B author collector + /// (`resolver.collectVisibleAuthors` — the ONE graph-walk) instead of a direct + /// `module_fns` + `flat_import_graph` traversal. Routes a bare identifier call + /// `name` from `caller_file` to the right same-name author when flat imports + /// introduce a genuine collision. Every single-author / local / parameter / + /// std / qualified name resolves through the EXISTING path unchanged: the + /// selector returns `.none` whenever the outcome would match first-wins, so + /// nothing on the common path is perturbed. /// - /// - **own-author wins**: if `caller_file` authors `name` and the bare-name - /// first-wins winner is a DIFFERENT author, bind the caller's own author. - /// (When the winner already IS the caller's own — the single-author and - /// first-importer cases — `.none` lets the existing path bind it.) - /// - else collect the authors reachable via `caller_file`'s FLAT import + /// The collector returns RAW authors across ALL decl domains; this selector + /// reproduces `module_fns`' fn-only view by filtering each author through + /// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn — the exact + /// `*FnDecl` `module_fns` stored; every other domain drops out), preserving + /// resolveBareCallee's negative space byte-for-byte. + /// + /// - **own-author wins**: if `caller_file` authors `name` as a fn and the + /// bare-name first-wins winner is a DIFFERENT author, select the caller's + /// own author. (When the winner already IS the caller's own — the + /// single-author and first-importer cases — `.none` lets the existing path + /// bind it.) + /// - else select among the authors reachable via `caller_file`'s FLAT import /// edges (bare `#import` of a file or directory, never a namespaced - /// `ns :: #import`), deduped by `FnDecl` identity (a diamond import of the + /// `ns :: #import`), deduped by author identity (a diamond import of the /// same module is one author): `≥2 distinct` → `.ambiguous`; exactly one - /// that DIFFERS from the winner → bind it; otherwise `.none`. + /// that DIFFERS from the winner → select it; otherwise `.none`. /// /// Generic / comptime / foreign / builtin authors are never rerouted — the - /// existing dispatch owns those shapes — so the resolver returns `.none`. - pub fn resolveBareCallee(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee { - const module_fns = self.program_index.module_fns orelse return .none; + /// existing dispatch owns those shapes; `isPlainFreeFn` filters them out + /// BEFORE the count gate (so a same-name collision of non-plain authors is + /// NOT ambiguous), and the selector returns `.none`. No eager + /// materialization: the returned `SelectedFunc` carries decl + source and + /// `materialized = null`; a consumer fills the FuncId via `selectedFuncId` + /// only when it truly needs it (0102d). + pub fn selectPlainCallableAuthor(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee { const winner = self.program_index.fn_ast_map.get(name); + var res = self.resolver(); + const set = res.collectVisibleAuthors(name, caller_file, .user_bare_flat); + defer if (set.flat.len > 0) self.alloc.free(set.flat); - // own-author wins. - if (module_fns.get(caller_file)) |own_fns| { - if (own_fns.get(name)) |own| { + // own-author wins. The collector's `own` spans all domains; a non-fn + // (or a const not bound to a function) means `caller_file` has no fn + // `name` — fall through to the flat authors, exactly as the fn-only + // `module_fns` walk did. + if (set.own) |own_author| { + if (fnDeclOfRaw(own_author.raw)) |own| { if (winner != null and winner.? == own) return .none; if (!isPlainFreeFn(own)) return .none; - return .{ .func = .{ .fid = self.bareAuthorFuncId(own, name, caller_file), .decl = own } }; + return .{ .func = .{ .decl = own, .source = own_author.source } }; } } - // Caller does not author `name` → collect its flat-reachable authors. - const flat_graph = self.program_index.flat_import_graph orelse return .none; - const edges = flat_graph.get(caller_file) orelse return .none; - var distinct = std.AutoHashMap(*const ast.FnDecl, []const u8).init(self.alloc); - defer distinct.deinit(); - var edge_it = edges.iterator(); - while (edge_it.next()) |e| { - const fns = module_fns.get(e.key_ptr.*) orelse continue; - // Only plain free functions are eligible for rerouting; generic / - // foreign / builtin / #compiler authors keep their existing - // dispatch. Filtering BEFORE the count gate means a same-name - // collision of non-plain authors (e.g. two flat-imported modules - // each `#foreign`ing the same symbol) is NOT counted as ambiguous — - // it falls through to `.none` and the existing first-wins path. - if (fns.get(name)) |fd| { - if (!isPlainFreeFn(fd)) continue; - distinct.put(fd, e.key_ptr.*) catch {}; - } + // Caller does not author `name` as a fn → its flat-reachable authors. + // Filter to plain free functions BEFORE counting: a same-name collision + // of non-plain authors (e.g. two flat-imported modules each `#foreign`ing + // the same symbol) is NOT counted as ambiguous — it falls through to + // `.none` and the existing first-wins path. + var the_one: ?*const ast.FnDecl = null; + var the_source: []const u8 = &.{}; + var count: usize = 0; + for (set.flat) |fa| { + const fd = fnDeclOfRaw(fa.raw) orelse continue; + if (!isPlainFreeFn(fd)) continue; + count += 1; + if (count >= 2) return .ambiguous; + the_one = fd; + the_source = fa.source; } - if (distinct.count() == 0) return .none; - if (distinct.count() >= 2) return .ambiguous; + if (count == 0) return .none; + if (winner != null and winner.? == the_one.?) return .none; + return .{ .func = .{ .decl = the_one.?, .source = the_source } }; + } - var one_it = distinct.iterator(); - const entry = one_it.next().?; - const the_one = entry.key_ptr.*; - const the_path = entry.value_ptr.*; - if (winner != null and winner.? == the_one) return .none; - return .{ .func = .{ .fid = self.bareAuthorFuncId(the_one, name, the_path), .decl = the_one } }; + /// The `*FnDecl` a raw author wraps, or null when the author is not a + /// function — `imports.fnDeclOf` over a `RawDeclRef` so the collector's + /// all-domain authors reproduce `module_fns`' fn-only view (a `const`-wrapped + /// fn unwraps to its inner fn, the same pointer `module_fns` holds; every + /// other domain → null). + fn fnDeclOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.FnDecl { + return switch (ref) { + .fn_decl => |fd| fd, + .const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null, + else => null, + }; + } + + /// Materialize (lower-on-demand) the FuncId for a selected bare-call author, + /// caching into `sf.materialized`. Shadow-only: the winner owns the + /// name-keyed slot and lowers through the lazy path, so + /// `selectPlainCallableAuthor` returns `.none` for it and this is never asked + /// to lower the winner (0102d). `name` is the call name (== the author's + /// registered name); `sf.source` pins the author's own visibility context. + fn selectedFuncId(self: *Lowering, sf: *SelectedFunc, name: []const u8) FuncId { + if (sf.materialized) |fid| return fid; + const fid = self.bareAuthorFuncId(sf.decl, name, sf.source); + sf.materialized = fid; + return fid; } /// The FuncId for a resolved bare-call author, ensuring its body is lowered. /// Only ever called for a SHADOW (an author that is not the name-keyed /// winner): the winner owns the name-keyed slot and lowers through the - /// normal lazy path, so `resolveBareCallee` returns `.none` for it. A shadow + /// normal lazy path, so `selectPlainCallableAuthor` returns `.none` for it. A shadow /// is declared a fresh same-name FuncId in its OWN module's visibility /// context and its body lowered into that slot via fix-0102b's identity- /// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks @@ -3325,8 +3371,11 @@ pub const Lowering = struct { (if (self.scope) |scope| scope.lookup(id.name) == null else true)) { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(id.name, caller_file)) { - .func => |resolved| break :blk_fv resolved.fid, + switch (self.selectPlainCallableAuthor(id.name, caller_file)) { + .func => |sf| { + var selected = sf; + break :blk_fv self.selectedFuncId(&selected, id.name); + }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name}); @@ -7377,8 +7426,11 @@ pub const Lowering = struct { (if (self.scope) |scope| scope.lookup(fn_name) == null else true)) { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(fn_name, caller_file)) { - .func => |resolved| break :blk_cl resolved.fid, + switch (self.selectPlainCallableAuthor(fn_name, caller_file)) { + .func => |sf| { + var selected = sf; + break :blk_cl self.selectedFuncId(&selected, fn_name); + }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name}); @@ -7682,27 +7734,28 @@ pub const Lowering = struct { // author. Only a plain top-level identifier call routes here: // scope-mangled / UFCS-aliased / locally-shadowed names and // 0/1-author names fall straight to the existing path below - // (`resolveBareCallee` returns `.none`). + // (`selectPlainCallableAuthor` returns `.none`). if (std.mem.eql(u8, func_name, id.name) and (if (self.scope) |scope| scope.lookup(id.name) == null else true)) { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(func_name, caller_file)) { + switch (self.selectPlainCallableAuthor(func_name, caller_file)) { .none => {}, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name}); return Ref.none; }, - .func => |resolved| { - const fid = resolved.fid; + .func => |sf| { + var selected = sf; + const fid = self.selectedFuncId(&selected, func_name); const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; const params = func.params; // The RESOLVED author's decl drives variadic // packing — not a first-wins re-lookup by name, // whose variadic shape may differ (fix-0102c F1). - self.packVariadicCallArgs(resolved.decl, c, &args); + self.packVariadicCallArgs(selected.decl, c, &args); const final_args = self.prependCtxIfNeeded(func, args.items); self.coerceCallArgs(final_args, params); if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); @@ -8202,8 +8255,11 @@ pub const Lowering = struct { // → existing first-wins path. const ufcs_fid: ?FuncId = blk_uf: { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(fa.field, caller_file)) { - .func => |resolved| break :blk_uf resolved.fid, + switch (self.selectPlainCallableAuthor(fa.field, caller_file)) { + .func => |sf| { + var selected = sf; + break :blk_uf self.selectedFuncId(&selected, fa.field); + }, .ambiguous => { if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field}); @@ -12054,8 +12110,14 @@ pub const Lowering = struct { (if (self.scope) |scope| scope.lookup(id.name) == null else true)) { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(id.name, caller_file)) { - .func => |resolved| break :blk resolved.decl, + switch (self.selectPlainCallableAuthor(id.name, caller_file)) { + // Default expansion needs only the author's decl + // (its param defaults) — never the FuncId. Reading + // `sf.decl` here keeps `materialized` null, so a + // bare call whose body is never emitted (e.g. only + // its defaults are inspected) does not lower the + // author (0102d). + .func => |sf| break :blk sf.decl, .ambiguous => return null, .none => {}, } @@ -12296,9 +12358,11 @@ pub const Lowering = struct { (if (self.scope) |scope| scope.lookup(bare_name) == null else true)) { if (self.current_source_file) |caller_file| { - switch (self.resolveBareCallee(bare_name, caller_file)) { - .func => |resolved| { - const func = &self.module.functions.items[@intFromEnum(resolved.fid)]; + switch (self.selectPlainCallableAuthor(bare_name, caller_file)) { + .func => |sf| { + var selected = sf; + const fid = self.selectedFuncId(&selected, bare_name); + const func = &self.module.functions.items[@intFromEnum(fid)]; return self.userParamTypes(func); }, .ambiguous, .none => {}, @@ -14548,6 +14612,13 @@ pub const Lowering = struct { return .{ .l = self }; } + /// A `Resolver` facade over the borrowed Phase A import facts (Phase B). Cheap + /// by-value; `collectVisibleAuthors`'s `AuthorSet.flat` slice is backed by + /// `self.alloc` and owned by the caller (`selectPlainCallableAuthor` frees it). + fn resolver(self: *Lowering) resolver_mod.Resolver { + return resolver_mod.Resolver.init(&self.program_index, self.alloc); + } + pub fn genericResolver(self: *Lowering) GenericResolver { return .{ .l = self }; }