lang: range bound markers — '=' inclusive / '<' exclusive on either side of '..'

Each side of '..' takes an optional bound marker, defaulting to
start-inclusive, end-exclusive (a..b == a=..<b; a..=b stays the short
end-inclusive spelling):

    for 0<..<N (i) { }   // 1 .. N-1   (both exclusive)
    for 0=..=N (i) { }   // 0 .. N     (both inclusive)
    for 0<..=N (i) { }   // 1 .. N
    for 0..<N  (i) { }   // 0 .. N-1   (explicit default)
    for xs, 2<.. (x, i)  // open range, exclusive start: i = 3, 4, ...

The nine lexemes are single tokens (maximal munch on '<'/'='/'..'), so
expression parsing never sees the leading marker as a comparison; '<',
'<<', '<=', '==', '=>' lex unchanged. An explicit end marker makes the
end expression mandatory; open forms are a.. / a<.. / a=... Works in
runtime, multi-iterable, and inline-for headers.

Regression: examples/0051-basic-for-range-bounds.sx (full matrix, open
start-marked ranges, comptime unroll, runtime bounds, lexer
non-regression); 1152's pinned message generalized.
This commit is contained in:
agra
2026-06-10 20:55:31 +03:00
parent 116af2359e
commit fd14ab5694
14 changed files with 194 additions and 17 deletions

View File

@@ -646,13 +646,19 @@ pub const WhileExpr = struct {
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. Null for a plain collection AND for the open-ended range
/// `a..` (distinguished by `is_range`).
range_end: ?*Node = null,
/// True for any range form (`a..`, `a..b`, `a..=b`).
/// True for any range form. Each side of `..` takes an optional bound
/// marker — `=` inclusive, `<` exclusive — with defaults start-inclusive,
/// end-exclusive: `a..b` ≡ `a=..<b`; `a<..<b` is 1-past-start to
/// end-1; `a=..=b` includes both ends; `a..=b` keeps the short
/// end-inclusive spelling.
is_range: bool = false,
/// `a..=b` — end is inclusive.
inclusive: bool = false,
/// `<..` family — start is exclusive (cursor begins at start+1).
start_exclusive: bool = false,
/// `..=` family — end is inclusive.
end_inclusive: bool = false,
};
/// One capture of a `for` header: `(x)`, `(*x)`, `(x, y, ...)`.

View File

