[Concurrency] Reimplement @TaskLocal as a macro (#73078)

This commit is contained in:
Konrad `ktoso` Malawski
2024-05-02 12:57:20 +09:00
committed by GitHub
parent d451c3e4e1
commit dc5e354d69
18 changed files with 328 additions and 42 deletions

View File

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

View File

@@ -14,6 +14,7 @@ add_swift_macro_library(SwiftMacros
OptionSetMacro.swift
DebugDescriptionMacro.swift
DistributedResolvableMacro.swift
TaskLocalMacro.swift
SWIFT_DEPENDENCIES
SwiftDiagnostics
SwiftOperators

View File

@@ -0,0 +1,218 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftDiagnostics
/// Macro implementing the TaskLocal functionality.
///
/// It introduces a peer `static let $name: TaskLocal<Type>` as well as a getter
/// that accesses the task local storage.
public enum TaskLocalMacro {}
extension TaskLocalMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let varDecl = try requireVar(declaration, diagnose: false) else {
return []
}
guard try requireStaticContext(varDecl, in: context, diagnose: false) else {
return []
}
guard varDecl.bindings.count == 1 else {
throw DiagnosticsError(
syntax: declaration,
message: "'@TaskLocal' property must have exactly one binding", id: .incompatibleDecl)
}
guard let firstBinding = varDecl.bindings.first else {
throw DiagnosticsError(
syntax: declaration,
message: "'@TaskLocal' property must have declared binding", id: .incompatibleDecl)
}
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticsError(
syntax: declaration,
message: "'@TaskLocal' property must have name", id: .incompatibleDecl)
}
let type = firstBinding.typeAnnotation?.type
let explicitTypeAnnotation: TypeAnnotationSyntax?
if let type {
explicitTypeAnnotation = TypeAnnotationSyntax(type: TypeSyntax("TaskLocal<\(type.trimmed)>"))
} else {
explicitTypeAnnotation = nil
}
let initialValue: ExprSyntax
if let initializerValue = firstBinding.initializer?.value {
initialValue = ExprSyntax(initializerValue)
} else if let type, type.isOptional {
initialValue = ExprSyntax(NilLiteralExprSyntax())
} else {
throw DiagnosticsError(
syntax: declaration,
message: "'@TaskLocal' property must have default value, or be optional", id: .mustBeVar)
}
// If the property is global, do not prefix the synthesised decl with 'static'
let isGlobal = context.lexicalContext.isEmpty
let staticKeyword: TokenSyntax?
if isGlobal {
staticKeyword = nil
} else {
staticKeyword = TokenSyntax.keyword(.static, trailingTrivia: .space)
}
return [
"""
\(staticKeyword)let $\(name)\(explicitTypeAnnotation) = TaskLocal(wrappedValue: \(initialValue))
"""
]
}
}
extension TaskLocalMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
// We very specifically have to fail and diagnose in the accessor macro,
// rather than in the peer macro, since returning [] from the accessor
// macro adds another series of errors about it missing to emit a decl.
guard let varDecl = try requireVar(declaration) else {
return []
}
try requireStaticContext(varDecl, in: context)
guard let firstBinding = varDecl.bindings.first else {
return []
}
guard let name = firstBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
return []
}
return ["get { $\(name).get() }"]
}
}
@discardableResult
private func requireVar(_ decl: some DeclSyntaxProtocol,
diagnose: Bool = true) throws -> VariableDeclSyntax? {
if let varDecl = decl.as(VariableDeclSyntax.self) {
return varDecl
}
if diagnose {
throw DiagnosticsError(
syntax: decl,
message: "'@TaskLocal' can only be applied to properties", id: .mustBeVar)
}
return nil
}
@discardableResult
private func requireStaticContext(_ decl: VariableDeclSyntax,
in context: some MacroExpansionContext,
diagnose: Bool = true) throws -> Bool {
let isStatic = decl.modifiers.contains { modifier in
modifier.name.text == "\(Keyword.static)"
}
if isStatic {
return true
}
let isGlobal = context.lexicalContext.isEmpty
if isGlobal {
return true
}
if diagnose {
throw DiagnosticsError(
syntax: decl,
message: "'@TaskLocal' can only be applied to 'static' property, or global variables", id: .mustBeStatic)
}
return false
}
extension TypeSyntax {
// This isn't great since we can't handle type aliases since the macro
// has no type information, but at least for the common case for Optional<T>
// and T? we can detect the optional.
fileprivate var isOptional: Bool {
switch self.as(TypeSyntaxEnum.self) {
case .optionalType:
return true
case .identifierType(let identifierType):
return identifierType.name.text == "Optional"
case .memberType(let memberType):
guard let baseIdentifier = memberType.baseType.as(IdentifierTypeSyntax.self),
baseIdentifier.name.text == "Swift" else {
return false
}
return memberType.name.text == "Optional"
default: return false
}
}
}
struct TaskLocalMacroDiagnostic: DiagnosticMessage {
enum ID: String {
case mustBeVar = "must be var"
case mustBeStatic = "must be static"
case incompatibleDecl = "incompatible declaration"
}
var message: String
var diagnosticID: MessageID
var severity: DiagnosticSeverity
init(message: String, diagnosticID: SwiftDiagnostics.MessageID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
self.message = message
self.diagnosticID = diagnosticID
self.severity = severity
}
init(message: String, domain: String, id: ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
self.message = message
self.diagnosticID = MessageID(domain: domain, id: id.rawValue)
self.severity = severity
}
}
extension DiagnosticsError {
init(
syntax: some SyntaxProtocol,
message: String,
domain: String = "Swift",
id: TaskLocalMacroDiagnostic.ID,
severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
self.init(diagnostics: [
Diagnostic(
node: Syntax(syntax),
message: TaskLocalMacroDiagnostic(
message: message,
domain: domain,
id: id,
severity: severity))
])
}
}

