Files
sx/issues/0044-sx-defined-objc-class-method-self-param-must-be-named-self.md
agra b9cfe2554f refactor(ffi-linkage): Phase 9.3/9.4 — purge 'foreign' from issues/*.md; GATE PASS
Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern,
foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl,
findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→
dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the
renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed
issues/0043-…-foreign-class-…→…-runtime-class-….

PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/
issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant
SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party).
Suite green (644 corpus / 443 unit, 0 failed).
2026-06-15 11:18:35 +03:00

185 lines
7.7 KiB
Markdown

**FIXED.** Root cause was NOT the parameter name; the original `this`
rename surfaced an unrelated `target_type` leak in
`resolveCallParamTypes`. See "Root cause + fix" below.
## Symptom
In an `#objc_class` method body, the first `*Self` parameter MUST be
named `self`. Renaming to anything else (e.g. `this`) compiles cleanly
but produces wrong code at runtime: reading the parameter back (e.g.
`xx this` to coerce to `*void`) yields a small struct-offset-shaped
value (saw 0x20 / 32 in our repro) instead of the Obj-C `id` that the
IMP trampoline received. Calling into the Obj-C runtime with that value
crashes with `EXC_BAD_ACCESS` / `SIGSEGV` at a near-null address.
Observed:
- `library/modules/platform/uikit.sx` SxAppDelegate methods renamed
`self``this`. Body called `center.addObserver_selector_name_object(xx this, ...)`.
- Chess on iOS-sim crashed at first launch in
`-[NSNotificationCenter addObserver:selector:name:object:]``object_getClass(observer)`,
with `observer = 32` (low-int, not a real pointer).
- Reverting `this``self` (via `sed`) fixed the crash. Same body
shape, same `xx self` → call works fine.
Expected: parameter name should not matter. The IMP trampoline binds
the receiver `id` to whatever the first parameter is named in the
method declaration — by position, not by hardcoded name.
## Reproduction
```sx
#import "modules/std.sx";
#import "modules/std/objc.sx";
// Extern declaration so we can dispatch.
NSObject :: #objc_class("NSObject") extern {
class :: () -> *void;
description :: (self: *Self) -> *void;
}
// sx-defined class whose method's *Self param is named `this`.
Foo :: #objc_class("SxFooSelfTest") {
#extends NSObject;
poke :: (this: *Self) -> *void {
// Should return the receiver back to the caller.
return xx this;
}
}
main :: () -> i32 {
inline if OS == .macos {
f := Foo.alloc().init();
result := f.poke();
// result should be the same pointer as `f`. Expect equal.
if xx result == xx f {
print("ok\n");
} else {
print("WRONG: this != self\n");
// result will be a struct-offset shaped value
// (e.g. 0x20) instead of the Obj-C id.
}
}
inline if OS != .macos { print("skipped (not macos)\n"); }
0;
}
```
Build and run on macOS; expected `ok`; observed `WRONG: this != self`
(or a crash if the value is dereferenced).
If you swap `this``self` everywhere in the body and parameter list,
the test prints `ok`.
## Investigation prompt
In `src/ir/lower.zig`, several IMP-trampoline / method-body emission
paths hardcode the string `"self"` when binding the receiver parameter
or looking it up in the scope. Grep hits at the time of filing:
```
$ grep -n '"self"' src/ir/lower.zig
3422: init_scope.put("self", .{ ... });
5133: const self_binding = if (self.scope) |s| s.lookup("self") else null;
10044: .name = "self",
11999/12056/12499/12662: params.append(... .internString("self") ...);
```
The methods that synthesize the IMP trampoline (M1.2 A.2) and the
ones that wire `*Self` → opaque runtime-class stub (M1.2 A.3) appear
to either:
(a) emit the trampoline assuming the slot name in the body is "self",
so a body declared with `this: *Self` reads from an uninitialized /
different slot when it accesses the parameter; OR
(b) resolve `*Self`-typed parameters by name rather than by position
+ type, so a non-`self` name routes through a slower / different
binding path that doesn't see the IMP-passed receiver.
Likely fix: have the parser / type-checker for `#objc_class` method
bodies identify the first `*Self`-typed parameter by **position and
type**, and bind the IMP-passed receiver into whatever local name the
user chose. Remove hardcoded `"self"` literals from the trampoline
emission and from the M1.2 A.3 `lowerFieldAccess` / `lowerAssignment`
helpers (the ivar→struct_gep path needs to know which local IS the
receiver, not assume it's named "self").
Verification step:
1. Apply the fix.
2. Save the repro above as `examples/issue-0044.sx`.
3. Run `./zig-out/bin/sx run examples/issue-0044.sx` — expect `ok`
(not `WRONG: ...` and not a crash).
4. Bonus: confirm `bash tests/run_examples.sh` still passes (no
regression in existing `ffi-objc-*` tests, which all happen to
use `self` and so wouldn't have caught this).
## Background
Encountered during the FFI M3 follow-up cleanup of
`library/modules/platform/uikit.sx`. Renamed the IMP-side first
parameter from `self` to `this` across all `#objc_class` methods to
free `self` for upcoming M4 `self.method()` UFCS work on
`UIKitPlatform` methods called from those class bodies. Crash
manifested at first `[notificationCenter addObserver:self ...]` in
`-application:didFinishLaunchingWithOptions:`. Workaround in-session
was `sed -i '' 's/this: \*Self/self: *Self/g; s/xx this/xx self/g'`
across uikit.sx — the rename was uniform so the substitution was safe.
Cost of the foot-gun: future contributors who follow CLAUDE.md's
"any first parameter name that makes the body clearer is fine" mental
model will silently mis-compile their `#objc_class` method bodies.
## Root cause + fix
The parameter name `this` vs `self` was a red herring. What actually
went wrong:
1. uikit.sx renamed the AppDelegate's IMP method first param to `this`,
so `xx this` appeared inside the body of a `-> BOOL` method.
2. The body called `center.addObserver_selector_name_object(xx this, ..., null)`
on a `*NSNotificationCenter` runtime-class receiver.
3. `lowerCall` sets a per-arg `self.target_type` from
`resolveCallParamTypes(c)`. For UFCS dispatch on a runtime-class
alias, that function had no path covering `runtime_class_map`
it tried `resolveFuncByName(qualified)` and `fn_ast_map.get(qualified)`,
both of which miss for `#objc_class(…) extern` methods.
4. With `param_types` empty, the per-arg `target_type` assignment was
skipped, so `self.target_type` retained its previous value: the
enclosing fn's return type, **BOOL → i8**.
5. `xx this` then lowered with target type `i8`: `ptrtoint ptr to i64`
`trunc i64 to i8`. The receiver pointer became its low byte (0xC0
/ 0x20 / etc., depending on heap address).
6. `addObserver:selector:name:object:` got that byte as the observer.
Apple's runtime calls `object_getClass(observer)` internally for
validation → near-null deref → SIGSEGV.
The same shape works fine in `sx run` because the `xx` cast wasn't
exercised in the body in the original tests, OR the encoding
happened to land somewhere benign (e.g. the AOT-with-iOS-sim path
plus UIKit's specific validation order).
**Fix:** add a `runtime_class_map.get(sname)`
`findRuntimeMethodInChain` path to `resolveCallParamTypes`. When the
UFCS receiver is a runtime-class alias, walk the `#extends` chain to
find the method, then resolve its declared param types (skipping the
implicit `*Self` for instance methods). With the fix, `param_types`
returns `[*void, *void, *void, *void]` for the addObserver: call,
each `xx ptr` gets target type `*void`, and the cast is a clean
`ptrtoint``inttoptr` round-trip (or no-op since both sides are
pointer-typed).
[src/ir/lower.zig:8617-8639](../src/ir/lower.zig#L8617).
The parameter-name hardcoding in `lower.zig` (lines 3422, 5133, 10044,
11999, 12056, 12499, 12662) is unrelated — those are all SYNTHESIZED
parameters in compiler-generated functions (init scopes, JNI stubs,
property IMPs, dealloc IMPs), not the user-facing `#objc_class`
method body. The user's first param can be named anything.
Regression test: `examples/issue-0044.sx`. Pre-fix, the
`captureSelf-from-BOOL` probe prints `WRONG` because `xx this` gets
truncated to its low byte and the round-trip comparison fails. With
the fix, all three probes print `ok`.