@@ -315,13 +315,14 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
for (fe.iterables, 0..) |it, i| {
if (it.is_range) {
const start_ref = self.lowerExpr(it.expr);
var start_ref = self.lowerExpr(it.expr);
if (it.start_exclusive) start_ref = self.builder.add(start_ref, self.builder.constInt(1, .s64), .s64);
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);
if (it.end_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;
@@ -476,15 +477,16 @@ pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
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 {
var 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);
};
if (it.start_exclusive) start += 1;
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;
if (it.end_inclusive) end += 1;
const capture_name = if (fe.captures.len > 0) fe.captures[0].name else "";
var i: i64 = start;

View File

@@ -150,6 +150,10 @@ pub const Lexer = struct {
self.index += 1;
return self.makeToken(.dot_dot_eq, start, self.index);
}
if (self.peek() == '<') {
self.index += 1;
return self.makeToken(.dot_dot_lt, start, self.index);
}
return self.makeToken(.dot_dot, start, self.index);
}
return self.makeToken(.dot, start, self.index);
@@ -175,6 +179,19 @@ pub const Lexer = struct {
self.index += 1;
return self.makeToken(.fat_arrow, start, self.index);
}
// Range with an explicit inclusive start: `=..`, `=..=`, `=..<`.
if (self.peek() == '.' and self.peekAt(1) == '.') {
self.index += 2;
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.eq_dot_dot_eq, start, self.index);
}
if (self.peek() == '<') {
self.index += 1;
return self.makeToken(.eq_dot_dot_lt, start, self.index);
}
return self.makeToken(.eq_dot_dot, start, self.index);
}
return self.makeToken(.equal, start, self.index);
},
'+' => {
@@ -266,6 +283,19 @@ pub const Lexer = struct {
return self.makeToken(.bang, start, self.index);
},
'<' => {
// Range with an exclusive start: `<..`, `<..=`, `<..<`.
if (self.peek() == '.' and self.peekAt(1) == '.') {
self.index += 2;
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.lt_dot_dot_eq, start, self.index);
}
if (self.peek() == '<') {
self.index += 1;
return self.makeToken(.lt_dot_dot_lt, start, self.index);
}
return self.makeToken(.lt_dot_dot, start, self.index);
}
if (self.peek() == '<') {
self.index += 1;
if (self.peek() == '=') {
@@ -433,6 +463,14 @@ pub const Lexer = struct {
return 0;
}
fn peekAt(self: *const Lexer, offset: u32) u8 {
const i = self.index + offset;
if (i < self.source.len) {
return self.source[i];
}
return 0;
}
fn makeToken(_: *const Lexer, tag: Tag, start: u32, end: u32) Token {
return .{ .tag = tag, .loc = .{ .start = start, .end = end } };
}

View File

@@ -1771,6 +1771,13 @@ pub const Server = struct {
.dot,
.dot_dot,
.dot_dot_eq,
.dot_dot_lt,
.lt_dot_dot,
.lt_dot_dot_eq,
.lt_dot_dot_lt,
.eq_dot_dot,
.eq_dot_dot_eq,
.eq_dot_dot_lt,
.dollar,
.l_paren,
.r_paren,

View File

@@ -3181,9 +3181,10 @@ pub const Parser = struct {
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) {
if (rangeTokenInfo(self.current.tag)) |rt| {
it.is_range = true;
it.inclusive = self.current.tag == .dot_dot_eq;
it.start_exclusive = rt.start_exclusive;
it.end_inclusive = rt.end_inclusive;
self.advance();
// End expression — absent for the open range `a..`, i.e. when
// the header continues (`,`), the body starts (`{` / `=>`),
@@ -3194,7 +3195,7 @@ pub const Parser = struct {
else => false,
};
if (open) {
if (it.inclusive) return self.fail("'..=' requires an end expression — the open form is 'a..'");
if (rt.end_marked) return self.fail("a range with an explicit end marker ('..=' / '..<') requires an end expression — the open form is 'a..'");
} else {
it.range_end = try self.parseExpr();
}
@@ -3831,6 +3832,32 @@ pub const Parser = struct {
return after == .l_brace or after == .fat_arrow;
}
const RangeTokenInfo = struct {
start_exclusive: bool,
end_inclusive: bool,
/// True when the lexeme carries an explicit end marker (`=` / `<`
/// after the dots) — the end expression is then mandatory.
end_marked: bool,
};
/// Range lexemes: each side of `..` takes an optional bound marker, `=`
/// inclusive / `<` exclusive, defaulting to start-inclusive,
/// end-exclusive (`a..b` ≡ `a=..<b`).
fn rangeTokenInfo(tag: Tag) ?RangeTokenInfo {
return switch (tag) {
.dot_dot => .{ .start_exclusive = false, .end_inclusive = false, .end_marked = false },
.dot_dot_eq => .{ .start_exclusive = false, .end_inclusive = true, .end_marked = true },
.dot_dot_lt => .{ .start_exclusive = false, .end_inclusive = false, .end_marked = true },
.lt_dot_dot => .{ .start_exclusive = true, .end_inclusive = false, .end_marked = false },
.lt_dot_dot_eq => .{ .start_exclusive = true, .end_inclusive = true, .end_marked = true },
.lt_dot_dot_lt => .{ .start_exclusive = true, .end_inclusive = false, .end_marked = true },
.eq_dot_dot => .{ .start_exclusive = false, .end_inclusive = false, .end_marked = false },
.eq_dot_dot_eq => .{ .start_exclusive = false, .end_inclusive = true, .end_marked = true },
.eq_dot_dot_lt => .{ .start_exclusive = false, .end_inclusive = false, .end_marked = true },
else => null,
};
}
fn advance(self: *Parser) void {
self.prev_end = self.current.loc.end;
self.current = self.lexer.next();

View File

@@ -53,6 +53,13 @@ pub const Tag = enum {
dot, // .
dot_dot, // ..
dot_dot_eq, // ..=
dot_dot_lt, // ..<
lt_dot_dot, // <..
lt_dot_dot_eq, // <..=
lt_dot_dot_lt, // <..<
eq_dot_dot, // =..
eq_dot_dot_eq, // =..=
eq_dot_dot_lt, // =..<
dollar, // $
// Operators
@@ -152,6 +159,13 @@ pub const Tag = enum {
.dot => ".",
.dot_dot => "..",
.dot_dot_eq => "..=",
.dot_dot_lt => "..<",
.lt_dot_dot => "<..",
.lt_dot_dot_eq => "<..=",
.lt_dot_dot_lt => "<..<",
.eq_dot_dot => "=..",
.eq_dot_dot_eq => "=..=",
.eq_dot_dot_lt => "=..<",
.dollar => "$",
.plus => "+",
.minus => "-",