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.
109 lines
4.9 KiB
Markdown
109 lines
4.9 KiB
Markdown
# 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`:
|
|
|
|
```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`):
|
|
|
|
```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 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 `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 `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).
|