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

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