Commit Graph

322 Commits

Author SHA1 Message Date
agra
556e4e12ea ffi M5.A.2: drop hand-rolled __block_invoke_* impls + Into(Block) per-sig boilerplate
The compiler-synthesised trampoline path (previous commit) covers
every closure signature on demand; the hand-rolled stdlib impls
were only for two specific shapes (`Closure() -> void`,
`Closure(bool) -> void`) and are now strictly redundant.

Kept: the `Block` struct, `BlockDescriptor`, the
`_NSConcreteStackBlock` extern decl, and the shared
`__sx_block_descriptor` global. The compiler-emitted code
references all four; users still need to `#import
"modules/std/objc_block.sx";` to bring them into the module.

Removed: `__block_invoke_void`, `__block_invoke_bool`, and both
`impl Into(Block) for Closure(...) -> void` blocks. Replaced with
a comment block explaining how the compiler now handles the cast.

After this commit, `xx my_closure : Block` works for ANY closure
signature with no per-signature stdlib boilerplate. 189/189
example tests pass; chess on iOS-sim green.
2026-05-27 00:24:36 +03:00
agra
22c087ff35 ffi M5.A: compiler-synthesised __block_invoke_<sig> trampolines
`xx closure : Block` casts now bypass the user-space Into(Block)
protocol path entirely. The compiler intercepts in
`tryUserConversion` BEFORE the Into lookup, detects when src is
`Closure(...)` and dst is `Block`, and emits:

1. A C-ABI trampoline `__block_invoke_<sig>` (deduped per closure
   signature via `block_invoke_trampolines` map). Body matches the
   existing hand-rolled `__block_invoke_void` exactly: load
   block_self struct, extract sx_env (field 5) + sx_fn (field 6),
   call sx_fn(__sx_default_context, sx_env, ...user_args), return.

2. Inline Block-struct construction at the cast site:
   `Block { isa = &_NSConcreteStackBlock, flags=0, reserved=0,
            invoke = &__block_invoke_<sig>,
            descriptor = &__sx_block_descriptor,
            sx_env = closure.env, sx_fn = closure.fn_ptr }`

Signature mangling: compact codes — `v` void, `b` bool, `i` s32,
`q` s64, `f` f32, `d` f64, `c/C/s/S/I/Q` for other ints, `p` for
pointers/aggregates that lower to a machine word. Return first,
then params underscore-joined. `Closure() -> void` mangles to `v`;
`Closure(bool) -> void` mangles to `v_b`.

Loud failures at the cast site:
- `Block` struct missing → "requires #import \"modules/std/objc_block.sx\";"
- `_NSConcreteStackBlock` extern missing → same diagnostic.
- `__sx_block_descriptor` global missing → same.
- `__sx_default_context` missing inside the trampoline emitter →
  compiler-bug diagnostic (the scan pass should always register it).

The existing hand-rolled stdlib impls (`__block_invoke_void`,
`__block_invoke_bool`, the two `Into(Block) for Closure(...)`
impls) are now redundant — the compiler-synthesised trampoline
takes over via the intercept. Next commit (M5.A.2) removes them.

95-objc-block-noop continues to pass; IR shows `__block_invoke_v`
(the synthesised name) replacing the hand-rolled
`__block_invoke_void` at the cast site. 189/189 example tests
pass; chess on iOS-sim green.
2026-05-27 00:23:19 +03:00
agra
e5ba06b66c checkpoint: M4 complete (M4.0 + M4.A + M4.B all landed)
12 commits this session shipped the entire M4 milestone. sx-defined
Obj-C classes now honor `context.allocator` end-to-end, route through
NSObject's retain/release at the source-language level, and emit
correct ARC ops in property setters/getters/dealloc per the Apple
ABI contract.

189/189 example tests pass; chess on iOS-sim green throughout.
Ready for M5 (closure↔block bridge) or M1.1.b (Class(T) phantom
typing) next session.
2026-05-26 23:10:41 +03:00
agra
fcbd7a4235 ffi M4.B dealloc: release strong/copy property ivars + destroyWeak weak
emitObjcDefinedClassDeallocImp now walks the class's #property fields
BEFORE freeing the state struct. For each:

