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

@@ -23,6 +23,10 @@ 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,
pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser {
var lexer = Lexer.init(source);
@@ -1995,6 +1999,13 @@ pub const Parser = struct {
try self.expectSemicolonAfter(expr);
return expr;
}
if (self.peekNext() == .kw_for) {
self.advance(); // skip 'inline'
const expr = try self.parseForExpr();
expr.data.for_expr.is_inline = true;
try self.expectSemicolonAfter(expr);
return expr;
}
}
// Block-form if/while/for as statements — parse directly to prevent
@@ -2187,7 +2198,7 @@ pub const Parser = struct {
var expr = try self.parsePrimary();
while (true) {
if (self.current.tag == .l_paren) {
if (self.current.tag == .l_paren and !self.suppress_call) {
// Call
self.advance();
var args = std.ArrayList(*Node).empty;
@@ -2274,6 +2285,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;
if (self.current.tag == .dot_dot) {
// [..end]
self.advance();
@@ -2458,6 +2473,12 @@ 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;
// Check for named tuple: (name: expr, ...)
if (self.current.tag == .identifier and self.peekNext() == .colon) {
return self.parseTupleLiteralNamed(start);
@@ -2803,25 +2824,45 @@ pub const Parser = struct {
const iterable = try self.parseExpr();
// Expect ': (' capture clause
try self.expect(.colon);
try self.expect(.l_paren);
// Capture variable name
if (self.current.tag != .identifier) return self.fail("expected capture variable name");
const capture_name = self.tokenSlice(self.current);
self.advance();
// Optional ', index_name'
var index_name: ?[]const u8 = null;
if (self.current.tag == .comma) {
self.advance();
if (self.current.tag != .identifier) return self.fail("expected index variable name");
index_name = self.tokenSlice(self.current);
self.advance();
// 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;
}
var capture_name: []const u8 = "";
var index_name: ?[]const u8 = null;
if (range_end != null) {
// Range capture is the optional cursor: `(i)` or nothing.
if (self.current.tag == .l_paren) {
self.advance();
if (self.current.tag != .identifier) return self.fail("expected cursor variable name");
capture_name = self.tokenSlice(self.current);
self.advance();
try self.expect(.r_paren);
}
} else {
// Collection form: `: (capture, index?)`.
try self.expect(.colon);
try self.expect(.l_paren);
if (self.current.tag != .identifier) return self.fail("expected capture variable name");
capture_name = self.tokenSlice(self.current);
self.advance();
if (self.current.tag == .comma) {
self.advance();
if (self.current.tag != .identifier) return self.fail("expected index variable name");
index_name = self.tokenSlice(self.current);
self.advance();
}
try self.expect(.r_paren);
}
try self.expect(.r_paren);
const body = try self.parseBlock();
return try self.createNode(start, .{ .for_expr = .{
@@ -2829,6 +2870,7 @@ pub const Parser = struct {
.body = body,
.capture_name = capture_name,
.index_name = index_name,
.range_end = range_end,
} });
}