lang: xx <pack> materializes a comptime pack into a runtime slice (issue 0053)
xx args with a slice target now bridges a comptime pack to a runtime slice: []Any boxes each element to Any; []P xx-erases each to the protocol (reusing the slice-of-protocol erasure from 0052). New lowerPackToSlice; the unary-op arm intercepts xx <pack> before the pack-as-value diagnostic. This is the working forward to a runtime []Any/[]P helper -- log_count(xx args) -> 3 -- so the 2.7 pack-as-value diagnostics now suggest xx <name> for the call case. examples/204-pack-xx-to-slice.sx (both []Any and []P paths); 203 help text updated. issue 0053 FIXED. 239 examples + unit green.
This commit is contained in:
32
examples/204-pack-xx-to-slice.sx
Normal file
32
examples/204-pack-xx-to-slice.sx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// `xx <pack>` 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;
|
||||||
|
}
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
**FIXED** via the `xx <pack>` 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 <pack>` 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
|
# Symptom
|
||||||
|
|
||||||
Spreading a comptime pack `..$args` into a `[]Any` parameter — `f(..args)` where
|
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
|
Expected: `3` (the pack spreads into the `[]Any` slice, like calling
|
||||||
`log_count(1, "hi", 2.5)` against a `[]Any` variadic would).
|
`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
|
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
|
```sx
|
||||||
forward :: (..args: []Any) -> s64 { return log_count(args); } // works -> 3
|
forward :: (..args: []Any) -> s64 { return log_count(args); } // works -> 3
|
||||||
```
|
```
|
||||||
|
|
||||||
This is what `examples/162-pack-bare-args.sx` now demonstrates, and what the
|
This is what `examples/162-pack-bare-args.sx` demonstrates.
|
||||||
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.
|
|
||||||
|
|
||||||
# Verification
|
# 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.
|
||||||
|
|||||||
@@ -2347,6 +2347,19 @@ pub const Lowering = struct {
|
|||||||
.binary_op => |bop| self.lowerBinaryOp(&bop),
|
.binary_op => |bop| self.lowerBinaryOp(&bop),
|
||||||
|
|
||||||
.unary_op => |uop| blk: {
|
.unary_op => |uop| blk: {
|
||||||
|
// `xx <pack>` 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
|
// 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) {
|
if (uop.op == .address_of and uop.operand.data == .index_expr) {
|
||||||
const ie = &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});
|
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) {
|
switch (kind) {
|
||||||
.storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}),
|
.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}),
|
.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 }),
|
.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);
|
return self.emitPlaceholder(name);
|
||||||
@@ -4931,6 +4944,48 @@ pub const Lowering = struct {
|
|||||||
return ppc.contains(name);
|
return ppc.contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `xx <pack>` 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 {
|
fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref {
|
||||||
const obj = self.lowerExpr(se.object);
|
const obj = self.lowerExpr(se.object);
|
||||||
const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64);
|
const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64);
|
||||||
|
|||||||
@@ -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
|
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
|
15 | call :: (..xs: Show) -> void { sink(xs); } // B: pass to a call
|
||||||
| ^^
|
| ^^
|
||||||
|
|||||||
1
tests/expected/204-pack-xx-to-slice.exit
Normal file
1
tests/expected/204-pack-xx-to-slice.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
5
tests/expected/204-pack-xx-to-slice.txt
Normal file
5
tests/expected/204-pack-xx-to-slice.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
any=3
|
||||||
|
A
|
||||||
|
B
|
||||||
|
A
|
||||||
|
show=3
|
||||||
Reference in New Issue
Block a user