Skip to content

Commit 27702fa

Browse files
authored
[Distributed][Macro] Handle more cases in distributed protocol macro (#72177)
1 parent f440663 commit 27702fa

10 files changed

+443
-48
lines changed

lib/Macros/Sources/SwiftMacros/DistributedProtocolMacro.swift

Lines changed: 238 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ import SwiftSyntaxBuilder
1919
/// - `distributed actor $MyDistributedActor<ActorSystem>: $MyDistributedActor, _DistributedActorStub where ...`
2020
/// - `extension MyDistributedActor where Self: _DistributedActorStub {}`
2121
public struct DistributedProtocolMacro: ExtensionMacro, PeerMacro {
22+
}
23+
24+
// ===== -----------------------------------------------------------------------
25+
// MARK: Default Stub implementations Extension
26+
27+
extension DistributedProtocolMacro {
28+
29+
/// Introduce the `extension MyDistributedActor` which contains default
30+
/// implementations of the protocol's requirements.
2231
public static func expansion(
2332
of node: AttributeSyntax,
2433
attachedTo declaration: some DeclGroupSyntax,
@@ -27,25 +36,24 @@ public struct DistributedProtocolMacro: ExtensionMacro, PeerMacro {
2736
in context: some MacroExpansionContext
2837
) throws -> [ExtensionDeclSyntax] {
2938
guard let proto = declaration.as(ProtocolDeclSyntax.self) else {
39+
// we diagnose here, only once
40+
try throwIllegalTargetDecl(node: node, declaration)
41+
}
42+
43+
guard !proto.memberBlock.members.isEmpty else {
44+
// ok, the protocol has no requirements so we no-op it
3045
return []
3146
}
3247

48+
let accessModifiers: String = proto.accessModifiersString
49+
3350
let requirements =
3451
proto.memberBlock.members.map { member in
3552
member.trimmed
3653
}
3754
let requirementStubs = requirements
38-
.map { req in
39-
"""
40-
\(req) {
41-
if #available(SwiftStdlib 6.0, *) {
42-
Distributed._distributedStubFatalError()
43-
} else {
44-
fatalError()
45-
}
46-
}
47-
"""
48-
}.joined(separator: "\n ")
55+
.map { stubMethod(access: accessModifiers, $0) }
56+
.joined(separator: "\n ")
4957

5058
let extensionDecl: DeclSyntax =
5159
"""
@@ -56,30 +64,236 @@ public struct DistributedProtocolMacro: ExtensionMacro, PeerMacro {
5664
return [extensionDecl.cast(ExtensionDeclSyntax.self)]
5765
}
5866

67+
static func stubMethod(access: String, _ requirementDeclaration: MemberBlockItemListSyntax.Element) -> String {
68+
"""
69+
\(access)\(requirementDeclaration) {
70+
\(stubFunctionBody())
71+
}
72+
"""
73+
}
74+
75+
static func stubFunctionBody() -> DeclSyntax {
76+
"""
77+
if #available(SwiftStdlib 6.0, *) {
78+
Distributed._distributedStubFatalError()
79+
} else {
80+
fatalError()
81+
}
82+
"""
83+
}
84+
}
85+
86+
// ===== -----------------------------------------------------------------------
87+
// MARK: Distributed Actor Stub type
88+
89+
extension DistributedProtocolMacro {
90+
91+
/// Introduce the `distributed actor` stub type.
5992
public static func expansion(
6093
of node: AttributeSyntax,
6194
providingPeersOf declaration: some DeclSyntaxProtocol,
6295
in context: some MacroExpansionContext
6396
) throws -> [DeclSyntax] {
6497
guard let proto = declaration.as(ProtocolDeclSyntax.self) else {
98+
// don't diagnose here (again),
99+
// we'll already report an error here from the other macro role
65100
return []
66101
}
67102

68-
// FIXME must detect this off the protocol
69-
let serializationRequirementType =
70-
"Codable"
103+
var isGenericStub = false
104+
var specificActorSystemRequirement: TypeSyntax?
71105

72-
let stubActorDecl: DeclSyntax =
73-
"""
74-
distributed actor $\(proto.name.trimmed)<ActorSystem>: \(proto.name.trimmed),
75-
Distributed._DistributedActorStub
76-
where ActorSystem: DistributedActorSystem<any \(raw: serializationRequirementType)>,
77-
ActorSystem.ActorID: \(raw: serializationRequirementType)
78-
{ }
79-
"""
106+
if proto.genericWhereClause == nil {
107+
throw DiagnosticsError(
108+
syntax: node,
109+
message: """
110+
Distributed protocol must declare actor system with SerializationRequirement, for example:
111+
protocol Greeter<ActorSystem>: DistributedActor where ActorSystem: DistributedActorSystem<any Codable>
112+
""", id: .invalidApplication)
113+
}
114+
115+
let accessModifiers = proto.accessModifiersString
116+
117+
for req in proto.genericWhereClause?.requirements ?? [] {
118+
switch req.requirement {
119+
case .conformanceRequirement(let conformanceReq)
120+
where conformanceReq.leftType.isActorSystem:
121+
specificActorSystemRequirement = conformanceReq.rightType.trimmed
122+
isGenericStub = true
123+
124+
case .sameTypeRequirement(let sameTypeReq)
125+
where sameTypeReq.leftType.isActorSystem:
126+
specificActorSystemRequirement = sameTypeReq.rightType.trimmed
127+
isGenericStub = false
128+
129+
default:
130+
continue
131+
}
132+
}
133+
134+
if isGenericStub, let specificActorSystemRequirement {
135+
return [
136+
"""
137+
\(proto.modifiers) distributed actor $\(proto.name.trimmed)<ActorSystem>: \(proto.name.trimmed),
138+
Distributed._DistributedActorStub
139+
where ActorSystem: \(specificActorSystemRequirement)
140+
{ }
141+
"""
142+
]
143+
} else if let specificActorSystemRequirement {
144+
return [
145+
"""
146+
\(proto.modifiers) distributed actor $\(proto.name.trimmed): \(proto.name.trimmed),
147+
Distributed._DistributedActorStub
148+
{
149+
\(typealiasActorSystem(access: accessModifiers, proto, specificActorSystemRequirement))
150+
}
151+
"""
152+
]
153+
} else {
154+
// there may be no `where` clause specifying an actor system,
155+
// but perhaps there is a typealias (or extension with a typealias),
156+
// specifying a concrete actor system so we let this synthesize
157+
// an empty `$Greeter` -- this may fail, or succeed depending on
158+
// surrounding code using a default distributed actor system,
159+
// or extensions providing it.
160+
return [
161+
"""
162+
\(proto.modifiers) distributed actor $\(proto.name.trimmed): \(proto.name.trimmed),
163+
Distributed._DistributedActorStub
164+
{
165+
}
166+
"""
167+
]
168+
}
169+
}
80170

81-
// return [extensionDecl, stubActorDecl]
82-
return [stubActorDecl]
171+
private static func typealiasActorSystem(access: String, _ proto: ProtocolDeclSyntax, _ type: TypeSyntax) -> DeclSyntax {
172+
"\(raw: access)typealias ActorSystem = \(type)"
83173
}
174+
}
84175

176+
// ===== -----------------------------------------------------------------------
177+
// MARK: Convenience Extensions
178+
179+
extension ProtocolDeclSyntax {
180+
var accessModifiersString: String {
181+
let modifiers = modifiers.filter { modifier in
182+
modifier.isAccessControl
183+
}
184+
185+
guard !modifiers.isEmpty else {
186+
return ""
187+
}
188+
189+
let string = modifiers
190+
.map { "\($0.trimmed)" }
191+
.joined(separator: " ")
192+
return "\(string) "
193+
}
194+
}
195+
196+
extension TypeSyntax {
197+
fileprivate var isActorSystem: Bool {
198+
self.trimmedDescription == "ActorSystem"
199+
}
200+
}
201+
202+
extension DeclSyntaxProtocol {
203+
var isClass: Bool {
204+
return self.is(ClassDeclSyntax.self)
205+
}
206+
207+
var isActor: Bool {
208+
return self.is(ActorDeclSyntax.self)
209+
}
210+
211+
var isEnum: Bool {
212+
return self.is(EnumDeclSyntax.self)
213+
}
214+
215+
var isStruct: Bool {
216+
return self.is(StructDeclSyntax.self)
217+
}
218+
}
219+
220+
extension DeclModifierSyntax {
221+
var isAccessControl: Bool {
222+
switch self.name.tokenKind {
223+
case .keyword(.private): fallthrough
224+
case .keyword(.fileprivate): fallthrough
225+
case .keyword(.internal): fallthrough
226+
case .keyword(.package): fallthrough
227+
case .keyword(.public):
228+
return true
229+
default:
230+
return false
231+
}
232+
}
233+
}
234+
235+
// ===== -----------------------------------------------------------------------
236+
// MARK: DistributedProtocol macro errors
237+
238+
extension DistributedProtocolMacro {
239+
static func throwIllegalTargetDecl(node: AttributeSyntax, _ declaration: some DeclSyntaxProtocol) throws -> Never {
240+
let kind: String
241+
if declaration.isClass {
242+
kind = "class"
243+
} else if declaration.isActor {
244+
kind = "actor"
245+
} else if declaration.isStruct {
246+
kind = "struct"
247+
} else if declaration.isStruct {
248+
kind = "enum"
249+
} else {
250+
kind = "\(declaration.kind)"
251+
}
252+
253+
throw DiagnosticsError(
254+
syntax: node,
255+
message: "'@DistributedProtocol' can only be applied to 'protocol', but was attached to '\(kind)'", id: .invalidApplication)
256+
}
257+
}
258+
259+
struct DistributedProtocolMacroDiagnostic: DiagnosticMessage {
260+
enum ID: String {
261+
case invalidApplication = "invalid type"
262+
case missingInitializer = "missing initializer"
263+
}
264+
265+
var message: String
266+
var diagnosticID: MessageID
267+
var severity: DiagnosticSeverity
268+
269+
init(message: String, diagnosticID: SwiftDiagnostics.MessageID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
270+
self.message = message
271+
self.diagnosticID = diagnosticID
272+
self.severity = severity
273+
}
274+
275+
init(message: String, domain: String, id: ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
276+
self.message = message
277+
self.diagnosticID = MessageID(domain: domain, id: id.rawValue)
278+
self.severity = severity
279+
}
280+
}
281+
282+
extension DiagnosticsError {
283+
init<S: SyntaxProtocol>(
284+
syntax: S,
285+
message: String,
286+
domain: String = "Distributed",
287+
id: DistributedProtocolMacroDiagnostic.ID,
288+
severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
289+
self.init(diagnostics: [
290+
Diagnostic(
291+
node: Syntax(syntax),
292+
message: DistributedProtocolMacroDiagnostic(
293+
message: message,
294+
domain: domain,
295+
id: id,
296+
severity: severity))
297+
])
298+
}
85299
}

lib/Sema/TypeCheckDeclPrimary.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3332,9 +3332,8 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
33323332
}
33333333
}
33343334

3335-
if (CD->isDistributedActor()) {
3336-
TypeChecker::checkDistributedActor(SF, CD);
3337-
}
3335+
// Check distributed actors
3336+
TypeChecker::checkDistributedActor(SF, CD);
33383337

33393338
// Force lowering of stored properties.
33403339
(void) CD->getStoredProperties();
@@ -3945,8 +3944,7 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
39453944

39463945
checkExplicitAvailability(ED);
39473946

3948-
if (nominal->isDistributedActor())
3949-
TypeChecker::checkDistributedActor(SF, nominal);
3947+
TypeChecker::checkDistributedActor(SF, nominal);
39503948

39513949
diagnoseIncompatibleProtocolsForMoveOnlyType(ED);
39523950
diagnoseExtensionOfMarkerProtocol(ED);

lib/Sema/TypeCheckDistributed.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ void swift::checkDistributedActorProperties(const NominalTypeDecl *decl) {
686686
// ==== ------------------------------------------------------------------------
687687

688688
void TypeChecker::checkDistributedActor(SourceFile *SF, NominalTypeDecl *nominal) {
689-
if (!nominal)
689+
if (!nominal || !nominal->isDistributedActor())
690690
return;
691691

692692
// ==== Ensure the Distributed module is available,

stdlib/public/Distributed/DistributedMacros.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@
1515
import Swift
1616
import _Concurrency
1717

18-
#if $Macros
18+
// Macros are disabled when Swift is built without swift-syntax.
19+
#if $Macros && hasAttribute(attached)
1920

21+
/// Enables the attached to protocol to be resolved as remote distributed
22+
/// actor reference.
23+
///
24+
/// ### Requirements
25+
///
26+
/// The attached to type must be a protocol that refines the `DistributedActor`
27+
/// protocol. It must either specify a concrete `ActorSystem` or constrain it
28+
/// in such way that the system's `SerializationRequirement` is statically known.
2029
@attached(peer, names: prefixed(`$`)) // provides $Greeter concrete stub type
2130
@attached(extension, names: arbitrary) // provides extension for Greeter & _DistributedActorStub
2231
public macro _DistributedProtocol() =
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// REQUIRES: swift_swift_parser, asserts
2+
//
3+
// UNSUPPORTED: back_deploy_concurrency
4+
// REQUIRES: concurrency
5+
// REQUIRES: distributed
6+
//
7+
// RUN: %empty-directory(%t)
8+
// RUN: %empty-directory(%t-scratch)
9+
10+
// RUN: %target-swift-frontend-emit-module -emit-module-path %t/FakeDistributedActorSystems.swiftmodule -module-name FakeDistributedActorSystems -disable-availability-checking %S/../Inputs/FakeDistributedActorSystems.swift
11+
// RUN: %target-swift-frontend -typecheck -verify -disable-availability-checking -plugin-path %swift-plugin-dir -parse-as-library -I %t %S/../Inputs/FakeDistributedActorSystems.swift -dump-macro-expansions %s 2>&1
12+
13+
import Distributed
14+
15+
@_DistributedProtocol // expected-error{{'@DistributedProtocol' can only be applied to 'protocol', but was attached to 'struct' (from macro '_DistributedProtocol')}}
16+
struct Struct {}
17+
18+
@_DistributedProtocol // expected-error{{'@DistributedProtocol' can only be applied to 'protocol', but was attached to 'class' (from macro '_DistributedProtocol')}}
19+
class Clazz {}
20+
21+
@_DistributedProtocol // expected-error{{'@DistributedProtocol' can only be applied to 'protocol', but was attached to 'actor' (from macro '_DistributedProtocol')}}
22+
actor Act {}
23+
24+
@_DistributedProtocol // expected-error{{'@DistributedProtocol' can only be applied to 'protocol', but was attached to 'actor' (from macro '_DistributedProtocol')}}
25+
distributed actor Caplin {
26+
typealias ActorSystem = FakeActorSystem
27+
}
28+
29+
@_DistributedProtocol // expected-error{{Distributed protocol must declare actor system with SerializationRequirement}}
30+
protocol Fail: DistributedActor {
31+
distributed func method() -> String
32+
}
33+

0 commit comments

Comments
 (0)