refactor: canonical failable syntax (T, !) — remove the bare -> T ! sugar
The trailing-`!`-after-the-value-type spelling (`-> T !`, `-> Tuple(A,B) !`) was a
redundant second way to write a failable return that the parser folded into the
same AST as the parenthesized `(T, !)` / `(A, B, !)` result list. Remove it so
there is ONE canonical spelling: the error channel always rides as the last slot
of the parenthesized list.
- parser: `parseFnReturnType` no longer folds a trailing `!` after a value type —
it rejects it with a located diagnostic ("a failable return is written `(T, !)`
… not `T !`"). This one chokepoint covers fn declarations, lambdas, fn-pointer
types `(A) -> R`, and closure types `Closure(A) -> R`. The error-ONLY `-> !` /
`-> !ErrSet` form is unaffected (parsed by parseTypeExpr as an error_type_expr).
- migrated every usage to canonical form across library/ + examples/ + issues/ +
tests/: `-> T !E` → `-> (T, !E)`; the value-carrying `-> Tuple(A, B) !` (which
FLATTENED to a multi-value failable) → `-> (A, B, !)`, preserving behavior. A
genuine single-tuple-value failable stays `-> (Tuple(A,B), !)`.
- parser unit tests: the "bare form folds" tests become "bare form is rejected";
canonical-form parse tests retained.
- docs: specs.md §12 + scattered refs and readme.md updated to the `(T, !)` form.
Behavior-preserving (the bare form was sugar for the same AST). Adversarial review
confirmed: rejection complete across all positions, every canonical form works on
both success/error paths, error-only `-> !` intact, no crashes. Full suite green
(unit tests + 850 corpus examples).
This commit is contained in:
35
specs.md
35
specs.md
@@ -903,7 +903,7 @@ VALUE: it is represented internally by a reused tuple TypeId (same ABI), but it
|
||||
is valid ONLY in a function/closure return position — a parameter, field, or
|
||||
variable annotation `x: (A, B)` is rejected (use `Tuple(…)` for a tuple value).
|
||||
A single-value `-> (T, !)` (one value + error) is NOT a multi-return; it is
|
||||
exactly the failable `-> T !`.
|
||||
exactly the failable `-> (T, !)`.
|
||||
|
||||
```sx
|
||||
divmod :: (a: i64, b: i64) -> (i64, i64) { return a / b, a % b; }
|
||||
@@ -1886,7 +1886,7 @@ name :: (params) -> return_type {
|
||||
|
||||
A trailing `!` in the return type marks the function **failable** — it adds a
|
||||
separate error channel alongside the normal returns. The `!` sits **outside**
|
||||
the tuple: `-> T !` (one value), `-> Tuple(T1, T2) !` (multi value), `-> !`
|
||||
the tuple: `-> (T, !)` (one value), `-> (T1, T2, !)` (multi value), `-> !`
|
||||
(void). The `!` is not a wrapper around the value; it is one more return slot.
|
||||
See [§12 Error Handling](#12-error-handling).
|
||||
|
||||
@@ -3154,7 +3154,7 @@ main :: () {
|
||||
|
||||
`main` takes no arguments. Its return type may be any of: void (`()`,
|
||||
`-> ()`, `-> void`, or no annotation), an integer type (POSIX exit code),
|
||||
`-> !` (pure failable), or `-> int_type !` (value-carrying failable).
|
||||
`-> !` (pure failable), or `-> (int_type, !)` (value-carrying failable).
|
||||
The exit code is `0` for void / `-> !` success, the integer return
|
||||
truncated to `u8` otherwise. An error that escapes a failable `main`
|
||||
prints the unhandled-error header + return trace to stderr and exits `1`.
|
||||
@@ -3165,11 +3165,12 @@ See [§12 Error Handling](#12-error-handling).
|
||||
## 12. Error Handling
|
||||
|
||||
sx models recoverable errors as a **separate return channel**, not a wrapped
|
||||
result type. A trailing `!` in a function's return type adds one extra return
|
||||
slot — a `u32` error tag — alongside the normal value slots. This keeps sx's
|
||||
native multi-return ergonomics: `-> Tuple(i32, i64) !` is a function returning
|
||||
two values *and* an error, with no tuple-in-a-wrapper. The `!` sits **outside**
|
||||
the `Tuple`.
|
||||
result type. A `!` as the **last slot** of the parenthesized result list adds one
|
||||
extra return slot — a `u32` error tag — alongside the normal value slots. This
|
||||
keeps sx's native multi-return ergonomics: `-> (i32, i64, !)` is a function
|
||||
returning two values *and* an error, with no tuple-in-a-wrapper. A single-value
|
||||
failable is `-> (T, !)`; an error-only failable is `-> !`. (There is no bare
|
||||
`-> T !` spelling — the error channel always rides inside the `(…, !)` list.)
|
||||
|
||||
This section is the canonical surface reference. The design rationale,
|
||||
trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
|
||||
@@ -3177,10 +3178,10 @@ trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
|
||||
### Failable signatures
|
||||
|
||||
```sx
|
||||
parse_digit :: (s: string) -> i32 ! { ... } // one value + error
|
||||
parse :: (s: string) -> Tuple(i32, i64) ! { ... } // multi-value + error
|
||||
must_init :: () -> ! { ... } // pure failable, no value
|
||||
divide :: (a: i32, b: i32) -> i32 !MathErr { ... } // named set
|
||||
parse_digit :: (s: string) -> (i32, !) { ... } // one value + error
|
||||
parse :: (s: string) -> (i32, i64, !) { ... } // multi-value + error
|
||||
must_init :: () -> ! { ... } // pure failable, no value
|
||||
divide :: (a: i32, b: i32) -> (i32, !MathErr) { ... } // named set
|
||||
```
|
||||
|
||||
The `!` is always the **last** slot. `0` in the error slot means "no error";
|
||||
@@ -3195,7 +3196,7 @@ Two forms of error set:
|
||||
ParseErr :: error { BadDigit, Overflow, Empty };
|
||||
|
||||
// Inferred set — bare `!` collects whatever tags the body raises.
|
||||
quick :: () -> i32 ! {
|
||||
quick :: () -> (i32, !) {
|
||||
if cond raise error.SomeAdHocTag; // mints into the inferred set
|
||||
return 0;
|
||||
}
|
||||
@@ -3393,14 +3394,14 @@ On success exit (fall-through, `return`, `break` / `continue` without an error)
|
||||
it is skipped — only `defer` runs.
|
||||
|
||||
```sx
|
||||
make_handle :: () -> Handle ! {
|
||||
make_handle :: () -> (Handle, !) {
|
||||
h := try open();
|
||||
onfail close(h); // close ONLY on a subsequent failure
|
||||
try configure(h); // fails → onfail runs → close(h)
|
||||
return h; // success → onfail skipped; caller owns h
|
||||
}
|
||||
|
||||
open :: (path: string) -> Handle ! {
|
||||
open :: (path: string) -> (Handle, !) {
|
||||
h := try sys_open(path);
|
||||
onfail (e) { log.warn("init failed for {}: {}", path, e); sys_close(h); }
|
||||
...
|
||||
@@ -3421,9 +3422,9 @@ function, or at top level, is rejected.
|
||||
|
||||
- **Explicit annotation required.** A closure literal's value type is inferred
|
||||
as today, but if its body raises or `try`-escapes, the `!` channel is **not**
|
||||
inferred — declare it (`closure((x: i32) -> i32 ! { ... })`). This keeps
|
||||
inferred — declare it (`closure((x: i32) -> (i32, !) { ... })`). This keeps
|
||||
adding a `raise` from silently changing a lambda's type.
|
||||
- **Program-wide union per shape.** All `Closure(<sig>) -> T !` occurrences
|
||||
- **Program-wide union per shape.** All `Closure(<sig>) -> (T, !)` occurrences
|
||||
with the same signature share one inferred-set node; the SCC pass unions
|
||||
every closure flowing into any matching slot.
|
||||
- **FFI boundary.** A failable closure cannot be assigned to a non-failable
|
||||
|
||||
Reference in New Issue
Block a user