feat: tuple syntax cutover — Tuple(...) type + .(...) value

Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:

  type     `(A, B)`        -> `Tuple(A, B)`          (named keeps `:`)
  value    `(a, b)`        -> `.(a, b)`              (named uses `=`)
  typed    (new)           -> `Tuple(A, B).(a, b)`   (like `Point.{...}`)
  failable `-> (T, !)`     -> `-> T !`
           `-> (T1, T2, !)`-> `-> Tuple(T1, T2) !`   (channel outside Tuple)

Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.

`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.

The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
This commit is contained in:
agra
2026-06-25 17:53:57 +03:00
parent c882c6c63e
commit 989e18b760
124 changed files with 941 additions and 1236 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

@@ -52,7 +52,7 @@ Event :: 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 };
@@ -96,7 +96,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

@@ -113,7 +113,7 @@ async :: ufcs (io: Io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) {
// `await(f)` — value-carrying failable. `.ready` → the result; `.failed`
// / `.canceled` → raise the stored / cancellation error.
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 == .canceled { raise error.Canceled; }
if f.state == .failed { raise error.Failed; }

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

@@ -804,7 +804,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.{};