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

@@ -132,6 +132,59 @@ v : `i2 = ---; // referenced as a type
x : i2 = 3; // bare `i2` in type position is still the int type
```
### Multiple return values
A function can return several values with a bare-paren return signature —
positional `-> (A, B)` or named `-> (x: A, y: B)`. The empty `-> ()` is `void`,
and a trailing `!` is the error channel (always the last slot): `-> (A, B, !)`. A
multi-return is **not** a tuple value — it is a distinct return shape (so a
parameter / field / variable annotation `x: (A, B)` is rejected; use `Tuple(…)`
for an actual tuple value).
```sx
divmod :: (a: i64, b: i64) -> (i64, i64) {
return a / b, a % b; // bare comma return — no `.( … )` literal
}
stats :: (a: i32, b: i32) -> (sum: i32, big: bool) {
return sum = a + b, big = a > b; // named, in slot order
}
```
Consume the result by **destructuring** or by binding it once and reaching the
value slots by **field**:
```sx
q, r := divmod(17, 5); // q = 3, r = 2
c := stats(40, 2); // c.sum = 42, c.big = true
```
For a **failable** multi-return, the error rides the separate `!` channel — a
bound value holds only the value slots, never the error:
```sx
classify :: (n: i32) -> (doubled: i32, big: bool, !) {
if n < 0 { raise error.Bad; }
return doubled = n * 2, big = n > 10;
}
d, b := classify(7) catch (e) { … }; // error stripped by `catch`; d, b are the values
```
**Named returns as locals.** Named slots are in-scope assignable locals; assigning
them *is* the return (no explicit `return` needed). A slot may carry a default,
which exempts it from the must-set rule:
```sx
combine :: (a: i32, b: i32) -> (sum: i32 = 0, good: bool) {
good = a > b;
sum = a + b; // both slots set → implicit return
}
```
A named slot that is **not assigned on every path** and has no default is a
compile error (definite-assignment) — rather than returning an uninitialized
value.
### Structs
```sx