Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
5.2 KiB
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:
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
#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:
#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)— emitsalloca [N x Any], oneconst_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.lenreads 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
buildPackSliceValuereturns a temporary that's not what the call-site argument-marshal step picks up, the callee sees uninit.
What to check first:
- In the pack-fn (
probe(..$args) -> string { walk($args); }), dump the IR oflower_pack_fn_callforwalk— confirmwalk's arg-0 is a slice value whoselenfield has been populated fromarg_tys.len(or the runtime-builtlen). - If the pack-slice goes through
lowerExpr'scomptime_pack_refarm, verify the resulting Ref points at the slice aggregate produced bybuildPackSliceValue— not at the underlyingalloca [N x Any](which would be the data pointer, not the slice). - Compare with the
[]i64round-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.