lang: inline for element form over packs — multi-iterable parity

This commit is contained in:
agra
2026-06-11 14:42:59 +03:00
parent 03dc10bba3
commit 40805e08ec
15 changed files with 277 additions and 40 deletions

View File

@@ -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();
}

View File

@@ -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) { }
}

View File

@@ -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
| ^^

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -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

View File

@@ -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) { }
| ^^^

View File

@@ -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 — `=`

View File

@@ -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[<i>]` it aliases.
if (binding.pack_elem) |elem| return self.l.inferExprType(elem);
return binding.ty;
}
}

View File

@@ -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[<i>]` 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.

View File

@@ -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[<i>]`
/// (`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);
}
}

View File

@@ -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[<i>]` as the object, so every pack-element rule below
// (interface-only constraint check, projection, substitution) sees the
// canonical `xs[i].<field>` 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[<i>]` 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);
}

View File

@@ -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 }),
}
}