issue 0151 RESOLVED: infer generic $T through generic-struct / pointer / UFCS-pack params

The generic-inference engine could not bind a $T from a generic-struct
argument head. Four gaps, all on the inference + UFCS dispatch path:

- extractTypeParam / matchTypeParam(Static) gained a parameterized_type_expr
  arm: recover the arg instance's recorded per-param bindings
  (struct_instance_bindings + the template's ordered type_params via
  struct_instance_author) and recurse positionally, so $T binds from
  Box($T) <=> Box(i64) like it does from []$T <=> []i64. This also fixes
  the pointer case — *Box($T) recurses into its Box($T) pointee.
- The pointer_type_expr arm now falls through to match the pointee against a
  non-pointer arg (auto-address-of: a *Box($T) param accepts a by-value
  Box($T), e.g. the UFCS receiver b.m()).
- ExprTyper.inferType gained a .lambda arm building the closure type from the
  lambda's annotations, so the UFCS binder (which types args from the raw AST
  before they are lowered) can bind a Closure(..) -> $R from the worker's
  declared return type.
- A pack UFCS target (worker: Closure(..) -> $R, ..$args) now routes through
  the same lowerPackFnCall the direct call uses, with the receiver spliced in
  as args[0] (lowerPackFnCall reads only call_node.args, never the callee).

Regression tests: examples/0214 (direct + UFCS closure-return pack) and
examples/0215 (by-value / pointer / multi-param / nested / UFCS-auto-ref
generic-struct-head inference). Suite green 728/0.
This commit is contained in:
agra
2026-06-21 05:25:39 +03:00
parent 0ab26c8a40
commit 362674f04d
13 changed files with 190 additions and 24 deletions

View File

@@ -401,6 +401,29 @@ pub const ExprTyper = struct {
}
break :blk self.l.inferExprType(nc.rhs);
},
// A lambda literal's type is the closure it denotes, recovered from
// its annotations. The generic-call binder types args from the raw
// AST (notably the UFCS path, before args are lowered), so without
// this a `Closure(..) -> $R` worker couldn't bind `$R` from the
// lambda's declared return type (issue 0151). An unannotated param /
// body-inferred return stays `.unresolved` here — that arg simply
// doesn't contribute a binding, exactly as before.
.lambda => |lam| blk: {
var pbuf = std.ArrayList(TypeId).empty;
defer pbuf.deinit(self.l.alloc);
for (lam.params) |p| {
const pty: TypeId = if (p.type_expr.data == .inferred_type)
.unresolved
else
self.l.resolveTypeWithBindings(p.type_expr);
pbuf.append(self.l.alloc, pty) catch {};
}
const ret: TypeId = if (lam.return_type) |rt|
self.l.resolveTypeWithBindings(rt)
else
.unresolved;
break :blk self.l.module.types.closureType(pbuf.items, ret);
},
// Inline asm result type (0→void, 1→T, N→named tuple) — the single
// owner is `Lowering.asmResultType`, shared with `lowerAsmExpr` so a
// `return asm`, a `x := asm`, and a `q, r := asm` destructure all

View File

@@ -1033,6 +1033,25 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
return Ref.none;
}
// A pack ufcs target (`worker: Closure(..) -> $R, ..$args`):
// route through the SAME pack-call path the direct call uses,
// with the receiver spliced in as the first arg so the pack
// boundary, the `$R` closure-return binding, and the pack
// expansion all line up with `fd.params[0]` (issue 0151).
// `lowerPackFnCall` reads only `call_node.args` (never the
// callee), so a synthetic spliced-args call is sufficient.
if (ufcs_fd) |fd| {
if (isPackFn(fd)) {
// `lowerPackFnCall` only READS these nodes; the const-cast
// back to `*Node` (Call.args' element type) is sound.
var syn_args = std.ArrayList(*Node).empty;
defer syn_args.deinit(self.alloc);
syn_args.append(self.alloc, @constCast(effective_obj_node)) catch unreachable;
for (c.args) |a| syn_args.append(self.alloc, a) catch unreachable;
const syn_call = ast.Call{ .callee = c.callee, .args = syn_args.items };
return self.lowerPackFnCall(fd, &syn_call);
}
}
// Generic ufcs target: monomorphize with the receiver's AST
// node prepended so bindings align with fd.params[0].
if (ufcs_fd) |fd| {

View File

@@ -537,6 +537,10 @@ pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8)
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
break :blk false;
},
.parameterized_type_expr => |pt| blk: {
for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true;
break :blk false;
},
else => false,
};
}
@@ -555,6 +559,10 @@ pub fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool {
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
break :blk false;
},
.parameterized_type_expr => |pt| blk: {
for (pt.args) |a| if (matchTypeParamStatic(a, tp_name)) break :blk true;
break :blk false;
},
else => false,
};
}
@@ -583,7 +591,11 @@ pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId,
const info = self.module.types.get(arg_ty);
break :blk switch (info) {
.pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name),
else => null,
// Auto-address-of: a `*Box($T)` param accepts a by-value
// `Box($T)` arg (the UFCS receiver `b.m()` / a value passed to a
// pointer param). Match the pointee against the value arg so the
// type-var still binds (issue 0151).
else => self.extractTypeParam(pt.pointee_type, arg_ty, tp_name),
};
},
.many_pointer_type_expr => |mp| blk: {
@@ -628,6 +640,33 @@ pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId,
}
break :blk null;
},
.parameterized_type_expr => |pt| blk: {
// A generic-struct param head (`Box($T)`, also reached recursively
// for a pointer-wrapped `*Box($T)`): the arg is a monomorphized
// instance whose per-param bindings were recorded at instantiation
// (`struct_instance_bindings`). Recover the concrete type the i-th
// template param bound and recurse against the i-th param-head arg,
// so `$T` is inferred from `Box($T)` ⇔ `Box(i64)` exactly as it is
// from `[]$T` ⇔ `[]i64` (issue 0151).
if (arg_ty.isBuiltin()) break :blk null;
const info = self.module.types.get(arg_ty);
if (info != .@"struct") break :blk null;
const inst_name = self.module.types.getString(info.@"struct".name);
const binds = self.struct_instance_bindings.getPtr(inst_name) orelse break :blk null;
// The param head must name the same template the arg instance was
// stamped from, so the positional args line up with the params.
const tmpl_name = self.struct_instance_template.get(inst_name) orelse break :blk null;
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
if (!std.mem.eql(u8, base_name, tmpl_name)) break :blk null;
const author = self.struct_instance_author.get(inst_name) orelse break :blk null;
for (author.type_params, 0..) |atp, i| {
if (i >= pt.args.len) break;
if (atp.is_variadic) break; // type-pack params not inferred here
const concrete = binds.get(atp.name) orelse continue;
if (self.extractTypeParam(pt.args[i], concrete, tp_name)) |ety| break :blk ety;
}
break :blk null;
},
else => null,
};
}