lang: slice-of-protocol variadic ..xs: []P erases each arg to the protocol
packVariadicCallArgs stored the raw concrete arg into a [N x P] array when the
element type was a protocol, so an 8-byte struct landed in a 16-byte {ctx,
vtable} slot -> garbage vtable -> Bus error on dispatch. Now, when the slice
element type is a protocol, each arg is xx-erased to the protocol value via
buildProtocolErasure (same impl-driven machinery as the xx cast). This makes
..xs: []P the runtime, protocol-erased counterpart to the comptime
heterogeneous pack ..xs: P (which stays comptime-only): xs[runtime_i].method()
now works in an ordinary loop.
specs.md: full variadic/pack form-comparison table (concrete-vs-erased,
comptime-vs-runtime). Regression: examples/202. Issue 0052 (FIXED). 237 green.
This commit is contained in:
31
examples/202-slice-of-protocol-variadic.sx
Normal file
31
examples/202-slice-of-protocol-variadic.sx
Normal file
@@ -0,0 +1,31 @@
|
||||
// Slice-of-protocol variadic `..xs: []P` — the RUNTIME counterpart to the
|
||||
// comptime pack `..xs: P`. Each trailing arg is `xx`-erased to a `P` protocol
|
||||
// value {ctx, vtable} and packed into a runtime `[]P`, so the elements can be
|
||||
// indexed by a RUNTIME index and dispatched through the protocol interface
|
||||
// (unlike a pack, which is comptime-only — see examples/163).
|
||||
//
|
||||
// This is the type-safe way to iterate a heterogeneous arg list at runtime:
|
||||
// concrete per-position types are erased to the constraint protocol.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Show :: protocol { show :: () -> string; }
|
||||
A :: struct { x: s64; }
|
||||
B :: struct { s: string; }
|
||||
impl Show for A { show :: (self: *A) -> string => "A"; }
|
||||
impl Show for B { show :: (self: *B) -> string => "B"; }
|
||||
|
||||
// Runtime loop over a []Show: runtime index + protocol-method dispatch.
|
||||
each :: (..xs: []Show) -> void {
|
||||
i := 0;
|
||||
while i < xs.len {
|
||||
print("[{}]={}\n", i, xs[i].show());
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
each(A.{ x = 1 }, B.{ s = "hi" }, A.{ x = 3 }); // heterogeneous, erased to Show
|
||||
each(); // empty is fine (len 0)
|
||||
0;
|
||||
}
|
||||
68
issues/0052-slice-of-protocol-variadic-not-erased.md
Normal file
68
issues/0052-slice-of-protocol-variadic-not-erased.md
Normal file
@@ -0,0 +1,68 @@
|
||||
**FIXED.** `packVariadicCallArgs` ([src/ir/lower.zig](../src/ir/lower.zig))
|
||||
now detects a protocol element type and `xx`-erases each arg into the
|
||||
`[N]P` array via `buildProtocolErasure`, instead of storing the raw concrete
|
||||
value. Regression: [examples/202-slice-of-protocol-variadic.sx](../examples/202-slice-of-protocol-variadic.sx).
|
||||
|
||||
# Symptom
|
||||
|
||||
A slice-of-protocol variadic `..xs: []P` (P a protocol) compiles but **crashes
|
||||
at runtime** (Bus error) the moment an element is used:
|
||||
|
||||
```
|
||||
Bus error at address 0x3fff
|
||||
```
|
||||
|
||||
# Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
Show :: protocol { show :: () -> string; }
|
||||
A :: struct { x: s64; }
|
||||
impl Show for A { show :: (self: *A) -> string => "A"; }
|
||||
|
||||
each :: (..xs: []Show) -> void {
|
||||
i := 0;
|
||||
while i < xs.len { print("{}\n", xs[i].show()); i = i + 1; }
|
||||
}
|
||||
main :: () -> s32 { each(A.{ x = 1 }, A.{ x = 2 }); 0; }
|
||||
```
|
||||
|
||||
# Root cause
|
||||
|
||||
`packVariadicCallArgs` packs trailing args into a `[N x elem_ty]` stack array.
|
||||
It special-cased `is_any = (elem_ty == .any)` (box each arg to `Any`), but for
|
||||
any other non-builtin `elem_ty` it stored the **raw lowered arg** into the
|
||||
array slot. When `elem_ty` is a protocol struct (16 bytes `{ctx, vtable}`), an
|
||||
8-byte concrete `A` value was written into a protocol-sized slot — a
|
||||
size/type mismatch producing a garbage `{ctx, vtable}`. Indexing it and
|
||||
calling `.show()` then jumped through a bad vtable → Bus error.
|
||||
|
||||
# Fix
|
||||
|
||||
Mirror the `xx` cast: when the slice element type is a protocol, erase each arg
|
||||
to the protocol value before storing.
|
||||
|
||||
```zig
|
||||
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;
|
||||
};
|
||||
// ... per arg, in the non-`is_any` path:
|
||||
} else if (elem_is_protocol) {
|
||||
var source_ty = self.inferExprType(arg_node);
|
||||
if (source_ty == .unresolved) source_ty = self.builder.getRefType(val);
|
||||
if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty);
|
||||
}
|
||||
```
|
||||
|
||||
This makes `..xs: []P` the runtime, protocol-erased counterpart to the
|
||||
comptime heterogeneous pack `..xs: P` (which stays comptime-only per Decision
|
||||
1). See specs.md §"Variadic Heterogeneous Type Packs" for the full
|
||||
form-comparison table.
|
||||
|
||||
# Verification
|
||||
|
||||
`examples/202-slice-of-protocol-variadic.sx` prints `[0]=A [1]=B [2]=A` and the
|
||||
empty call is a no-op. `zig build test` + `bash tests/run_examples.sh` (237)
|
||||
green.
|
||||
38
specs.md
38
specs.md
@@ -963,6 +963,11 @@ path_join :: (..parts: []string) -> string { ... }
|
||||
trailing args into a stack-allocated `[N x T]` and passes a slice over it.
|
||||
- For `[]Any`, each trailing arg is boxed into `Any` (type tag + payload) before
|
||||
packing; `args[i]` reads back the boxed value.
|
||||
- For `[]Protocol` (the element type is a protocol, e.g. `..xs: []Show`), each
|
||||
trailing arg is `xx`-erased to a protocol value `{ctx, vtable}` (impl-driven,
|
||||
like `xx`) and packed into a runtime `[N]Protocol`. `xs[runtime_i].method()`
|
||||
then dispatches through the protocol — this is the **runtime** counterpart to
|
||||
the comptime heterogeneous pack `..xs: Protocol`.
|
||||
- A `..` spread at the call site unpacks an existing slice/array into the variadic
|
||||
tail: `sum(..arr)`.
|
||||
- The heterogeneous comptime-pack form `..$args: []Type` binds per-position
|
||||
@@ -972,18 +977,31 @@ path_join :: (..parts: []string) -> string { ... }
|
||||
|
||||
A **pack** is a comptime sequence of per-position-typed arguments. Unlike a
|
||||
slice variadic (`..xs: []T`, one uniform element type, a runtime slice), a pack
|
||||
binds a *distinct* type to each position and exists only at compile time. Three
|
||||
declaration forms exist:
|
||||
binds a *distinct* type to each position and exists only at compile time.
|
||||
|
||||
| Form | Element typing | Body view |
|
||||
|---|---|---|
|
||||
| `..xs: []T` | uniform `T` | runtime `[]T` slice |
|
||||
| `..$xs: []Type` | per-position comptime *types* | comptime type list |
|
||||
| `..xs: Protocol` | per-position — each arg conforms to `Protocol` with its own type-arg | per-position-typed pack |
|
||||
The full family of variadic/pack forms and how they differ:
|
||||
|
||||
The third form is the heterogeneous pack. `map :: (mapper: ..., ..sources:
|
||||
ValueListenable) -> ...` accepts any number of trailing args, each some
|
||||
`ValueListenable(T)` for a possibly-different `T`.
|
||||
| Form | Element types | Lives at | `xs[i]` index | `xs[i]` yields | `xs.len` |
|
||||
|---|---|---|---|---|---|
|
||||
| `..xs: []T` | one uniform `T` | **runtime** (slice) | runtime or comptime | `T` | runtime |
|
||||
| `..xs: []Any` | mixed, **boxed** to `Any` | **runtime** (slice) | runtime or comptime | `Any` (match/unwrap to use) | runtime |
|
||||
| `..xs: []P` *(P a protocol)* | mixed, **erased** to `P` `{ctx,vtable}` | **runtime** (slice) | runtime or comptime | `P` (call protocol methods) | runtime |
|
||||
| `..xs: P` *(pack)* | per-position **concrete**, each conforms to `P` | **comptime** (no runtime value) | comptime only (literal / `inline for` cursor) | the concrete element, **viewed through `P`** | comptime int |
|
||||
| `..$args` / `..$xs: []Type` | per-position comptime **types** | **comptime** | comptime only | element value/type (reflection) | comptime int |
|
||||
|
||||
Key axis — **concrete vs erased, comptime vs runtime**:
|
||||
- `..xs: P` (pack) keeps each element's *concrete* type but is **comptime-only**:
|
||||
`xs[i]` needs a compile-time index (a literal or an `inline for` cursor); a
|
||||
runtime index is an error (a pack has no runtime representation). Use it when
|
||||
you need per-position types (monomorphization, `xs.T` / `xs.value` projection).
|
||||
- `..xs: []P` (slice of protocol) **erases** each element to the protocol value
|
||||
but is **runtime**: `xs[runtime_i].method()` works in an ordinary loop. Use it
|
||||
when you need to iterate the args at runtime and only the protocol interface
|
||||
matters. It is the runtime counterpart to the pack.
|
||||
|
||||
The heterogeneous pack (`..xs: P`) is what powers `map :: (mapper: ...,
|
||||
..sources: ValueListenable) -> ...`: it accepts any number of trailing args,
|
||||
each some `ValueListenable(T)` for a possibly-different `T`.
|
||||
|
||||
A pack is **not a runtime value** — it lowers to N typed positional parameters
|
||||
(zero overhead). The body refers to elements only through the comptime forms
|
||||
|
||||
@@ -8341,6 +8341,14 @@ pub const Lowering = struct {
|
||||
|
||||
// Determine if we need to box as Any (for ..Any params) or use raw type
|
||||
const is_any = (elem_ty == .any);
|
||||
// `..xs: []P` (slice of a protocol): each concrete arg must be erased to
|
||||
// a protocol value {ctx, vtable}, not stored raw (which would be a
|
||||
// size/type mismatch — a heap of garbage vtables → crash on dispatch).
|
||||
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;
|
||||
};
|
||||
|
||||
// Allocate stack array [N x ElemType]
|
||||
const array_elem = if (is_any) TypeId.any else elem_ty;
|
||||
@@ -8388,6 +8396,16 @@ pub const Lowering = struct {
|
||||
if (source_ty != .any) {
|
||||
val = self.builder.boxAny(val, source_ty);
|
||||
}
|
||||
} else if (elem_is_protocol) {
|
||||
// Erase each concrete arg to the protocol value via the same
|
||||
// impl-driven `xx` machinery, so the runtime `[]P` holds real
|
||||
// {ctx, vtable} values and `xs[i].method()` dispatches.
|
||||
const arg_node = c.args[fixed_count + i];
|
||||
var source_ty = self.inferExprType(arg_node);
|
||||
if (source_ty == .unresolved) source_ty = self.builder.getRefType(val);
|
||||
if (source_ty != elem_ty) {
|
||||
val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty);
|
||||
}
|
||||
}
|
||||
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
||||
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem));
|
||||
|
||||
1
tests/expected/202-slice-of-protocol-variadic.exit
Normal file
1
tests/expected/202-slice-of-protocol-variadic.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
3
tests/expected/202-slice-of-protocol-variadic.txt
Normal file
3
tests/expected/202-slice-of-protocol-variadic.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
[0]=A
|
||||
[1]=B
|
||||
[2]=A
|
||||
Reference in New Issue
Block a user