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

@@ -0,0 +1,21 @@
// Empty parens `()` as a return type mean `void`: `f :: () -> () { … }` is
// exactly `f :: () -> void { … }`. The zero-parameter FUNCTION type `() -> R`
// (a callable taking no args) is unaffected — only an empty `()` in the
// return-TYPE slot folds to void.
#import "modules/std.sx";
// `-> ()` is void: no value returned.
greet :: () -> () { print("hi\n"); }
// A `-> void` spelling, for contrast — identical behavior.
greet2 :: () -> void { print("bye\n"); }
// Zero-param function-typed parameter still parses as a callable, not void.
run :: (f: () -> i64) -> i64 { return f(); }
main :: () -> i64 {
greet();
greet2();
print("{}\n", run(() => 7));
return 0;
}

View File

@@ -0,0 +1,31 @@
// Multi-return signatures: a function may return MULTIPLE values, written as a
// bare-paren list in the return slot — `-> (i64, bool)` (positional) or
// `-> (sum: i32, bigger: bool)` (named). The returned values use the bare comma
// `return a, b` form (no `.(…)` literal).
//
// The result is consumed either by DESTRUCTURING (`q, r := f()`) or by binding
// it to a single name and reaching the value slots by field (`c := f(); c.sum`).
// A multi-return is return-position-only as a TYPE: a parameter / variable
// annotation `x: (A, B)` is rejected — use `Tuple(…)` for a tuple value.
#import "modules/std.sx";
// Positional multi-return.
divmod :: (a: i64, b: i64) -> (i64, i64) {
return a / b, a % b;
}
// Named multi-return — the slot names ride the signature.
stats :: (a: i32, b: i32) -> (sum: i32, bigger: bool) {
return sum = a + b, bigger = a > b;
}
main :: () -> i64 {
// Destructure the values.
q, r := divmod(17, 5);
print("17 / 5 = {} rem {}\n", q, r);
// …or bind once and reach the named slots by field.
c := stats(40, 2);
print("sum={} bigger={}\n", c.sum, c.bigger);
return 0;
}

View File

@@ -0,0 +1,34 @@
// A multi-return signature may carry an error channel as its LAST slot:
// `-> (i32, bool, !)` returns two values OR fails. This reuses the existing
// value-carrying-failable machinery — the error is always the final slot. A
// single-value failable `-> (T, !)` (one value + error) is exactly `-> T !`,
// NOT a multi-return.
//
// Consume it like any failable: `catch` / `or` / a guarded destructure or
// single bind. The error rides the SEPARATE `!` channel — a bound result holds
// only the VALUE slots, never the error (so `c.doubled` / `c.big`, no `c.err`).
#import "modules/std.sx";
CheckErr :: error { Negative }
// Two values plus an error channel. The success values use the bare comma
// `return` form; `raise` rides the error channel as usual.
classify :: (n: i32) -> (doubled: i32, big: bool, !) {
if n < 0 { raise error.Negative; }
return doubled = n * 2, big = n > 10;
}
main :: () -> i64 {
// Success: destructure the two value slots (the catch diverges on error).
d, b := classify(7) catch { print("unexpected error\n"); return 1; };
print("classify(7): doubled={} big={}\n", d, b);
// Or bind once (error stripped by `catch`) and reach the value slots by
// field — the error is NOT part of the bound value.
c := classify(21) catch { print("unexpected error\n"); return 1; };
print("classify(21): doubled={} big={}\n", c.doubled, c.big);
// Failure: the error path is taken.
classify(-3) catch { print("classify(-3): caught Negative\n"); return 0; };
return 0;
}

View File

