refactor(ir): distinguish free-function UFCS from namespace calls in CallPlan (A3.2 review fix)
CallPlan collapsed two different field-access dispatches onto namespace_fn: a true namespace call (`pkg.fn()`, no receiver) and free-function UFCS (`c.bump()`, receiver prepended + `*T` fixup). Return typing was preserved either way, but sub-step 3 could not consume the plan — it would have had to re-classify the AST to decide whether to prepend the receiver. Add a distinct `free_fn_ufcs` kind and a plan(c) branch, inserted after the struct-method block and gated on `objectIsValue` (the negation of lowerCall's `is_namespace`: a non-identifier receiver is always a value; an identifier/type_expr is a value iff it names a local or a global). The branch sets prepends_receiver = true and reads prepends_ctx from the resolved FuncId (best-effort, like direct_fn). namespace_fn now means strictly "receiver is a namespace/type prefix". New test `plan: free-function UFCS prepends receiver, distinct from namespace_fn` covers a scope-bound `c.bump()` against a lowered free fn: asserts free_fn_ufcs kind, func target, prepends_receiver, prepends_ctx, and preserved s32 return type. zig build, zig build test, tests/run_examples.sh (357/0) all green; return typing unchanged.
This commit is contained in:
@@ -420,6 +420,43 @@ test "plan: enum construction (qualified + dot-shorthand) carries variant tag" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "plan: free-function UFCS prepends receiver, distinct from namespace_fn" {
|
||||||
|
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 Counter, and a FREE function `bump :: (c: Counter) -> s32` — NOT
|
||||||
|
// registered as `Counter.bump`, so it can only be reached via UFCS.
|
||||||
|
const counter = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Counter"), .fields = &.{} } });
|
||||||
|
const c_param = ast.Param{ .name = "c", .name_span = .{ .start = 0, .end = 0 }, .type_expr = typeExpr(alloc, "Counter") };
|
||||||
|
const params = [_]ast.Param{c_param};
|
||||||
|
const ret_stmt = mk(alloc, .{ .return_stmt = .{ .value = intLit(alloc, 7) } });
|
||||||
|
const body = mk(alloc, .{ .block = .{ .stmts = &[_]*Node{ret_stmt} } });
|
||||||
|
const fd = ast.FnDecl{ .name = "bump", .params = ¶ms, .return_type = typeExpr(alloc, "s32"), .body = body };
|
||||||
|
l.lowerFunction(&fd, "bump", false);
|
||||||
|
const fid = l.resolveFuncByName("bump").?;
|
||||||
|
module.functions.items[@intFromEnum(fid)].has_implicit_ctx = true;
|
||||||
|
|
||||||
|
// A value receiver in scope: `c : Counter`. `c.bump()` is UFCS, not a
|
||||||
|
// namespace call — the receiver must be prepended.
|
||||||
|
var scope = Scope.init(alloc, null);
|
||||||
|
defer scope.deinit();
|
||||||
|
scope.put("c", .{ .ref = Ref.none, .ty = counter, .is_alloca = false });
|
||||||
|
l.scope = &scope;
|
||||||
|
|
||||||
|
const call = callNode(alloc, fieldAccess(alloc, ident(alloc, "c"), "bump"), &.{});
|
||||||
|
const p = cr.plan(&call.data.call);
|
||||||
|
try std.testing.expectEqual(CallPlan.Kind.free_fn_ufcs, p.kind);
|
||||||
|
try std.testing.expectEqual(fid, p.target.func);
|
||||||
|
try std.testing.expect(p.prepends_receiver);
|
||||||
|
try std.testing.expect(p.prepends_ctx);
|
||||||
|
try std.testing.expectEqual(TypeId.s32, p.return_type);
|
||||||
|
}
|
||||||
|
|
||||||
test "plan: qualified namespace function" {
|
test "plan: qualified namespace function" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
|
|||||||
@@ -46,8 +46,15 @@ pub const CallPlan = struct {
|
|||||||
fn_pointer,
|
fn_pointer,
|
||||||
protocol_dispatch,
|
protocol_dispatch,
|
||||||
struct_method,
|
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_instance,
|
||||||
foreign_static,
|
foreign_static,
|
||||||
|
/// `pkg.fn(args)` — the receiver is a namespace / module prefix, NOT a
|
||||||
|
/// value, so nothing is prepended.
|
||||||
namespace_fn,
|
namespace_fn,
|
||||||
enum_construct,
|
enum_construct,
|
||||||
enum_shorthand,
|
enum_shorthand,
|
||||||
@@ -278,8 +285,37 @@ pub const CallResolver = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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)) {
|
||||||
|
if (self.l.resolveFuncByName(cfa.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 (self.l.program_index.fn_ast_map.get(cfa.field)) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
||||||
|
return .{
|
||||||
|
.kind = .free_fn_ufcs,
|
||||||
|
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||||
|
.target = .{ .named = cfa.field },
|
||||||
|
.prepends_receiver = true,
|
||||||
|
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
// Type.variant(args) — qualified construction; foreign static; or a
|
// Type.variant(args) — qualified construction; foreign static; or a
|
||||||
// qualified namespace function.
|
// qualified namespace function. Reached for namespace / type
|
||||||
|
// prefixes (and inert for value receivers handled above).
|
||||||
const type_name = switch (cfa.object.data) {
|
const type_name = switch (cfa.object.data) {
|
||||||
.identifier => |id| id.name,
|
.identifier => |id| id.name,
|
||||||
.type_expr => |te| te.name,
|
.type_expr => |te| te.name,
|
||||||
@@ -372,6 +408,23 @@ pub const CallResolver = struct {
|
|||||||
return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } };
|
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.
|
||||||
|
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,
|
||||||
|
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
|
/// True when a call supplying `supplied` leading params (user args plus a
|
||||||
/// prepended receiver for methods) omits a trailing param the callee
|
/// prepended receiver for methods) omits a trailing param the callee
|
||||||
/// defaults — i.e. lowering will splice that default in.
|
/// defaults — i.e. lowering will splice that default in.
|
||||||
|
|||||||
Reference in New Issue
Block a user