- assign  → no-op (primitives, no ARC traffic).
- strong  → val = load field; objc_release(val).
- copy    → same as strong (the stored value is a +1 retained copy
            produced by the setter's [val copy]; we release it here).
- weak    → objc_destroyWeak(&field) — unregisters the slot from
            libobjc's side-table so the runtime stops tracking it.

Order matters: property releases happen BEFORE freeing the state
struct (which would invalidate the pointers we need to read), which
happens BEFORE [super dealloc] (which eventually frees the Obj-C
instance's own memory). The full sequence is now:

  %state    = object_getIvar(self, __sx_state_ivar)
  // M4.B (this commit):
  for each strong/copy property P:
      val = load struct_gep(state, P.idx); objc_release(val)
  for each weak property P:
      objc_destroyWeak(struct_gep(state, P.idx))
  // M4.0c (already shipped):
  allocator = load struct_gep(state, 0)
  allocator.dealloc(state)
  object_setIvar(self, ivar, null)
  // M1.2 A.6:
  [super dealloc]   // → objc_msgSendSuper2

ffi-objc-arc-02-strong-property now passes: child held by parent's
strong property gets released when parent deallocates, refcount → 0,
child deallocates, both states freed via tracker. Balanced 2/2.

189/189 example tests pass; chess on iOS-sim green. M4 complete.
2026-05-26 23:10:00 +03:00
agra
c88a293cf4 ffi M4.B getter: weak property reads through objc_loadWeakRetained
emitObjcDefinedPropertyGetter dispatches on objcPropertyKind. The
strong/copy/assign paths keep their bare load. The weak path:

  retained     = objc_loadWeakRetained(field_addr)
  autoreleased = objc_autorelease(retained)
  return autoreleased

`objc_loadWeakRetained` does the race-safe upgrade via libobjc's
side-table: if the target has deinitialized (or is mid-dealloc on
another thread), returns null; otherwise returns the target with
refcount bumped (+1 retained, transferred to caller).

`objc_autorelease` drops the +1 into the current pool so the
caller doesn't need to manually balance — matches Apple's auto-nil
weak-getter contract.

The bare-load weak path (still in place pre-M4.B-getter) worked
for the single-threaded test scenario because the runtime nils the
slot before the load happens. The load-retained version covers the
multi-threaded "between load and use, target deinit's" race that
silent bare-load can't.

189/189 example tests pass; chess on iOS-sim green.
2026-05-26 23:04:00 +03:00
agra
f4faef97dd ffi M4.B setter: emit ARC ops in sx-defined property setters
emitObjcDefinedPropertySetter now dispatches on objcPropertyKind to
emit the right runtime ops per Apple's ARC contract:

- assign  → bare store (primitives, explicitly opted-out object slots).
- strong  → load old; objc_retain(new); store new; objc_release(old).
            Apple's runtime treats release(NULL) as a safe no-op, so
            no explicit null-check on the old value.
- weak    → objc_storeWeak(field_addr, val) — handles first-store
            (init) and re-store (destroy + init) atomically. Registers
            the slot with libobjc's side-table; the runtime auto-nils
            it when the target deallocates.
- copy    → [val copy] (sends `copy` selector — returns retained per
            the NSCopying contract); load old; store the copied
            instance; release old.

Side-effect on the weak path: even with the bare-load getter still in
place (loaded directly from the slot), weak reads work because Apple's
runtime side-table-nils the slot at target dealloc. The getter
improvement via objc_loadWeakRetained is the next commit and is
needed for race-safe reads (between load and use, the target could
deinit on another thread); for the single-threaded test scenarios
the bare load is sufficient.

ffi-objc-arc-02-strong-property advances from "child dealloc'd at
midpoint" to "unbalanced; alloc=2 dealloc=1" — strong setter now
retains, but the M4.B-dealloc cleanup hasn't landed so the child
held by the property isn't released when the parent deallocates.
Final commit (M4.B dealloc) closes the loop.

ffi-objc-arc-03-weak-property turns fully green: storeWeak +
auto-nil side-table do the work.

189/189 example tests pass; chess on iOS-sim green.
2026-05-26 23:02:08 +03:00
agra
5c1d00a877 ffi M4.B helpers: objcPropertyKind + ARC runtime decls + xfail tests
Three pieces, no behavior change yet:

1. `ObjcPropertyKind` enum (strong/weak/copy/assign) + `objcPropertyKind`
   helper in lower.zig. Reads `field.property_modifiers`, applies the
   default rule (`*<ObjC-class>` → strong; primitives → assign), and
   emits loud diagnostics for the silent-error budget:
   - unknown modifier name (typo) → "expected one of: strong, weak, copy, ..."
   - conflicting modifiers (e.g. `strong,weak`) → "mutually exclusive"
   - `weak` on non-object slot → "requires a pointer-to-Obj-C-class type"
   - `copy` on non-object slot → same
   - `strong` (default or explicit) on `*void` → "ambiguous: specify
     #property(strong|weak|copy|assign) explicitly"
   Called from `emitObjcDefinedClassPropertyImps` for validation; the
   returned kind isn't wired into setter/getter/dealloc yet — that's
   the next three commits.

2. `ensureArcRuntimeDecls` lazily declares libobjc's ARC helpers:
   objc_retain, objc_release, objc_storeWeak, objc_loadWeakRetained,
   objc_initWeak, objc_destroyWeak. Uses the existing
   `ensureCRuntimeDecl` pattern; idempotent.

3. Fix existing NSObject method names in std/objc.sx — `isEqual_`,
   `isKindOfClass_`, `respondsToSelector_` had trailing underscores
   that the selector mangling turned into double-colon selectors
   (`isEqual::`). Removed the trailing underscore so the selectors
   come out as `isEqual:`, `isKindOfClass:`, `respondsToSelector:`
   as Apple's runtime expects.

4. Two xfail regression tests:
   - ffi-objc-arc-02-strong-property: assigns child to parent's strong
     property, releases the original child reference. Midpoint check:
     child's dealloc should NOT have fired (strong setter retained).
     Pre-M4.B-setter: child dealloc fires immediately → "FAIL: child
     dealloc'd at midpoint" snapshot. Exit code 1.
   - ffi-objc-arc-03-weak-property: assigns target to holder's weak
     property, releases target. Reads holder.target → should be null
     (auto-niled). Pre-M4.B-getter/setter: reads stale pointer →
     "FAIL: weak property didn't auto-nil" snapshot.

These will turn green as M4.B setter (commit 2), getter (commit 3),
and dealloc-cleanup (commit 4) land. Each subsequent commit updates
the snapshot to reflect the now-passing output.

189/189 example tests pass; chess on iOS-sim green.
2026-05-26 22:58:30 +03:00
agra
8c3831acd2 test: M4.0 allocator-threading regression coverage
Two regression tests pinning down the silent-error surface in M4.0:

ffi-objc-arc-00 — single sx-defined-class instance round-trips
through a TrackingAllocator-wrapped GPA. Captures alloc/dealloc
deltas around the lifecycle, verifies (+1, +1). Pre-M4.0 the +alloc
IMP used libc malloc and -dealloc used libc free; tracker would
have observed (+0, +0) and missed the leak silently.

ffi-objc-arc-00b — three instances alloc'd and released. Catches
bugs where:
- the captured allocator becomes shared (one global slot vs
  per-instance);
- alloc captures the wrong allocator on the 2nd+ instance;
- dealloc reads garbage if state[0] is overwritten between
  instances.

Both tests are macos-only (libobjc + NSObject must be present at
runtime). Both wrap the lifecycle in `push Context.{ allocator =
xx tracker }` so the threading path is exercised.

Important authoring note: `print` inside the push-block also routes
through tracker (string formatting allocs), polluting the leak
delta. Tests capture before/after counts WITHOUT any prints between
alloc and release, then verify the BALANCE — every alloc paired
with a dealloc — rather than absolute counts. Discovered while
writing 00: an initial naive "leak_count() == 0" assertion failed
not because M4.0 was broken but because print's string allocs
weren't freed at scope exit.

187/187 example tests pass.
2026-05-26 22:46:56 +03:00
agra
afa9d4aa46 checkpoint: M4.0 + M4.A complete; M4.B remaining
Snapshot of FFI progress mid-M4. Allocator-aware sx-defined-class
lifecycle is done end-to-end (state struct + +alloc + -dealloc).
Stdlib NSObject + autoreleasepool helper landed; defer-release
pattern works at user code. Property ARC ops (M4.B) is the next
slice.

185/185 example tests pass; chess on iOS-sim regression-verified
at every M4 sub-commit.
2026-05-26 22:39:52 +03:00
agra
29404afdee ffi M4.A: stdlib NSObject + autoreleasepool helper + extends rooting
Declare `NSObject` in std/objc.sx as `#foreign #objc_class("NSObject")`
with the canonical instance + class-method surface every Obj-C class
inherits: `retain`/`release`/`autorelease`/`new`/`alloc`/`init`/
`description`/`hash`/`isEqual_`/`isKindOfClass_`/`respondsToSelector_`/
`class`. Root the foreign-class hierarchy in uikit.sx at NSObject by
adding `#extends NSObject;` to every previously-unrooted declaration
(NSValue, NSNumber, NSDictionary, NSSet, NSNotification, NSBundle,
NSNotificationCenter, NSRunLoop, CADisplayLink, CALayer, EAGLContext,
UIScreen, UIResponder) plus deeper chain fixes (NSMutableDictionary
extends NSDictionary; UIWindow extends UIView; UIViewController
extends UIResponder). After this, M2.3's extends-chain walk finds
`retain`/`release` on any UIKit-typed value:

  view := UIView.alloc().init();
  defer view.release();        // canonical sx idiom — no language magic

Plus `autoreleasepool(body: Closure())` stdlib helper that wraps
`body` in `objc_autoreleasePoolPush` / `defer objc_autoreleasePoolPop`.
Required for Foundation factory returns; closure-call frame is real
cost so hot loops should inline the push/defer-pop pattern manually.

Smoke test `ffi-objc-arc-01-autoreleasepool.sx` exercises both
patterns; refresh of two IR snapshots picks up the new stdlib decls
appearing in test outputs that include `modules/std/objc.sx`.

185/185 example tests pass; chess on iOS-sim green.
2026-05-26 22:38:32 +03:00
agra
92ac51445d ffi M4.0c: -dealloc frees state through captured __sx_allocator
The synthesized -dealloc IMP now loads `state->__sx_allocator` (the
slot captured at +alloc time by M4.0a + M4.0b) and dispatches
`allocator.dealloc(state)` through the inline-protocol fn-ptr at
slot 2. Old behaviour was `free(state)` — went straight to libc,
ignoring whatever allocator the instance was constructed with.

After this commit, the per-instance allocator design from M1.2 A.5
is finally end-to-end correct:

  push Context.{ allocator = arena } {
      f := SxFoo.alloc();     ← arena.alloc(STATE_SIZE) + capture
      // ... use f ...
  }
  // refcount → 0 ⇒ -dealloc:
  //   load state->__sx_allocator   = arena
  //   arena.dealloc(state)         ← same allocator round-trips

TrackingAllocator now sees the alloc/dealloc pair; the deferred M1.2
A.5 work is done. Closes the loop on M4.0.

The dealloc IMP passes `__sx_default_context` as the implicit __sx_ctx
when invoking the dealloc fn-ptr — the IMP itself has no caller-side
ctx (it's called by Apple's runtime at refcount-zero), and the
default GPA is the right baseline for any nested allocations the
dealloc body might perform.

Each compiler-internal lookup that "can't fail" (Context type,
__sx_default_context global) emits a loud diagnostic instead of
silent fall-through, per the silent-error budget.

184/184 example tests pass; chess on iOS-sim green.
2026-05-26 22:30:48 +03:00
agra
2bbd63d929 ffi M4.0b: thread context.allocator through sx-defined +alloc
Two converging paths now allocate the state struct via the protocol's
allocator instead of raw malloc:

(1) sx-side `Cls.alloc()`: compiler intercepts in `lowerObjcStaticCall`
    when the receiver is a sx-defined `#objc_class` and the method is
    the niladic `alloc`. Emits the inline alloc-and-init sequence
    using the caller's `current_ctx_ref` as the context — so
    `push Context.{ allocator = my_arena } { let f := SxFoo.alloc(); }`
    honors `my_arena` end-to-end. The msgSend dispatch is bypassed
    entirely for this case.

(2) Obj-C-runtime `[Cls alloc]` (Info.plist principal class, NSCoder,
    UIKit reflection): the synthesized `+alloc` IMP shim reads
    `__sx_default_context.allocator` and calls into the same shared
    helper. The IMP has `has_implicit_ctx = false` and runs with no
    caller-side context — the default GPA is the right policy choice
    for "everything Apple's runtime instantiates".

Shared helper `emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr)`
does the work: `class_createInstance` → `ctx.allocator.alloc(STATE_SIZE)`
via the inline-protocol fn-ptr → memset 0 → store allocator at
state[0] (the M4.0a slot, captured for -dealloc's later use) →
`object_setIvar(instance, __sx_state_ivar, state)`. Loud failures
on missing globals via the diagnostics system.

The sx-side interception must explicitly bitcast the
`class_createInstance` result from `*void` to the method's declared
return type (`*<Cls>` or `?*<Cls>`). lowerVarDecl reads the Ref's IR
type when no type annotation is present, and coerceToType is a
no-op for ptr→ptr — without the bitcast, `let f := SxFoo.alloc();`
binds `f` at `*void` and downstream `f.class` / `f.method()` fails
to find anything.

-dealloc still uses `free(state)` (M4.0c rewrites it). 184/184 tests
pass; chess on iOS-sim green.
2026-05-26 22:27:33 +03:00
agra
8d7164f45f ffi M4.0a: prepend __sx_allocator to sx-defined-class state struct
State struct for an sx-defined `#objc_class` now leads with an
Allocator field at index 0 — captured at +alloc time, read by
-dealloc to free the state through the same allocator. User fields
shift to index 1+; the existing by-name lookups in
emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer
naturally resolve them at the new indices.

This step is the layout change only; the +alloc IMP still mallocs
(M4.0b will rewrite it to thread context.allocator through), and
-dealloc still uses free() (M4.0c). The field is allocated but
uninitialised; nobody reads it yet.

Storage type comes from `Context.fields[0].ty` via the new
`objcStateAllocatorType` helper — same Allocator value-shape the
implicit context machinery has used all along. If Context isn't
registered (early-init paths), the helper falls back to omitting
the field rather than synthesising a half-broken layout.

IR snapshot for 142-objc-class-method-lowering updated to reflect
the new struct shape and the +24-byte state allocation. Chess on
iOS-sim green; 184/184 example tests pass.
2026-05-26 22:07:56 +03:00
agra
9fbc24a602 ffi uikit cleanup: helpers → UIKitPlatform methods + declarative layerClass
Three threads, one commit because they're entangled:

1. Helper free functions on `*UIKitPlatform` (refresh_safe_insets,
   read_screen_scale, create_gl_context, setup_renderbuffer,
   present_renderbuffer, compute_layer_pixel_size) → methods on the
   `impl Platform for UIKitPlatform` block. IMP-shape trampolines
   (`uikit_keyboard_will_change_frame`, `uikit_scene_will_connect[_ios]`,
   `uikit_gl_view_tick/layout/touches_*`, `uikit_subscribe_keyboard_notifications`)
   also collapse into methods on UIKitPlatform — the
   `(self: *void, _cmd: *void, ...)` form is no longer needed since
   M3 made the #objc_class trampolines compiler-synthesized. Class
   method bodies in SxAppDelegate / SxSceneDelegate / SxGLView /
   SxMetalView now read `if g_uikit_plat == null { return; }
   g_uikit_plat.x();` — no more `xx self, xx 0` casts at every IMP
   call site.

2. Declarative `layerClass` form. SxGLView and SxMetalView promote
   from the M2.1(a) constant-with-runtime-string-lookup workaround
   (`layerClass :: *void = objc_getClass("CAEAGLLayer".ptr);`) to
   the class-method expression-body form
   (`layerClass :: () => CAEAGLLayer.class();`). Type stays `*void`
   until M1.1.b lands `Class(T)` parameterisation; the value side
   already matches the plan. Backing this: foreign-class declarations
   for CAEAGLLayer (extended with `class :: () -> *void;`) and a new
   CAMetalLayer foreign-class declaration alongside it. Both
   `#extends CALayer` so the dispatch chain knows about the parent.

3. Optional-shape idiom pass on uikit.sx. `xx`-as-optional-wrap on
   field assignments (`plat.gl_ctx = xx ctx`, `plat.text_field = xx tf`,
   `plat.display_link = xx link`) dropped — implicit `T → ?T` does
   the right thing. `!` force-unwraps replaced with `if val := opt
   { ... }` safe-narrowing (the keyboard handler, the GL-context
   read in setup/present renderbuffer, the gl_view read in scene
   bootstrap). `orelse` (Zig keyword) that briefly snuck into the
   keyboard handler removed in favour of the `if win := plat.window`
   narrowing pattern. Result: no `xx` casts left on the implicit
   T→?T path; all optional access goes through `if val :=`.

IR snapshots `ffi-objc-call-06-sret-return.ir` and
`ffi-objc-dsl-07-mangling-table.ir` refresh to pick up the new
`object_getIvar` / `object_setIvar` runtime-helper declarations
introduced when M1.2 A.3 made instance-method bodies route field
access through the state ivar.

Chess on iOS-sim green throughout. 184/184 example tests pass.
2026-05-26 16:42:57 +03:00
agra
a923b6f6f0 ffi fix: route foreign-class UFCS arg target_types through extends chain
For UFCS dispatch on foreign-class receivers (`#foreign #objc_class`
aliases), `resolveCallParamTypes` was returning an empty slice — both
`resolveFuncByName(qualified)` and `fn_ast_map.get(qualified)` miss
for `#foreign` methods (they live in `foreign_class_map`, not the
regular fn maps). With `param_types` empty, the per-arg `target_type`
assignment in `lowerCall` was skipped, leaving `self.target_type` as
whatever it held on entry — usually the enclosing function's return
type. Inside a `-> BOOL` method, `xx ptr` then lowered with target
type `i8`: `ptrtoint ptr to i64` → `trunc i64 to i8`, sending the low
byte of the pointer through.

Symptom: chess on iOS-sim crashed in
`-[NSNotificationCenter addObserver:selector:name:object:]` with
`observer = 0xC0` (low byte of the SxAppDelegate receiver) when the
AppDelegate method's first param was renamed to anything other than
`self`. The original session diagnosed it as a `self`-vs-`this`
hardcoding in `lower.zig`, but those hardcoded `"self"` strings are
all on compiler-synthesized parameters (init scopes, JNI stubs,
property IMPs, dealloc IMPs) — not the user-facing #objc_class body
params. The bug was in arg-type resolution.

Fix walks `foreign_class_map` + `findForeignMethodInChain` to recover
the declared param types (skipping the implicit `*Self` for instance
methods). Regression test `examples/issue-0044.sx` exercises the
BOOL-return + foreign-class arg shape; pre-fix the receiver round-trip
prints WRONG, post-fix it prints ok.
2026-05-26 16:42:21 +03:00
agra
f75923af00 uikit: type UIKitPlatform fields properly + handle optional in Obj-C encoding
The UIKitPlatform struct had a string of '*void = null; // UIWindow*'
fields — the type lived in a comment, every callsite had to 'xx'-cast
back to the real type. Migrated to the real foreign-class pointer
types now that M3 declared all the relevant '#objc_class' aliases:

  window:       ?*UIWindow
  root_vc:      ?*UIViewController
  gl_view:      ?*UIView         (SxGLView OR SxMetalView — both extend UIView)
  gl_layer:     ?*CALayer        (CAEAGLLayer OR CAMetalLayer)
  gl_ctx:       ?*EAGLContext
  display_link: ?*CADisplayLink

Each field is wrapped in '?' since the platform may not have set
it yet (gl_ctx is null in metal mode, display_link is null before
the first frame, etc.).

SxSceneDelegate's window getter/setter now take/return '?*UIWindow'
instead of '*void' so calling code doesn't need an xx-cast.

Required fix in objcTypeEncodingFromSignature: '?T' (optional) was
bailing with 'type kind not yet supported'. Apple's runtime treats
nullability as 'pointer may be null' — the wire encoding is the
same as T. Recursive unwrap handles ?*UIView → '@', ?*CADisplayLink
→ '@', etc.

Chess on iOS-sim: board renders, full pipeline intact. 183 tests
+ zig build test green.
2026-05-26 07:51:12 +03:00
agra
d403f56673 uikit: inline three trivial legacy IMP helpers into #objc_class methods
Three holdover free functions from the pre-M3 era were each
just two or three lines that forwarded to a global. With M3
finished, every call site is one #objc_class method body, so
the wrapper indirection earns nothing — inline them.

Deleted:
  uikit_window_getter         → body of SxSceneDelegate.window
  uikit_window_setter         → body of SxSceneDelegate.setWindow
  uikit_did_finish_launching  → body of
    SxAppDelegate.application_didFinishLaunchingWithOptions

The bigger helpers (uikit_keyboard_will_change_frame,
uikit_scene_will_connect, uikit_gl_view_tick/_layout, the four
uikit_gl_view_touches_*) stay — their bodies are 30-80 lines
each, so wrapping them in a small forwarding method body inside
#objc_class is the cleaner factoring.

Chess on iOS-sim: board renders, full game state intact. 183
example tests + zig build test green.
2026-05-26 07:48:15 +03:00
agra
78288b98ac ffi M3.3 + M3.4 + M3.5: SxGLView/SxMetalView migrated; uikit_register_classes deleted
Three slices in one commit since they're tightly coupled (the
M3.5 deletion only makes sense after M3.3 and M3.4):

M3.3 — SxGLView migrated to declarative '#objc_class("SxGLView")':
  - '#extends UIView' for the view-hierarchy + responder chain.
  - 'layerClass :: *void = objc_getClass("CAEAGLLayer".ptr);' uses
    the M2.1(a) class-level constant form. Registered on the
    metaclass; UIView's +layerClass override dispatches here so
    EAGL gets the right backing layer.
  - Six instance methods (sxTick, layoutSubviews, four touch
    selectors) forward to existing legacy IMP free functions.

M3.4 — SxMetalView migrated, same shape as SxGLView; differs only
  in the 'layerClass' constant returning CAMetalLayer instead of
  CAEAGLLayer. The five shared IMPs (sxTick/layoutSubviews/4 touch
  handlers) reach the same free functions — they already branch on
  plat.gpu_mode for GL-specific renderbuffer code.

M3.5 — uikit_register_classes() and the two helper registration
  functions are deleted outright. Every sx-defined Obj-C class in
  this module now goes through the compiler's M1.2 / M2.1(a)
  synthesis path at module init. The call site inside
  UIKitPlatform.init is gone too — just a comment marking the
  migration point.

Chess on iOS-sim: board renders, scene-delegate connection still
fires, GL/Metal layer setup intact, touch dispatch routes through
the synthesized IMP trampolines. 183 example tests + zig build
test green.

End of M3. The platform layer's Obj-C-runtime wiring is fully
declarative.

Remaining: M4 (autoreleasepool + ARC ops), M5 (closure↔block),
M6 (auto-import + production hardening). M1.1.b (Class(T)
parameterization + instancetype) is still deferred — none of
the migrated uikit code needed it.
2026-05-26 07:41:07 +03:00
agra
066840d9e0 ffi M3.2: SxSceneDelegate migrated + #implements protocol conformance
Migrates SxSceneDelegate from the hand-rolled
objc_allocateClassPair + class_addMethod + class_addProtocol
sequence to the declarative form:

  SxSceneDelegate :: #objc_class("SxSceneDelegate") {
      #extends UIResponder;
      #implements UISceneDelegate;
      #implements UIWindowSceneDelegate;

      scene_willConnectToSession_options :: (self, scene, session, options) { ... }
      window    :: (self) -> *void { ... }
      setWindow :: (self, w) { ... }
  }

emit_llvm now honors '#implements' in the class-pair init
constructor — for each #implements ProtocolAlias on the cache
entry's AST, emit before objc_registerClassPair:

  proto = objc_getProtocol("ProtocolName")
  class_addProtocol(cls, proto)

iOS checks 'class_conformsToProtocol' when instantiating scene
delegates; without the conformance the runtime silently rejects
the class and a default scene with no delegate gets created
instead. The protocol-getter returns null on dead-strip /
runtime mismatch (rare but possible) — the runtime treats
class_addProtocol(cls, null) as a no-op, so no explicit null
check needed.

Method bodies forward to the existing legacy free IMP functions
(uikit_scene_will_connect, uikit_window_getter,
uikit_window_setter) so we don't have to inline the scene-
connect setup logic (~80 lines).

uikit_register_classes is now tiny — just the two remaining
view-class helpers (M3.3 SxGLView + M3.4 SxMetalView). M3.5
deletes the function entirely once those land.

Chess on iOS-sim: board renders, scene delegate fires, touch
events route correctly. 183 example tests + zig build test
green.
2026-05-26 07:37:14 +03:00
agra
66f84f67b8 ffi M3.1 + M1.2 A.3 refactor: self=Obj-C id, self.field via ivar; SxAppDelegate migrated
Two coupled changes that unblock the uikit_register_classes
migration:

1) M1.2 A.3 — body's 'self' is the Obj-C id (opaque), NOT the
   state struct. Matches Apple's ObjC semantics where 'self' IS
   the object. Cocoa idiom 'xx self → id' works at runtime calls
   (addObserver:, etc.); previously the trampoline replaced
   'self' with the state-struct pointer, breaking any runtime
   call that expected an id.

   '*Self' substitution in resolveTypeWithBindings now points at
   foreignClassStructType(fcd) — the opaque class stub — instead
   of objcDefinedStateStructType(fcd).

   'self.field' access on a sx-defined class instance field is
   rewritten by lowerFieldAccess to go through the __sx_state
   ivar:
     state = object_getIvar(self, load(__<Cls>_state_ivar))
     val   = struct_gep(state, field_idx) → load

   Both read (lowerFieldAccess) and write (lowerAssignment) take
   this path. Compound ops (+=, -=, etc.) are supported via
   storeOrCompound. The lookup is filtered: skip property fields
   (those still go through the M2.2 msgSend getter/setter
   dispatch) and foreign classes (no state).

   New helpers in lower.zig:
   - lookupObjcDefinedStateFieldOnPointer — match check.
   - lowerObjcDefinedStateForObj — emit the object_getIvar +
     ivar-global-load idiom (shared between read + write paths).
   - lowerObjcDefinedStateFieldRead — the load path.

   Also moved the @llvm.global_ctors registration out of the
   sx-defined class-pair init constructor — global_ctors fires
   DURING dyld's framework load, before UIKit registers its Obj-C
   classes. objc_getClass("UIResponder") returned null, super
   was null, objc_registerClassPair crashed. main's entry block
   is post-framework-load but pre-user-code — exactly the right
   window. New helper injectCtorIntoMain.

