feat: multiple return values — bare-paren signatures, named returns, must-set, defaults

A function may return multiple values via a bare-paren return signature:
`-> (A, B)` / `-> (x: A, y: B)` / `-> (A, B, !)` (error always the last slot),
and `-> ()` is `void`. This is DISTINCT from a `Tuple(…)` value — return-position
only (a dedicated `ReturnTypeExpr` AST node resolving to a reused `.tuple`
TypeId); a parameter / field / variable annotation `x: (A, B)` is rejected. A
single-value `-> (T, !)` stays a plain failable (= `-> T !`).

Returns use the bare comma form `return a, b` / `return x = a, y = b` (no `.( … )`
literal). Consume by destructuring (`a, b := f()`) or single-bind + field access
(`c := f(); c.sum`); a failable bound value holds only the value slots (the error
stays on the `!` channel).

Named return slots are in-scope assignable locals; with no explicit `return` the
implicit return is synthesized from them. Path-sensitive definite-assignment
enforces the must-set rule, and a slot may carry a default that exempts it.
Validation rejects arity mismatches, out-of-slot-order named elements, a
slot/parameter name collision, a comma list from a single-value function, and a
multi-return signature used as a value type.

Examples 0202-0213; readme + specs updated. issues/0197 files a pre-existing
annotated-assignment type-check gap (`x: i32 = "hi"` segfaults) surfaced by the
adversarial review.
This commit is contained in:
agra
2026-06-27 12:31:23 +03:00
parent c94f878e7e
commit 76689a1ea6
65 changed files with 1236 additions and 48 deletions

View File

@@ -312,6 +312,16 @@ pub const Lowering = struct {
/// Cleared per function body (the `Ref` space is per-function).
narrowed_refs: std.AutoHashMap(Ref, void) = undefined,
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
// Set while lowering a NAMED multi-return function body (`-> (x: A, y: B)`):
// the slot names (1:1 with the return tuple's fields; a trailing "!" marks
// the failable error slot). The slots are bound as in-scope assignable locals;
// at end-of-body with no explicit `return`, `lowerValueBody` synthesizes the
// implicit return from them (must-set rule: an unset, undefaulted slot errors).
named_return_names: ?[]const []const u8 = null,
// Per-slot default exprs (1:1 with the return tuple's fields; null where the
// slot has none). A defaulted named-return slot is seeded with its default
// and exempt from the must-set rule.
named_return_defaults: ?[]const ?*const ast.Node = null,
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`)
defer_stack: std.ArrayList(CleanupEntry) = std.ArrayList(CleanupEntry).empty, // block-scoped defer + onfail cleanup stack
@@ -646,6 +656,12 @@ pub const Lowering = struct {
pub fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId {
if (fd.return_type) |rt| {
// A bare-paren multi-return signature `(A, B)` is valid HERE (return
// position); it resolves to its reused tuple TypeId. Misuse as a VALUE
// type (a param / field / var annotation) is rejected at those sites
// (`resolveParamType` et al.), not in the common resolver — return
// types are re-resolved in many places (call-result typing, protocol
// impls) that a central reject would wrongly trip.
return self.resolveTypeWithBindings(rt);
}
// No explicit annotation — the type is inferred from the body, which
@@ -715,6 +731,19 @@ pub const Lowering = struct {
};
}
/// A bare-paren `(A, B)` multi-return SIGNATURE is valid only as a
/// function/closure return type — never as a VALUE type (a parameter /
/// variable / field annotation), where a tuple value uses `Tuple(…)`. Emits a
/// diagnostic and returns true when `node` is a `ReturnTypeExpr`. (`what` names
/// the offending position, e.g. "parameter" / "variable" / "field".)
pub fn rejectMultiReturnValueType(self: *Lowering, node: *const ast.Node, what: []const u8) bool {
if (node.data != .return_type_expr) return false;
if (self.diagnostics) |d| {
d.addFmt(.err, node.span, "a bare-paren `(A, B)` is a multi-return signature, valid only as a return type; a tuple-valued {s} uses `Tuple(…)`", .{what});
}
return true;
}
pub fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId {
// A plain value param with no annotation can only be typed from
// context (a lambda's target closure signature). When `resolveParamType`
@@ -728,6 +757,9 @@ pub const Lowering = struct {
}
return .unresolved;
}
// A bare-paren `(A, B)` is a MULTI-RETURN signature, valid only as a
// return type — not a parameter value type (use `Tuple(…)`).
if (self.rejectMultiReturnValueType(p.type_expr, "parameter")) return .unresolved;
const declared_ty = self.resolveTypeWithBindings(p.type_expr);
if (p.is_variadic) {
// Two surface forms:
@@ -1834,6 +1866,9 @@ pub const Lowering = struct {
pub const lowerInlineBranch = lower_stmt.lowerInlineBranch;
pub const lowerBlockValue = lower_stmt.lowerBlockValue;
pub const lowerValueBody = lower_stmt.lowerValueBody;
pub const bindNamedReturnSlots = lower_stmt.bindNamedReturnSlots;
pub const synthesizeNamedReturn = lower_stmt.synthesizeNamedReturn;
pub const validateMultiReturn = lower_stmt.validateMultiReturn;
pub const tryLowerAsExpr = lower_stmt.tryLowerAsExpr;
pub const lowerStmt = lower_stmt.lowerStmt;
pub const lowerVarDecl = lower_stmt.lowerVarDecl;