lang: inline for element form over packs — multi-iterable parity
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user