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 (commits1ea9cda/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:
@@ -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).
|
||||
|
||||
142
issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md
Normal file
142
issues/0043-lazy-lower-loses-foreign-class-method-dispatch.md
Normal 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.
|
||||
Reference in New Issue
Block a user