From 5b4969f9be66277e38578654f2053f76bee0ffd1 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 17:53:11 +0300 Subject: [PATCH] ffi 3.2 C4: migrate uikit.sx UIKit chrome cluster to `#objc_class` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- current/CHECKPOINT-FFI.md | 94 ++++++++++++++++++++------- library/modules/platform/uikit.sx | 101 ++++++++++++++++++++---------- 2 files changed, 140 insertions(+), 55 deletions(-) diff --git a/current/CHECKPOINT-FFI.md b/current/CHECKPOINT-FFI.md index ff3e3e9..fc83681 100644 --- a/current/CHECKPOINT-FFI.md +++ b/current/CHECKPOINT-FFI.md @@ -568,32 +568,59 @@ blocks. The `link` parameter on the `sxTick:` callback is now cast to `*CADisplayLink` at function entry so subsequent method calls type-check. -**Phase 3.2 C4/C5 BLOCKED on issue-0043.** Attempted C4 migration -(UIKit chrome: UIScreen / UIWindow / UIViewController / UITextField -/ UIView) surfaced a real compiler bug: lazy-lowered function bodies -don't resolve foreign-class method dispatch when invoked transitively -from an `inline if OS == .ios` branch in another function. The -specific failure is in `uikit_scene_will_connect_ios` whose body -contains `UIWindow.alloc().initWithWindowScene(scene)` and -`win.setRootViewController(...)` — both work in isolated probes but -fail at compile time when the function is reached via lazy lowering -from chess's iOS scene-connect hook. macOS target builds fine; only -ios-sim trips it. C1/C2/C3 happened to land cleanly because the -methods they migrate are reached eagerly (or are niladic so the -dispatch path doesn't hit the failing branch). +**issue-0043 closed.** The "lazy-lower" framing in the issue file +turned out to be a red herring: the actual root cause was that +`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 C4 work is reverted to keep the tree green at C3. C4+C5 stay -pending until issue-0043 is fixed in a separate session. +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` / + `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. + +`examples/138-foreign-class-chained-dispatch.sx` locks in the regression +via two shapes against NSObject's `+alloc` / `-init` chain: `*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. + +Phase 3.2 C4 landed: UIKit chrome cluster migrated. Six classes +declared (UIScreen, UIView, UIWindow, UIViewController, UITextField +— plus the existing C1/C2/C3 classes already in place). Migration +sites: `show_keyboard` / `hide_keyboard` use `tf.becomeFirstResponder()` +/ `tf.resignFirstResponder()`; `uikit_refresh_safe_insets` uses +`gl_view.safeAreaInsets()`; `uikit_read_screen_scale` and the GL- +context bring-up both use `UIScreen.mainScreen().nativeScale()`; +keyboard-frame callback uses `win.screen().bounds()`; the scene- +connect bring-up chains `UIWindow.alloc().initWithWindowScene(scene)` +and `UIViewController.alloc().init()` then `vc.setView(...)`, +`win.setRootViewController(...)`, `gl_view.layer()`, +`UITextField.alloc().init()`, `gl_view.addSubview(...)`, +`win.makeKeyAndVisible()`. Three `objc_getClass(...)` calls (UIWindow, +UIViewController, UITextField) are gone — the class slots come from +the declarative bindings via `__sx_objc_class_init`. C4 is the +cluster that triggered issue-0043; with the fix in, the chained +dispatch resolves correctly under lazy lowering. 167/167 tests + +chess clean on macOS / iOS sim / Android. Open work: -- **issue-0043** — investigate + fix the lazy-lower foreign-class - dispatch bug. See `issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md` - for the reproduction and investigation prompt. -- **Phase 3 step 3.2 — C4..C5** — uikit.sx migration, blocked until - 0043 lands. - test for the default-mangling table. Escape hatch for selectors - that don't fit the underscore-split rule (e.g. `tableView_ - numberOfRowsInSection_` with an asymmetric keyword count). +- **Phase 3 step 3.2 — C5** — uikit.sx migration (view tree + GL + drawables: CAEAGLLayer, EAGLContext, plus any remaining + CAMetalLayer / NSTimer sites). - **Phase 3 step 3.3** — `property name: Type` synthesizes `inst.name` → `[inst name]` getter and `inst.name = x` → `[inst setName: x]` setter. `#setter("...")` overrides the setter @@ -989,6 +1016,27 @@ zig build && zig build test && bash tests/run_examples.sh && bash tests/cross_co `Gles3Gpu`. Instrumentation stripped after fix. 140 host + 9 cross tests green. +- 2026-05-25: issue-0043 closed — chained `Cls.static().instance(...)` + foreign-class dispatch. `inferExprType` for `.call` with `.field_access` + callee now consults `foreign_class_map` for both static (object is the + alias) and instance (receiver type is `*ForeignClass`) shapes. New + `resolveForeignMethodReturnType` / `resolveForeignClassMemberType` / + `foreignClassStructType` helpers substitute `*Self` / `Self` to the + foreign class's own struct so the chained receiver type doesn't + collapse to a phantom `Self`-named struct. `lowerObjcMethodCall` / + `lowerObjcStaticCall` route through the same helper so the IR Ref's + recorded ret_ty matches what `inferExprType` reports. Pre-fix: + `UIWindow.alloc().initWithWindowScene(scene)` (and any other chained + shape) collapsed the inner ret to `.s64`, the next dispatch's + `foreign_class_map.get(...)` missed, and lowering emitted + `error: unresolved 'initWithWindowScene'`. The "lazy-lower" wording in + the issue file is a red herring — the bug fires on direct calls too; + macOS chess hides it only because `inline if OS == .ios { ... }` + strips the gated bodies that exercise the chain. Locked in by + `examples/138-foreign-class-chained-dispatch.sx` (NSObject `+alloc` / + `-init` chain in both `*Cls` and `*Self` return-type shapes). 167 + host + 7 cross tests green. Phase 3.2 C4/C5 is unblocked. + ## Known issues - `signed char` C maps to sx `u8` in c_import.zig (current behavior; diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index 714d14a..53337c8 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -113,6 +113,45 @@ CADisplayLink :: #foreign #objc_class("CADisplayLink") { duration :: (self: *Self) -> f64; } +// ── UIKit chrome (Phase 3.2 C4) ──────────────────────────────────────── + +UIScreen :: #foreign #objc_class("UIScreen") { + mainScreen :: () -> *UIScreen; + nativeScale :: (self: *Self) -> f64; + bounds :: (self: *Self) -> CGRect; +} + +UIView :: #foreign #objc_class("UIView") { + safeAreaInsets :: (self: *Self) -> UIEdgeInsets; + addSubview :: (self: *Self, view: *void); + // `-layer` returns a CALayer*; declared as opaque pointer for now + // (CALayer's own declarative binding is in the C5 cluster). + layer :: (self: *Self) -> *void; +} + +UIWindow :: #foreign #objc_class("UIWindow") { + alloc :: () -> *UIWindow; + initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow; + setRootViewController :: (self: *Self, vc: *void); + makeKeyAndVisible :: (self: *Self); + screen :: (self: *Self) -> *UIScreen; +} + +UIViewController :: #foreign #objc_class("UIViewController") { + alloc :: () -> *UIViewController; + init :: (self: *Self) -> *UIViewController; + setView :: (self: *Self, view: *void); +} + +UITextField :: #foreign #objc_class("UITextField") { + alloc :: () -> *UITextField; + init :: (self: *Self) -> *UITextField; + // Inherited from UIResponder via the runtime; declared here directly + // until `#extends UIResponder` lands (Phase 3.4). + becomeFirstResponder :: (self: *Self) -> s8; + resignFirstResponder :: (self: *Self) -> s8; +} + // GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's // loader path (they live on the framework's symbol table directly). GL_RENDERBUFFER :u32: 0x8D41; @@ -281,14 +320,16 @@ impl Platform for UIKitPlatform { show_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { if self.text_field == null { return; } - #objc_call(bool)(self.text_field, "becomeFirstResponder"); + tf : *UITextField = xx self.text_field; + tf.becomeFirstResponder(); } } hide_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { if self.text_field == null { return; } - #objc_call(bool)(self.text_field, "resignFirstResponder"); + tf : *UITextField = xx self.text_field; + tf.resignFirstResponder(); } } @@ -324,7 +365,8 @@ uikit_refresh_safe_insets :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } if plat.gl_view == null { return; } - i := #objc_call(UIEdgeInsets)(plat.gl_view, "safeAreaInsets"); + gl_view : *UIView = xx plat.gl_view; + i := gl_view.safeAreaInsets(); plat.safe_top = xx i.top; plat.safe_left = xx i.left; plat.safe_bottom = xx i.bottom; @@ -400,10 +442,8 @@ uikit_register_classes :: () { // (that's where the gles path picks the scale up). uikit_read_screen_scale :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } - UIScreen := objc_getClass("UIScreen".ptr); - screen := #objc_call(*void)(UIScreen, "mainScreen"); - scale_d := #objc_call(f64)(screen, "nativeScale"); - plat.dpi_scale = xx scale_d; + screen := UIScreen.mainScreen(); + plat.dpi_scale = xx screen.nativeScale(); } // NSNotification callback. The notification's userInfo dict has the @@ -441,8 +481,9 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo // Screen height in points. The window lives on the connected scene's screen. if plat.window == null { return; } - win_screen := #objc_call(*void)(plat.window, "screen"); - screen_bounds := #objc_call(CGRect)(win_screen, "bounds"); + win : *UIWindow = xx plat.window; + win_screen := win.screen(); + screen_bounds := win_screen.bounds(); // Keyboard height = how much of the screen the keyboard covers from the // bottom. When the keyboard is hiding, its end-frame.y == screen.height, @@ -486,13 +527,12 @@ uikit_create_gl_context :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } EAGLContext := objc_getClass("EAGLContext".ptr); - UIScreen := objc_getClass("UIScreen".ptr); + // UIScreen class slot comes from the declarative #objc_class binding. // Read the screen scale up-front so callers can size font caches and // textures with the right DPI before the window even exists. - screen := #objc_call(*void)(UIScreen, "mainScreen"); - scale_d := #objc_call(f64)(screen, "nativeScale"); - plat.dpi_scale = xx scale_d; + screen := UIScreen.mainScreen(); + plat.dpi_scale = xx screen.nativeScale(); ctx_raw := #objc_call(*void)(EAGLContext, "alloc"); plat.gl_ctx = #objc_call(*void)(ctx_raw, "initWithAPI:", EAGL_API_GLES3); @@ -543,24 +583,21 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) { } plat := g_uikit_plat; - UIWindow := objc_getClass("UIWindow".ptr); - UIViewController := objc_getClass("UIViewController".ptr); SxGLView := objc_getClass("SxGLView".ptr); SxMetalView := objc_getClass("SxMetalView".ptr); - // CADisplayLink and NSRunLoop class objects come from the - // declarative `#objc_class` slots populated by emit_llvm's - // __sx_objc_class_init constructor — no local objc_getClass needed. + // UIWindow / UIViewController / CADisplayLink / NSRunLoop class + // slots come from the declarative #objc_class bindings. - win_raw := #objc_call(*void)(UIWindow, "alloc"); - plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene); + win := UIWindow.alloc().initWithWindowScene(scene); + plat.window = xx win; // Make the scene delegate own the window so iOS retains it. Per the // scene-based lifecycle, the scene delegate is expected to provide the // UIWindow via -window/-setWindow:. #objc_call(void)(delegate, "setWindow:", plat.window); - vc_raw := #objc_call(*void)(UIViewController, "alloc"); - plat.root_vc = #objc_call(*void)(vc_raw, "init"); + vc := UIViewController.alloc().init(); + plat.root_vc = xx vc; // Allocate either SxGLView or SxMetalView based on gpu_mode and install // it as the VC's view. The view's +layerClass override gives us the @@ -569,11 +606,12 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) { view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView; glv_raw := #objc_call(*void)(view_class, "alloc"); plat.gl_view = #objc_call(*void)(glv_raw, "init"); - #objc_call(void)(plat.root_vc, "setView:", plat.gl_view); + vc.setView(plat.gl_view); - #objc_call(void)(plat.window, "setRootViewController:", plat.root_vc); + win.setRootViewController(plat.root_vc); - plat.gl_layer = #objc_call(*void)(plat.gl_view, "layer"); + gl_view : *UIView = xx plat.gl_view; + plat.gl_layer = gl_view.layer(); // Mark the layer opaque (no compositor blend). Required for EAGL + // recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't @@ -606,23 +644,22 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) { // Match the layer's drawable scale to the screen's native scale so we get // pixel-accurate rendering on retina displays. CGFloat is `double` on // 64-bit Apple platforms; reading as f32 would clobber the value. - screen := #objc_call(*void)(plat.window, "screen"); - scale := #objc_call(f64)(screen, "nativeScale"); + screen2 := win.screen(); + scale := screen2.nativeScale(); plat.dpi_scale = xx scale; #objc_call(void)(plat.gl_view, "setContentScaleFactor:", scale); // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once // the layer has its real on-screen bounds. makeKeyAndVisible triggers // a layout pass; layoutSubviews calls uikit_setup_renderbuffer. - #objc_call(void)(plat.window, "makeKeyAndVisible"); + win.makeKeyAndVisible(); // Hidden UITextField as the firstResponder source for show_keyboard / // hide_keyboard. Lives as a subview of the GL view so it's in the // responder chain but is sized 0×0 so it can't be tapped. - UITextField := objc_getClass("UITextField".ptr); - tf_raw := #objc_call(*void)(UITextField, "alloc"); - plat.text_field = #objc_call(*void)(tf_raw, "init"); - #objc_call(void)(plat.gl_view, "addSubview:", plat.text_field); + tf := UITextField.alloc().init(); + plat.text_field = xx tf; + gl_view.addSubview(plat.text_field); // (Keyboard observer is registered in didFinishLaunching via