2) M3.1 — SxAppDelegate migrated to declarative #objc_class.
   uikit_register_classes' hand-rolled objc_allocateClassPair +
   class_addMethod for SxAppDelegate is gone; the compiler
   synthesises the class at module init. The method bodies
   forward to the existing legacy IMP free functions
   (uikit_did_finish_launching, uikit_keyboard_will_change_frame)
   so we don't have to inline 70+ lines of keyboard-frame logic
   right now.

   Also adds UIResponder foreign-class declaration and chains
   UIView / UITextField to it via #extends UIResponder so the
   methods that previously lived on UITextField directly
   (becomeFirstResponder etc.) move to their proper home.

Chess on iOS-sim: board renders, full state intact. 183 example
tests + zig build test green.
2026-05-26 07:32:57 +03:00
agra
ea32f8a27a ffi M2.3: #extends method-resolution chaining + Obj-C parent resolution
When 'obj.method()' is called on a foreign-class pointer and the
method isn't declared on the receiver's class, the compiler walks
the '#extends' chain to find an ancestor that declared it.
Property lookup (M2.2) flows through the same chain walker.

  ParentX :: #foreign #objc_class("...") { foo :: ... }
  ChildX  :: #foreign #objc_class("...") { #extends ParentX; }

  child.foo()   // now resolves — was 'no method foo on ChildX'

