lang 2.3: pack spread into call args (f(..xs) / f(..xs.value))

A pack spread in call-arg position now expands to N positional args:
`add2(..xs.get)` ≈ `add2(xs[0].get(), xs[1].get())` — the canonical's
`mapper(..sources.value)` shape. The call-arg loop detects a spread whose
operand is a pack (`..xs`) or a pack projection (`..xs.method`) and splices the
per-element Refs in; a runtime-slice spread (`..arr`) is still left to the
slice-variadic path.

Factored the per-element synthesis out of lowerPackValueProjection into
`lowerPackElems` (used by both projection-to-tuple and spread-to-args), plus a
`packSpreadRefs` helper. examples/197-pack-spread-call.sx (2- and 3-arg, mixed
element types).
This commit is contained in:
agra
2026-05-29 19:53:04 +03:00
parent c03db7938c
commit d7ecf02d7a
4 changed files with 89 additions and 23 deletions

View File

@@ -0,0 +1,26 @@
// Feature 1 — pack spread into a call's positional arguments. `f(..xs.get)`
// projects `get` over the pack and spreads the resulting tuple into f's params:
// add2(..xs.get) ≈ add2(xs[0].get(), xs[1].get())
// The canonical's `mapper(..sources.value)` is this shape.
#import "modules/std.sx";
Box :: protocol(T: Type) {
get :: () -> s64;
}
IntCell :: struct { v: s64; }
Dbl :: struct { n: s64; }
impl Box(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; }
impl Box(s64) for Dbl { get :: (self: *Dbl) -> s64 => self.n * 2; }
add2 :: (a: s64, b: s64) -> s64 { return a + b; }
add3 :: (a: s64, b: s64, c: s64) -> s64 { return a + b + c; }
via2 :: (..xs: Box) -> s64 { return add2(..xs.get); }
via3 :: (..xs: Box) -> s64 { return add3(..xs.get); }
main :: () -> s32 {
print("two={}\n", via2(IntCell.{ v = 10 }, Dbl.{ n = 5 })); // 10 + 10 = 20
print("three={}\n", via3(Dbl.{ n = 1 }, IntCell.{ v = 2 }, Dbl.{ n = 3 })); // 2 + 2 + 6 = 10
0;
}

View File

@@ -4091,41 +4091,71 @@ pub const Lowering = struct {
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
}
/// Lower each pack element to a Ref: `pack_name[i]` when `method` is null,
/// or `pack_name[i].method()` when given. Synthesizes the index/field/call
/// AST per element and lowers it (substitution turns `xs[i]` into the
/// concrete arg; UFCS dispatches the method). Caller owns the returned slice.
fn lowerPackElems(self: *Lowering, pack_name: []const u8, method: ?[]const u8, span: ast.Span) []Ref {
const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0;
var refs = std.ArrayList(Ref).empty;
var i: u32 = 0;
while (i < n) : (i += 1) {
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 = @intCast(i) } } };
const index_node = self.alloc.create(Node) catch break;
index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } };
var elem_node = index_node;
if (method) |m| {
const fa_node = self.alloc.create(Node) catch break;
fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = m } } };
const call_node = self.alloc.create(Node) catch break;
call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } };
elem_node = call_node;
}
refs.append(self.alloc, self.lowerExpr(elem_node)) catch break;
}
return refs.toOwnedSlice(self.alloc) catch &.{};
}
/// Value-position pack projection `xs.<method>`: call the (zero-arg)
/// protocol method on each element and collect the results into a tuple
/// `(xs[0].<method>(), …, xs[N-1].<method>())`. N=0 yields the empty tuple.
/// Synthesizes `xs[i].<method>()` per element and lowers it (substitution
/// turns `xs[i]` into the concrete arg; UFCS dispatches the method).
fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref {
const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0;
var refs = std.ArrayList(Ref).empty;
defer refs.deinit(self.alloc);
const refs = self.lowerPackElems(pack_name, method, span);
defer self.alloc.free(refs);
var tys = std.ArrayList(TypeId).empty;
defer tys.deinit(self.alloc);
var i: u32 = 0;
while (i < n) : (i += 1) {
const id_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void);
id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } };
const idx_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void);
idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = @intCast(i) } } };
const index_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void);
index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } };
const fa_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void);
fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = method } } };
const call_node = self.alloc.create(Node) catch return self.builder.constInt(0, .void);
call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } };
const r = self.lowerExpr(call_node);
refs.append(self.alloc, r) catch return self.builder.constInt(0, .void);
tys.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void);
}
for (refs) |r| tys.append(self.alloc, self.builder.getRefType(r)) catch {};
const tuple_ty = self.module.types.intern(.{ .tuple = .{
.fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void),
.names = null,
} });
const owned = self.alloc.dupe(Ref, refs.items) catch return self.builder.constInt(0, .void);
const owned = self.alloc.dupe(Ref, refs) catch return self.builder.constInt(0, .void);
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty);
}
/// If `operand` is a pack spread — `..xs` (bare pack) or `..xs.method`
/// (per-element projection) — return the per-element Refs to splice into a
/// call's positional args. Null when it's not a pack spread (e.g. a runtime
/// slice `..arr`, handled by the slice-variadic path). Caller owns the slice.
fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref {
const ppc = self.pack_param_count orelse return null;
switch (operand.data) {
.identifier => |id| {
if (ppc.contains(id.name)) return self.lowerPackElems(id.name, null, span);
},
.field_access => |fa| {
if (fa.object.data == .identifier and ppc.contains(fa.object.data.identifier.name)) {
return self.lowerPackElems(fa.object.data.identifier.name, fa.field, span);
}
},
else => {},
}
return null;
}
/// Lower a struct-level constant value (e.g., Phys.GRAVITY).
fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
const val_node = info.value;
@@ -5974,8 +6004,15 @@ pub const Lowering = struct {
}
}
for (c.args, 0..) |arg, ai| {
// Skip spread expressions — they'll be handled by packVariadicCallArgs from AST
if (arg.data == .spread_expr) {
// Pack spread `..xs` / `..xs.method` → expand to N positional
// args here. A runtime-slice spread (`..arr`) is left as a
// placeholder for the slice-variadic path (packVariadicCallArgs).
if (self.packSpreadRefs(arg.data.spread_expr.operand, arg.span)) |elems| {
defer self.alloc.free(elems);
for (elems) |e| args.append(self.alloc, e) catch unreachable;
continue;
}
args.append(self.alloc, Ref.none) catch unreachable;
continue;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
two=20
three=10