From ae1072d415d6852fa35d1bbfc60fe7d0b230493d Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 21:59:23 +0300 Subject: [PATCH] ffi M1.2 A.2b: register sx-defined #objc_class methods + *Self substitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bodied instance methods on a sx-defined '#objc_class("Cls") { ... }' declaration are now registered in fn_ast_map under '.' and declared in the IR with their *Self params substituted to the hidden state-struct type (M1.2 A.2a). registerObjcDefinedClassMethods walks the foreign_class_decl's members, synthesizes an FnDecl from each ForeignMethodDecl (zipping params + param_names), and feeds it through declareFunction with current_foreign_class temporarily pinned so resolveTypeWithBindings substitutes Self → __SxFooState. resolveTypeWithBindings now treats type_expr 'Self' as a contextual alias: when current_foreign_class points to a sx-defined Obj-C class, the substitution returns objcDefinedStateStructType(fcd). Other Self contexts (protocols, JNI super, foreign-class member type resolution) are untouched — the check filters on (!is_foreign and runtime == .objc_class). lowerFunction also sets current_foreign_class for the duration of the body lowering when the name is qualified . and Cls is in objc_defined_class_cache. Save+restore via defer so nested calls round-trip cleanly. Verification (manual): 'sx ir' on an sx-defined class shows 'declare void @SxFoo.bump(ptr, ptr)' — two args = implicit __sx_ctx + the state-struct pointer (correct *Self substitution). Body emission happens lazily; A.2c will trigger it eagerly so the IMP trampoline (A.4) can reference it. 170 example tests + zig build test green. --- src/ir/lower.zig | 90 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5aecc7f..f56c987 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1075,6 +1075,16 @@ pub const Lowering = struct { /// Lower a single function declaration. pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, is_imported: bool) void { + // For sx-defined `#objc_class` methods (qualified `.`), + // set `current_foreign_class` so `*Self` substitutions through + // `resolveTypeWithBindings` find the state-struct type (M1.2 A.2b). + // Save+restore — function lowering can re-enter. + const saved_fc = self.current_foreign_class; + defer self.current_foreign_class = saved_fc; + if (self.lookupObjcDefinedClassForMethod(name)) |fcd| { + self.current_foreign_class = fcd; + } + const name_id = self.module.types.internString(name); const ret_ty = self.resolveReturnType(fd); @@ -8797,6 +8807,20 @@ pub const Lowering = struct { /// Resolve a type node, checking type_bindings first for generic type params. fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { + // *Self substitution for sx-defined `#objc_class` method bodies + // (M1.2 A.2b). `current_foreign_class` is set by lowerFunction + // and registerObjcDefinedClassMethods when the surrounding + // function is a class method. `Self` inside the body resolves + // to the class's hidden state-struct type — `self.field` then + // works as a plain struct field access (M1.2 A.3 "free if + // types align"). + if (node.data == .type_expr and std.mem.eql(u8, node.data.type_expr.name, "Self")) { + if (self.current_foreign_class) |fcd| { + if (!fcd.is_foreign and fcd.runtime == .objc_class) { + return self.objcDefinedStateStructType(fcd); + } + } + } if (self.type_bindings) |tb| { switch (node.data) { .type_expr => |te| { @@ -9567,16 +9591,80 @@ pub const Lowering = struct { /// /// sx-defined Obj-C classes (no `#foreign`, runtime == .objc_class) /// also land in `module.objc_defined_class_cache` in declaration - /// order — that cache drives M1.2 class-synthesis emission (A.4+). + /// order AND have their bodied methods registered into `fn_ast_map` + /// under qualified names `.`. Lazy lowering + /// then handles the body via the standard path; `*Self` is + /// substituted to `*State` during body lowering (M1.2 A.2b). fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { self.foreign_class_map.put(fcd.name, fcd) catch {}; if (!fcd.is_foreign and fcd.runtime == .objc_class) { if (self.module.lookupObjcDefinedClass(fcd.name) == null) { self.module.appendObjcDefinedClass(fcd.name, fcd); } + self.registerObjcDefinedClassMethods(fcd); } } + /// For each bodied instance method on an sx-defined `#objc_class`, + /// synthesize an `FnDecl` from the `ForeignMethodDecl`, register it + /// in `fn_ast_map` under `.`, and declare + /// the IR function so callers can resolve the name. Bodyless + /// declarations are skipped — they reference inherited / external + /// methods, not sx-side bodies. + fn registerObjcDefinedClassMethods(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { + for (fcd.members) |m| { + const method = switch (m) { + .method => |md| md, + else => continue, + }; + const body = method.body orelse continue; + const fd = self.synthesizeFnDeclFromObjcMethod(method, body) orelse continue; + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue; + self.fn_ast_map.put(qualified, fd) catch {}; + // Set current_foreign_class while declaring so `*Self` in + // the signature resolves to the state struct (M1.2 A.2b). + const saved = self.current_foreign_class; + self.current_foreign_class = fcd; + defer self.current_foreign_class = saved; + self.declareFunction(fd, qualified); + } + } + + /// Build an `FnDecl` whose params are zipped from the + /// `ForeignMethodDecl.params` (type nodes) and `param_names`. Used + /// to feed sx-defined class methods through the standard + /// fn-lowering pipeline. Allocator-owned; lives for the duration + /// of the Lowering pass. + fn synthesizeFnDeclFromObjcMethod(self: *Lowering, method: ast.ForeignMethodDecl, body: *ast.Node) ?*ast.FnDecl { + if (method.params.len != method.param_names.len) return null; + var params = std.ArrayList(ast.Param).empty; + for (method.params, method.param_names) |type_node, p_name| { + params.append(self.alloc, .{ + .name = p_name, + .name_span = .{ .start = 0, .end = 0 }, + .type_expr = type_node, + }) catch unreachable; + } + const fd = self.alloc.create(ast.FnDecl) catch return null; + fd.* = .{ + .name = method.name, + .params = params.toOwnedSlice(self.alloc) catch unreachable, + .return_type = method.return_type, + .body = body, + }; + return fd; + } + + /// If `name` matches an sx-defined `#objc_class`'s qualified-method + /// pattern (`.`), return the class's + /// ForeignClassDecl. Used by `lowerFunction` to set + /// `current_foreign_class` so `*Self` resolves to the state struct + /// during body lowering. + fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl { + const dot = std.mem.indexOf(u8, name, ".") orelse return null; + return self.module.lookupObjcDefinedClass(name[0..dot]); + } + /// Lazily declare the `sx_jni_env_tl_get` / `sx_jni_env_tl_set` /// runtime externs (step 2.16c). The storage lives in /// `library/vendors/sx_jni_runtime/sx_jni_env_tl.c` as a