ffi M1.2 A.2b: register sx-defined #objc_class methods + *Self substitution

Bodied instance methods on a sx-defined '#objc_class("Cls") { ... }'
declaration are now registered in fn_ast_map under '<Cls>.<method>'
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 <Cls>.<method> 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.
This commit is contained in:
agra
2026-05-25 21:59:23 +03:00
parent 7b98b3ae78
commit ae1072d415

View File

@@ -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 `<Class>.<method>`),
// 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 `<ClassName>.<methodName>`. Lazy lowering
/// then handles the body via the standard path; `*Self` is
/// substituted to `*<ClassName>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 `<ClassName>.<methodName>`, 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 (`<ClassName>.<methodName>`), 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