Skip to content

Commit 45a38f1

Browse files
authored
🍒[5.9][Executors] Move assert/assume APIs onto actors; assumeIsolated() (#64853)
1 parent 954cb6f commit 45a38f1

14 files changed

+537
-332
lines changed

‎stdlib/public/Concurrency/ExecutorAssertions.swift

Lines changed: 208 additions & 202 deletions
Large diffs are not rendered by default.

‎stdlib/public/Concurrency/MainActor.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,46 @@ extension MainActor {
9898
return try await body()
9999
}
100100
}
101+
102+
@available(SwiftStdlib 5.9, *)
103+
extension MainActor {
104+
/// A safe way to synchronously assume that the current execution context belongs to the MainActor.
105+
///
106+
/// This API should only be used as last resort, when it is not possible to express the current
107+
/// execution context definitely belongs to the main actor in other ways. E.g. one may need to use
108+
/// this in a delegate style API, where a synchronous method is guaranteed to be called by the
109+
/// main actor, however it is not possible to annotate this legacy API with `@MainActor`.
110+
///
111+
/// - Warning: If the current executor is *not* the MainActor's serial executor, this function will crash.
112+
///
113+
/// Note that this check is performed against the MainActor's serial executor, meaning that
114+
/// if another actor uses the same serial executor--by using ``MainActor/sharedUnownedExecutor``
115+
/// as its own ``Actor/unownedExecutor``--this check will succeed, as from a concurrency safety
116+
/// perspective, the serial executor guarantees mutual exclusion of those two actors.
117+
@available(SwiftStdlib 5.9, *)
118+
@_unavailableFromAsync(message: "await the call to the @MainActor closure directly")
119+
public static func assumeIsolated<T>(
120+
_ operation: @MainActor () throws -> T,
121+
file: StaticString = #fileID, line: UInt = #line
122+
) rethrows -> T {
123+
124+
typealias YesActor = @MainActor () throws -> T
125+
typealias NoActor = () throws -> T
126+
127+
/// This is guaranteed to be fatal if the check fails,
128+
/// as this is our "safe" version of this API.
129+
let executor: Builtin.Executor = Self.shared.unownedExecutor.executor
130+
guard _taskIsCurrentExecutor(executor) else {
131+
// TODO: offer information which executor we actually got
132+
fatalError("Incorrect actor executor assumption; Expected same executor as \(self).", file: file, line: line)
133+
}
134+
135+
// To do the unsafe cast, we have to pretend it's @escaping.
136+
return try withoutActuallyEscaping(operation) {
137+
(_ fn: @escaping YesActor) throws -> T in
138+
let rawFn = unsafeBitCast(fn, to: NoActor.self)
139+
return try rawFn()
140+
}
141+
}
142+
}
101143
#endif

‎stdlib/public/Distributed/DistributedAssertions.swift

Lines changed: 133 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -16,98 +16,97 @@ import _Concurrency
1616
// ==== -----------------------------------------------------------------------
1717
// MARK: Precondition APIs
1818

