Commit Graph

4 Commits

Author SHA1 Message Date
agra
c0b338eaa4 ffi M1.2 A.4b.ii: emit C-ABI IMP trampolines (dead code pending class_addMethod)
For each bodied instance method on a sx-defined #objc_class,
emit a C-callconv trampoline function '__<Cls>_<method>_imp':

  void __SxFoo_bump_imp(ptr obj, ptr _cmd, ...user_args) {
      ivar  = load @__SxFoo_state_ivar
      state = object_getIvar(obj, ivar)
      call @SxFoo.bump(__sx_default_context, state, ...user_args)
      ret
  }

The trampoline bridges the Obj-C runtime's IMP calling convention
('id self, SEL _cmd, ...args' as C ABI) to the sx body's
default-callconv shape ('__sx_ctx ptr, state ptr, ...user_args').
Implicit context comes from '&__sx_default_context'; the body
keeps its sx-side personality intact and can use 'self.field'
through the substituted state-struct pointer (M1.2 A.2b + A.3).

New helpers in lower.zig:
- 'getObjcObjectGetIvarFid' lazily declares object_getIvar.
- 'emitObjcDefinedClassImps' + 'emitObjcDefinedClassImp' walk the
  cache and synthesise each trampoline.
- 'lookupGlobalIdByName' for finding the per-class ivar handle
  global. Linear scan — same N-is-small rationale as the other
  Obj-C caches.

Dead code at this commit: the trampolines exist in the module
but no class_addMethod call registers them with the runtime.
'objc_msgSend(obj, sel_bump)' would still fall through to the
parent class (NSObject 'doesNotRecognizeSelector:') today.
A.4b.iii wires up class_addMethod in emit_llvm's class-pair-init
constructor — that's when the trampolines come alive.

142's IR snapshot refreshed to show the trampoline.

173 example tests pass. zig build test green.
2026-05-25 22:52:34 +03:00
agra
c2178c062b ffi M1.2 A.4b.i: __sx_state ivar registration
Class-pair init constructor now registers a single hidden ivar
on each sx-defined class:

  class_addIvar(cls, "__sx_state", 8, 3, "^v")

before objc_registerClassPair. After the class is registered,
the constructor calls class_getInstanceVariable to fetch the
runtime Ivar handle and stores it in a per-class global
'__<ClassName>_state_ivar : *void'. Trampolines (A.4b.ii) will
read this global to 'object_getIvar' the state struct pointer.

lower.zig declares the per-class global at scan time
(declareObjcDefinedStateIvarGlobal) so emit_llvm finds it by
name when populating. Encoding '^v' = void* (a generic pointer
— the runtime treats it as opaque storage). log2 alignment = 3
for 8-byte pointer alignment on 64-bit.

144-objc-class-ivar-registration.sx exercises the round-trip:
after main starts, class_getInstanceVariable(SxFoo, "__sx_state")
returns non-null. Runs against the real Obj-C runtime on macOS.

142's IR snapshot refreshed to include the new constructor body
(class_addIvar + class_getInstanceVariable + ivar-global store).

173 example tests pass (+1 from 144). zig build test green.
2026-05-25 22:23:59 +03:00
agra
b98a22e3f9 ffi M1.2 A.4: emitObjcDefinedClassInit class-pair registration
For every sx-defined '#objc_class', emit a module-init constructor
that registers the class with the Obj-C runtime at module load.
Pattern mirrors the Phase 3.1 emitObjcClassInit companion:
'@llvm.global_ctors' + ORC-JIT main injection.

Constructor body, per cache entry:

  super = objc_getClass("<ParentName>")  // default NSObject
  cls   = objc_allocateClassPair(super, "<ClassName>", 0)
  objc_registerClassPair(cls)

Parent is read from the foreign_class_decl's '.extends' member;
absent ⇒ NSObject (matches M1.2 A.0 spec). Class-name strings
go through new emitPrivateCString helper that mirrors the
selector-init / class-init shape.

Two new small helpers extracted while we were here:
- lazyDeclareCRuntime — declare-once extern wrapper for Obj-C
  runtime APIs.
- appendModuleCtor — append-or-create global_ctors + ORC-JIT
  injection, factored out of emitObjcClassInit.

143-objc-class-registration.sx exercises the round-trip on
macOS: after main starts, objc_getClass("SxFoo".ptr) returns
non-null. Runs against the real Obj-C runtime.

142's IR snapshot updated — the constructor + ctors metadata
are now part of the expected shape.

DEFERRED (A.4b): method-IMP registration (class_addMethod with
a C-ABI trampoline that reads __sx_state ivar and calls the sx
body). DEFERRED (A.5+): synthesized +alloc / -dealloc IMPs and
the '__sx_state' ivar setup.

172 example tests pass (+1 from 143). zig build test green.
2026-05-25 22:14:31 +03:00
agra
659cdc2276 ffi M1.2 A.2c + A.3: eager body lowering + self.field via state struct
Adds Pass 4b 'lowerObjcDefinedClassMethods' to lowerRoot: after
scan, walk objc_defined_class_cache and force-lower each bodied
instance method. The Obj-C runtime invokes these via the IMP
pointers wired up in A.4 — no sx-side call path drives lazy
lowering, so we trigger it here. Mirrors the JNI eager-lower
pattern in Pass 5.

Bug fix: lazyLowerFunction has its OWN inline body-lowering
path (separate from lowerFunction) that re-resolves param types
at line 1025. It was running without current_foreign_class set,
so '*Self' fell through to the type_bridge fallback and got
interned as a 0-field struct named 'Self' — body's
'self.counter' GEP'd into '{}' and LLVM verification rejected.
Fix: set current_foreign_class at the top of lazyLowerFunction
via the same lookupObjcDefinedClassForMethod path lowerFunction
uses. Save+restore via defer.

A.3 ('self.field access via the ivar') falls out for free —
'*Self' resolves to '*__SxFooState' so 'self.counter' is a
plain struct field access. IR snapshot in
142-objc-class-method-lowering.ir shows the round-trip:

    define internal void @SxFoo.bump(ptr, ptr self) {
        %gep = getelementptr inbounds { i32 }, ptr %self, 0, 0
        %v = load i32, ptr %gep
        store i32 (%v + 1), ptr %gep
        ret void
    }

171 examples pass (+1 from 142); zig build test green.

Still gated: Obj-C runtime dispatch (A.7) — sx-side
'f.bump()' calls bail at lower.zig:4407 with the existing
diagnostic. IMP-trampoline emission (the C-ABI shim that bridges
'objc_msgSend' → this body) lands in A.4 alongside class-pair
init.
2026-05-25 22:08:23 +03:00