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`.
This commit is contained in:
agra
2026-05-25 17:53:11 +03:00
parent 2b717d9b38
commit 5b4969f9be
2 changed files with 140 additions and 55 deletions

View File

@@ -568,32 +568,59 @@ blocks. The `link` parameter on the `sxTick:` callback is now cast
to `*CADisplayLink` at function entry so subsequent method calls to `*CADisplayLink` at function entry so subsequent method calls
type-check. type-check.
**Phase 3.2 C4/C5 BLOCKED on issue-0043.** Attempted C4 migration **issue-0043 closed.** The "lazy-lower" framing in the issue file
(UIKit chrome: UIScreen / UIWindow / UIViewController / UITextField turned out to be a red herring: the actual root cause was that
/ UIView) surfaced a real compiler bug: lazy-lowered function bodies `inferExprType` for a chained call `Cls.static().instance(...)` never
don't resolve foreign-class method dispatch when invoked transitively looked the inner call's foreign-class declaration up, so the outer
from an `inline if OS == .ios` branch in another function. The dispatch saw a `.s64` receiver, the `foreign_class_map.get(...)` lookup
specific failure is in `uikit_scene_will_connect_ios` whose body missed, and lowering emitted `error: unresolved 'method'`. The macOS
contains `UIWindow.alloc().initWithWindowScene(scene)` and target appeared to work because `inline if OS == .ios { ... }` strips
`win.setRootViewController(...)` — both work in isolated probes but the gated body before lowering — eliding every call that would have
fail at compile time when the function is reached via lazy lowering exercised the broken path.
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).
The C4 work is reverted to keep the tree green at C3. C4+C5 stay Fix in `src/ir/lower.zig`:
pending until issue-0043 is fixed in a separate session. 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: Open work:
- **issue-0043** — investigate + fix the lazy-lower foreign-class - **Phase 3 step 3.2 — C5** — uikit.sx migration (view tree + GL
dispatch bug. See `issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md` drawables: CAEAGLLayer, EAGLContext, plus any remaining
for the reproduction and investigation prompt. CAMetalLayer / NSTimer sites).
- **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.3** — `property name: Type` synthesizes - **Phase 3 step 3.3** — `property name: Type` synthesizes
`inst.name``[inst name]` getter and `inst.name = x` `inst.name``[inst name]` getter and `inst.name = x`
`[inst setName: x]` setter. `#setter("...")` overrides the setter `[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 `Gles3Gpu`. Instrumentation stripped after fix. 140 host + 9
cross tests green. 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 ## Known issues
- `signed char` C maps to sx `u8` in c_import.zig (current behavior; - `signed char` C maps to sx `u8` in c_import.zig (current behavior;

View File

@@ -113,6 +113,45 @@ CADisplayLink :: #foreign #objc_class("CADisplayLink") {
duration :: (self: *Self) -> f64; 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 // GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
// loader path (they live on the framework's symbol table directly). // loader path (they live on the framework's symbol table directly).
GL_RENDERBUFFER :u32: 0x8D41; GL_RENDERBUFFER :u32: 0x8D41;
@@ -281,14 +320,16 @@ impl Platform for UIKitPlatform {
show_keyboard :: (self: *UIKitPlatform) { show_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios { inline if OS == .ios {
if self.text_field == null { return; } 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) { hide_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios { inline if OS == .ios {
if self.text_field == null { return; } 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; } inline if OS != .ios { return; }
if plat.gl_view == null { 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_top = xx i.top;
plat.safe_left = xx i.left; plat.safe_left = xx i.left;
plat.safe_bottom = xx i.bottom; plat.safe_bottom = xx i.bottom;
@@ -400,10 +442,8 @@ uikit_register_classes :: () {
// (that's where the gles path picks the scale up). // (that's where the gles path picks the scale up).
uikit_read_screen_scale :: (plat: *UIKitPlatform) { uikit_read_screen_scale :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; } inline if OS != .ios { return; }
UIScreen := objc_getClass("UIScreen".ptr); screen := UIScreen.mainScreen();
screen := #objc_call(*void)(UIScreen, "mainScreen"); plat.dpi_scale = xx screen.nativeScale();
scale_d := #objc_call(f64)(screen, "nativeScale");
plat.dpi_scale = xx scale_d;
} }
// NSNotification callback. The notification's userInfo dict has the // 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. // Screen height in points. The window lives on the connected scene's screen.
if plat.window == null { return; } if plat.window == null { return; }
win_screen := #objc_call(*void)(plat.window, "screen"); win : *UIWindow = xx plat.window;
screen_bounds := #objc_call(CGRect)(win_screen, "bounds"); win_screen := win.screen();
screen_bounds := win_screen.bounds();
// Keyboard height = how much of the screen the keyboard covers from the // Keyboard height = how much of the screen the keyboard covers from the
// bottom. When the keyboard is hiding, its end-frame.y == screen.height, // 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; } inline if OS != .ios { return; }
EAGLContext := objc_getClass("EAGLContext".ptr); 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 // Read the screen scale up-front so callers can size font caches and
// textures with the right DPI before the window even exists. // textures with the right DPI before the window even exists.
screen := #objc_call(*void)(UIScreen, "mainScreen"); screen := UIScreen.mainScreen();
scale_d := #objc_call(f64)(screen, "nativeScale"); plat.dpi_scale = xx screen.nativeScale();
plat.dpi_scale = xx scale_d;
ctx_raw := #objc_call(*void)(EAGLContext, "alloc"); ctx_raw := #objc_call(*void)(EAGLContext, "alloc");
plat.gl_ctx = #objc_call(*void)(ctx_raw, "initWithAPI:", EAGL_API_GLES3); 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; plat := g_uikit_plat;
UIWindow := objc_getClass("UIWindow".ptr);
UIViewController := objc_getClass("UIViewController".ptr);
SxGLView := objc_getClass("SxGLView".ptr); SxGLView := objc_getClass("SxGLView".ptr);
SxMetalView := objc_getClass("SxMetalView".ptr); SxMetalView := objc_getClass("SxMetalView".ptr);
// CADisplayLink and NSRunLoop class objects come from the // UIWindow / UIViewController / CADisplayLink / NSRunLoop class
// declarative `#objc_class` slots populated by emit_llvm's // slots come from the declarative #objc_class bindings.
// __sx_objc_class_init constructor — no local objc_getClass needed.
win_raw := #objc_call(*void)(UIWindow, "alloc"); win := UIWindow.alloc().initWithWindowScene(scene);
plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene); plat.window = xx win;
// Make the scene delegate own the window so iOS retains it. Per the // Make the scene delegate own the window so iOS retains it. Per the
// scene-based lifecycle, the scene delegate is expected to provide the // scene-based lifecycle, the scene delegate is expected to provide the
// UIWindow via -window/-setWindow:. // UIWindow via -window/-setWindow:.
#objc_call(void)(delegate, "setWindow:", plat.window); #objc_call(void)(delegate, "setWindow:", plat.window);
vc_raw := #objc_call(*void)(UIViewController, "alloc"); vc := UIViewController.alloc().init();
plat.root_vc = #objc_call(*void)(vc_raw, "init"); plat.root_vc = xx vc;
// Allocate either SxGLView or SxMetalView based on gpu_mode and install // 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 // 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; view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView;
glv_raw := #objc_call(*void)(view_class, "alloc"); glv_raw := #objc_call(*void)(view_class, "alloc");
plat.gl_view = #objc_call(*void)(glv_raw, "init"); 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 + // Mark the layer opaque (no compositor blend). Required for EAGL +
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't // 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 // 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 // pixel-accurate rendering on retina displays. CGFloat is `double` on
// 64-bit Apple platforms; reading as f32 would clobber the value. // 64-bit Apple platforms; reading as f32 would clobber the value.
screen := #objc_call(*void)(plat.window, "screen"); screen2 := win.screen();
scale := #objc_call(f64)(screen, "nativeScale"); scale := screen2.nativeScale();
plat.dpi_scale = xx scale; plat.dpi_scale = xx scale;
#objc_call(void)(plat.gl_view, "setContentScaleFactor:", scale); #objc_call(void)(plat.gl_view, "setContentScaleFactor:", scale);
// Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once
// the layer has its real on-screen bounds. makeKeyAndVisible triggers // the layer has its real on-screen bounds. makeKeyAndVisible triggers
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer. // 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 / // Hidden UITextField as the firstResponder source for show_keyboard /
// hide_keyboard. Lives as a subview of the GL view so it's in the // 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. // responder chain but is sized 0×0 so it can't be tapped.
UITextField := objc_getClass("UITextField".ptr); tf := UITextField.alloc().init();
tf_raw := #objc_call(*void)(UITextField, "alloc"); plat.text_field = xx tf;
plat.text_field = #objc_call(*void)(tf_raw, "init"); gl_view.addSubview(plat.text_field);
#objc_call(void)(plat.gl_view, "addSubview:", plat.text_field);
// (Keyboard observer is registered in didFinishLaunching via // (Keyboard observer is registered in didFinishLaunching via