Skip to content

Commit ffd852c

Browse files
committed
[Concurrency] Reimplement @TaskLocal as a macro
resolves rdar://120914014
1 parent 763421c commit ffd852c

15 files changed

+226
-28
lines changed

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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
// TODO: docs
18+
public enum TaskLocalMacro {}
19+
20+
extension TaskLocalMacro: PeerMacro {
21+
public static func expansion(
22+
of node: AttributeSyntax,
23+
providingPeersOf declaration: some DeclSyntaxProtocol,
24+
in context: some MacroExpansionContext
25+
) throws -> [DeclSyntax] {
26+
guard let varDecl = try requireVar(declaration, diagnose: false) else {
27+
return []
28+
}
29+
guard try requireModifier(varDecl, .static, diagnose: false) else {
30+
return []
31+
}
32+
33+
guard let firstBinding = varDecl.bindings.first else {
34+
return [] // TODO: make error
35+
}
36+
37+
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
38+
return [] // TODO: make error
39+
}
40+
41+
let type = firstBinding.typeAnnotation?.type
42+
let explicitType: String =
43+
if let type {
44+
": TaskLocal<\(type.trimmed)>"
45+
} else {
46+
""
47+
}
48+
49+
let initialValue: Any
50+
if let initializerValue = firstBinding.initializer?.value {
51+
initialValue = initializerValue
52+
} else if let type, type.isOptional {
53+
initialValue = "nil"
54+
} else {
55+
throw DiagnosticsError(
56+
syntax: declaration,
57+
message: "'@TaskLocal' property must have default value, or be optional", id: .mustBeVar)
58+
}
59+
60+
return [
61+
"""
62+
static let $\(name)\(raw: explicitType) = TaskLocal(wrappedValue: \(raw: initialValue))
63+
"""
64+
]
65+
}
66+
}
67+
68+
extension TaskLocalMacro: AccessorMacro {
69+
public static func expansion(
70+
of node: AttributeSyntax,
71+
providingAccessorsOf declaration: some DeclSyntaxProtocol,
72+
in context: some MacroExpansionContext
73+
) throws -> [AccessorDeclSyntax] {
74+
// We very specifically have to fail and diagnose in the accessor macro,
75+
// rather than in the peer macro, since returning [] from the accessor
76+
// macro adds another series of errors about it missing to emit a decl.
77+
guard let varDecl = try requireVar(declaration) else {
78+
return []
79+
}
80+
try requireModifier(varDecl, .static)
81+
82+
guard let firstBinding = varDecl.bindings.first else {
83+
return [] // TODO: make error
84+
}
85+
86+
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
87+
return [] // TODO: make error
88+
}
89+
90+
return ["get { $\(name).get() }"]
91+
}
92+
}
93+
94+
@discardableResult
95+
private func requireVar(_ decl: some DeclSyntaxProtocol,
96+
diagnose: Bool = true) throws -> VariableDeclSyntax? {
97+
if let varDecl = decl.as(VariableDeclSyntax.self) {
98+
return varDecl
99+
}
100+
if diagnose {
101+
throw DiagnosticsError(
102+
syntax: decl,
103+
message: "'@TaskLocal' can only be applied to properties", id: .mustBeVar)
104+
}
105+
106+
return nil
107+
}
108+
109+
@discardableResult
110+
private func requireModifier(_ decl: VariableDeclSyntax,
111+
_ keyword: Keyword,
112+
diagnose: Bool = true) throws -> Bool {
113+
let isStatic = decl.modifiers.contains { modifier in
114+
modifier.name.text == "\(keyword)"
115+
}
116+
117+
if !isStatic {
118+
if diagnose {
119+
throw DiagnosticsError(
120+
syntax: decl,
121+
message: "'@TaskLocal' can only be applied to 'static' property", id: .mustBeStatic)
122+
} else {
123+
return false
124+
}
125+
}
126+
127+
return true
128+
}
129+
130+
extension TypeSyntax {
131+
// This isn't great since we can't handle type aliases since the macro
132+
// has no type information, but at least for the common case for Optional<T>
133+
// and T? we can detect the optional.
134+
fileprivate var isOptional: Bool {
135+
let strRepr = "\(self)"
136+
return strRepr.last == "?" ||
137+
strRepr.starts(with: "Optional<") ||
138+
strRepr.starts(with: "Swift.Optional<")
139+
}
140+
}
141+
142+
struct TaskLocalMacroDiagnostic: DiagnosticMessage {
143+
enum ID: String {
144+
case mustBeStatic = "must be static"
145+
case mustBeVar = "must be var"
146+
}
147+
148+
var message: String
149+
var diagnosticID: MessageID
150+
var severity: DiagnosticSeverity
151+
152+
init(message: String, diagnosticID: SwiftDiagnostics.MessageID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
153+
self.message = message
154+
self.diagnosticID = diagnosticID
155+
self.severity = severity
156+
}
157+
158+
init(message: String, domain: String, id: ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
159+
self.message = message
160+
self.diagnosticID = MessageID(domain: domain, id: id.rawValue)
161+
self.severity = severity
162+
}
163+
}
164+
165+
extension DiagnosticsError {
166+
init<S: SyntaxProtocol>(
167+
syntax: S,
168+
message: String,
169+
domain: String = "Swift",
170+
id: TaskLocalMacroDiagnostic.ID,
171+
severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
172+
self.init(diagnostics: [
173+
Diagnostic(
174+
node: Syntax(syntax),
175+
message: TaskLocalMacroDiagnostic(
176+
message: message,
177+
domain: domain,
178+
id: id,
179+
severity: severity))
180+
])
181+
}
182+
}

