|
| 1 | +# Package Manager Support for Custom Macros |
| 2 | + |
| 3 | +* Proposal: [SE-0394](0394-swiftpm-expression-macros.md) |
| 4 | +* Authors: [Boris Buegling](https://github.com/neonichu), [Doug Gregor](https://github.com/DougGregor) |
| 5 | +* Review Manager: [Becca Royal-Gordon](https://github.com/beccadax) |
| 6 | +* Status: **Active Review (April 3...April 17, 2023)** |
| 7 | +* Implementation: **Available behind pre-release tools-version** ([apple/swift-package-manager#6185](https://github.com/apple/swift-package-manager/pull/6185), [apple/swift-package-manager#6200](https://github.com/apple/swift-package-manager/pull/6200)) |
| 8 | +* Review: ([pitch 1](https://forums.swift.org/t/pitch-package-manager-support-for-custom-macros/63482)) ([pitch 2](https://forums.swift.org/t/pitch-2-package-manager-support-for-custom-macros/63868)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +Macros provide a way to extend Swift by performing arbitary syntactic transformations on input source code to produce new code. One example for this are expression macros which were previously proposed in [SE-0382](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md). This proposal covers how custom macros are defined, built and distributed as part of a Swift package. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +[SE-0382](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md) and [A Possible Vision for Macros in Swift](https://gist.github.com/DougGregor/4f3ba5f4eadac474ae62eae836328b71) covered the motivation for macros themselves, defining them as part of a package will offer a straightforward way to reuse and distribute macros as source code. |
| 17 | + |
| 18 | +## Proposed solution |
| 19 | + |
| 20 | +Macros implemented in an external program can be declared as part of a package via a new macro target type, defined in |
| 21 | +the `CompilerPluginSupport` library: |
| 22 | + |
| 23 | +```swift |
| 24 | +public extension Target { |
| 25 | + /// Creates a macro target. |
| 26 | + /// |
| 27 | + /// - Parameters: |
| 28 | + /// - name: The name of the macro. |
| 29 | + /// - dependencies: The macro's dependencies. |
| 30 | + /// - path: The path of the macro, relative to the package root. |
| 31 | + /// - exclude: The paths to source and resource files you want to exclude from the macro. |
| 32 | + /// - sources: The source files in the macro. |
| 33 | + static func macro( |
| 34 | + name: String, |
| 35 | + dependencies: [Dependency] = [], |
| 36 | + path: String? = nil, |
| 37 | + exclude: [String] = [], |
| 38 | + sources: [String]? = nil |
| 39 | + ) -> Target { ... } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +Similar to package plugins ([SE-0303 "Package Manager Extensible Build Tools"](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md)), macro plugins are built as executables for the host (i.e, where the compiler is run). The compiler receives the paths to these executables from the build system and will run them on demand as part of the compilation process. Macro executables are automatically available for any target that transitively depends on them via the package manifest. |
| 44 | + |
| 45 | +A minimal package containing the implementation, definition and client of a macro would look like this: |
| 46 | + |
| 47 | +```swift |
| 48 | +import PackageDescription |
| 49 | +import CompilerPluginSupport |
| 50 | + |
| 51 | +let package = Package( |
| 52 | + name: "MacroPackage", |
| 53 | + dependencies: [ |
| 54 | + .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), |
| 55 | + ], |
| 56 | + targets: [ |
| 57 | + .macro(name: "MacroImpl", |
| 58 | + dependencies: [ |
| 59 | + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), |
| 60 | + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") |
| 61 | + ]), |
| 62 | + .target(name: "MacroDef", dependencies: ["MacroImpl"]), |
| 63 | + .executableTarget(name: "MacroClient", dependencies: ["MacroDef"]), |
| 64 | + .testTarget(name: "MacroTests", dependencies: ["MacroImpl"]), |
| 65 | + ] |
| 66 | +) |
| 67 | +``` |
| 68 | + |
| 69 | +Macro implementations will be executed in a sandbox [similar to package plugins](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md#security), preventing file system and network access. This is a practical way of encouraging macros to not depend on any state other than the specific macro expansion node they are given to expand and its child nodes (but not its parent nodes), and the information specifically provided by the macro expansion context. If in the future macros need access to other information, this will be accomplished by extending the macro expansion context, which also provides a mechanism for the compiler to track what information the macro actually queried. |
| 70 | + |
| 71 | +Any code from macro implementations can be tested by declaring a dependency on the macro target from a test, this works similarly to the [testing of executable targets](https://github.com/apple/swift-package-manager/pull/3316). |
| 72 | + |
| 73 | +## Detailed Design |
| 74 | + |
| 75 | +SwiftPM builds each macro as an executable for the host platform, applying certain additional compiler flags. Macros are expected to depend on SwiftSyntax using a versioned dependency that corresponds to a particular major Swift release. Note that SwiftPM's dependency resolution is workspace-wide, so all macros (and potentially other clients) will end up consolidating on one particular version of SwiftSyntax. Each target that transitively depends on a macro will have access to it, concretely this happens by SwiftPM passing `-load-plugin-executable` to the compiler to specify which executable contains the implementation of a certain macro module (e.g. `-load-plugin-executable /path/to/package/.build/debug/MacroImpl#MacroImpl` where the argument after the hash symbol is a comma separated list of module names which can be referenced by the `module` parameter of external macro declarations). The macro defintion refers to the module and concrete type via an `#externalMacro` declaration which allows any dependency of the defining target to have access to the concrete macro. If any target of a library product depends on a macro, clients of said library will also get access to any public macros. Macros can have dependencies like any other target, but product dependencies of macros need to be statically linked, so explicitly dynamic library products cannot be used by a macro target. |
| 76 | + |
| 77 | +Concretely, the code for the macro package shown earlier would contain a macro implementation looking like this: |
| 78 | + |
| 79 | +```swift |
| 80 | +import SwiftSyntax |
| 81 | +import SwiftCompilerPlugin |
| 82 | +import SwiftSyntaxBuilder |
| 83 | +import SwiftSyntaxMacros |
| 84 | + |
| 85 | +@main |
| 86 | +struct MyPlugin: CompilerPlugin { |
| 87 | + var providingMacros: [Macro.Type] = [FontLiteralMacro.self] |
| 88 | +} |
| 89 | + |
| 90 | +/// Implementation of the `#fontLiteral` macro, which is similar in spirit |
| 91 | +/// to the built-in expressions `#colorLiteral`, `#imageLiteral`, etc., but in |
| 92 | +/// a small macro. |
| 93 | +public struct FontLiteralMacro: ExpressionMacro { |
| 94 | + public static func expansion( |
| 95 | + of macro: some FreestandingMacroExpansionSyntax, |
| 96 | + in context: some MacroExpansionContext |
| 97 | + ) -> ExprSyntax { |
| 98 | + let argList = replaceFirstLabel( |
| 99 | + of: macro.argumentList, |
| 100 | + with: "fontLiteralName" |
| 101 | + ) |
| 102 | + let initSyntax: ExprSyntax = ".init(\(argList))" |
| 103 | + if let leadingTrivia = macro.leadingTrivia { |
| 104 | + return initSyntax.with(\.leadingTrivia, leadingTrivia) |
| 105 | + } |
| 106 | + return initSyntax |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +/// Replace the label of the first element in the tuple with the given |
| 111 | +/// new label. |
| 112 | +private func replaceFirstLabel( |
| 113 | + of tuple: TupleExprElementListSyntax, |
| 114 | + with newLabel: String |
| 115 | +) -> TupleExprElementListSyntax { |
| 116 | + guard let firstElement = tuple.first else { |
| 117 | + return tuple |
| 118 | + } |
| 119 | + |
| 120 | + return tuple.replacing( |
| 121 | + childAt: 0, |
| 122 | + with: firstElement.with(\.label, .identifier(newLabel)) |
| 123 | + ) |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +The macro definition would look like this: |
| 128 | + |
| 129 | +```swift |
| 130 | +public enum FontWeight { |
| 131 | + case thin |
| 132 | + case normal |
| 133 | + case medium |
| 134 | + case semiBold |
| 135 | + case bold |
| 136 | +} |
| 137 | + |
| 138 | +public protocol ExpressibleByFontLiteral { |
| 139 | + init(fontLiteralName: String, size: Int, weight: FontWeight) |
| 140 | +} |
| 141 | + |
| 142 | +/// Font literal similar to, e.g., #colorLiteral. |
| 143 | +@freestanding(expression) public macro fontLiteral<T>(name: String, size: Int, weight: FontWeight) -> T = #externalMacro(module: "MacroImpl", type: "FontLiteralMacro") |
| 144 | + where T: ExpressibleByFontLiteral |
| 145 | +``` |
| 146 | + |
| 147 | +And the client of the macro would look like this: |
| 148 | + |
| 149 | +```swift |
| 150 | +import MacroDef |
| 151 | + |
| 152 | +struct Font: ExpressibleByFontLiteral { |
| 153 | + init(fontLiteralName: String, size: Int, weight: MacroDef.FontWeight) { |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin) |
| 158 | +``` |
| 159 | + |
| 160 | +SwiftSyntax's versioning scheme is based on Swift major versions (e.g. 509.0.0 for Swift 5.9). |
| 161 | + |
| 162 | +If a package depends on two macros using the `from` version dependency and minor versions of a macro use different versions of SwiftSyntax, users should automatically get a version that's compatible with all macros. For example consider the following where a package depends on both Macro 1 and Macro 2 using `from: "1.0.0"` |
| 163 | + |
| 164 | +``` |
| 165 | +Macro 1 SwiftSyntax Macro 2 |
| 166 | +
|
| 167 | +1.0 --------------> 509.0.0 <-------------- 1.0 |
| 168 | + 509.0.1 <-------------- 1.1 |
| 169 | + 510.0.0 <-------------- 1.2 |
| 170 | +``` |
| 171 | + |
| 172 | +In this case, SwiftPM would choose version 1.0 for Macro 1, version 1.1 for Macro 2 and end up with version 509.0.1 for SwiftSyntax. We're going to monitor how the versioning story plays out in practice and may take further action in SwiftSyntax or SwiftPM's dependency resolution if the concrete need arises. |
| 173 | + |
| 174 | + |
| 175 | +## Impact on existing packages |
| 176 | + |
| 177 | +Since macro plugins are entirely additive, there's no impact on existing packages. |
| 178 | + |
| 179 | +## Alternatives considered |
| 180 | + |
| 181 | +The original pitch of expression macros considered declaring macros by introducing a new capability to [package plugins](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md), but since the execution model is significantly different and the APIs used for macros are external to SwiftPM, this idea was discarded. |
| 182 | + |
| 183 | +## Future Directions |
| 184 | + |
| 185 | +### Generalized support for additional manifest API |
| 186 | + |
| 187 | +The macro target type is provided by a new library `CompilerPluginSupport` as a starting point for making package manifests themselves more extensible. Support for product and target type plugins should eventually be generalized to allow other types of externally defined specialized target types, such as, for example, a Windows application. |
0 commit comments