Skip to content

Commit 112c5cf

Browse files
committed
[Executors] Move assert/assume APIs onto actors; assumeIsolated()
1 parent 4b9334e commit 112c5cf

11 files changed

+387
-323
lines changed

stdlib/public/Concurrency/ExecutorAssertions.swift

Lines changed: 228 additions & 198 deletions
Large diffs are not rendered by default.

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 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 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 { _ in
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(message: "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(message: "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
}

test/Concurrency/Runtime/custom_executors_complex_equality_subclass.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ actor MyActor {
8686
}
8787

8888
func test(expectedExecutor: BaseExecutor, expectedQueue: DispatchQueue) {
89-
preconditionTaskOnExecutor(expectedExecutor, message: "Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
89+
expectedExecutor.preconditionIsolated(message: "Expected deep equality to trigger for \(expectedExecutor) and our \(self.executor)")
9090
print("\(Self.self): [\(self.executor.name)] on same context as [\(expectedExecutor.name)]")
9191
}
9292
}

test/Concurrency/Runtime/custom_executors_protocol.swift

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

6666
func test(expectedExecutor: some SerialExecutor, expectedQueue: DispatchQueue) {
67-
// FIXME(waiting on preconditions to merge): preconditionTaskOnExecutor(expectedExecutor, "Expected to be on: \(expectedExecutor)")
67+
// FIXME(waiting on preconditions to merge): expectedExecutor.preconditionIsolated("Expected to be on: \(expectedExecutor)")
6868
dispatchPrecondition(condition: .onQueue(expectedQueue))
6969
print("\(Self.self): on executor \(expectedExecutor)")
7070
}

test/Distributed/Runtime/distributed_actor_custom_executor_basic.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ distributed actor Worker {
2929

3030
distributed func test(x: Int) async throws {
3131
print("executed: \(#function)")
32-
assumeOnMainActorExecutor {
32+
MainActor.assumeIsolated {
3333
print("assume: this distributed actor shares executor with MainActor")
3434
}
3535
print("done executed: \(#function)")

0 commit comments

Comments
 (0)