diff --git a/examples/204-pack-xx-to-slice.sx b/examples/204-pack-xx-to-slice.sx new file mode 100644 index 0000000..5eb758c --- /dev/null +++ b/examples/204-pack-xx-to-slice.sx @@ -0,0 +1,32 @@ +// `xx ` materializes a comptime pack into a runtime slice (issue 0053): +// the explicit pack→slice bridge. With a `[]Any` target each element is boxed +// to `Any`; with a `[]P` target each is `xx`-erased to the protocol `P`. This is +// how you forward a pack to a runtime (`[]Any` / `[]P`) helper. + +#import "modules/std.sx"; + +Show :: protocol { show :: () -> string; } +A :: struct {} +B :: struct { s: string; } +impl Show for A { show :: (self: *A) -> string => "A"; } +impl Show for B { show :: (self: *B) -> string => "B"; } + +count_any :: (items: []Any) -> s64 { return items.len; } + +show_all :: (items: []Show) -> s64 { + i := 0; + while i < items.len { print("{}\n", items[i].show()); i = i + 1; } + return items.len; +} + +// `..$args` pack → []Any via `xx`. +fwd_any :: (..$args) -> s64 { return count_any(xx args); } + +// `..xs: Show` pack → []Show via `xx`. +fwd_show :: (..xs: Show) -> s64 { return show_all(xx xs); } + +main :: () -> s32 { + print("any={}\n", fwd_any(1, "hi", 2.5)); // 3 + print("show={}\n", fwd_show(A.{}, B.{ s = "x" }, A.{})); // A B A, 3 + 0; +} diff --git a/issues/0053-comptime-pack-spread-into-any-slice.md b/issues/0053-comptime-pack-spread-into-any-slice.md index 60874a1..86a91b7 100644 --- a/issues/0053-comptime-pack-spread-into-any-slice.md +++ b/issues/0053-comptime-pack-spread-into-any-slice.md @@ -1,3 +1,11 @@ +**FIXED** via the `xx ` bridge (the preferred fix below), not by changing +the `..args` spread. `xx args` with a slice target now materializes the pack +into a runtime `[]Any`/`[]P` — see [examples/204-pack-xx-to-slice.sx](../examples/204-pack-xx-to-slice.sx). +`lowerXX`/the unary-op arm intercepts `xx ` before the pack-as-value +check and calls the new `lowerPackToSlice` ([src/ir/lower.zig](../src/ir/lower.zig)). +The bare `..args` spread into a non-variadic `[]Any` param is still unsupported +(use `xx args`); left as-is. + # Symptom Spreading a comptime pack `..$args` into a `[]Any` parameter — `f(..args)` where @@ -23,28 +31,43 @@ main :: () -> s32 { print("{}\n", forward(1, "hi", 2.5)); return 0; } Expected: `3` (the pack spreads into the `[]Any` slice, like calling `log_count(1, "hi", 2.5)` against a `[]Any` variadic would). -# Workaround / current advice +# Preferred fix — `xx args` (pack → slice materialization) + +Rather than make the splat-y `..args` spread collapse into a single slice arg, +the cleaner spelling is an **`xx` cast**, which already means "erase/convert to +the expected type": + +```sx +forward :: (..$args) -> s64 { return log_count(xx args); } // target: []Any +``` + +`xx args` (target-typed) should materialize the pack into the expected slice: +- target `[]Any` → box each pack element to `Any`, build `[N]Any` → `[]Any`; +- target `[]P` → `xx`-erase each element to the protocol `P`, build `[N]P` + → `[]P` (reuse the slice-of-protocol erasure landed in `packVariadicCallArgs`, + issue 0052). + +This reuses the existing `xx`/protocol machinery, reads naturally, and keeps +`..xs` reserved for true spreads into pack/variadic callees. + +**Currently `xx args` errors** ("pack 'args' has no runtime value") because the +Step 2.7 pack-as-value check fires on the bare `args` operand before `xx` is +considered. The fix: in `xx` (unary_op `.xx`) lowering, intercept a pack operand +*before* the pack-as-value diagnostic and, when the target type is a slice, +materialize as above. + +# Workaround today Declare the forwarder as the **slice** variadic instead of a pack — then it's -already a runtime `[]Any` and forwards directly (no spread needed): +already a runtime `[]Any` and forwards directly: ```sx forward :: (..args: []Any) -> s64 { return log_count(args); } // works -> 3 ``` -This is what `examples/162-pack-bare-args.sx` now demonstrates, and what the -Step 2.7 pack-as-value diagnostic recommends (declare `..xs: []P` for runtime -use rather than spreading a pack). - -# Suspected area - -The call-arg spread lowering (`packSpreadRefs` / `lowerVariadicArgs` / -`packVariadicCallArgs` interaction in [src/ir/lower.zig](../src/ir/lower.zig)): -when the spread source is a comptime pack and the callee parameter is a single -`[]Any` (not itself variadic/pack), the spread must **collect** the pack -elements into one `[]Any` slice arg, not splat them as separate positional args. -Compare the working path where the callee is itself a `[]Any` variadic. +This is what `examples/162-pack-bare-args.sx` demonstrates. # Verification -The reproduction above should print `3` and pass `sx ir` LLVM verification. +After the fix, `log_count(xx args)` (and the original `..args` form, if also +fixed) should print `3` and pass `sx ir` LLVM verification. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1666289..be5a209 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2347,6 +2347,19 @@ pub const Lowering = struct { .binary_op => |bop| self.lowerBinaryOp(&bop), .unary_op => |uop| blk: { + // `xx ` with a slice target materializes the comptime + // pack into a runtime `[]elem` (issue 0053). Must run before the + // operand is lowered (a bare pack name otherwise hits the + // pack-as-value error). + if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) { + const pname = uop.operand.data.identifier.name; + if (self.target_type) |tt| { + if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) { + break :blk self.lowerPackToSlice(pname, tt); + } + } + break :blk self.diagPackAsValue(pname, node.span, .generic); + } // address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of if (uop.op == .address_of and uop.operand.data == .index_expr) { const ie = &uop.operand.data.index_expr; @@ -4916,10 +4929,10 @@ pub const Lowering = struct { const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name}); switch (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 on at runtime, declare `{s}` as a slice variadic `..{s}: []P` (a protocol slice) instead of a pack `..{s}: P`", .{ name, name, 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 }), - .generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or declare `{s}` as a slice variadic `..{s}: []P` for runtime use instead of a pack `..{s}: P`", .{ name, 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 }), } } return self.emitPlaceholder(name); @@ -4931,6 +4944,48 @@ pub const Lowering = struct { return ppc.contains(name); } + /// `xx ` with a slice target: materialize the comptime pack into a + /// runtime `[]elem` by lowering each element node and boxing (`[]Any`) or + /// `xx`-erasing (`[]P`) it into a stack `[N]elem`, then return the slice. + /// This is the explicit pack→slice bridge (issue 0053). + fn lowerPackToSlice(self: *Lowering, pack_name: []const u8, slice_ty: TypeId) Ref { + const arg_nodes = (self.pack_arg_nodes orelse return self.builder.constInt(0, .unresolved)).get(pack_name) orelse + return self.builder.constInt(0, .unresolved); + const elem_ty = self.module.types.get(slice_ty).slice.element; + const is_any = elem_ty == .any; + const elem_is_protocol = blk: { + if (elem_ty.isBuiltin()) break :blk false; + const ei = self.module.types.get(elem_ty); + break :blk ei == .@"struct" and ei.@"struct".is_protocol; + }; + const slice_slot = self.builder.alloca(slice_ty); + const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty); + const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty); + if (arg_nodes.len == 0) { + self.builder.store(ptr_gep, self.builder.constNull(self.module.types.ptrTo(elem_ty))); + self.builder.store(len_gep, self.builder.constInt(0, .s64)); + return self.builder.load(slice_slot, slice_ty); + } + const array_ty = self.module.types.arrayOf(elem_ty, @intCast(arg_nodes.len)); + const array_slot = self.builder.alloca(array_ty); + for (arg_nodes, 0..) |arg, i| { + var val = self.lowerExpr(arg); + var source_ty = self.inferExprType(arg); + if (source_ty == .unresolved) source_ty = self.builder.getRefType(val); + if (is_any) { + if (source_ty != .any) val = self.builder.boxAny(val, source_ty); + } else if (elem_is_protocol) { + if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg, source_ty, elem_ty); + } + const ep = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(@intCast(i), .s64) } }, self.module.types.ptrTo(elem_ty)); + self.builder.store(ep, val); + } + const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(0, .s64) } }, self.module.types.ptrTo(elem_ty)); + self.builder.store(ptr_gep, data_ptr); + self.builder.store(len_gep, self.builder.constInt(@intCast(arg_nodes.len), .s64)); + return self.builder.load(slice_slot, slice_ty); + } + fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { const obj = self.lowerExpr(se.object); const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64); diff --git a/tests/expected/203-pack-as-value.txt b/tests/expected/203-pack-as-value.txt index 6825693..b4d567a 100644 --- a/tests/expected/203-pack-as-value.txt +++ b/tests/expected/203-pack-as-value.txt @@ -15,7 +15,7 @@ error: pack 'xs' has no runtime value — a pack is comptime-only and can't be u 15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call | ^^ -help: materialize a tuple `(..xs)` to store it, or declare `xs` as a slice variadic `..xs: []P` for runtime use instead of a pack `..xs: P` +help: materialize a tuple `(..xs)` to store it, or `xx xs` to convert it to an expected `[]Any`/`[]P` slice | 15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call | ^^ diff --git a/tests/expected/204-pack-xx-to-slice.exit b/tests/expected/204-pack-xx-to-slice.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/204-pack-xx-to-slice.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/204-pack-xx-to-slice.txt b/tests/expected/204-pack-xx-to-slice.txt new file mode 100644 index 0000000..797c1f5 --- /dev/null +++ b/tests/expected/204-pack-xx-to-slice.txt @@ -0,0 +1,5 @@ +any=3 +A +B +A +show=3