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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user