diff --git a/src/ir/calls.test.zig b/src/ir/calls.test.zig index 51026aa..d3b091f 100644 --- a/src/ir/calls.test.zig +++ b/src/ir/calls.test.zig @@ -1,7 +1,15 @@ -// 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. +// Tests for calls.zig. +// +// Two layers: +// 1. Result-type delegation reached via the public `Lowering.inferExprType` +// (builtin / reflection classification, cast, dot-shorthand fallthrough) — +// these need no lexical scope / fn registration. +// 2. The `CallPlan` object built by `CallResolver.plan` — its selected +// kind / target / variant and the receiver / `__sx_ctx` / default-arg +// properties, across every call form pinned by A3.2 sub-step 1 +// (direct / UFCS / protocol / closure / fn-pointer / foreign / enum / +// namespace). `resultType` is just `plan(c).return_type`, so these also +// lock the typing the regression suite relies on. const std = @import("std"); const ast = @import("../ast.zig"); @@ -9,12 +17,49 @@ const Node = ast.Node; const ir_mod = @import("ir.zig"); const TypeId = ir_mod.TypeId; +const FuncId = ir_mod.FuncId; +const Ref = ir_mod.Ref; const Lowering = ir_mod.Lowering; +const CallResolver = ir_mod.CallResolver; +const CallPlan = ir_mod.CallPlan; + +const lower = @import("lower.zig"); +const Scope = lower.Scope; +const Binding = lower.Binding; +const BuiltinId = @import("inst.zig").BuiltinId; fn node(data: ast.Node.Data) Node { return .{ .span = .{ .start = 0, .end = 0 }, .data = data }; } +// ── AST builders (heap-allocated so the call graph outlives one statement) ── + +fn mk(alloc: std.mem.Allocator, data: ast.Node.Data) *Node { + const n = alloc.create(Node) catch unreachable; + n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data }; + return n; +} +fn ident(alloc: std.mem.Allocator, name: []const u8) *Node { + return mk(alloc, .{ .identifier = .{ .name = name } }); +} +fn typeExpr(alloc: std.mem.Allocator, name: []const u8) *Node { + return mk(alloc, .{ .type_expr = .{ .name = name } }); +} +fn intLit(alloc: std.mem.Allocator, v: i64) *Node { + return mk(alloc, .{ .int_literal = .{ .value = v } }); +} +fn emptyBody(alloc: std.mem.Allocator) *Node { + return mk(alloc, .{ .block = .{ .stmts = &.{} } }); +} +fn fieldAccess(alloc: std.mem.Allocator, obj: *Node, field: []const u8) *Node { + return mk(alloc, .{ .field_access = .{ .object = obj, .field = field } }); +} +fn callNode(alloc: std.mem.Allocator, callee: *Node, args: []const *Node) *Node { + return mk(alloc, .{ .call = .{ .callee = callee, .args = args } }); +} + +// ── Layer 1: result-type delegation (no scope / registration needed) ──────── + test "calls: builtin and reflection result types, unknown fallthrough" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); @@ -94,3 +139,303 @@ test "calls: dot-shorthand enum construction types as the target type" { l.target_type = .s32; try std.testing.expectEqual(TypeId.s32, l.inferExprType(&enum_call)); } + +// ── Layer 2: the CallPlan object (kind / target / variant / properties) ───── + +test "plan: builtin and reflection carry kind + target" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + var arg = node(.{ .int_literal = .{ .value = 1 } }); + var args = [_]*Node{&arg}; + + var so_callee = node(.{ .identifier = .{ .name = "size_of" } }); + var so_call = node(.{ .call = .{ .callee = &so_callee, .args = &args } }); + const so = cr.plan(&so_call.data.call); + try std.testing.expectEqual(CallPlan.Kind.builtin, so.kind); + try std.testing.expectEqual(BuiltinId.size_of, so.target.builtin); + try std.testing.expectEqual(TypeId.s64, so.return_type); + + var tn_callee = node(.{ .identifier = .{ .name = "type_name" } }); + var tn_call = node(.{ .call = .{ .callee = &tn_callee, .args = &args } }); + const tn = cr.plan(&tn_call.data.call); + try std.testing.expectEqual(CallPlan.Kind.reflection, tn.kind); + try std.testing.expectEqualStrings("type_name", tn.target.named); + try std.testing.expectEqual(TypeId.string, tn.return_type); +} + +test "plan: unresolved bare callee" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + var callee = node(.{ .identifier = .{ .name = "nope" } }); + var call = node(.{ .call = .{ .callee = &callee, .args = &.{} } }); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.unresolved, p.kind); + try std.testing.expectEqual(TypeId.unresolved, p.return_type); +} + +test "plan: lazy free fn classifies as direct_fn and flags default-arg expansion" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + // greet :: (a: s64, b: s64 = 0) -> s64 — registered but NOT lowered, so + // it resolves through the AST (lazy) arm and `b`'s default is splice-able. + const params = [_]ast.Param{ + .{ .name = "a", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeExpr(alloc, "s64") }, + .{ .name = "b", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeExpr(alloc, "s64"), .default_expr = intLit(alloc, 0) }, + }; + const fd = ast.FnDecl{ .name = "greet", .params = ¶ms, .return_type = typeExpr(alloc, "s64"), .body = emptyBody(alloc) }; + l.program_index.fn_ast_map.put("greet", &fd) catch unreachable; + + // greet(1) — omits `b`, so its default is spliced in. + { + const one = [_]*Node{intLit(alloc, 1)}; + const call = callNode(alloc, ident(alloc, "greet"), &one); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.direct_fn, p.kind); + try std.testing.expectEqualStrings("greet", p.target.named); + try std.testing.expectEqual(TypeId.s64, p.return_type); + try std.testing.expect(p.expands_defaults); + try std.testing.expect(!p.prepends_receiver); + } + // greet(1, 2) — all args supplied, no expansion. + { + const two = [_]*Node{ intLit(alloc, 1), intLit(alloc, 2) }; + const call = callNode(alloc, ident(alloc, "greet"), &two); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.direct_fn, p.kind); + try std.testing.expect(!p.expands_defaults); + } +} + +test "plan: resolved free fn carries func target + __sx_ctx prepend" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + // noop :: () { } — lowered, so it resolves to a concrete FuncId. + const fd = ast.FnDecl{ .name = "noop", .params = &.{}, .return_type = null, .body = emptyBody(alloc) }; + l.lowerFunction(&fd, "noop", false); + const fid = l.resolveFuncByName("noop").?; + // Stamp the implicit-ctx flag the way the implicit-Context machinery would. + module.functions.items[@intFromEnum(fid)].has_implicit_ctx = true; + + var callee = node(.{ .identifier = .{ .name = "noop" } }); + var call = node(.{ .call = .{ .callee = &callee, .args = &.{} } }); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.direct_fn, p.kind); + try std.testing.expectEqual(fid, p.target.func); + try std.testing.expectEqual(TypeId.void, p.return_type); + try std.testing.expect(p.prepends_ctx); +} + +test "plan: closure and fn-pointer callees, __sx_ctx by calling convention" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + l.implicit_ctx_enabled = true; + const cr = CallResolver{ .l = &l }; + + var scope = Scope.init(alloc, null); + defer scope.deinit(); + l.scope = &scope; + + // cb : Closure() -> bool — sx-side closure, carries ctx at slot 0. + const closure_ty = module.types.closureType(&.{}, .bool); + scope.put("cb", .{ .ref = Ref.none, .ty = closure_ty, .is_alloca = false }); + { + const call = callNode(alloc, ident(alloc, "cb"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.closure, p.kind); + try std.testing.expectEqualStrings("cb", p.target.named); + try std.testing.expectEqual(TypeId.bool, p.return_type); + try std.testing.expect(p.prepends_ctx); + } + + // fp : () -> s32 (default conv) — sx fn-pointer, carries ctx. + const fp_ty = module.types.functionType(&.{}, .s32); + scope.put("fp", .{ .ref = Ref.none, .ty = fp_ty, .is_alloca = false }); + { + const call = callNode(alloc, ident(alloc, "fp"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.fn_pointer, p.kind); + try std.testing.expectEqual(TypeId.s32, p.return_type); + try std.testing.expect(p.prepends_ctx); + } + + // cfp : () -> s32 (C conv) — C fn-pointer, NO implicit ctx. + const cfp_ty = module.types.functionTypeCC(&.{}, .s32, .c); + scope.put("cfp", .{ .ref = Ref.none, .ty = cfp_ty, .is_alloca = false }); + { + const call = callNode(alloc, ident(alloc, "cfp"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.fn_pointer, p.kind); + try std.testing.expect(!p.prepends_ctx); + } +} + +test "plan: protocol dispatch selects method index + prepends receiver" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + // Drawable :: protocol { measure :: () -> s64; draw :: () -> bool; } + const methods = [_]ast.ProtocolMethodDecl{ + .{ .name = "measure", .params = &.{}, .param_names = &.{}, .return_type = typeExpr(alloc, "s64"), .default_body = null }, + .{ .name = "draw", .params = &.{}, .param_names = &.{}, .return_type = typeExpr(alloc, "bool"), .default_body = null }, + }; + const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods }; + l.registerProtocolDecl(&pd); + + // A receiver typed as the protocol: `cast(Drawable, _)`. + const recv = callNode(alloc, ident(alloc, "cast"), &[_]*Node{ typeExpr(alloc, "Drawable"), intLit(alloc, 0) }); + const call = callNode(alloc, fieldAccess(alloc, recv, "draw"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.protocol_dispatch, p.kind); + try std.testing.expectEqual(@as(u32, 1), p.target.protocol_method); + try std.testing.expectEqual(TypeId.bool, p.return_type); + try std.testing.expect(p.prepends_receiver); +} + +test "plan: struct (UFCS) method via #compiler dispatch + prepends receiver" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + // struct Point, with a `#compiler` method Point.scale(self) -> s64. + _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &.{} } }); + const self_param = ast.Param{ .name = "self", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeExpr(alloc, "Point") }; + const params = [_]ast.Param{self_param}; + const compiler_body = mk(alloc, .{ .compiler_expr = {} }); + const method_fd = ast.FnDecl{ .name = "Point.scale", .params = ¶ms, .return_type = typeExpr(alloc, "s64"), .body = compiler_body }; + l.program_index.fn_ast_map.put("Point.scale", &method_fd) catch unreachable; + + const recv = callNode(alloc, ident(alloc, "cast"), &[_]*Node{ typeExpr(alloc, "Point"), intLit(alloc, 0) }); + const call = callNode(alloc, fieldAccess(alloc, recv, "scale"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.struct_method, p.kind); + try std.testing.expectEqualStrings("Point.scale", p.target.named); + try std.testing.expectEqual(TypeId.s64, p.return_type); + try std.testing.expect(p.prepends_receiver); +} + +test "plan: foreign-class instance vs static dispatch" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + const members = [_]ast.ForeignClassMember{ + .{ .method = .{ .name = "length", .params = &.{}, .param_names = &.{}, .return_type = typeExpr(alloc, "s64"), .is_static = false } }, + .{ .method = .{ .name = "stringWithUTF8String", .params = &.{}, .param_names = &.{}, .return_type = typeExpr(alloc, "s64"), .is_static = true } }, + }; + var fcd = ast.ForeignClassDecl{ .name = "NSString", .foreign_path = "NSString", .runtime = .objc_class, .members = &members }; + l.program_index.foreign_class_map.put("NSString", &fcd) catch unreachable; + _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("NSString"), .fields = &.{} } }); + + // Instance: `cast(NSString, _).length` — receiver prepended. + { + const recv = callNode(alloc, ident(alloc, "cast"), &[_]*Node{ typeExpr(alloc, "NSString"), intLit(alloc, 0) }); + const call = callNode(alloc, fieldAccess(alloc, recv, "length"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.foreign_instance, p.kind); + try std.testing.expectEqualStrings("length", p.target.foreign_method.name); + try std.testing.expect(!p.target.foreign_method.is_static); + try std.testing.expectEqual(TypeId.s64, p.return_type); + try std.testing.expect(p.prepends_receiver); + } + // Static: `NSString.stringWithUTF8String(...)` — no receiver. + { + const call = callNode(alloc, fieldAccess(alloc, ident(alloc, "NSString"), "stringWithUTF8String"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.foreign_static, p.kind); + try std.testing.expectEqualStrings("stringWithUTF8String", p.target.foreign_method.name); + try std.testing.expect(p.target.foreign_method.is_static); + try std.testing.expect(!p.prepends_receiver); + } +} + +test "plan: enum construction (qualified + dot-shorthand) carries variant tag" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + const red = module.types.internString("Red"); + const green = module.types.internString("Green"); + const variants = [_]@TypeOf(red){ red, green }; + const color = module.types.intern(.{ .@"enum" = .{ .name = module.types.internString("Color"), .variants = &variants } }); + + // Qualified: `Color.Green`. + { + const call = callNode(alloc, fieldAccess(alloc, typeExpr(alloc, "Color"), "Green"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.enum_construct, p.kind); + try std.testing.expectEqual(color, p.target.constructed); + try std.testing.expectEqual(@as(?u32, 1), p.variant); + try std.testing.expectEqual(color, p.return_type); + } + // Dot-shorthand: `.Green` with the union as the target type. + { + l.target_type = color; + const call = callNode(alloc, mk(alloc, .{ .enum_literal = .{ .name = "Green" } }), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.enum_shorthand, p.kind); + try std.testing.expectEqual(color, p.target.constructed); + try std.testing.expectEqual(@as(?u32, 1), p.variant); + try std.testing.expectEqual(color, p.return_type); + } +} + +test "plan: qualified namespace function" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var l = Lowering.init(&module); + const cr = CallResolver{ .l = &l }; + + // mathlib.square :: () -> s64 — registered under its qualified name, lazy. + const fd = ast.FnDecl{ .name = "mathlib.square", .params = &.{}, .return_type = typeExpr(alloc, "s64"), .body = emptyBody(alloc) }; + l.program_index.fn_ast_map.put("mathlib.square", &fd) catch unreachable; + + const call = callNode(alloc, fieldAccess(alloc, ident(alloc, "mathlib"), "square"), &.{}); + const p = cr.plan(&call.data.call); + try std.testing.expectEqual(CallPlan.Kind.namespace_fn, p.kind); + try std.testing.expectEqualStrings("mathlib.square", p.target.named); + try std.testing.expectEqual(TypeId.s64, p.return_type); +} diff --git a/src/ir/calls.zig b/src/ir/calls.zig index 50e53ea..94bbcaa 100644 --- a/src/ir/calls.zig +++ b/src/ir/calls.zig @@ -3,11 +3,77 @@ const ast = @import("../ast.zig"); const types = @import("types.zig"); const type_bridge = @import("type_bridge.zig"); const lower = @import("lower.zig"); +const inst = @import("inst.zig"); const Node = ast.Node; const TypeId = types.TypeId; +const FuncId = inst.FuncId; +const BuiltinId = inst.BuiltinId; const Lowering = lower.Lowering; +/// The classification of a call expression: which dispatch path lowering will +/// take, the IR type the call evaluates to, and the properties (selected +/// target, enum variant, receiver / `__sx_ctx` prepend, default-arg expansion) +/// that path implies. +/// +/// `plan(c)` is the single point that recognises a call form; `resultType(c)` +/// is the thin "just the type" projection (`plan(c).return_type`). This step +/// (A3.2 convergence sub-step 2) builds the plan object and routes typing +/// through it; `lowerCall` still owns its own dispatch and is rerouted onto +/// the plan in sub-step 3. +pub const CallPlan = struct { + kind: Kind, + return_type: TypeId, + target: Target = .none, + /// Enum / tagged-union variant tag, for the construction kinds. + variant: ?u32 = null, + /// Lowering prepends the receiver as arg 0 (UFCS / instance-method forms). + prepends_receiver: bool = false, + /// Lowering prepends the implicit `__sx_ctx` as arg 0. + prepends_ctx: bool = false, + /// The caller omits trailing positional args the callee provides defaults + /// for, so lowering splices them in (`expandCallDefaults` / `appendDefaultArgs`). + expands_defaults: bool = false, + + pub const Kind = enum { + builtin, + reflection, + generic_fn, + /// A plain free function — resolved (`target.func`) or known only by + /// AST and lowered lazily (`target.named`). + direct_fn, + closure, + fn_pointer, + protocol_dispatch, + struct_method, + foreign_instance, + foreign_static, + namespace_fn, + enum_construct, + enum_shorthand, + unresolved, + }; + + /// What `plan` selected. The active arm is disambiguated by `kind`: + /// e.g. a `.named` under `.reflection` is a builtin name, under + /// `.direct_fn` a lazily-lowered fn, under `.closure` a binding. + pub const Target = union(enum) { + none, + builtin: BuiltinId, + /// A resolved (lowered) free / method / namespace function. + func: FuncId, + /// A callee carried by name — reflection builtin, generic / lazy fn, + /// closure / fn-pointer binding, or a not-yet-lowered namespace fn. + named: []const u8, + /// Protocol method, by index in the protocol's method table. + protocol_method: u32, + /// Foreign-class method (Obj-C / JNI), with its static-ness. + foreign_method: struct { name: []const u8, is_static: bool }, + /// Enum / tagged-union type under construction. + constructed: TypeId, + }; +}; + /// 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 @@ -19,14 +85,19 @@ const Lowering = lower.Lowering; /// 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. +/// than re-threading every field. 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 { + return self.plan(c).return_type; + } + + /// Classify a call: pick the dispatch kind / target / variant and derive + /// the result type and prepend / default-expansion properties. The single + /// source of truth for "what kind of call is this?". + pub fn plan(self: CallResolver, c: *const ast.Call) CallPlan { if (c.callee.data == .identifier) { const bare_name = c.callee.data.identifier.name; // Resolve local function name (bare → mangled) and UFCS aliases @@ -38,7 +109,7 @@ pub const CallResolver = struct { break :blk scoped; }; if (Lowering.resolveBuiltin(bare_name)) |bid| { - return switch (bid) { + const rt: TypeId = switch (bid) { .sqrt, .sin, .cos, .floor => blk: { if (c.args.len > 0) { const arg_ty = self.l.inferExprType(c.args[0]); @@ -50,69 +121,98 @@ pub const CallResolver = struct { .cast => if (c.args.len > 0) self.l.resolveTypeArg(c.args[0]) else .unresolved, else => .unresolved, }; + return .{ .kind = .builtin, .return_type = rt, .target = .{ .builtin = bid } }; } - // Reflection builtins live outside `resolveBuiltin`'s - // table (their lowering goes through - // `tryLowerReflectionCall`, not the `BuiltinId` - // dispatch). Recognize them here so pack-fn callers + // 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, "type_name")) return refl(bare_name, .string); + if (std.mem.eql(u8, bare_name, "type_eq")) return refl(bare_name, .bool); + if (std.mem.eql(u8, bare_name, "has_impl")) return refl(bare_name, .bool); + if (std.mem.eql(u8, bare_name, "field_count")) return refl(bare_name, .s64); + if (std.mem.eql(u8, bare_name, "field_index")) return refl(bare_name, .s64); + if (std.mem.eql(u8, bare_name, "field_name")) return refl(bare_name, .string); + if (std.mem.eql(u8, bare_name, "error_tag_name")) return refl(bare_name, .string); + if (std.mem.eql(u8, bare_name, "is_comptime")) return refl(bare_name, .bool); + if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return refl(bare_name, .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 + return refl(bare_name, self.l.module.types.findByName(self.l.module.types.internString("Frame")) orelse .unresolved); + if (std.mem.eql(u8, bare_name, "is_flags")) 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); + // 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); + return .{ + .kind = .generic_fn, + .return_type = self.l.inferGenericReturnType(fd, c), + .target = .{ .named = name }, + .expands_defaults = defaultsFor(fd, c.args.len), + }; } } - // Check declared functions for return type + // Declared (lowered) function — return type from its signature. if (self.l.resolveFuncByName(name)) |fid| { - return self.l.module.functions.items[@intFromEnum(fid)].ret; + const func = &self.l.module.functions.items[@intFromEnum(fid)]; + return .{ + .kind = .direct_fn, + .return_type = func.ret, + .target = .{ .func = fid }, + .prepends_ctx = func.has_implicit_ctx, + .expands_defaults = if (self.l.program_index.fn_ast_map.get(name)) |fd| defaultsFor(fd, c.args.len) else false, + }; } - // Not lowered yet (lazy lowering): take the return type from - // the declared AST. A void/return-less fn is void — not an + // 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; + return .{ + .kind = .direct_fn, + .return_type = if (fd.return_type) |rt| self.l.resolveType(rt) else .void, + .target = .{ .named = name }, + .expands_defaults = defaultsFor(fd, c.args.len), + }; } - // 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. + // Local closure- / function-typed binding (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; + if (ti == .closure) return .{ + .kind = .closure, + .return_type = ti.closure.ret, + .target = .{ .named = bare_name }, + .prepends_ctx = self.l.implicit_ctx_enabled, + }; + if (ti == .function) return .{ + .kind = .fn_pointer, + .return_type = ti.function.ret, + .target = .{ .named = bare_name }, + .prepends_ctx = self.l.implicit_ctx_enabled and ti.function.call_conv != .c, + }; } } } } 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; - } + // Receiver is a protocol type → protocol method dispatch. + if (self.l.getProtocolInfo(recv_ty)) |proto_info| { + for (proto_info.methods, 0..) |m, mi| { + if (std.mem.eql(u8, m.name, cfa.field)) return .{ + .kind = .protocol_dispatch, + .return_type = m.ret_type, + .target = .{ .protocol_method = @intCast(mi) }, + .prepends_receiver = true, + }; } } - // Foreign-class instance method: look up the method's - // declared return type so chained calls (e.g. + // 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; @@ -127,7 +227,12 @@ pub const CallResolver = struct { 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); + return .{ + .kind = .foreign_instance, + .return_type = self.l.resolveForeignMethodReturnType(fcd, md), + .target = .{ .foreign_method = .{ .name = md.name, .is_static = false } }, + .prepends_receiver = true, + }; }, else => {}, }; @@ -135,7 +240,7 @@ pub const CallResolver = struct { } } } - // Instance method call: obj.method(args) → look up StructName.method + // Instance method call: obj.method(args) → StructName.method. { var obj_ty = recv_ty; if (!obj_ty.isBuiltin()) { @@ -147,20 +252,34 @@ pub const CallResolver = struct { 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 + // 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; + return .{ + .kind = .struct_method, + .return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map) else .void, + .target = .{ .named = qualified }, + .prepends_receiver = true, + .expands_defaults = defaultsFor(method_fd, c.args.len + 1), + }; } } if (self.l.resolveFuncByName(qualified)) |fid| { - return self.l.module.functions.items[@intFromEnum(fid)].ret; + const func = &self.l.module.functions.items[@intFromEnum(fid)]; + return .{ + .kind = .struct_method, + .return_type = func.ret, + .target = .{ .func = fid }, + .prepends_receiver = true, + .prepends_ctx = func.has_implicit_ctx, + .expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len + 1) else false, + }; } } } } - // Type.variant(args) — qualified enum construction + // Type.variant(args) — qualified construction; foreign static; or a + // qualified namespace function. const type_name = switch (cfa.object.data) { .identifier => |id| id.name, .type_expr => |te| te.name, @@ -171,7 +290,11 @@ pub const CallResolver = struct { 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); + return .{ + .kind = .foreign_static, + .return_type = self.l.resolveForeignMethodReturnType(fcd, md), + .target = .{ .foreign_method = .{ .name = md.name, .is_static = true } }, + }; }, else => {}, }; @@ -179,37 +302,81 @@ pub const CallResolver = struct { 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; + if (ti == .tagged_union or ti == .@"enum") return .{ + .kind = .enum_construct, + .return_type = ty, + .target = .{ .constructed = ty }, + .variant = self.l.resolveVariantIndex(ty, cfa.field), + }; } - // 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. + // 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 `.unresolved` + // and any pack-fn caller (e.g. `print("{}\n", pkg.hello())`) + // mangles the arg, 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; + const func = &self.l.module.functions.items[@intFromEnum(fid)]; + return .{ + .kind = .namespace_fn, + .return_type = func.ret, + .target = .{ .func = fid }, + .prepends_ctx = func.has_implicit_ctx, + .expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len) else false, + }; } if (self.l.program_index.fn_ast_map.get(qualified)) |qfd| { - if (qfd.return_type) |rt| return self.l.resolveType(rt); - return .void; + return .{ + .kind = .namespace_fn, + .return_type = if (qfd.return_type) |rt| self.l.resolveType(rt) else .void, + .target = .{ .named = qualified }, + .expands_defaults = defaultsFor(qfd, c.args.len), + }; } - // Namespace aliases sometimes register the function - // under its bare name (matches `lowerCall`'s effective- - // name resolution order). + // Namespace aliases sometimes register the function under its + // bare name (matches `lowerCall`'s effective-name resolution). if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| { - if (bfd.return_type) |rt| return self.l.resolveType(rt); - return .void; + return .{ + .kind = .namespace_fn, + .return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void, + .target = .{ .named = cfa.field }, + .expands_defaults = defaultsFor(bfd, c.args.len), + }; } } } else if (c.callee.data == .enum_literal) { - // .Variant(args) — dot-shorthand enum construction - return self.l.target_type orelse .unresolved; + // .Variant(args) — dot-shorthand construction. Result type is + // whatever target type is in scope; absent one it stays unresolved. + const rt = self.l.target_type orelse .unresolved; + var variant: ?u32 = null; + if (self.l.target_type) |tgt| { + if (!tgt.isBuiltin()) { + const ti = self.l.module.types.get(tgt); + if (ti == .tagged_union or ti == .@"enum") + variant = self.l.resolveVariantIndex(tgt, c.callee.data.enum_literal.name); + } + } + return .{ + .kind = .enum_shorthand, + .return_type = rt, + .target = if (variant != null) .{ .constructed = rt } else .none, + .variant = variant, + }; } - return .unresolved; + return .{ .kind = .unresolved, .return_type = .unresolved }; + } + + fn refl(name: []const u8, rt: TypeId) CallPlan { + return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } }; + } + + /// True when a call supplying `supplied` leading params (user args plus a + /// prepended receiver for methods) omits a trailing param the callee + /// defaults — i.e. lowering will splice that default in. + fn defaultsFor(fd: *const ast.FnDecl, supplied: usize) bool { + if (supplied >= fd.params.len) return false; + return fd.params[supplied].default_expr != null; } }; diff --git a/src/ir/ir.zig b/src/ir/ir.zig index 33c46f0..a92eb38 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -42,6 +42,7 @@ pub const ResolveEnv = type_resolver.ResolveEnv; pub const PackResolver = packs.PackResolver; pub const ExprTyper = expr_typer.ExprTyper; pub const CallResolver = calls.CallResolver; +pub const CallPlan = calls.CallPlan; pub const compiler_hooks = @import("compiler_hooks.zig"); pub const emit_llvm = @import("emit_llvm.zig"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a981b29..1455bb8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -48,19 +48,23 @@ fn isExportedEntryName(name: []const u8) bool { // ── Scope ─────────────────────────────────────────────────────────────── -const Binding = struct { +pub const Binding = struct { ref: Ref, ty: TypeId, is_alloca: bool, // true if ref is a pointer that needs load is_ref_capture: bool = false, // `for xs: (*x)` — `ref` is `*elem`; auto-deref in value positions }; -const Scope = struct { +// `init` / `deinit` / `put` are pub so collaborator unit tests (e.g. +// calls.test.zig) can stand up a lexical scope and exercise the +// scope-dependent call forms (closure / fn-pointer callees) without +// driving a full function lowering. +pub const Scope = struct { map: std.StringHashMap(Binding), fn_names: std.StringHashMap([]const u8), // bare name → mangled name for local functions parent: ?*Scope, - fn init(alloc: Allocator, parent: ?*Scope) Scope { + pub fn init(alloc: Allocator, parent: ?*Scope) Scope { return .{ .map = std.StringHashMap(Binding).init(alloc), .fn_names = std.StringHashMap([]const u8).init(alloc), @@ -68,12 +72,12 @@ const Scope = struct { }; } - fn deinit(self: *Scope) void { + pub fn deinit(self: *Scope) void { self.map.deinit(); self.fn_names.deinit(); } - fn put(self: *Scope, name: []const u8, binding: Binding) void { + pub fn put(self: *Scope, name: []const u8, binding: Binding) void { self.map.put(name, binding) catch unreachable; } @@ -5617,7 +5621,7 @@ pub const Lowering = struct { } /// Resolve a variant name to its tag index within an enum or union type. - fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { + pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { if (ty.isBuiltin()) return 0; const info = self.module.types.get(ty); const name_id = self.module.types.internString(variant_name);