stdlib/public/Concurrency/TaskLocal.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,20 @@
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+
// TODO: docs
21+
@available(SwiftStdlib 5.1, *)
22+
@attached(accessor)
23+
@attached(peer, names: prefixed(`$`))
24+
public macro TaskLocal() =
25+
#externalMacro(module: "SwiftMacros", type: "TaskLocalMacro")
26+
27+
#endif
28+
29+
/// Wrapper that defines a task-local value key.
1730
///
1831
/// A task-local value is a value that can be bound and read in the context of a
1932
/// `Task`. It is implicitly carried with the task, and is accessible by any
@@ -137,7 +150,6 @@ import Swift
137150
/// read() // traceID: nil
138151
/// }
139152
/// }
140-
@propertyWrapper
141153
@available(SwiftStdlib 5.1, *)
142154
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
143155
let defaultValue: Value

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_spawn_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_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

test/Concurrency/async_task_locals_basic_warnings.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// RUN: %empty-directory(%t)
2-
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
2+
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
33

4-
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
5-
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify -enable-upcoming-feature RegionBasedIsolation
4+
// RUN: %target-swift-frontend -I %t -plugin-path %swift-plugin-dir -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
5+
// RUN: %target-swift-frontend -I %t -plugin-path %swift-plugin-dir -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify -enable-upcoming-feature RegionBasedIsolation
66

77
// REQUIRES: concurrency
88
// REQUIRES: asserts

test/Concurrency/async_task_locals_basic_warnings_bug_isolation.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// RUN: %empty-directory(%t)
2-
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
2+
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
33

4-
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
5-
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify -enable-upcoming-feature RegionBasedIsolation
4+
// RUN: %target-swift-frontend -I %t -plugin-path %swift-plugin-dir -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
5+
// RUN: %target-swift-frontend -I %t -plugin-path %swift-plugin-dir -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify -enable-upcoming-feature RegionBasedIsolation
66

77
// REQUIRES: concurrency
88
// REQUIRES: asserts

test/Concurrency/task_local.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
1-
// RUN: %target-swift-frontend -strict-concurrency=targeted -disable-availability-checking -emit-sil -verify -o /dev/null %s
2-
// RUN: %target-swift-frontend -strict-concurrency=complete -verify-additional-prefix complete- -disable-availability-checking -emit-sil -verify -o /dev/null %s
3-
// RUN: %target-swift-frontend -strict-concurrency=complete -verify-additional-prefix complete- -disable-availability-checking -emit-sil -verify -o /dev/null %s -enable-upcoming-feature RegionBasedIsolation
1+
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -strict-concurrency=targeted -disable-availability-checking -emit-sil -verify -o /dev/null %s
2+
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -strict-concurrency=complete -verify-additional-prefix complete- -disable-availability-checking -emit-sil -verify -o /dev/null %s
3+
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -strict-concurrency=complete -verify-additional-prefix complete- -disable-availability-checking -emit-sil -verify -o /dev/null %s -enable-upcoming-feature RegionBasedIsolation
44

55
// REQUIRES: concurrency
66
// REQUIRES: asserts
77

88
@available(SwiftStdlib 5.1, *)
99
struct TL {
10-
@TaskLocal
10+
@TaskLocal // expected-note{{in expansion of macro 'TaskLocal' on static property 'number' here}}
1111
static var number: Int = 0
1212

1313
@TaskLocal
1414
static var someNil: Int?
1515

16-
@TaskLocal
17-
static var noValue: Int // expected-error{{'static var' declaration requires an initializer expression or an explicitly stated getter}}
18-
// expected-note@-1{{add an initializer to silence this error}}
16+
// expected-note@+1{{in expansion of macro 'TaskLocal' on static property 'noValue' here}}
17+
@TaskLocal // expected-error{{@TaskLocal' property must have default value, or be optional}}
18+
static var noValue: Int // expected-note{{'noValue' declared here}}
1919

20-
@TaskLocal
21-
var notStatic: String? // expected-error{{property 'notStatic', must be static because property wrapper 'TaskLocal<String?>' can only be applied to static properties}}
20+
@TaskLocal // expected-error{{'@TaskLocal' can only be applied to 'static' property}}
21+
var notStatic: String?
2222
}
2323

24-
@TaskLocal // expected-error{{property wrappers are not yet supported in top-level code}}
24+
@TaskLocal // expected-error{{'@TaskLocal' can only be applied to 'static' property}}
2525
var global: Int = 0
2626

2727
class NotSendable {}
2828

2929
@available(SwiftStdlib 5.1, *)
3030
func test () async {
3131
TL.number = 10 // expected-error{{cannot assign to property: 'number' is a get-only property}}
32+
3233
TL.$number = 10 // expected-error{{cannot assign value of type 'Int' to type 'TaskLocal<Int>'}}
34+
// expected-error@-1{{cannot assign to property: '$number' is a 'let' constant}}
35+
3336
let _: Int = TL.number
3437
let _: Int = TL.$number.get()
3538
}

0 commit comments

Comments
 (0)