refactor(ir): add CallPlan + CallResolver.plan(c); resultType delegates (A3.2 convergence step 2)
Introduce CallPlan — the single classification record for a call: kind (14 variants), return_type, a Target union (builtin/func/named/protocol_method/ foreign_method/constructed/none), variant tag, and the prepends_receiver / prepends_ctx / expands_defaults properties the selected dispatch implies. Move call recognition into CallResolver.plan(c) (branch order preserved exactly) and reimplement resultType(c) as plan(c).return_type — the typing consumer converges onto the plan first. lowerCall is untouched; routing it through plan(c) is sub-step 3. 10 plan-object tests assert kind/target/variant + receiver/ctx/default properties for every pinned call form: builtin/reflection, lazy + resolved direct fn (incl. default-arg expansion + __sx_ctx prepend), closure / default-conv vs C-conv fn-pointer, protocol dispatch, struct/UFCS #compiler method, foreign instance vs static, qualified + dot-shorthand enum construction, namespace fn, and the unresolved fallthrough. Widen for the new collaborator only: resolveVariantIndex -> pub (plan resolves the variant tag); Scope/Binding + init/deinit/put -> pub (so unit tests can stand up a lexical scope for closure/fn-ptr callees without a full lowering). zig build, zig build test, and tests/run_examples.sh (357/0) all green; no behavior change.
This commit is contained in:
311
src/ir/calls.zig
311
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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user