Skip to content

Commit d733140

Browse files
authored
Initial support for Swift macros (#6185)
- Add a new module `CompilerPluginSupport` which vends the macro target type - Represent macro targets in the package model - Build system support for macros In order to be able to test this end-to-end before compiler support has landed, we're building macros as dylibs using the `BUILD_MACROS_AS_DYLIBS` flag since that is already supported by the Swift compiler in `main`. The macro API is behind the 999.0 tools version since this has not gone through Swift evolution yet.
1 parent 822b5d7 commit d733140

31 files changed

+434
-41
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version: 999.0
2+
import PackageDescription
3+
import CompilerPluginSupport
4+
5+
let settings: [SwiftSetting] = [
6+
.enableExperimentalFeature("Macros"),
7+
.unsafeFlags(["-Xfrontend", "-dump-macro-expansions"])
8+
]
9+
10+
let package = Package(
11+
name: "MacroPackage",
12+
platforms: [
13+
.macOS(.v10_15),
14+
],
15+
targets: [
16+
.macro(name: "MacroImpl"),
17+
.target(name: "MacroDef", dependencies: ["MacroImpl"], swiftSettings: settings),
18+
.executableTarget(name: "MacroClient", dependencies: ["MacroDef"], swiftSettings: settings),
19+
]
20+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import MacroDef
2+
3+
struct Font: ExpressibleByFontLiteral {
4+
init(fontLiteralName: String, size: Int, weight: MacroDef.FontWeight) {
5+
}
6+
}
7+
8+
let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
public enum FontWeight {
3+
case thin
4+
case normal
5+
case medium
6+
case semiBold
7+
case bold
8+
}
9+
10+
public protocol ExpressibleByFontLiteral {
11+
init(fontLiteralName: String, size: Int, weight: FontWeight)
12+
}
13+
14+
/// Font literal similar to, e.g., #colorLiteral.
15+
@freestanding(expression) public macro fontLiteral<T>(name: String, size: Int, weight: FontWeight) -> T = #externalMacro(module: "MacroImpl", type: "FontLiteralMacro")
16+
where T: ExpressibleByFontLiteral
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
import SwiftSyntaxMacros
4+
5+
/// Implementation of the `#fontLiteral` macro, which is similar in spirit
6+
/// to the built-in expressions `#colorLiteral`, `#imageLiteral`, etc., but in
7+
/// a small macro.
8+
public struct FontLiteralMacro: ExpressionMacro {
9+
public static func expansion(
10+
of macro: some FreestandingMacroExpansionSyntax,
11+
in context: some MacroExpansionContext
12+
) -> ExprSyntax {
13+
let argList = replaceFirstLabel(
14+
of: macro.argumentList,
15+
with: "fontLiteralName"
16+
)
17+
let initSyntax: ExprSyntax = ".init(\(argList))"
18+
if let leadingTrivia = macro.leadingTrivia {
19+
return initSyntax.with(\.leadingTrivia, leadingTrivia)
20+
}
21+
return initSyntax
22+
}
23+
}
24+
25+
/// Replace the label of the first element in the tuple with the given
26+
/// new label.
27+
private func replaceFirstLabel(
28+
of tuple: TupleExprElementListSyntax,
29+
with newLabel: String
30+
) -> TupleExprElementListSyntax {
31+
guard let firstElement = tuple.first else {
32+
return tuple
33+
}
34+
35+
return tuple.replacing(
36+
childAt: 0,
37+
with: firstElement.with(\.label, .identifier(newLabel))
38+
)
39+
}

Package.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ let package = Package(
128128
.library(
129129
name: "PackageDescription",
130130
type: .dynamic,
131-
targets: ["PackageDescription"]
131+
targets: ["PackageDescription", "CompilerPluginSupport"]
132132
),
133133
.library(
134134
name: "PackagePlugin",
@@ -515,6 +515,18 @@ let package = Package(
515515
.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../../lib/swift/macosx"], .when(platforms: [.macOS])),
516516
]),
517517

518+
// MARK: Support for Swift macros, should eventually move to a plugin-based solution
519+
520+
.target(
521+
name: "CompilerPluginSupport",
522+
dependencies: ["PackageDescription"],
523+
exclude: ["CMakeLists.txt"],
524+
swiftSettings: [
525+
.unsafeFlags(["-package-description-version", "999.0"]),
526+
.unsafeFlags(["-enable-library-evolution"]),
527+
]
528+
),
529+
518530
// MARK: Additional Test Dependencies
519531

520532
.target(
@@ -726,3 +738,8 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
726738
.package(path: "../swift-collections"),
727739
]
728740
}
741+
742+
// Enable building macros as dynamic libraries by default for bring-up.
743+
for target in package.targets.filter({ $0.type == .regular || $0.type == .test }) {
744+
target.swiftSettings = (target.swiftSettings ?? []) + [ .define("BUILD_MACROS_AS_DYLIBS") ]
745+
}

Sources/Build/BuildDescription/ProductBuildDescription.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,21 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription
177177

178178
let containsSwiftTargets = self.product.containsSwiftTargets
179179

180+
let derivedProductType: ProductType
180181
switch self.product.type {
182+
case .macro:
183+
#if BUILD_MACROS_AS_DYLIBS
184+
derivedProductType = .library(.dynamic)
185+
#else
186+
derivedProductType = .executable
187+
#endif
188+
default:
189+
derivedProductType = self.product.type
190+
}
191+
192+
switch derivedProductType {
193+
case .macro:
194+
throw InternalError("macro not supported") // should never be reached
181195
case .library(.automatic):
182196
throw InternalError("automatic library not supported")
183197
case .library(.static):
@@ -221,7 +235,7 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription
221235
// version of the package that defines the executable product.
222236
let executableTarget = try product.executableTarget
223237
if executableTarget.underlyingTarget is SwiftTarget, self.toolsVersion >= .v5_5,
224-
self.buildParameters.canRenameEntrypointFunctionName
238+
self.buildParameters.canRenameEntrypointFunctionName, executableTarget.underlyingTarget.type != .macro
225239
{
226240
if let flags = buildParameters.linkerFlagsForRenamingMainFunction(of: executableTarget) {
227241
args += flags
@@ -247,7 +261,7 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription
247261
switch self.product.type {
248262
case .library(let type):
249263
useStdlibRpath = type == .dynamic
250-
case .test, .executable, .snippet:
264+
case .test, .executable, .snippet, .macro:
251265
useStdlibRpath = true
252266
case .plugin:
253267
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
@@ -302,6 +316,13 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription
302316
args += ["-L", toolchainLibDir.pathString]
303317
}
304318

319+
// Library search path for the toolchain's copy of SwiftSyntax.
320+
#if BUILD_MACROS_AS_DYLIBS
321+
if product.type == .macro {
322+
args += try ["-L", buildParameters.toolchain.hostLibDir.pathString]
323+
}
324+
#endif
325+
305326
return args
306327
}
307328

Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public final class SwiftTargetBuildDescription {
9494
var moduleOutputPath: AbsolutePath {
9595
// If we're an executable and we're not allowing test targets to link against us, we hide the module.
9696
let allowLinkingAgainstExecutables = (buildParameters.triple.isDarwin() || self.buildParameters.triple
97-
.isLinux() || self.buildParameters.triple.isWindows()) && self.toolsVersion >= .v5_5
97+
.isLinux() || self.buildParameters.triple.isWindows()) && self.toolsVersion >= .v5_5 && target.type != .macro
9898
let dirPath = (target.type == .executable && !allowLinkingAgainstExecutables) ? self.tempsPath : self
9999
.buildParameters.buildPath
100100
return dirPath.appending(component: self.target.c99name + ".swiftmodule")
@@ -213,6 +213,9 @@ public final class SwiftTargetBuildDescription {
213213
/// The results of running any prebuild commands for this target.
214214
public let prebuildCommandResults: [PrebuildCommandResult]
215215

216+
/// Any macro products that this target requires to build.
217+
public let requiredMacroProducts: [ResolvedProduct]
218+
216219
/// ObservabilityScope with which to emit diagnostics
217220
private let observabilityScope: ObservabilityScope
218221

@@ -225,6 +228,7 @@ public final class SwiftTargetBuildDescription {
225228
buildParameters: BuildParameters,
226229
buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] = [],
227230
prebuildCommandResults: [PrebuildCommandResult] = [],
231+
requiredMacroProducts: [ResolvedProduct] = [],
228232
testTargetRole: TestTargetRole? = nil,
229233
fileSystem: FileSystem,
230234
observabilityScope: ObservabilityScope
@@ -250,6 +254,7 @@ public final class SwiftTargetBuildDescription {
250254
self.pluginDerivedSources = Sources(paths: [], root: buildParameters.dataPath)
251255
self.buildToolPluginInvocationResults = buildToolPluginInvocationResults
252256
self.prebuildCommandResults = prebuildCommandResults
257+
self.requiredMacroProducts = requiredMacroProducts
253258
self.observabilityScope = observabilityScope
254259

255260
// Add any derived files that were declared for any commands from plugin invocations.
@@ -381,6 +386,22 @@ public final class SwiftTargetBuildDescription {
381386
try self.fileSystem.writeIfChanged(path: path, bytes: stream.bytes)
382387
}
383388

389+
private func macroArguments() throws -> [String] {
390+
var args = [String]()
391+
392+
#if BUILD_MACROS_AS_DYLIBS
393+
self.requiredMacroProducts.forEach { macro in
394+
args += ["-Xfrontend", "-load-plugin-library", "-Xfrontend", self.buildParameters.binaryPath(for: macro).pathString]
395+
}
396+
#else
397+
if self.requiredMacroProducts.isEmpty == false {
398+
throw InternalError("building macros is not supported yet")
399+
}
400+
#endif
401+
402+
return args
403+
}
404+
384405
/// The arguments needed to compile this target.
385406
public func compileArguments() throws -> [String] {
386407
var args = [String]()
@@ -480,6 +501,8 @@ public final class SwiftTargetBuildDescription {
480501
}
481502
}
482503

504+
args += try self.macroArguments()
505+
483506
return args
484507
}
485508

@@ -571,6 +594,7 @@ public final class SwiftTargetBuildDescription {
571594
result += try self.moduleCacheArgs
572595
result += self.stdlibArguments
573596
result += try self.buildSettingsFlags()
597+
result += try self.macroArguments()
574598

575599
return result
576600
}
@@ -626,6 +650,7 @@ public final class SwiftTargetBuildDescription {
626650
result += try self.buildSettingsFlags()
627651
result += self.buildParameters.toolchain.extraFlags.swiftCompilerFlags
628652
result += self.buildParameters.swiftCompilerFlags
653+
result += try self.macroArguments()
629654
return result
630655
}
631656

@@ -740,6 +765,13 @@ public final class SwiftTargetBuildDescription {
740765
// Other C flags.
741766
flags += scope.evaluate(.OTHER_CFLAGS).flatMap { ["-Xcc", $0] }
742767

768+
// Include path for the toolchain's copy of SwiftSyntax.
769+
#if BUILD_MACROS_AS_DYLIBS
770+
if target.type == .macro {
771+
flags += try ["-I", self.buildParameters.toolchain.hostLibDir.pathString]
772+
}
773+
#endif
774+
743775
return flags
744776
}
745777

Sources/Build/BuildOperation.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
615615
if importedTarget.target.type == .executable {
616616
return "module '\(importedModule)' is the main module of an executable, and cannot be imported by tests and other targets"
617617
}
618+
if importedTarget.target.type == .macro {
619+
return "module '\(importedModule)' is a macro, and cannot be imported by tests and other targets"
620+
}
618621

619622
// Here we can add more checks in the future.
620623
}

Sources/Build/BuildPlan.swift

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,30 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
413413
self.fileSystem = fileSystem
414414
self.observabilityScope = observabilityScope.makeChildScope(description: "Build Plan")
415415

416+
var productMap: [ResolvedProduct: ProductBuildDescription] = [:]
417+
// Create product description for each product we have in the package graph that is eligible.
418+
for product in graph.allProducts where product.shouldCreateProductDescription {
419+
guard let package = graph.package(for: product) else {
420+
throw InternalError("unknown package for \(product)")
421+
}
422+
// Determine the appropriate tools version to use for the product.
423+
// This can affect what flags to pass and other semantics.
424+
let toolsVersion = package.manifest.toolsVersion
425+
productMap[product] = try ProductBuildDescription(
426+
package: package,
427+
product: product,
428+
toolsVersion: toolsVersion,
429+
buildParameters: buildParameters,
430+
fileSystem: fileSystem,
431+
observabilityScope: observabilityScope
432+
)
433+
}
434+
let macroProductsByTarget = productMap.keys.filter { $0.type == .macro }.reduce(into: [ResolvedTarget: ResolvedProduct]()) {
435+
if let target = $1.targets.first {
436+
$0[target] = $1
437+
}
438+
}
439+
416440
// Create build target description for each target which we need to plan.
417441
// Plugin targets are noted, since they need to be compiled, but they do
418442
// not get directly incorporated into the build description that will be
@@ -448,6 +472,9 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
448472
guard let package = graph.package(for: target) else {
449473
throw InternalError("package not found for \(target)")
450474
}
475+
476+
let requiredMacroProducts = try target.recursiveTargetDependencies().filter { $0.underlyingTarget.type == .macro }.compactMap { macroProductsByTarget[$0] }
477+
451478
targetMap[target] = try .swift(SwiftTargetBuildDescription(
452479
package: package,
453480
target: target,
@@ -456,6 +483,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
456483
buildParameters: buildParameters,
457484
buildToolPluginInvocationResults: buildToolPluginInvocationResults[target] ?? [],
458485
prebuildCommandResults: prebuildCommandResults[target] ?? [],
486+
requiredMacroProducts: requiredMacroProducts,
459487
fileSystem: fileSystem,
460488
observabilityScope: observabilityScope)
461489
)
@@ -509,25 +537,6 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
509537
}
510538
}
511539

512-
var productMap: [ResolvedProduct: ProductBuildDescription] = [:]
513-
// Create product description for each product we have in the package graph that is eligible.
514-
for product in graph.allProducts where product.shouldCreateProductDescription {
515-
guard let package = graph.package(for: product) else {
516-
throw InternalError("unknown package for \(product)")
517-
}
518-
// Determine the appropriate tools version to use for the product.
519-
// This can affect what flags to pass and other semantics.
520-
let toolsVersion = package.manifest.toolsVersion
521-
productMap[product] = try ProductBuildDescription(
522-
package: package,
523-
product: product,
524-
toolsVersion: toolsVersion,
525-
buildParameters: buildParameters,
526-
fileSystem: fileSystem,
527-
observabilityScope: observabilityScope
528-
)
529-
}
530-
531540
self.productMap = productMap
532541
self.targetMap = targetMap
533542
self.pluginDescriptions = pluginDescriptions
@@ -701,7 +710,7 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
701710
switch product.type {
702711
case .library(.automatic), .library(.static), .plugin:
703712
return product.targets.map { .target($0, conditions: []) }
704-
case .library(.dynamic), .test, .executable, .snippet:
713+
case .library(.dynamic), .test, .executable, .snippet, .macro:
705714
return []
706715
}
707716
}
@@ -759,6 +768,10 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
759768
case.unknown:
760769
throw InternalError("unknown binary target '\(target.name)' type")
761770
}
771+
case .macro:
772+
if product.type == .macro {
773+
staticTargets.append(target)
774+
}
762775
case .plugin:
763776
continue
764777
}

0 commit comments

Comments
 (0)