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:
21
examples/types/0202-types-void-empty-parens.sx
Normal file
21
examples/types/0202-types-void-empty-parens.sx
Normal 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;
|
||||
}
|
||||
31
examples/types/0203-types-multi-return.sx
Normal file
31
examples/types/0203-types-multi-return.sx
Normal 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;
|
||||
}
|
||||
34
examples/types/0204-types-multi-return-failable.sx
Normal file
34
examples/types/0204-types-multi-return-failable.sx
Normal 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;
|
||||
}
|
||||
42
examples/types/0205-types-named-return-locals.sx
Normal file
42
examples/types/0205-types-named-return-locals.sx
Normal 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;
|
||||
}
|
||||
16
examples/types/0206-types-named-return-must-set.sx
Normal file
16
examples/types/0206-types-named-return-must-set.sx
Normal 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;
|
||||
}
|
||||
26
examples/types/0207-types-named-return-defaults.sx
Normal file
26
examples/types/0207-types-named-return-defaults.sx
Normal 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;
|
||||
}
|
||||
18
examples/types/0208-types-named-return-conditional-unset.sx
Normal file
18
examples/types/0208-types-named-return-conditional-unset.sx
Normal 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;
|
||||
}
|
||||
8
examples/types/0209-types-multi-return-arity.sx
Normal file
8
examples/types/0209-types-multi-return-arity.sx
Normal 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; }
|
||||
8
examples/types/0210-types-multi-return-name-order.sx
Normal file
8
examples/types/0210-types-multi-return-name-order.sx
Normal 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; }
|
||||
@@ -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; }
|
||||
7
examples/types/0212-types-single-return-comma.sx
Normal file
7
examples/types/0212-types-single-return-comma.sx
Normal 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; }
|
||||
6
examples/types/0213-types-multi-return-as-value-type.sx
Normal file
6
examples/types/0213-types-multi-return-as-value-type.sx
Normal 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; }
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
hi
|
||||
bye
|
||||
7
|
||||
1
examples/types/expected/0203-types-multi-return.exit
Normal file
1
examples/types/expected/0203-types-multi-return.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
examples/types/expected/0203-types-multi-return.stderr
Normal file
1
examples/types/expected/0203-types-multi-return.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
examples/types/expected/0203-types-multi-return.stdout
Normal file
2
examples/types/expected/0203-types-multi-return.stdout
Normal file
@@ -0,0 +1,2 @@
|
||||
17 / 5 = 3 rem 2
|
||||
sum=42 bigger=true
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
classify(7): doubled=14 big=false
|
||||
classify(21): doubled=42 big=true
|
||||
classify(-3): caught Negative
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
combine: sum=42 good=true
|
||||
bounds: lo=5 hi=15
|
||||
roots: val=7 sq=49
|
||||
roots(-1): caught Negative
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 | }
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
classify(5): sum=-1 good=true
|
||||
combine(3,4): total=7 ok=true
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 | }
|
||||
| ^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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
|
||||
| ^^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user