Skip to content

Commit 5fe47bf

Browse files
authored
Merge pull request #64030 from ktoso/wip-assume
[Concurrency] Add "assume on (actor) executor" APIs
2 parents 814488f + 3eb04e2 commit 5fe47bf

File tree

4 files changed

+252
-6
lines changed

4 files changed

+252
-6
lines changed

stdlib/public/Concurrency/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ add_swift_target_library(swift_Concurrency ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} I
8787
Errors.swift
8888
Error.cpp
8989
Executor.swift
90+
ExecutorAssertions.swift
9091
AsyncCompactMapSequence.swift
9192
AsyncDropFirstSequence.swift
9293
AsyncDropWhileSequence.swift
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Swift
14+
15+
/// A safe way to synchronously assume that the current execution context belongs to the MainActor.
16+
///
17+
/// This API should only be used as last resort, when it is not possible to express the current
18+
/// execution context definitely belongs to the main actor in other ways. E.g. one may need to use
19+
/// this in a delegate style API, where a synchronous method is guaranteed to be called by the
20+
/// main actor, however it is not possible to annotate this legacy API with `@MainActor`.
21+
///
22+
/// - Warning: If the current executor is *not* the MainActor's serial executor, this function will crash.
23+
///
24+
/// Note that this check is performed against the MainActor's serial executor, meaning that
25+
/// if another actor uses the same serial executor--by using ``MainActor/sharedUnownedExecutor``
26+
/// as its own ``Actor/unownedExecutor``--this check will succeed, as from a concurrency safety
27+
/// perspective, the serial executor guarantees mutual exclusion of those two actors.
28+
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
29+
@_unavailableFromAsync(message: "await the call to the @MainActor closure directly")
30+
public
31+
func assumeOnMainActorExecutor<T>(
32+
_ operation: @MainActor () throws -> T,
33+
file: StaticString = #fileID, line: UInt = #line
34+
) rethrows -> T {
35+
typealias YesMainActor = @MainActor () throws -> T
36+
typealias NoMainActor = () throws -> T
37+
38+
/// This is guaranteed to be fatal if the check fails,
39+
/// as this is our "safe" version of this API.
40+
guard _taskIsCurrentExecutor(Builtin.buildMainActorExecutorRef()) else {
41+
// TODO: offer information which executor we actually got
42+
fatalError("Incorrect actor executor assumption; Expected 'MainActor' executor.", file: file, line: line)
43+
}
44+
45+
// To do the unsafe cast, we have to pretend it's @escaping.
46+
return try withoutActuallyEscaping(operation) {
47+
(_ fn: @escaping YesMainActor) throws -> T in
48+
let rawFn = unsafeBitCast(fn, to: NoMainActor.self)
49+
return try rawFn()
50+
}
51+
}
52+
53+
/// A safe way to synchronously assume that the current execution context belongs to the passed in actor.
54+
///
55+
/// This API should only be used as last resort, when it is not possible to express the current
56+
/// execution context definitely belongs to the specified actor in other ways. E.g. one may need to use
57+
/// this in a delegate style API, where a synchronous method is guaranteed to be called by the
58+
/// specified actor, however it is not possible to move this method as being declared on the specified actor.
59+
///
60+
/// - Warning: If the current executor is *not* the expected serial executor, this function will crash.
61+
///
62+
/// Note that this check is performed against the passed in actor's serial executor, meaning that
63+
/// if another actor uses the same serial executor--by using that actor's ``Actor/unownedExecutor``
64+
/// as its own ``Actor/unownedExecutor``--this check will succeed, as from a concurrency safety
65+
/// perspective, the serial executor guarantees mutual exclusion of those two actors.
66+
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
67+
@_unavailableFromAsync(message: "express the closure as an explicit function declared on the specified 'actor' instead")
68+
public
69+
func assumeOnActorExecutor<Act: Actor, T>(
70+
_ actor: Act,
71+
_ operation: (isolated Act) throws -> T,
72+
file: StaticString = #fileID, line: UInt = #line
73+
) rethrows -> T {
74+
typealias YesActor = (isolated Act) throws -> T
75+
typealias NoActor = (Act) throws -> T
76+
77+
/// This is guaranteed to be fatal if the check fails,
78+
/// as this is our "safe" version of this API.
79+
let executor: Builtin.Executor = actor.unownedExecutor.executor
80+
guard _taskIsCurrentExecutor(executor) else {
81+
// TODO: offer information which executor we actually got
82+
fatalError("Incorrect actor executor assumption; Expected same executor as \(actor).", file: file, line: line)
83+
}
84+
85+
// To do the unsafe cast, we have to pretend it's @escaping.
86+
return try withoutActuallyEscaping(operation) {
87+
(_ fn: @escaping YesActor) throws -> T in
88+
let rawFn = unsafeBitCast(fn, to: NoActor.self)
89+
return try rawFn(actor)
90+
}
91+
}
92+
93+
// TODO(ktoso): implement assume for distributed actors as well
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -Xfrontend -disable-availability-checking -parse-as-library %s -o %t/a.out
3+
// RUN: %target-codesign %t/a.out
4+
// RUN: %target-run %t/a.out
5+
6+
// REQUIRES: executable_test
7+
// REQUIRES: concurrency
8+
// REQUIRES: concurrency_runtime
9+
// UNSUPPORTED: back_deployment_runtime
10+
11+
// UNSUPPORTED: back_deploy_concurrency
12+
// UNSUPPORTED: use_os_stdlib
13+
// UNSUPPORTED: freestanding
14+
15+
import StdlibUnittest
16+
17+
func checkAssumeMainActor(echo: MainActorEcho) /* synchronous! */ {
18+
// Echo.get("any") // error: main actor isolated, cannot perform async call here
19+
assumeOnMainActorExecutor {
20+
let input = "example"
21+
let got = echo.get(input)
22+
precondition(got == "example", "Expected echo to match \(input)")
23+
}
24+
}
25+
26+
@MainActor
27+
func mainActorCallCheck(echo: MainActorEcho) {
28+
checkAssumeMainActor(echo: echo)
29+
}
30+
31+
actor MainFriend {
32+
nonisolated var unownedExecutor: UnownedSerialExecutor {
33+
MainActor.sharedUnownedExecutor
34+
}
35+
36+
func callCheck(echo: MainActorEcho) {
37+
checkAssumeMainActor(echo: echo)
38+
}
39+
}
40+
41+
func checkAssumeSomeone(someone: Someone) /* synchronous */ {
42+
// someone.something // can't access, would need a hop but we can't
43+
assumeOnActorExecutor(someone) { someone in
44+
let something = someone.something
45+
let expected = "isolated something"
46+
precondition(something == expected, "expected '\(expected)', got: \(something)")
47+
}
48+
}
49+
50+
actor Someone {
51+
func callCheckMainActor(echo: MainActorEcho) {
52+
checkAssumeMainActor(echo: echo)
53+
}
54+
55+
func callCheckSomeone() {
56+
checkAssumeSomeone(someone: self)
57+
}
58+
59+
var something: String {
60+
"isolated something"
61+
}
62+
}
63+
64+
actor SomeonesFriend {
65+
let someone: Someone
66+
nonisolated var unownedExecutor: UnownedSerialExecutor {
67+
self.someone.unownedExecutor
68+
}
69+
70+
init(someone: Someone) {
71+
self.someone = someone
72+
}
73+
74+
func callCheckSomeone() {
75+
checkAssumeSomeone(someone: someone)
76+
}
77+
}
78+
79+
actor CompleteStranger {
80+
let someone: Someone
81+
init(someone: Someone) {
82+
self.someone = someone
83+
}
84+
85+
func callCheckSomeone() {
86+
checkAssumeSomeone(someone: someone)
87+
}
88+
}
89+
90+
@MainActor
91+
final class MainActorEcho {
92+
func get(_ key: String) -> String {
93+
key
94+
}
95+
}
96+
97+
@main struct Main {
98+
static func main() async {
99+
let tests = TestSuite("AssumeActorExecutor")
100+
101+
let echo = MainActorEcho()
102+
103+
if #available(SwiftStdlib 5.9, *) {
104+
// === MainActor --------------------------------------------------------
105+
106+
tests.test("assumeOnMainActorExecutor: assume the main executor, from 'main() async'") {
107+
await checkAssumeMainActor(echo: echo)
108+
}
109+
110+
tests.test("assumeOnMainActorExecutor: assume the main executor, from MainActor method") {
111+
await mainActorCallCheck(echo: echo)
112+
}
113+
114+
tests.test("assumeOnMainActorExecutor: assume the main executor, from actor on MainActor executor") {
115+
await MainFriend().callCheck(echo: echo)
116+
}
117+
118+
tests.test("assumeOnMainActorExecutor: wrongly assume the main executor, from actor on other executor") {
119+
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected 'MainActor' executor.")
120+
await Someone().callCheckMainActor(echo: echo)
121+
}
122+
123+
// === some Actor -------------------------------------------------------
124+
125+
let someone = Someone()
126+
tests.test("assumeOnActorExecutor: wrongly assume someone's executor, from 'main() async'") {
127+
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
128+
checkAssumeSomeone(someone: someone)
129+
}
130+
131+
tests.test("assumeOnActorExecutor: wrongly assume someone's executor, from MainActor method") {
132+
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
133+
checkAssumeSomeone(someone: someone)
134+
}
135+
136+
tests.test("assumeOnActorExecutor: assume someone's executor, from Someone") {
137+
await someone.callCheckSomeone()
138+
}
139+
140+
tests.test("assumeOnActorExecutor: assume someone's executor, from actor on the Someone.unownedExecutor") {
141+
await SomeonesFriend(someone: someone).callCheckSomeone()
142+
}
143+
144+
tests.test("assumeOnActorExecutor: wrongly assume the main executor, from actor on other executor") {
145+
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
146+
await CompleteStranger(someone: someone).callCheckSomeone()
147+
}
148+
149+
150+
}
151+
152+
await runAllTestsAsync()
153+
}
154+
}

