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:
@@ -485,12 +485,23 @@ override (3.2). New helpers `deriveObjcSelector` and
|
|||||||
working output (and the mismatch case to the specific keyword-count
|
working output (and the mismatch case to the specific keyword-count
|
||||||
error).
|
error).
|
||||||
|
|
||||||
Open work, in roughly the order they make sense:
|
Phase 3 step 3.1 landed: `Cls.static_method(args)` on an `#objc_class`
|
||||||
|
alias loads the class object through a module-scoped cached slot
|
||||||
|
(`OBJC_CLASSLIST_REFERENCES_<Cls>`, populated once per module via
|
||||||
|
`objc_getClass` at module-init) and dispatches `objc_msg_send` with
|
||||||
|
the same selector derivation as 3.0. New `Module.objc_class_cache`
|
||||||
|
parallel to `objc_selector_cache`; `internObjcClassObject` and
|
||||||
|
`lowerObjcStaticCall` helpers in lower.zig; `emitObjcClassInit`
|
||||||
|
constructor in emit_llvm.zig that walks the cache, runs
|
||||||
|
`objc_getClass` per slot, registers via `@llvm.global_ctors`, and
|
||||||
|
injects a direct call into `main` for the ORC JIT path. Surface form
|
||||||
|
is `.` (matching JNI's `Alias.new(...)` convention) rather than the
|
||||||
|
plan's notional `::` — avoids a new postfix operator. Test:
|
||||||
|
`examples/ffi-objc-dsl-05-static.sx` — exercises NSObject's `+class`
|
||||||
|
and `+description` class methods (NSObject is always available at
|
||||||
|
module-load, unlike test classes created in main's body).
|
||||||
|
|
||||||
- **Phase 3 step 3.1** — static call `Cls::class_method(args)` lowers
|
Open work, in roughly the order they make sense:
|
||||||
to `#objc_call` on the class object (loaded via `objc_getClass` once
|
|
||||||
and interned per module). Same pattern as 3.0 for the niladic /
|
|
||||||
arity-N selector derivation; the new piece is the class-object slot.
|
|
||||||
- **Phase 3 step 3.2** — `#selector("explicit:")` override + golden
|
- **Phase 3 step 3.2** — `#selector("explicit:")` override + golden
|
||||||
test for the default-mangling table. Escape hatch for selectors
|
test for the default-mangling table. Escape hatch for selectors
|
||||||
that don't fit the underscore-split rule (e.g. `tableView_
|
that don't fit the underscore-split rule (e.g. `tableView_
|
||||||
|
|||||||
@@ -4,44 +4,36 @@
|
|||||||
// selector is derived by the same default mangling as Phase 3.0
|
// selector is derived by the same default mangling as Phase 3.0
|
||||||
// (`stringWithUTF8String_(s)` → "stringWithUTF8String:").
|
// (`stringWithUTF8String_(s)` → "stringWithUTF8String:").
|
||||||
//
|
//
|
||||||
// Mirrors the JNI surface (`Alias.new(...)` etc.); the lowering
|
// Mirrors JNI's static-dispatch surface (`Alias.new(...)` etc.); the
|
||||||
// disambiguates static vs instance by looking at `method.is_static` on
|
// lowering disambiguates static vs instance by looking at
|
||||||
// the foreign-class member.
|
// `method.is_static` on the foreign-class member.
|
||||||
//
|
//
|
||||||
// Pre-3.1: `lowerForeignStaticCall` only handles JNI runtime + the
|
// Uses NSObject because the cached class slot is populated by a
|
||||||
// `new` constructor; any other static call bails. Snapshot pins the
|
// constructor at module-load — runtime-created test classes wouldn't
|
||||||
// bail diagnostic.
|
// exist yet when `objc_getClass` runs. NSObject is always available
|
||||||
|
// on macOS via libobjc.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/compiler.sx";
|
||||||
#import "modules/std/objc.sx";
|
|
||||||
|
|
||||||
SxProbeStatic :: #foreign #objc_class("SxProbeStatic") {
|
NSObject :: #foreign #objc_class("NSObject") {
|
||||||
static answer :: () -> s32;
|
// `+(Class)class` — niladic, name verbatim, selector = "class".
|
||||||
static add :: (a: s32, b: s32) -> s32;
|
// Returns the class object itself.
|
||||||
}
|
static class :: () -> *void;
|
||||||
|
// `+(NSString *)description` on the class returns a description
|
||||||
answer_imp :: (self: *void, _cmd: *void) -> s32 callconv(.c) {
|
// string. Niladic, selector = "description".
|
||||||
42;
|
static description :: () -> *void;
|
||||||
}
|
|
||||||
|
|
||||||
add_imp :: (self: *void, _cmd: *void, a: s32, b: s32) -> s32 callconv(.c) {
|
|
||||||
a + b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main :: () -> s32 {
|
main :: () -> s32 {
|
||||||
inline if OS == .macos {
|
inline if OS == .macos {
|
||||||
ns_object := objc_getClass("NSObject".ptr);
|
c := NSObject.class();
|
||||||
cls := objc_allocateClassPair(ns_object, "SxProbeStatic".ptr, 0);
|
if c != null {
|
||||||
// class_addMethod on the metaclass — that's where class methods live.
|
print("class non-null\n");
|
||||||
metacls := object_getClass(xx cls);
|
}
|
||||||
class_addMethod(metacls, sel_registerName("answer".ptr), xx answer_imp, "i@:".ptr);
|
d := NSObject.description();
|
||||||
class_addMethod(metacls, sel_registerName("add:b:".ptr), xx add_imp, "i@:ii".ptr);
|
if d != null {
|
||||||
objc_registerClassPair(cls);
|
print("description non-null\n");
|
||||||
|
}
|
||||||
n := SxProbeStatic.answer();
|
|
||||||
print("answer() = {}\n", n);
|
|
||||||
s := SxProbeStatic.add(7, 35);
|
|
||||||
print("add(7, 35) = {}\n", s);
|
|
||||||
}
|
}
|
||||||
inline if OS != .macos {
|
inline if OS != .macos {
|
||||||
print("skipped (not macos)\n");
|
print("skipped (not macos)\n");
|
||||||
|
|||||||
@@ -275,6 +275,11 @@ pub const LLVMEmitter = struct {
|
|||||||
// Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5).
|
// Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5).
|
||||||
self.emitObjcSelectorInit();
|
self.emitObjcSelectorInit();
|
||||||
|
|
||||||
|
// Pass 2.5b: Emit Obj-C class-object init constructor (Phase 3.1).
|
||||||
|
// Same shape as the selector init — populates the per-module
|
||||||
|
// cached `Class*` slots via `objc_getClass` at module-init time.
|
||||||
|
self.emitObjcClassInit();
|
||||||
|
|
||||||
// Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at
|
// Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at
|
||||||
// startup so relative asset paths work when Finder/`open`
|
// startup so relative asset paths work when Finder/`open`
|
||||||
// launches the binary with CWD=/. Non-bundled binaries no-op.
|
// launches the binary with CWD=/. Non-bundled binaries no-op.
|
||||||
@@ -392,6 +397,133 @@ pub const LLVMEmitter = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 3.1 companion to `emitObjcSelectorInit`. Walks
|
||||||
|
/// `module.objc_class_cache` and synthesizes a constructor that
|
||||||
|
/// populates each cached `Class*` slot via `objc_getClass(name)`
|
||||||
|
/// exactly once at module-init. Registered in `@llvm.global_ctors`
|
||||||
|
/// AND injected at the top of `main()` for the ORC JIT path.
|
||||||
|
fn emitObjcClassInit(self: *LLVMEmitter) void {
|
||||||
|
if (self.ir_mod.objc_class_cache.items.len == 0) return;
|
||||||
|
|
||||||
|
// Lazy-declare objc_getClass(name: *u8) -> *void.
|
||||||
|
const get_class_name = "objc_getClass";
|
||||||
|
const get_class_z = self.alloc.dupeZ(u8, get_class_name) catch unreachable;
|
||||||
|
defer self.alloc.free(get_class_z);
|
||||||
|
var get_class_fn = c.LLVMGetNamedFunction(self.llvm_module, get_class_z.ptr);
|
||||||
|
var get_class_ty: c.LLVMTypeRef = undefined;
|
||||||
|
if (get_class_fn == null) {
|
||||||
|
var params: [1]c.LLVMTypeRef = .{self.cached_ptr};
|
||||||
|
get_class_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 1, 0);
|
||||||
|
get_class_fn = c.LLVMAddFunction(self.llvm_module, get_class_z.ptr, get_class_ty);
|
||||||
|
c.LLVMSetLinkage(get_class_fn, c.LLVMExternalLinkage);
|
||||||
|
} else {
|
||||||
|
get_class_ty = c.LLVMGlobalGetValueType(get_class_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor: void __sx_objc_class_init().
|
||||||
|
var no_params: [0]c.LLVMTypeRef = .{};
|
||||||
|
const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0);
|
||||||
|
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_class_init", ctor_ty);
|
||||||
|
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
|
||||||
|
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
|
||||||
|
c.LLVMPositionBuilderAtEnd(self.builder, entry);
|
||||||
|
|
||||||
|
for (self.ir_mod.objc_class_cache.items) |entry_kv| {
|
||||||
|
const class_name = entry_kv.name;
|
||||||
|
const slot_gid = entry_kv.slot;
|
||||||
|
const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue;
|
||||||
|
|
||||||
|
// Class-name C-string.
|
||||||
|
const name_z = self.alloc.allocSentinel(u8, class_name.len, 0) catch continue;
|
||||||
|
defer self.alloc.free(name_z);
|
||||||
|
@memcpy(name_z[0..class_name.len], class_name);
|
||||||
|
const str_const = c.LLVMConstStringInContext(self.context, name_z.ptr, @intCast(class_name.len), 0);
|
||||||
|
const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_CLASS_NAME_");
|
||||||
|
c.LLVMSetInitializer(str_global, str_const);
|
||||||
|
c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage);
|
||||||
|
c.LLVMSetGlobalConstant(str_global, 1);
|
||||||
|
c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr);
|
||||||
|
|
||||||
|
var call_args: [1]c.LLVMValueRef = .{str_global};
|
||||||
|
const class_val = c.LLVMBuildCall2(self.builder, get_class_ty, get_class_fn, &call_args, 1, "cls");
|
||||||
|
_ = c.LLVMBuildStore(self.builder, class_val, slot_global);
|
||||||
|
}
|
||||||
|
_ = c.LLVMBuildRetVoid(self.builder);
|
||||||
|
|
||||||
|
// Register in @llvm.global_ctors for AOT + inject into main for ORC JIT.
|
||||||
|
const i32_ty = self.cached_i32;
|
||||||
|
const ptr_ty = self.cached_ptr;
|
||||||
|
var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty };
|
||||||
|
const ctor_struct_ty = c.LLVMStructTypeInContext(self.context, &ctor_field_types, 3, 0);
|
||||||
|
var ctor_fields: [3]c.LLVMValueRef = .{
|
||||||
|
c.LLVMConstInt(i32_ty, 65535, 0),
|
||||||
|
ctor,
|
||||||
|
c.LLVMConstNull(ptr_ty),
|
||||||
|
};
|
||||||
|
const ctor_entry = c.LLVMConstNamedStruct(ctor_struct_ty, &ctor_fields, 3);
|
||||||
|
|
||||||
|
// Append-vs-replace the existing global_ctors. Selector init may
|
||||||
|
// have created `@llvm.global_ctors` already — extend its array
|
||||||
|
// rather than overwriting.
|
||||||
|
const existing_z = "llvm.global_ctors";
|
||||||
|
const existing = c.LLVMGetNamedGlobal(self.llvm_module, existing_z);
|
||||||
|
if (existing != null) {
|
||||||
|
const existing_init = c.LLVMGetInitializer(existing);
|
||||||
|
const existing_arr_ty = c.LLVMGlobalGetValueType(existing);
|
||||||
|
const old_count = c.LLVMGetArrayLength(existing_arr_ty);
|
||||||
|
const new_count: c_uint = old_count + 1;
|
||||||
|
var new_entries = std.ArrayList(c.LLVMValueRef).empty;
|
||||||
|
defer new_entries.deinit(self.alloc);
|
||||||
|
var i: c_uint = 0;
|
||||||
|
while (i < old_count) : (i += 1) {
|
||||||
|
new_entries.append(self.alloc, c.LLVMGetAggregateElement(existing_init, i)) catch unreachable;
|
||||||
|
}
|
||||||
|
new_entries.append(self.alloc, ctor_entry) catch unreachable;
|
||||||
|
const new_arr_ty = c.LLVMArrayType2(ctor_struct_ty, new_count);
|
||||||
|
const new_init = c.LLVMConstArray2(ctor_struct_ty, new_entries.items.ptr, new_count);
|
||||||
|
const new_global = c.LLVMAddGlobal(self.llvm_module, new_arr_ty, "llvm.global_ctors.new");
|
||||||
|
c.LLVMSetInitializer(new_global, new_init);
|
||||||
|
c.LLVMSetLinkage(new_global, c.LLVMAppendingLinkage);
|
||||||
|
c.LLVMSetValueName2(existing, "llvm.global_ctors.old", "llvm.global_ctors.old".len);
|
||||||
|
c.LLVMSetValueName2(new_global, "llvm.global_ctors", "llvm.global_ctors".len);
|
||||||
|
c.LLVMDeleteGlobal(existing);
|
||||||
|
} else {
|
||||||
|
const ctors_arr_ty = c.LLVMArrayType2(ctor_struct_ty, 1);
|
||||||
|
var ctor_entries: [1]c.LLVMValueRef = .{ctor_entry};
|
||||||
|
const ctors_init = c.LLVMConstArray2(ctor_struct_ty, &ctor_entries, 1);
|
||||||
|
const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors");
|
||||||
|
c.LLVMSetInitializer(ctors_global, ctors_init);
|
||||||
|
c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORC JIT injection: same trick as emitObjcSelectorInit. Inject a
|
||||||
|
// direct call from main's entry so the JIT path populates the
|
||||||
|
// slots too. Must run AFTER the selector init's main injection
|
||||||
|
// (selectors are needed independently of class objects), so we
|
||||||
|
// place this call AFTER the first instruction (which is the
|
||||||
|
// selector-init call, if present) rather than at the very top.
|
||||||
|
const main_z = "main";
|
||||||
|
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
|
||||||
|
if (main_fn != null) {
|
||||||
|
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
|
||||||
|
// Walk past any existing init calls (selector init etc.) so
|
||||||
|
// class init runs after them. The order within main's prelude
|
||||||
|
// doesn't matter functionally (the two caches are independent),
|
||||||
|
// but stable ordering keeps IR snapshots deterministic.
|
||||||
|
var insert_before = c.LLVMGetFirstInstruction(entry_bb);
|
||||||
|
while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) {
|
||||||
|
if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break;
|
||||||
|
}
|
||||||
|
if (insert_before != null) {
|
||||||
|
c.LLVMPositionBuilderBefore(self.builder, insert_before);
|
||||||
|
} else {
|
||||||
|
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
|
||||||
|
}
|
||||||
|
var no_args: [0]c.LLVMValueRef = .{};
|
||||||
|
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// On macOS, emit a startup helper that chdir's to the .app bundle's
|
/// On macOS, emit a startup helper that chdir's to the .app bundle's
|
||||||
/// `Contents/Resources` directory when the executable lives inside a
|
/// `Contents/Resources` directory when the executable lives inside a
|
||||||
/// `.app/Contents/MacOS/` path. Lets relative asset paths like
|
/// `.app/Contents/MacOS/` path. Lets relative asset paths like
|
||||||
|
|||||||
@@ -4196,6 +4196,33 @@ pub const Lowering = struct {
|
|||||||
return gid;
|
return gid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Intern an Obj-C class name into a module-scoped `Class*` slot.
|
||||||
|
/// First call creates the global; subsequent calls return the same
|
||||||
|
/// `GlobalId`. emit_llvm.zig walks `module.objc_class_cache` and
|
||||||
|
/// synthesizes a constructor that populates each slot via
|
||||||
|
/// `objc_getClass` exactly once at module load.
|
||||||
|
///
|
||||||
|
/// Slot name matches clang's convention: `OBJC_CLASSLIST_REFERENCES_<Cls>`.
|
||||||
|
fn internObjcClassObject(self: *Lowering, class_name: []const u8) inst_mod.GlobalId {
|
||||||
|
if (self.module.lookupObjcClass(class_name)) |gid| return gid;
|
||||||
|
|
||||||
|
var mangled = std.ArrayList(u8).empty;
|
||||||
|
defer mangled.deinit(self.alloc);
|
||||||
|
mangled.appendSlice(self.alloc, "OBJC_CLASSLIST_REFERENCES_") catch unreachable;
|
||||||
|
mangled.appendSlice(self.alloc, class_name) catch unreachable;
|
||||||
|
const slot_name = self.module.types.internString(mangled.items);
|
||||||
|
const vptr_ty = self.module.types.ptrTo(.void);
|
||||||
|
const gid = self.module.addGlobal(.{
|
||||||
|
.name = slot_name,
|
||||||
|
.ty = vptr_ty,
|
||||||
|
.init_val = .null_val,
|
||||||
|
.is_extern = false,
|
||||||
|
.is_const = false,
|
||||||
|
});
|
||||||
|
self.module.appendObjcClass(class_name, gid);
|
||||||
|
return gid;
|
||||||
|
}
|
||||||
|
|
||||||
/// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern.
|
/// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern.
|
||||||
/// Cached per Lowering instance so multiple `#objc_call` sites share
|
/// Cached per Lowering instance so multiple `#objc_call` sites share
|
||||||
/// one declaration.
|
/// one declaration.
|
||||||
@@ -4543,6 +4570,57 @@ pub const Lowering = struct {
|
|||||||
} }, ret_ty);
|
} }, ret_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lower `Cls.static_method(args)` on an `#objc_class` /
|
||||||
|
/// `#objc_protocol` alias. Loads the class object through the
|
||||||
|
/// module-scoped cached slot (populated by `objc_getClass` at
|
||||||
|
/// module-init) and dispatches `objc_msg_send` with the same
|
||||||
|
/// selector mangling as instance methods (Phase 3.0).
|
||||||
|
fn lowerObjcStaticCall(
|
||||||
|
self: *Lowering,
|
||||||
|
fcd: *const ast.ForeignClassDecl,
|
||||||
|
method: ast.ForeignMethodDecl,
|
||||||
|
method_args: []const Ref,
|
||||||
|
span: ast.Span,
|
||||||
|
) Ref {
|
||||||
|
const arity = method_args.len;
|
||||||
|
const derived = self.deriveObjcSelector(method.name, arity);
|
||||||
|
|
||||||
|
if (arity > 0 and derived.keyword_count != arity) {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
d.addFmt(
|
||||||
|
.err,
|
||||||
|
span,
|
||||||
|
"Obj-C selector for static call '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")` once that lands (3.2)",
|
||||||
|
.{ fcd.name, method.name, derived.keyword_count, arity, arity },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ref.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||||
|
|
||||||
|
const vptr_ty = self.module.types.ptrTo(.void);
|
||||||
|
|
||||||
|
// Load the class object from its module-scoped cached slot.
|
||||||
|
// `objc_getClass(<name>)` runs once at module-init via the
|
||||||
|
// constructor emit_llvm synthesizes (see `emitObjcClassInit`).
|
||||||
|
const class_slot_gid = self.internObjcClassObject(fcd.foreign_path);
|
||||||
|
const class_slot_ptr = self.builder.emit(.{ .global_addr = class_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
||||||
|
const class_obj = self.builder.emit(.{ .load = .{ .operand = class_slot_ptr } }, vptr_ty);
|
||||||
|
|
||||||
|
// Load the SEL from its slot.
|
||||||
|
const sel_slot_gid = self.internObjcSelector(derived.sel);
|
||||||
|
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
||||||
|
const sel = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, vptr_ty);
|
||||||
|
|
||||||
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
||||||
|
return self.builder.emit(.{ .objc_msg_send = .{
|
||||||
|
.recv = class_obj,
|
||||||
|
.sel = sel,
|
||||||
|
.args = args_owned,
|
||||||
|
} }, ret_ty);
|
||||||
|
}
|
||||||
|
|
||||||
/// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier
|
/// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier
|
||||||
/// with `static new :: (...) -> *Self;` — JNI constructor dispatch:
|
/// with `static new :: (...) -> *Self;` — JNI constructor dispatch:
|
||||||
/// `FindClass + GetMethodID("<init>", "(args)V") + NewObject(env,
|
/// `FindClass + GetMethodID("<init>", "(args)V") + NewObject(env,
|
||||||
@@ -4559,6 +4637,13 @@ pub const Lowering = struct {
|
|||||||
method_args: []const Ref,
|
method_args: []const Ref,
|
||||||
span: ast.Span,
|
span: ast.Span,
|
||||||
) Ref {
|
) Ref {
|
||||||
|
// Obj-C static dispatch (Phase 3 step 3.1). `Cls.static_method(args)`
|
||||||
|
// on an `#objc_class` alias loads the class object through a
|
||||||
|
// module-scoped cached slot (populated once per module via
|
||||||
|
// `objc_getClass`) and dispatches with the derived selector.
|
||||||
|
if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) {
|
||||||
|
return self.lowerObjcStaticCall(fcd, method, method_args, span);
|
||||||
|
}
|
||||||
if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) {
|
if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) {
|
||||||
if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)});
|
if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)});
|
||||||
return Ref.none;
|
return Ref.none;
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ pub const Module = struct {
|
|||||||
/// hashtable rehash). `#objc_call` lowering uses
|
/// hashtable rehash). `#objc_call` lowering uses
|
||||||
/// `lookupObjcSelector` / `appendObjcSelector` to read/write it.
|
/// `lookupObjcSelector` / `appendObjcSelector` to read/write it.
|
||||||
objc_selector_cache: std.ArrayList(ObjcSelectorEntry),
|
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,
|
alloc: Allocator,
|
||||||
/// True when this module's program imports `std.sx` (and therefore
|
/// True when this module's program imports `std.sx` (and therefore
|
||||||
/// has the `Context` type). Set by lowering's Pass 0 pre-scan. Read
|
/// 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,
|
has_implicit_ctx: bool = false,
|
||||||
|
|
||||||
pub const ObjcSelectorEntry = struct { sel: []const u8, slot: GlobalId };
|
pub const ObjcSelectorEntry = struct { sel: []const u8, slot: GlobalId };
|
||||||
|
pub const ObjcClassEntry = struct { name: []const u8, slot: GlobalId };
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) Module {
|
pub fn init(alloc: Allocator) Module {
|
||||||
return .{
|
return .{
|
||||||
@@ -49,6 +58,7 @@ pub const Module = struct {
|
|||||||
.globals = std.ArrayList(Global).empty,
|
.globals = std.ArrayList(Global).empty,
|
||||||
.impl_table = ImplTable.init(alloc),
|
.impl_table = ImplTable.init(alloc),
|
||||||
.objc_selector_cache = std.ArrayList(ObjcSelectorEntry).empty,
|
.objc_selector_cache = std.ArrayList(ObjcSelectorEntry).empty,
|
||||||
|
.objc_class_cache = std.ArrayList(ObjcClassEntry).empty,
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -61,6 +71,7 @@ pub const Module = struct {
|
|||||||
self.globals.deinit(self.alloc);
|
self.globals.deinit(self.alloc);
|
||||||
self.impl_table.deinit();
|
self.impl_table.deinit();
|
||||||
self.objc_selector_cache.deinit(self.alloc);
|
self.objc_selector_cache.deinit(self.alloc);
|
||||||
|
self.objc_class_cache.deinit(self.alloc);
|
||||||
self.types.deinit();
|
self.types.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +89,18 @@ pub const Module = struct {
|
|||||||
self.objc_selector_cache.append(self.alloc, .{ .sel = sel, .slot = slot }) catch unreachable;
|
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 {
|
pub fn addFunction(self: *Module, func: Function) FuncId {
|
||||||
const id = FuncId.fromIndex(@intCast(self.functions.items.len));
|
const id = FuncId.fromIndex(@intCast(self.functions.items.len));
|
||||||
self.functions.append(self.alloc, func) catch unreachable;
|
self.functions.append(self.alloc, func) catch unreachable;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1
|
0
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-05-static.sx:41:14: error: static calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
class non-null
|
||||||
/Users/agra/projects/sx/examples/ffi-objc-dsl-05-static.sx:43:14: error: static calls on 'objc_class' runtime not yet supported (Phase 3/4)
|
description non-null
|
||||||
|
|||||||
Reference in New Issue
Block a user