Two new helpers in lower.zig:
- findForeignMethodInChain(fcd, name) walks the cache via
  fcd.members[i].extends → foreign_class_map[parent] → ...
  Depth-capped at 16 to break accidental cycles.
- findForeignPropertyInChain(fcd, name) — same shape for fields.

ALSO fixes a latent class-hierarchy bug uncovered while testing
M2.3: emit_llvm was passing the sx alias name to
objc_allocateClassPair(super, ...) rather than the actual Obj-C
runtime class name. For 'SxThing :: #objc_class(...) { #extends
NSObjectBase; }' where 'NSObjectBase' is aliased to "NSObject",
emit_llvm produced 'objc_getClass("NSObjectBase")' → NULL →
'objc_allocateClassPair(NULL, ...)' → SxThing's super-class link
was broken → '[sx_thing hash]' bypassed NSObject and crashed in
the forwarding machinery.

Fix: ObjcDefinedClassEntry gains a 'parent_objc_name' field
pre-resolved by lower.zig's 'resolveObjcParentName' through
foreign_class_map (which has the alias → foreign_path mapping).
emit_llvm just reads the resolved name from the entry.

153-objc-extends-chain.sx exercises both fixes:
  1-level: SxThing → NSObject — t.hash() walks one #extends.
  2-level: SxLeaf  → SxMiddle → NSObject — chained #extends.
Both return real NSObject.hash values from libobjc.

183 example tests pass (+1). zig build test green.
2026-05-26 01:56:25 +03:00
agra
239e7df27c ffi M2.2 (sx-defined): property getter/setter IMPs
Properties on sx-defined #objc_class declarations now synthesize
getter (always) and setter (unless 'readonly') IMPs that GEP into
the hidden state struct and load / store the corresponding field.
The state struct already holds every user-declared field
(objcDefinedStateStructType), so no new layout work — the IMPs
just dispatch a struct_gep + load/store through the __sx_state
ivar.

For each '#property' field on a sx-defined class:

  Getter '__<Cls>_<field>_imp(self, _cmd) -> T':
    state = object_getIvar(self, load(__<Cls>_state_ivar))
    return state.<field>

  Setter '__<Cls>_set<Field>_imp(self, _cmd, val) -> void':
    state = object_getIvar(self, load(__<Cls>_state_ivar))
    state.<field> = val

Both IMPs land in the cache's methods slice (mirroring the
method-IMP wiring from M1.2 A.4b.iii) so emit_llvm's
class_addMethod loop registers them on the class without
special-casing. Selector mangling:
  getter: <field>            (e.g. 'width')
  setter: set<Field>:        (e.g. 'setWidth:')
Type encoding derived from the field's resolved IR TypeId.

'readonly' (the only modifier honored in this slice) skips the
setter emission AND the corresponding method entry — so the
runtime reports the selector as absent. Other modifiers
(strong, weak, copy, assign) parse fine but stay no-ops until
M4.2 wires up ARC ops in the setter body.

152-objc-property-sx-defined.sx round-trips on macOS:
  b.width = 10; b.height = 7;
  read back through getter IMPs.
  area is readonly — class_getInstanceMethod(SxBox, sel(setArea:))
  returns NULL, confirming the setter is absent.

182 example tests pass (+1). zig build test green.
2026-05-26 01:49:31 +03:00
agra
95f13849af ffi M2.2 (first pass): #property directive on foreign-class fields
Adds:
  field: T #property[(modifier, modifier, ...)];

inside #objc_class declarations. For FOREIGN classes (this slice),
'obj.field' and 'obj.field = x' lower as objc_msgSend dispatches —
no struct GEP, no per-field storage on the sx side. The receiver
is opaque and the Obj-C runtime owns the data.

Selector mangling (Apple convention):
  getter: <fieldName>            (e.g. 'count')
  setter: set<FieldName>:        (e.g. 'setBackgroundColor:')

