diff --git a/examples/197-pack-spread-call.sx b/examples/197-pack-spread-call.sx new file mode 100644 index 0000000..fe2cc01 --- /dev/null +++ b/examples/197-pack-spread-call.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0b92488..c42fca6 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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.`: call the (zero-arg) /// protocol method on each element and collect the results into a tuple /// `(xs[0].(), …, xs[N-1].())`. N=0 yields the empty tuple. - /// Synthesizes `xs[i].()` 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; } diff --git a/tests/expected/197-pack-spread-call.exit b/tests/expected/197-pack-spread-call.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/197-pack-spread-call.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/197-pack-spread-call.txt b/tests/expected/197-pack-spread-call.txt new file mode 100644 index 0000000..df0a027 --- /dev/null +++ b/tests/expected/197-pack-spread-call.txt @@ -0,0 +1,2 @@ +two=20 +three=10