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

@@ -1,5 +1,5 @@
// Range-based for loops: `for start..end: (i) { }` (cursor optional, `end`
// exclusive) is a runtime counting loop; `inline for start..end: (i) { }`
// Range-based for loops: `for start..end (i) { }` (cursor optional, `end`
// exclusive) is a runtime counting loop; `inline for start..end (i) { }`
// is comptime-unrolled — the cursor is a compile-time constant each
// iteration, so `xs[i]` over a heterogeneous pack substitutes the concrete
// per-position element (this is what drives pack iteration).
@@ -16,14 +16,14 @@ impl Show for B { show :: (self: *B) -> string => "B"; }
// Comptime-unrolled iteration over a pack; cursor `i` indexes the pack.
each :: (..xs: Show) -> void {
inline for 0..xs.len: (i) {
inline for 0..xs.len (i) {
print("[{}]={}\n", i, xs[i].show());
}
}
main :: () -> s32 {
// Runtime range, cursor used.
for 0..3: (i) { print("i={}\n", i); }
for 0..3 (i) { print("i={}\n", i); }
// Runtime range, no cursor — body runs `end - start` times.
n := 0;
@@ -31,7 +31,7 @@ main :: () -> s32 {
print("n={}\n", n);
// Non-zero start.
for 2..5: (j) { print("j={}\n", j); }
for 2..5 (j) { print("j={}\n", j); }
// Inline unroll over a heterogeneous pack.
each(A.{ x = 1 }, B.{ s = "hi" }, A.{ x = 3 });

View File

@@ -1,4 +1,4 @@
// `for xs: (*x)` binds each element by pointer — no per-element copy.
// `for xs (*x)` binds each element by pointer — no per-element copy.
// Mutations write back, and a pointer subject matches through the deref.
#import "modules/std.sx";
@@ -10,12 +10,12 @@ Shape :: enum {
main :: () -> s32 {
// By-ref mutation writes back into the array (impossible with a value copy).
xs : [3]s64 = .[1, 2, 3];
for xs: (*x) { x.* = x + 100; }
for xs (*x) { x.* = x + 100; }
print("{} {} {}\n", xs[0], xs[1], xs[2]);
// Pointer subject matches through the deref; payload reads through the ref.
shapes : [2]Shape = .[.circle(2.0), .none];
for shapes: (*s) {
for shapes (*s) {
if s == {
case .circle: (r) { print("circle {}\n", r); }
case .none: { print("none\n"); }

View File

@@ -10,7 +10,7 @@ Box :: struct {
sum_ptr :: (xs: *List(s64)) -> s64 {
total : s64 = 0;
for xs: (n) { total = total + n; } // iterate through a *List
for xs (n) { total = total + n; } // iterate through a *List
total
}
@@ -21,12 +21,12 @@ main :: () -> s32 {
xs.append(30);
s : s64 = 0;
for xs: (n) { s = s + n; } // value capture
for xs (n) { s = s + n; } // value capture
print("sum {}\n", s); // 60
for xs: (*n) { n.* = n + 100; } // by-ref: writes back
for xs (*n) { n.* = n + 100; } // by-ref: writes back
s = 0;
for xs: (n) { s = s + n; }
for xs (n) { s = s + n; }
print("sum2 {}\n", s); // 360
print("via ptr {}\n", sum_ptr(@xs)); // 360
@@ -34,7 +34,7 @@ main :: () -> s32 {
bs := List(Box).{};
bs.append(.{ v = 7 });
bt : s64 = 0;
for bs: (*b) { bt = bt + b.boxed(); } // *Box receiver, value-self method
for bs (*b) { bt = bt + b.boxed(); } // *Box receiver, value-self method
print("boxes {}\n", bt); // 7
0
}

View File

@@ -118,7 +118,7 @@ main :: () {
// For loop basic
farr : [4]s32 = .[10, 20, 30, 40];
out("for:");
for farr: (it) {
for farr (it) {
out(" ");
out(int_to_string(it));
}
@@ -126,14 +126,14 @@ main :: () {
// For with print
out("for-print:");
for farr: (it) {
for farr (it) {
print(" {}", it);
}
out("\n");
// For with index
out("for-idx:");
for farr: (_, ix) {
for farr, 0.. (_, ix) {
out(" ");
out(int_to_string(ix));
}
@@ -141,14 +141,14 @@ main :: () {
// For with print two args
out("for-2arg:");
for farr: (it, ix) {
for farr, 0.. (it, ix) {
print(" {}@{}", it, ix);
}
out("\n");
// For with break
out("for-break:");
for farr: (it) {
for farr (it) {
if it == 30 { break; }
print(" {}", it);
}
@@ -156,7 +156,7 @@ main :: () {
// For with continue
out("for-continue:");
for farr: (it) {
for farr (it) {
if it == 20 { continue; }
print(" {}", it);
}
@@ -165,14 +165,14 @@ main :: () {
// For on slice
fsl : []s32 = .[10, 20, 30];
out("for-slice:");
for fsl: (it) {
for fsl (it) {
print(" {}", it);
}
out("\n");
// For on slice with index
out("for-slice-idx:");
for fsl: (it, ix) {
for fsl, 0.. (it, ix) {
print(" {}:{}", ix, it);
}
out("\n");
@@ -181,8 +181,8 @@ main :: () {
nf_a : [2]s32 = .[0, 1];
nf_b : [2]s32 = .[0, 1];
out("for-nested:");
for nf_a: (oa) {
for nf_b: (ob) {
for nf_a (oa) {
for nf_b (ob) {
print(" ({},{})", oa, ob);
}
}
@@ -191,7 +191,7 @@ main :: () {
// For with break preserving index
fbi : [5]s32 = .[10, 20, 30, 40, 50];
fbi_idx := 0;
for fbi: (it, ix) {
for fbi, 0.. (it, ix) {
if it == 30 { fbi_idx = ix; break; }
}
print("for-break-idx: {}\n", fbi_idx);

View File

@@ -16,7 +16,7 @@ pair_add :: (a: $T, b: $U) -> s64 {
typed_sum :: (..args: []s32) -> s32 {
result := 0;
for args: (it) { result = result + it; }
for args (it) { result = result + it; }
result
}

View File

@@ -10,7 +10,7 @@
main :: () -> s32 {
sum := 0;
for 0..1000000: (i) {
for 0..1000000 (i) {
buf : [128]s64 = ---;
buf[0] = i;
sum += buf[0];
@@ -18,8 +18,8 @@ main :: () -> s32 {
print("sum={}\n", sum);
n := 0;
for 0..3000000: (i) {
for 0..1: (j) { n += 1; }
for 0..3000000 (i) {
for 0..1 (j) { n += 1; }
}
print("n={}\n", n);
0

View File

@@ -13,12 +13,12 @@ main :: () -> s32 {
i := 0;
while i < 4096 { arr[i] = i; i += 1; }
sum := 0;
for arr: (x) { sum += x; }
for arr (x) { sum += x; }
print("sum={}\n", sum);
// By-value capture is a copy: mutating it leaves the array untouched.
small : [3]s64 = .[10, 20, 30];
for small: (x) { x += 100; }
for small (x) { x += 100; }
print("copy-guard: {} {} {}\n", small[0], small[1], small[2]);
0
}

View File

@@ -7,14 +7,14 @@
#import "modules/std.sx";
main :: () -> s32 {
for 0..3: (i) {
for 0..3 (i) {
defer print("cleanup {}\n", i);
if i == 1 { break; }
print("body {}\n", i);
}
print("after break loop\n");
for 0..3: (i) {
for 0..3 (i) {
defer print("c2 {}\n", i);
if i == 1 { continue; }
print("b2 {}\n", i);
@@ -33,7 +33,7 @@ main :: () -> s32 {
// A break inside a nested block drains the nested block's defers AND the
// loop body's, in LIFO order.
for 0..2: (j) {
for 0..2 (j) {
defer print("outer {}\n", j);
{
defer print("inner {}\n", j);

View File

@@ -0,0 +1,54 @@
#import "modules/std.sx";
pair_sum :: (xs: []s64, ys: []s64) -> s64 {
total := 0;
for xs, ys (x, y) { total += x * y; }
total
}
make :: () -> [3]s64 {
r : [3]s64 = .[7, 8, 9];
r
}
main :: () -> s32 {
// Agra's example: a 1..5 inclusive, b open-ended following along.
for 1..=5, 0.. (a, b) { print("{}:{} ", a, b); }
print("\n");
// Index idiom replacing the old (x, i) form.
xs : [3]s64 = .[10, 20, 30];
for xs, 0.. (x, i) { print("[{}]={} ", i, x); }
print("\n");
// Parallel slices.
a4 : [4]s64 = .[1, 2, 3, 4];
b4 : [4]s64 = .[10, 20, 30, 40];
print("dot={}\n", pair_sum(a4, b4));
// Arrow bodies.
s := 0;
for 0..4 (i) => s += i;
print("arrow-range s={}\n", s);
t := 0;
for xs (x) => t += x;
print("arrow-coll t={}\n", t);
// Call iterable + capture (first group = args, last group = capture).
for make() (v) { print("v{} ", v); }
print("\n");
// No-capture call iterable via leading-group escape.
n := 0;
for (make()) { n += 1; }
print("escape n={}\n", n);
// Three-way zip: two collections + cursor.
for a4, b4, 100.. (p, q, k) { print("{}/{}/{} ", p, q, k); }
print("\n");
// By-ref capture in a multi-iterable header.
for a4, 0.. (*p, i) { p.* += i; }
print("after ref: {} {} {} {}\n", a4[0], a4[1], a4[2], a4[3]);
0
}

View File

@@ -16,7 +16,7 @@ main :: () {
spts : [2]Point = .[Point.{1, 2}, Point.{3, 4}];
spt2 := spts[1];
print("arr-struct-x: {}\n", spt2.x);
for spts: (it) {
for spts (it) {
print("for-struct: {}\n", it);
}
}

View File

@@ -63,6 +63,6 @@ main :: () {
mk : Make(N, s64) = ---; mk[2] = 33; print("vp.typefn.expr: len={} v={}\n", mk.len, mk[2]);
// inline-for bound — expr const (3) and integral float (4)
s := 0; inline for 0..N: (i) { s += i; } print("for.expr: {}\n", s); // 0+1+2 = 3
t := 0; inline for 0..F: (i) { t += i; } print("for.float: {}\n", t); // 0+1+2+3 = 6
s := 0; inline for 0..N (i) { s += i; } print("for.expr: {}\n", s); // 0+1+2 = 3
t := 0; inline for 0..F (i) { t += i; } print("for.float: {}\n", t); // 0+1+2+3 = 6
}

View File

@@ -35,7 +35,7 @@ main :: () -> s32 {
// for capture + index names
xs := [3]s64.{ 10, 20, 30 };
for xs: (`bool, `u16) {
for xs, 0.. (`bool, `u16) {
print("for = {} @ {}\n", `bool, `u16);
}

View File

@@ -61,7 +61,7 @@ pair_add :: (a: $T, b: $U) -> s64 {
typed_sum :: (..args: []s32) -> s32 {
result := 0;
for args: (it) { result = result + it; }
for args (it) { result = result + it; }
result
}

View File

@@ -2,14 +2,14 @@
sum :: (..args: []s32) -> s32 {
result := 0;
for args: (it) {
for args (it) {
result = result + it;
}
result
}
print_all :: (..args: []s32) {
for args: (it) {
for args (it) {
out(int_to_string(it));
out(" ");
}
@@ -26,7 +26,7 @@ main :: () -> s32 {
out(int_to_string(sum(..arr)));
out("\n");
for arr: (it) {
for arr (it) {
out(int_to_string(it));
out(" ");
}

View File

@@ -7,7 +7,7 @@ Point :: struct {
// Print all arguments — accepts any type, dispatches via type-switch
print_any :: (..args: []Any) {
for args: (it) {
for args (it) {
type := type_of(it);
if type == {
case int: out(int_to_string(cast(s32) it));

View File

@@ -6,7 +6,7 @@
// constant (a literal, or an `inline for` cursor). A runtime index (here a
// `while`-loop counter) must produce a clear diagnostic, not the confusing
// "unresolved 'args'" the slice-index fall-through used to give. To walk a
// pack, use `inline for 0..args.len: (i) { ... }`, which unrolls so each
// pack, use `inline for 0..args.len (i) { ... }`, which unrolls so each
// `args[i]` is a comptime index.
#import "modules/std.sx";

View File

@@ -14,7 +14,7 @@ sink :: (v: s64) -> void { _ = v; }
storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
ret :: (..xs: Show) -> s64 { return xs; } // C: return
iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
main :: () -> s32 {
storage(A.{});

View File

@@ -14,10 +14,10 @@ M :: 3;
main :: () {
s := 0;
inline for 0..M: (i) { s += i; }
inline for 0..M (i) { s += i; }
print("sum 0..M = {}\n", s); // 0 + 1 + 2 = 3
t := 0;
inline for 0..(M + 1): (i) { t += i; }
inline for 0..(M + 1) (i) { t += i; }
print("sum 0..(M+1) = {}\n", t); // 0 + 1 + 2 + 3 = 6
}

View File

@@ -9,6 +9,6 @@ M :: 3.0;
main :: () {
s := 0;
inline for 0..M: (i) { s += i; }
inline for 0..M (i) { s += i; }
print("sum 0..M = {}\n", s); // 0 + 1 + 2 = 3
}

View File

@@ -11,14 +11,14 @@
main :: () {
s := 0;
inline for -2..1: (i) { s += i; }
inline for -2..1 (i) { s += i; }
print("inline for -2..1 sum = {}\n", s); // -2 + -1 + 0 = -3
r := 0;
for -2..1: (i) { r += i; }
for -2..1 (i) { r += i; }
print("for -2..1 sum = {}\n", r); // -2 + -1 + 0 = -3 (runtime parity)
e := 0;
inline for 0..(-2.0): (i) { e += i; }
inline for 0..(-2.0) (i) { e += i; }
print("inline for 0..(-2.0) sum = {}\n", e); // empty range -> 0 iterations
}

View File

@@ -4,7 +4,7 @@
combine :: (x: s64, y: s64) -> s64 { return x + y; }
pick :: (..xs: []s64) -> s64 {
result := 0;
for xs: (it) { result = result + it; }
for xs (it) { result = result + it; }
result
}
from_a_combine :: () -> s64 { return combine(10, 20); }

View File

@@ -4,7 +4,7 @@
// pack, pick subtracts its two fixed args.
combine :: (..xs: []s64) -> s64 {
result := 0;
for xs: (it) { result = result + it; }
for xs (it) { result = result + it; }
result
}
pick :: (a: s64, b: s64) -> s64 { return b - a; }

View File

@@ -17,7 +17,7 @@ g :: () -> !E { return; }
f :: () -> !E {
defer { return; } // ERROR: return in defer body
onfail { try g(); } // ERROR: try in onfail body
defer { for 0..1: (i) { break; } } // ERROR: break in defer body (transitive through loop)
defer { for 0..1 (i) { break; } } // ERROR: break in defer body (transitive through loop)
onfail e { if e == error.Bad { continue; } } // ERROR: continue in onfail body
try g();
return;

View File

@@ -1,4 +1,4 @@
// A by-reference loop capture (`for xs: (*m)`) binds `m` to a `*T`.
// A by-reference loop capture (`for xs (*m)`) binds `m` to a `*T`.
// Passing it where a `T` value is expected used to slip through to the
// LLVM verifier ("Call parameter type does not match function signature").
// The compiler now reports it at the call site with a fix-it: write `m.*`.
@@ -11,7 +11,7 @@ take :: (m: Move) -> s64 { return m.flag; }
main :: () -> s32 {
moves : [2]Move = .[ Move.{ flag = 1 }, Move.{ flag = 2 } ];
for moves: (*m) {
for moves (*m) {
take(m);
}
return 0;

View File

@@ -1,5 +1,5 @@
// Passing a `*T` where a `T` value is expected is caught at the call site —
// not only for `for xs: (*m)` loop captures (see 215) but for any pointer,
// not only for `for xs (*m)` loop captures (see 215) but for any pointer,
// here a `*Move` parameter forwarded into a by-value parameter. Without the
// check this slipped through to the LLVM verifier as "Call parameter type
// does not match function signature".

View File

@@ -19,8 +19,8 @@ main :: () -> s32 {
if u8 := maybe() { } // if optional binding
while s16 := maybe() { break; } // while optional binding
xs := [3]s64.{ 10, 20, 30 };
for xs: (bool) { } // for capture name
for xs: (v, s32) { } // for index name
for xs (bool) { } // for capture name
for xs, 0.. (v, s32) { } // for index name
opt: ?s64 = 5;
r := if opt == { // match-arm capture
case .some: (string) { 0 }

View File

@@ -9,6 +9,6 @@
main :: () {
s := 0;
inline for 0..4.5: (i) { s += i; }
inline for 0..4.5 (i) { s += i; }
print("unreachable: {}\n", s);
}

View File

@@ -0,0 +1,9 @@
// The pre-multi-iterable `for xs: (x)` syntax (colon before the capture) is
// rejected with a migration hint.
#import "modules/std.sx";
main :: () {
xs : [2]s64 = .[1, 2];
for xs: (x) { }
}

View File

@@ -0,0 +1,9 @@
// A for capture group is positional: one capture per iterable (or none).
#import "modules/std.sx";
main :: () {
xs : [2]s64 = .[1, 2];
ys : [2]s64 = .[3, 4];
for xs, ys (x) { }
}

View File

@@ -0,0 +1,8 @@
// The FIRST iterable drives the loop, so it must be bounded; an open range
// `a..` may only follow it.
#import "modules/std.sx";
main :: () {
for 0.. (i) { }
}

View File

@@ -0,0 +1,7 @@
// `..=` is the inclusive bounded form — it requires an end expression.
#import "modules/std.sx";
main :: () {
for 0..= (i) { }
}

View File

@@ -0,0 +1,8 @@
// Range elements are synthesized values with no storage — `*` capture is
// rejected.
#import "modules/std.sx";
main :: () {
for 0..3 (*i) { }
}

View File

@@ -0,0 +1,11 @@
// In a for header the trailing paren group is the CAPTURE; a call iterable
// therefore needs one too. `()` cannot be a capture — parse error with the
// hint (`for f(n) (x) { }` / `for (f(n)) { }` / bind it first).
#import "modules/std.sx";
g :: () -> s64 { 1 }
main :: () {
for g() { }
}

View File

@@ -0,0 +1,12 @@
// The collision case of the positional capture rule: `for f(n) { }` reads
// `(n)` as the capture, making the iterable `f` itself — not iterable, with
// the parenthesize/add-capture hint.
#import "modules/std.sx";
f :: (n: s64) -> s64 { n }
main :: () {
n := 1;
for f(n) { }
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,9 @@
1:0 2:1 3:2 4:3 5:4
[0]=10 [1]=20 [2]=30
dot=300
arrow-range s=6
arrow-coll t=60
v7 v8 v9
escape n=3
1/10/100 2/20/101 3/30/102 4/40/103
after ref: 1 3 5 7

View File

@@ -1,5 +1,5 @@
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
--> /Users/agra/projects/sx/examples/0536-packs-pack-as-value.sx:14:40
--> examples/0536-packs-pack-as-value.sx:14:40
|
14 | storage :: (..xs: Show) -> void { y := xs; _ = y; } // A: store
| ^^
@@ -10,7 +10,7 @@ help: to store it, materialize a tuple: `(..xs)`
| ^^
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
--> /Users/agra/projects/sx/examples/0536-packs-pack-as-value.sx:15:40
--> examples/0536-packs-pack-as-value.sx:15:40
|
15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
| ^^
@@ -21,7 +21,7 @@ help: materialize a tuple `(..xs)` to store it, or `xx xs` to convert it to an e
| ^^
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
--> /Users/agra/projects/sx/examples/0536-packs-pack-as-value.sx:16:42
--> examples/0536-packs-pack-as-value.sx:16:42
|
16 | ret :: (..xs: Show) -> s64 { return xs; } // C: return
| ^^
@@ -32,12 +32,12 @@ help: to return it, return a tuple `(..xs)` and make the return type that tuple
| ^^
error: pack 'xs' has no runtime value — a pack is comptime-only and can't be used as a value here
--> /Users/agra/projects/sx/examples/0536-packs-pack-as-value.sx:17:39
--> examples/0536-packs-pack-as-value.sx:17:39
|
17 | iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
17 | iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
| ^^
help: to iterate at comptime use `inline for 0..xs.len (i)`; for a runtime loop declare it as `..xs: []P` (a protocol slice) instead of a pack
|
17 | iter :: (..xs: Show) -> void { for xs : (x) { _ = x; } } // D: runtime iterate
17 | iter :: (..xs: Show) -> void { for xs (x) { _ = x; } } // D: runtime iterate
| ^^

View File

@@ -17,16 +17,16 @@ error: 's16' is a reserved type name and cannot be used as an identifier
| ^^^
error: 'bool' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:22:14
--> examples/1121-diagnostics-reserved-name-control-flow.sx:22:13
|
22 | for xs: (bool) { } // for capture name
| ^^^^
22 | for xs (bool) { } // for capture name
| ^^^^
error: 's32' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:23:17
--> examples/1121-diagnostics-reserved-name-control-flow.sx:23:21
|
23 | for xs: (v, s32) { } // for index name
| ^^^
23 | for xs, 0.. (v, s32) { } // for index name
| ^^^
error: 'string' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:26:22

View File

@@ -1,5 +1,5 @@
error: inline for: range end is not a compile-time integer
--> examples/1138-diagnostics-inline-for-non-integral-bound.sx:12:19
|
12 | inline for 0..4.5: (i) { s += i; }
12 | inline for 0..4.5 (i) { s += i; }
| ^^^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: for-loop syntax: the ':' before the capture was removed — write `for xs (x) { }` (index via `for xs, 0.. (x, i)`)
--> examples/1149-diagnostics-for-colon-removed.sx:8:11
|
8 | for xs: (x) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: for capture count must match the iterable count — one capture per iterable
--> examples/1150-diagnostics-for-capture-arity.sx:8:20
|
8 | for xs, ys (x) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: the first iterable must have a bounded length (it drives the loop) — an open range 'a..' may only follow it
--> examples/1151-diagnostics-for-open-first.sx:7:17
|
7 | for 0.. (i) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: '..=' requires an end expression — the open form is 'a..'
--> examples/1152-diagnostics-for-inclusive-open.sx:6:14
|
6 | for 0..= (i) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: a range element cannot be captured by reference
--> examples/1153-diagnostics-for-range-by-ref.sx:7:19
|
7 | for 0..3 (*i) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: expected capture variable name (a call iterable also needs a capture: `for f(n) (x) { }`)
--> examples/1154-diagnostics-for-call-needs-capture.sx:10:11
|
10 | for g() { }
| ^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: cannot iterate this expression — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) { }`) or parentheses (`for (f(n)) { }`)
--> examples/1155-diagnostics-for-not-iterable.sx:11:9
|
11 | for f(n) { }
| ^

View File

@@ -0,0 +1 @@

View File

@@ -340,9 +340,11 @@ if 0 <= x <= 100 { ... }
// While
while i < 10 { i += 1; }
// For (arrays and slices)
for items: (val) { print("{}\n", val); }
for items: (val, idx) { print("[{}] = {}\n", idx, val); }
// For — collections, ranges, and parallel iteration
for items (val) { print("{}\n", val); }
for items, 0.. (val, idx) { print("[{}] = {}\n", idx, val); }
for 1..=5, 0.. (a, b) { print("{}:{}\n", a, b); } // a: 1..5, b follows
for items (val) => total += val; // arrow body
// Defer
f := open("file.txt");

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

View File

@@ -642,29 +642,41 @@ pub const WhileExpr = struct {
binding_is_raw: bool = false,
};
pub const ForExpr = struct {
iterable: *Node,
body: *Node,
capture_name: []const u8,
capture_span: ?Span = null, // span of `capture_name` (null when omitted, e.g. `for 0..N { }`)
/// True when `capture_name` was a backtick raw identifier
/// (`` for xs: (`s2) ``) — exempt from the reserved-type-name check.
capture_is_raw: bool = false,
index_name: ?[]const u8 = null,
index_span: ?Span = null, // span of `index_name` (set iff `index_name` is)
/// True when `index_name` was a backtick raw identifier
/// (`` for xs: (x, `s2) ``) — exempt from the reserved-type-name check.
index_is_raw: bool = false,
/// Range form `for start..end (i) { }`: `iterable` is the start, `range_end`
/// the (exclusive) end. Null for the iterate-a-collection form
/// (`for coll : (x) { }`). For the range form `capture_name` is the cursor
/// (empty when omitted, `for 0..N { }`).
/// One position of a (possibly multi-iterable) `for` header.
pub const ForIterable = struct {
/// Collection expression, or the range START for the range forms.
expr: *Node,
/// `a..b` / `a..=b` end. Null for a plain collection AND for the
/// open-ended range `a..` (distinguished by `is_range`).
range_end: ?*Node = null,
/// `inline for` — comptime-unrolled (range bounds must be comptime).
/// True for any range form (`a..`, `a..b`, `a..=b`).
is_range: bool = false,
/// `a..=b` — end is inclusive.
inclusive: bool = false,
};
/// One capture of a `for` header: `(x)`, `(*x)`, `(x, y, ...)`.
pub const ForCapture = struct {
name: []const u8,
span: ?Span = null,
/// True when the name was a backtick raw identifier (`` for xs (`s2) ``)
/// — exempt from the reserved-type-name check.
is_raw: bool = false,
/// `(*x)` — bind a pointer into the collection (no per-element copy).
by_ref: bool = false,
};
/// `for it1, it2, ... (c1, c2, ...) { }` — parallel iteration. The FIRST
/// iterable's length drives the loop (first-iterable-wins); the others are
/// indexed along it, and a non-first range's end is not consulted. The
/// capture group is positional: empty (no bindings) or one capture per
/// iterable. The body is a block or an `=> expr;` arrow body.
pub const ForExpr = struct {
iterables: []ForIterable,
captures: []ForCapture,
body: *Node,
/// `inline for` — comptime-unrolled (single bounded range, comptime bounds).
is_inline: bool = false,
/// `for xs: (*x)` — bind `x` to a pointer into the collection (no per-element
/// copy) rather than a value copy of each element.
capture_by_ref: bool = false,
};
pub const SpreadExpr = struct {

View File

@@ -61,8 +61,10 @@ pub const ErrorAnalysis = struct {
self.collectErrorSites(w.body, tags, edges);
},
.for_expr => |f| {
self.collectErrorSites(f.iterable, tags, edges);
if (f.range_end) |re| self.collectErrorSites(re, tags, edges);
for (f.iterables) |it| {
self.collectErrorSites(it.expr, tags, edges);
if (it.range_end) |re| self.collectErrorSites(re, tags, edges);
}
self.collectErrorSites(f.body, tags, edges);
},
.return_stmt => |r| if (r.value) |v| self.collectErrorSites(v, tags, edges),
@@ -216,8 +218,10 @@ pub const ErrorAnalysis = struct {
self.collectClosureShapes(w.body);
},
.for_expr => |f| {
self.collectClosureShapes(f.iterable);
if (f.range_end) |re| self.collectClosureShapes(re);
for (f.iterables) |it| {
self.collectClosureShapes(it.expr);
if (it.range_end) |re| self.collectClosureShapes(re);
}
self.collectClosureShapes(f.body);
},
.return_stmt => |r| if (r.value) |v| self.collectClosureShapes(v),

View File

@@ -152,8 +152,10 @@ pub const ErrorFlow = struct {
return false;
},
.for_expr => |fe| {
self.flowExpr(fe.iterable, ctx, proven.*);
if (fe.range_end) |re| self.flowExpr(re, ctx, proven.*);
for (fe.iterables) |it| {
self.flowExpr(it.expr, ctx, proven.*);
if (it.range_end) |re| self.flowExpr(re, ctx, proven.*);
}
var loop_proven = self.provenClone(proven.*);
_ = self.flowWalk(fe.body, ctx, &loop_proven);
return false;

View File

@@ -1368,7 +1368,6 @@ pub const Lowering = struct {
pub const lowerWhile = lower_control_flow.lowerWhile;
pub const listView = lower_control_flow.listView;
pub const lowerFor = lower_control_flow.lowerFor;
pub const lowerRuntimeRangeFor = lower_control_flow.lowerRuntimeRangeFor;
pub const lowerInlineRangeFor = lower_control_flow.lowerInlineRangeFor;
pub const lowerMatch = lower_control_flow.lowerMatch;
pub const lowerBreak = lower_control_flow.lowerBreak;

View File

@@ -639,9 +639,12 @@ pub fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.Str
self.collectCaptures(de.operand, param_names, captures);
},
.for_expr => |fe| {
self.collectCaptures(fe.iterable, param_names, captures);
// Register capture name as local so it's not captured
param_names.put(fe.capture_name, {}) catch {};
for (fe.iterables) |it| {
self.collectCaptures(it.expr, param_names, captures);
if (it.range_end) |re| self.collectCaptures(re, param_names, captures);
}
// Register capture names as locals so they're not captured
for (fe.captures) |cap| param_names.put(cap.name, {}) catch {};
self.collectCaptures(fe.body, param_names, captures);
},
.slice_expr => |se| {

View File

@@ -277,48 +277,110 @@ pub fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, da
};
}
/// Lowered prep for one position of a multi-iterable `for` header. Every
/// position gets its own s64 cursor slot (ranges start at their `start`,
/// collections at 0); all cursors advance by 1 per iteration, and ONLY the
/// first position's bound terminates the loop (first-iterable-wins).
const IterPrep = struct {
is_range: bool,
slot: Ref,
// Collection-only fields:
data: Ref = Ref.none,
data_ty: TypeId = .unresolved,
elem_ty: TypeId = .unresolved,
is_array: bool = false,
storage: ?Ref = null, // array's own alloca when addressable (not deref'd)
};
/// `for it1, it2, ... (c1, c2, ...) { }` — parallel iteration. The first
/// iterable's length/bound drives the loop; the others follow by position.
/// Consequences of first-iterable-wins: a non-first range's end is never
/// lowered (its side effects do not run), and a shorter non-first collection
/// is read past its length on mismatch — the first iterable is the
/// authoritative one.
pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
if (fe.range_end) |end_node| {
if (fe.is_inline) return self.lowerInlineRangeFor(fe, end_node);
return self.lowerRuntimeRangeFor(fe, end_node);
}
// Collection-form `for xs : (x)` over a pack: a pack has no runtime
// value to iterate (Decision 1) — point the user at `inline for`.
if (fe.iterable.data == .identifier and self.isPackName(fe.iterable.data.identifier.name)) {
return self.diagPackAsValue(fe.iterable.data.identifier.name, fe.iterable.span, .runtime_iter);
if (fe.is_inline) return self.lowerInlineRangeFor(fe);
// A pack has no runtime value to iterate (Decision 1) — point the user
// at `inline for`.
for (fe.iterables) |it| {
if (!it.is_range and it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) {
return self.diagPackAsValue(it.expr.data.identifier.name, it.expr.span, .runtime_iter);
}
}
// Lower iterable + resolve its static type.
var iterable = self.lowerExpr(fe.iterable);
var iterable_ty = self.inferExprType(fe.iterable);
var preps = std.ArrayList(IterPrep).empty;
defer preps.deinit(self.alloc);
var limit: Ref = Ref.none; // exclusive bound of position 0
// `*List` / `*[]T` etc. — deref to the collection value. Tracked because
// a deref'd iterable's identifier binding holds the POINTER, so its
// alloca is not the collection's storage.
var was_deref = false;
const ptr_info = if (iterable_ty.isBuiltin()) null else self.module.types.get(iterable_ty);
if (ptr_info != null and ptr_info.? == .pointer) {
iterable = self.builder.load(iterable, ptr_info.?.pointer.pointee);
iterable_ty = ptr_info.?.pointer.pointee;
was_deref = true;
for (fe.iterables, 0..) |it, i| {
if (it.is_range) {
const start_ref = self.lowerExpr(it.expr);
const slot = self.builder.alloca(.s64);
self.builder.store(slot, start_ref);
if (i == 0) {
// Parser guarantees the first iterable is bounded.
var end_ref = self.lowerExpr(it.range_end.?);
if (it.inclusive) end_ref = self.builder.add(end_ref, self.builder.constInt(1, .s64), .s64);
limit = end_ref;
}
preps.append(self.alloc, .{ .is_range = true, .slot = slot }) catch unreachable;
} else {
var data = self.lowerExpr(it.expr);
var data_ty = self.inferExprType(it.expr);
// `*List` / `*[]T` etc. — deref to the collection value. Tracked
// because a deref'd iterable's identifier binding holds the
// POINTER, so its alloca is not the collection's storage.
var was_deref = false;
const ptr_info = if (data_ty.isBuiltin()) null else self.module.types.get(data_ty);
if (ptr_info != null and ptr_info.? == .pointer) {
data = self.builder.load(data, ptr_info.?.pointer.pointee);
data_ty = ptr_info.?.pointer.pointee;
was_deref = true;
}
// A `List(T)`-like struct iterates its `items[0..len]`;
// arrays/slices use their intrinsic length.
var len: Ref = Ref.none;
if (self.listView(data, data_ty)) |lv| {
data = lv.data;
data_ty = lv.data_ty;
len = lv.len;
} else if (i == 0) {
len = self.builder.emit(.{ .length = .{ .operand = data } }, .s64);
}
const elem_ty = self.getElementType(data_ty);
if (elem_ty == .unresolved) {
// Not a collection. The common trip: `for f(n) { }` — the
// trailing parens are the CAPTURE, so the iterable is `f`.
if (self.diagnostics) |d| {
if (data_ty == .unresolved) {
d.addFmt(.err, it.expr.span, "cannot iterate this expression — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{});
} else {
d.addFmt(.err, it.expr.span, "cannot iterate a value of type '{s}' — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{self.module.types.typeName(data_ty)});
}
}
return self.builder.constInt(0, .void);
}
const is_array = !data_ty.isBuiltin() and self.module.types.get(data_ty) == .array;
const storage = if (is_array and !was_deref) self.getExprAlloca(it.expr) else null;
const slot = self.builder.alloca(.s64);
self.builder.store(slot, self.builder.constInt(0, .s64));
if (i == 0) limit = len;
preps.append(self.alloc, .{
.is_range = false,
.slot = slot,
.data = data,
.data_ty = data_ty,
.elem_ty = elem_ty,
.is_array = is_array,
.storage = storage,
}) catch unreachable;
}
}
// A `List(T)`-like struct iterates its `items[0..len]`; arrays/slices
// use their intrinsic length.
var len: Ref = undefined;
if (self.listView(iterable, iterable_ty)) |lv| {
iterable = lv.data;
iterable_ty = lv.data_ty;
len = lv.len;
} else {
len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64);
}
// Create index variable
const idx_slot = self.builder.alloca(.s64);
const zero = self.builder.constInt(0, .s64);
self.builder.store(idx_slot, zero);
const header_bb = self.freshBlock("for.hdr");
const body_bb = self.freshBlock("for.body");
const inc_bb = self.freshBlock("for.inc");
@@ -326,49 +388,44 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
self.builder.br(header_bb, &.{});
// Header: compare index < length
// Header: first cursor against the first bound.
self.builder.switchToBlock(header_bb);
const idx_val = self.builder.load(idx_slot, .s64);
const cmp = self.builder.cmpLt(idx_val, len);
const cur0 = self.builder.load(preps.items[0].slot, .s64);
const cmp = self.builder.cmpLt(cur0, limit);
self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{});
// Body
// Body: bind one capture per position (when captures are present).
self.builder.switchToBlock(body_bb);
// Bind element — resolve element type from iterable. `for xs: (*x)`
// binds a pointer into the collection (no per-element copy); `(x)`
// binds a value copy.
const elem_ty = self.getElementType(iterable_ty);
const bind_ty = if (fe.capture_by_ref) self.module.types.ptrTo(elem_ty) else elem_ty;
const is_array = !iterable_ty.isBuiltin() and self.module.types.get(iterable_ty) == .array;
const elem = if (fe.capture_by_ref) blk: {
// A slice value carries its backing pointer, so GEP on it writes
// through. An array is a value — GEP needs its storage (alloca) or
// mutations would hit a copy.
const base = if (is_array) (self.getExprAlloca(fe.iterable) orelse iterable) else iterable;
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx_val } }, bind_ty);
} else blk: {
// By-value over an array with addressable storage: GEP + load ONE
// element. `index_get` on the array VALUE spills the whole array to
// a temp on every iteration — O(N²) bytes copied per loop.
if (is_array and !was_deref) {
if (self.getExprAlloca(fe.iterable)) |storage| {
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = storage, .rhs = idx_val } }, self.module.types.ptrTo(elem_ty));
break :blk self.builder.load(elem_ptr, elem_ty);
}
}
break :blk self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, bind_ty);
};
var body_scope = Scope.init(self.alloc, self.scope);
const old_scope = self.scope;
self.scope = &body_scope;
body_scope.put(fe.capture_name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = fe.capture_by_ref });
// Bind index if requested
if (fe.index_name) |iname| {
body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false });
for (fe.captures, 0..) |cap, i| {
const prep = preps.items[i];
const cur = if (i == 0) cur0 else self.builder.load(prep.slot, .s64);
if (prep.is_range) {
body_scope.put(cap.name, .{ .ref = cur, .ty = .s64, .is_alloca = false });
continue;
}
const bind_ty = if (cap.by_ref) self.module.types.ptrTo(prep.elem_ty) else prep.elem_ty;
const elem = if (cap.by_ref) blk: {
// A slice value carries its backing pointer, so GEP on it writes
// through. An array is a value — GEP needs its storage (alloca)
// or mutations would hit a copy.
const base = if (prep.is_array) (prep.storage orelse prep.data) else prep.data;
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = cur } }, bind_ty);
} else blk: {
// By-value over an array with addressable storage: GEP + load ONE
// element. `index_get` on the array VALUE spills the whole array
// to a temp on every iteration — O(N²) bytes copied per loop.
if (prep.storage) |storage| {
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = storage, .rhs = cur } }, self.module.types.ptrTo(prep.elem_ty));
break :blk self.builder.load(elem_ptr, prep.elem_ty);
}
break :blk self.builder.emit(.{ .index_get = .{ .lhs = prep.data, .rhs = cur } }, bind_ty);
};
body_scope.put(cap.name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = cap.by_ref });
}
// Save and set loop targets
@@ -392,13 +449,15 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
self.builder.br(inc_bb, &.{});
}
// Increment block: increment index and jump back to header
// Increment block: advance every cursor and jump back to header.
self.builder.switchToBlock(inc_bb);
{
const cur_idx = self.builder.load(idx_slot, .s64);
const one = self.builder.constInt(1, .s64);
const next_idx = self.builder.add(cur_idx, one, .s64);
self.builder.store(idx_slot, next_idx);
for (preps.items) |prep| {
const cur = self.builder.load(prep.slot, .s64);
const next = self.builder.add(cur, one, .s64);
self.builder.store(prep.slot, next);
}
self.builder.br(header_bb, &.{});
}
@@ -407,81 +466,26 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
return self.builder.constInt(0, .void);
}
/// Runtime counting loop `for start..end (i) { }` — `i` (optional) is the
/// cursor, `end` is exclusive. Lowers to the same header/inc/exit shape as
/// the collection form, minus the element fetch.
pub fn lowerRuntimeRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref {
const start = self.lowerExpr(fe.iterable);
const end = self.lowerExpr(end_node);
const idx_slot = self.builder.alloca(.s64);
self.builder.store(idx_slot, start);
const header_bb = self.freshBlock("for.hdr");
const body_bb = self.freshBlock("for.body");
const inc_bb = self.freshBlock("for.inc");
const exit_bb = self.freshBlock("for.exit");
self.builder.br(header_bb, &.{});
self.builder.switchToBlock(header_bb);
const idx_val = self.builder.load(idx_slot, .s64);
const cmp = self.builder.cmpLt(idx_val, end);
self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{});
self.builder.switchToBlock(body_bb);
var body_scope = Scope.init(self.alloc, self.scope);
const old_scope = self.scope;
self.scope = &body_scope;
if (fe.capture_name.len > 0) {
body_scope.put(fe.capture_name, .{ .ref = idx_val, .ty = .s64, .is_alloca = false });
}
const old_break = self.break_target;
const old_continue = self.continue_target;
const old_defer_base = self.loop_defer_base;
self.break_target = exit_bb;
self.continue_target = inc_bb;
self.loop_defer_base = self.defer_stack.items.len;
self.lowerBlock(fe.body);
self.break_target = old_break;
self.continue_target = old_continue;
self.loop_defer_base = old_defer_base;
self.scope = old_scope;
body_scope.deinit();
if (!self.currentBlockHasTerminator()) {
self.builder.br(inc_bb, &.{});
}
self.builder.switchToBlock(inc_bb);
{
const cur_idx = self.builder.load(idx_slot, .s64);
const one = self.builder.constInt(1, .s64);
const next_idx = self.builder.add(cur_idx, one, .s64);
self.builder.store(idx_slot, next_idx);
self.builder.br(header_bb, &.{});
}
self.builder.switchToBlock(exit_bb);
return self.builder.constInt(0, .void);
}
/// Comptime-unrolled `inline for start..end (i) { }`. `start`/`end` must be
/// comptime-known. The body is lowered `end - start` times with the cursor
/// bound as an `int_val` comptime constant, so `xs[i]` over a pack
/// Comptime-unrolled `inline for start..end (i) { }`. A single bounded range
/// with comptime-known bounds. The body is lowered once per value with the
/// cursor bound as an `int_val` comptime constant, so `xs[i]` over a pack
/// substitutes the concrete per-position argument each iteration.
pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref {
const start = self.evalComptimeInt(fe.iterable) orelse {
if (self.diagnostics) |d| d.addFmt(.err, fe.iterable.span, "inline for: range start is not a compile-time integer", .{});
pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
const it = fe.iterables[0];
if (fe.iterables.len != 1 or !it.is_range or it.range_end == null) {
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: a single bounded range is required — `inline for 0..N (i) {{ }}`", .{});
return self.builder.constInt(0, .void);
}
const start = self.evalComptimeInt(it.expr) orelse {
if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: range start is not a compile-time integer", .{});
return self.builder.constInt(0, .void);
};
const end = self.evalComptimeInt(end_node) orelse {
if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{});
var end = self.evalComptimeInt(it.range_end.?) orelse {
if (self.diagnostics) |d| d.addFmt(.err, it.range_end.?.span, "inline for: range end is not a compile-time integer", .{});
return self.builder.constInt(0, .void);
};
if (it.inclusive) end += 1;
const capture_name = if (fe.captures.len > 0) fe.captures[0].name else "";
var i: i64 = start;
while (i < end) : (i += 1) {
@@ -493,22 +497,22 @@ pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *N
// `print(i)`) and as a comptime constant (for `xs[i]` substitution).
var had_prev = false;
var prev: ComptimeValue = undefined;
if (fe.capture_name.len > 0) {
body_scope.put(fe.capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false });
if (self.comptime_constants.get(fe.capture_name)) |p| {
if (capture_name.len > 0) {
body_scope.put(capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false });
if (self.comptime_constants.get(capture_name)) |p| {
had_prev = true;
prev = p;
}
self.comptime_constants.put(fe.capture_name, .{ .int_val = i }) catch {};
self.comptime_constants.put(capture_name, .{ .int_val = i }) catch {};
}
self.lowerBlock(fe.body);
if (fe.capture_name.len > 0) {
if (capture_name.len > 0) {
if (had_prev) {
self.comptime_constants.put(fe.capture_name, prev) catch {};
self.comptime_constants.put(capture_name, prev) catch {};
} else {
_ = self.comptime_constants.remove(fe.capture_name);
_ = self.comptime_constants.remove(capture_name);
}
}

View File

@@ -158,10 +158,13 @@ pub const UnknownTypeChecker = struct {
self.checkBindingNames(we.body);
},
.for_expr => |fe| {
if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span, fe.capture_is_raw);
if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span, fe.index_is_raw);
self.checkBindingNames(fe.iterable);
if (fe.range_end) |re| self.checkBindingNames(re);
for (fe.captures) |cap| {
if (cap.name.len != 0) self.checkBindingName(cap.name, cap.span, cap.is_raw);
}
for (fe.iterables) |it| {
self.checkBindingNames(it.expr);
if (it.range_end) |re| self.checkBindingNames(re);
}
self.checkBindingNames(fe.body);
},
.match_expr => |me| {
@@ -417,8 +420,10 @@ pub const UnknownTypeChecker = struct {
self.harvestScopeDecls(we.body, out);
},
.for_expr => |fe| {
self.harvestScopeDecls(fe.iterable, out);
if (fe.range_end) |re| self.harvestScopeDecls(re, out);
for (fe.iterables) |it| {
self.harvestScopeDecls(it.expr, out);
if (it.range_end) |re| self.harvestScopeDecls(re, out);
}
self.harvestScopeDecls(fe.body, out);
},
.match_expr => |me| {
@@ -566,8 +571,10 @@ pub const UnknownTypeChecker = struct {
self.walkBodyTypes(we.body, declared, in_scope, type_vals);
},
.for_expr => |fe| {
self.walkBodyTypes(fe.iterable, declared, in_scope, type_vals);
if (fe.range_end) |re| self.walkBodyTypes(re, declared, in_scope, type_vals);
for (fe.iterables) |it| {
self.walkBodyTypes(it.expr, declared, in_scope, type_vals);
if (it.range_end) |re| self.walkBodyTypes(re, declared, in_scope, type_vals);
}
self.walkBodyTypes(fe.body, declared, in_scope, type_vals);
},
.match_expr => |me| {

View File

@@ -146,6 +146,10 @@ pub const Lexer = struct {
'.' => {
if (self.peek() == '.') {
self.index += 1;
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.dot_dot_eq, start, self.index);
}
return self.makeToken(.dot_dot, start, self.index);
}
return self.makeToken(.dot, start, self.index);

View File

@@ -1282,8 +1282,10 @@ pub const Server = struct {
collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints);
},
.for_expr => |fe| {
if (!std.mem.eql(u8, fe.capture_name, "_")) {
addForCaptureHint(allocator, fe.capture_name, node.span, symbols, source, hints);
for (fe.captures) |cap| {
if (!std.mem.eql(u8, cap.name, "_")) {
addForCaptureHint(allocator, cap.name, node.span, symbols, source, hints);
}
}
collectInlayHints(allocator, fe.body, symbols, fn_signatures, source, hints);
},
@@ -1534,7 +1536,10 @@ pub const Server = struct {
self.collectCallHints(doc, we.body, hints);
},
.for_expr => |fe| {
self.collectCallHints(doc, fe.iterable, hints);
for (fe.iterables) |it| {
self.collectCallHints(doc, it.expr, hints);
if (it.range_end) |re| self.collectCallHints(doc, re, hints);
}
self.collectCallHints(doc, fe.body, hints);
},
.var_decl => |vd| {
@@ -1765,6 +1770,7 @@ pub const Server = struct {
.comma,
.dot,
.dot_dot,
.dot_dot_eq,
.dollar,
.l_paren,
.r_paren,
@@ -3297,7 +3303,7 @@ test "analyzeDocument: for-loop capture variables are registered" {
const src: [:0]const u8 =
\\main :: () {
\\ arr : [3]s32 = ---;
\\ for arr: (it, ix) {
\\ for arr, 0.. (it, ix) {
\\ x := it + ix;
\\ }
\\}
@@ -3323,7 +3329,7 @@ test "analyzeDocument: for-loop with underscore capture" {
const src: [:0]const u8 =
\\main :: () {
\\ arr : [3]s32 = ---;
\\ for arr: (_, ix) {
\\ for arr, 0.. (_, ix) {
\\ x := ix;
\\ }
\\}
@@ -3348,7 +3354,7 @@ test "analyzeDocument: for-loop value-only capture" {
const src: [:0]const u8 =
\\main :: () {
\\ arr : [3]s32 = ---;
\\ for arr: (val) {
\\ for arr (val) {
\\ x := val;
\\ }
\\}
@@ -3463,7 +3469,7 @@ test "lsp/inlayHint: a for-loop capture in a struct method shows its element typ
\\Game :: struct {
\\ legal: List(Move);
\\ scan :: (self: *Game) {
\\ for self.legal: (m) { x := m.flag; }
\\ for self.legal (m) { x := m.flag; }
\\ }
\\}
;

View File

@@ -24,10 +24,13 @@ pub const Parser = struct {
/// a `.compiler_expr` body so the per-method `#compiler` suffix can be
/// omitted.
struct_default_compiler: bool = false,
/// When true, parsePostfix does not treat a trailing `(` as a call. Set
/// while parsing a `for` range bound so `for 0..N (i)` reads `N` as the
/// end and leaves `(i)` for the cursor rather than parsing `N(i)`.
suppress_call: bool = false,
/// When true (set while parsing a `for` header's iterable expressions),
/// a top-level `(` group immediately followed by `{` or `=>` is the loop
/// CAPTURE, never call arguments — `for xs (x) { }` reads `(x)` as the
/// capture, while `for zip(a, b) (x, y) { }` still calls `zip(a, b)`
/// because that group is not the trailing one. Cleared inside any nested
/// bracket/paren/argument context.
in_for_header: bool = false,
/// When true (set while parsing an `onfail` body), a `raise` statement is
/// rejected — an error during cleanup has no propagation target. E1.7
/// extends this to the full {try, return, break, continue} set.
@@ -2477,9 +2480,13 @@ pub const Parser = struct {
var expr = try self.parsePrimary();
while (true) {
if (self.current.tag == .l_paren and !self.suppress_call) {
// Call
if (self.current.tag == .l_paren and !self.parenGroupIsForCapture()) {
// Call. Argument expressions are an ordinary nested context —
// the for-header capture rule does not apply inside them.
self.advance();
const saved_hdr_args = self.in_for_header;
self.in_for_header = false;
defer self.in_for_header = saved_hdr_args;
var args = std.ArrayList(*Node).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (args.items.len > 0) {
@@ -2564,10 +2571,10 @@ pub const Parser = struct {
} else if (self.current.tag == .l_bracket) {
// Index or slice access: expr[expr] or expr[start..end]
self.advance();
// Inside `[...]`, calls parse normally even within a range bound.
const saved_suppress_idx = self.suppress_call;
self.suppress_call = false;
defer self.suppress_call = saved_suppress_idx;
// Inside `[...]`, calls parse normally even within a for header.
const saved_hdr_idx = self.in_for_header;
self.in_for_header = false;
defer self.in_for_header = saved_hdr_idx;
if (self.current.tag == .dot_dot) {
// [..end]
self.advance();
@@ -2794,11 +2801,11 @@ pub const Parser = struct {
}
self.advance(); // skip '('
// A `(` here opens a grouping/tuple, not a `for` range bound, so
// calls inside it parse normally even within a range bound.
const saved_suppress_grp = self.suppress_call;
self.suppress_call = false;
defer self.suppress_call = saved_suppress_grp;
// A `(` here opens a grouping/tuple, so calls inside it parse
// normally even within a for header.
const saved_hdr_grp = self.in_for_header;
self.in_for_header = false;
defer self.in_for_header = saved_hdr_grp;
// Check for named tuple: (name: expr, ...)
if (self.current.tag == .identifier and self.peekNext() == .colon) {
@@ -3163,79 +3170,94 @@ pub const Parser = struct {
const start = self.current.loc.start;
self.advance(); // skip 'for'
const iterable = try self.parseExpr();
var iterables = std.ArrayList(ast.ForIterable).empty;
var captures = std.ArrayList(ast.ForCapture).empty;
// Range form: `for start..end (i)? { }`. The `..` only appears here for a
// range (slice ranges live inside `[]`), so it's unambiguous.
var range_end: ?*Node = null;
if (self.current.tag == .dot_dot) {
self.advance(); // skip '..'
const saved_suppress = self.suppress_call;
self.suppress_call = true;
range_end = try self.parseExpr();
self.suppress_call = saved_suppress;
// Header: comma-separated iterables, each a collection expression or
// a range (`a..b`, `a..=b`, open `a..`). Top-level trailing call
// parens are read as the capture (see parenGroupIsForCapture).
const saved_hdr = self.in_for_header;
self.in_for_header = true;
while (true) {
const expr = try self.parseExpr();
var it = ast.ForIterable{ .expr = expr };
if (self.current.tag == .dot_dot or self.current.tag == .dot_dot_eq) {
it.is_range = true;
it.inclusive = self.current.tag == .dot_dot_eq;
self.advance();
// End expression — absent for the open range `a..`, i.e. when
// the header continues (`,`), the body starts (`{` / `=>`),
// or the capture group follows.
const open = switch (self.current.tag) {
.comma, .l_brace, .fat_arrow => true,
.l_paren => self.parenGroupIsForCapture(),
else => false,
};
if (open) {
if (it.inclusive) return self.fail("'..=' requires an end expression — the open form is 'a..'");
} else {
it.range_end = try self.parseExpr();
}
}
try iterables.append(self.allocator, it);
if (self.current.tag != .comma) break;
self.advance();
}
self.in_for_header = saved_hdr;
// Migration aid for the pre-multi-iterable syntax.
if (self.current.tag == .colon) {
return self.fail("for-loop syntax: the ':' before the capture was removed — write `for xs (x) { }` (index via `for xs, 0.. (x, i)`)");
}
var capture_name: []const u8 = "";
var capture_span: ?ast.Span = null;
var capture_is_raw = false;
var index_name: ?[]const u8 = null;
var index_span: ?ast.Span = null;
var index_is_raw = false;
var capture_by_ref = false;
if (range_end != null) {
// Optional cursor, introduced by `:` for symmetry with the
// collection form: `for 0..N: (i)` (or `for 0..N` with no cursor).
// The colon is required when a cursor is present.
if (self.current.tag == .colon) {
self.advance();
try self.expect(.l_paren);
if (self.current.tag != .identifier) return self.fail("expected cursor variable name");
capture_name = self.tokenSlice(self.current);
capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
capture_is_raw = self.current.is_raw;
self.advance();
try self.expect(.r_paren);
}
} else {
// Collection form: `: (capture, index?)`. A leading `*` on the
// capture (`(*x)`) binds it by pointer into the collection.
try self.expect(.colon);
try self.expect(.l_paren);
if (self.current.tag == .star) {
capture_by_ref = true;
self.advance();
}
if (self.current.tag != .identifier) return self.fail("expected capture variable name");
capture_name = self.tokenSlice(self.current);
capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
capture_is_raw = self.current.is_raw;
// Capture group: `(x)`, `(*x)`, `(a, b, ...)` — positional, one
// capture per iterable.
if (self.current.tag == .l_paren) {
self.advance();
if (self.current.tag == .comma) {
while (true) {
var cap = ast.ForCapture{ .name = "" };
if (self.current.tag == .star) {
cap.by_ref = true;
self.advance();
}
if (self.current.tag != .identifier) return self.fail("expected capture variable name (a call iterable also needs a capture: `for f(n) (x) { }`)");
cap.name = self.tokenSlice(self.current);
cap.span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
cap.is_raw = self.current.is_raw;
self.advance();
if (self.current.tag != .identifier) return self.fail("expected index variable name");
index_name = self.tokenSlice(self.current);
index_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
index_is_raw = self.current.is_raw;
try captures.append(self.allocator, cap);
if (self.current.tag != .comma) break;
self.advance();
}
try self.expect(.r_paren);
}
const body = try self.parseBlock();
if (captures.items.len != 0 and captures.items.len != iterables.items.len) {
return self.fail("for capture count must match the iterable count — one capture per iterable");
}
if (iterables.items[0].is_range and iterables.items[0].range_end == null) {
return self.fail("the first iterable must have a bounded length (it drives the loop) — an open range 'a..' may only follow it");
}
for (iterables.items, 0..) |it, i| {
if (it.is_range and i < captures.items.len and captures.items[i].by_ref) {
return self.fail("a range element cannot be captured by reference");
}
}
// Body: a block, or the arrow form `=> stmt` (a full statement, so
// assignments like `=> s += x;` work; parseStmt owns the `;`).
var body: *Node = undefined;
if (self.current.tag == .fat_arrow) {
self.advance();
body = try self.parseStmt();
} else {
body = try self.parseBlock();
}
return try self.createNode(start, .{ .for_expr = .{
.iterable = iterable,
.iterables = try iterables.toOwnedSlice(self.allocator),
.captures = try captures.toOwnedSlice(self.allocator),
.body = body,
.capture_name = capture_name,
.capture_span = capture_span,
.capture_is_raw = capture_is_raw,
.index_name = index_name,
.index_span = index_span,
.index_is_raw = index_is_raw,
.range_end = range_end,
.capture_by_ref = capture_by_ref,
} });
}
@@ -3780,6 +3802,35 @@ pub const Parser = struct {
return tok.tag;
}
/// With `current` on `(`: the tag of the token right after the matching
/// `)`, scanning a throwaway copy of the lexer. Only parens are counted —
/// they must balance lexically regardless of what nests inside.
fn tagAfterParenGroup(self: *Parser) Tag {
var lex = self.lexer;
var depth: u32 = 1;
while (true) {
const tok = lex.next();
switch (tok.tag) {
.l_paren => depth += 1,
.r_paren => {
depth -= 1;
if (depth == 0) return lex.next().tag;
},
.eof => return .eof,
else => {},
}
}
}
/// For-header capture rule: a top-level `(` group immediately followed by
/// `{` or `=>` is the loop capture, so parsePostfix must not consume it
/// as call arguments.
fn parenGroupIsForCapture(self: *Parser) bool {
if (!self.in_for_header) return false;
const after = self.tagAfterParenGroup();
return after == .l_brace or after == .fat_arrow;
}
fn advance(self: *Parser) void {
self.prev_end = self.current.loc.end;
self.current = self.lexer.next();
@@ -4541,7 +4592,7 @@ test "E1.7 try rejected inside an onfail body" {
test "E1.7 break rejected inside a defer body (transitive through a loop)" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), "f :: () { defer { for 0..1: (i) { break; } } }");
var parser = Parser.init(arena.allocator(), "f :: () { defer { for 0..1 (i) { break; } } }");
try std.testing.expectError(error.ParseError, parser.parse());
}

View File

@@ -1186,24 +1186,24 @@ pub const Analyzer = struct {
}
},
.for_expr => |fe| {
try self.analyzeNode(fe.iterable);
for (fe.iterables) |it| {
try self.analyzeNode(it.expr);
if (it.range_end) |re| try self.analyzeNode(re);
}
try self.pushScope();
if (!std.mem.eql(u8, fe.capture_name, "_")) {
for (fe.captures, 0..) |cap, i| {
if (std.mem.eql(u8, cap.name, "_")) continue;
const it = fe.iterables[i];
var cap_ty: ?Type = null;
if (fe.range_end != null) {
if (it.is_range) {
cap_ty = .{ .signed = 64 };
} else if (self.elementTypeOf(self.inferExprType(fe.iterable))) |elem| {
cap_ty = if (fe.capture_by_ref)
} else if (self.elementTypeOf(self.inferExprType(it.expr))) |elem| {
cap_ty = if (cap.by_ref)
(if (elem.toName()) |en| Type{ .pointer_type = .{ .pointee_name = en, .is_raw = innerNameIsRaw(elem) } } else elem)
else
elem;
}
try self.addSymbol(fe.capture_name, .variable, cap_ty, node.span);
}
if (fe.index_name) |idx_name| {
if (!std.mem.eql(u8, idx_name, "_")) {
try self.addSymbol(idx_name, .variable, .{ .signed = 64 }, node.span);
}
try self.addSymbol(cap.name, .variable, cap_ty, node.span);
}
try self.analyzeNode(fe.body);
self.popScope();
@@ -1680,7 +1680,12 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
if (findNodeAtOffset(we.body, offset)) |found| return found;
},
.for_expr => |fe| {
if (findNodeAtOffset(fe.iterable, offset)) |found| return found;
for (fe.iterables) |it| {
if (findNodeAtOffset(it.expr, offset)) |found| return found;
if (it.range_end) |re| {
if (findNodeAtOffset(re, offset)) |found| return found;
}
}
if (findNodeAtOffset(fe.body, offset)) |found| return found;
},
.spread_expr => |se| {
@@ -2242,9 +2247,9 @@ test "sema: for-loop captures resolve element, by-ref pointer, and range cursor"
"List :: struct ($T: Type) { items: [*]T = null; len: s64 = 0; }" ++
"Game :: struct { legal: List(Move);" ++
" scan :: (self: *Game) {" ++
" for self.legal: (m) { a := m.flag; }" ++
" for self.legal: (*p) { b := p.flag; }" ++
" for 0..10: (i) { c := i; }" ++
" for self.legal (m) { a := m.flag; }" ++
" for self.legal (*p) { b := p.flag; }" ++
" for 0..10 (i) { c := i; }" ++
" } }";
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();

View File

@@ -52,6 +52,7 @@ pub const Tag = enum {
comma, // ,
dot, // .
dot_dot, // ..
dot_dot_eq, // ..=
dollar, // $
// Operators
@@ -150,6 +151,7 @@ pub const Tag = enum {
.comma => ",",
.dot => ".",
.dot_dot => "..",
.dot_dot_eq => "..=",
.dollar => "$",
.plus => "+",
.minus => "-",