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:
54
src/ast.zig
54
src/ast.zig
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
\\ }
|
||||
\\}
|
||||
;
|
||||
|
||||
203
src/parser.zig
203
src/parser.zig
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
35
src/sema.zig
35
src/sema.zig
@@ -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();
|
||||
|
||||
@@ -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 => "-",
|
||||
|
||||
Reference in New Issue
Block a user