Skip to content

Commit d52623e

Browse files
authored
πŸ’[6.0][Concurrency] Reimplement @TaskLocal as a macro (#73100)
1 parent a85dbaf commit d52623e

18 files changed

+314
-42
lines changed

β€ŽCHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@
55
66
## Swift 6.0
77

8+
* Since its introduction in Swift 5.1 the @TaskLocal property wrapper was used to
9+
create and access task-local value bindings. Property wrappers introduce mutable storage,
10+
which was now properly flagged as potential source of concurrency unsafety.
11+
12+
In order for Swift 6 language mode to not flag task-locals as potentially thread-unsafe,
13+
task locals are now implemented using a macro. The macro has the same general semantics
14+
and usage patterns, however there are two source-break situations which the Swift 6
15+
task locals cannot handle:
16+
17+
Using an implicit default `nil` value for task local initialization, when combined with a type alias:
18+
```swift
19+
// allowed in Swift 5.x, not allowed in Swift 6.x
20+
21+
typealias MyValue = Optional<Int>
22+
23+
@TaskLocal
24+
static var number: MyValue // Swift 6: error, please specify default value explicitly
25+
26+
// Solution 1: Specify the default value
27+
@TaskLocal
28+
static var number: MyValue = nil
29+
30+
// Solution 2: Avoid the type-alias
31+
@TaskLocal
32+
static var number: Optional<Int>
33+
```
34+
35+
At the same time, task locals can now be declared as global properties, which wasn't possible before.
36+
837
* Swift 5.10 missed a semantic check from [SE-0309][]. In type context, a reference to a
938
protocol `P` that has associated types or `Self` requirements should use
1039
the `any` keyword, but this was not enforced in nested generic argument positions.

β€Žlib/Macros/Sources/SwiftMacros/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ add_swift_macro_library(SwiftMacros
1414
OptionSetMacro.swift
1515
DebugDescriptionMacro.swift
1616
DistributedResolvableMacro.swift
17-
SWIFT_DEPENDENCIES
17+
TaskLocalMacro.swift
18+
SWIFT_DEPENDENCIES
1819
SwiftDiagnostics
1920
SwiftSyntax
2021
SwiftSyntaxBuilder
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2022-2023 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 SwiftSyntax
14+
import SwiftSyntaxMacros
15+
import SwiftDiagnostics
16+
17+
/// Macro implementing the TaskLocal functionality.
18+
///
19+
/// It introduces a peer `static let $name: TaskLocal<Type>` as well as a getter
20+
/// that accesses the task local storage.
21+
public enum TaskLocalMacro {}
22+
23+
extension TaskLocalMacro: PeerMacro {
24+
public static func expansion(
25+
of node: AttributeSyntax,
26+
providingPeersOf declaration: some DeclSyntaxProtocol,
27+
in context: some MacroExpansionContext
28+
) throws -> [DeclSyntax] {
29+
guard let varDecl = try requireVar(declaration, diagnose: false) else {
30+
return []
31+
}
32+
guard try requireStaticContext(varDecl, in: context, diagnose: false) else {
33+
return []
34+
}
35+
36+
guard varDecl.bindings.count == 1 else {
37+
throw DiagnosticsError(
38+
syntax: declaration,
39+
message: "'@TaskLocal' property must have exactly one binding", id: .incompatibleDecl)
40+
}
41+
guard let firstBinding = varDecl.bindings.first else {
42+
throw DiagnosticsError(
43+
syntax: declaration,
44+
message: "'@TaskLocal' property must have declared binding", id: .incompatibleDecl)
45+
}
46+
47+
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
48+
throw DiagnosticsError(
49+
syntax: declaration,
50+
message: "'@TaskLocal' property must have name", id: .incompatibleDecl)
51+
}
52+
53+
let type = firstBinding.typeAnnotation?.type
54+
let explicitTypeAnnotation: TypeAnnotationSyntax?
55+
if let type {
56+
explicitTypeAnnotation = TypeAnnotationSyntax(type: TypeSyntax("TaskLocal<\(type.trimmed)>"))
57+
} else {
58+
explicitTypeAnnotation = nil
59+
}
60+
61+
let initialValue: ExprSyntax
62+
if let initializerValue = firstBinding.initializer?.value {
63+
initialValue = ExprSyntax(initializerValue)
64+
} else if let type, type.isOptional {
65+
initialValue = ExprSyntax(NilLiteralExprSyntax())
66+
} else {
67+
throw DiagnosticsError(
68+
syntax: declaration,
69+
message: "'@TaskLocal' property must have default value, or be optional", id: .mustBeVar)
70+
}
71+
72+
// If the property is global, do not prefix the synthesised decl with 'static'
73+
let isGlobal = context.lexicalContext.isEmpty
74+
let staticKeyword: TokenSyntax?
75+
if isGlobal {
76+
staticKeyword = nil
77+
} else {
78+
staticKeyword = TokenSyntax.keyword(.static, trailingTrivia: .space)
79+
}
80+
81+
return [
82+
"""
83+
\(staticKeyword)let $\(name)\(explicitTypeAnnotation) = TaskLocal(wrappedValue: \(initialValue))
84+
"""
85+
]
86+
}
87+
}
88+
89+
extension TaskLocalMacro: AccessorMacro {
90+
public static func expansion(
91+
of node: AttributeSyntax,
92+
providingAccessorsOf declaration: some DeclSyntaxProtocol,
93+
in context: some MacroExpansionContext
94+
) throws -> [AccessorDeclSyntax] {
95+
// We very specifically have to fail and diagnose in the accessor macro,
96+
// rather than in the peer macro, since returning [] from the accessor
97+
// macro adds another series of errors about it missing to emit a decl.
98+
guard let varDecl = try requireVar(declaration) else {
99+
return []
100+
}
101+
try requireStaticContext(varDecl, in: context)
102+
103+
guard let firstBinding = varDecl.bindings.first else {
104+
return []
105+
}
106+
107+
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
108+
return []
109+
}
110+
111+
return ["get { $\(name).get() }"]
112+
}
113+
}
114+
115+
@discardableResult
116+
private func requireVar(_ decl: some DeclSyntaxProtocol,
117+
diagnose: Bool = true) throws -> VariableDeclSyntax? {
118+
if let varDecl = decl.as(VariableDeclSyntax.self) {
119+
return varDecl
120+
}
121+
if diagnose {
122+
throw DiagnosticsError(
123+
syntax: decl,
124+
message: "'@TaskLocal' can only be applied to properties", id: .mustBeVar)
125+
}
126+
127+
return nil
128+
}
129+
130+
@discardableResult
131+
private func requireStaticContext(_ decl: VariableDeclSyntax,
132+
in context: some MacroExpansionContext,
133+
diagnose: Bool = true) throws -> Bool {
134+
let isStatic = decl.modifiers.contains { modifier in
135+
modifier.name.text == "\(Keyword.static)"
136+
}
137+
138+
if isStatic {
139+
return true
140+
}
141+
142+
let isGlobal = context.lexicalContext.isEmpty
143+
if isGlobal {
144+
return true
145+
}
146+
147+
if diagnose {
148+
throw DiagnosticsError(
149+
syntax: decl,
150+
message: "'@TaskLocal' can only be applied to 'static' property, or global variables", id: .mustBeStatic)
151+
}
152+
153+
return false
154+
}
155+
156+
extension TypeSyntax {
157+
// This isn't great since we can't handle type aliases since the macro
158+
// has no type information, but at least for the common case for Optional<T>
159+
// and T? we can detect the optional.
160+
fileprivate var isOptional: Bool {
161+
switch self.as(TypeSyntaxEnum.self) {
162+
case .optionalType:
163+
return true
164+
case .identifierType(let identifierType):
165+
return identifierType.name.text == "Optional"
166+
case .memberType(let memberType):
167+
guard let baseIdentifier = memberType.baseType.as(IdentifierTypeSyntax.self),
168+
baseIdentifier.name.text == "Swift" else {
169+
return false
170+
}
171+
return memberType.name.text == "Optional"
172+
default: return false
173+
}
174+
}
175+
}
176+
177+
struct TaskLocalMacroDiagnostic: DiagnosticMessage {
178+
enum ID: String {
179+
case mustBeVar = "must be var"
180+
case mustBeStatic = "must be static"
181+
case incompatibleDecl = "incompatible declaration"
182+
}
183+
184+
var message: String
185+
var diagnosticID: MessageID
186+
var severity: DiagnosticSeverity
187+
188+
init(message: String, diagnosticID: SwiftDiagnostics.MessageID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
189+
self.message = message
190+
self.diagnosticID = diagnosticID
191+
self.severity = severity
192+
}
193+
194+
init(message: String, domain: String, id: ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
195+
self.message = message
196+
self.diagnosticID = MessageID(domain: domain, id: id.rawValue)
197+
self.severity = severity
198+
}
199+
}
200+
201+
extension DiagnosticsError {
202+
init(
203+
syntax: some SyntaxProtocol,
204+
message: String,
205+
domain: String = "Swift",
206+
id: TaskLocalMacroDiagnostic.ID,
207+
severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
208+
self.init(diagnostics: [
209+
Diagnostic(
210+
node: Syntax(syntax),
211+
message: TaskLocalMacroDiagnostic(
212+
message: message,
213+
domain: domain,
214+
id: id,
215+
severity: severity))
216+
])
217+
}
218+
}