@@ -0,0 +1,42 @@
// Named multi-return slots are in-scope assignable LOCALS. Assign each by name
// in the body; with no explicit `return`, the function implicitly returns the
// slot values once they are all set. A named slot that is never assigned (and
// has no default) is a compile error — see the companion negative example.
//
// This is sugar over the multi-return machinery: `b` below is equivalent to
// `return sum = f1 + f2, good = f1 > f2`, but reads as straight-line setup.
#import "modules/std.sx";
// The slot names `sum` / `good` are locals; assigning them IS the return.
combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) {
good = f1 > f2;
sum = f1 + f2;
}
// A slot local can be read after it is set (here `hi` depends on `lo`).
bounds :: (n: i32) -> (lo: i32, hi: i32) {
lo = n;
hi = lo + 10;
}
// Named slots work with an error channel too (the error is the last slot).
Err :: error { Negative }
roots :: (n: i32) -> (val: i32, sq: i32, !) {
if n < 0 { raise error.Negative; }
val = n;
sq = n * n;
}
main :: () -> i64 {
s, g := combine(40, 2);
print("combine: sum={} good={}\n", s, g);
lo, hi := bounds(5);
print("bounds: lo={} hi={}\n", lo, hi);
v, sq := roots(7) catch { print("unexpected\n"); return 1; };
print("roots: val={} sq={}\n", v, sq);
roots(-1) catch { print("roots(-1): caught Negative\n"); return 0; };
return 0;
}

View File

@@ -0,0 +1,16 @@
// Negative: a NAMED multi-return slot that the body never assigns (and that has
// no default) is a compile error. Here `good` is never set, so the implicit
// return cannot be synthesized — the must-set rule rejects it. (Assign every
// slot, give it a default, or end with an explicit `return`.)
#import "modules/std.sx";
combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) {
sum = f1 + f2;
// `good` is never assigned — must-set violation.
}
main :: () -> i64 {
s, g := combine(1, 2);
print("{} {}\n", s, g);
return 0;
}

View File

@@ -0,0 +1,26 @@
// A named multi-return slot may carry a DEFAULT: `-> (sum: i32 = 0, good: bool)`.
// A defaulted slot is EXEMPT from the must-set rule — if the body never assigns
// it, the default seeds it at the implicit return. A non-defaulted slot must
// still be set (see 0206). An explicit assignment overrides the default.
#import "modules/std.sx";
// `sum` defaults to -1; only `good` must be assigned.
classify :: (n: i32) -> (sum: i32 = -1, good: bool) {
good = n > 0;
// `sum` is left unset → the default (-1) is returned.
}
// Both slots default; the body overrides them.
combine :: (a: i32, b: i32) -> (total: i32 = 0, ok: bool = false) {
total = a + b;
ok = true;
}
main :: () -> i64 {
s, g := classify(5);
print("classify(5): sum={} good={}\n", s, g); // default sum -1, good true
t, o := combine(3, 4);
print("combine(3,4): total={} ok={}\n", t, o); // overridden: 7, true
return 0;
}

View File

@@ -0,0 +1,18 @@
// Negative: the named-return must-set rule is PATH-SENSITIVE (definite
// assignment). A slot assigned on only SOME paths is NOT definitely set, so the
// implicit return is rejected at COMPILE time — rather than returning a stale /
// uninitialized value at run time. Here `s` is assigned only inside `if c`, so
// the `c == false` path would leave it unset. (Assign it on every path — e.g.
// add an `else`, give it a default, or use an explicit `return`.)
#import "modules/std.sx";
pick :: (c: bool) -> (s: i64, y: i64) {
if c { s = 10; } // `s` unset on the else path
y = 1;
}
main :: () -> i64 {
a, b := pick(false);
print("{} {}\n", a, b);
return 0;
}

View File

@@ -0,0 +1,8 @@
// Negative: a multi-return's `return` must yield the right number of values.
// Returning a single value where two are required would leave the second slot
// uninitialized, so it is a compile error.
#import "modules/std.sx";
divmod :: (a: i64, b: i64) -> (i64, i64) {
return a / b; // only one value — needs two
}
main :: () -> i64 { q, r := divmod(7, 2); print("{} {}\n", q, r); return 0; }

View File

@@ -0,0 +1,8 @@
// Negative: named return elements must be given in SLOT ORDER. A mismatched
// name would otherwise be matched positionally and silently produce the wrong
// result, so it is rejected. (Here `b` is given where slot `a` is expected.)
#import "modules/std.sx";
pair :: (n: i32) -> (a: i32, b: i32) {
return b = n, a = n + 1; // out of slot order
}
main :: () -> i64 { x, y := pair(5); print("{} {}\n", x, y); return 0; }

