ffi M1.2 A.7: open the dispatch gate — sx-defined class methods callable

Delete the bail at lower.zig:4407 that diagnosed sx-defined Obj-C
class dispatch as 'not yet supported'. Both foreign and
sx-defined '#objc_class' decls now flow through the same
'lowerObjcMethodCall' path — instance methods on sx-defined
classes dispatch via objc_msgSend, and the registered IMP
trampolines (M1.2 A.4b.iii) route to the sx bodies.

The runtime non-Obj-C branch (.swift_class / .swift_struct /
.swift_protocol) keeps its 'not yet supported' diagnostic;
M1.2 only addresses the Obj-C runtimes.

Constructor reorder in emit_llvm: emitObjcDefinedClassInit
runs BEFORE emitObjcClassInit. Otherwise the Phase 3.1
class-cache populator calls objc_getClass("SxFoo") before our
constructor registers the class — cache slot stored null and
'SxFoo.method()' dispatched against a null class pointer.

ffi-objc-defined-class-01-instance.sx (the integration test
from the plan) now runs the full lifecycle on macOS:

  f := SxFoo.alloc()    // synthesized +alloc IMP fires
  f.bump()              // dispatch → IMP trampoline → sx body
  f.bump()              // state persists across calls
  f.bump()
  f.get()               // → 3
  release_fn(f, sel_release)  // synthesized -dealloc fires

The user declares 'alloc :: () -> *SxFoo;' bodyless to give the
synthesized +alloc IMP a typed contract at sx call sites —
same convention as foreign classes today.

M1.2 complete: A.0 A.1 A.2 A.3 A.4 A.4b.i A.4b.ii A.4b.iii
A.5 A.6 A.7. End-to-end class-synthesis foundation works.

177 example tests pass (+1 from the integration test). zig
build test green.
This commit is contained in:
agra
2026-05-25 23:29:55 +03:00
parent c107aa4e21
commit 51277afadf
5 changed files with 88 additions and 22 deletions

View File

@@ -275,19 +275,20 @@ pub const LLVMEmitter = struct {
// Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5).
self.emitObjcSelectorInit();
// Pass 2.5b: Emit Obj-C class-object init constructor (Phase 3.1).
// Pass 2.5b: Emit Obj-C class-pair registration constructor for
// sx-defined classes (M1.2 A.4+). Runs BEFORE the foreign
// class-cache populator (2.5c) so a sx-defined class is already
// registered with the Obj-C runtime by the time
// `objc_getClass(\"SxFoo\")` runs to populate the Phase 3.1
// class-object cache — otherwise the cache slot would store
// null and `SxFoo.method()` dispatches against null.
self.emitObjcDefinedClassInit();
// Pass 2.5c: 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.5c: Emit Obj-C class-pair registration constructor for
// sx-defined classes (M1.2 A.4). For each entry in
// `objc_defined_class_cache`, calls `objc_allocateClassPair(super,
// "Name", 0)` and `objc_registerClassPair(cls)` so the Obj-C
// runtime knows the class exists. Methods, ivars, and the +alloc
// override come in A.4b / A.5 / A.6.
self.emitObjcDefinedClassInit();
// Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at
// startup so relative asset paths work when Finder/`open`
// launches the binary with CWD=/. Non-bundled binaries no-op.

View File

@@ -4431,22 +4431,24 @@ pub const Lowering = struct {
return Ref.none;
};
if (!fcd.is_foreign) {
if (self.diagnostics) |d| {
d.addFmt(.err, span, "sx-defined foreign classes can't yet be dispatched into (class '{s}' missing '#foreign' modifier? — runtime synthesis is a follow-up)", .{fcd.name});
}
return Ref.none;
}
// Obj-C instance dispatch (Phase 3 step 3.0). `inst.method(args)` on
// an `#objc_class` / `#objc_protocol` receiver derives a selector
// from the sx method name (default mangling: split on `_`, each
// piece becomes a keyword with a trailing `:`; niladic stays
// verbatim) and lowers to `objc_msg_send`. The Swift runtimes
// still bail — Phase 4.
// Obj-C instance dispatch (Phase 3 step 3.0 + M1.2 A.7).
// `inst.method(args)` on an `#objc_class` / `#objc_protocol`
// receiver derives a selector from the sx method name (default
// mangling: split on `_`, each piece becomes a keyword with a
// trailing `:`; niladic stays verbatim) and lowers to
// `objc_msg_send`. Both foreign and sx-defined classes flow
// through the same path — sx-defined classes have their IMPs
// registered at module-init (M1.2 A.4b.iii) so `objc_msgSend`
// finds them. The Swift runtimes still bail — Phase 4.
if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) {
return self.lowerObjcMethodCall(fcd, method, target, method_args, span);
}
if (!fcd.is_foreign) {
if (self.diagnostics) |d| {
d.addFmt(.err, span, "sx-defined classes on non-Obj-C runtimes can't yet be dispatched into (class '{s}', runtime '{s}')", .{ fcd.name, @tagName(fcd.runtime) });
}
return Ref.none;
}
if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) {
if (self.diagnostics) |d| {
d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)});