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:
@@ -142,6 +142,11 @@ pub const FnDecl = struct {
|
||||
/// is a REQUIRED parameter, so a parser site cannot drop it; the default
|
||||
/// here serves only post-check synthesized decls (which are never raw).
|
||||
is_raw: bool = false,
|
||||
/// `name :: ufcs (params) { body }` — the fn opted into dot-call
|
||||
/// dispatch (`recv.name(args)`). Dot-calls on free functions are
|
||||
/// OPT-IN: only `is_ufcs` fns and `ufcs` aliases dispatch; a plain
|
||||
/// fn is callable directly or via `|>` only.
|
||||
is_ufcs: bool = false,
|
||||
};
|
||||
|
||||
pub const Param = struct {
|
||||
|
||||
@@ -315,6 +315,36 @@ pub const CallResolver = struct {
|
||||
// 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
|
||||
@@ -335,7 +365,7 @@ pub const CallResolver = struct {
|
||||
},
|
||||
.ambiguous, .none => {},
|
||||
}
|
||||
if (self.l.resolveFuncByName(cfa.field)) |fid| {
|
||||
if (self.l.resolveFuncByName(eff_field)) |fid| {
|
||||
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
||||
return .{
|
||||
.kind = .free_fn_ufcs,
|
||||
@@ -343,14 +373,14 @@ pub const CallResolver = struct {
|
||||
.target = .{ .func = fid },
|
||||
.prepends_receiver = true,
|
||||
.prepends_ctx = func.has_implicit_ctx,
|
||||
.expands_defaults = if (self.l.program_index.fn_ast_map.get(cfa.field)) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
||||
.expands_defaults = if (ufcs_fd) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
||||
};
|
||||
}
|
||||
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
||||
if (ufcs_fd) |bfd| {
|
||||
return .{
|
||||
.kind = .free_fn_ufcs,
|
||||
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||
.target = .{ .named = cfa.field },
|
||||
.target = .{ .named = eff_field },
|
||||
.prepends_receiver = true,
|
||||
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
||||
};
|
||||
|
||||
@@ -263,53 +263,16 @@ pub const GenericResolver = struct {
|
||||
pub fn inferGenericReturnType(self: GenericResolver, fd: *const ast.FnDecl, c: *const ast.Call) TypeId {
|
||||
if (fd.return_type == null) return .void;
|
||||
|
||||
// Build ALL type bindings from call args before resolving return type
|
||||
var tmp_bindings = std.StringHashMap(TypeId).init(self.l.alloc);
|
||||
// ONE binding builder: the same `buildTypeBindings` the lowering /
|
||||
// monomorphization path uses, so plan-side return typing can't
|
||||
// disagree with the instance actually dispatched. (The previous
|
||||
// local strategies only bound BARE `$T` value params — a structured
|
||||
// param (`[]$T`, `*$T`) never bound, so the planned return type of
|
||||
// e.g. `gfirst(xs: []$T) -> T` was the `T` stub and print's Any
|
||||
// boxing mis-tagged the value.)
|
||||
var tmp_bindings = self.buildTypeBindings(fd, c.args);
|
||||
defer tmp_bindings.deinit();
|
||||
|
||||
for (fd.type_params) |tp| {
|
||||
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
|
||||
// Only fires when the caller actually supplied a type expression at
|
||||
// that position; otherwise fall through to value-based inference.
|
||||
var found = false;
|
||||
for (fd.params, 0..) |param, pi| {
|
||||
if (std.mem.eql(u8, param.name, tp.name)) {
|
||||
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.l.module.types)) {
|
||||
const ty = self.l.resolveTypeArg(c.args[pi]);
|
||||
tmp_bindings.put(tp.name, ty) catch {};
|
||||
found = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) continue;
|
||||
|
||||
// Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest
|
||||
var inferred_ty: ?TypeId = null;
|
||||
for (fd.params, 0..) |param, pi| {
|
||||
if (param.type_expr.data == .type_expr) {
|
||||
const te = param.type_expr.data.type_expr;
|
||||
if (std.mem.eql(u8, te.name, tp.name)) {
|
||||
if (pi < c.args.len) {
|
||||
const arg_ty = self.l.inferExprType(c.args[pi]);
|
||||
if (inferred_ty) |prev| {
|
||||
if (arg_ty == .f64 and prev != .f64) {
|
||||
inferred_ty = arg_ty;
|
||||
} else if (arg_ty == .f32 and prev != .f64 and prev != .f32) {
|
||||
inferred_ty = arg_ty;
|
||||
}
|
||||
} else {
|
||||
inferred_ty = arg_ty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inferred_ty) |ty| {
|
||||
tmp_bindings.put(tp.name, ty) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve return type with whatever bindings we built. Even an
|
||||
// empty `tmp_bindings` is a valid input — non-generic literal
|
||||
// return types (e.g. `walk(..$args) -> string`) still need to
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -266,11 +266,18 @@ pub const Parser = struct {
|
||||
return self.parseUnionDecl(name, start_pos, name_is_raw);
|
||||
}
|
||||
|
||||
// UFCS alias: name :: ufcs target;
|
||||
// UFCS forms:
|
||||
// name :: ufcs (params) -> ret { body } — fn declared dot-callable
|
||||
// name :: ufcs target; — dot-callable alias
|
||||
if (self.current.tag == .kw_ufcs) {
|
||||
self.advance();
|
||||
if (self.current.tag == .l_paren) {
|
||||
const node = try self.parseFnDecl(name, name_span, name_is_raw, start_pos);
|
||||
node.data.fn_decl.is_ufcs = true;
|
||||
return node;
|
||||
}
|
||||
if (self.current.tag != .identifier) {
|
||||
return self.fail("expected function name after 'ufcs'");
|
||||
return self.fail("expected '(' (a ufcs function declaration) or a function name (a ufcs alias) after 'ufcs'");
|
||||
}
|
||||
const target = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
|
||||
Reference in New Issue
Block a user