0143: a ..$args pack forwarded as a []Type argument across a call is backed by a [N x Any] (16B) array but viewed as []type_value (8B) -> half-stride reads. A lowering bug the legacy Value model masks; the byte-accurate VM exposes it. Blocks examples/0114 on the VM. Filed per CLAUDE.md (not worked around; the type_name .unresolved guard only makes the VM decline rather than emit garbage). Checkpoint also records the sequencing insight: comptime `out` (print) can only land once the fallback is removed (a print-then-bail double-prints under the legacy re-run), so side-effecting ops + fallback-removal are the FINAL step; pure ops + migrations land first.
4.2 KiB
0143 — A variadic pack passed as []Type across a call is mis-strided (Any-sized backing, Type-sized view)
Symptom — When a variadic ..$args pack is forwarded as a []Type argument
to another function, reading args[i] yields the wrong element: the backing
array is laid out as [N x Any] (16-byte {i64,i64} slots) but the slice's
element type is Type/type_value (8 bytes), so an 8-byte-strided index read
lands on elem0, then the padding of elem0 (reads as .unresolved), then
elem1, … — i.e. half-stride reads.
Observed (on the flat-memory comptime VM, which is byte-accurate) vs expected
(legacy interp, whose tagged-Value model is stride-agnostic and reads the right
logical element):
VM: [i64 <unresolved> string ]
legacy: [i64 string bool ]
The legacy interpreter masks the bug (it indexes a logical sequence of Values,
not bytes). The VM exposes it. This is what makes examples/0114-types-build-block-convert
read arg1: <unresolved> instead of arg1: string once the VM handles type_name
(it currently bails on the .unresolved read — see the guard added in comptime_vm.zig
type_name, commit 379ed05 — so under the fallback it falls back; under
SX_COMPTIME_FLAT_STRICT it is a hard error).
Reproduction
#import "modules/std.sx";
inner :: (args: []Type) -> string {
s := ""; i : i64 = 0;
while i < args.len { s = concat(s, type_name(args[i])); s = concat(s, " "); i = i + 1; }
return s;
}
outer :: (..$args) -> string { return inner($args); } // <-- pack passed as []Type across a call
R :: #run outer(42, "hi", true);
main :: () { print("[{}]\n", R); }
Expected: [i64 string bool ]. Actual (VM): [i64 <unresolved> string ].
Note the direct use (no cross-call) is fine — walk :: (..$args) { list := $args; … type_name(list[i]) … }
reads correctly. Only forwarding the pack as a []Type argument mis-strides.
Root cause (hypothesis)
The pack $args is materialised as a [N x Any] array (each arg boxed as a
16-byte Any — confirmed in the LLVM IR: array_to_string__AR_3_Any /
[3 x { i64, i64 }]), but when forwarded to a []Type parameter the slice is
typed with element type_value (8 bytes). The slice {ptr,len} points at the
[N x Any] data, so index_get with an 8-byte element stride reads garbage.
The fix belongs in lowering, not the VM: a pack consumed as []Type must
either (a) materialise a real [N x Type] (8-byte) array by extracting each
arg's type tag from its Any box, or (b) keep the slice element type as Any and
have type_name/reflection read the Any's tag (the VM's type_name already
handles an .any arg). Option (b) is likely smaller. Whichever: the slice's
element type and its backing array's element size MUST agree.
Suspected area: the pack-expansion / pack-as-slice lowering (search src/ir/lower/
for the ..$args → slice materialisation and the []Type coercion of a value
pack; pack.zig).
Investigation prompt (paste into a fresh session)
A variadic
..$argspack forwarded as a[]Typeargument across a call is mis-strided: the backing array is[N x Any](16B slots) but the slice element type istype_value(8B), soargs[i]reads at the wrong offset. The legacy interp masks it (Value-indexed); the byte-accurate comptime VM exposes it (examples/0114, and the minimal repro inissues/0143-…md). Fix it in lowering: make the pack→[]Typematerialisation agree on element size — either build a real[N x Type]array (extract each Any's type tag) or type the slice[]Anyand let reflection read the Any tag (type_namealready handles.any). Verify: run the repro on the VM (SX_COMPTIME_FLAT=1 sx run) and expect[i64 string bool ]; thenexamples/0114must producearg1: string(not<unresolved>) and run HANDLED underSX_COMPTIME_FLAT_STRICT=1matching legacy. Drop thetid == .unresolvedguard incomptime_vm.zigtype_nameonce the root cause is fixed (it was a stopgap so the VM declines rather than emits garbage).
Status
OPEN. Blocks examples/0114 from running HANDLED on the comptime VM (Phase 4
legacy-interp retirement). Not a blocker for the other comptime-op ports.