refactor(ir): extract CallResolver for call result typing (A3.2 relocation)

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.
This commit is contained in:
agra
2026-06-02 18:44:08 +03:00
parent 7d069107c8
commit 7f3a7b35ef
4 changed files with 275 additions and 196 deletions

46
src/ir/calls.test.zig Normal file
View File

@@ -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));
}
}

215
src/ir/calls.zig Normal file
View File

@@ -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;
}
};

View File

@@ -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");

View File

@@ -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