lang: multi-iterable for loops — drop ':', add '..=', open ranges, arrow bodies

The for header is now a comma-separated list of iterables with a
positional capture group and no ':' separator:

    for xs (x) { }                    // collection
    for 0..n (i) { }                  // range (end exclusive)
    for 1..=5 (a) { }                 // ..= inclusive end
    for xs, 0.. (x, i) { }            // index idiom (replaces (x, i))
    for xs, ys (x, y) { }             // parallel (zip) iteration
    for xs (x) => sum += x;           // arrow body (full statement)

First-iterable-wins: the first iterable's length drives the loop and
must be bounded; the other positions follow by their own cursors (a
non-first range's end is not consulted or evaluated; a shorter
non-first collection is read past its length on mismatch). The old
single-iterable index capture is replaced by the trailing open range.

Capture/call disambiguation is positional: the paren group immediately
before '{' or '=>' is the capture, every earlier top-level group is a
call. 'for zip(a, b) (x, y)' calls zip; 'for f(n) { }' reads (n) as
the capture and errors with a parenthesize/add-capture hint. The old
':' form errors with a migration hint.

Lowering is unified across forms: one cursor slot per position (ranges
start at their start, collections at 0), all advanced together, the
first position's bound terminating. inline for keeps the single
bounded comptime range.

Migrated the full corpus (examples, library modules, issue repros,
in-source test strings). New coverage: examples/0050 (the full feature
surface) and examples/1149-1155 (seven diagnostic faces). specs.md For
Loop section + grammar rewritten; readme teaser updated.
This commit is contained in:
agra
2026-06-10 20:30:55 +03:00
parent c640e88513
commit 116af2359e
75 changed files with 701 additions and 391 deletions

View File

