Files
sx/issues/0057-xx-any-arg-in-imported-module-fn-segfaults.md
agra a694d91bca fix(0057): clear target_type when lowering variadic pack args
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).
2026-06-01 08:51:44 +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 (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 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 s32 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).