**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"; // Foreign declaration so we can dispatch. NSObject :: #foreign #objc_class("NSObject") { 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 :: () -> s32 { 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 foreign-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` foreign-class receiver. 3. `lowerCall` sets a per-arg `self.target_type` from `resolveCallParamTypes(c)`. For UFCS dispatch on a foreign-class alias, that function had no path covering `foreign_class_map` — it tried `resolveFuncByName(qualified)` and `fn_ast_map.get(qualified)`, both of which miss for `#foreign #objc_class` 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 `foreign_class_map.get(sname)` → `findForeignMethodInChain` path to `resolveCallParamTypes`. When the UFCS receiver is a foreign-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`.