So:
  view.backgroundColor          → [view backgroundColor]
  view.backgroundColor = red    → [view setBackgroundColor:red]

Plumbing:
- New token hash_property + lexer entry + LSP keyword classification.
- ForeignFieldDecl gains 'is_property' + 'property_modifiers' slice;
  the parser captures both. Modifiers are recorded verbatim (strong,
  weak, copy, readonly, getter("name"), ...) — semantic interpretation
  lands with M4.2 ARC wiring.
- lowerFieldAccess: lookupObjcPropertyOnPointer() detects the case
  before the auto-deref / struct-GEP path and dispatches via
  lowerObjcPropertyGetter (objc_msg_send).
- lowerAssignment: same check on the field_access LHS routes to
  lowerObjcPropertySetter (objc_msg_send with set<Field>:).
- inferExprType: 'obj.field' returns the property's declared type
  so chained access / coerced assignment work.

151-objc-property-foreign.sx round-trips:
  inst.tag        → [inst tag]       → reads g_probe_tag → 0
  inst.tag = 42   → [inst setTag:42] → writes g_probe_tag
  inst.tag = -7   → ditto
  Final: 0 -> 42 -> -7  (real Obj-C runtime dispatch).

DEFERRED for M2.2 (later passes):
- Sx-defined property IMPs (synthesized getter/setter trampolines
  reading/writing the state struct).
- Modifier-driven setter behavior: readonly (compile error on
  write), copy (deep-copy), weak (objc_storeWeak), strong/assign
  (Month 4.2 ARC ops).
- getter("name") / setter("name:") selector overrides.

181 example tests pass (+1). zig build test green.
2026-05-26 01:45:21 +03:00
agra
d6ef691e42 ffi M2.1(a): class-level constants 'name :: Type = expr;'
Inside a '#objc_class { ... }' block, 'name :: Type = expr;' is
accepted alongside the existing method form. Parsed as sugar for
'name :: () -> Type => expr;' — a niladic class method with an
expression body. The synthesized class method flows through the
M2.1(b) class-method pipeline: a C-ABI IMP is emitted and
registered on the metaclass.

Apple's runtime sees zero distinction — '[Cls foo]' dispatches to
our IMP regardless of source spelling. The constant form is
purely syntactic sugar; it reads better for static metadata
returns:

  SxGLView :: #objc_class("SxGLView") {
      layerClass :: Class = CAEAGLLayer.class();
  }

vs. the equivalent method form:

  layerClass :: () -> Class => CAEAGLLayer.class();

Parser change: after 'name ::' if the next token isn't '(' we
take the constant branch — parse a type expr, expect '=', parse
the value expr, expect ';'. The result is a ForeignMethodDecl
with is_static=true, empty params, return_type=Type, body=block
wrapping the expr. Pure parser-level transformation; no new AST
nodes, no new lowering passes.

150-objc-class-level-constant.sx exercises both shapes on macOS:
a primitive (s32 answer) and a pointer ('*NSObject seedClass'
— the canonical '+layerClass'-style factory return).

180 example tests pass (+1). zig build test green.

M2.1 complete: both (a) the constant form and (b) the
expression-bodied class method shape land.

Next: M2.2 — 'field: T #property(modifiers...)' synthesizes
getter/setter pairs.
2026-05-25 23:43:46 +03:00
agra
c39c8e15eb ffi M2.1(b): class methods on sx-defined #objc_class
Bodied methods without a '*Self' first param (parser marks
is_static=true) are now registered as Obj-C CLASS methods on
the metaclass.

Each such method gets:
- A synthesized FnDecl + body lowering through the existing
  M1.2 A.2 path.
- A C-ABI trampoline 'emitObjcDefinedClassStaticImp' — same
  shape as the instance trampoline but skips the __sx_state
  ivar read (no instance state) and passes only
  '__sx_default_context' (plus user args) to the sx body.
- An entry in ObjcDefinedMethodEntry with 'is_class=true'.

emit_llvm's class-pair init constructor now computes the
metaclass once up-front (via object_getClass(cls)) and shares
it between the +alloc IMP registration (M1.2 A.5) and the
M2.1(b) class-method registrations. The per-method registration
loop picks the target via 'method.is_class ? metaclass : cls'.

149-objc-class-method-static-imp.sx end-to-end on macOS:

  SxFoo :: #objc_class("SxFoo") {
      answer :: () -> s32 { return 42; }
  }

  // [SxFoo answer] via objc_msgSend → 42
  // class_getClassMethod(SxFoo, sel_answer) → non-null

Still TODO for M2.1: the (a) class-LEVEL constant form
'layerClass :: Class = CAEAGLLayer.class();' — needs parser
extension to recognize 'name :: Type = expr;' inside #objc_class
blocks, plus lazy-init-slot synthesis.

179 example tests pass (+1). zig build test green.
2026-05-25 23:40:51 +03:00
agra
0ac5ba2ccd ffi M1.3: obj.class accessor on Obj-C-class pointers
Adds a special case to lowerFieldAccess: when the field is
literally 'class' and the receiver is a pointer to an Obj-C
(or Obj-C protocol) foreign-class struct, emit
'object_getClass(obj)' instead of falling through to struct GEP.

Returns 'Class' (the M1.1 first-pass alias for *void;
parameterized Class(T) covariance is deferred to M1.1.b).

  f := SxFoo.alloc();
  cls := f.class;                       // → object_getClass(f)
  cls == objc_getClass("SxFoo".ptr);   // ok

New helper isObjcClassPointer(ty) detects 'ptr -> struct in
foreign_class_map under .objc_class / .objc_protocol'. The
check fires BEFORE the auto-deref so the runtime call sees the
opaque Obj-C pointer rather than the load'd struct stub.

148-objc-self-class-accessor.sx exercises both shapes end-to-end
against the macOS runtime: sx-defined class (SxFoo) and foreign
class (NSObject). Round-trips against objc_getClass(name).

178 example tests pass. zig build test green.

This effectively closes Month 1 — M1.0, M1.1 (first pass), M1.2,
M1.3 all done. Remaining: M1.1.b (Class(T) covariance +
instancetype), then Month 2 (declarative sugar).
2026-05-25 23:33:52 +03:00
agra
51277afadf ffi M1.2 A.7: open the dispatch gate — sx-defined class methods callable
Delete the bail at lower.zig:4407 that diagnosed sx-defined Obj-C
class dispatch as 'not yet supported'. Both foreign and
sx-defined '#objc_class' decls now flow through the same
'lowerObjcMethodCall' path — instance methods on sx-defined
classes dispatch via objc_msgSend, and the registered IMP
trampolines (M1.2 A.4b.iii) route to the sx bodies.

The runtime non-Obj-C branch (.swift_class / .swift_struct /
.swift_protocol) keeps its 'not yet supported' diagnostic;
M1.2 only addresses the Obj-C runtimes.

Constructor reorder in emit_llvm: emitObjcDefinedClassInit
runs BEFORE emitObjcClassInit. Otherwise the Phase 3.1
class-cache populator calls objc_getClass("SxFoo") before our
constructor registers the class — cache slot stored null and
'SxFoo.method()' dispatched against a null class pointer.

ffi-objc-defined-class-01-instance.sx (the integration test
from the plan) now runs the full lifecycle on macOS:

  f := SxFoo.alloc()    // synthesized +alloc IMP fires
  f.bump()              // dispatch → IMP trampoline → sx body
  f.bump()              // state persists across calls
  f.bump()
  f.get()               // → 3
  release_fn(f, sel_release)  // synthesized -dealloc fires

The user declares 'alloc :: () -> *SxFoo;' bodyless to give the
synthesized +alloc IMP a typed contract at sx call sites —
same convention as foreign classes today.

M1.2 complete: A.0 A.1 A.2 A.3 A.4 A.4b.i A.4b.ii A.4b.iii
A.5 A.6 A.7. End-to-end class-synthesis foundation works.

177 example tests pass (+1 from the integration test). zig
build test green.
2026-05-25 23:29:55 +03:00
agra
c107aa4e21 ffi M1.2 A.6: synthesized -dealloc IMP + [super dealloc] chain
For every sx-defined #objc_class, emit a C-callconv -dealloc IMP
that runs at refcount-zero. Frees the sx state struct, nils the
ivar, then chains to [super dealloc] so NSObject's runtime
cleanup (object_dispose, associated-object teardown, KVO, etc.)
runs as usual.

  -dealloc IMP (self: id, _cmd: SEL) -> void
      state = object_getIvar(self, load @__<Cls>_state_ivar)
      free(state)                              // free(NULL) is safe
      object_setIvar(self, ivar, NULL)
      sup = alloca { receiver: *void, super_class: *void }
      sup.receiver    = self
      sup.super_class = load @__<Cls>_class
      sel_dealloc = sel_registerName("dealloc")
      objc_msgSendSuper2(&sup, sel_dealloc)
      return

Two new per-class globals:
- '__<Cls>_class' : *void — populated by emit_llvm's
  class-pair init constructor with the freshly-allocated Class
  pointer (after objc_registerClassPair).
- The existing '__<Cls>_state_ivar' is also consulted to find
  the state struct.

