lang: reject unbindable $T-only generic returns at declaration (audit follow-up)

This commit is contained in:
agra
2026-06-11 15:00:52 +03:00
parent 40805e08ec
commit ca5bd52262
5 changed files with 95 additions and 0 deletions

View File

@@ -1966,6 +1966,41 @@ pub fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8
return fid;
}
/// Walk a return-type expression for a `$T` generic leaf, returning the
/// first generic name found. The parser builds `fd.type_params` from
/// PARAMS only (`collectTypeParams`), so a `$`-generic that appears ONLY
/// in the return type makes the fn look non-generic while its return can
/// never be bound — `declareFunction` rejects that shape loudly.
fn returnGenericLeaf(node: *const Node) ?[]const u8 {
return switch (node.data) {
.type_expr => |te| if (te.is_generic) te.name else null,
.pointer_type_expr => |pte| returnGenericLeaf(pte.pointee_type),
.many_pointer_type_expr => |mpte| returnGenericLeaf(mpte.element_type),
.slice_type_expr => |ste| returnGenericLeaf(ste.element_type),
.array_type_expr => |ate| returnGenericLeaf(ate.element_type),
.optional_type_expr => |ote| returnGenericLeaf(ote.inner_type),
.parameterized_type_expr => |pte| {
for (pte.args) |arg| if (returnGenericLeaf(arg)) |n| return n;
return null;
},
.tuple_type_expr => |tte| {
for (tte.field_types) |ft| if (returnGenericLeaf(ft)) |n| return n;
return null;
},
.closure_type_expr => |cte| {
for (cte.param_types) |pt| if (returnGenericLeaf(pt)) |n| return n;
if (cte.return_type) |rt| return returnGenericLeaf(rt);
return null;
},
.function_type_expr => |fte| {
for (fte.param_types) |pt| if (returnGenericLeaf(pt)) |n| return n;
if (fte.return_type) |rt| return returnGenericLeaf(rt);
return null;
},
else => null,
};
}
/// Declare a function as an extern stub (signature only, no body).
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
// Skip generic templates — they're monomorphized on demand, not declared as extern
@@ -1973,6 +2008,23 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
const ret_ty = self.resolveReturnType(fd);
// A `$T`-generic return with NO parameter mentioning `$T`: the fn isn't
// a template (the guard above runs on param-derived `type_params`) yet
// its return can never be bound by any call site. Declaring it would
// carry the `.unresolved` sentinel into LLVM emission and panic the
// tripwire — diagnose at the declaration instead. Named unknown types
// (`-> Bogus`) are covered by the semantic pass's "unknown type".
if (ret_ty == .unresolved) {
if (fd.return_type) |rtn| {
if (returnGenericLeaf(rtn)) |gen_name| {
if (self.diagnostics) |d| {
d.addFmt(.err, rtn.span, "generic return type '${s}' cannot be bound — '{s}' has no parameter mentioning '${s}', so no call site can infer it", .{ gen_name, name, gen_name });
}
return;
}
}
}
// Foreign declarations with a trailing variadic param map to the C
// calling convention's `...` tail. Drop the variadic param from the
// IR signature (it has no C-level slot) and set is_variadic.