An `xx <int>` argument to a variadic `format`/`print` (a comptime `..$args`
pack) segfaulted when the call was inside an imported-module function. Root
cause: lowerPackCall lowered each pack arg with whatever self.target_type was
set to from the surrounding context. A bare arg is unaffected (inferExprType
ignores target_type), but `xx <expr>`'s result type IS target_type — so
`format("…", xx i)` inside a `-> string` fn cast the int to `string`,
monomorphized __pack_string, and ABI-coerced the 4-byte int as a 16-byte string
fat pointer → corruption. Inline it worked only because target_type was null
there; the imported-module path left it set.
Fix: save/clear/restore self.target_type around the pack-arg lowering loop. A
pack arg is independently typed — comptime `..$args` auto-boxes to Any; a value
pack takes its declared element/protocol type — never a leftover outer target.
examples/242-xx-any-pack-cross-module.sx (+ companion fmt.sx) is the regression.
issues/0057 marked resolved. Unblocks ERR E3.3 (the trace.sx formatter formats
frames with `xx frame`).
Gates: zig build, zig build test, bash tests/run_examples.sh (279 passed; lone
failure is the user's uncommitted 213-canonical-map pack WIP).
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 (s32, u64). - Expected: prints the formatted string, same as the auto-boxed / inline forms.
Reproduction
library/modules/zz_repro.sx:
#import "std.sx";
build :: (n: s32) -> string {
result := "x:\n";
i : s32 = 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 :: () -> s32 { 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 ans32or 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).