diff --git a/examples/242-xx-any-pack-cross-module.sx b/examples/242-xx-any-pack-cross-module.sx new file mode 100644 index 0000000..9ce77d9 --- /dev/null +++ b/examples/242-xx-any-pack-cross-module.sx @@ -0,0 +1,13 @@ +// Regression for issue 0057: `xx ` 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; +} diff --git a/examples/242-xx-any-pack-cross-module/fmt.sx b/examples/242-xx-any-pack-cross-module/fmt.sx new file mode 100644 index 0000000..8aac92b --- /dev/null +++ b/examples/242-xx-any-pack-cross-module/fmt.sx @@ -0,0 +1,20 @@ +// Companion module for issue-0057 regression. The bug: an `xx ` 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; +} diff --git a/issues/0057-xx-any-arg-in-imported-module-fn-segfaults.md b/issues/0057-xx-any-arg-in-imported-module-fn-segfaults.md new file mode 100644 index 0000000..77b4e9c --- /dev/null +++ b/issues/0057-xx-any-arg-in-imported-module-fn-segfaults.md @@ -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 `'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 ` → 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). diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f0782ff..5ceb228 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 ` 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. diff --git a/tests/expected/242-xx-any-pack-cross-module.exit b/tests/expected/242-xx-any-pack-cross-module.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/242-xx-any-pack-cross-module.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/242-xx-any-pack-cross-module.txt b/tests/expected/242-xx-any-pack-cross-module.txt new file mode 100644 index 0000000..ecf5398 --- /dev/null +++ b/tests/expected/242-xx-any-pack-cross-module.txt @@ -0,0 +1,4 @@ +items: + item 0 + item 1 + item 2