lang: for-loop by-ref element capture (for xs: (*x))

(*x) binds x to a pointer into the collection (index_gep) instead of a per-element value copy: passing it on (e.g. to a *T param) is zero-copy and mutations write back. In a value position x auto-derefs — a binary-op operand loads the element, a pointer-typed slot keeps the pointer, and an 'if x == {...}' match derefs the pointee for its tag/payload. Arrays GEP through their storage so writes hit the original. Regression test: examples/for-by-ref-capture.sx.
This commit is contained in:
agra
2026-05-31 10:29:16 +03:00
parent 4415274894
commit 185df9afb7
6 changed files with 88 additions and 10 deletions

View File

@@ -38,6 +38,7 @@ const Binding = struct {
ref: Ref,
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
};
const Scope = struct {
@@ -2542,6 +2543,17 @@ pub const Lowering = struct {
};
}
/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns
/// the element (pointee) type so a value-position use can auto-deref it.
fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId {
if (node.data != .identifier) return null;
const scope = self.scope orelse return null;
const binding = scope.lookup(node.data.identifier.name) orelse return null;
if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null;
const info = self.module.types.get(binding.ty);
return if (info == .pointer) info.pointer.pointee else null;
}
fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
// Short-circuit: `a and b` → if a then b else false
if (bop.op == .and_op) {
@@ -2656,10 +2668,15 @@ pub const Lowering = struct {
}
}
var lhs = self.lowerExpr(bop.lhs);
// A `for xs: (*x)` capture is a pointer; in a value position (here, an
// operand) it auto-derefs to the element.
const lhs_ref_pointee = self.refCapturePointee(bop.lhs);
if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p);
// Set target_type from LHS so enum literals on RHS resolve correctly.
// When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use
// the lowered operand's concrete type rather than a guess.
const lhs_ty = blk: {
if (lhs_ref_pointee) |p| break :blk p;
const it = self.inferExprType(bop.lhs);
break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it;
};
@@ -2675,6 +2692,8 @@ pub const Lowering = struct {
}
}
var rhs = self.lowerExpr(bop.rhs);
const rhs_ref_pointee = self.refCapturePointee(bop.rhs);
if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p);
self.target_type = saved_tt;
// Infer result type from LHS operand (covers float, bool, etc.)
var ty = lhs_ty;
@@ -2682,7 +2701,7 @@ pub const Lowering = struct {
// Promote int×float → float (e.g., s64 * f32 → f32)
// Only for scalar int LHS — don't affect vectors or structs.
{
const rhs_inferred = self.inferExprType(bop.rhs);
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
const l_int = isInt(ty);
const r_float = (rhs_inferred == .f32 or rhs_inferred == .f64);
if (l_int and r_float) {
@@ -2698,7 +2717,7 @@ pub const Lowering = struct {
lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty);
}
}
const rhs_ty = self.inferExprType(bop.rhs);
const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
if (!rhs_ty.isBuiltin()) {
const rhs_info = self.module.types.get(rhs_ty);
if (rhs_info == .optional) {
@@ -3365,16 +3384,26 @@ pub const Lowering = struct {
// Body
self.builder.switchToBlock(body_bb);
// Bind element — resolve element type from iterable
// Bind element — resolve element type from iterable. `for xs: (*x)`
// binds a pointer into the collection (no per-element copy); `(x)`
// binds a value copy.
const iterable_ty = self.inferExprType(fe.iterable);
const elem_ty = self.getElementType(iterable_ty);
const elem = self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, elem_ty);
const bind_ty = if (fe.capture_by_ref) self.module.types.ptrTo(elem_ty) else elem_ty;
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);
var body_scope = Scope.init(self.alloc, self.scope);
const old_scope = self.scope;
self.scope = &body_scope;
body_scope.put(fe.capture_name, .{ .ref = elem, .ty = elem_ty, .is_alloca = false });
body_scope.put(fe.capture_name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = fe.capture_by_ref });
// Bind index if requested
if (fe.index_name) |iname| {
@@ -3563,10 +3592,20 @@ pub const Lowering = struct {
}
const is_type_match = isTypeCategoryMatch(me);
const subject = self.lowerExpr(me.subject);
// Detect optional subject type
const subject_ty = self.inferExprType(me.subject);
var subject = self.lowerExpr(me.subject);
var subject_ty = self.inferExprType(me.subject);
// A pointer subject (e.g. a `for xs: (*x)` element capture) — deref to
// the pointed-to union/enum so tag/payload extraction works.
if (!subject_ty.isBuiltin()) {
const sinfo = self.module.types.get(subject_ty);
if (sinfo == .pointer and !sinfo.pointer.pointee.isBuiltin()) {
const pinfo = self.module.types.get(sinfo.pointer.pointee);
if (pinfo == .tagged_union or pinfo == .@"enum") {
subject = self.builder.load(subject, sinfo.pointer.pointee);
subject_ty = sinfo.pointer.pointee;
}
}
}
const is_optional_match = blk: {
if (!subject_ty.isBuiltin()) {
const info = self.module.types.get(subject_ty);