Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
570 lines
31 KiB
Zig
570 lines
31 KiB
Zig
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 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,
|
|
/// Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`, where `fn`
|
|
/// is a plain free function and `recv` is a value (not a namespace /
|
|
/// type prefix). Distinct from `namespace_fn` precisely because the
|
|
/// receiver IS prepended (`prepends_receiver`).
|
|
free_fn_ufcs,
|
|
foreign_instance,
|
|
foreign_static,
|
|
/// `pkg.fn(args)` — the receiver is a namespace / module prefix, NOT a
|
|
/// value, so nothing is prepended.
|
|
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,
|
|
/// The single bare-call author `selectPlainCallableAuthor` selected for a
|
|
/// genuine flat same-name collision (R5 §#3). Carries the resolved
|
|
/// `*FnDecl` + source so `plan` and the lowering call-path read ONE
|
|
/// author and can no longer disagree; the FuncId is
|
|
/// materialized on demand. Only set when the bare name reroutes away from
|
|
/// the first-wins winner; the common path still uses `func` / `named`.
|
|
selected: Lowering.SelectedFunc,
|
|
/// 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
|
|
/// 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.
|
|
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
|
|
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| {
|
|
const rt: TypeId = 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 => .i64,
|
|
.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
|
|
// mangle their results with the right tag.
|
|
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, .i64);
|
|
if (std.mem.eql(u8, bare_name, "field_index")) return refl(bare_name, .i64);
|
|
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 refl(bare_name, self.l.module.types.findByName(self.l.module.types.internString("TraceFrame")) orelse .unresolved);
|
|
if (std.mem.eql(u8, bare_name, "is_flags")) return refl(bare_name, .bool);
|
|
if (std.mem.eql(u8, bare_name, "type_is_unsigned")) 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);
|
|
// Plain bare same-name flat collision (R5 §C): route through the ONE
|
|
// author producer `selectedFreeAuthor` so `plan` types the call as the
|
|
// SAME author the lowering call-path binds — they can no longer
|
|
// disagree. A generic / foreign / builtin author is not
|
|
// plain-free so the producer returns `.none`; `.ambiguous` / `.none`
|
|
// fall through to the first-wins path below, byte-for-byte.
|
|
switch (self.selectedFreeAuthor(c)) {
|
|
.func => |sf| return .{
|
|
.kind = .direct_fn,
|
|
.return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .selected = sf },
|
|
.expands_defaults = defaultsFor(sf.decl, c.args.len),
|
|
},
|
|
.ambiguous, .none => {},
|
|
}
|
|
// 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 .{
|
|
.kind = .generic_fn,
|
|
.return_type = self.l.genericResolver().inferGenericReturnType(fd, c),
|
|
.target = .{ .named = name },
|
|
.expands_defaults = defaultsFor(fd, c.args.len),
|
|
};
|
|
}
|
|
}
|
|
// Declared (lowered) function — return type from its signature.
|
|
if (self.l.resolveFuncByName(name)) |fid| {
|
|
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
|
|
// `.unresolved` guess.
|
|
if (self.l.program_index.fn_ast_map.get(name)) |fd| {
|
|
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),
|
|
};
|
|
}
|
|
// 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 .{
|
|
.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;
|
|
const recv_ty = self.l.inferExprType(cfa.object);
|
|
// 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.
|
|
// `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 .{
|
|
.kind = .foreign_instance,
|
|
.return_type = self.l.resolveForeignMethodReturnType(fcd, md),
|
|
.target = .{ .foreign_method = .{ .name = md.name, .is_static = false } },
|
|
.prepends_receiver = true,
|
|
};
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Instance method call: obj.method(args) → 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) {
|
|
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, &self.l.program_index.module_const_map) else .void,
|
|
.target = .{ .named = qualified },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(method_fd, c.args.len + 1),
|
|
};
|
|
}
|
|
}
|
|
if (self.l.resolveFuncByName(qualified)) |fid| {
|
|
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,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`. lowerCall
|
|
// reaches this only when the receiver is a VALUE (the
|
|
// `is_namespace == false` path), in which case it prepends the
|
|
// receiver and fixes up a `*T` first param. Mirror that boundary so
|
|
// the plan carries `prepends_receiver`, distinct from a true
|
|
// namespace call (`pkg.fn()`), which must NOT prepend.
|
|
if (self.objectIsValue(cfa.object)) {
|
|
// Free-fn dot-dispatch is OPT-IN (mirror lowerCall's gate so
|
|
// plan and dispatch agree): only a `ufcs` alias or a fn
|
|
// declared `name :: ufcs (...)` classifies as free_fn_ufcs.
|
|
// A plain fn falls through (lowering emits the tailored
|
|
// not-a-ufcs-function diagnostic).
|
|
const alias_target = self.l.program_index.ufcs_alias_map.get(cfa.field);
|
|
const eff_field = alias_target orelse cfa.field;
|
|
const ufcs_fd = self.l.program_index.fn_ast_map.get(eff_field);
|
|
const opted_in = alias_target != null or (ufcs_fd != null and ufcs_fd.?.is_ufcs);
|
|
if (!opted_in) return .{ .kind = .unresolved, .return_type = .unresolved };
|
|
// Generic ufcs target: infer the return type with the
|
|
// RECEIVER prepended so binding positions align with
|
|
// fd.params[0] (mirrors the lowering side's eff_args).
|
|
if (ufcs_fd) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
const eff_call_args = self.l.alloc.alloc(*ast.Node, c.args.len + 1) catch
|
|
return .{ .kind = .unresolved, .return_type = .unresolved };
|
|
eff_call_args[0] = cfa.object;
|
|
@memcpy(eff_call_args[1..], c.args);
|
|
var c2 = c.*;
|
|
c2.args = eff_call_args;
|
|
return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = self.l.genericResolver().inferGenericReturnType(fd, &c2),
|
|
.target = .{ .named = eff_field },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(fd, c.args.len + 1),
|
|
};
|
|
}
|
|
}
|
|
// Value-receiver free-fn UFCS (`recv.fn(args)` → `fn(recv, args)`)
|
|
// routes through the SAME author producer `selectedFreeAuthor` as a
|
|
// bare call, so the planned target / return type IS the author
|
|
// lowering dispatches — they can't disagree under a flat same-name
|
|
// collision (R5 §C). Without this, plan typed the
|
|
// first-wins winner while lowering bound the selected shadow,
|
|
// mis-tagging the call's result (a string-typed winner over an i64
|
|
// shadow boxes a raw int as a string pointer → segfault).
|
|
// `.ambiguous` / `.none` fall through to the first-wins path below,
|
|
// unchanged.
|
|
switch (self.selectedFreeAuthor(c)) {
|
|
.func => |sf| return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .selected = sf },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(sf.decl, c.args.len + 1),
|
|
},
|
|
.ambiguous, .none => {},
|
|
}
|
|
if (self.l.resolveFuncByName(eff_field)) |fid| {
|
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
|
return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = func.ret,
|
|
.target = .{ .func = fid },
|
|
.prepends_receiver = true,
|
|
.prepends_ctx = func.has_implicit_ctx,
|
|
.expands_defaults = if (ufcs_fd) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
|
};
|
|
}
|
|
if (ufcs_fd) |bfd| {
|
|
return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .named = eff_field },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
|
};
|
|
}
|
|
}
|
|
// Type.variant(args) — qualified construction; foreign static; or a
|
|
// qualified namespace function. Reached for namespace / type
|
|
// prefixes (and inert for value receivers handled above).
|
|
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 .{
|
|
.kind = .foreign_static,
|
|
.return_type = self.l.resolveForeignMethodReturnType(fcd, md),
|
|
.target = .{ .foreign_method = .{ .name = md.name, .is_static = true } },
|
|
};
|
|
},
|
|
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 .{
|
|
.kind = .enum_construct,
|
|
.return_type = ty,
|
|
.target = .{ .constructed = ty },
|
|
.variant = self.l.resolveVariantIndex(ty, cfa.field),
|
|
};
|
|
}
|
|
// 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| {
|
|
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| {
|
|
// Generic callee: the declared return type is the unbound
|
|
// `T` stub — infer through the call's bindings, exactly
|
|
// like the bare-identifier path above.
|
|
if (qfd.type_params.len > 0) return .{
|
|
.kind = .generic_fn,
|
|
.return_type = self.l.genericResolver().inferGenericReturnType(qfd, c),
|
|
.target = .{ .named = qualified },
|
|
.expands_defaults = defaultsFor(qfd, c.args.len),
|
|
};
|
|
return .{
|
|
.kind = .namespace_fn,
|
|
.return_type = if (qfd.return_type) |rt| self.l.resolveTypeInSource(self.l.program_index.qualified_fn_source.get(qualified), 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).
|
|
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
|
if (bfd.type_params.len > 0) return .{
|
|
.kind = .generic_fn,
|
|
.return_type = self.l.genericResolver().inferGenericReturnType(bfd, c),
|
|
.target = .{ .named = cfa.field },
|
|
.expands_defaults = defaultsFor(bfd, c.args.len),
|
|
};
|
|
return .{
|
|
.kind = .namespace_fn,
|
|
.return_type = if (bfd.return_type) |rt| self.l.resolveTypeInSource(self.l.program_index.qualified_fn_source.get(qualified), 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 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 .{ .kind = .unresolved, .return_type = .unresolved };
|
|
}
|
|
|
|
/// THE single producer of the bare / value-UFCS same-name call author
|
|
/// verdict (R5 §#3). Both `plan` (typing, via its `.selected` arm) and
|
|
/// `lowerCall` (default expansion / param typing / dispatch) consume THIS one
|
|
/// result, so they can never pick different same-name authors for the same
|
|
/// call. Side-effect-free: it consults ONLY the author selector
|
|
/// (`selectPlainCallableAuthor`) — never return-type inference or type-arg
|
|
/// resolution — so `lowerCall` can compute it eagerly without emitting a
|
|
/// premature diagnostic the full `plan` would (e.g. `cast(type)`'s type-arg).
|
|
///
|
|
/// - identifier callee: a plain bare call. The gate mirrors `plan`/`lowerCall`
|
|
/// — a builtin, a scope-mangled / UFCS-aliased name, or a locally-shadowed
|
|
/// name is never a same-name free-fn collision → `.none`.
|
|
/// - field-access callee with a VALUE receiver: a free-function UFCS
|
|
/// (`recv.fn(args)`). A namespace / type prefix receiver → `.none`. The
|
|
/// verdict over-selects a struct-method / protocol / foreign call whose
|
|
/// field happens to name a free fn, but those dispatch BEFORE the free-fn
|
|
/// UFCS path in both `plan` and `lowerCall`, so the verdict is consumed only
|
|
/// when the call truly is a free-fn UFCS.
|
|
pub fn selectedFreeAuthor(self: CallResolver, c: *const ast.Call) Lowering.BareCallee {
|
|
const caller_file = self.l.current_source_file orelse return .none;
|
|
switch (c.callee.data) {
|
|
.identifier => |id| {
|
|
const bare_name = id.name;
|
|
if (Lowering.resolveBuiltin(bare_name) != null) return .none;
|
|
const scoped = if (self.l.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
|
const name = if (self.l.program_index.ufcs_alias_map.get(bare_name)) |target|
|
|
(if (self.l.scope) |scope| scope.lookupFn(target) orelse target else target)
|
|
else
|
|
scoped;
|
|
if (!std.mem.eql(u8, name, bare_name)) return .none;
|
|
if (self.l.scope) |scope| if (scope.lookup(bare_name) != null) return .none;
|
|
return self.l.selectPlainCallableAuthor(bare_name, caller_file);
|
|
},
|
|
.field_access => |cfa| {
|
|
if (!self.objectIsValue(cfa.object)) return .none;
|
|
return self.l.selectPlainCallableAuthor(cfa.field, caller_file);
|
|
},
|
|
else => return .none,
|
|
}
|
|
}
|
|
|
|
fn refl(name: []const u8, rt: TypeId) CallPlan {
|
|
return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } };
|
|
}
|
|
|
|
/// True when a field-access receiver is a value (so `recv.fn(...)` is a
|
|
/// method / UFCS call), false when it is a bare namespace / type prefix
|
|
/// (so `pkg.fn(...)` is a namespace call). This is exactly the negation of
|
|
/// `lowerCall`'s `is_namespace`: a non-identifier object is always a value;
|
|
/// an identifier / type_expr is a value iff it names a local or a global.
|
|
/// `pub` so `lowerCall` sources its namespace/value boundary here rather
|
|
/// than re-deriving it — one definition, shared by typing and lowering.
|
|
pub fn objectIsValue(self: CallResolver, obj: *const Node) bool {
|
|
const obj_name: []const u8 = switch (obj.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
// `alias.Type` (namespace-rooted prefix) is a type head, not a
|
|
// value — `mem.GPA.init()` must take the namespace-call path.
|
|
.field_access => return self.l.namespaceRootedMember(obj) == null,
|
|
else => return true,
|
|
};
|
|
if (self.l.scope) |scope| {
|
|
if (scope.lookup(obj_name) != null) return true;
|
|
}
|
|
return self.l.program_index.global_names.contains(obj_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;
|
|
}
|
|
};
|