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

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