lang: opt-in UFCS — ufcs-marked fns + alias dot-dispatch, generic binding via receiver; one binding builder for plan-side generic returns

This commit is contained in:
agra
2026-06-11 17:04:51 +03:00
parent 84e0fb0752
commit a47ea1416e
27 changed files with 316 additions and 137 deletions

View File

@@ -843,9 +843,15 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
}
}
// Check if receiver is a protocol type → dispatch through vtable/fn_ptrs
// Check if receiver is a protocol type → dispatch through
// vtable/fn_ptrs — but only for the protocol's OWN methods. A
// non-member field falls through to the free-fn ufcs machinery
// (`context.allocator.create(Session)` — a ufcs fn taking the
// protocol value as its first param).
if (self.getProtocolInfo(obj_ty)) |proto_info| {
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty);
if (protocolHasMethod(proto_info, fa.field)) {
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty);
}
}
// Check if receiver is `?Protocol` — for sentinel-shaped
@@ -860,7 +866,9 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
if (opt_info == .optional) {
const pay_ty = opt_info.optional.child;
if (self.getProtocolInfo(pay_ty)) |proto_info| {
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty);
if (protocolHasMethod(proto_info, fa.field)) {
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty);
}
}
}
}
@@ -993,10 +1001,11 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
}
}
// Try to resolve as bare function name (free-function UFCS:
// `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body —
// a function reached ONLY via UFCS would otherwise be declared
// but never emitted (undefined symbol at link).
// Free-function dot-call (`recv.fn(args)` → `fn(recv, args)`)
// is OPT-IN: only a fn declared `name :: ufcs (...) {...}` or a
// `name :: ufcs target;` alias dispatches. A plain fn is
// callable directly or via `|>` only — a dot-call on one gets a
// tailored diagnostic rather than silently becoming a method.
//
// R5 §C: a free-function UFCS target with a
// genuine flat same-name collision dispatches to the author the
@@ -1008,34 +1017,95 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
// (`sel_author` / `cplan.ambiguous_collision`, computed once above)
// rather than re-resolving the field name. `.ambiguous` → loud
// diagnostic; otherwise the existing first-wins lazy path.
const ufcs_fid: ?FuncId = blk_uf: {
const alias_target = self.program_index.ufcs_alias_map.get(fa.field);
const eff_field = alias_target orelse fa.field;
const ufcs_fd = self.program_index.fn_ast_map.get(eff_field);
const ufcs_opted_in = alias_target != null or (ufcs_fd != null and ufcs_fd.?.is_ufcs);
if (ufcs_opted_in) {
if (author_ambiguous) {
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
return Ref.none;
}
if (sel_author) |sf| {
break :blk_uf self.selectedFuncId(sf, fa.field);
}
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
if (!self.lowered_functions.contains(fa.field)) {
self.lazyLowerFunction(fa.field);
// Generic ufcs target: monomorphize with the receiver's AST
// node prepended so bindings align with fd.params[0].
if (ufcs_fd) |fd| {
if (fd.type_params.len > 0) {
var eff_args = std.ArrayList(*const Node).empty;
defer eff_args.deinit(self.alloc);
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
for (c.args) |arg| eff_args.append(self.alloc, arg) catch unreachable;
var gbindings = self.genericResolver().buildTypeBindings(fd, eff_args.items);
defer gbindings.deinit();
const gmangled = self.genericResolver().mangleGenericName(eff_field, fd, &gbindings);
if (!self.lowered_functions.contains(gmangled)) {
self.monomorphizeFunction(fd, gmangled, &gbindings);
}
if (self.resolveFuncByName(gmangled)) |gfid| {
const gfunc = &self.module.functions.items[@intFromEnum(gfid)];
const gret_ty = gfunc.ret;
const gparams = gfunc.params;
// Strip type-decl slots. method_args[0] is the
// receiver (a VALUE — a type-expr receiver
// classifies as a namespace call, never here),
// so fd.params[0] is a value param.
var gvalue_args = std.ArrayList(Ref).empty;
defer gvalue_args.deinit(self.alloc);
gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable;
const types_explicit = method_args.items.len == fd.params.len;
var arg_idx: usize = 1;
for (fd.params[1..]) |p| {
if (isTypeParamDecl(&p, fd.type_params)) {
if (types_explicit) arg_idx += 1;
continue;
}
if (arg_idx < method_args.items.len) {
gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable;
}
arg_idx += 1;
}
self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty);
const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items);
self.coerceCallArgs(final_args, gparams);
return self.builder.call(gfid, final_args, gret_ty);
}
return self.emitError(eff_field, c.callee.span);
}
}
break :blk_uf self.resolveFuncByName(fa.field);
};
if (ufcs_fid) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
// Same implicit address-of as a struct-defined method: if the
// free function's first param is `*T` and the receiver is a
// value `T`, pass its address instead of a by-value copy
const ufcs_fid: ?FuncId = blk_uf: {
if (sel_author) |sf| {
break :blk_uf self.selectedFuncId(sf, eff_field);
}
if (ufcs_fd != null) {
if (!self.lowered_functions.contains(eff_field)) {
self.lazyLowerFunction(eff_field);
}
}
break :blk_uf self.resolveFuncByName(eff_field);
};
if (ufcs_fid) |fid| {
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
// Same implicit address-of as a struct-defined method: if the
// free function's first param is `*T` and the receiver is a
// value `T`, pass its address instead of a by-value copy
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
const final_args = self.prependCtxIfNeeded(func, method_args.items);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
}
return self.emitError(eff_field, c.callee.span);
}
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
const final_args = self.prependCtxIfNeeded(func, method_args.items);
self.coerceCallArgs(final_args, params);
return self.builder.call(fid, final_args, ret_ty);
// A fn by this name exists but is not dot-callable: tailored help.
if (ufcs_fd != null or self.resolveFuncByName(fa.field) != null) {
if (self.diagnostics) |d| {
const id = d.addFmtId(.err, c.callee.span, "'{s}' is not a ufcs function — a plain function does not dispatch via dot-call", .{fa.field});
d.addHelpFmt(id, c.callee.span, null, "call it directly (`{s}(receiver, ...)`), pipe it (`receiver |> {s}(...)`), or declare it `{s} :: ufcs (...) {{ ... }}`", .{ fa.field, fa.field, fa.field });
}
return Ref.none;
}
return self.emitError(fa.field, c.callee.span);
},
@@ -1188,6 +1258,14 @@ pub fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref)
return new_args;
}
fn protocolHasMethod(proto_info: anytype, name: []const u8) bool {
for (proto_info.methods) |m| {
if (std.mem.eql(u8, m.name, name)) return true;
}
return false;
}
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;