Skip to content

Investigate compile-time discovery of all imported KMP observable properties #14

Description

@sonmbol

Problem

The current macro requires every observable imported Kotlin property to be selected explicitly:

@KMPObservable(
    ArticleViewModel.self,
    fields: \.articleState, \.permissionsState
)
extension ArticleViewModel: @retroactive KMPStaticallyObservable {}

The ideal consumer API would allow:

@KMPObservable
extension ArticleViewModel: @retroactive KMPStaticallyObservable {}

and discover every supported exported observable property at compile time.

Hard constraints

Any proposed solution must:

  • perform discovery at build/compile time, never through runtime reflection;
  • avoid Objective-C selectors, swizzling, Mirror, objc_msgSend, or method interception;
  • preserve typed key paths and compile-time diagnostics;
  • introduce no per-emission reflection, lookup, allocation, or actor hop;
  • keep SKIE and NativeCoroutines integrations isolated from one another;
  • avoid generated Swift source files that consumers must add to Xcode manually;
  • preserve incremental-build correctness when the KMP framework changes;
  • support large applications with hundreds of ViewModels;
  • preserve the existing explicit fields: API as a deterministic fallback.

Why the current macro cannot simply discover the fields

Swift macros operate on the syntax supplied to the expansion. They do not receive a general semantic reflection API for enumerating members of an imported Kotlin/Objective-C framework type. Build ordering alone does not change this limitation: the KMP/SKIE framework may exist before Swift compilation, but the macro expansion context still cannot query all imported members safely.

Approaches already considered

Swift macro-only discovery

Rejected as currently unavailable. An attached macro can inspect the annotated extension syntax, but not enumerate arbitrary members of the imported ViewModel declaration.

Freestanding or peer macro generating the conformance

Swift restricts the external names introduced by freestanding and peer macros. Earlier experiments found that these forms cannot legally synthesize an arbitrary extension/conformance for an unrelated imported type. Attaching the macro directly to the extension is the supported shape.

Objective-C runtime discovery

Rejected. SKIE-enhanced Swift declarations are not reliably represented by Objective-C property metadata, generic StateFlow types are erased or incomplete, and runtime lookup would undermine type safety, performance, and exporter isolation.

Runtime Swift reflection

Rejected. Mirror does not provide the required key paths or writable/current-value type relationships and would add runtime work and fragile implementation coupling.

Xcode/SwiftPM generator

Technically feasible, but previously removed because it introduces build-order and incremental-build complexity, DerivedData outputs, sandboxing concerns, plugin/toolchain coupling, and generated-file debugging. A generator also cannot inject information "into" a macro expansion invisibly: macros are compiler plugins, not a mutable source-generation destination. Any metadata must be made available through a compiler-visible source/module or another supported input mechanism.

Kotlin-generated metadata

Potentially viable, but adds Kotlin-side integration and may compromise the bridge's Swift-only dependency goal. This option should be evaluated only if metadata can remain exporter-neutral, deterministic, and optional.

Investigation areas

  • New Swift macro/compiler APIs in future Swift toolchains that expose imported declaration members.
  • SKIE-supported compile-time metadata surfaces intended for tooling.
  • A SwiftPM build-tool plugin that produces private compiler-visible metadata without project-managed files.
  • Kotlin/Native-generated static descriptors that do not require a bridge runtime dependency.
  • A hybrid system where explicit fields remain source-compatible and generated descriptors are an opt-in optimization.

Performance and correctness risks

Automatic discovery could accidentally observe event streams, mutable implementation details, or expensive flows that were never intended to drive SwiftUI. Observing every export may increase collectors, Kotlin/ARC cross-boundary references, equality work, and global invalidations. Discovery must classify read-only state, writable state, and event streams correctly rather than treating every flow identically.

Any generated metadata must be invalidated when the KMP binary changes. Stale metadata is worse than explicit fields because it can compile against an outdated interface or silently miss state.

Acceptance criteria

  • @KMPObservable extension ViewModel: ... discovers supported state without runtime inspection.
  • Clean and incremental builds regenerate/revalidate metadata correctly after adding, removing, or changing a Kotlin property.
  • Unsupported or ambiguous properties produce compile-time diagnostics.
  • Explicit fields: and event-selection APIs remain available.
  • No Objective-C runtime APIs or Swift reflection exist in the production path.
  • No additional per-emission allocations, tasks, collectors, or View Graph invalidations compared with explicit key paths.
  • Benchmarks and real SKIE/NativeCoroutines fixtures prove parity with the existing hot path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions