From 40805e08ec17a70cf8361fcb24f38c18f8ed20c9 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 14:42:59 +0300 Subject: [PATCH] =?UTF-8?q?lang:=20inline=20for=20element=20form=20over=20?= =?UTF-8?q?packs=20=E2=80=94=20multi-iterable=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/0545-packs-inline-for-element.sx | 63 ++++++++ ...-diagnostics-inline-for-pack-rejections.sx | 31 ++++ .../expected/0536-packs-pack-as-value.stderr | 2 +- .../0545-packs-inline-for-element.exit | 1 + .../0545-packs-inline-for-element.stderr | 1 + .../0545-packs-inline-for-element.stdout | 11 ++ ...iagnostics-inline-for-pack-rejections.exit | 1 + ...gnostics-inline-for-pack-rejections.stderr | 23 +++ ...gnostics-inline-for-pack-rejections.stdout | 1 + specs.md | 12 +- src/ir/expr_typer.zig | 3 + src/ir/lower.zig | 6 + src/ir/lower/control_flow.zig | 141 +++++++++++++----- src/ir/lower/expr.zig | 19 +++ src/ir/lower/pack.zig | 2 +- 15 files changed, 277 insertions(+), 40 deletions(-) create mode 100644 examples/0545-packs-inline-for-element.sx create mode 100644 examples/1164-diagnostics-inline-for-pack-rejections.sx create mode 100644 examples/expected/0545-packs-inline-for-element.exit create mode 100644 examples/expected/0545-packs-inline-for-element.stderr create mode 100644 examples/expected/0545-packs-inline-for-element.stdout create mode 100644 examples/expected/1164-diagnostics-inline-for-pack-rejections.exit create mode 100644 examples/expected/1164-diagnostics-inline-for-pack-rejections.stderr create mode 100644 examples/expected/1164-diagnostics-inline-for-pack-rejections.stdout diff --git a/examples/0545-packs-inline-for-element.sx b/examples/0545-packs-inline-for-element.sx new file mode 100644 index 0000000..df15b6d --- /dev/null +++ b/examples/0545-packs-inline-for-element.sx @@ -0,0 +1,63 @@ +// `inline for` element form over a pack — multi-iterable parity with the +// runtime for-loop. Position 0 drives the unroll count (a pack's arity or a +// bounded range's span); trailing iterables pair with it. A pack capture is +// the concrete per-position element viewed through the constraint protocol +// (same semantics as `xs[i]`); a range capture is a comptime cursor. +// +// inline for xs (x) — element form +// inline for xs, 0.. (x, i) — element + paired index +// inline for 0..xs.len, xs (i, x) — range driver, trailing pack +// inline for xs { } — captureless; N=0 unrolls nothing + +#import "modules/std.sx"; + +Show :: protocol { show :: () -> string; } +IntBox :: struct { v: s64; } +StrBox :: struct { s: string; } +impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } } +impl Show for StrBox { show :: (self: *StrBox) -> string { self.s } } + +bare :: (..xs: Show) { + inline for xs (x) { + print("bare: {}\n", x.show()); + } +} +elem_and_index :: (..xs: Show) { + inline for xs, 0.. (x, i) { + print("{}: {}\n", i, x.show()); + } +} +range_driver :: (..xs: Show) { + inline for 0..xs.len, xs (i, x) { + print("r{}: {}\n", i, x.show()); + } +} +offset_index :: (..xs: Show) { + inline for xs, 10.. (x, i) { + print("{} -> {}\n", i, x.show()); + } +} +captureless :: (..xs: Show) { + n := 0; + inline for xs { n += 1; } + print("ran {}\n", n); +} +value_pos :: (..xs: Show) { + inline for xs (x) { + print("val: {}\n", x); + } +} +empty :: (..xs: Show) { + inline for xs (x) { print("never\n"); } + print("empty ok\n"); +} + +main :: () { + bare(IntBox.{ v = 7 }, StrBox.{ s = "hi" }); + elem_and_index(IntBox.{ v = 7 }, StrBox.{ s = "hi" }); + range_driver(IntBox.{ v = 1 }, StrBox.{ s = "two" }); + offset_index(StrBox.{ s = "x" }, StrBox.{ s = "y" }); + captureless(IntBox.{ v = 0 }, IntBox.{ v = 0 }, IntBox.{ v = 0 }); + value_pos(IntBox.{ v = 42 }); + empty(); +} diff --git a/examples/1164-diagnostics-inline-for-pack-rejections.sx b/examples/1164-diagnostics-inline-for-pack-rejections.sx new file mode 100644 index 0000000..9b14b53 --- /dev/null +++ b/examples/1164-diagnostics-inline-for-pack-rejections.sx @@ -0,0 +1,31 @@ +// `inline for` pack rejections: (1) a pack-element capture exposes only the +// constraint protocol's interface (same rule as `xs[i]`, example 0530); +// (2) a pack element cannot be captured by reference (`(*x)` — an element is +// an AST-substituted call arg, no storage); (3) a trailing pack shorter than +// the driving iterable; (4) a non-pack, non-range iterable. + +#import "modules/std.sx"; + +Show :: protocol { show :: () -> string; } +IntBox :: struct { v: s64; } +impl Show for IntBox { show :: (self: *IntBox) -> string { int_to_string(self.v) } } + +leak :: (..xs: Show) { + inline for xs (x) { + print("{}\n", x.v); + } +} +borrow :: (..xs: Show) { + inline for xs (*x) { } +} +short :: (..xs: Show) { + inline for 0..5, xs (i, x) { } +} + +main :: () { + leak(IntBox.{ v = 5 }); + borrow(IntBox.{ v = 5 }); + short(IntBox.{ v = 1 }, IntBox.{ v = 2 }); + arr := .[1, 2, 3]; + inline for arr (x) { } +} diff --git a/examples/expected/0536-packs-pack-as-value.stderr b/examples/expected/0536-packs-pack-as-value.stderr index dae0bef..cbd24b9 100644 --- a/examples/expected/0536-packs-pack-as-value.stderr +++ b/examples/expected/0536-packs-pack-as-value.stderr @@ -37,7 +37,7 @@ error: pack 'xs' has no runtime value — a pack is comptime-only and can't be u 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 +help: to iterate at comptime use `inline for xs (x)` (or `inline for 0..xs.len (i)` for the index); 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 | ^^ diff --git a/examples/expected/0545-packs-inline-for-element.exit b/examples/expected/0545-packs-inline-for-element.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0545-packs-inline-for-element.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0545-packs-inline-for-element.stderr b/examples/expected/0545-packs-inline-for-element.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0545-packs-inline-for-element.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0545-packs-inline-for-element.stdout b/examples/expected/0545-packs-inline-for-element.stdout new file mode 100644 index 0000000..34fdf53 --- /dev/null +++ b/examples/expected/0545-packs-inline-for-element.stdout @@ -0,0 +1,11 @@ +bare: 7 +bare: hi +0: 7 +1: hi +r0: 1 +r1: two +10 -> x +11 -> y +ran 3 +val: IntBox{v: 42} +empty ok diff --git a/examples/expected/1164-diagnostics-inline-for-pack-rejections.exit b/examples/expected/1164-diagnostics-inline-for-pack-rejections.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1164-diagnostics-inline-for-pack-rejections.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1164-diagnostics-inline-for-pack-rejections.stderr b/examples/expected/1164-diagnostics-inline-for-pack-rejections.stderr new file mode 100644 index 0000000..d907d03 --- /dev/null +++ b/examples/expected/1164-diagnostics-inline-for-pack-rejections.stderr @@ -0,0 +1,23 @@ +error: 'v' is not part of protocol 'Show' — a pack element exposes only the protocol's interface + --> examples/1164-diagnostics-inline-for-pack-rejections.sx:15:23 + | +15 | print("{}\n", x.v); + | ^^^ + +error: a pack element cannot be captured by reference + --> examples/1164-diagnostics-inline-for-pack-rejections.sx:19:21 + | +19 | inline for xs (*x) { } + | ^ + +error: inline for: pack 'xs' has 2 elements but the unroll is 5 iterations + --> examples/1164-diagnostics-inline-for-pack-rejections.sx:22:22 + | +22 | inline for 0..5, xs (i, x) { } + | ^^ + +error: inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) { }` / `inline for xs (x) { }` + --> examples/1164-diagnostics-inline-for-pack-rejections.sx:30:16 + | +30 | inline for arr (x) { } + | ^^^ diff --git a/examples/expected/1164-diagnostics-inline-for-pack-rejections.stdout b/examples/expected/1164-diagnostics-inline-for-pack-rejections.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1164-diagnostics-inline-for-pack-rejections.stdout @@ -0,0 +1 @@ + diff --git a/specs.md b/specs.md index 95e8503..8a93605 100644 --- a/specs.md +++ b/specs.md @@ -1343,6 +1343,8 @@ 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 0..xs.len (i) { ... }` | unrolled loop; cursor `i` is a comptime constant per iteration; not `#for` | +| Comptime unroll (element) | `inline for xs (x) { ... }` | unrolled loop; `x` is the concrete i-th element, viewed through the constraint protocol (≡ `xs[i]`) | +| Comptime unroll (element + index) | `inline for xs, 0.. (x, i) { ... }` | multi-iterable parity with the runtime `for`: position 0 drives the count, a trailing open range pairs the cursor | | 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 | @@ -1395,8 +1397,9 @@ 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)` - for a comptime unroll, or take `..xs: []P` for a runtime loop. +- iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for xs (x)` (or + `inline for 0..xs.len (i)` for the index) for a comptime unroll, or take + `..xs: []P` for a runtime loop. The recurring runtime escape hatch is the **slice-of-protocol variadic** `..xs: []P` (see "Variadic Functions"): it is the runtime, protocol-erased @@ -2093,7 +2096,10 @@ 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 +inline for 0..n (i) { } // comptime unroll; first range bounded +inline for xs, 0.. (x, i) { } // comptime unroll over a PACK: x = the + // concrete i-th element (see "Variadic + // Heterogeneous Type Packs") ``` **Range bound markers.** Each side of `..` takes an optional marker — `=` diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index e77cd20..c1c2451 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -265,6 +265,9 @@ pub const ExprTyper = struct { .identifier => |id| { if (self.l.scope) |scope| { if (scope.lookup(id.name)) |binding| { + // `inline for xs (x)` element capture — type as the + // synthesized `xs[]` it aliases. + if (binding.pack_elem) |elem| return self.l.inferExprType(elem); return binding.ty; } } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 6e70c79..c142b81 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -129,6 +129,12 @@ pub const Binding = struct { ty: TypeId, is_alloca: bool, // true if ref is a pointer that needs load is_ref_capture: bool = false, // `for xs: (*x)` — `ref` is `*elem`; auto-deref in value positions + /// `inline for xs (x)` element capture: `x` is an AST ALIAS for the + /// synthesized `xs[]` of the current unroll iteration (`ref` is + /// `.none`). Identifier consumers substitute this node, so the capture + /// inherits the full pack-element semantics — concrete-arg substitution, + /// typing, and the interface-only constraint check. + pack_elem: ?*ast.Node = null, }; // `init` / `deinit` / `put` are pub so collaborator unit tests (e.g. diff --git a/src/ir/lower/control_flow.zig b/src/ir/lower/control_flow.zig index 8399849..b4ed707 100644 --- a/src/ir/lower/control_flow.zig +++ b/src/ir/lower/control_flow.zig @@ -467,54 +467,125 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref { return self.builder.constInt(0, .void); } -/// 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. +/// Comptime-unrolled `inline for`. Iterables are comptime ranges and/or +/// PACKS, mirroring the runtime multi-iterable contract: position 0 drives +/// the iteration count (a pack's arity, or a bounded range's span) and +/// trailing range bounds are ignored. Per iteration the body is lowered +/// once; a range capture binds as an `int_val` comptime constant (so +/// `xs[i]` substitutes the concrete per-position argument), and a pack +/// capture binds as an AST alias for the synthesized `xs[]` +/// (`Binding.pack_elem`), inheriting full pack-element semantics — +/// substitution, typing, and the interface-only constraint check. +/// +/// inline for 0..xs.len (i) { xs[i].show(); } // index form +/// inline for xs (x) { x.show(); } // element form +/// inline for xs, 0.. (x, i) { ... } // element + index 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); - } - 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); + const IterClass = union(enum) { + range: i64, // comptime start value + pack: []const u8, // pack name }; - 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.end_inclusive) end += 1; - const capture_name = if (fe.captures.len > 0) fe.captures[0].name else ""; + var classes = std.ArrayList(IterClass).empty; + defer classes.deinit(self.alloc); + var count: i64 = 0; - var i: i64 = start; - while (i < end) : (i += 1) { + for (fe.iterables, 0..) |it, idx| { + if (it.is_range) { + 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; + if (idx == 0) { + const end_node = it.range_end orelse { + if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: the first range must be bounded — `inline for 0..N (i) {{ }}`", .{}); + return self.builder.constInt(0, .void); + }; + var 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", .{}); + return self.builder.constInt(0, .void); + }; + if (it.end_inclusive) end += 1; + count = end - start; + } + classes.append(self.alloc, .{ .range = start }) catch unreachable; + } else if (it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) { + const name = it.expr.data.identifier.name; + const len: i64 = if (self.pack_param_count) |ppc| @intCast(ppc.get(name) orelse 0) else 0; + if (idx == 0) { + count = len; + } else if (len < count) { + if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: pack '{s}' has {} element{s} but the unroll is {} iterations", .{ + name, len, if (len == 1) @as([]const u8, "") else @as([]const u8, "s"), count, + }); + return self.builder.constInt(0, .void); + } + classes.append(self.alloc, .{ .pack = name }) catch unreachable; + } else { + if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) {{ }}` / `inline for xs (x) {{ }}`", .{}); + return self.builder.constInt(0, .void); + } + } + + // `(*x)` on a pack element: there is no storage to borrow — an element + // is an AST-substituted call argument. + for (fe.captures, 0..) |cap, ci| { + if (cap.by_ref and ci < classes.items.len and classes.items[ci] == .pack) { + const sp = cap.span orelse fe.iterables[ci].expr.span; + if (self.diagnostics) |d| d.addFmt(.err, sp, "a pack element cannot be captured by reference", .{}); + return self.builder.constInt(0, .void); + } + } + + const CursorSave = struct { name: []const u8, had_prev: bool, prev: ComptimeValue }; + + var i: i64 = 0; + while (i < count) : (i += 1) { var body_scope = Scope.init(self.alloc, self.scope); const old_scope = self.scope; self.scope = &body_scope; - // Bind the cursor both as a runtime value (constInt, for uses like - // `print(i)`) and as a comptime constant (for `xs[i]` substitution). - var had_prev = false; - var prev: ComptimeValue = undefined; - 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; + var saves = std.ArrayList(CursorSave).empty; + defer saves.deinit(self.alloc); + + for (fe.captures, 0..) |cap, ci| { + if (cap.name.len == 0) continue; + switch (classes.items[ci]) { + .range => |start| { + // Bind the cursor both as a runtime value (constInt, for + // uses like `print(i)`) and as a comptime constant (for + // `xs[i]` substitution). + const v = start + i; + body_scope.put(cap.name, .{ .ref = self.builder.constInt(v, .s64), .ty = .s64, .is_alloca = false }); + var save = CursorSave{ .name = cap.name, .had_prev = false, .prev = undefined }; + if (self.comptime_constants.get(cap.name)) |p| { + save.had_prev = true; + save.prev = p; + } + saves.append(self.alloc, save) catch {}; + self.comptime_constants.put(cap.name, .{ .int_val = v }) catch {}; + }, + .pack => |pack_name| { + const span = fe.iterables[ci].expr.span; + const id_node = self.alloc.create(Node) catch break; + id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } }; + const idx_node = self.alloc.create(Node) catch break; + idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = i } } }; + const elem_node = self.alloc.create(Node) catch break; + elem_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } }; + const elem_ty = self.inferExprType(elem_node); + body_scope.put(cap.name, .{ .ref = Ref.none, .ty = elem_ty, .is_alloca = false, .pack_elem = elem_node }); + }, } - self.comptime_constants.put(capture_name, .{ .int_val = i }) catch {}; } self.lowerBlock(fe.body); - if (capture_name.len > 0) { - if (had_prev) { - self.comptime_constants.put(capture_name, prev) catch {}; + for (saves.items) |save| { + if (save.had_prev) { + self.comptime_constants.put(save.name, save.prev) catch {}; } else { - _ = self.comptime_constants.remove(capture_name); + _ = self.comptime_constants.remove(save.name); } } diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index bd295e3..3a904b5 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -407,6 +407,22 @@ pub fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId { } pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref { + // `inline for xs (x)` element capture as the receiver: re-enter with the + // synthesized `xs[]` as the object, so every pack-element rule below + // (interface-only constraint check, projection, substitution) sees the + // canonical `xs[i].` shape. + if (fa.object.data == .identifier) { + if (self.scope) |scope| { + if (scope.lookup(fa.object.data.identifier.name)) |binding| { + if (binding.pack_elem) |elem| { + var patched = fa.*; + patched.object = elem; + return self.lowerFieldAccess(&patched, span); + } + } + } + } + // `error.X` — an error-tag literal. The `error` keyword in expression // position parses as identifier "error" (E0.2), so `error.X` is a // field access we intercept here. `error` is reserved, so this is @@ -1602,6 +1618,9 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { } if (self.scope) |scope| { if (scope.lookup(id.name)) |binding| { + // `inline for xs (x)` element capture — lower the + // synthesized `xs[]` it aliases. + if (binding.pack_elem) |elem| break :blk self.lowerExpr(elem); if (binding.is_alloca) { break :blk self.builder.load(binding.ref, binding.ty); } diff --git a/src/ir/lower/pack.zig b/src/ir/lower/pack.zig index 3cd97f4..7d4df20 100644 --- a/src/ir/lower/pack.zig +++ b/src/ir/lower/pack.zig @@ -147,7 +147,7 @@ pub fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: .storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}), .call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}), .return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}), - .runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }), + .runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for {s} (x)` (or `inline for 0..{s}.len (i)` for the index); for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name, name }), .generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }), } }