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).
This commit is contained in:
agra
2026-06-01 08:51:44 +03:00
parent ea40724b61
commit a694d91bca
6 changed files with 157 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
// Regression for issue 0057: `xx <int>` as a variadic `format` arg inside an
// imported-module function used to segfault (mis-typed as the enclosing fn's
// return type, monomorphizing __pack_string + ABI-coercing the int as a
// 16-byte string fat pointer). Now it auto-boxes to Any like the bare form.
// The companion module is examples/242-xx-any-pack-cross-module/fmt.sx.
#import "modules/std.sx";
#import "242-xx-any-pack-cross-module/fmt.sx";
main :: () -> s32 {
print("{}", build(3));
return 0;
}

View File

@@ -0,0 +1,20 @@
// Companion module for issue-0057 regression. The bug: an `xx <int>` argument
// to a variadic `format` (a comptime `..$args` pack), inside a function that
// lives in an IMPORTED module, was mis-typed as the enclosing fn's
// `target_type` (here `string`) instead of auto-boxing to `Any` — so it
// monomorphized `__pack_string` and ABI-coerced the 4-byte int as a 16-byte
// string fat pointer, corrupting memory at runtime. Fixed by clearing
// `target_type` while lowering pack args.
#import "modules/std.sx";
build :: (n: s32) -> string {
result := "items:\n";
i : s32 = 0;
while i < n {
line := format(" item {}\n", xx i); // <-- the xx-to-Any pack arg
result = concat(result, line);
i = i + 1;
}
result;
}

View File

@@ -0,0 +1,108 @@
# 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`:
```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`):
```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``i` 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 `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 `memmove`s 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 .xx``coerceToType(..., .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).

View File

@@ -9591,6 +9591,16 @@ pub const Lowering = struct {
// lambda arg types its params from the projected closure signature.
// (A comptime `..$args` pack keeps `inferExprType` — its args may be
// type-position.)
// A pack arg is independently typed — it takes its natural type and
// (for a comptime `..$args` pack) auto-boxes to `Any` at the call
// boundary. It is NEVER coerced to a leftover outer `target_type`, so
// clear it: otherwise an `xx <expr>` pack arg (whose result type IS
// `target_type`) would cast to the stale target — e.g. `format("…", xx i)`
// inside a `-> string` fn mis-typed the arg as `string`, monomorphizing
// `__pack_string` and ABI-coercing the 4-byte int as a 16-byte fat
// pointer → memory corruption (issue 0057).
const saved_pack_tt = self.target_type;
self.target_type = null;
var pack_refs = std.ArrayList(Ref).empty;
defer pack_refs.deinit(self.alloc);
for (call_node.args[pack_start..]) |a| {
@@ -9603,6 +9613,7 @@ pub const Lowering = struct {
pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void);
}
}
self.target_type = saved_pack_tt;
// Install the pack's element types + constraint so prefix-arg param
// types like `Closure(..sources.T)` resolve while lowering the prefix.

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
items:
item 0
item 1
item 2