19-
/// Unconditionally if the current task is executing on the serial executor of the passed in `actor`,
20-
/// and if not crash the program offering information about the executor mismatch.
21-
///
22-
/// This function's effect varies depending on the build flag used:
23-
///
24-
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
25-
/// configuration), stops program execution in a debuggable state after
26-
/// printing `message`.
27-
///
28-
/// * In `-O` builds (the default for Xcode's Release configuration), stops
29-
/// program execution.
30-
///
31-
/// * In `-Ounchecked` builds, the optimizer may assume that this function is
32-
/// never called. Failure to satisfy that assumption is a serious
33-
/// programming error.
34-
///
35-
/// - Parameter actor: the actor whose serial executor we expect to be the current executor
3619
@available(SwiftStdlib 5.9, *)
37-
public func preconditionOnExecutor(
38-
of actor: some DistributedActor,
39-
_ message: @autoclosure () -> String = String(),
40-
file: StaticString = #fileID, line: UInt = #line
41-
) {
42-
guard _isDebugAssertConfiguration() || _isReleaseAssertConfiguration() else {
43-
return
44-
}
45-
46-
guard __isLocalActor(actor) else {
47-
return
48-
}
20+
extension DistributedActor {
21+
/// Unconditionally if the current task is executing on the serial executor of the passed in `actor`,
22+
/// and if not crash the program offering information about the executor mismatch.
23+
///
24+
/// This function's effect varies depending on the build flag used:
25+
///
26+
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
27+
/// configuration), stops program execution in a debuggable state after
28+
/// printing `message`.
29+
///
30+
/// * In `-O` builds (the default for Xcode's Release configuration), stops
31+
/// program execution.
32+
///
33+
/// * In `-Ounchecked` builds, the optimizer may assume that this function is
34+
/// never called. Failure to satisfy that assumption is a serious
35+
/// programming error.
36+
@available(SwiftStdlib 5.9, *)
37+
public nonisolated func preconditionIsolated(
38+
_ message: @autoclosure () -> String = String(),
39+
file: StaticString = #fileID, line: UInt = #line
40+
) {
41+
guard _isDebugAssertConfiguration() || _isReleaseAssertConfiguration() else {
42+
return
43+
}
44+
45+
guard __isLocalActor(self) else {
46+
return
47+
}
48+
49+
guard let unownedExecutor = self.localUnownedExecutor else {
50+
preconditionFailure(
51+
"Incorrect actor executor assumption; Distributed actor \(self) is 'local' but has no executor!",
52+
file: file, line: line)
53+
}
54+
55+
let expectationCheck = _taskIsCurrentExecutor(unownedExecutor._executor)
4956

50-
guard let unownedExecutor = actor.localUnownedExecutor else {
51-
preconditionFailure(
52-
"Incorrect actor executor assumption; Distributed actor \(actor) is 'local' but has no executor!",
57+
// TODO: offer information which executor we actually got
58+
precondition(expectationCheck,
59+
// TODO: figure out a way to get the typed repr out of the unowned executor
60+
"Incorrect actor executor assumption; Expected '\(unownedExecutor)' executor. \(message())",
5361
file: file, line: line)
5462
}
55-
56-
let expectationCheck = _taskIsCurrentExecutor(unownedExecutor._executor)
57-
58-
// TODO: offer information which executor we actually got
59-
precondition(expectationCheck,
60-
// TODO: figure out a way to get the typed repr out of the unowned executor
61-
"Incorrect actor executor assumption; Expected '\(unownedExecutor)' executor. \(message())",
62-
file: file, line: line)
6363
}
6464

6565
// ==== -----------------------------------------------------------------------
6666
// MARK: Assert APIs
6767

68-
/// Performs an executor check in debug builds.
69-
///
70-
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
71-
/// configuration): If `condition` evaluates to `false`, stop program
72-
/// execution in a debuggable state after printing `message`.
73-
///
74-
/// * In `-O` builds (the default for Xcode's Release configuration),
75-
/// `condition` is not evaluated, and there are no effects.
76-
///
77-
/// * In `-Ounchecked` builds, `condition` is not evaluated, but the optimizer
78-
/// may assume that it *always* evaluates to `true`. Failure to satisfy that
79-
/// assumption is a serious programming error.
80-
///
81-
///
82-
/// - Parameter actor: the actor whose serial executor we expect to be the current executor
8368
@available(SwiftStdlib 5.9, *)
84-
@_transparent
85-
public func assertOnExecutor(
86-
of actor: some DistributedActor,
87-
_ message: @autoclosure () -> String = String(),
88-
file: StaticString = #fileID, line: UInt = #line
89-
) {
90-
guard _isDebugAssertConfiguration() else {
91-
return
92-
}
93-
94-
guard __isLocalActor(actor) else {
95-
return
96-
}
97-
98-
guard let unownedExecutor = actor.localUnownedExecutor else {
99-
preconditionFailure(
100-
"Incorrect actor executor assumption; Distributed actor \(actor) is 'local' but has no executor!",
101-
file: file, line: line)
102-
}
103-
104-
guard _taskIsCurrentExecutor(unownedExecutor._executor) else {
105-
// TODO: offer information which executor we actually got
106-
// TODO: figure out a way to get the typed repr out of the unowned executor
107-
let msg = "Incorrect actor executor assumption; Expected '\(unownedExecutor)' executor. \(message())"
108-
/// TODO: implement the logic in-place perhaps rather than delegating to precondition()?
109-
assertionFailure(msg, file: file, line: line) // short-cut so we get the exact same failure reporting semantics
110-
return
69+
extension DistributedActor {
70+
/// Performs an executor check in debug builds.
71+
///
72+
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
73+
/// configuration): If `condition` evaluates to `false`, stop program
74+
/// execution in a debuggable state after printing `message`.
75+
///
76+
/// * In `-O` builds (the default for Xcode's Release configuration),
77+
/// `condition` is not evaluated, and there are no effects.
78+
///
79+
/// * In `-Ounchecked` builds, `condition` is not evaluated, but the optimizer
80+
/// may assume that it *always* evaluates to `true`. Failure to satisfy that
81+
/// assumption is a serious programming error.
82+
@available(SwiftStdlib 5.9, *)
83+
@_transparent
84+
public nonisolated func assertIsolated(
85+
_ message: @autoclosure () -> String = String(),
86+
file: StaticString = #fileID, line: UInt = #line
87+
) {
88+
guard _isDebugAssertConfiguration() else {
89+
return
90+
}
91+
92+
guard __isLocalActor(self) else {
93+
return
94+
}
95+
96+
guard let unownedExecutor = self.localUnownedExecutor else {
97+
preconditionFailure(
98+
"Incorrect actor executor assumption; Distributed actor \(self) is 'local' but has no executor!",
99+
file: file, line: line)
100+
}
101+
102+
guard _taskIsCurrentExecutor(unownedExecutor._executor) else {
103+
// TODO: offer information which executor we actually got
104+
// TODO: figure out a way to get the typed repr out of the unowned executor
105+
let msg = "Incorrect actor executor assumption; Expected '\(unownedExecutor)' executor. \(message())"
106+
/// TODO: implement the logic in-place perhaps rather than delegating to precondition()?
107+
assertionFailure(msg, file: file, line: line) // short-cut so we get the exact same failure reporting semantics
108+
return
109+
}
111110
}
112111
}
113112

@@ -116,34 +115,58 @@ public func assertOnExecutor(
116115
// MARK: Assume APIs
117116

118117
@available(SwiftStdlib 5.9, *)
119-
@_unavailableFromAsync(message: "express the closure as an explicit function declared on the specified 'distributed actor' instead")
120-
public func assumeOnLocalDistributedActorExecutor<Act: DistributedActor, T>(
121-
of actor: Act,
122-
_ operation: (isolated Act) throws -> T,
123-
file: StaticString = #fileID, line: UInt = #line
124-
) rethrows -> T {
125-
typealias YesActor = (isolated Act) throws -> T
126-
typealias NoActor = (Act) throws -> T
127-
128-
guard __isLocalActor(actor) else {
129-
fatalError("Cannot assume to be 'isolated \(Act.self)' since distributed actor '\(actor)' is remote.")
130-
}
131-
132-
/// This is guaranteed to be fatal if the check fails,
133-
/// as this is our "safe" version of this API.
134-
guard let executor = actor.localUnownedExecutor else {
135-
fatalError("Distributed local actor MUST have executor, but was nil")
136-
}
137-
guard _taskIsCurrentExecutor(executor._executor) else {
138-
// TODO: offer information which executor we actually got when
139-
fatalError("Incorrect actor executor assumption; Expected same executor as \(actor).", file: file, line: line)
140-
}
141-
142-
// To do the unsafe cast, we have to pretend it's @escaping.
143-
return try withoutActuallyEscaping(operation) {
144-
(_ fn: @escaping YesActor) throws -> T in
145-
let rawFn = unsafeBitCast(fn, to: NoActor.self)
146-
return try rawFn(actor)
118+
extension DistributedActor {
119+
120+
/// Assume that the current actor is a local distributed actor and that the currently executing context is the same as that actors
121+
/// serial executor, or crash.
122+
///
123+
/// This method allows developers to *assume and verify* that the currently executing synchronous function
124+
/// is actually executing on the serial executor that this distributed (local) actor is using.
125+
///
126+
/// If that is the case, the operation is invoked with an `isolated` version of the actoe,
127+
/// allowing synchronous access to actor local state without hopping through asynchronous boundaries.
128+
///
129+
/// If the current context is not running on the actor's serial executor, or if the actor is a reference to a remote actor,
130+
/// this method will crash with a fatalError (similar to ``preconditionIsolated()``).
131+
///
132+
/// This method can only be used from synchronous functions, as asynchronous ones should instead
133+
/// perform normal method call to the actor.
134+
///
135+
/// - Parameters:
136+
/// - operation: the operation that will be executed if the current context is executing on the actors serial executor, and the actor is a local reference
137+
/// - file: source location where the assume call is made
138+
/// - file: source location where the assume call is made
139+
/// - Returns: the return value of the `operation`
140+
/// - Throws: rethrows the `Error` thrown by the operation if it threw
141+
@available(SwiftStdlib 5.9, *)
142+
@_unavailableFromAsync(message: "express the closure as an explicit function declared on the specified 'distributed actor' instead")
143+
public nonisolated func assumeIsolated<T>(
144+
_ operation: (isolated Self) throws -> T,
145+
file: StaticString = #fileID, line: UInt = #line
146+
) rethrows -> T {
147+
typealias YesActor = (isolated Self) throws -> T
148+
typealias NoActor = (Self) throws -> T
149+
150+
guard __isLocalActor(self) else {
151+
fatalError("Cannot assume to be 'isolated \(Self.self)' since distributed actor '\(self)' is a remote actor reference.")
152+
}
153+
154+
/// This is guaranteed to be fatal if the check fails,
155+
/// as this is our "safe" version of this API.
156+
guard let executor = self.localUnownedExecutor else {
157+
fatalError("Distributed local actor MUST have executor, but was nil")
158+
}
159+
guard _taskIsCurrentExecutor(executor._executor) else {
160+
// TODO: offer information which executor we actually got when
161+
fatalError("Incorrect actor executor assumption; Expected same executor as \(self).", file: file, line: line)
162+
}
163+
164+
// To do the unsafe cast, we have to pretend it's @escaping.
165+
return try withoutActuallyEscaping(operation) {
166+
(_ fn: @escaping YesActor) throws -> T in
167+
let rawFn = unsafeBitCast(fn, to: NoActor.self)
168+
return try rawFn(self)
169+
}
147170
}
148171
}
149172

‎test/Concurrency/Runtime/actor_assert_precondition_executor.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
import StdlibUnittest
1616

1717
func checkPreconditionMainActor() /* synchronous! */ {
18-
preconditionTaskOnActorExecutor(MainActor.shared)
18+
MainActor.shared.preconditionIsolated()
19+
MainActor.preconditionIsolated()
20+
21+
// check for the existence of the assert version of APIs
22+
MainActor.shared.assertIsolated()
23+
MainActor.assertIsolated()
1924
}
2025

2126
func checkPreconditionFamousActor() /* synchronous! */ {
22-
preconditionTaskOnActorExecutor(FamousActor.shared)
27+
FamousActor.shared.preconditionIsolated() // instance version for global actor
28+
FamousActor.preconditionIsolated() // static version for global actor
2329
}
2430

2531
@MainActor
@@ -62,7 +68,7 @@ actor Someone {
6268
if #available(SwiftStdlib 5.9, *) {
6369
// === MainActor --------------------------------------------------------
6470

65-
tests.test("preconditionTaskOnActorExecutor(main): from 'main() async', with await") {
71+
tests.test("precondition on actor (main): from 'main() async', with await") {
6672
await checkPreconditionMainActor()
6773
}
6874

@@ -72,14 +78,14 @@ actor Someone {
7278
await MainFriend().callCheckMainActor()
7379
}
7480

75-
tests.test("preconditionTaskOnActorExecutor(main): wrongly assume the main executor, from actor on other executor") {
81+
tests.test("precondition on actor (main): wrongly assume the main executor, from actor on other executor") {
7682
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected 'MainActor' executor.")
7783
await Someone().callCheckMainActor()
7884
}
7985

8086
// === Global actor -----------------------------------------------------
8187

82-
tests.test("preconditionTaskOnActorExecutor(main): assume FamousActor, from FamousActor") {
88+
tests.test("precondition on actor (main): assume FamousActor, from FamousActor") {
8389
await FamousActor.shared.callCheckFamousActor()
8490
}
8591

‎test/Concurrency/Runtime/actor_assume_executor.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import StdlibUnittest
1616

1717
func checkAssumeMainActor(echo: MainActorEcho) /* synchronous! */ {
1818
// Echo.get("any") // error: main actor isolated, cannot perform async call here
19-
assumeOnMainActorExecutor {
19+
MainActor.assumeIsolated {
2020
let input = "example"
2121
let got = echo.get(input)
2222
precondition(got == "example", "Expected echo to match \(input)")
@@ -40,7 +40,7 @@ actor MainFriend {
4040

4141
func checkAssumeSomeone(someone: Someone) /* synchronous */ {
4242
// someone.something // can't access, would need a hop but we can't
43-
assumeOnActorExecutor(someone) { someone in
43+
someone.assumeIsolated { someone in
4444
let something = someone.something
4545
let expected = "isolated something"
4646
precondition(something == expected, "expected '\(expected)', got: \(something)")

‎test/Concurrency/Runtime/custom_executors_complex_equality.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ actor MyActor {
6666
}
6767

6868
func test(expectedExecutor: NaiveQueueExecutor, expectedQueue: DispatchQueue) {
69-
preconditionTaskOnExecutor(expectedExecutor, message: "Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
69+
expectedExecutor.preconditionIsolated("Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
7070
print("\(Self.self): [\(self.executor.name)] on same context as [\(expectedExecutor.name)]")
7171
}
7272
}

‎test/Concurrency/Runtime/custom_executors_complex_equality_crash.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ actor MyActor {
6565
}
6666

6767
func test(expectedExecutor: NaiveQueueExecutor) {
68-
preconditionTaskOnExecutor(expectedExecutor, message: "Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
68+
expectedExecutor.preconditionIsolated("Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
6969
print("\(Self.self): [\(self.executor.name)] on same context as [\(expectedExecutor.name)]")
7070
}
7171
}

0 commit comments

Comments
 (0)