test/IDE/complete_concurrency_keyword.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
// REQUIRES: concurrency
44

5-
// CHECK_DECL: NOT
6-
75
#^GLOBAL^#
86
// GLOBAL: Begin completions
97
// GLOBAL-DAG: Keyword/None: actor; name=actor
@@ -13,9 +11,9 @@
1311
enum Namespace {
1412
#^TYPEMEMBER^#
1513
// TYPEMEMBER: Begin completions
16-
// TYPEMEMBER-NOT: await
14+
// TYPEMEMBER-NOT: Keyword{{.*}}await
1715
// TYPEMEMBER-DAG: Keyword/None: actor; name=actor
18-
// TYPEMEMBER-NOT: await
16+
// TYPEMEMBER-NOT: Keyword{{.*}}await
1917
// TYPEMEMBER: End completion
2018
}
2119

@@ -30,9 +28,9 @@ func testFunc() {
3028
func testExpr() {
3129
_ = #^EXPR^#
3230
// EXPR: Begin completions
33-
// EXPR-NOT: actor
31+
// EXPR-NOT: Keyword{{.*}}actor
3432
// EXPR-DAG: Keyword/None: await; name=await
35-
// EXPR-NOT: actor
33+
// EXPR-NOT: Keyword{{.*}}actor
3634
// EXPR: End completion
3735
}
3836
func testClosure() {

0 commit comments

Comments
 (0)