Commit Graph

4 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
d43385112c ffi 1.6: objc_msg_send IR opcode + per-call-site LLVM fn type
102/102 regression tests pass; chess Android + iOS-sim still build
clean. `ffi-objc-call-04-primitive-returns` flips from xfail to
passing with both nil-recv and real-recv flavors of *void / s64
returns exercised.

Key change: a new `objc_msg_send` IR opcode bundles (recv, sel,
extra args) and carries the return type via the `Inst.ty` field.
emit_llvm.zig builds a per-call-site LLVM function type from the
argument Refs' IR types (recv/sel as ptr; extra args through
abiCoerceParamType) and dispatches with LLVMBuildCall2. One
declared `@objc_msgSend` symbol is reused across every return
type — opaque pointers make the function value type-erased, so
each call site picks its own ABI.

  before:  one (recv, sel) -> ptr LLVM declaration, hard-coded
           per call site; only void return wired in 1.3.
  after:   same declaration, each call site provides a fresh
           LLVMBuildCall2 fn-type → s64 / *void / bool / f64
           returns all dispatch correctly without separate FuncIds.

Selector init mechanism: stayed with the @llvm.global_ctors
constructor. Investigated clang's
`__DATA,__objc_selrefs` + `externally_initialized` shape — works
for fully-linked binaries (dyld substitutes the SEL at load
time) but **LLVM ORC JIT** (the engine behind `sx run`) doesn't
process Mach-O Obj-C metadata sections, so the slot keeps its
initial value (the method-name string pointer) and dispatch
crashes with "<null selector>". The portable choice: keep the
constructor AND inject a direct call to it at `main`'s entry —
idempotent under dyld (sel_registerName returns the same SEL on
re-registration), required for ORC JIT.

Files touched:
  src/ir/inst.zig    | new ObjcMsgSend struct + opcode
  src/ir/lower.zig   | drop the void-only restriction; emit the
                       new opcode; remove the orphaned
                       getObjcMsgSendFid path (objc_msgSend
                       declaration moved to emit_llvm)
  src/ir/emit_llvm.zig | objc_msg_send arm (per-call-site
                       LLVMBuildCall2); lazy `@objc_msgSend`
                       declaration via getObjcMsgSendValue;
                       emitObjcSelectorInit refactored to inject
                       the ctor call at main's entry
  src/ir/{print,interp}.zig | switch arms for the new opcode

`ffi-objc-call-03-selector-sharing.ir` snapshot updates to
reflect the new shape (the `call ... @objc_msgSend` call sites
no longer mention a typed wrapper).
2026-05-19 18:39:10 +03:00
agra
b8a412ddc7 ffi 1.5: intern Obj-C selectors — one static SEL slot per unique name
101/101 regression tests pass; the IR snapshot for the selector-
sharing test diff flips from four per-call `sel_registerName` calls
to two (one per unique selector) routed through a module-init
constructor — matching what clang emits for `@selector(...)`.

Hot-path cost collapses from a libobjc hashtable lookup per call to
a single load of a static `SEL*` slot:

  Before (Phase 1.3):
    %sel = call ptr @sel_registerName(<"init">)
    call ptr @objc_msgSend(<recv>, %sel)

  After (Phase 1.5):
    %sel = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init
    call ptr @objc_msgSend(<recv>, %sel)

  +  @OBJC_SELECTOR_REFERENCES_init    = internal global ptr null
  +  @OBJC_SELECTOR_REFERENCES_release = internal global ptr null
  +  define internal void @__sx_objc_selector_init() {
  +    %sel  = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
  +    store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_init
  +    %sel1 = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_.2)
  +    store ptr %sel1, ptr @OBJC_SELECTOR_REFERENCES_release
  +    ret void
  +  }
  +  @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }]
  +    [{ ..., ptr @__sx_objc_selector_init, ptr null }]

Implementation:
  module.zig    | new `objc_selector_cache: ArrayList(ObjcSelectorEntry)`
                  with `lookupObjcSelector` / `appendObjcSelector`. List
                  (not hashmap) keeps emit order stable across builds so
                  the IR snapshot doesn't flicker on rehash.
  lower.zig     | `internObjcSelector(sel)` creates the slot on first
                  use, returns the same `GlobalId` on every subsequent
                  call to the same selector. lowerFfiIntrinsicCall now
                  emits `global_addr + load` for literal selectors.
                  Non-literal selectors keep the `sel_registerName`
                  fallback. Declaring `sel_registerName` lazily on
                  first intern so emit_llvm finds it for the
                  constructor body.
  emit_llvm.zig | new `emitObjcSelectorInit` pass synthesizes a void
                  constructor that loops over the cache, calls
                  `sel_registerName` for each unique selector string,
                  stores the result in the slot. Constructor is
                  registered in `@llvm.global_ctors` with default
                  priority (65535) so dyld runs it before main.

The `@OBJC_METH_VAR_NAME_` private string globals and unnamed-addr
flag match clang's exact emission shape — picked up by the system
linker into the right Mach-O sections on macOS / iOS. Chess
Android + iOS-sim still build clean (no `#objc_call` in chess yet —
phase-3 migration will start exercising this).
2026-05-19 13:09:34 +03:00
agra
26a04e49d0 tests: IR-snapshot harness — diff sx ir output when .ir present
run_examples.sh now supports an optional `tests/expected/<name>.ir`
sibling to `.txt`/`.exit`. When present, the runner also captures
`sx ir <file>` output, normalizes target-/host-specific noise
(module ID, target triple/datalayout, attribute groups, LLVM's
auto-suffixed %temp numbering), and diffs against the snapshot.
`--update` regenerates it alongside the runtime output.

Catches lowering changes that don't affect what the program prints
— exactly the shape Phase 1.5's selector interning will produce
(same runtime output, very different IR).

First snapshot: `ffi-objc-call-03-selector-sharing.ir`. Today the
test emits four `call ptr @sel_registerName(ptr @str.N)` lines for
its four call sites; after 1.5 we expect two static
`@OBJC_SELECTOR_REFERENCES_<sel>` globals + loads at each call
site. The diff between the two snapshots will be the visible
artifact of the optimization.
2026-05-19 13:01:28 +03:00