The -dealloc IMP is registered on the class itself (instance
method) via class_addMethod with encoding 'v@:'. emit_llvm
ALSO stores cls_val into '__<Cls>_class' so the trampoline
can build the objc_super struct.

internStringConstantGlobal helper added to lower.zig — interns
C strings as [N:0]u8 globals with byte-level aggregate inits.
Used here for the 'dealloc' selector string.

147-objc-class-dealloc-roundtrip.sx verifies end-to-end on
macOS: alloc + release fires the IMP, and a second alloc/release
cycle proves runtime state isn't corrupted. class_getMethod-
Implementation confirms the IMP is registered.

176 example tests pass (+1). zig build test green.

Still gated: sx-side 'obj.method()' calls bail at lower.zig:4407
with the existing diagnostic. A.7 opens the gate — last sub-step
of M1.2.
2026-05-25 23:25:13 +03:00
agra
a1736f3213 ffi M1.2 A.5: synthesized +alloc IMP + ensureCRuntimeDecl helper
For every sx-defined #objc_class, emit a C-callconv +alloc IMP
that the Obj-C runtime calls when '[Cls alloc]' fires (from sx
code, UIKit instantiation, Info.plist principal class, etc.):

  +alloc IMP (cls: Class, _cmd: SEL) -> id
      instance = class_createInstance(cls, 0)
      state    = malloc(STATE_SIZE)
      memset(state, 0, STATE_SIZE)
      object_setIvar(instance, load(@__<Cls>_state_ivar), state)
      return instance

STATE_SIZE = max(typeSizeBytes(state struct), 1) — always at
least one byte so the ivar is never null after +alloc returns.

The IMP is registered on the METACLASS (class methods live there
— every Class object's isa points to the metaclass) in emit_llvm's
class-pair init constructor:

  metaclass = object_getClass(cls)
  sel_alloc = sel_registerName("alloc")
  class_addMethod(metaclass, sel_alloc, alloc_imp, "@@:")

That override wins over NSObject's default +alloc; runtime
instantiations get the __sx_state ivar bound automatically.

Per-instance allocator binding (the plan's full design — store
the Allocator value in the state struct so -dealloc frees through
the same one) is deferred. libc malloc/free is fine for v1; we'll
upgrade once Month 4's autoreleasepool + ARC ops shake out.

REFACTOR: collapsed five duplicate 'get<Name>Fid' helpers and
their cache fields (object_getIvar, object_setIvar,
class_createInstance, malloc, memset) into a single
'ensureCRuntimeDecl(name, params, ret) -> FuncId'. The helper
checks for an existing decl by name first (avoids the
'class_createInstance.1' duplicate-symbol crash when stdlib's
'#foreign' decl is already in the module). One helper instead
of one-per-function = ~150 lines deleted.

object_getIvar / object_setIvar added to stdlib std/objc.sx
so user code can use them too (146 exercises object_getIvar
to verify __sx_state was bound to a non-null state pointer
after +alloc).

146-objc-class-alloc-roundtrip.sx end-to-end against macOS:
'[SxFoo alloc]' returns non-null AND object_getIvar(instance,
__sx_state) returns the state ptr. Real Obj-C runtime, no
mocks.

175 example tests pass (+1). zig build test green.
2026-05-25 23:17:30 +03:00
agra
87572579b4 ffi M1.2 A.4b.iii: class_addMethod wires IMPs to the Obj-C runtime
For each instance method on a sx-defined '#objc_class', the
class-pair init constructor now:

  sel = sel_registerName("selector_string")
  imp = @__<Cls>_<method>_imp                  (M1.2 A.4b.ii)
  class_addMethod(cls, sel, imp, "<encoding>")

before objc_registerClassPair. The IMP trampoline (A.4b.ii)
already bridges C-ABI -> sx body. With registration in place,
'objc_msgSend(obj, sel_bump)' now routes to the trampoline,
which reads __sx_state ivar and forwards to '@<Cls>.<method>'.

To get selector + type-encoding strings out of lower.zig and
into emit_llvm, ObjcDefinedClassEntry gains a 'methods' slice:

  pub const ObjcDefinedMethodEntry = struct {
      sel: []const u8,       // mangled selector (M1.2 A.1's deriveObjcSelector)
      encoding: []const u8,  // type encoding (M1.2 A.1's objcTypeEncodingFromSignature)
      imp_name: []const u8,  // C-callconv trampoline symbol
  };

registerObjcDefinedClassMethods populates this when it declares
each method's body function; Module.setObjcDefinedClassMethods
attaches the slice to the cache entry by name. Static (class-
side) methods are skipped — A.4b only covers instance methods;
class-method hooks like '+layerClass' land in M2.1.

emit_llvm reads entry.methods and emits class_addMethod inside
the per-class init block, before objc_registerClassPair (the
runtime locks the method list at register time on some SDK
versions).

145-objc-class-method-dispatch.sx verifies end-to-end:
class_getMethodImplementation(SxFoo, sel_registerName("bump"))
returns non-null after main starts. Both niladic ('bump') and
single-arg ('add:') selectors checked.

Still gated (A.7): sx-side 'obj.bump()' calls. The dispatch
gate at lower.zig:4407 hasn't opened — A.5 (+alloc) and A.6
(-dealloc) need to land first so the integration test
ffi-objc-defined-class-01-instance.sx (full state round-trip)
can exercise the full lifecycle.

174 example tests pass (+1 from 145). zig build test green.
2026-05-25 22:58:20 +03:00
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
agra
ae1072d415 ffi M1.2 A.2b: register sx-defined #objc_class methods + *Self substitution
Bodied instance methods on a sx-defined '#objc_class("Cls") { ... }'
declaration are now registered in fn_ast_map under '<Cls>.<method>'
and declared in the IR with their *Self params substituted to
the hidden state-struct type (M1.2 A.2a).

registerObjcDefinedClassMethods walks the foreign_class_decl's
members, synthesizes an FnDecl from each ForeignMethodDecl (zipping
params + param_names), and feeds it through declareFunction with
current_foreign_class temporarily pinned so resolveTypeWithBindings
substitutes Self → __SxFooState.

resolveTypeWithBindings now treats type_expr 'Self' as a contextual
alias: when current_foreign_class points to a sx-defined Obj-C
class, the substitution returns objcDefinedStateStructType(fcd).
Other Self contexts (protocols, JNI super, foreign-class member
type resolution) are untouched — the check filters on (!is_foreign
and runtime == .objc_class).

lowerFunction also sets current_foreign_class for the duration of
the body lowering when the name is qualified <Cls>.<method> and
Cls is in objc_defined_class_cache. Save+restore via defer so
nested calls round-trip cleanly.

Verification (manual): 'sx ir' on an sx-defined class shows
'declare void @SxFoo.bump(ptr, ptr)' — two args = implicit
__sx_ctx + the state-struct pointer (correct *Self substitution).
Body emission happens lazily; A.2c will trigger it eagerly so
the IMP trampoline (A.4) can reference it.

170 example tests + zig build test green.
2026-05-25 21:59:23 +03:00
agra
7b98b3ae78 ffi M1.2 A.2a: objcDefinedStateStructType helper
Builds (and interns) the hidden sx-state struct type for an
sx-defined '#objc_class'. Layout:

    __<ClassName>State {
        user_field_0,
        user_field_1,
        ...
    }

This struct is what the runtime's '__sx_state' ivar points at —
separate from the Obj-C object itself, which stays opaque. The
sx method bodies will operate on '*__SxFooState' (after '*Self'
substitution in A.2b) so 'self.field' resolves to a plain struct
field access — A.3's 'free if types align' premise.

M1.2 A.5 will prepend '__sx_allocator: Allocator' so dealloc can
free through the per-instance allocator. Field-by-name access
stays correct across the future repositioning.

Methods / '#extends' / '#implements' members are ignored — only
'.field' contributes. Three unit tests pin: typical-field case,
empty-class case, mixed-member case.

Dead code at this commit — helper isn't called yet. A.2b (body
lowering with '*Self' substitution) wires it in. 170 example
tests + zig build test green.
2026-05-25 21:51:07 +03:00
agra
6cc016cd4f ffi M1.2 A.1: objcTypeEncodingFromSignature helper + encoding table
Derives Apple's runtime type-encoding string from an IR method
signature. Called by class_addMethod(cls, sel, imp, types) when
M1.2 A.4+ synthesise IMPs for sx-defined classes.

Layout: <ret> @ : <param0> <param1> ...   — @ is the receiver,
: is _cmd. Caller passes user-declared params AFTER stripping
'self: *Self'.

Encoding table:
  v=void  B=bool  c=s8/BOOL  s=s16  i=s32  q=s64
  C=u8    S=u16   I=u32      Q=u64  f=f32  d=f64
  @=foreign Obj-C class ptr        #=Class  :=SEL
  *=[*]u8 (C string)               ^v=any other ptr

