diff --git a/examples/1165-diagnostics-generic-return-unbound.sx b/examples/1165-diagnostics-generic-return-unbound.sx new file mode 100644 index 0000000..0116313 --- /dev/null +++ b/examples/1165-diagnostics-generic-return-unbound.sx @@ -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"); } diff --git a/examples/expected/1165-diagnostics-generic-return-unbound.exit b/examples/expected/1165-diagnostics-generic-return-unbound.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1165-diagnostics-generic-return-unbound.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1165-diagnostics-generic-return-unbound.stderr b/examples/expected/1165-diagnostics-generic-return-unbound.stderr new file mode 100644 index 0000000..43bc776 --- /dev/null +++ b/examples/expected/1165-diagnostics-generic-return-unbound.stderr @@ -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 } + | ^^ diff --git a/examples/expected/1165-diagnostics-generic-return-unbound.stdout b/examples/expected/1165-diagnostics-generic-return-unbound.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1165-diagnostics-generic-return-unbound.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 4f36308..9018778 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -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.