fix(0110): for-over-array by-value fetch reads one element, not a full copy

lowerFor's by-value element fetch emitted index_get on the array VALUE;
the emitter realizes that as a whole-array spill to a stack temp + GEP,
per iteration — O(N^2) bytes copied per loop (and pre-0109 it also grew
the stack per iteration, segfaulting a [4096]s64 loop).

When the iterable is an array with addressable storage (and not deref'd
from a pointer, whose identifier alloca holds the pointer rather than
the array), the fetch is now index_gep on the storage + one element
load. Storage-less arrays keep the index_get fallback. The loaded
element remains a copy — mutating the capture does not write back.

Regression: examples/0048-basic-for-array-large.sx (sum over 4096
elements + by-value copy-guard).
This commit is contained in:
agra
2026-06-10 17:34:35 +03:00
parent 878c4226a6
commit bf47146085
6 changed files with 158 additions and 3 deletions

View File

@@ -289,11 +289,15 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
var iterable = self.lowerExpr(fe.iterable);
var iterable_ty = self.inferExprType(fe.iterable);
// `*List` / `*[]T` etc. — deref to the collection value.
// `*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;
}
// A `List(T)`-like struct iterates its `items[0..len]`; arrays/slices
@@ -333,14 +337,25 @@ pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
// 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 is_array = !iterable_ty.isBuiltin() and self.module.types.get(iterable_ty) == .array;
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 self.builder.emit(.{ .index_get = .{ .lhs = iterable, .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;