Unapplied Actor-isolated Method References
Haven’t you been missing lately a super versatile mechanism - the heart of tons of abstractions - like unapplied method references for actors? E.g. for certain dependency injection setups? Because I have. But here’s the good news: the fix is already chilling in Swift’s repo, waiting patiently on the main branch. No word yet on which version it’ll land in.
If this bug has you itching to tidy up your knowledge on the topic like it did for me, come take a peek at the infographic below! It sneaks in a quick summary of the issue itself, the isolation context, and those unapplied/partially applied method references (in case that’s new to you).
Unapplied & Partially Applied Actor Method References
BankAccount.deposit(amount:) · BankAccount().deposit
// Unapplied (type-level):
let fn = String.uppercased
// (String) -> () -> String
// Partially applied (instance):
let fn2 = "hello".uppercased
// () -> String
// Works in any context actor BankAccount {
func deposit(amount: Double) { ... }
} // implicit: isolated self
let fn = BankAccount.deposit(amount:)
// (isolated BankAccount)
// -> (Double) -> Void
// SE-0313 — 'isolated' in type actor BankAccount {
func deposit(amount: Double) { ... }
}
func apply(account: isolated BankAccount) {
let fn = account.deposit
// (Double) -> Void
// ✓ already isolated to account
} // — Unapplied type-level reference (the SE-0313 regression):
let fn = BankAccount.deposit(amount:)
// warning/error: call to actor-isolated instance method
// 'deposit(amount:)' in a synchronous nonisolated context
// Root cause: broken thunk emission for isolated parameters
// Fixed by PR #86904.
// — Instance-bound in nonisolated context:
let fn2 = BankAccount().deposit
// error: can not be partially applied (PR #34051)
// ✓ correct only when called from within that actor nonisolated func withdraw(
amount: Double) { ... }
// Both forms ✓:
BankAccount.withdraw ✓
BankAccount().withdraw ✓
// No 'isolated' in type — never affected // implicit isolated self:
func deposit(
amount: Double) { ... }
BankAccount.deposit ❌→✓
// thunk bug — Fixed by PR #86904
actorInstance.deposit
// ✓ already isolated to that actor
// (actorInstance can be self)
// ❌ outside or on a different actor // also isolated by default:
func load() async { ... }
BankAccount.load ✓
BankAccount().load ✓
// async hop is available — both forms work
// exception: @preconcurrency + global actor
// (e.g. SomeView: View) fires a warning // — Unapplied type-level reference (now fixed):
let fn = BankAccount.deposit(amount:)
// ✓ type: (isolated BankAccount) -> (Double) -> Void
// Sync usage within actor context (no await):
extension BankAccount {
func batch(_ amounts: [Double]) {
let op = BankAccount.deposit(amount:)
amounts.forEach { op(self)($0) } // ✓ sync
}
}
// — Partial application: context-dependent
// actorInstance.deposit ✓ (already isolated to it)
// BankAccount().deposit ❌ (outside or different actor) // BankAccount.deposit(amount:) has type:
// (isolated BankAccount) -> (Double) -> Void
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// 'isolated BankAccount':
// The actor instance you supply as 1st argument.
// 'isolated' means the call runs INSIDE that
// actor — synchronously, no await needed.
// -> (Double) -> Void (the rest of the args):
// Curried: pass the actor → get back a function
// that accepts the original argument(s). // Inside the actor — pass self as the instance:
extension BankAccount {
func batch(_ amounts: [Double]) {
let op = BankAccount.deposit(amount:)
// op type: (isolated BankAccount)
// -> (Double) -> Void
amounts.forEach {
op(self)($0) // ✓ sync, no await
// same as: self.deposit(amount: $0)
}
}
} If you’re keen to dive deeper (but not too deep) into the “thunk emissions” puzzle and sink your teeth into it, this infographic below will help you wrap your head around the whole thing.
Actor Method Thunks — The Compiler Bug Behind SE-0313
BankAccount.deposit(amount:) → compiler-generated wrapper
// Any method can be used as a value:
let fn = String.uppercased
// type: (String) -> () -> String
fn("hello")()
// → "HELLO" // Compiler generates (conceptually):
func __thunk(_ s: String)
-> () -> String {
return { s.uppercased() }
}
// Nonisolated — no actor involved // Actor-isolated method reference:
let fn = BankAccount.deposit(amount:)
// (isolated BankAccount)
// -> (Double) -> Void
// 'isolated' stays in the type // Compiler should generate (conceptually):
func __thunk(
account: isolated BankAccount
) -> (Double) -> Void {
return { amount in
account.deposit(amount: amount)
// ✓ sync on account's executor
}
}
// Thunk isolation = first param's isolation // Step 1: Parse function type
// (isolated BankAccount) -> (Double) -> Void ✓
// Step 2: Determine thunk isolation ← BROKEN HERE
// Isolated first param → thunk should inherit isolation
// COMPILER: no machinery to propagate isolated info ❌
// Step 3: Emit IR
// ❌ Wrong IR / broken types at call site
// ❌ error/warning: 'actor-isolated method deposit(amount:)
// in synchronous nonisolated context'
// PR description: 'it won't be possible to compute
// a correct isolation for the thunk' // PR #86904 added the missing machinery:
// 1. Detect isolated first parameter
// in the function type
// 2. Propagate its isolation to the thunk
// → thunk isolation = isolated(account)
// 3. Emit correct IR:
// executor hop ✓
// isolated self forwarded ✓
// correct type in output ✓ // Unapplied form now compiles:
let fn = BankAccount.deposit(amount:)
// ✓ (isolated BankAccount) -> (Double) -> Void
// Partial application: context-dependent
// actorInstance.deposit ✓ (already isolated to it)
// BankAccount().deposit ❌ (from outside)
// Thunk fixed for unapplied form ✓
// → SE-0313 unapplied refs work as intended