Share
Embed
Swift Concurrency Type System
Swift Concurrency Type System / try! Swift Tokyo 2026
Paper: https://github.com/inamiy/swift-concurrency-type-system/
Yasuhiro Inami
April 13, 2026
Transcript
46 slides
-
Swift Concurrency Keywords async await actor @globalActor @MainActor Task TaskGroup
@TaskLocal AsyncSequence Continuation isolated nonisolated nonisolated(unsafe) nonisolated(nonsending) @isolated(any) @concurrent #isolation Sendable @Sendable sending 20+ concurrency-related types, keywords, and attributes -
What makes Swift Concurrency Hard? Especially when closures are involved:
Swift compiler knows. But do you know why? -
Closure Conversion This is subtyping / coercion between concurrency annotations
Given f: @A () -> Void , can we assign to / use in g: @B () -> Void ? N o t e : @ A a n d @ B c a n b e A N Y c o n c u r re n c y a t t r i b u t e s , n o t o n l y a b o u t g l o b a l a c t o r i s o l a t i o n -
Sync Conversion Matrix From ↓ \ To → ~iso @S
@MA @MA @S @iso? @iso? @S iso(a) iso(a) @S ~iso ✅ ❌ ❌ ❌ ✅ ❌ ❌ ❌ @S ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @MA ❌ ❌ ✅ ✅ ✅ ✅ ❌ ❌ @MA @S ❌ ❌ ✅ ✅ ✅ ✅ ❌ ❌ @iso? ⚠️ ❌ ❌ ❌ ✅ ❌ ❌ ❌ @iso? @S ⚠️ ⚠️ ❌ ❌ ✅ ✅ ❌ ❌ iso(a) ❌ ❌ ❌ ❌ ❌ ❌ ✅ ❌ iso(a) @S ❌ ❌ ❌ ❌ ❌ ❌ ✅ ✅ ~ i s o = n o n i s o l a t e d , @ S = @ S e n d a b l e , @ M A = @ M a i n A c t o r, @ i s o ? = @ i s o l a t e d ( a n y ) , i s o ( a ) = i s o l a t e d Lo c a l A c t o r, ⚠️ = w a r n i n g ( f u t u re e r ro r ) -
Sync Conversion Sendable Sync nonisolated @Sendable sync @MainActor (@Sendable) sync
@isolated(any) @Sendable sync isolated LocalActor @Sendable sync @isolated(any) sync nonisolated sync isolated LocalActor sync ⚠ deprecated -
Async Conversion Matrix From ↓ \ To → ~iso @S
@MA @MA @S @iso? @iso? @S @conc @conc @S ~iso ✅ ❌ ❌ ❌ ✅ ❌ ✅ ❌ @S ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @MA ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @MA @S ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @iso? ✅ ❌ ❌ ❌ ✅ ❌ ✅ ❌ @iso? @S ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @conc ✅ ❌ ❌ ❌ ✅ ❌ ✅ ❌ @conc @S ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ @ c o n c = @ c o n c u r re n t . i s o ( a ) o m i t t e d i n t h i s t a b l e ( s a m e a s " s y n c " ) . -
Async Conversion Sendable Async Non-Sendable Async nonisolated @Sendable async @MainActor
(@Sendable) async @concurrent @Sendable async @isolated(any) @Sendable async isolated LocalActor @Sendable async nonisolated async @concurrent async @isolated(any) async isolated LocalActor async -
Isolation Conversion Sync Conversion Async Conversion Sendable Sync Sendable Async
Non-Sendable Async nonisolated @Sendable sync @MainActor (@Sendable) sync @isolated(any) @Sendable sync isolated LocalActor @Sendable sync @isolated(any) sync nonisolated sync isolated LocalActor sync nonisolated @Sendable async @MainActor (@Sendable) async @concurrent @Sendable async @isolated(any) @Sendable async isolated LocalActor @Sendable async nonisolated async @concurrent async @isolated(any) async isolated LocalActor async ⚠ deprecated Async Lift -
Closure Conversion Rules @Sendable is stronger closure than non-@Sendable nonisolated
is stronger closure than isolated @isolated(any) is the weakest closure (most generic) isolated LocalActor is tied to extra isolated param, which can be added but cannot be reduced for conversion Sync closure conversion is the most painful due to await being impossible Async closure conversion is easier to achieve via await (actor hop) Sync to Async conversion is always possible (but not vice-versa) -
Answer Where does the code run? → @BarActor Where does
the value live? → @FooActor @MainActor is not used in handler 😛 Isolation Domain (capability, execution context) and Isolation Region (value isolation) are different! -
Axis 1: Capability ( ) Where code runs — Actor's
execution context Determines what values actor can access Controls whether await is needed for cross-isolation calls @κ -
Function Annotation ( ) & Capability ( ) @φ ::=
s e n d a b i l i t y @σ i s o l a t i o n @ι (Sendability) (Isolation Domain) (none) (none) ( @nonisolated ) @Sendable @isolated(a) (e.g. @MainActor ) @isolated(any) @concurrent (async only) Capability @nonisolated | @isolated(a) ( ) @φ @κ @σ @ι ⋅ ⋅ @κ ::= @κ ⊆ @ι -
Axis 2: Region ( ) Where data lives — Actor's
value isolation (SE-0414) ρ ::= ns disconnected ∣ isolated(a) ∣ task ∣ invalid Meaning Free to travel (not yet bound) Bound to actor Bound to current async task Tracks which isolation a NonSendable value belongs to. ρ ρ n s disconnected isolated(a) a task -
Region in Action Region merge: Once bound, a value cannot
be sent across isolation boundaries. disconnected ⊔ isolated(a) = isolated(a) -
Region Merge ( ) invalid invalid ρ ⊔ 1
ρ 2 ρ 1 ρ 2 ρ ⊔ 1 ρ 2 disconnected disconnected disconnected disconnected isolated(a) isolated(a) disconnected task task isolated(a) isolated(a) isolated(a) isolated(a) isolated(b) (a = b) isolated(a) task task task task S e e a l s o : S E - 0 4 1 4 : R e g i o n b a s e d I s o l a t i o n -
Region Merge as Join Semilattice disconnected (⊥) isolated(a) task invalid
(⊤) = bottom of = top (compile error) disconnected ρ ns invalid -
The Two Axes Together Aspect Capability Region What Execution context
Value location Question Where does code run? Where does data live? Examples @nonisolated , @MainActor , Changes Fixed per scope Flow-sensitive Controls async / await sending @κ ρ disconnected isolated(a) -
Swift Ownership Keyword Description Copyable Can be copied and used
multiple times ~Copyable Cannot be implicitly copied (consume once) consuming param Takes ownership borrowing param Temporary read-only access -
Affine Types A ⊬ A ⊗ A ← can NOT
copy A ⊢ 1 ← can discard Linear Types: use exactly once (no copy, must use) Affine Types: use at most once (no copy, can skip use) No copy = Enforced consumption -
Similarity: Consumption Ownership Concurrency Both consuming and sending enforces consumption
of ~Copyable / ~Sendable value. -
Region-based Isolation is like Ownership Sendable is like Copyable Value
and reference can be copied and used in multiple isolation domains NonSendable is like ~Copyable Cannot be used after transferring to another isolation domain sending is like consuming (but not always) sending param may still not get consumed in the same region condition -
sending param (in same isolation) non-sending binds to actor region
sending keeps in same region -
Formalizing Swift Concurrency Type System D i s c l
a i m e r : Th i s s e c t i o n i s a n A I v i b e - f o r m a l i z a t i o n . -
The Judgment Form ; ;
: env in Γ capability @κ mode α proves ⊢ expr e at type T region ρ outputs ⊣ env out Γ ′ / : what variables are available (input / output) e.g. Every expression transforms the type environment from to (Affinity) : current capability (where we're running) : (asynchrony) : which region the expression result lives in Γ Γ′ Γ = {e : 1 T at ρ , e : 1 1 2 T at ρ , …} 2 2 Γ Γ′ @κ α sync ∣ async ρ -
Example 1 Γ; @nonisolated; sync ⊢ x : T at
disconnected ⊣ Γ -
Example 2 Γ; @MainActor; async ⊢ await f (x) :
T at _ ⊣ Γ ∖ {x} -
Key Rules: Variables Access-only variable rule: (var) Γ; @κ;
α ⊢ x : T at ρ ⊣ Γ x : T at ρ ∈ Γ ρ ∈ accessible(@κ) accessible(@κ) : P (Regions) accessible(@nonisolated) = {disconnected, task, _} accessible(@isolated(a)) = {disconnected, isolated(a), task, _} T : Sendable ⇔ (ρ = _) (normal form in Γ) -
Key Rules: Transfer & Merge call-nonsendable-consume — cross-isolation consumption:
(call-nonsendable-consume) Γ; @κ; async ⊢ await f (arg) : B at _ ⊣ Γ ∖ {arg} 2 f : @σ @ι (A) α → B arg : A at disconnected ∈ Γ 2 A : ∼Sendable B : Sendable @κ = @ι region-merge — e.g. binding via assignment: (region-merge) Γ; @κ; α ⊢ (e .field = e ) : () at _ ⊣ 1 2 Γ [ρ ↦ ρ, ρ ↦ ρ] 2 1 2 Γ; @κ; α ⊢ e : T at ρ ⊣ Γ Γ ; @κ; α ⊢ e : T at ρ ⊣ Γ 1 1 1 1 1 2 2 2 2 T , T : ∼Sendable ρ , ρ ∈ ρ ρ = ρ ⊔ ρ 1 2 1 2 ns 1 2 Cross-isolation shrinks . Region-merge refines it. Γ -
Key Rules: Closures — Isolation Inference Non-@Sendable — closure inherits
parent's capability : (closure-inherit-parent) Γ; @κ; α ⊢ {e} : at ρ ⊣ Γ @κ () α → B ′ closure Γ ⊆ Γ cap Γ ; @κ; α ⊢ e : B at ρ ⊣ Γ cap ′ ret cl ′ @Sendable — No inheritance, uses instead: (closure-no-inherit-parent) Γ; @κ; α ⊢ {e} : at _ ⊣ Γ @Sendable @ι () α → B ′ Γ ⊆ Γ isAllSendable(Γ ) cap cap @κ = toCapability(@ι) cl Γ ; @κ ; α ⊢ e : B at ρ ⊣ Γ cap cl ′ ret cl ′ @κ @ι -
Key Rules: Closures — sending Capture transfer case — consumes
NonSendable captures: (closure-sending-consume) Γ; @κ; α ⊢ f ({e}) : () at _ ⊣ Γ′ f : (sending (@κ () α → T )) → () Γ ⊆ Γ cl ′ cap ¬(⋯ ∧ isActorIsolated(@κ) ∧ @κ = @κ) ⋯ cl Γ ; @κ ; α ⊢ e : T at ρ ⊣ Γ cap cl ′ ret cl ′ Γ = Γ ∖ {x ∣ (x : U at disconnected) ∈ Γ , U : ∼Sendable} ′ cap -
Recap (Formalization) Γ; @κ; α ⊢ e : T at
ρ ⊣ Γ′ Component Role Details WHERE code runs @nonisolated , @MainActor , ... WHERE data lives Affine discipline Merge refines, transfer shrinks @κ ρ disconnected → isolated(a) Γ → Γ′ -
Wrap Up 1. Swift Concurrency = Capability × Region @κ
: where code runs / ρ : where data lives Covers the mystery of closure conversion rules 2. sending = Affine transfer (in many cases) Precondition: x : T at disconnected Effect: (use-after-send is rejected) 3. Region Merge = Context refinement e.g. disconnected ⊔ isolated(a) = isolated(a) Γ = ′ Γ ∖ {x} Γ -
References Swift Evolution SE-0414: Region-based Isolation SE-0430: sending parameter and
result values SE-0431: @isolated(any) function types SE-0461: Run nonisolated async functions on the caller's actor Papers Tofte & Talpin (1994), Region-Based Memory Management Walker, Crary & Morrisett (2000), Typed Memory Management via Static Capabilities Walker & Watkins (2001), On Regions and Linear Types Grossman et al. (2002), Region-Based Memory Management in Cyclone Charguéraud & Pottier (2008), Functional Translation of a Calculus of Capabilities Milano, Turcotti & Myers (2022), A Flexible Type System for Fearless Concurrency