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

@@ -1006,7 +1006,26 @@ pub const LLVMEmitter = struct {
.bitcast => |conv| {
const operand = self.resolveRef(conv.operand);
const to_ty = self.toLLVMType(conv.to);
self.mapRef(c.LLVMBuildBitCast(self.builder, operand, to_ty, "bitcast"));
// LLVMBuildBitCast doesn't accept ptr↔int on modern
// LLVM. Dispatch to PtrToInt / IntToPtr when needed —
// lower.zig emits a `bitcast` IR op for both shapes.
const from_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(operand));
const to_kind = c.LLVMGetTypeKind(to_ty);
if (from_kind == c.LLVMPointerTypeKind and to_kind == c.LLVMIntegerTypeKind) {
const i64_val = c.LLVMBuildPtrToInt(self.builder, operand, self.cached_i64, "pti");
const w = c.LLVMGetIntTypeWidth(to_ty);
if (w == 64) {
self.mapRef(i64_val);
} else if (w < 64) {
self.mapRef(c.LLVMBuildTrunc(self.builder, i64_val, to_ty, "pti.tr"));
} else {
self.mapRef(c.LLVMBuildZExt(self.builder, i64_val, to_ty, "pti.ext"));
}
} else if (from_kind == c.LLVMIntegerTypeKind and to_kind == c.LLVMPointerTypeKind) {
self.mapRef(c.LLVMBuildIntToPtr(self.builder, operand, to_ty, "itp"));
} else {
self.mapRef(c.LLVMBuildBitCast(self.builder, operand, to_ty, "bitcast"));
}
},
.int_to_float => |conv| {
const operand = self.resolveRef(conv.operand);

View File

@@ -9290,6 +9290,8 @@ pub const Lowering = struct {
const dst_float = isFloat(dst_ty);
const src_int = self.isIntEx(src_ty);
const dst_int = self.isIntEx(dst_ty);
const src_ptr = !src_ty.isBuiltin() and self.module.types.get(src_ty) == .pointer;
const dst_ptr = !dst_ty.isBuiltin() and self.module.types.get(dst_ty) == .pointer;
// Int → Float
if (src_int and dst_float) {
@@ -9299,6 +9301,13 @@ pub const Lowering = struct {
if (src_float and dst_int) {
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
}
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
// since LLVMBuildBitCast itself doesn't accept ptr↔int.
if ((src_ptr and dst_int) or (src_int and dst_ptr)) {
return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
}
// Same kind — widen/narrow based on bit width
const src_bits = self.typeBitsEx(src_ty);
const dst_bits = self.typeBitsEx(dst_ty);