bool (sx i1) maps to 'B' (C99 _Bool); s8 to 'c' (Apple's BOOL).
Foreign-class pointers detected via foreign_class_map lookup on
the pointee struct name. Other pointers fall to ^v — encoding is
metadata, not ABI, so conservative is safe.

Struct / slice / closure / etc. BAIL via diagnostic
(ObjcEncodingUnsupported) rather than silently mis-encoding, per
CLAUDE.md rejected-patterns rule. Future passes will widen the
table as new shapes show up in real IMPs.

Dead code at this commit — helper isn't called yet. Three unit
tests in src/ir/lower.test.zig pin the primitive / pointer /
Obj-C-class-pointer encodings before A.2 wires the helper in.

170 example tests + zig build test green.
2026-05-25 21:43:53 +03:00
agra
61a2593020 ffi M1.2 A.0: objc_defined_class_cache + scan-pass registration
Adds an insertion-ordered cache on Module for sx-defined Obj-C
classes — every '#objc_class("Cls") { ... }' declaration WITHOUT
'#foreign'. registerForeignClassDecl appends the entry alongside
its existing foreign_class_map insert; lookup helper available
via Module.lookupObjcDefinedClass.

  ObjcDefinedClassEntry { name, *const ast.ForeignClassDecl }

The pointer back into the AST lets later passes (M1.2 A.1+) walk
'members' for fields / methods / '#extends' / '#implements'
without duplicating that data on the entry. Insertion order
matters because class-pair init constructors (A.4) must register
parent classes before children — 'objc_allocateClassPair(super,
...)' resolves super by lookup.

Infrastructure only — no observable behavior change. The cache
is populated but not yet read; A.1+ start pulling from it. 170
example tests + zig build test green.
2026-05-25 21:37:36 +03:00
agra
d9dbdad3f5 ffi M1.1 (first pass): id / Class / SEL / BOOL type aliases
Adds named stand-ins for the three opaque Obj-C runtime types
and Apple's signed-char boolean to library/modules/std/objc.sx:

  id    :: *void;   // any Obj-C instance pointer
  Class :: *void;   // a class object pointer
  SEL   :: *void;   // a registered selector
  BOOL  :: s8;      // Apple's signed-char boolean (NOT sx's bool)

All resolve to their underlying type at the LLVM layer — no
runtime cost — but make foreign-class declarations read closer
to Objective-C source. The header's old caveat about lacking
type aliases is gone.

141-objc-type-aliases.sx exercises the aliases against the real
macOS Obj-C runtime: alloc/init an NSObject, fetch its class
via objc_getClass, sel_registerName a SEL, then call
'isKindOfClass:' returning BOOL=1. Non-macOS paths print the
same line to keep the snapshot stable.

DEFERRED (M1.1.b, follow-up): 'Class(T)' parameterization with
#extends-aware covariance, and 'instancetype' per-decl
substitution. Both require compiler-level type-check support
beyond plain stdlib aliases.

170 examples pass (+1).
2026-05-25 21:33:20 +03:00
agra
86c1127c46 ffi M1.0 (3/3): accept '=>' body in '#objc_class' member methods
Extends parseForeignClassDecl ([src/parser.zig:1262]) with an
arrow arm that mirrors the existing parseFnDecl shape — single-
expression body wrapped in a one-statement block so downstream
lowering sees the same AST as a brace-body method.

Closes the M1.0 surface: '=> expr;' is now valid for top-level
functions, struct methods, AND '#objc_class' member methods.
The sx-defined class lowering (A.7 dispatch gate) is still gated,
so 140-expression-bodied-objc-method.sx exercises parse-only —
the body is reachable but the method is never invoked. When M1.2
lights up sx-defined class instantiation, the arrow-body form
will flow through unchanged.

169 examples pass (+1 from 140 now green); zig build test green.
2026-05-25 21:18:09 +03:00
agra
4a048d34fd ffi M1.0 (2/3, xfail): '=>' body inside '#objc_class' member
parseForeignClassDecl ([src/parser.zig:1262]) accepts ';'
(declaration) or '{ ... }' (block body) but not '=>' for member
methods. The arrow form, which parseFnDecl ([src/parser.zig:1647])
already handles for top-level/struct decls (M1.0 1/3), surfaces
'expected ;' at the arrow today.

Snapshot pins that error so the next commit (the parser
extension) shows up as a single diagnostic→runtime-output diff
in 140-expression-bodied-objc-method.{txt,exit}.
2026-05-25 21:16:32 +03:00
agra
6c95b2ae72 ffi M1.0 (1/3): lock in expression-bodied top-level + struct-method form
sx's '=>' body form (already used for lambdas) works today for
top-level function declarations and struct member methods. Pin
the surface with examples/139-expression-bodied-fn.sx so a
parser regression here surfaces immediately.

Coverage:
- module-top:      double :: (x: s32) -> s32 => x * 2;
- niladic:         answer :: () -> s32 => 42;
- struct method:   total :: (self: *Point) -> s32 => self.x + self.y;

Next: extend the same form to '#objc_class' member methods (the
M2.1(a/b) class-constant + class-method overrides path).
2026-05-25 21:15:44 +03:00
agra
bd3033dc5a ffi 3.2 C5: migrate uikit.sx view tree + GL drawables cluster
Final Phase 3.2 cluster. CALayer / CAEAGLLayer / EAGLContext declared
as `#foreign #objc_class` blocks, plus `setContentScaleFactor` added
to UIView and `-layer` now returns `*CALayer` (was opaque `*void`).

Classes declared:

- CALayer      → setOpaque (instance)
- CAEAGLLayer  → setDrawableProperties (instance)
- EAGLContext  → alloc (class), initWithAPI (instance),
                 setCurrentContext (class — takes EAGLContext arg),
                 renderbufferStorage_fromDrawable (instance),
                 presentRenderbuffer (instance)
- UIView       → +setContentScaleFactor (existing decl extended)

The C5 group sits above UIView in the file so the `-layer` return type
`*CALayer` forward-resolves cleanly.

Migration sites in uikit.sx:

- `uikit_create_gl_context` → `EAGLContext.alloc().initWithAPI(api)`
  + `EAGLContext.setCurrentContext(ctx)`.
- `uikit_setup_renderbuffer` → cast `*EAGLContext` and
  `gl_ctx.renderbufferStorage_fromDrawable(target, layer)`.
- `uikit_present_renderbuffer` → same cast + `presentRenderbuffer(target)`.
- Scene-connect bring-up: `gl_layer.setOpaque(1)`,
  `eagl_layer.setDrawableProperties(...)`,
  `gl_view.setContentScaleFactor(scale)`.

One more `objc_getClass(...)` lookup (EAGLContext) retired — the class
slot comes from the declarative binding via `__sx_objc_class_init`.

**Phase 3.2 complete.** Five clusters migrated (C1: Foundation
utility; C2: Notifications + Bundle; C3: RunLoop + display timing;
C4: UIKit chrome; C5: view tree + GL drawables). 8 foreign Cocoa
classes declared; ~30 `#objc_call(T)(...)` sites rewritten to
`recv.method(args)` / `Cls.method(args)`; 6 `objc_getClass`
lookups retired. Sx-defined classes (SxAppDelegate, SxSceneDelegate,
SxGLView, SxMetalView) and a handful of foreign sites that exercise
delegate/protocol surfaces stay on the explicit `#objc_call` form
pending Phase 3.7's class synthesis.

167/167 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
2026-05-25 17:58:55 +03:00
agra
5b4969f9be ffi 3.2 C4: migrate uikit.sx UIKit chrome cluster to #objc_class
Fourth cluster — was blocked on issue-0043, now unblocked by the
preceding `Self`-substitution fix.

Classes declared:

- UIScreen         → mainScreen (class), nativeScale + bounds (instance)
- UIView           → safeAreaInsets, addSubview, layer (all instance)
- UIWindow         → alloc (class), initWithWindowScene, setRootViewController,
                     makeKeyAndVisible, screen (instance)
- UIViewController → alloc (class), init, setView (instance)
- UITextField      → alloc (class), init, becomeFirstResponder,
                     resignFirstResponder (instance)

Migration sites in uikit.sx:

- `show_keyboard` / `hide_keyboard` → `tf.becomeFirstResponder()` /
  `tf.resignFirstResponder()` on a `*UITextField` cast of `text_field`.
- `uikit_refresh_safe_insets` → `gl_view.safeAreaInsets()` on a
  `*UIView` cast of `plat.gl_view`.
- `uikit_read_screen_scale` and GL-context bring-up →
  `UIScreen.mainScreen().nativeScale()`.
- `uikit_keyboard_will_change_frame` → `win.screen().bounds()`.
- `uikit_scene_will_connect_ios` (the function that triggered 0043) →
  `UIWindow.alloc().initWithWindowScene(scene)`,
  `UIViewController.alloc().init()`, `vc.setView(...)`,
  `win.setRootViewController(...)`, `gl_view.layer()`,
  `UITextField.alloc().init()`, `gl_view.addSubview(...)`,
  `win.makeKeyAndVisible()`.

Three `objc_getClass(...)` lookups (UIWindow, UIViewController,
UITextField) are gone — the class slots come from the declarative
bindings via `__sx_objc_class_init`. UIScreen has the same shape.

167/167 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
2026-05-25 17:53:11 +03:00
agra
2b717d9b38 ffi: resolve foreign-class member types through Self substitution (issue-0043)
`inferExprType` for a chained call `Cls.static().instance(...)` never
looked the inner call's foreign-class declaration up, so the outer
dispatch saw a `.s64` receiver, the `foreign_class_map.get(...)` lookup
missed, and lowering emitted `error: unresolved 'method'`. The macOS
target appeared to work because `inline if OS == .ios { ... }` strips
the gated body before lowering — eliding every call that would have
exercised the broken path.

