Files
sx/issues/0057-xx-any-arg-in-imported-module-fn-segfaults.md
agra d8076b9333 lang: rename signed integer types sN -> iN
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.
2026-06-12 09:31:53 +03:00

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, via runJITFromObject (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 ii in the module → prints fine.
  • Inline works: put the same build body (with xx i) directly in the driver's main (no import) → prints fine.
  • Width-independent: xx on an i32 or a u64 both crash.
  • So the trigger is specifically: explicit xx <int> → Any as a variadic format/print arg, 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 xxAny 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: the unary_op .xxcoerceToType(..., .any) / boxAny path, and how variadic args are collected for format/print (build_format / the ..$args pack). Compare the IR for the inline vs imported-module versions (sx ir on each) — the diff at the xx i arg 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 wrong source_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).