# 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 `[]s64`
slice round-trips correctly across the same kind of call:

```sx
walk :: (xs: []s64) -> s64 { return xs.len; }
main :: () {
    arr : [3]s64 = .{10, 20, 30};
    sl : []s64 = 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) -> s64 { 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: s64 }` 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 `[]s64` 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.
