ffi 3.1: Cls.static_method(args) lowers to objc_msg_send on the class object

Implementation half of the Phase 3.1 cadence step.
`lowerForeignStaticCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcStaticCall` helper
that loads the class object from a module-scoped cached slot (populated
once per module via `objc_getClass`) and dispatches `objc_msg_send`
with the same selector-mangling as Phase 3.0's instance dispatch.

Three pieces:

1. `Module.objc_class_cache` — parallel to `objc_selector_cache`,
   insertion-ordered list of (class_name, slot_GlobalId) so the
   constructor that calls `objc_getClass` per slot at module load
   is deterministic. `lookupObjcClass` / `appendObjcClass` accessors.
2. `internObjcClassObject` in lower.zig — get-or-create a
   `OBJC_CLASSLIST_REFERENCES_<Cls>` global pointer; matches clang's
   naming convention. `lowerObjcStaticCall` reuses
   `deriveObjcSelector` from 3.0 for the selector, loads the class
   slot, and emits `objc_msg_send(class_obj, sel, args)`.
3. `emitObjcClassInit` in emit_llvm.zig — companion to
   `emitObjcSelectorInit`. Walks `objc_class_cache`, synthesizes a
   constructor `__sx_objc_class_init` that calls `objc_getClass(name)`
   per slot, registers in `@llvm.global_ctors` for AOT (extending the
   existing array if the selector init already created it), and
   injects a direct call into main's prelude after any prior init
   calls so the ORC JIT path runs it too.

Surface form is `.` (`NSObject.class()`) matching JNI's `Alias.new(...)`
convention rather than the plan's notional `::` — avoids extending the
parser for a new postfix operator with no other use case.

Test `examples/ffi-objc-dsl-05-static.sx` exercises NSObject's
`+class` and `+description` class methods via the new syntax, asserts
both return non-null. NSObject is always available at module-load,
unlike runtime-created test classes that wouldn't exist yet when
the class-init constructor runs.

164/164 tests; chess builds + runs clean on all three platforms.
This commit is contained in:
agra
2026-05-25 16:23:24 +03:00
parent b07ee53a39
commit 8406cc1fed
7 changed files with 281 additions and 38 deletions

View File

@@ -33,6 +33,14 @@ pub const Module = struct {
/// hashtable rehash). `#objc_call` lowering uses
/// `lookupObjcSelector` / `appendObjcSelector` to read/write it.
objc_selector_cache: std.ArrayList(ObjcSelectorEntry),
/// Interned Obj-C class objects. Parallel structure to
/// `objc_selector_cache` — kept as an insertion-ordered list of
/// (class_name, slot_GlobalId) so the constructor that calls
/// `objc_getClass` per slot at module load is deterministic.
/// Used by static method dispatch (Phase 3.1) — every
/// `Cls.static_method(...)` against an `#objc_class` alias resolves
/// the class object through this cache once per module.
objc_class_cache: std.ArrayList(ObjcClassEntry),
alloc: Allocator,
/// True when this module's program imports `std.sx` (and therefore
/// has the `Context` type). Set by lowering's Pass 0 pre-scan. Read
@@ -41,6 +49,7 @@ pub const Module = struct {
has_implicit_ctx: bool = false,
pub const ObjcSelectorEntry = struct { sel: []const u8, slot: GlobalId };
pub const ObjcClassEntry = struct { name: []const u8, slot: GlobalId };
pub fn init(alloc: Allocator) Module {
return .{
@@ -49,6 +58,7 @@ pub const Module = struct {
.globals = std.ArrayList(Global).empty,
.impl_table = ImplTable.init(alloc),
.objc_selector_cache = std.ArrayList(ObjcSelectorEntry).empty,
.objc_class_cache = std.ArrayList(ObjcClassEntry).empty,
.alloc = alloc,
};
}
@@ -61,6 +71,7 @@ pub const Module = struct {
self.globals.deinit(self.alloc);
self.impl_table.deinit();
self.objc_selector_cache.deinit(self.alloc);
self.objc_class_cache.deinit(self.alloc);
self.types.deinit();
}
@@ -78,6 +89,18 @@ pub const Module = struct {
self.objc_selector_cache.append(self.alloc, .{ .sel = sel, .slot = slot }) catch unreachable;
}
/// Linear scan — same rationale as `lookupObjcSelector`.
pub fn lookupObjcClass(self: *const Module, name: []const u8) ?GlobalId {
for (self.objc_class_cache.items) |entry| {
if (std.mem.eql(u8, entry.name, name)) return entry.slot;
}
return null;
}
pub fn appendObjcClass(self: *Module, name: []const u8, slot: GlobalId) void {
self.objc_class_cache.append(self.alloc, .{ .name = name, .slot = slot }) catch unreachable;
}
pub fn addFunction(self: *Module, func: Function) FuncId {
const id = FuncId.fromIndex(@intCast(self.functions.items.len));
self.functions.append(self.alloc, func) catch unreachable;