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:
agra
2026-06-27 18:11:20 +03:00
parent b322dcfe61
commit 213cedf0b5
53 changed files with 184 additions and 232 deletions

View File

@@ -263,7 +263,7 @@ is_long_flag :: (s: string) -> bool {
// Parse `args` (the logical argv) against the `commands` table, writing
// the offending token into `diag` on the error path. See the section
// header for grammar, failure contract, and heap discipline.
parse :: (args: []string, commands: []Command, diag: *Diag) -> Parsed !CliError {
parse :: (args: []string, commands: []Command, diag: *Diag) -> (Parsed, !CliError) {
// ── Dispatch: match (args[0], args[1]) against the command table ──
if args.len < 2 {
diag.index = if args.len == 0 then -1 else 0;

View File

@@ -116,7 +116,7 @@ Loop :: struct {
// long-lived-container rule).
own: Allocator;
init :: () -> Loop !EventErr {
init :: () -> (Loop, !EventErr) {
e := ep.ep_create();
if e < 0 { raise error.Init; }
return Loop.{ epfd = e, regs = .{}, own = context.allocator };
@@ -216,7 +216,7 @@ Loop :: struct {
// Fill `out` with ready events, waiting at most `timeout_ms`
// (negative = forever). Returns the count; 0 is a timeout.
wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> i64 !EventErr {
wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> (i64, !EventErr) {
raw : [64]ep.EpollEvent = ---;
cap : i64 = 64;
if xx out.len < cap { cap = xx out.len; }
@@ -254,7 +254,7 @@ Loop :: struct {
Loop :: struct {
kq: i32 = -1;
init :: () -> Loop !EventErr {
init :: () -> (Loop, !EventErr) {
q := kqb.kqueue();
if q < 0 { raise error.Init; }
return Loop.{ kq = q };
@@ -298,7 +298,7 @@ Loop :: struct {
// Fill `out` with ready events, waiting at most `timeout_ms`
// (negative = forever). Returns the count; 0 is a timeout.
wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> i64 !EventErr {
wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> (i64, !EventErr) {
raw : [64]kqb.Kevent = ---;
cap : i64 = 64;
if xx out.len < cap { cap = xx out.len; }

View File

@@ -263,7 +263,7 @@ Server :: struct {
ctx: usize = 0;
ps: *PoolState = null; // non-null iff cfg.thread_pool_count > 0
init :: (cfg: Config, handler: (*Request, *Response, usize) -> void, ctx: usize) -> Server !HttpErr {
init :: (cfg: Config, handler: (*Request, *Response, usize) -> void, ctx: usize) -> (Server, !HttpErr) {
lfd := socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0);
if lfd < 0 { raise error.Bind; }
one : i32 = 1;

View File

@@ -172,7 +172,7 @@ async :: ufcs (io: Io, worker: Closure() -> $R) -> *Future($R) {
// resumes it. Re-checks state after the wake (the worker set `.ready` before
// waking). A worker that finished BEFORE `await` leaves `.ready`, so no park, no
// lost wakeup.
await :: ufcs (f: *Future($R)) -> $R !IoErr {
await :: ufcs (f: *Future($R)) -> ($R, !IoErr) {
if f.canceled.load(.acquire) { raise error.Canceled; }
if f.state == .pending {
// ONE awaiter per future (M:1): the single `park` slot records one parked

View File

@@ -321,7 +321,7 @@ write_object :: (obj: Object, sink: *Sink) -> !JsonError {
// bytes written. Raises `error.Overflow` if `dst` is too small (the
// partial contents of `dst` are then undefined — nothing is truncated
// silently). No allocation.
write_to_buffer :: (v: Value, dst: []u8) -> i64 !JsonError {
write_to_buffer :: (v: Value, dst: []u8) -> (i64, !JsonError) {
sink := Sink.{ dst = dst };
try write_value(v, @sink);
return sink.pos;
@@ -386,7 +386,7 @@ JsonParseError :: error { UnexpectedToken, UnexpectedEnd, BadEscape, BadNumber,
// Lowercase/uppercase hex nibble value (0..15) of an ASCII byte; a non-hex
// byte in a `\uXXXX` escape is a `BadEscape`.
hex_value :: (c: u8) -> i64 !JsonParseError {
hex_value :: (c: u8) -> (i64, !JsonParseError) {
if c >= 48 and c <= 57 { return (cast(i64) c) - 48; } // '0'..'9'
if c >= 97 and c <= 102 { return (cast(i64) c) - 97 + 10; } // 'a'..'f'
if c >= 65 and c <= 70 { return (cast(i64) c) - 65 + 10; } // 'A'..'F'
@@ -450,7 +450,7 @@ Parser :: struct {
// Read 4 hex digits at `i` (which must lie within [.., end)); returns
// the 16-bit value. Fewer than 4 digits before `end` is a BadEscape.
read_hex4 :: (self: *Parser, i: i64, end: i64) -> i64 !JsonParseError {
read_hex4 :: (self: *Parser, i: i64, end: i64) -> (i64, !JsonParseError) {
if i + 4 > end { raise error.BadEscape; }
v := 0;
k := 0;
@@ -464,7 +464,7 @@ Parser :: struct {
// Decode the escaped string body in [start, end) into `out`, returning
// the decoded byte length. Pass 1 (in parse_string) guarantees there is
// no dangling backslash, so the byte after every `\` is in range.
decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> i64 !JsonParseError {
decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> (i64, !JsonParseError) {
di := 0;
i := start;
while i < end {
@@ -511,7 +511,7 @@ Parser :: struct {
// a zero-copy VIEW into `src` when the body has no escapes; otherwise
// decodes into an `alloc`-ed buffer (bounded by the raw span). `pos`
// ends just past the closing quote.
parse_string :: (self: *Parser) -> string !JsonParseError {
parse_string :: (self: *Parser) -> (string, !JsonParseError) {
self.pos += 1; // consume opening quote
start := self.pos;
has_escape := false;
@@ -547,7 +547,7 @@ Parser :: struct {
// Parse an i64 integer (optional '-', then digits). Rejects leading
// zeros, a fraction/exponent tail, and any value outside i64 — all
// `BadNumber`. Accumulates in NEGATIVE space so i64 MIN parses exactly.
parse_number :: (self: *Parser) -> i64 !JsonParseError {
parse_number :: (self: *Parser) -> (i64, !JsonParseError) {
// i64 bounds, built positionally because |MIN| is not a
// representable positive i64 literal. `min_div10` is `MIN / 10`
// truncated toward zero (remainder -8) — the digit loop's overflow
@@ -585,7 +585,7 @@ Parser :: struct {
}
// Parse an array starting at '['. Builds an `Array` through `alloc`.
parse_array :: (self: *Parser) -> Value !JsonParseError {
parse_array :: (self: *Parser) -> (Value, !JsonParseError) {
self.pos += 1; // consume '['
arr : Array = .{};
self.skip_ws();
@@ -609,7 +609,7 @@ Parser :: struct {
// Parse an object starting at '{'. Keys must be strings; insertion
// order is preserved (duplicate keys are kept, never merged).
parse_object :: (self: *Parser) -> Value !JsonParseError {
parse_object :: (self: *Parser) -> (Value, !JsonParseError) {
self.pos += 1; // consume '{'
obj : Object = .{};
self.skip_ws();
@@ -640,7 +640,7 @@ Parser :: struct {
}
// Parse any single value (after skipping leading whitespace).
parse_value :: (self: *Parser) -> Value !JsonParseError {
parse_value :: (self: *Parser) -> (Value, !JsonParseError) {
self.skip_ws();
if self.pos >= self.src.len { raise error.UnexpectedEnd; }
c := self.src[self.pos];
@@ -659,7 +659,7 @@ Parser :: struct {
// `alloc` for composite nodes and decoded (escaped) strings. Un-escaped
// string values are VIEWS into `src` and are valid only while `src` lives.
// Trailing non-whitespace after the value raises `error.TrailingGarbage`.
parse :: (src: string, alloc: Allocator) -> Value !JsonParseError {
parse :: (src: string, alloc: Allocator) -> (Value, !JsonParseError) {
p := Parser.{ src = src, alloc = alloc };
v := try p.parse_value();
p.skip_ws();

View File

@@ -1079,7 +1079,7 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) {
// Suspend the caller until the task completes; return its value (or raise on
// cancel). MUST be called from inside a fiber (so there is a `self.current` to
// park) — typically from a fiber spawned via `s.spawn(...)`.
wait :: ufcs (t: *Task($R)) -> $R !TaskErr {
wait :: ufcs (t: *Task($R)) -> ($R, !TaskErr) {
if t.canceled != 0 { raise error.Canceled; }
if t.state == .pending {
// ONE waiter per task (enforced). A `Task` holds a single `waiter` slot;

View File

@@ -93,7 +93,7 @@ SockErr :: error {
// Accept one pending connection on a nonblocking listener. A connection
// that died between queueing and accept (ECONNABORTED) is skipped, not
// surfaced — the listener is fine.
accept_nb :: (fd: i32) -> i32 !SockErr {
accept_nb :: (fd: i32) -> (i32, !SockErr) {
while true {
c := accept(fd, null, null);
if c >= 0 { return c; }
@@ -107,7 +107,7 @@ accept_nb :: (fd: i32) -> i32 !SockErr {
// Read up to `cap` bytes. Returns the byte count (> 0); an orderly EOF
// or a peer reset is Closed.
read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> i64 !SockErr {
read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> (i64, !SockErr) {
while true {
n := read(fd, buf, cap);
if n > 0 { return xx n; }
@@ -123,7 +123,7 @@ read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> i64 !SockErr {
// Write up to `len` bytes, returning how many the kernel took (possibly
// fewer — the caller continues from there on the next writability).
write_nb :: (fd: i32, buf: [*]u8, len: usize) -> i64 !SockErr {
write_nb :: (fd: i32, buf: [*]u8, len: usize) -> (i64, !SockErr) {
while true {
n := write(fd, buf, len);
if n >= 0 { return xx n; }

View File

@@ -106,7 +106,7 @@ Thread :: struct {
// `entry` is the C->sx boundary: abi(.c), fabricates its own
// Context before touching default-conv sx code (examples/1636).
spawn :: (entry: (*void) -> *void abi(.c), arg: *void) -> Thread !ThreadErr {
spawn :: (entry: (*void) -> *void abi(.c), arg: *void) -> (Thread, !ThreadErr) {
t : Thread = .{};
if pthread_create(@t.handle, null, entry, arg) != 0 { raise error.Spawn; }
return t;
@@ -144,7 +144,7 @@ Pool :: struct {
// Heap-allocate (the pool must never move: workers hold its address,
// and it embeds a live mutex), init in place, spawn the workers.
create :: (workers: i64, backlog: i64) -> *Pool !ThreadErr {
create :: (workers: i64, backlog: i64) -> (*Pool, !ThreadErr) {
alloc := context.allocator;
p : *Pool = xx alloc.alloc_bytes(size_of(Pool));
p.* = Pool.{};

View File

@@ -461,7 +461,7 @@ SqliteStmt :: struct {
// ── execution ──
// SQLITE_ROW / SQLITE_DONE on success; anything else raises with the
// detail left in the connection's errmsg.
step :: (self: *SqliteStmt) -> i32 !SqliteErr {
step :: (self: *SqliteStmt) -> (i32, !SqliteErr) {
rc := sqlite3_step(self.handle);
if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; }
return rc;
@@ -564,7 +564,7 @@ ColumnMeta :: struct {
Sqlite :: struct {
handle: usize;
open :: (path: string) -> Sqlite !SqliteErr {
open :: (path: string) -> (Sqlite, !SqliteErr) {
h : usize = 0;
rc := sqlite3_open(to_cstring(path), @h);
if rc != SQLITE_OK {
@@ -574,7 +574,7 @@ Sqlite :: struct {
return Sqlite.{ handle = h };
}
open_v2 :: (path: string, flags: i32) -> Sqlite !SqliteErr {
open_v2 :: (path: string, flags: i32) -> (Sqlite, !SqliteErr) {
h : usize = 0;
rc := sqlite3_open_v2(to_cstring(path), @h, flags, 0);
if rc != SQLITE_OK {
@@ -603,7 +603,7 @@ Sqlite :: struct {
return;
}
prepare :: (self: *Sqlite, sql: string) -> SqliteStmt !SqliteErr {
prepare :: (self: *Sqlite, sql: string) -> (SqliteStmt, !SqliteErr) {
sh : usize = 0;
rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0);
if rc != SQLITE_OK { raise error.Prepare; }
@@ -611,7 +611,7 @@ Sqlite :: struct {
}
// prepare with SQLITE_PREPARE_* flags (e.g. PERSISTENT for the
// statement cache a storage layer keeps hot).
prepare_v3 :: (self: *Sqlite, sql: string, flags: u32) -> SqliteStmt !SqliteErr {
prepare_v3 :: (self: *Sqlite, sql: string, flags: u32) -> (SqliteStmt, !SqliteErr) {
sh : usize = 0;
rc := sqlite3_prepare_v3(self.handle, sql.ptr, xx sql.len, flags, @sh, 0);
if rc != SQLITE_OK { raise error.Prepare; }
@@ -685,7 +685,7 @@ Sqlite :: struct {
}
// Schema introspection for one column of "main".`table`.
table_column_metadata :: (self: *Sqlite, table: string, column: string) -> ColumnMeta !SqliteErr {
table_column_metadata :: (self: *Sqlite, table: string, column: string) -> (ColumnMeta, !SqliteErr) {
dt : usize = 0;
cs : usize = 0;
nn : i32 = 0;
@@ -703,7 +703,7 @@ Sqlite :: struct {
// ── serialization ──
// The whole "main" database as bytes (a valid database image).
serialize :: (self: *Sqlite) -> string !SqliteErr {
serialize :: (self: *Sqlite) -> (string, !SqliteErr) {
size : i64 = 0;
p := sqlite3_serialize(self.handle, to_cstring("main"), @size, 0);
if p == null { raise error.Serialize; }
@@ -734,7 +734,7 @@ Sqlite :: struct {
SqliteBlob :: struct {
handle: usize;
open :: (db: *Sqlite, table: string, column: string, rowid: i64, writable: bool) -> SqliteBlob !SqliteErr {
open :: (db: *Sqlite, table: string, column: string, rowid: i64, writable: bool) -> (SqliteBlob, !SqliteErr) {
h : usize = 0;
rc := sqlite3_blob_open(db.handle, to_cstring("main"), to_cstring(table), to_cstring(column),
rowid, if writable then 1 else 0, @h);
@@ -751,7 +751,7 @@ SqliteBlob :: struct {
bytes :: (self: *SqliteBlob) -> i32 {
return sqlite3_blob_bytes(self.handle);
}
read :: (self: *SqliteBlob, offset: i32, n: i32) -> string !SqliteErr {
read :: (self: *SqliteBlob, offset: i32, n: i32) -> (string, !SqliteErr) {
len : i64 = n;
raw : [*]u8 = xx context.allocator.alloc_bytes(len + 1);
rc := sqlite3_blob_read(self.handle, raw, n, offset);
@@ -778,7 +778,7 @@ SqliteBlob :: struct {
SqliteBackup :: struct {
handle: usize;
init :: (dst: *Sqlite, src: *Sqlite) -> SqliteBackup !SqliteErr {
init :: (dst: *Sqlite, src: *Sqlite) -> (SqliteBackup, !SqliteErr) {
h := sqlite3_backup_init(dst.handle, to_cstring("main"), src.handle, to_cstring("main"));
if h == 0 { raise error.Backup; }
return SqliteBackup.{ handle = h };