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.
4.9 KiB
0057 — xx-to-Any variadic arg inside an imported-module function segfaults
✅ RESOLVED
Root cause: when lowering a variadic-pack call (lowerPackCall in
src/ir/lower.zig), the pack args were lowered with whatever self.target_type
happened to be set to from the surrounding context. For a bare arg this is
harmless (inferExprType ignores target_type), but xx <expr>'s result type
IS target_type — so format("…", xx i) inside a -> string function cast the
int to string, monomorphized __pack_string, and ABI-coerced the 4-byte int
as a 16-byte string fat pointer → memory corruption. (Inline it happened to work
because target_type was null there; the imported-module path left it set.)
Fix: clear self.target_type (save/restore) around the pack-arg lowering loop —
a pack arg is independently typed (comptime ..$args auto-boxes to Any; a
value pack takes its element/protocol type), never coerced to a leftover outer
target. Regression: examples/242-xx-any-pack-cross-module.sx (+ companion
242-xx-any-pack-cross-module/fmt.sx). Gates: zig build, zig build test, 279
examples pass. The original symptom/repro/investigation notes are kept below.
Symptom
A format(...) / print(...) call whose variadic arg is an explicit xx
cast to Any segfaults at runtime (__platform_memmove, an Any-box/string
copy corruption) when the call is inside a function defined in an imported
module. The identical code works (a) inline in the main file, and (b) in an
imported module if the arg is passed without xx (auto-boxed).
- Observed:
Segmentation fault at address 0x1...in__platform_memmove, viarunJITFromObject(target.zig:244). Crashes for any int width (i32, u64). - Expected: prints the formatted string, same as the auto-boxed / inline forms.
Reproduction
library/modules/zz_repro.sx:
#import "std.sx";
build :: (n: i32) -> string {
result := "x:\n";
i : i32 = 0;
while i < n {
line := format(" item {}\n", xx i); // <-- xx cast to Any is the trigger
result = concat(result, line);
i = i + 1;
}
result;
}
Driver (e.g. .sx-tmp/repro.sx):
#import "modules/std.sx";
m :: #import "modules/zz_repro.sx";
main :: () -> i32 { print("[{}]", m.build(2)); return 0; }
Run: ./zig-out/bin/sx run .sx-tmp/repro.sx → segfault.
Isolation (what is / isn't the trigger)
- Auto-box works: change
xx i→iin the module → prints fine. - Inline works: put the same
buildbody (withxx i) directly in the driver'smain(no import) → prints fine. - Width-independent:
xxon ani32or au64both crash. - So the trigger is specifically: explicit
xx <int>→ Any as a variadicformat/printarg, in a function that lives in an imported module.
The crash blocked ERR step E3.3 (library/modules/trace.sx), whose
trace.to_string() formatted each frame with format("... {}\n", i, xx frame)
where frame : u64 — exactly this pattern. (Dropping the xx would dodge it,
but per the project STOP rule that workaround is not taken; the trace formatter
waits on this fix.)
Investigation prompt
The bug is in how an explicit xx-to-Any cast lowers for a variadic argument
when the enclosing function is monomorphized/emitted as part of an imported
module (vs the root module). Auto-boxing (the implicit T → Any coercion at
the variadic call site) produces correct code; the explicit xx path does not,
but only across the module boundary — strongly suggesting the xx→Any
box (box_any / boxAny in src/ir/lower.zig + src/ir/emit_llvm.zig's
.box_any arm) emits a value/width that the variadic-pack marshalling
(any_to_string / the #insert build_format arg materialization in
library/modules/std.sx) then memmoves incorrectly — possibly a stale
source_type, a pointer-vs-value confusion, or the imported-module emit losing
the box's type so the fat-pointer/string copy reads garbage.
Suspected area:
src/ir/lower.zig: theunary_op .xx→coerceToType(..., .any)/boxAnypath, and how variadic args are collected forformat/print(build_format/ the..$argspack). Compare the IR for the inline vs imported-module versions (sx iron each) — the diff at thexx iarg site is the lead.src/ir/emit_llvm.zig:.box_any(≈ line 3258) —coerceToI64/anyTag(source_type). Check whether the imported-module path supplies a wrongsource_type(e.g..unresolved/.void) so the tag/width is off.
Verification: run the reproduction above; expect [x:\n item 0\n item 1\n]
(no segfault). Then re-confirm the auto-box and inline forms still work, and
that xx on a non-Any target (e.g. xx ptr to integer) is unaffected.
Once fixed, ERR E3.3 resumes: restore library/modules/trace.sx (the
trace.to_string() / print_current() formatter) using the xx frame form,
and complete the E3.3 step (example + snapshot + commit).