Skip to content

Commit 7e569ac

Browse files
committed
[Concurrency] Improve new TaskLocal macro documentation
1 parent dbacc24 commit 7e569ac

File tree

3 files changed

+77
-25
lines changed

3 files changed

+77
-25
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 explicitl
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/TaskLocalMacro.swift

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ extension TaskLocalMacro: PeerMacro {
3333
return []
3434
}
3535

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+
}
3641
guard let firstBinding = varDecl.bindings.first else {
3742
throw DiagnosticsError(
3843
syntax: declaration,
@@ -46,18 +51,18 @@ extension TaskLocalMacro: PeerMacro {
4651
}
4752

4853
let type = firstBinding.typeAnnotation?.type
49-
let explicitType: String
54+
let explicitTypeAnnotation: TypeAnnotationSyntax?
5055
if let type {
51-
explicitType = ": TaskLocal<\(type.trimmed)>"
56+
explicitTypeAnnotation = TypeAnnotationSyntax(type: TypeSyntax("TaskLocal<\(type.trimmed)>"))
5257
} else {
53-
explicitType = ""
58+
explicitTypeAnnotation = nil
5459
}
5560

56-
let initialValue: Any
61+
let initialValue: ExprSyntax
5762
if let initializerValue = firstBinding.initializer?.value {
58-
initialValue = initializerValue
63+
initialValue = ExprSyntax(initializerValue)
5964
} else if let type, type.isOptional {
60-
initialValue = "nil"
65+
initialValue = ExprSyntax(NilLiteralExprSyntax())
6166
} else {
6267
throw DiagnosticsError(
6368
syntax: declaration,
@@ -66,16 +71,16 @@ extension TaskLocalMacro: PeerMacro {
6671

6772
// If the property is global, do not prefix the synthesised decl with 'static'
6873
let isGlobal = context.lexicalContext.isEmpty
69-
let staticKeyword: String
74+
let staticKeyword: TokenSyntax?
7075
if isGlobal {
71-
staticKeyword = ""
76+
staticKeyword = nil
7277
} else {
73-
staticKeyword = "static "
78+
staticKeyword = TokenSyntax.keyword(.static, trailingTrivia: .space)
7479
}
7580

7681
return [
7782
"""
78-
\(raw: staticKeyword)let $\(name)\(raw: explicitType) = TaskLocal(wrappedValue: \(raw: initialValue))
83+
\(staticKeyword)let $\(name)\(explicitTypeAnnotation) = TaskLocal(wrappedValue: \(initialValue))
7984
"""
8085
]
8186
}
@@ -96,11 +101,11 @@ extension TaskLocalMacro: AccessorMacro {
96101
try requireStaticContext(varDecl, in: context)
97102

98103
guard let firstBinding = varDecl.bindings.first else {
99-
return [] // TODO: make error
104+
return []
100105
}
101106

102107
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
103-
return [] // TODO: make error
108+
return []
104109
}
105110

106111
return ["get { $\(name).get() }"]
@@ -142,7 +147,7 @@ private func requireStaticContext(_ decl: VariableDeclSyntax,
142147
if diagnose {
143148
throw DiagnosticsError(
144149
syntax: decl,
145-
message: "'@TaskLocal' can only be applied to 'static' property", id: .mustBeStatic)
150+
message: "'@TaskLocal' can only be applied to 'static' property, or global variables", id: .mustBeStatic)
146151
}
147152

148153
return false
@@ -153,10 +158,19 @@ extension TypeSyntax {
153158
// has no type information, but at least for the common case for Optional<T>
154159
// and T? we can detect the optional.
155160
fileprivate var isOptional: Bool {
156-
let strRepr = "\(self)"
157-
return strRepr.last == "?" ||
158-
strRepr.starts(with: "Optional<") ||
159-
strRepr.starts(with: "Swift.Optional<")
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+
}
160174
}
161175
}
162176

@@ -185,8 +199,8 @@ struct TaskLocalMacroDiagnostic: DiagnosticMessage {
185199
}
186200

187201
extension DiagnosticsError {
188-
init<S: SyntaxProtocol>(
189-
syntax: S,
202+
init(
203+
syntax: some SyntaxProtocol,
190204
message: String,
191205
domain: String = "Swift",
192206
id: TaskLocalMacroDiagnostic.ID,

stdlib/public/Concurrency/TaskLocal.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import Swift
1717
// Macros are disabled when Swift is built without swift-syntax.
1818
#if $Macros && hasAttribute(attached)
1919

20-
// TODO: docs
20+
/// Macro that introduces a ``TaskLocal-class`` binding.
21+
///
22+
/// For information about task-local bindings, see ``TaskLocal-class``.
23+
///
24+
/// - SeeAlso: ``TaskLocal-class``
2125
@available(SwiftStdlib 5.1, *)
2226
@attached(accessor)
2327
@attached(peer, names: prefixed(`$`))
@@ -26,22 +30,25 @@ public macro TaskLocal() =
2630

2731
#endif
2832

29-
/// Wrapper that defines a task-local value key.
33+
/// Wrapper type that defines a task-local value key.
3034
///
3135
/// A task-local value is a value that can be bound and read in the context of a
32-
/// `Task`. It is implicitly carried with the task, and is accessible by any
33-
/// 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).
3438
///
3539
/// ### Task-local declarations
3640
///
37-
/// Task locals must be declared as static properties (or global properties,
38-
/// once property wrappers support these), like this:
41+
/// Task locals must be declared as static properties or global properties, like this:
3942
///
4043
/// enum Example {
4144
/// @TaskLocal
4245
/// static let traceID: TraceID?
4346
/// }
4447
///
48+
/// // Global task local properties are supported since Swift 6.0:
49+
/// @TaskLocal
50+
/// var contextualNumber: Int = 12
51+
///
4552
/// ### Default values
4653
/// Reading a task local value when no value was bound to it results in returning
4754
/// its default value. For a task local declared as optional (such as e.g. `TraceID?`),
@@ -150,6 +157,8 @@ public macro TaskLocal() =
150157
/// read() // traceID: nil
151158
/// }
152159
/// }
160+
///
161+
/// - SeeAlso: ``TaskLocal-macro``
153162
@available(SwiftStdlib 5.1, *)
154163
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
155164
let defaultValue: Value

0 commit comments

Comments
 (0)