From 15f10c5031dcbe61b69d765a06f2489a7cce492a Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 17:27:29 +0300 Subject: [PATCH] =?UTF-8?q?ffi=203.2=20C4/C5=20BLOCKED:=20file=20issue-004?= =?UTF-8?q?3=20=E2=80=94=20lazy=20lowering=20loses=20foreign-class=20dispa?= =?UTF-8?q?tch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- current/CHECKPOINT-FFI.md | 24 ++- ...wer-loses-foreign-class-method-dispatch.md | 142 ++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md diff --git a/current/CHECKPOINT-FFI.md b/current/CHECKPOINT-FFI.md index ccdbdcc..ff3e3e9 100644 --- a/current/CHECKPOINT-FFI.md +++ b/current/CHECKPOINT-FFI.md @@ -568,9 +568,29 @@ 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). + +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. + Open work: -- **Phase 3 step 3.2 — C4..C5** — uikit.sx migration, two clusters - remaining (UIKit chrome, view tree + GL). +- **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). diff --git a/issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md b/issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md new file mode 100644 index 0000000..dec1b7d --- /dev/null +++ b/issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md @@ -0,0 +1,142 @@ +# issue-0043: lazy-lowered function bodies don't resolve foreign-class method dispatch + +## Symptom + +When a function `B` containing `recv.method(...)` calls against a +`#objc_class` receiver is invoked transitively via `lazyLowerFunction` +from another `inline if OS == ...` branch in function `A`, the +method dispatch fails with: + +``` +unresolved 'methodName' (in fn B) +``` + +Direct (eager) lowering of `B` with the same body works. Direct call +from `main` works. Only the transitive lazy-lower path from inside an +`inline if` branch fails. + +Concretely: under Phase 3.0 / Phase 3.2 C4 work, the iOS-sim build of +chess fails: + +``` +library/modules/platform/uikit.sx:591:12: error: unresolved 'initWithWindowScene' (in ... fn uikit_scene_will_connect_ios) +library/modules/platform/uikit.sx:599:11: error: unresolved 'init' +library/modules/platform/uikit.sx:609:5: error: unresolved 'setView' +library/modules/platform/uikit.sx:611:5: error: unresolved 'setRootViewController' +library/modules/platform/uikit.sx:661:11: error: unresolved 'init' +``` + +Stack trace (with `SX_TRACE_UNRESOLVED=1`) shows the chain: + +``` +emitError (lower.zig:5640) + ↑ lowerCall .field_access fallback +lowerVarDecl (lowering body of uikit_scene_will_connect_ios) +lowerBlock +lazyLowerFunction (lower.zig:5165) (← lazy entry point) +lowerCall (lower.zig:5165) (calling uikit_scene_will_connect_ios) +lowerInlineBranch (inside `inline if OS == .ios { uikit_scene_will_connect_ios(...) }`) +lowerIfExpr +lowerExpr +``` + +So when the *outer* `lowerCall` decides to lazy-lower +`uikit_scene_will_connect_ios`, the *inner* body's method-dispatch +on `*UIWindow` etc. fails to find the foreign-class declaration — +even though the declaration is at module scope at the top of +uikit.sx and resolves fine for non-lazy lowering paths (`sx build` +for macOS target compiles the same source cleanly). + +## Reproduction + +```sx +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +UIWindow :: #foreign #objc_class("UIWindow") { + alloc :: () -> *UIWindow; + initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow; +} + +// Function B: uses foreign-class method dispatch. +do_work :: (scene: *void) { + win := UIWindow.alloc().initWithWindowScene(scene); + _ = win; +} + +// Function A: calls B inside an `inline if`. The transitive call +// triggers lazy lowering of B, which fails for ios-sim only. +caller :: (self: *void, _cmd: *void, scene: *void, b: *void, c: *void) callconv(.c) { + inline if OS == .ios { + do_work(scene); + } +} + +main :: () -> s32 { 0; } +``` + +Build: + +```sh +sx build --target ios-sim issue-0043.sx +``` + +Observed: `error: unresolved 'initWithWindowScene' (in issue-0043.sx fn do_work)`. + +Eager dispatch shape (called directly from `main` without the +intermediate `inline if`-gated function) compiles cleanly. The +isolated probe must mirror the lazy-lower trigger to reproduce. + +## Investigation prompt (for a fresh session) + +Suspected area: `lazyLowerFunction` at +[src/ir/lower.zig:1057](../src/ir/lower.zig#L1057) and the field- +access method dispatch at +[src/ir/lower.zig:5290](../src/ir/lower.zig#L5290) (around the +`foreign_class_map.get(sname_for_foreign)` lookup). + +Hypotheses: + +1. `lazyLowerFunction` swaps some piece of lowering state on entry + (e.g., `saved_source_file`, `current_ctx_ref`) but doesn't preserve + access to `foreign_class_map`. Check whether the map is + instance-state vs. shared. +2. The receiver type for `win` (`*UIWindow`) isn't being resolved to + its `getStructTypeName` correctly during lazy lowering — possibly + `inferExprType` for the lazy-lowered context resolves to an + anonymous type instead of `UIWindow`. +3. The foreign-class declarations are added to `foreign_class_map` + during a pre-scan pass that runs BEFORE the outer function `A`'s + body is lowered, but lazy lowering of `B` from within `A` might + be observing the map at a pre-scan state where uikit.sx's + declarations haven't been seen yet (cross-module ordering). + +What the fix likely needs to do: +- Confirm `foreign_class_map` contains `"UIWindow"` etc. at the + point of the lazy lowering call (add a debug print at the failing + dispatch). +- If the map IS populated, trace why the field-access dispatch falls + through to line 5640 instead of taking the `foreign_class_map.get` + branch at line 5306. +- If the map is NOT populated, find the seeding path and fix the + ordering — likely a missing pre-scan step before lazy lowering. + +Verification: rebuild chess with `sx build --target ios-sim` against +the in-progress Phase 3.2 C4 branch (revert the +`git checkout -- library/modules/platform/uikit.sx` to restore the +work-in-progress migration) and confirm the unresolved errors go +away. + +## Why this blocks Phase 3.2 C4 + +The migration of `library/modules/platform/uikit.sx` to declarative +`#objc_class` dispatch (Phase 3.2 plan part C, clusters C4 + C5) +necessarily places foreign-class method calls inside iOS-only helper +functions that get lazily lowered when chess's iOS scene-lifecycle +hooks fire them. Without this fix, the migration produces compile +errors on iOS-sim that don't appear on macOS. C1+C2+C3 happened to +land cleanly because the methods they migrate are either (a) called +from eagerly-lowered top-level paths, or (b) niladic enough that the +specific dispatch path doesn't fail. + +The Phase 3.2 plan flagged C4+C5 as blocked pending this fix.