Commit Graph

13 Commits

Author SHA1 Message Date
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
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
agra
572ab12142 ffi 3.2 A2: implement #selector("explicit:string") override
Make-green half of the cadence step started in A1. Wires the
`#selector` directive end-to-end:

- Lexer token `hash_selector` at src/token.zig + lookup row in
  src/lexer.zig.
- AST field `selector_override: ?[]const u8 = null` on
  `ForeignMethodDecl` (src/ast.zig).
- Parser block in src/parser.zig that mirrors
  `#jni_method_descriptor` — both occupy the same slot after the
  optional `-> ReturnType` and before the body/terminator. Not
  mutually exclusive at parse time.
- LSP semantic-token list (src/lsp/server.zig) updated.
- Lowering: `deriveObjcSelector` returns
  `{ sel, keyword_count, is_override }`. When `is_override` is true,
  the selector string is the user's literal and `keyword_count` is
  the colon count in that literal. Both `lowerObjcMethodCall` and
  `lowerObjcStaticCall` use the result.

Diagnostic policy when override colon-count ≠ call arity:

- Default mangling path: stays an error (`.err`). The user can fix
  the sx-side name to produce the right keyword count.
- Override path: downgrades to a warning (`.warn`). Rationale:
  Obj-C's `objc_msgSend` doesn't validate colon-vs-arg the way JNI's
  `GetMethodID` validates the descriptor — the runtime dispatches
  regardless and the wrong-arity case becomes silent calling-
  convention corruption. The compiler is the last line of defense
  for this typo class, but the warning preserves the override's
  escape-hatch character (deliberate mismatches still proceed).

Snapshot for `examples/ffi-objc-dsl-06-selector-override.sx` flips
from the pre-3.2 parser-error to working output:

  static override non-null: true

The mismatch diagnostic text in
`examples/ffi-objc-dsl-04-mismatch.sx`'s snapshot is updated to
drop the "once that lands (3.2)" phrasing now that 3.2 is here.

165/165 example tests.
2026-05-25 17:00:23 +03:00
agra
a908ecf28f ffi 3.2 A1 (xfail): add #selector("...") override regression test
Phase 3.2 xfail half. `#selector("explicit:string")` is the escape
hatch for cases where the sx-side method name doesn't conveniently
produce the target selector under the default mangling rule
(Phase 3.0 — split on `_`, each piece becomes a keyword with a
trailing `:`).

Surface form mirrors `#jni_method_descriptor("(Sig)Ret")` — sits
after the optional `-> ReturnType` and before the method body /
terminator.

Test fixture covers both lowering paths:

- Static method override: `NSObject.gimme()` with override
  "description" — exercises lowerObjcStaticCall (Phase 3.1).
- Instance method override: `NSDictionary.lookup(self, key)` with
  override "objectForKey:" — declared (parse + AST + lowering
  wiring) but not invoked at runtime (no real NSDictionary in
  scope). The declaration alone locks in the multi-arg-override path.

Pre-3.2: parser doesn't know `#selector`; snapshot captures
"expected ';'" at the override site, exit=1. Next commit (A2) wires
the lexer token, AST field, parser block, and lowering integration;
snapshot flips to working output.

165/165 example tests. Plan at
`~/.claude/plans/lets-see-options-for-merry-dijkstra.md`.
2026-05-25 16:55:32 +03:00
agra
56414407fc ffi: drop static keyword on foreign-class methods; param type discriminates
`static name :: ...` was redundant — instance methods always declare
`self: *Self` as their first param by convention. The parser now derives
`is_static` from the first param's TYPE: if it's `*Self` the method is
an instance method; anything else (including no params at all) is a
class method. Removes a token from the surface, keeps the dispatch
behavior identical.

The receiver param's NAME doesn't matter — only its type. Calling the
first param `this`, `me`, `receiver`, etc. is fine as long as the type
is `*Self`. This mirrors how the rest of sx handles receiver dispatch.

Migration of every site that used the keyword:

- `library/modules/platform/android.sx` — `SurfaceView.new(ctx)`.
- `examples/ffi-jni-class-03-static.sx` — `Math.abs(n)`.
- `examples/ffi-jni-main-03-ctor.sx` — `SurfaceView.new(ctx)` in the
  `#jni_main` body.
- `examples/ffi-objc-dsl-05-static.sx` — NSObject's `.class()` /
  `.description()`.

164/164 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
2026-05-25 16:32:32 +03:00
agra
8406cc1fed ffi 3.1: Cls.static_method(args) lowers to objc_msg_send on the class object
Implementation half of the Phase 3.1 cadence step.
`lowerForeignStaticCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcStaticCall` helper
that loads the class object from a module-scoped cached slot (populated
once per module via `objc_getClass`) and dispatches `objc_msg_send`
with the same selector-mangling as Phase 3.0's instance dispatch.

Three pieces:

1. `Module.objc_class_cache` — parallel to `objc_selector_cache`,
   insertion-ordered list of (class_name, slot_GlobalId) so the
   constructor that calls `objc_getClass` per slot at module load
   is deterministic. `lookupObjcClass` / `appendObjcClass` accessors.
