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

6.7 KiB

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

#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:

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 and the field- access method dispatch at src/ir/lower.zig:5290 (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.