View File

@@ -0,0 +1,8 @@
// Negative: a named-return slot may not share a name with a PARAMETER — the slot
// local would silently shadow the parameter. Rename one.
#import "modules/std.sx";
inc :: (sum: i32) -> (sum: i32, ok: bool) {
ok = true;
sum = sum + 1;
}
main :: () -> i64 { s, o := inc(10); print("{} {}\n", s, o); return 0; }

View File

@@ -0,0 +1,7 @@
// Negative: a SINGLE-value function may not be given a comma list — the extra
// values would be silently dropped. (Did you mean to declare `-> (i64, i64)`?)
#import "modules/std.sx";
one :: () -> i64 {
return 1, 2; // single-value return, two given
}
main :: () -> i64 { print("{}\n", one()); return 0; }

View File

@@ -0,0 +1,6 @@
// Negative: a bare-paren `(A, B)` is a multi-return SIGNATURE — valid only as a
// return type, never as a value type. A tuple-valued field/variable/parameter
// uses `Tuple(…)`.
#import "modules/std.sx";
Point :: struct { coords: (i64, i64); } // field value type — rejected
main :: () -> i64 { return 0; }

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
hi
bye
7

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
17 / 5 = 3 rem 2
sum=42 bigger=true

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
classify(7): doubled=14 big=false
classify(21): doubled=42 big=true
classify(-3): caught Negative

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
combine: sum=42 good=true
bounds: lo=5 hi=15
roots: val=7 sq=49
roots(-1): caught Negative

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: named return 'good' may be unset (not assigned on every path) and has no default — assign it on every path, give it a default, or end with an explicit `return`
--> examples/types/0206-types-named-return-must-set.sx:7:57
|
7 | combine :: (f1: i32, f2: i32) -> (sum: i32, good: bool) {
| ^
8 | sum = f1 + f2;
| ^^^^^^^^^^^^^^^^^^
9 | // `good` is never assigned — must-set violation.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | }
| ^

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
classify(5): sum=-1 good=true
combine(3,4): total=7 ok=true

View File

@@ -0,0 +1,11 @@
error: named return 's' may be unset (not assigned on every path) and has no default — assign it on every path, give it a default, or end with an explicit `return`
--> examples/types/0208-types-named-return-conditional-unset.sx:9:39
|
9 | pick :: (c: bool) -> (s: i64, y: i64) {
| ^
10 | if c { s = 10; } // `s` unset on the else path
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | y = 1;
| ^^^^^^^^^^
12 | }
| ^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: this function returns 2 values — return them as `return a, b`, not a single value
--> examples/types/0209-types-multi-return-arity.sx:6:12
|
6 | return a / b; // only one value — needs two
| ^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: named return element 'b' does not match the slot 'a' at position 0 — name the elements in slot order
--> examples/types/0210-types-multi-return-name-order.sx:6:5
|
6 | return b = n, a = n + 1; // out of slot order
| ^^^^^^^^^^^^^^^^^^^^^^^^
error: named return element 'a' does not match the slot 'b' at position 1 — name the elements in slot order
--> examples/types/0210-types-multi-return-name-order.sx:6:5
|
6 | return b = n, a = n + 1; // out of slot order
| ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,5 @@
error: named return 'sum' collides with a parameter of the same name — rename one
--> examples/types/0211-types-named-return-param-collision.sx:4:22
|
4 | inc :: (sum: i32) -> (sum: i32, ok: bool) {
| ^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: this function returns a single value, but a list of 2 was given
--> examples/types/0212-types-single-return-comma.sx:5:5
|
5 | return 1, 2; // single-value return, two given
| ^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
error: a bare-paren `(A, B)` is a multi-return signature, valid only as a return type; a tuple-valued field uses `Tuple(…)`
--> examples/types/0213-types-multi-return-as-value-type.sx:5:27
|
5 | Point :: struct { coords: (i64, i64); } // field value type — rejected
| ^^^^^^^^^^