2. `internObjcClassObject` in lower.zig — get-or-create a
   `OBJC_CLASSLIST_REFERENCES_<Cls>` global pointer; matches clang's
   naming convention. `lowerObjcStaticCall` reuses
   `deriveObjcSelector` from 3.0 for the selector, loads the class
   slot, and emits `objc_msg_send(class_obj, sel, args)`.
3. `emitObjcClassInit` in emit_llvm.zig — companion to
   `emitObjcSelectorInit`. Walks `objc_class_cache`, synthesizes a
   constructor `__sx_objc_class_init` that calls `objc_getClass(name)`
   per slot, registers in `@llvm.global_ctors` for AOT (extending the
   existing array if the selector init already created it), and
   injects a direct call into main's prelude after any prior init
   calls so the ORC JIT path runs it too.

Surface form is `.` (`NSObject.class()`) matching JNI's `Alias.new(...)`
convention rather than the plan's notional `::` — avoids extending the
parser for a new postfix operator with no other use case.

Test `examples/ffi-objc-dsl-05-static.sx` exercises NSObject's
`+class` and `+description` class methods via the new syntax, asserts
both return non-null. NSObject is always available at module-load,
unlike runtime-created test classes that wouldn't exist yet when
the class-init constructor runs.

164/164 tests; chess builds + runs clean on all three platforms.
2026-05-25 16:23:24 +03:00
agra
53fe73acda ffi 3.0: inst.method(args) DSL dispatch on #objc_class receivers
Implementation half of the cadence step started in the previous commit.
`lowerForeignMethodCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcMethodCall` helper
that derives the Obj-C selector from the sx method name and lowers to
`objc_msg_send` against the cached SEL slot (same intern path as
explicit `#objc_call`).

Default selector mangling (matches clang's keyword-method convention):
- Niladic (arity 0 excluding self): name verbatim. `length()` → "length".
- Arity ≥ 1: split the sx method name on `_`; each piece becomes a
  keyword with a trailing `:`. `addObject(o)` → "addObject:";
  `combine_and(a, b)` → "combine:and:";
  `initWithFrame_options(f, o)` → "initWithFrame:options:".

Arity validation: keyword count (pieces from the `_`-split) must equal
call-site arity excluding self. Mismatch diagnoses at the call site
with a hint pointing at the forthcoming `#selector("...")` override
(Phase 3.2) for selectors that don't fit the underscore-split rule.

Mangling helper `deriveObjcSelector` and dispatch helper
`lowerObjcMethodCall` sit alongside `lowerForeignMethodCall`. The
existing fall-through diagnostic for non-JNI/non-Obj-C runtimes
remains for Swift (Phase 4 territory).

Tests `examples/ffi-objc-dsl-{01-niladic,02-one-arg,03-multi-keyword,
04-mismatch}.sx` snapshots flip from the pre-3.0 bail diagnostic
(exit=1) to working output (exit=0 for cases 01-03) and the specific
keyword-count mismatch diagnostic for case 04. Each test follows the
established pattern from `ffi-objc-call-08-multi-keyword.sx`:
synthesize a class at runtime via `objc_allocateClassPair` /
`class_addMethod`, declare a matching `#objc_class`, invoke the DSL
form. 163/163 tests; chess unaffected (JNI dispatch path untouched).
2026-05-25 16:10:22 +03:00
agra
a593d150ca ffi 3.0 (xfail): add inst.method(args) DSL regression tests + correct checkpoint
The previous FFI checkpoint claimed Phase 3 step 3.0 ("`inst.method(args)`
on #objc_class receivers") had landed. It hadn't — `lowerForeignMethodCall`
in lower.zig:4353 still bails for any non-JNI runtime with the generic
"method calls on '{runtime}' runtime not yet supported (Phase 3/4)"
diagnostic, no commit introduced an Obj-C DSL dispatch path, and the
planned regression files weren't on disk.

This commit is the xfail half of the proper cadence (test-add then
make-green in separate commits):

- examples/ffi-objc-dsl-01-niladic.sx — `length()` → selector "length".
- examples/ffi-objc-dsl-02-one-arg.sx — `addObject(o)` → "addObject:".
- examples/ffi-objc-dsl-03-multi-keyword.sx — `combine_and(a, b)` →
  "combine:and:" (sx name split on `_`, each piece becomes a keyword
  with a trailing `:`).
- examples/ffi-objc-dsl-04-mismatch.sx — `something_extra(x)` —
  keyword count (2) ≠ arity (1); must diagnose at the call site.

Each test follows the same pattern as `ffi-objc-call-08-multi-keyword.sx`:
synthesize a class at runtime via `objc_allocateClassPair` /
`class_addMethod`, declare the sx-side `#objc_class` against the same
name, then invoke the DSL form. Skips with a "(not macos)" line on
non-macOS hosts. Snapshots currently lock in the bail diagnostic with
exit=1; the next commit implements the dispatch and the snapshots
flip to the working output (and exit=0).

Checkpoint corrected to flag the prior false claim and reposition 3.0
back at the top of the open list.
2026-05-25 16:07:19 +03:00