issue-0037 fixed: ptr↔int conversion in coerceToType / bitcast emit

109/109 regression tests pass; chess Android + iOS-sim still
build clean.

Root cause: sx's `xx <ptr>` cast targeting an integer type
(common pattern: `xx u64 = xx @some_global`) lowered to a no-op
because `coerceToType` had branches for int↔float and same-kind
widen/narrow, but nothing for pointer↔integer. The cast left the
value as a pointer Ref, and `emitInst`'s `.ret` arm tried to
coerce a `ptr` value to an `i64` slot — coerceArg had no
ptr↔int branch either, fell through to undef.

Why it worked in main but failed in helpers: an
`alloca u64`+`store ptr @g, alloca`+`load i64, alloca` sequence
preserves the address bits as raw memory, so the
"store-then-load through an alloca" workaround happened to do
the right thing without a real cast. A `ret i64 <ptr>` has no
such intermediate slot and triggers an LLVM type mismatch.

Fix layered into two existing IR opcodes:

  lower.zig (coerceToType):
    new branch — when src and dst types are ptr↔int, emit a
    `bitcast` IR opcode with the right from/to. Mirrors how
    int↔float emits `.int_to_float` / `.float_to_int`.

  emit_llvm.zig (.bitcast arm):
    dispatch ptr→int to `LLVMBuildPtrToInt` (+ trunc/zext if the
    target int width != 64), int→ptr to `LLVMBuildIntToPtr`. The
    "real bitcast" path stays for same-kind type punning.
    Modern LLVM's BuildBitCast rejects ptr↔int directly, hence
    the dispatch.

The fix also closes a quiet behavior gap that affected non-`#foreign`
globals (any `xx @<global>` from a helper fn). Surfaced while
investigating issue-0037; verified independently with a
non-`#foreign` sx-side global of type `s64`.

File mechanics: issue-0037 promoted to a focused feature example
per CLAUDE.md's resolution flow:
  examples/issue-0037.sx        -> examples/102-foreign-global-from-helper.sx
  tests/expected/issue-0037.{txt,exit} -> tests/expected/102-foreign-global-from-helper.{txt,exit}

ffi-objc-call-03 + ffi-objc-call-06 IR snapshots updated to
reflect the ptr→int store-via-ptrtoint shape that's now correct
at the LLVM-IR level (same bits in memory, but properly typed).
This commit is contained in:
agra
2026-05-19 19:18:31 +03:00
parent 5fad92785e
commit 0bb7b8cc27
9 changed files with 96 additions and 69 deletions

View File

@@ -0,0 +1,28 @@
// `xx @<foreign_global>` round-trips through a non-main helper
// function: the helper's `xx @__stdinp` cast lowers to a `bitcast`
// IR opcode that emit_llvm.zig dispatches to `LLVMBuildPtrToInt`
// (BitCast doesn't accept ptr↔int on modern LLVM with opaque
// pointers), and a `u64`-returning function correctly returns
// the address.
//
// Was issue-0037 — the helper used to emit `ret i64 undef` because
// `coerceToType` had no pointer↔integer case, so the explicit
// `xx ptr` cast produced an unchanged pointer value handed to a
// `ret i64` slot.
#import "modules/std.sx";
__stdinp : *void #foreign;
stdinp_addr_via_helper :: () -> u64 {
xx @__stdinp;
}
main :: () -> s32 {
direct : u64 = xx @__stdinp;
via_helper := stdinp_addr_via_helper();
print("direct non-null = {}\n", direct != 0);
print("helper non-null = {}\n", via_helper != 0);
print("eq = {}\n", direct == via_helper);
0;
}

View File

@@ -1,47 +0,0 @@
// `@__stdinp` (address-of a `#foreign` global) lowers to `undef` /
// 0 when accessed from inside a non-main function body. From `main`
// directly it works:
//
// single file:
// __stdinp : *void #foreign;
// stdinp_addr :: () -> u64 { xx @__stdinp; }
// main :: () -> s32 {
// a : u64 = xx @__stdinp; // a = <real symbol address>
// b := stdinp_addr(); // b = 0
// ...
// }
//
// The emitted IR for the helper is just `ret i64 undef`, suggesting
// the `address_of(identifier=__stdinp)` branch in `lower.zig`
// (~line 1719) doesn't see `__stdinp` in `global_names` at the time
// the helper's body is being lowered — even though the same lookup
// succeeds inside main.
//
// Likely cause: lazy/lazy-deferred body lowering ordering vs. the
// pass that registers extern global decls into `global_names`. Or
// global_names is scoped per-function (?). Worth verifying which
// before fixing.
//
// Expected when fixed: the helper returns the same address as the
// direct read, prints `eq = true`. Today: prints `eq = false`.
//
// Filed during PLAN-FFI step 0.10 (cross-file `#foreign` global —
// the cross-file dimension surfaced the bug, but it reproduces in
// a single file too).
#import "modules/std.sx";
__stdinp : *void #foreign;
stdinp_addr_via_helper :: () -> u64 {
xx @__stdinp;
}
main :: () -> s32 {
direct : u64 = xx @__stdinp;
via_helper := stdinp_addr_via_helper();
print("direct non-null = {}\n", direct != 0);
print("helper non-null = {}\n", via_helper != 0);
print("eq = {}\n", direct == via_helper);
0;
}