Skip to content

Commit 0e9c3ef

Browse files
committed
[Macros] Add OptionSet and plumb it through
1 parent 3adf16e commit 0e9c3ef

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

lib/Macros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ function(add_swift_macro_library name)
5656
set_property(GLOBAL APPEND PROPERTY SWIFT_MACRO_PLUGINS ${name})
5757
endfunction()
5858

59+
add_subdirectory(Sources/SwiftMacros)
5960
add_subdirectory(Sources/ObservationMacros)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#===--- CMakeLists.txt - Swift macros library ----------------------===#
2+
#
3+
# This source file is part of the Swift.org open source project
4+
#
5+
# Copyright (c) 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+
add_swift_macro_library(SwiftMacros
14+
OptionSetMacro.swift
15+
)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import SwiftDiagnostics
2+
import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
6+
enum OptionSetMacroDiagnostic {
7+
case requiresStruct
8+
case requiresStringLiteral(String)
9+
case requiresOptionsEnum(String)
10+
case requiresOptionsEnumRawType
11+
}
12+
13+
extension OptionSetMacroDiagnostic: DiagnosticMessage {
14+
func diagnose(at node: some SyntaxProtocol) -> Diagnostic {
15+
Diagnostic(node: Syntax(node), message: self)
16+
}
17+
18+
var message: String {
19+
switch self {
20+
case .requiresStruct:
21+
return "'OptionSet' macro can only be applied to a struct"
22+
23+
case .requiresStringLiteral(let name):
24+
return "'OptionSet' macro argument \(name) must be a string literal"
25+
26+
case .requiresOptionsEnum(let name):
27+
return "'OptionSet' macro requires nested options enum '\(name)'"
28+
29+
case .requiresOptionsEnumRawType:
30+
return "'OptionSet' macro requires a raw type"
31+
}
32+
}
33+
34+
var severity: DiagnosticSeverity { .error }
35+
36+
var diagnosticID: MessageID {
37+
MessageID(domain: "Swift", id: "OptionSet.\(self)")
38+
}
39+
}
40+
41+
42+
/// The label used for the OptionSet macro argument that provides the name of
43+
/// the nested options enum.
44+
private let optionsEnumNameArgumentLabel = "optionsName"
45+
46+
/// The default name used for the nested "Options" enum. This should
47+
/// eventually be overridable.
48+
private let defaultOptionsEnumName = "Options"
49+
50+
extension TupleExprElementListSyntax {
51+
/// Retrieve the first element with the given label.
52+
func first(labeled name: String) -> Element? {
53+
return first { element in
54+
if let label = element.label, label.text == name {
55+
return true
56+
}
57+
58+
return false
59+
}
60+
}
61+
}
62+
63+
public struct OptionSetMacro {
64+
/// Decodes the arguments to the macro expansion.
65+
///
66+
/// - Returns: the important arguments used by the various roles of this
67+
/// macro inhabits, or nil if an error occurred.
68+
static func decodeExpansion(
69+
of attribute: AttributeSyntax,
70+
attachedTo decl: some DeclGroupSyntax,
71+
in context: some MacroExpansionContext
72+
) -> (StructDeclSyntax, EnumDeclSyntax, TypeSyntax)? {
73+
// Determine the name of the options enum.
74+
let optionsEnumName: String
75+
if case let .argumentList(arguments) = attribute.argument,
76+
let optionEnumNameArg = arguments.first(labeled: optionsEnumNameArgumentLabel) {
77+
// We have a options name; make sure it is a string literal.
78+
guard let stringLiteral = optionEnumNameArg.expression.as(StringLiteralExprSyntax.self),
79+
stringLiteral.segments.count == 1,
80+
case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first else {
81+
context.diagnose(OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(at: optionEnumNameArg.expression))
82+
return nil
83+
}
84+
85+
optionsEnumName = optionsEnumNameString.content.text
86+
} else {
87+
optionsEnumName = defaultOptionsEnumName
88+
}
89+
90+
// Only apply to structs.
91+
guard let structDecl = decl.as(StructDeclSyntax.self) else {
92+
context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl))
93+
return nil
94+
}
95+
96+
// Find the option enum within the struct.
97+
guard let optionsEnum = decl.members.members.compactMap({ member in
98+
if let enumDecl = member.decl.as(EnumDeclSyntax.self),
99+
enumDecl.identifier.text == optionsEnumName {
100+
return enumDecl
101+
}
102+
103+
return nil
104+
}).first else {
105+
context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl))
106+
return nil
107+
}
108+
109+
// Retrieve the raw type from the attribute.
110+
guard let genericArgs = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause,
111+
let rawType = genericArgs.arguments.first?.argumentType else {
112+
context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute))
113+
return nil
114+
}
115+
116+
117+
return (structDecl, optionsEnum, rawType)
118+
}
119+
}
120+
121+
extension OptionSetMacro: ConformanceMacro {
122+
public static func expansion(
123+
of attribute: AttributeSyntax,
124+
providingConformancesOf decl: some DeclGroupSyntax,
125+
in context: some MacroExpansionContext
126+
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
127+
// Decode the expansion arguments.
128+
guard let (structDecl, _, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else {
129+
return []
130+
}
131+
132+
// If there is an explicit conformance to OptionSet already, don't add one.
133+
if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypeCollection,
134+
inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "OptionSet" }) {
135+
return []
136+
}
137+
138+
return [("OptionSet", nil)]
139+
}
140+
}
141+
142+
extension OptionSetMacro: MemberMacro {
143+
public static func expansion(
144+
of attribute: AttributeSyntax,
145+
providingMembersOf decl: some DeclGroupSyntax,
146+
in context: some MacroExpansionContext
147+
) throws -> [DeclSyntax] {
148+
// Decode the expansion arguments.
149+
guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else {
150+
return []
151+
}
152+
153+
// Find all of the case elements.
154+
let caseElements = optionsEnum.members.members.flatMap { member in
155+
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
156+
return Array<EnumCaseElementSyntax>()
157+
}
158+
159+
return Array(caseDecl.elements)
160+
}
161+
162+
// Dig out the access control keyword we need.
163+
let access = decl.modifiers?.first(where: \.isNeededAccessLevelModifier)
164+
165+
let staticVars = caseElements.map { (element) -> DeclSyntax in
166+
"""
167+
\(access) static let \(element.identifier): Self =
168+
Self(rawValue: 1 << \(optionsEnum.identifier).\(element.identifier).rawValue)
169+
"""
170+
}
171+
172+
return [
173+
"\(access)typealias RawValue = \(rawType)",
174+
"\(access)var rawValue: RawValue",
175+
"\(access)init() { self.rawValue = 0 }",
176+
"\(access)init(rawValue: RawValue) { self.rawValue = rawValue }",
177+
] + staticVars
178+
}
179+
}
180+
181+
extension DeclModifierSyntax {
182+
var isNeededAccessLevelModifier: Bool {
183+
switch self.name.tokenKind {
184+
case .keyword(.public): return true
185+
default: return false
186+
}
187+
}
188+
}
189+
190+
extension SyntaxStringInterpolation {
191+
// It would be nice for SwiftSyntaxBuilder to provide this out-of-the-box.
192+
mutating func appendInterpolation<Node: SyntaxProtocol>(_ node: Node?) {
193+
if let node {
194+
appendInterpolation(node)
195+
}
196+
}
197+
}

