From 7f3a7b35effc1e63409c7c2a959abf5776b1b3eb Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 18:44:08 +0300 Subject: [PATCH] refactor(ir): extract CallResolver for call result typing (A3.2 relocation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move call-result-type discovery out of Lowering into a new src/ir/calls.zig (CallResolver): the A3.1 Lowering.inferCallType body moves verbatim into CallResolver.resultType. inferExprType's `.call` arm now delegates via callResolver(); Lowering.inferCallType is gone. CallResolver is a *Lowering facade (Principle 5, like ExprTyper/PackResolver): call typing reads live lexical-scope / target-type state and the function / foreign-class / protocol resolver helpers, so it borrows *Lowering. Transform was `self.` -> `self.l.` plus the file-local static `resolveBuiltin(` -> `Lowering.resolveBuiltin(`. Widened to pub only what the facade actually consumes: resolveTypeArg, inferGenericReturnType, resolveFuncByName, getProtocolInfo, resolveForeignMethodReturnType, the static resolveBuiltin, and Scope.lookupFn. resolveTypeArg widening is genuinely required here — the `cast` builtin's result type calls it. calls.test.zig adds focused tests (builtin/reflection classification, unknown callee -> unresolved) for the scope-free paths. Barrel-wired in ir.zig. This is the relocation half of PLAN-ARCH A3.2; call LOWERING (lowerCall) still owns its own dispatch, and the CallPlan convergence (one plan shared by typing and lowering, deleting the duplicated qualified/bare/lazy logic) remains. Behavior-preserving. Gate: zig build, zig build test (incl. new CallResolver tests), bash tests/run_examples.sh -> 356/0. lower.zig 18598 -> 18413. --- src/ir/calls.test.zig | 46 +++++++++ src/ir/calls.zig | 215 ++++++++++++++++++++++++++++++++++++++++++ src/ir/ir.zig | 3 + src/ir/lower.zig | 207 +++------------------------------------- 4 files changed, 275 insertions(+), 196 deletions(-) create mode 100644 src/ir/calls.test.zig create mode 100644 src/ir/calls.zig diff --git a/src/ir/calls.test.zig b/src/ir/calls.test.zig new file mode 100644 index 0000000..e98b100 --- /dev/null +++ b/src/ir/calls.test.zig @@ -0,0 +1,46 @@ +// Tests for calls.zig — focused on the call-result-typing paths CallResolver +// owns that need no lexical scope / fn registration: builtin and reflection +// builtin classification, and the unresolved fallthrough. Reached via the +// public `Lowering.inferExprType` delegation. + +const std = @import("std"); +const ast = @import("../ast.zig"); +const Node = ast.Node; + +const ir_mod = @import("ir.zig"); +const TypeId = ir_mod.TypeId; +const Lowering = ir_mod.Lowering; + +fn node(data: ast.Node.Data) Node { + return .{ .span = .{ .start = 0, .end = 0 }, .data = data }; +} + +test "calls: builtin and reflection result types, unknown fallthrough" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + + // One shared throwaway argument — the classified builtins below type by + // callee name and don't inspect it. + var arg = node(.{ .int_literal = .{ .value = 1 } }); + var args = [_]*Node{&arg}; + + const cases = [_]struct { name: []const u8, want: TypeId }{ + .{ .name = "size_of", .want = .s64 }, + .{ .name = "align_of", .want = .s64 }, + .{ .name = "type_name", .want = .string }, + .{ .name = "field_count", .want = .s64 }, + .{ .name = "is_flags", .want = .bool }, + .{ .name = "type_of", .want = .any }, + // Unknown bare callee with no builtin / declared fn / scope binding + // types as unresolved, not a fabricated guess. + .{ .name = "definitely_not_a_fn", .want = .unresolved }, + }; + + for (cases) |tc| { + var callee = node(.{ .identifier = .{ .name = tc.name } }); + var call = node(.{ .call = .{ .callee = &callee, .args = &args } }); + try std.testing.expectEqual(tc.want, l.inferExprType(&call)); + } +} diff --git a/src/ir/calls.zig b/src/ir/calls.zig new file mode 100644 index 0000000..50e53ea --- /dev/null +++ b/src/ir/calls.zig @@ -0,0 +1,215 @@ +const std = @import("std"); +const ast = @import("../ast.zig"); +const types = @import("types.zig"); +const type_bridge = @import("type_bridge.zig"); +const lower = @import("lower.zig"); + +const Node = ast.Node; +const TypeId = types.TypeId; +const Lowering = lower.Lowering; + +/// Call result typing (architecture phase A3.2), extracted from +/// `Lowering.inferExprType`'s call arm. Discovers the IR type a call +/// expression evaluates to — across builtins / reflection builtins, generic +/// and plain free functions (lowered or lazy via `fn_ast_map`), closure / +/// function-typed locals, protocol dispatch, foreign-class instance/static +/// methods, struct (UFCS) methods, qualified namespace calls, and +/// enum/tagged-union construction. +/// +/// A `*Lowering` facade (Principle 5, like `ExprTyper` / `PackResolver`): call +/// typing reads live lexical-scope / target-type state and the function / +/// foreign-class / protocol resolver helpers, so it borrows `*Lowering` rather +/// than re-threading every field. This step relocates the result-typing logic +/// only; call LOWERING (`lowerCall`) still owns its own dispatch — the two +/// converge onto a shared `CallPlan` in the follow-up A3.2 convergence work. +pub const CallResolver = struct { + l: *Lowering, + + /// Infer the IR type a call expression evaluates to (without lowering it). + pub fn resultType(self: CallResolver, c: *const ast.Call) TypeId { + if (c.callee.data == .identifier) { + const bare_name = c.callee.data.identifier.name; + // Resolve local function name (bare → mangled) and UFCS aliases + const name = blk: { + const scoped = if (self.l.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; + if (self.l.program_index.ufcs_alias_map.get(bare_name)) |target| { + break :blk if (self.l.scope) |scope| scope.lookupFn(target) orelse target else target; + } + break :blk scoped; + }; + if (Lowering.resolveBuiltin(bare_name)) |bid| { + return switch (bid) { + .sqrt, .sin, .cos, .floor => blk: { + if (c.args.len > 0) { + const arg_ty = self.l.inferExprType(c.args[0]); + if (arg_ty == .f32) break :blk TypeId.f32; + } + break :blk TypeId.f64; + }, + .size_of, .align_of => .s64, + .cast => if (c.args.len > 0) self.l.resolveTypeArg(c.args[0]) else .unresolved, + else => .unresolved, + }; + } + // Reflection builtins live outside `resolveBuiltin`'s + // table (their lowering goes through + // `tryLowerReflectionCall`, not the `BuiltinId` + // dispatch). Recognize them here so pack-fn callers + // mangle their results with the right tag. + if (std.mem.eql(u8, bare_name, "type_name")) return .string; + if (std.mem.eql(u8, bare_name, "type_eq")) return .bool; + if (std.mem.eql(u8, bare_name, "has_impl")) return .bool; + if (std.mem.eql(u8, bare_name, "field_count")) return .s64; + if (std.mem.eql(u8, bare_name, "field_index")) return .s64; + if (std.mem.eql(u8, bare_name, "field_name")) return .string; + if (std.mem.eql(u8, bare_name, "error_tag_name")) return .string; + if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool; + if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void; + if (std.mem.eql(u8, bare_name, "__trace_resolve_frame")) + return self.l.module.types.findByName(self.l.module.types.internString("Frame")) orelse .unresolved; + if (std.mem.eql(u8, bare_name, "is_flags")) return .bool; + if (std.mem.eql(u8, bare_name, "type_of")) return .any; + if (std.mem.eql(u8, bare_name, "field_value")) return .any; + // Check if it's a 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) { + return self.l.inferGenericReturnType(fd, c); + } + } + // Check declared functions for return type + if (self.l.resolveFuncByName(name)) |fid| { + return self.l.module.functions.items[@intFromEnum(fid)].ret; + } + // Not lowered yet (lazy lowering): take the return type from + // the declared AST. A void/return-less fn is void — not an + // `.unresolved` guess. + if (self.l.program_index.fn_ast_map.get(name)) |fd| { + if (fd.return_type) |rt| return self.l.resolveType(rt); + return .void; + } + // Check if callee is a local closure / function-type variable + // (e.g. a `cb: Closure(...) -> R` or bare `cb: (T) -> R` + // parameter) — extract its declared return type so `try` / + // `catch` on the call see the (possibly failable) result. + if (self.l.scope) |scope| { + if (scope.lookup(bare_name)) |binding| { + if (!binding.ty.isBuiltin()) { + const ti = self.l.module.types.get(binding.ty); + if (ti == .closure) return ti.closure.ret; + if (ti == .function) return ti.function.ret; + } + } + } + } else if (c.callee.data == .field_access) { + const cfa = c.callee.data.field_access; + // Check if receiver is a protocol type → return protocol method type + const recv_ty = self.l.inferExprType(cfa.object); + { + if (self.l.getProtocolInfo(recv_ty)) |proto_info| { + for (proto_info.methods) |m| { + if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; + } + } + } + // Foreign-class instance method: look up the method's + // declared return type so chained calls (e.g. + // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. + { + var recv_inner = recv_ty; + if (!recv_inner.isBuiltin()) { + const ri = self.l.module.types.get(recv_inner); + if (ri == .pointer) recv_inner = ri.pointer.pointee; + } + if (!recv_inner.isBuiltin()) { + const inner_info = self.l.module.types.get(recv_inner); + if (inner_info == .@"struct") { + const sn = self.l.module.types.getString(inner_info.@"struct".name); + if (self.l.program_index.foreign_class_map.get(sn)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + return self.l.resolveForeignMethodReturnType(fcd, md); + }, + else => {}, + }; + } + } + } + } + // Instance method call: obj.method(args) → look up StructName.method + { + var obj_ty = recv_ty; + if (!obj_ty.isBuiltin()) { + const oi = self.l.module.types.get(obj_ty); + if (oi == .pointer) obj_ty = oi.pointer.pointee; + } + if (!obj_ty.isBuiltin()) { + const oi = self.l.module.types.get(obj_ty); + if (oi == .@"struct") { + const struct_name = self.l.module.types.getString(oi.@"struct".name); + const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; + // Generic #compiler method dispatch — return type from declaration + if (self.l.program_index.fn_ast_map.get(qualified)) |method_fd| { + if (method_fd.body.data == .compiler_expr) { + if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map); + return .void; + } + } + if (self.l.resolveFuncByName(qualified)) |fid| { + return self.l.module.functions.items[@intFromEnum(fid)].ret; + } + } + } + } + // Type.variant(args) — qualified enum construction + const type_name = switch (cfa.object.data) { + .identifier => |id| id.name, + .type_expr => |te| te.name, + else => null, + }; + if (type_name) |tn| { + // Foreign-class static method: `Alias.static_method(args)`. + if (self.l.program_index.foreign_class_map.get(tn)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { + return self.l.resolveForeignMethodReturnType(fcd, md); + }, + else => {}, + }; + } + const type_name_id = self.l.module.types.internString(tn); + if (self.l.module.types.findByName(type_name_id)) |ty| { + const ti = self.l.module.types.get(ty); + if (ti == .tagged_union or ti == .@"enum") return ty; + } + // Check for qualified function call. `resolveFuncByName` + // only finds ALREADY-LOWERED functions; namespace + // imports are typically lowered lazily on demand, so + // a fresh `pkg.hello()` call site may resolve through + // `fn_ast_map` first. Without this, the call's return + // type silently falls through to `.s64` and any + // pack-fn caller (e.g. `print("{}\n", pkg.hello())`) + // mangles the arg as s64, mis-tagging the actual + // string in the Any box. + const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; + if (self.l.resolveFuncByName(qualified)) |fid| { + return self.l.module.functions.items[@intFromEnum(fid)].ret; + } + if (self.l.program_index.fn_ast_map.get(qualified)) |qfd| { + if (qfd.return_type) |rt| return self.l.resolveType(rt); + return .void; + } + // Namespace aliases sometimes register the function + // under its bare name (matches `lowerCall`'s effective- + // name resolution order). + if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| { + if (bfd.return_type) |rt| return self.l.resolveType(rt); + return .void; + } + } + } else if (c.callee.data == .enum_literal) { + // .Variant(args) — dot-shorthand enum construction + return self.l.target_type orelse .unresolved; + } + return .unresolved; + } +}; diff --git a/src/ir/ir.zig b/src/ir/ir.zig index 9f0b855..33c46f0 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -8,6 +8,7 @@ pub const program_index = @import("program_index.zig"); pub const type_resolver = @import("type_resolver.zig"); pub const packs = @import("packs.zig"); pub const expr_typer = @import("expr_typer.zig"); +pub const calls = @import("calls.zig"); pub const semantic_diagnostics = @import("semantic_diagnostics.zig"); pub const TypeId = types.TypeId; @@ -40,6 +41,7 @@ pub const TypeResolver = type_resolver.TypeResolver; pub const ResolveEnv = type_resolver.ResolveEnv; pub const PackResolver = packs.PackResolver; pub const ExprTyper = expr_typer.ExprTyper; +pub const CallResolver = calls.CallResolver; pub const compiler_hooks = @import("compiler_hooks.zig"); pub const emit_llvm = @import("emit_llvm.zig"); @@ -62,6 +64,7 @@ pub const program_index_tests = @import("program_index.test.zig"); pub const type_resolver_tests = @import("type_resolver.test.zig"); pub const packs_tests = @import("packs.test.zig"); pub const expr_typer_tests = @import("expr_typer.test.zig"); +pub const calls_tests = @import("calls.test.zig"); pub const type_bridge_tests = @import("type_bridge.test.zig"); pub const emit_llvm_tests = @import("emit_llvm.test.zig"); pub const jni_descriptor_tests = @import("jni_descriptor.test.zig"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 191d393..a981b29 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -23,6 +23,7 @@ const TypeResolver = @import("type_resolver.zig").TypeResolver; const ResolveEnv = @import("type_resolver.zig").ResolveEnv; const PackResolver = @import("packs.zig").PackResolver; const ExprTyper = @import("expr_typer.zig").ExprTyper; +const CallResolver = @import("calls.zig").CallResolver; const semantic_diagnostics = @import("semantic_diagnostics.zig"); const TypeId = types.TypeId; @@ -82,7 +83,7 @@ const Scope = struct { return null; } - fn lookupFn(self: *const Scope, name: []const u8) ?[]const u8 { + pub fn lookupFn(self: *const Scope, name: []const u8) ?[]const u8 { if (self.fn_names.get(name)) |mangled| return mangled; if (self.parent) |p| return p.lookupFn(name); return null; @@ -6682,7 +6683,7 @@ pub const Lowering = struct { return self.resolveType(type_node); } - fn resolveForeignMethodReturnType( + pub fn resolveForeignMethodReturnType( self: *Lowering, fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl, @@ -8155,7 +8156,7 @@ pub const Lowering = struct { return new_args; } - fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId { + pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId { // Check foreign name map first (e.g., "c_abs" → "abs") const effective_name = self.foreign_name_map.get(name) orelse name; const name_id = self.module.types.internString(effective_name); @@ -8165,7 +8166,7 @@ pub const Lowering = struct { return null; } - fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { + pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId { const builtins = .{ // Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin .{ "out", inst_mod.BuiltinId.out }, @@ -11315,7 +11316,7 @@ pub const Lowering = struct { return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map); } - fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { + pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` // or `type_eq($args[i], s64)`). Same shape as the // `resolveTypeWithBindings` arm — looks up the bound pack types @@ -14038,7 +14039,7 @@ pub const Lowering = struct { } /// Get protocol info for a TypeId (if it's a protocol type). - fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { + pub fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo { if (ty.isBuiltin()) return null; const info = self.module.types.get(ty); if (info != .@"struct") return null; @@ -14449,7 +14450,7 @@ pub const Lowering = struct { /// Infer the type of an expression from its AST node (used for untyped var decls). pub fn inferExprType(self: *Lowering, node: *const Node) TypeId { return switch (node.data) { - .call => |*c| self.inferCallType(c), + .call => |*c| self.callResolver().resultType(c), else => self.exprTyper().inferType(node), }; } @@ -14458,198 +14459,12 @@ pub const Lowering = struct { return .{ .l = self }; } - /// Infer the result type of a call expression. Call typing stays in - /// `Lowering` for now (A3.1); A3.2 converges it into `CallResolver`. The - /// structural / non-call shapes live in `ExprTyper` (`expr_typer.zig`). - fn inferCallType(self: *Lowering, c: *const ast.Call) TypeId { - if (c.callee.data == .identifier) { - const bare_name = c.callee.data.identifier.name; - // Resolve local function name (bare → mangled) and UFCS aliases - const name = blk: { - const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name; - if (self.program_index.ufcs_alias_map.get(bare_name)) |target| { - break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target; - } - break :blk scoped; - }; - if (resolveBuiltin(bare_name)) |bid| { - return switch (bid) { - .sqrt, .sin, .cos, .floor => blk: { - if (c.args.len > 0) { - const arg_ty = self.inferExprType(c.args[0]); - if (arg_ty == .f32) break :blk TypeId.f32; - } - break :blk TypeId.f64; - }, - .size_of, .align_of => .s64, - .cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .unresolved, - else => .unresolved, - }; - } - // Reflection builtins live outside `resolveBuiltin`'s - // table (their lowering goes through - // `tryLowerReflectionCall`, not the `BuiltinId` - // dispatch). Recognize them here so pack-fn callers - // mangle their results with the right tag. - if (std.mem.eql(u8, bare_name, "type_name")) return .string; - if (std.mem.eql(u8, bare_name, "type_eq")) return .bool; - if (std.mem.eql(u8, bare_name, "has_impl")) return .bool; - if (std.mem.eql(u8, bare_name, "field_count")) return .s64; - if (std.mem.eql(u8, bare_name, "field_index")) return .s64; - if (std.mem.eql(u8, bare_name, "field_name")) return .string; - if (std.mem.eql(u8, bare_name, "error_tag_name")) return .string; - if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool; - if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return .void; - if (std.mem.eql(u8, bare_name, "__trace_resolve_frame")) - return self.module.types.findByName(self.module.types.internString("Frame")) orelse .unresolved; - if (std.mem.eql(u8, bare_name, "is_flags")) return .bool; - if (std.mem.eql(u8, bare_name, "type_of")) return .any; - if (std.mem.eql(u8, bare_name, "field_value")) return .any; - // Check if it's a generic function — infer return type via type bindings - if (self.program_index.fn_ast_map.get(name)) |fd| { - if (fd.type_params.len > 0) { - return self.inferGenericReturnType(fd, c); - } - } - // Check declared functions for return type - if (self.resolveFuncByName(name)) |fid| { - return self.module.functions.items[@intFromEnum(fid)].ret; - } - // Not lowered yet (lazy lowering): take the return type from - // the declared AST. A void/return-less fn is void — not an - // `.unresolved` guess. - if (self.program_index.fn_ast_map.get(name)) |fd| { - if (fd.return_type) |rt| return self.resolveType(rt); - return .void; - } - // Check if callee is a local closure / function-type variable - // (e.g. a `cb: Closure(...) -> R` or bare `cb: (T) -> R` - // parameter) — extract its declared return type so `try` / - // `catch` on the call see the (possibly failable) result. - if (self.scope) |scope| { - if (scope.lookup(bare_name)) |binding| { - if (!binding.ty.isBuiltin()) { - const ti = self.module.types.get(binding.ty); - if (ti == .closure) return ti.closure.ret; - if (ti == .function) return ti.function.ret; - } - } - } - } else if (c.callee.data == .field_access) { - const cfa = c.callee.data.field_access; - // Check if receiver is a protocol type → return protocol method type - const recv_ty = self.inferExprType(cfa.object); - { - if (self.getProtocolInfo(recv_ty)) |proto_info| { - for (proto_info.methods) |m| { - if (std.mem.eql(u8, m.name, cfa.field)) return m.ret_type; - } - } - } - // Foreign-class instance method: look up the method's - // declared return type so chained calls (e.g. - // `UIWindow.alloc().initWithWindowScene(scene)`) resolve. - { - var recv_inner = recv_ty; - if (!recv_inner.isBuiltin()) { - const ri = self.module.types.get(recv_inner); - if (ri == .pointer) recv_inner = ri.pointer.pointee; - } - if (!recv_inner.isBuiltin()) { - const inner_info = self.module.types.get(recv_inner); - if (inner_info == .@"struct") { - const sn = self.module.types.getString(inner_info.@"struct".name); - if (self.program_index.foreign_class_map.get(sn)) |fcd| { - for (fcd.members) |m| switch (m) { - .method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) { - return self.resolveForeignMethodReturnType(fcd, md); - }, - else => {}, - }; - } - } - } - } - // Instance method call: obj.method(args) → look up StructName.method - { - var obj_ty = recv_ty; - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .pointer) obj_ty = oi.pointer.pointee; - } - if (!obj_ty.isBuiltin()) { - const oi = self.module.types.get(obj_ty); - if (oi == .@"struct") { - const struct_name = self.module.types.getString(oi.@"struct".name); - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; - // Generic #compiler method dispatch — return type from declaration - if (self.program_index.fn_ast_map.get(qualified)) |method_fd| { - if (method_fd.body.data == .compiler_expr) { - if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map); - return .void; - } - } - if (self.resolveFuncByName(qualified)) |fid| { - return self.module.functions.items[@intFromEnum(fid)].ret; - } - } - } - } - // Type.variant(args) — qualified enum construction - const type_name = switch (cfa.object.data) { - .identifier => |id| id.name, - .type_expr => |te| te.name, - else => null, - }; - if (type_name) |tn| { - // Foreign-class static method: `Alias.static_method(args)`. - if (self.program_index.foreign_class_map.get(tn)) |fcd| { - for (fcd.members) |m| switch (m) { - .method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) { - return self.resolveForeignMethodReturnType(fcd, md); - }, - else => {}, - }; - } - const type_name_id = self.module.types.internString(tn); - if (self.module.types.findByName(type_name_id)) |ty| { - const ti = self.module.types.get(ty); - if (ti == .tagged_union or ti == .@"enum") return ty; - } - // Check for qualified function call. `resolveFuncByName` - // only finds ALREADY-LOWERED functions; namespace - // imports are typically lowered lazily on demand, so - // a fresh `pkg.hello()` call site may resolve through - // `fn_ast_map` first. Without this, the call's return - // type silently falls through to `.s64` and any - // pack-fn caller (e.g. `print("{}\n", pkg.hello())`) - // mangles the arg as s64, mis-tagging the actual - // string in the Any box. - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field; - if (self.resolveFuncByName(qualified)) |fid| { - return self.module.functions.items[@intFromEnum(fid)].ret; - } - if (self.program_index.fn_ast_map.get(qualified)) |qfd| { - if (qfd.return_type) |rt| return self.resolveType(rt); - return .void; - } - // Namespace aliases sometimes register the function - // under its bare name (matches `lowerCall`'s effective- - // name resolution order). - if (self.program_index.fn_ast_map.get(cfa.field)) |bfd| { - if (bfd.return_type) |rt| return self.resolveType(rt); - return .void; - } - } - } else if (c.callee.data == .enum_literal) { - // .Variant(args) — dot-shorthand enum construction - return self.target_type orelse .unresolved; - } - return .unresolved; + fn callResolver(self: *Lowering) CallResolver { + return .{ .l = self }; } /// Infer the return type of a generic function call by resolving type bindings. - fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId { + pub fn inferGenericReturnType(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call) TypeId { if (fd.return_type == null) return .void; // Build ALL type bindings from call args before resolving return type