The "lazy-lower" framing in the original issue file was a red herring.

Fix in `src/ir/lower.zig`:

1. `inferExprType` for `.call` with `.field_access` callee now checks
   `foreign_class_map` for both shapes — `Cls.static_method(args)` (object
   identifier matches a foreign-class alias, look up static members) and
   `inst.instance_method(args)` (receiver is a pointer to a foreign-class
   struct, look up non-static members).
2. New helpers `resolveForeignMethodReturnType` and
   `resolveForeignClassMemberType` substitute `*Self` / `Self` to the
   foreign-class struct so a `*Self` return doesn't synthesize a phantom
   `Self`-named struct that future dispatches can't resolve.
3. The Obj-C lowering paths (`lowerObjcMethodCall`, `lowerObjcStaticCall`)
   route through the same helper for `ret_ty` so the IR Ref's type matches
   what `inferExprType` reports.

Regression test at `examples/138-foreign-class-chained-dispatch.sx`
exercises NSObject's `+alloc` / `-init` chain in both shapes —
`*NSObject` return then `*Self` return, and `*Self` then `*Self`. Runs
on the host (macOS) for live exercise; non-macOS hosts fall through to
a stub matching the expected output.

This unblocks Phase 3.2 C4/C5 — the `UIWindow.alloc().initWithWindowScene(scene)`
pattern that surfaced the bug is the cluster's bread-and-butter shape.

167/167 example tests; chess builds clean on macOS, iOS-sim, Android.
2026-05-25 17:52:53 +03:00
agra
15f10c5031 ffi 3.2 C4/C5 BLOCKED: file issue-0043 — lazy lowering loses foreign-class dispatch
Per CLAUDE.md IMPASSIBLE RULES. Attempted Phase 3.2 C4 migration of
the UIKit chrome cluster in `library/modules/platform/uikit.sx`
(UIScreen / UIWindow / UIViewController / UITextField / UIView)
surfaced a real compiler bug: when a function body contains
`recv.method(...)` calls against an `#objc_class` receiver AND that
body is reached via `lazyLowerFunction` invoked from another
`inline if OS == ...` branch, the method dispatch fails with
"unresolved 'methodName'".

Specifically: `uikit_scene_will_connect_ios` (the iOS-sim crashing
case) contains `UIWindow.alloc().initWithWindowScene(scene)` etc.
The same calls compile cleanly in isolated probes — only the lazy-
lower-via-inline-if entry chain reproduces the bug. macOS target
builds fine throughout; ios-sim trips it.

C1/C2/C3 (commits 1ea9cda / 17775b2 / 2a7c8e0) happen to land cleanly
because the methods they migrate are reached eagerly (or are niladic
so the dispatch path doesn't hit the failing branch). C4 + C5 stay
blocked pending issue-0043's fix in a separate session.

Issue filed at `issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md`
with the reproduction, stack trace, and investigation prompt
pointing at `lower.zig:1057` (`lazyLowerFunction`) and
`lower.zig:5290` (the field-access foreign-class dispatch chain).

FFI checkpoint updated to mark C4+C5 as BLOCKED on 0043.

The in-progress C4 working-tree changes were reverted; tree is at
the C3 commit `2a7c8e0` and chess on macOS/iOS-sim/Android builds
cleanly.
2026-05-25 17:27:29 +03:00
agra
2a7c8e0a6f ffi 3.2 C3: migrate uikit.sx RunLoop + display-timing cluster
Third cluster: NSRunLoop and CADisplayLink move to declarative
`#objc_class` blocks.

Classes declared:

- NSRunLoop     → currentRunLoop (class)
- CADisplayLink → displayLinkWithTarget_selector (class),
                  addToRunLoop_forMode (instance),
                  targetTimestamp (instance), duration (instance)

The display-link instance is created with the new typed call shape:

    link := CADisplayLink.displayLinkWithTarget_selector(plat.gl_view, sel_tick);
    plat.display_link = xx link;  // keep the *void slot in the
                                   // platform struct for ABI parity
    runloop := NSRunLoop.currentRunLoop();
    link.addToRunLoop_forMode(runloop, mode_ns);

The `sxTick:` callback's `link: *void` param is cast to
`*CADisplayLink` at function entry so the body's `link.duration()` /
`link.targetTimestamp()` calls type-check.

Two now-redundant `objc_getClass(...)` lookups for CADisplayLink /
NSRunLoop are gone — the class slots come from the declarative
declarations via `__sx_objc_class_init`.

166/166 tests; chess builds clean on macOS / iOS / Android.
2026-05-25 17:15:27 +03:00
agra
17775b27a4 ffi 3.2 C2: migrate uikit.sx Notifications + Bundle cluster
Second cluster: NSNotification, NSBundle, NSNotificationCenter move
from `#objc_call(T)(recv, "sel:", args)` to declarative
`#foreign #objc_class("Cls") { ... }` blocks.

Classes declared (alongside the C1 Foundation utility group):

- NSNotification        → userInfo (instance, returns *NSDictionary)
- NSBundle              → mainBundle (class), resourcePath (instance)
- NSNotificationCenter  → defaultCenter (class),
                          addObserver_selector_name_object (instance)

The 4-keyword `addObserver:selector:name:object:` selector derives
from the underscore-separated sx name via the default mangling rule
— no `#selector` override needed.

Cleanup wins:

- `objc_getClass("NSBundle")` and `objc_getClass("NSNotificationCenter")`
  call sites gone — class slots now populated by emit_llvm's
  `__sx_objc_class_init` constructor.
- `userInfo`'s return type is `*NSDictionary` directly, so the
  previous `*void → *NSDictionary` cast at the keyboard-frame
  callsite collapses.

166/166 tests; chess builds clean on macOS + iOS + Android via
`sx build main.sx`.
2026-05-25 17:12:33 +03:00
agra
1ea9cda12b ffi 3.2 C1: migrate uikit.sx Foundation utility cluster to #objc_class
First of five Phase-3.2 migration clusters. Foundation utility
classes (NSValue, NSNumber, NSDictionary, NSMutableDictionary, NSSet)
in `library/modules/platform/uikit.sx` move from the explicit
`#objc_call(T)(recv, "selector:", args)` form to declarative
`#foreign #objc_class("Cls") { ... }` blocks with `recv.method(args)`
dispatch.

Classes declared (all near the top of uikit.sx, after the CGRect
struct):

- NSValue          → CGRectValue (instance)
- NSNumber         → numberWithBool (class), doubleValue +
                     unsignedLongValue (instance)
- NSDictionary     → objectForKey (instance)
- NSMutableDictionary → dictionary (class), setObject_forKey (instance)
- NSSet            → anyObject (instance)

Each call site casts the `*void` receiver to the typed foreign-class
pointer before dispatch — the existing `*void` shape is preserved
in the struct fields and outer parameter types; only the dispatch-
local copy is typed. This keeps the diff scoped to call-site
rewrites without rippling type changes through every consumer.

The four `objc_getClass(...)` calls that previously resolved
NSMutableDictionary / NSNumber at runtime are gone — Phase 3.1's
`__sx_objc_class_init` constructor populates the cached class slot
for each declared `#objc_class` at module load via
`OBJC_CLASSLIST_REFERENCES_<Cls>`.

166/166 example tests; chess clean on macOS + Android via
`tools/verify-step.sh` (iOS sim skipped — no booted simulator in
this run; previous full run was green at HEAD~6).
2026-05-25 17:10:23 +03:00
agra
a32cc2dc27 ffi 3.2 B: locked-in golden test for the Obj-C selector mangling table
`examples/ffi-objc-dsl-07-mangling-table.sx` exercises every common
mangling shape in one fixture and pins the resolved selectors via
both `.txt` and `.ir` snapshots:

| sx method                          | derived selector            |
|-----------------------------------|----------------------------|
| `length`                           | `length`                    |
| `addObject(o)`                     | `addObject:`                |
| `combine_and(a, b)`                | `combine:and:`              |
| `insert_after_index(a, b, c)`      | `insert:after:index:`       |
| `add_observer_for_event(a, b, c, d)` | `add:observer:for:event:`   |
| `initWithFrame_options(f, o)`      | `initWithFrame:options:`    |
| `custom_name #selector("actualSelectorName")` | `actualSelectorName` |

The class is synthesized at runtime via `objc_allocateClassPair` +
`class_addMethod` per selector (mirrors the pattern in
`ffi-objc-dsl-{01..05}.sx`), so the test actually dispatches through
the real Obj-C runtime on macOS.

Single commit because the implementation already shipped in 3.0/3.2;
this is a new regression that locks in current behavior, not a
test-then-make-green pair.

The `.ir` snapshot opts in via the existing run_examples.sh mechanism
(presence of a `.ir` file for the same name triggers capture). The
captured `OBJC_METH_VAR_NAME_*` constants surface every selector
string change at a glance.

166/166 tests.
2026-05-25 17:03:16 +03:00