@@ -108,7 +108,7 @@ M :: union { `s1: s32; } // union tag
`u16 :: enum { A; B; } // type-declaration name
`u8, rest := pair(); // destructure name
if `s16 := maybe() { } // optional binding
for xs: (`bool, `u16) { } // for capture + index
for xs, 0.. (`bool, `u16) { } // for captures
x catch `s2 { } // catch tag binding
```
@@ -1370,7 +1370,7 @@ suggestion:
variadic `..xs: []P` (a runtime slice) instead of a pack `..xs: P`;
- returning it (`return xs;`) → return a tuple `(..xs)` (and make the return
type that tuple);
- iterating it (`for xs : (x)`, `xs[runtime_i]`) → `inline for 0..xs.len (i)`
- iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for 0..xs.len (i)`
for a comptime unroll, or take `..xs: []P` for a runtime loop.
The recurring runtime escape hatch is the **slice-of-protocol variadic**
@@ -1940,33 +1940,55 @@ while i < 10 {
### For Loop
#### Range form
```sx
for start..end: (i) { } // counting loop, cursor `i` (s64), `end` exclusive
for start..end { } // no cursor — body runs `end - start` times
inline for start..end: (i) { } // comptime-unrolled; `i` is a comptime constant per iteration
for it1, it2, ... (c1, c2, ...) { } // parallel iteration, one capture per iterable
for it1, it2, ... (c1, c2, ...) => stmt; // arrow body — a single statement
```
`start` and `end` are `s64` expressions; the loop counts `start, start+1, …, end-1`.
The cursor is optional — omit `: (i)` entirely when the body doesn't need the index
(`for 0..n { … }`). When present it is introduced by `:`, matching the collection
form (`for xs: (x)`).
The `inline` variant requires comptime-known bounds and unrolls the body once per
value, binding the cursor as a compile-time constant (so it can index a pack:
`inline for 0..xs.len: (i) { xs[i].m() }`). `break;` / `continue;` work in the
runtime form.
#### Collection form
A `for` header is a comma-separated list of **iterables** followed by an
optional **capture group** and the body. Each iterable is a collection
(array, slice, string, `List(T)`-like struct) or a range:
```sx
for iterable: (elem) { } // element alias (no copy)
for iterable: (elem, ix) { } // element + index
for iterable: (_, ix) { } // index only
for iterable: (*elem) { } // element pointer (*T) — by-reference
for iterable: (*elem, ix) { } // element pointer + index
for xs (x) { } // collection, element capture
for 0..n (i) { } // range, `end` exclusive; cursor i (s64)
for 1..=5 (a) { } // `..=` — end inclusive: 1 2 3 4 5
for 0..5 { } // no captures — body runs 5 times
for xs { } // no captures — body runs xs.len times
for xs, 0.. (x, i) { } // THE index idiom: open range follows along
for xs, ys (x, y) { } // parallel (zip) iteration
for 1..=5, 0.. (a, b) { } // a: 1..5, b: 0..4 (end inferred)
for a4, b4, 100.. (p, q, k) { } // any number of positions
for xs (x) => sum += x; // arrow body
inline for 0..n (i) { } // comptime-unrolled single bounded range
```
Iterates over arrays and slices. The capture clause after `:` binds loop variables:
- The first name is the element capture (non-reassignable alias into the array/slice)
- The optional second name is the index (s64, starting at 0, also non-reassignable)
- Use `_` to discard a capture
**First-iterable-wins.** The FIRST iterable's length drives the loop: a
bounded range runs `end - start` times (`..=`: `end - start + 1`), a
collection runs `len` times. The first iterable must be bounded — an open
range `a..` may only follow it. Every other position simply follows along by
its own cursor; consequences:
- a non-first range's end is **not consulted** (and not evaluated — write
`start..` for clarity);
- a non-first collection shorter than the first is read **past its length**
on mismatch — the first iterable is the authoritative one.
**Captures are positional**: the group binds one name per iterable, in
order — range positions bind the cursor value (s64), collection positions
bind the element. An empty group is omitted entirely (no parens). Capture
names shadow outer bindings, like any inner declaration. Use `_` to discard
a position. The old single-iterable index form `for xs: (x, i)` is gone —
write `for xs, 0.. (x, i)`.
**The capture/call rule.** In a for header, the parenthesized group
immediately before `{` or `=>` is the capture; every earlier top-level paren
group is ordinary call syntax. So `for zip(a, b) (x, y) { }` calls
`zip(a, b)` and captures `(x, y)`, while `for f(n) { }` reads `(n)` as the
capture — making the iterable `f` itself, which errors ("cannot iterate")
with a hint. A call iterable therefore always needs a capture group; to
iterate a call result without one, parenthesize (`for (f(n)) { }`) or bind
it to a local first. A leading paren group is a normal grouped expression
(`for (a ++ b) (x)` iterates the grouped value).
The element capture is a direct alias — reads and field writes go to the original array element. Direct reassignment of the capture (`elem = x`) is a compile error.
@@ -1974,18 +1996,25 @@ The element capture is a direct alias — reads and field writes go to the origi
- Passing it onward is zero-copy — `f(elem)` where `f` takes `*T` hands over the pointer, not a copy.
- Writes through it land in the original: `elem.* = v` (or `elem.field = v`).
- In a value position the pointer auto-derefs to the element: `elem + 1` reads the value, and `if elem == { … }` matches the pointee (a pointer subject matches through the deref). Where a `*T` is expected, the pointer is passed as-is.
- Range positions have no storage — `*` on a range capture is a compile error.
```sx
events := plat.poll_events(); // []Event
for events: (*ev) { // ev : *Event — no copy
for events (*ev) { // ev : *Event — no copy
pipeline.dispatch_event(ev); // passes the pointer
}
```
`break;` exits the loop. `continue;` skips to the next iteration.
The `inline` variant requires a single bounded range with comptime-known
bounds and unrolls the body once per value, binding the cursor as a
compile-time constant (so it can index a pack:
`inline for 0..xs.len (i) { xs[i].m() }`).
`break;` exits the loop. `continue;` skips to the next iteration. Both run
the iteration's pending `defer`s first (see Defer).
```sx
arr : [5]s32 = .[1, 2, 3, 4, 5];
for arr: (val, ix) {
for arr, 0.. (val, ix) {
if ix == 2 { continue; }
print("{}\n", val);
}
@@ -3039,7 +3068,9 @@ multi_assign = lvalue (',' lvalue)+ '=' expr (',' expr)+
lvalue = IDENT | postfix '.' IDENT
expr = if_expr | match_expr | while_expr | for_expr | lambda | binary
while_expr = 'while' expr block
for_expr = 'for' expr ':' '(' IDENT [',' IDENT] ')' block
for_expr = 'for' for_iter (',' for_iter)* [for_capture] (block | '=>' stmt)
for_iter = expr [('..' | '..=') [expr]]
for_capture = '(' ['*'] IDENT (',' ['*'] IDENT)* ')'
binary = catch_expr (binop catch_expr)* // binop includes `or` (fallback / chain)
catch_expr = unary ('catch' IDENT? (block | '==' '{' case_arm* else_arm? '}' | unary))?
unary = ('-' | '!' | 'xx' | 'try' | 'cast' '(' type ')') postfix