View File

@@ -4980,15 +4980,6 @@ ActorIsolation ActorIsolationRequest::evaluate(
if (var->isGlobalStorage() && !isActorType) {
auto *diagVar = var;
if (auto *originalVar = var->getOriginalWrappedProperty()) {
// temporary 5.10 checking bypass for @TaskLocal <rdar://120907014>
// TODO: @TaskLocal should be a macro <rdar://120914014>
if (auto *classDecl =
var->getInterfaceType()->getClassOrBoundGenericClass()) {
auto &ctx = var->getASTContext();
if (classDecl == ctx.getTaskLocalDecl()) {
return isolation;
}
}
diagVar = originalVar;
}
if (var->isLet()) {

View File

@@ -13,22 +13,42 @@
import Swift
@_implementationOnly import _SwiftConcurrencyShims
/// Property wrapper that defines a task-local value key.
// Macros are disabled when Swift is built without swift-syntax.
#if $Macros && hasAttribute(attached)
/// Macro that introduces a ``TaskLocal-class`` binding.
///
/// For information about task-local bindings, see ``TaskLocal-class``.
///
/// - SeeAlso: ``TaskLocal-class``
@available(SwiftStdlib 5.1, *)
@attached(accessor)
@attached(peer, names: prefixed(`$`))
public macro TaskLocal() =
#externalMacro(module: "SwiftMacros", type: "TaskLocalMacro")
#endif
/// Wrapper type that defines a task-local value key.
///
/// A task-local value is a value that can be bound and read in the context of a
/// `Task`. It is implicitly carried with the task, and is accessible by any
/// child tasks the task creates (such as TaskGroup or `async let` created tasks).
/// ``Task``. It is implicitly carried with the task, and is accessible by any
/// child tasks it creates (such as TaskGroup or `async let` created tasks).
///
/// ### Task-local declarations
///
/// Task locals must be declared as static properties (or global properties,
/// once property wrappers support these), like this:
/// Task locals must be declared as static properties or global properties, like this:
///
/// enum Example {
/// @TaskLocal
/// static var traceID: TraceID?
/// }
///
/// // Global task local properties are supported since Swift 6.0:
/// @TaskLocal
/// var contextualNumber: Int = 12
///
/// ### Default values
/// Reading a task local value when no value was bound to it results in returning
/// its default value. For a task local declared as optional (such as e.g. `TraceID?`),
@@ -137,7 +157,8 @@ import Swift
/// read() // traceID: nil
/// }
/// }
@propertyWrapper
///
/// - SeeAlso: ``TaskLocal-macro``
@available(SwiftStdlib 5.1, *)
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
let defaultValue: Value

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency
@@ -8,11 +8,14 @@
// REQUIRES: concurrency_runtime
// UNSUPPORTED: back_deployment_runtime
final class StringLike: Sendable, CustomStringConvertible {
final class StringLike: Sendable, ExpressibleByStringLiteral, CustomStringConvertible {
let value: String
init(_ value: String) {
self.value = value
}
init(stringLiteral value: StringLiteralType) {
self.value = value
}
var description: String { value }
}
@@ -33,6 +36,9 @@ enum TL {
static var clazz: ClassTaskLocal?
}
@TaskLocal
var globalTaskLocal: StringLike = StringLike("<not-set>")
@available(SwiftStdlib 5.1, *)
final class ClassTaskLocal: Sendable {
init() {
@@ -217,6 +223,13 @@ func inside_actor() async {
await Worker().setAndRead()
}
@available(SwiftStdlib 5.1, *)
func global_task_local() async {
await $globalTaskLocal.withValue("value-1") {
await printTaskLocalAsync($globalTaskLocal) // CHECK-NEXT: TaskLocal<StringLike>(defaultValue: <not-set>) (value-1)
}
}
@available(SwiftStdlib 5.1, *)
@main struct Main {
static func main() async {
@@ -229,5 +242,6 @@ func inside_actor() async {
await nested_3_onlyTopContributesAsync()
await nested_3_onlyTopContributesMixed()
await inside_actor()
await global_task_local()
}
}

View File

@@ -1,5 +1,5 @@
// REQUIRES: rdar80824152
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library %import-libdispatch) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

@@ -1,4 +1,4 @@
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// RUN: %target-run-simple-swift( -plugin-path %swift-plugin-dir -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: concurrency

View File

@@ -1,8 +1,8 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
// 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
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
// 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
// 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
// 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
// REQUIRES: concurrency
// REQUIRES: asserts

View File

@@ -1,8 +1,8 @@
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
// 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
// RUN: %target-swift-frontend -I %t -disable-availability-checking -strict-concurrency=complete -parse-as-library %s -emit-sil -o /dev/null -verify
// 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
// 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
// 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
// REQUIRES: concurrency
// REQUIRES: asserts

View File

@@ -1,27 +1,27 @@
// RUN: %target-swift-frontend -strict-concurrency=targeted -disable-availability-checking -emit-sil -verify -o /dev/null %s
// RUN: %target-swift-frontend -strict-concurrency=complete -verify-additional-prefix complete- -disable-availability-checking -emit-sil -verify -o /dev/null %s
// 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
// RUN: %target-swift-frontend -plugin-path %swift-plugin-dir -strict-concurrency=targeted -disable-availability-checking -emit-sil -verify -o /dev/null %s
// 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
// 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
// REQUIRES: concurrency
// REQUIRES: asserts
@available(SwiftStdlib 5.1, *)
struct TL {
@TaskLocal
@TaskLocal // expected-note{{in expansion of macro 'TaskLocal' on static property 'number' here}}
static var number: Int = 0
@TaskLocal
static var someNil: Int?
@TaskLocal
static var noValue: Int // expected-error{{'static var' declaration requires an initializer expression or an explicitly stated getter}}
// expected-note@-1{{add an initializer to silence this error}}
// expected-note@+1{{in expansion of macro 'TaskLocal' on static property 'noValue' here}}
@TaskLocal // expected-error{{@TaskLocal' property must have default value, or be optional}}
static var noValue: Int // expected-note{{'noValue' declared here}}
@TaskLocal
var notStatic: String? // expected-error{{property 'notStatic', must be static because property wrapper 'TaskLocal<String?>' can only be applied to static properties}}
@TaskLocal // expected-error{{'@TaskLocal' can only be applied to 'static' property}}
var notStatic: String?
}
@TaskLocal // expected-error{{property wrappers are not yet supported in top-level code}}
@TaskLocal
var global: Int = 0
class NotSendable {}
@@ -29,7 +29,19 @@ class NotSendable {}
@available(SwiftStdlib 5.1, *)
func test () async {
TL.number = 10 // expected-error{{cannot assign to property: 'number' is a get-only property}}
TL.$number = 10 // expected-error{{cannot assign value of type 'Int' to type 'TaskLocal<Int>'}}
// expected-error@-1{{cannot assign to property: '$number' is a 'let' constant}}
let _: Int = TL.number
let _: Int = TL.$number.get()
}
@TaskLocal // expected-error{{'accessor' macro cannot be attached to global function ('test')}}
func test() {}
class X {
@TaskLocal // expected-error{{'accessor' macro cannot be attached to static method ('test')}}
static func test() {
}
}

View File

@@ -1,5 +1,5 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift -O -Xfrontend -disable-availability-checking %s -parse-as-library -module-name main -o %t/main
// RUN: %target-build-swift -O -Xfrontend -disable-availability-checking %s -plugin-path %swift-plugin-dir -parse-as-library -module-name main -o %t/main
// RUN: %target-codesign %t/main
// RUN: %target-run %t/main | %FileCheck %s