-
Notifications
You must be signed in to change notification settings - Fork 441
[Macros] Infer nonisolated conformances in macro-expanded code. #3069
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
base: main
Are you sure you want to change the base?
Changes from all commits
3c65036
200ffec
791127b
37c5870
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,147 @@ | ||||||
//===----------------------------------------------------------------------===// | ||||||
// | ||||||
// This source file is part of the Swift.org open source project | ||||||
// | ||||||
// Copyright (c) 2014 - 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 | ||||||
// | ||||||
//===----------------------------------------------------------------------===// | ||||||
// | ||||||
// This file implements inference of "nonisolated" on the conformances that | ||||||
// occur within macro-expanded code. It's meant to provide source compatibility | ||||||
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. Incomplete comment? |
||||||
// | ||||||
|
||||||
import SwiftSyntax | ||||||
|
||||||
extension SyntaxProtocol { | ||||||
/// Given some Swift syntax that may contain type definitions and extensions, | ||||||
/// add "nonisolated" to protocol conformances when there are nonisolated | ||||||
/// members. For example, given: | ||||||
/// | ||||||
/// extension X: P { | ||||||
/// nonisolated func f() { } | ||||||
/// } | ||||||
/// | ||||||
/// this operation will produce: | ||||||
/// | ||||||
/// extension X: nonisolated P { | ||||||
/// nonisolated func f() { } | ||||||
/// } | ||||||
@_spi(Testing) @_spi(Compiler) | ||||||
public func inferNonisolatedConformances() -> Syntax { | ||||||
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 could be a computed property. Also, because it is non-mutating, it should be called |
||||||
let rewriter = NonisolatedConformanceRewriter() | ||||||
return rewriter.rewrite(self) | ||||||
} | ||||||
} | ||||||
|
||||||
fileprivate class NonisolatedConformanceRewriter: SyntaxRewriter { | ||||||
override func visitAny(_ node: Syntax) -> Syntax? { | ||||||
// We only care about decl groups (non-protocol nominal types + extensions) | ||||||
// that have nonisolated members and an inheritance clause. | ||||||
guard let declGroup = node.asProtocol(DeclGroupSyntax.self), | ||||||
!declGroup.is(ProtocolDeclSyntax.self), | ||||||
declGroup.containsNonisolatedMembers, | ||||||
let inheritanceClause = declGroup.inheritanceClause | ||||||
else { | ||||||
return nil | ||||||
} | ||||||
|
||||||
var skipFirst = | ||||||
declGroup.is(ClassDeclSyntax.self) | ||||||
|| (declGroup.is(EnumDeclSyntax.self) && inheritanceClause.inheritedTypes.first?.looksLikeEnumRawType ?? false) | ||||||
let inheritedTypes = inheritanceClause.inheritedTypes.map { inheritedType in | ||||||
// If there's already a 'nonisolated' or some kind of custom attribute | ||||||
if inheritedType.type.hasNonisolatedOrCustomAttribute { | ||||||
return inheritedType | ||||||
} | ||||||
|
||||||
if skipFirst { | ||||||
skipFirst = false | ||||||
return inheritedType | ||||||
} | ||||||
Comment on lines
+61
to
+64
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. Should this check be before the |
||||||
|
||||||
return inheritedType.with(\.type, "nonisolated \(inheritedType.type)") | ||||||
} | ||||||
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. If this doesn't change anything, can we return 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. Returning a non-nil result here means that we’ll traverse the children as well. Returning the node itself stops traversal. We do check the ID of the returned node and if it’s the same as the original, we don’t perform any rewriting. |
||||||
|
||||||
return Syntax( | ||||||
fromProtocol: declGroup.with( | ||||||
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.
Suggested change
so that it doesn't rewrite the whole tree. Same for other |
||||||
\.inheritanceClause, | ||||||
inheritanceClause.with( | ||||||
\.inheritedTypes, | ||||||
InheritedTypeListSyntax(inheritedTypes) | ||||||
) | ||||||
) | ||||||
) | ||||||
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. Returning something from extension MyStruct: P {
nonisolated func f() { }
struct Inner: Q {
nonisolated func b() {}
}
} to extension MyStruct: nonisolated P {
nonisolated func f() { }
struct Inner: nonisolated Q {
nonisolated func b() {}
}
} You'd need .with(
\.memberBlock
self.rewrite(declGroup.memberBlock, detach: true)
) |
||||||
} | ||||||
} | ||||||
|
||||||
extension TypeSyntax { | ||||||
/// Determine whether the given type has a 'nonisolated' specifier or a | ||||||
/// custom attribute (that could be a global actor). | ||||||
fileprivate var hasNonisolatedOrCustomAttribute: Bool { | ||||||
var type = self | ||||||
while let attributedType = type.as(AttributedTypeSyntax.self) { | ||||||
// nonisolated | ||||||
let hasNonisolated = attributedType.specifiers.contains { specifier in | ||||||
if case .nonisolatedTypeSpecifier = specifier { | ||||||
return true | ||||||
} | ||||||
|
||||||
return false | ||||||
} | ||||||
if hasNonisolated { | ||||||
return true | ||||||
} | ||||||
|
||||||
// Any attribute will do. | ||||||
if !attributedType.attributes.isEmpty { | ||||||
return true | ||||||
} | ||||||
|
||||||
type = attributedType.baseType | ||||||
} | ||||||
|
||||||
return false | ||||||
} | ||||||
} | ||||||
|
||||||
extension InheritedTypeSyntax { | ||||||
/// Determine whether this inherited type "looks like" a raw type, e.g., | ||||||
/// if it's one of the integer types or String. This can only be an heuristic, | ||||||
/// because it does not | ||||||
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. Incomplete comment? |
||||||
fileprivate var looksLikeEnumRawType: Bool { | ||||||
// TODO: We could probably use a utility to syntactically recognize types | ||||||
// from the | ||||||
Comment on lines
+116
to
+117
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. Incomplete comment? |
||||||
var text = type.trimmed.description[...] | ||||||
if text.starts(with: "Swift.") { | ||||||
text = text.dropFirst(6) | ||||||
} | ||||||
Comment on lines
+119
to
+121
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. It would be nice to instead look for a |
||||||
|
||||||
switch text { | ||||||
case "Int", "Int8", "Int16", "Int32", "Int64", | ||||||
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", | ||||||
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. Do we also need to include |
||||||
"String": | ||||||
return true | ||||||
|
||||||
default: return false | ||||||
} | ||||||
} | ||||||
} | ||||||
extension DeclModifierListSyntax { | ||||||
/// Whether the modifier list contains "nonisolated". | ||||||
fileprivate var hasNonisolated: Bool { | ||||||
contains { $0.name.tokenKind == .keyword(.nonisolated) } | ||||||
} | ||||||
} | ||||||
|
||||||
extension DeclGroupSyntax { | ||||||
/// Determine whether any of members is marked "nonisolated. | ||||||
fileprivate var containsNonisolatedMembers: Bool { | ||||||
memberBlock.members.lazy.map(\.decl).contains { | ||||||
$0.asProtocol(WithModifiersSyntax.self)?.modifiers.hasNonisolated ?? false | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,4 +15,29 @@ public protocol Macro { | |
/// How the resulting expansion should be formatted, `.auto` by default. | ||
/// Use `.disabled` for the expansion to be used as is. | ||
static var formatMode: FormatMode { get } | ||
|
||
/// Whether to infer "nonisolated" on protocol conformances introduced in | ||
/// the macro expansion when there are some nonisolated members in the | ||
/// corresponding declaration group. When true, macro expansion will adjust | ||
/// expanded code such as | ||
/// | ||
/// extension C: P { | ||
/// nonisolated func f() { } | ||
/// } | ||
/// | ||
/// to | ||
/// | ||
/// extension C: nonisolated P { | ||
/// nonisolated func f() { } | ||
/// } | ||
/// | ||
/// This operation defaults to `true`. Macros can implement it to return | ||
/// `false` to prevent this adjustment to the macro-expanded code. | ||
static var inferNonisolatedConformances: Bool { get } | ||
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. Could you create an RFC for the API addition? |
||
} | ||
|
||
extension Macro { | ||
/// Default implementation of the Macro protocol's | ||
/// `inferNonisolatedConformances` that returns `true`. | ||
public static var inferNonisolatedConformances: Bool { true } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem to be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It ends up getting passed back to the compiler, which checks for its presence. See the compiler PR I just put up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, but I consider
PluginFeature
is more like variation of plugin executables. E.g. single-module-plugin vs. plugin-server.For something like this, I'd use
PluginMessage.PROTOCOL_VERSION_NUMBER
. Like, inPluginHost.swift
in the swift repo:Also, you only added this feature to
LibraryPluginProvider
, which doesn't cover single-module executable plugins.