-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Concurrency] Reimplement @TaskLocal as a macro #73078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
1dee757
[Concurrency] Reimplement @TaskLocal as a macro
ktoso 70ac50b
[Concurrency] Allow @TaskLocal on global variables
ktoso 490e67d
[Concurrency] Improve new TaskLocal macro documentation
ktoso 4b515ed
fix typos
ktoso 95da1b5
add missing swift-plugin-dir to test
ktoso 2ac9f70
[Concurrency] Extra runtime test for global task local
ktoso 33e432b
Merge branch 'main' into wip-tasklocal-macro
ktoso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)" | ||
ktoso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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)) | ||
]) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. workaround was removed |
||
} | ||
diagVar = originalVar; | ||
} | ||
if (var->isLet()) { | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be the way to refer to the same named type |
||
@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 | ||
|
2 changes: 1 addition & 1 deletion
2
...Runtime/async_task_locals_spawn_let.swift → ...Runtime/async_task_locals_async_let.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.