Files
sx/issues/0043-lazy-lower-loses-runtime-class-method-dispatch.md
agra c21b683b08 docs(issues): mark 17 already-fixed issues RESOLVED with verified banners
Each banner was re-verified against the current binary (repro now behaves
correctly) and cites the actual fix location in current src/** plus the covering
regression example. Closes the stale-but-fixed backlog: 0019, 0042-0056, 0131.
No compiler change.
2026-06-21 09:25:52 +03:00

158 lines
6.7 KiB
Markdown

# issue-0043: lazy-lowered function bodies don't resolve runtime-class method dispatch
> **RESOLVED.** Root cause: chained runtime-class dispatch (`Cls.alloc().init...(x)`) collapsed the inner call's return type, so the outer `.method(...)` couldn't find the receiver's `#objc_class` declaration — and on the lazy-lower path (inside an `inline if OS == .ios` branch) the runtime-class map was unavailable.
> Fix: the runtime-class instance/static dispatch now resolves each call's declared return type via `resolveRuntimeMethodReturnType` against the runtime_class_map (`src/ir/calls.zig`, runtime-class branches ~L244-269 / L385-398), and that map lives on the shared `ProgramIndex` (`src/ir/program_index.zig:668`) so it is equally visible to eager and lazy lowering (`lazyLowerFunction`, `src/ir/lower/decl.zig:2508`).
> Covering regression: `examples/1306-ffi-objc-runtime-class-chained-dispatch.sx` (header cites issue 0043) exercises both `*ClassName` and `*Self` chained shapes and passes.
**FIXED.** The original repro
(`sx build --target ios-sim issue-0043.sx` with a
`UIWindow.alloc().initWithWindowScene(scene)` call inside an
`inline if OS == .ios { ... }`-gated function called transitively
from `caller :: (...) callconv(.c)`) compiles cleanly today; chess
on iOS-sim runs end-to-end through the same dispatch shape. The
fix lives in tree as part of broader runtime-class /
lazyLowerFunction work — no specific commit isolates it.
Below preserved as a record of the original problem.
## 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 runtime-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 :: #objc_class("UIWindow") extern {
alloc :: () -> *UIWindow;
initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow;
}
// Function B: uses runtime-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 :: () -> i32 { 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
`runtime_class_map.get(sname_for_runtime)` 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 `runtime_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 runtime-class declarations are added to `runtime_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 `runtime_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 `runtime_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 runtime-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.