β€Žlib/Sema/TypeCheckConcurrency.cpp

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4978,15 +4978,6 @@ ActorIsolation ActorIsolationRequest::evaluate(
49784978
if (var->isGlobalStorage() && !isActorType) {
49794979
auto *diagVar = var;
49804980
if (auto *originalVar = var->getOriginalWrappedProperty()) {
4981-
// temporary 5.10 checking bypass for @TaskLocal <rdar://120907014>
4982-
// TODO: @TaskLocal should be a macro <rdar://120914014>
4983-
if (auto *classDecl =
4984-
var->getInterfaceType()->getClassOrBoundGenericClass()) {
4985-
auto &ctx = var->getASTContext();
4986-
if (classDecl == ctx.getTaskLocalDecl()) {
4987-
return isolation;
4988-
}
4989-
}
49904981
diagVar = originalVar;
49914982
}
49924983
if (var->isLet()) {

β€Žstdlib/public/Concurrency/TaskLocal.swift

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,42 @@
1313
import Swift
1414
@_implementationOnly import _SwiftConcurrencyShims
1515

16-
/// Property wrapper that defines a task-local value key.
16+
17+
// Macros are disabled when Swift is built without swift-syntax.
18+
#if $Macros && hasAttribute(attached)
19+
20+
/// Macro that introduces a ``TaskLocal-class`` binding.
21+
///
22+
/// For information about task-local bindings, see ``TaskLocal-class``.
23+
///
24+
/// - SeeAlso: ``TaskLocal-class``
25+
@available(SwiftStdlib 5.1, *)
26+
@attached(accessor)
27+
@attached(peer, names: prefixed(`$`))
28+
public macro TaskLocal() =
29+
#externalMacro(module: "SwiftMacros", type: "TaskLocalMacro")
30+
31+
#endif
32+
33+
/// Wrapper type that defines a task-local value key.
1734
///
1835
/// A task-local value is a value that can be bound and read in the context of a
19-
/// `Task`. It is implicitly carried with the task, and is accessible by any
20-
/// child tasks the task creates (such as TaskGroup or `async let` created tasks).
36+
/// ``Task``. It is implicitly carried with the task, and is accessible by any
37+
/// child tasks it creates (such as TaskGroup or `async let` created tasks).
2138
///
2239
/// ### Task-local declarations
2340
///
24-
/// Task locals must be declared as static properties (or global properties,
25-
/// once property wrappers support these), like this:
41+
/// Task locals must be declared as static properties or global properties, like this:
2642
///
2743
/// enum Example {
2844
/// @TaskLocal
2945
/// static let traceID: TraceID?
3046
/// }
3147
///
48+
/// // Global task local properties are supported since Swift 6.0:
49+
/// @TaskLocal
50+
/// var contextualNumber: Int = 12
51+
///
3252
/// ### Default values
3353
/// Reading a task local value when no value was bound to it results in returning
3454
/// its default value. For a task local declared as optional (such as e.g. `TraceID?`),
@@ -137,7 +157,8 @@ import Swift
137157
/// read() // traceID: nil
138158
/// }
139159
/// }
140-
@propertyWrapper
160+
///
161+
/// - SeeAlso: ``TaskLocal-macro``
141162
@available(SwiftStdlib 5.1, *)
142163
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
143164
let defaultValue: Value

β€Žtest/Concurrency/Runtime/async_task_locals_spawn_let.swift renamed to β€Žtest/Concurrency/Runtime/async_task_locals_async_let.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_basic.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_copy_to_async.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// REQUIRES: rdar80824152
2-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
2+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
33

44
// REQUIRES: executable_test
55
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_copy_to_sync.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_groups.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_prevent_illegal_use_discarding_taskgroup.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-fail-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) 2>&1 | %FileCheck %s
1+
// RUN: %target-fail-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) 2>&1 | %FileCheck %s
22
//
33
// // TODO: could not figure out how to use 'not --crash' it never is used with target-run-simple-swift
44
// This test is intended to *crash*, so we're using target-fail-simple-swift

β€Žtest/Concurrency/Runtime/async_task_locals_prevent_illegal_use_taskgroup.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-fail-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) 2>&1 | %FileCheck %s
1+
// RUN: %target-fail-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) 2>&1 | %FileCheck %s
22
//
33
// // TODO: could not figure out how to use 'not --crash' it never is used with target-run-simple-swift
44
// This test is intended to *crash*, so we're using target-fail-simple-swift

β€Žtest/Concurrency/Runtime/async_task_locals_synchronous_bind.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

β€Žtest/Concurrency/Runtime/async_task_locals_wrapper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
1+
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
22

33
// REQUIRES: executable_test
44
// REQUIRES: concurrency

0 commit comments

Comments
Β (0)