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

@@ -0,0 +1,24 @@
// A `$T`-generic RETURN type with no parameter mentioning `$T` is rejected
// at the declaration: the fn isn't a template (type params derive from
// params), and no call site could ever bind the return. All three declare
// surfaces diagnose: a top-level fn, a struct-body method, and a
// (non-parameterised) impl method. Each used to PANIC the compiler at LLVM
// emission via the `.unresolved` tripwire — even when never called.
#import "modules/std.sx";
make :: () -> $T { 0 }
Foo :: struct {
x: s64;
weird :: (self: *Foo) -> $T { 0 }
}
Show2 :: protocol { show2 :: () -> string; }
IntBox :: struct { v: s64; }
impl Show2 for IntBox {
show2 :: (self: *IntBox) -> string { "x" }
leak :: (self: *IntBox) -> $T { 0 }
}
main :: () { print("ok\n"); }

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
error: generic return type '$T' cannot be bound — 'make' has no parameter mentioning '$T', so no call site can infer it
--> examples/1165-diagnostics-generic-return-unbound.sx:10:15
|
10 | make :: () -> $T { 0 }
| ^^
error: generic return type '$T' cannot be bound — 'Foo.weird' has no parameter mentioning '$T', so no call site can infer it
--> examples/1165-diagnostics-generic-return-unbound.sx:14:30
|
14 | weird :: (self: *Foo) -> $T { 0 }
| ^^
error: generic return type '$T' cannot be bound — 'IntBox.leak' has no parameter mentioning '$T', so no call site can infer it
--> examples/1165-diagnostics-generic-return-unbound.sx:21:32
|
21 | leak :: (self: *IntBox) -> $T { 0 }
| ^^

View File

@@ -1966,6 +1966,41 @@ pub fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8
return fid; 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). /// Declare a function as an extern stub (signature only, no body).
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { 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 // 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); 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 // Foreign declarations with a trailing variadic param map to the C
// calling convention's `...` tail. Drop the variadic param from the // calling convention's `...` tail. Drop the variadic param from the
// IR signature (it has no C-level slot) and set is_variadic. // IR signature (it has no C-level slot) and set is_variadic.