stdlib/public/core/Macros.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,33 @@ public macro column<T: ExpressibleByIntegerLiteral>() -> T =
6060
/// Produces the shared object handle for the macro expansion location.
6161
@freestanding(expression)
6262
public macro dsohandle() -> UnsafeRawPointer = Builtin.DSOHandleMacro
63+
64+
/// Create an option set from a struct that contains a nested `Options` enum.
65+
///
66+
/// Attach this macro to a struct that contains a nested `Options` enum
67+
/// with an integer raw value. The struct will be transformed to conform to
68+
/// `OptionSet` by
69+
/// 1. Introducing a `rawValue` stored property to track which options are set,
70+
/// along with the necessary `RawType` typealias and initializers to satisfy
71+
/// the `OptionSet` protocol. The raw type is specified after `@OptionSet`,
72+
/// e.g., `@OptionSet<UInt8>`.
73+
/// 2. Introducing static properties for each of the cases within the `Options`
74+
/// enum, of the type of the struct.
75+
///
76+
/// The `Options` enum must have a raw value, where its case elements
77+
/// each indicate a different option in the resulting option set. For example,
78+
/// the struct and its nested `Options` enum could look like this:
79+
///
80+
/// @OptionSet<UInt8>
81+
/// struct ShippingOptions {
82+
/// private enum Options: Int {
83+
/// case nextDay
84+
/// case secondDay
85+
/// case priority
86+
/// case standard
87+
/// }
88+
/// }
89+
@attached(member)
90+
@attached(conformance)
91+
public macro OptionSet<RawType>() =
92+
#externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

test/Macros/option_set.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins)
2+
// REQUIRES: executable_test
3+
// REQUIRES: OS=macosx
4+
5+
import Swift
6+
7+
@OptionSet<UInt8>
8+
struct ShippingOptions {
9+
private enum Options: Int {
10+
case nextDay
11+
case secondDay
12+
case priority
13+
case standard
14+
}
15+
16+
static let express: ShippingOptions = [.nextDay, .secondDay]
17+
static let all: ShippingOptions = [.express, .priority, .standard]
18+
}
19+
20+
let options = ShippingOptions.express
21+
assert(options.contains(.nextDay))
22+
assert(options.contains(.secondDay))
23+
assert(!options.contains(.standard))
24+

0 commit comments

Comments
 (0)