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.
This commit is contained in:
agra
2026-05-25 17:27:29 +03:00
parent 2a7c8e0a6f
commit 15f10c5031
2 changed files with 164 additions and 2 deletions

View File

@@ -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).

View File

@@ -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 <file> 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.