# 0048 — bare `$args` slice loses `.len` (reads 0) when passed across a call ## Symptom Bare `$args` evaluated inside a pack-fn body has the correct `.len` inline (e.g. `($args).len == 2` for a two-arg shape). But the moment the same slice is passed as an argument to another function, the callee's read of `.len` returns 0 — regardless of the pack's actual element count. The `data` pointer is presumably similarly broken (haven't probed yet, but every element-access would index off a zero-length view). Observed: ``` inline: len=4 callee: len=0 ``` Expected: callee receives the same `{ptr, len}` slice that the caller materialised; `.len` matches the pack's element count. This blocks the **step-5 generic `Into(Block)` impl** (FFI plan): its body `#insert build_block_convert($args, $R);` calls a `build_block_convert(args: []Type, ret: Type) -> string` builder fn. The builder walks `args` to emit the per-shape trampoline + Block literal source. With this bug, the builder receives an empty slice and emits the empty-pack source for every call shape, silently producing wrong block trampolines. Baseline regression check (not affected): a hand-built `[]i64` slice round-trips correctly across the same kind of call: ```sx walk :: (xs: []i64) -> i64 { return xs.len; } main :: () { arr : [3]i64 = .{10, 20, 30}; sl : []i64 = arr; print("call: {}\n", walk(sl)); // prints 3 — works } ``` So the bug is specific to slices produced by the pack-bare-`$args` materialisation path (`buildPackSliceValue` / `materialisePackSlice` in `src/ir/lower.zig`). ## Reproduction ```sx #import "modules/std.sx"; walk :: (args: []Any) -> string { return concat("len=", int_to_string(args.len)); } probe :: (..$args) -> string { return walk($args); } #run print("inline: len={}\n", ($args).len); // not legal — replace // with a body-local form #run print("callee: {}\n", probe()); // expected: len=0 #run print("callee: {}\n", probe(1, "x")); // expected: len=2 — fails, reads 0 #run print("callee: {}\n", probe(1, "x", true, 3.14)); // expected: len=4 — fails, reads 0 main :: () {} ``` Cleaner repro that contrasts inline vs callee: ```sx #import "modules/std.sx"; walk :: (args: []Any) -> i64 { return args.len; } probe :: (..$args) -> string { inline_list := $args; callee_len := walk($args); return concat(concat("inline=", int_to_string(inline_list.len)), concat(" callee=", int_to_string(callee_len))); } #run print("{}\n", probe(1, "x", true, 3.14)); // observed: inline=4 callee=0 // expected: inline=4 callee=4 main :: () {} ``` Replace `[]Any` with `[]Type` — same wrong result (callee reads 0). Tested on commit `a394372` (master, 2026-05-27 head as of this file). `zig build && bash tests/run_examples.sh` is green; the bug is uncovered, not pre-existing red. ## Investigation prompt A pack-fn's bare `$args` lowers (per `current/CHECKPOINT-FFI.md` entry **M5.A.next.4A.bare.1.B**) to: > `buildPackSliceValue(arg_types)` — emits `alloca [N x Any]`, one > `const_type(arg_tys[i])` per slot, then a `{data_ptr, len}` slice > aggregate. Suspected area: - `src/ir/lower.zig` — `buildPackSliceValue` / `materialisePackSlice`. - Whether the slice aggregate it returns is the same shape sx uses for an ordinary slice — `{ ptr: *T, len: i64 }` in field order used by `.len` reads at consumer sites. - Whether the slice survives the function-call ABI: the callee reads the slice fields from its frame's slot for the argument; if `buildPackSliceValue` returns a temporary that's not what the call-site argument-marshal step picks up, the callee sees uninit. What to check first: 1. In the pack-fn (`probe(..$args) -> string { walk($args); }`), dump the IR of `lower_pack_fn_call` for `walk` — confirm `walk`'s arg-0 is a slice value whose `len` field has been populated from `arg_tys.len` (or the runtime-built `len`). 2. If the pack-slice goes through `lowerExpr`'s `comptime_pack_ref` arm, verify the resulting Ref points at the slice aggregate produced by `buildPackSliceValue` — not at the underlying `alloca [N x Any]` (which would be the data pointer, not the slice). 3. Compare with the `[]i64` round-trip path that works — what's different about how the slice is bound at the call site? Verification step after fix: Run the cleaner repro above; both lines should print `inline=4 callee=4` (and 0/0 for the empty-pack case, 2/2 for two-arg, etc). A regression test goes in `examples/NNN-pack-bare-args-cross-call.sx` once the fix lands. ## Why this matters The whole point of bare `$args` (step 4A) is that builder fns can walk the pack as a runtime slice — step 5 calls `build_block_convert($args, $R)` and the builder emits the trampoline + Block literal source by iterating `args`. With the slice's `.len` silently reading 0, every monomorphisation would emit the empty-pack source, producing wrong trampoline signatures and silently corrupt block dispatch on Apple targets. The bug is invisible inline (the same call shape works inside the pack-fn body), so without the cross-call regression it'd ship quietly and burn the next person trying to write a `#insert build_x($args, ...)` style builder.