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:
46
src/ir/calls.test.zig
Normal file
46
src/ir/calls.test.zig
Normal 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
215
src/ir/calls.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
207
src/ir/lower.zig
207
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
|
||||
|
||||
Reference in New Issue
Block a user