lang F1: range-based for + inline-for unroll over packs

Add range loop syntax:
- runtime  for start..end (i) { }   counting loop, cursor optional, end exclusive
- comptime inline for start..end (i) { }   comptime-unrolled body

The inline form binds the cursor as an int_val comptime constant per
iteration, so xs[i] over a heterogeneous pack substitutes the concrete
per-position element -- the canonical's pack-iteration vehicle
(inline for 0..sources.len (i) { sources[i].addListener(...) }).

- AST: ForExpr.range_end, ForExpr.is_inline
- parser: parseForExpr range vs collection form; suppress_call flag so
  N (i) is not read as a call N(i) while parsing a range bound
- lower: lowerRuntimeRangeFor / lowerInlineRangeFor; evalComptimeInt;
  comptimeIndexOf extends pack-index resolution beyond int literals

Revises spec's inline for i in 0..N to the no-in, range-first, paren-cursor
form. Regression: examples/200-for-range.sx.
This commit is contained in:
agra
2026-05-29 21:36:17 +03:00
parent 27fd5e1e6a
commit 27c88d4d26
7 changed files with 305 additions and 29 deletions

View File

@@ -1006,8 +1006,7 @@ may do, regardless of the concrete arg types at any particular call site.
|---|---|---|
| Length | `xs.len` | comptime int (field-style, not `len(xs)`) |
| Index | `xs[i]` | i-th element; `i` must be comptime |
| Comptime unroll (index) | `inline for i in 0..xs.len { ... }` | unrolled loop; not `#for` |
| Comptime unroll (element) | `inline for x in xs { ... }` | desugars to index form; `x`'s type varies per iteration |
| Comptime unroll (index) | `inline for 0..xs.len (i) { ... }` | unrolled loop; cursor `i` is a comptime constant per iteration; not `#for` |
| Projection | `xs.field` | see "Pack projection" |
| Spread → call args | `..xs` / `..xs.field` | expands to N positional args |
| Spread → tuple value | `(..xs)` / `(..xs.field)` | materializes a tuple |
@@ -1057,7 +1056,7 @@ value-requiring position is a compile error with a tailored suggestion:
- storing/binding it (`let x = xs;`, `self.f = xs;`) → suggest `(..xs)`;
- passing to a non-pack-taking call (`f(xs)`) → suggest `..xs`;
- returning it (`return xs;`) → suggest a tuple return with `(..xs)`;
- iterating at runtime (`for x in xs`, `xs[runtime_i]`) → suggest `inline for`.
- iterating at runtime (`for xs : (x)`, `xs[runtime_i]`) → suggest `inline for`.
#### Storage and protocol conformance
@@ -1089,7 +1088,7 @@ map :: (mapper: Closure(..sources.T) -> $R, ..sources: ValueListenable)
c.own_allocator = context.allocator;
c.mapper = mapper;
c.sources = (..sources); // pack-to-tuple materialization
inline for i in 0..sources.len { // comptime unroll over the pack
inline for 0..sources.len (i) { // comptime unroll over the pack
sources[i].addListener((_) => c.recompute());
}
c.value = mapper(..sources.value); // pack spread + projection in a call
@@ -1510,6 +1509,21 @@ 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
```
`start` and `end` are `s64` expressions; the loop counts `start, start+1, …, end-1`.
The cursor parens are optional — omit them when the body doesn't need the index.
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
```sx
for iterable: (elem) { } // element alias (no copy)
for iterable: (elem, ix) { } // element + index