Commit Graph

3 Commits

Author SHA1 Message Date
agra
0bb7b8cc27 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).
2026-05-19 19:18:31 +03:00
agra
e388687f1a ffi 1.8b: sret transform for #objc_call(>16 B non-HFA struct)
104/104 regression tests pass. The Triple round-trip
(triple_imp writes {11, 22, 33} on the IMP side → #objc_call(Triple)
reads them back) is the test of record.

emit_llvm.zig changes:

1. `objc_msg_send` arm — when `needsByval(ret_ty)` (same predicate
   the plain-foreign-call path uses), apply the sret transform:
     - ret type collapses to void
     - prepend a `ptr` param at index 0 (call site provides an
       alloca slot)
     - mirror `sret(<RetType>)` on the call site so the AArch64 x8
       / SysV-AMD64 hidden-ptr ABI lowers correctly
     - load the result from the slot post-call
   The IR shape now matches clang exactly:
     call void @objc_msgSend(ptr sret({...}) %slot, ptr %recv, ptr %sel)

2. `.ret` arm — the body-side counterpart for sx fns whose declared
   return type is sret-shaped (sx-defined IMPs registered via
   `class_addMethod` produce these). When the current function's
   `needsByval(func.ret)` predicate holds, store the IR ret value
   through the prepended sret slot (param 0) and emit `ret void`.
   Previously the unconditional coerceArg path turned the struct
   value into `undef` and emitted `ret void undef` — illegal LLVM.

Test mechanics: registers `SxTripleProbe : NSObject` at runtime via
`objc_allocateClassPair` + `class_addMethod`, IMP returns
Triple{11, 22, 33}. `#objc_call(Triple)(instance, "tripleValue")`
gets them back, round-trip pinned in the .txt snapshot and the
IR-shape snapshot.
2026-05-19 18:50:26 +03:00
agra
865890aed9 ffi 1.8a: xfail — #objc_call(>16 B non-HFA) skips the sret transform
103/103 regression tests pass (+ffi-objc-call-06-sret-return).

The runtime output is misleadingly clean — `[nil tripleValue]`
zeros all three fields because libobjc's nil-stub clears the
return registers. But the IR snapshot reveals the actual ABI
mismatch:

  %objc.msg = call { i64, i64, i64 } @objc_msgSend(ptr null, ptr %load)

A live receiver returning a non-zero `Triple` would surface
garbage in the third field — the AArch64 backend lowers
{ i64, i64, i64 } returns to x0/x1 pair + a third register that
the runtime's sret-shaped stub doesn't populate.

Next commit (1.8b): emit_llvm's `objc_msg_send` arm gains the
same sret transform we did for plain `#foreign` calls in Phase
0.3 — ret type collapses to void, prepend a ptr sret param,
alloca the result slot at the call site, mirror the
`sret(<T>)` attribute on the call, load result from the slot
post-call. IR snapshot will flip to:

  %slot = alloca <Triple>
  call void @objc_msgSend(ptr sret(<Triple>) %slot, ptr null, ptr %load)
  %objc.msg = load <Triple>, ptr %slot
2026-05-19 18:45:57 +03:00