From 2fd4ea7224793450fd16d26b2fd3ba928091126f Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 26 Sep 2025 10:35:29 -0700 Subject: [PATCH 01/11] [SwiftIfConfig] Add StaticBuildConfiguration Introduce StaticBuildConfiguration, a data structure that describes all of the aspects of a build configuration that are needed for `#if` evaluation. It is Codable so that it can be stored and replayed later. --- .../SwiftIfConfig/BuildConfiguration.swift | 2 +- Sources/SwiftIfConfig/CMakeLists.txt | 1 + .../StaticBuildConfiguration.swift | 431 ++++++++++++++++++ Sources/SwiftIfConfig/VersionTuple.swift | 2 +- .../SwiftIfConfigTest/ActiveRegionTests.swift | 3 +- Tests/SwiftIfConfigTest/EvaluateTests.swift | 12 +- Tests/SwiftIfConfigTest/VisitorTests.swift | 6 +- 7 files changed, 444 insertions(+), 13 deletions(-) create mode 100644 Sources/SwiftIfConfig/StaticBuildConfiguration.swift diff --git a/Sources/SwiftIfConfig/BuildConfiguration.swift b/Sources/SwiftIfConfig/BuildConfiguration.swift index 407aa1a13d6..dfa90599340 100644 --- a/Sources/SwiftIfConfig/BuildConfiguration.swift +++ b/Sources/SwiftIfConfig/BuildConfiguration.swift @@ -14,7 +14,7 @@ import SwiftSyntax /// Describes the ordering of a sequence of bytes that make up a word of /// storage for a particular architecture. -public enum Endianness: String { +public enum Endianness: String, Codable { /// Little endian, meaning that the least significant byte of a word is /// stored at the lowest address. case little diff --git a/Sources/SwiftIfConfig/CMakeLists.txt b/Sources/SwiftIfConfig/CMakeLists.txt index 2df83517c5b..c9534ce9ec8 100644 --- a/Sources/SwiftIfConfig/CMakeLists.txt +++ b/Sources/SwiftIfConfig/CMakeLists.txt @@ -17,6 +17,7 @@ add_swift_syntax_library(SwiftIfConfig IfConfigDiagnostic.swift IfConfigEvaluation.swift IfConfigFunctions.swift + StaticBuildConfiguration.swift SyntaxLiteralUtils.swift SyntaxProtocol+IfConfig.swift VersionTuple+Parsing.swift diff --git a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift new file mode 100644 index 00000000000..2c799e08c2e --- /dev/null +++ b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift @@ -0,0 +1,431 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// A statically-determined build configuration that can be used with any +/// API that requires a build configuration. Static build configurations can +/// be (de-)serialized via Codable. +public struct StaticBuildConfiguration: Codable { + public init( + customConditions: Set = [], + features: Set = [], + attributes: Set = [], + importedModules: [String: [VersionedImportModule]] = [:], + targetOSNames: Set = [], + targetArchitectures: Set = [], + targetEnvironments: Set = [], + targetRuntimes: Set = [], + targetPointerAuthenticationSchemes: Set = [], + targetObjectFileFormats: Set = [], + targetPointerBitWidth: Int = 64, + targetAtomicBitWidths: [Int] = [], + endianness: Endianness = .little, + languageVersion: VersionTuple = VersionTuple(6), + compilerVersion: VersionTuple = VersionTuple(6, 2) + ) { + self.customConditions = customConditions + self.features = features + self.attributes = attributes + self.importedModules = importedModules + self.targetOSNames = targetOSNames + self.targetArchitectures = targetArchitectures + self.targetEnvironments = targetEnvironments + self.targetRuntimes = targetRuntimes + self.targetPointerAuthenticationSchemes = targetPointerAuthenticationSchemes + self.targetObjectFileFormats = targetObjectFileFormats + self.targetPointerBitWidth = targetPointerBitWidth + self.targetAtomicBitWidths = targetAtomicBitWidths + self.endianness = endianness + self.languageVersion = languageVersion + self.compilerVersion = compilerVersion + } + + /// The set of custom conditions that are present and can be used with `#if`. + /// + /// Custom build conditions can be set by the `-D` command line option to + /// the Swift compiler. For example, `-DDEBUG` sets the custom condition + /// named `DEBUG`, which could be checked with, e.g., + /// + /// ```swift + /// #if DEBUG + /// // ... + /// #endif + /// ``` + public var customConditions: Set = [] + + /// The set of language features that are enabled. + /// + /// Features are determined by the Swift compiler, language mode, and other + /// options such as `--enable-upcoming-feature`, and can be checked with + /// the `hasFeature` syntax, e.g., + /// + /// ```swift + /// #if hasFeature(VariadicGenerics) + /// // ... + /// #endif + /// ``` + public var features: Set = [] + + /// The set of attributes that are available. + /// + /// Attributes are determined by the Swift compiler. They can be checked + /// with `hasAttribute` syntax, e.g., + /// + /// ```swift + /// #if hasAttribute(available) + /// // ... + /// #endif + /// ``` + public var attributes: Set = [] + + /// The set of modules that can be imported, and their version and underlying + /// versions (if known). These are organized by top-level module name, + /// with paths (to submodules) handled internally. + public var importedModules: [String: [VersionedImportModule]] = [:] + + /// The active target OS names, e.g., "Windows", "iOS". + public var targetOSNames: Set = [] + + /// The active target architectures, e.g., "x64_64". + public var targetArchitectures: Set = [] + + /// The active target environments, e.g., "simulator". + public var targetEnvironments: Set = [] + + /// The active target runtimes, e.g., _ObjC. + public var targetRuntimes: Set = [] + + /// The active target's pointer authentication schemes, e.g., "arm64e". + public var targetPointerAuthenticationSchemes: Set = [] + + /// The active target's object file formats, e.g., "COFF" + public var targetObjectFileFormats: Set = [] + + /// The bit width of a data pointer for the target architecture. + /// + /// The target's pointer bit width (which also corresponds to the number of + /// bits in `Int`/`UInt`) can only be queried with the experimental syntax + /// `_pointerBitWidth(_)`, e.g., + /// + /// ```swift + /// #if _pointerBitWidth(_32) + /// // 32-bit system + /// #endif + /// ``` + public var targetPointerBitWidth: Int = 64 + + /// The atomic bit widths that are natively supported by the target + /// architecture. + /// + /// This lists all of the bit widths for which the target provides support + /// for atomic operations. It can be queried with + /// `_hasAtomicBitWidth(_)`, e.g. + /// + /// ```swift + /// #if _hasAtomicBitWidth(_64) + /// // 64-bit atomics are available + /// #endif + public var targetAtomicBitWidths: [Int] = [] + + /// The endianness of the target architecture. + /// + /// The target's endianness can onyl be queried with the experimental syntax + /// `_endian()`, where `` can be either "big" or "little", e.g., + /// + /// ```swift + /// #if _endian(little) + /// // Swap some bytes around for network byte order + /// #endif + /// ``` + public var endianness: Endianness = .little + + /// The effective language version, which can be set by the user (e.g., 5.0). + /// + /// The language version can be queried with the `swift` directive that checks + /// how the supported language version compares, as described by + /// [SE-0212](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: + /// + /// ```swift + /// #if swift(>=5.5) + /// // Hooray, we can use tasks! + /// ``` + public var languageVersion: VersionTuple = VersionTuple(6) + + /// The version of the compiler (e.g., 5.9). + /// + /// The compiler version can be queried with the `compiler` directive that + /// checks the specific version of the compiler being used to process the + /// code, e.g., + /// + /// ```swift + /// #if compiler(>=5.7) + /// // Hoorway, we can implicitly open existentials! + /// #endif + public var compilerVersion: VersionTuple = VersionTuple(6, 2) +} + +extension StaticBuildConfiguration: BuildConfiguration { + /// Determine whether a given custom build condition has been set. + /// + /// Custom build conditions can be set by the `-D` command line option to + /// the Swift compiler. For example, `-DDEBUG` sets the custom condition + /// named `DEBUG`, which could be checked with, e.g., + /// + /// ```swift + /// #if DEBUG + /// // ... + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the custom build condition being checked (e.g., + /// `DEBUG`. + /// - Returns: Whether the custom condition is set. + public func isCustomConditionSet(name: String) -> Bool { + // "$FeatureName" can be used to check for a language feature. + if let firstChar = name.first, firstChar == "$" { + return features.contains(String(name.dropFirst())) + } + + return customConditions.contains(name) + } + + /// Determine whether the given feature is enabled. + /// + /// Features are determined by the Swift compiler, language mode, and other + /// options such as `--enable-upcoming-feature`, and can be checked with + /// the `hasFeature` syntax, e.g., + /// + /// ```swift + /// #if hasFeature(VariadicGenerics) + /// // ... + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the feature being checked. + /// - Returns: Whether the requested feature is available. + public func hasFeature(name: String) -> Bool { + features.contains(name) + } + + /// Determine whether the given attribute is available. + /// + /// Attributes are determined by the Swift compiler. They can be checked + /// with `hasAttribute` syntax, e.g., + /// + /// ```swift + /// #if hasAttribute(available) + /// // ... + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the attribute being queried. + /// - Returns: Whether the requested attribute is supported. + public func hasAttribute(name: String) -> Bool { + attributes.contains(name) + } + + /// Determine whether a module with the given import path can be imported, + /// with additional version information. + /// + /// The availability of a module for import can be checked with `canImport`, + /// e.g., + /// + /// ```swift + /// #if canImport(UIKit) + /// // ... + /// #endif + /// ``` + /// + /// There is an experimental syntax for providing required module version + /// information, which will translate into the `version` argument. + /// + /// - Parameters: + /// - importPath: A nonempty sequence of (token, identifier) pairs + /// describing the imported module, which was written in source as a + /// dotted sequence, e.g., `UIKit.UIViewController` will be passed in as + /// the import path array `[(token, "UIKit"), (token, "UIViewController")]`. + /// - version: The version restriction on the imported module. For the + /// normal `canImport()` syntax, this will always be + /// `CanImportVersion.unversioned`. + /// - Returns: Whether the module can be imported. + public func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) -> Bool { + // If we don't have any record of the top-level module, we cannot import it. + guard let topLevelModuleName = importPath.first?.1, + let versionedImports = importedModules[topLevelModuleName] + else { + return false + } + + // Match on submodule path. + let submodulePath = Array(importPath.lazy.map(\.1).dropFirst()) + guard let matchingImport = versionedImports.first(where: { $0.submodulePath == submodulePath }) else { + return false + } + + switch version { + case .unversioned: + return true + + case .version(let expectedVersion): + guard let actualVersion = matchingImport.version else { + return false + } + + return actualVersion >= expectedVersion + + case .underlyingVersion(let expectedVersion): + guard let actualVersion = matchingImport.underlyingVersion else { + return false + } + + return actualVersion >= expectedVersion + } + } + + /// Determine whether the given name is the active target OS (e.g., Linux, iOS). + /// + /// The target operating system can be queried with `os()`, e.g., + /// + /// ```swift + /// #if os(Linux) + /// // Linux-specific implementation + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the operating system being queried, such as `Linux`, + /// `Windows`, `macOS`, etc. + /// - Returns: Whether the given operating system name is the target operating + /// system, i.e., the operating system for which code is being generated. + public func isActiveTargetOS(name: String) -> Bool { + targetOSNames.contains(name) + } + + /// Determine whether the given name is the active target architecture + /// (e.g., x86_64, arm64). + /// + /// The target processor architecture can be queried with `arch()`, e.g., + /// + /// ```swift + /// #if arch(x86_64) + /// // 64-bit x86 Intel-specific code + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the target architecture to check. + /// - Returns: Whether the given processor architecture is the target + /// architecture. + public func isActiveTargetArchitecture(name: String) -> Bool { + targetArchitectures.contains(name) + } + + /// Determine whether the given name is the active target environment (e.g., simulator) + /// + /// The target environment can be queried with `targetEnvironment()`, + /// e.g., + /// + /// ```swift + /// #if targetEnvironment(simulator) + /// // Simulator-specific code + /// #endif + /// ``` + /// + /// - Parameters: + /// - name: The name of the target environment to check. + /// - Returns: Whether the target platform is for a specific environment, + /// such as a simulator or emulator. + public func isActiveTargetEnvironment(name: String) -> Bool { + targetEnvironments.contains(name) + } + + /// Determine whether the given name is the active target runtime (e.g., _ObjC vs. _Native) + /// + /// The target runtime can only be queried by an experimental syntax + /// `_runtime()`, e.g., + /// + /// ```swift + /// #if _runtime(_ObjC) + /// // Code that depends on Swift being built for use with the Objective-C + /// // runtime, e.g., on Apple platforms. + /// #endif + /// ``` + /// + /// The only other runtime is "none", when Swift isn't tying into any other + /// specific runtime. + /// + /// - Parameters: + /// - name: The name of the runtime. + /// - Returns: Whether the target runtime matches the given name. + public func isActiveTargetRuntime(name: String) -> Bool { + targetRuntimes.contains(name) + } + + /// Determine whether the given name is the active target pointer authentication scheme (e.g., arm64e). + /// + /// The target pointer authentication scheme describes how pointers are + /// signed, as a security mitigation. This scheme can only be queried by + /// an experimental syntax `_ptrath()`, e.g., + /// + /// ```swift + /// #if _ptrauth(arm64e) + /// // Special logic for arm64e pointer signing + /// #endif + /// ``` + /// - Parameters: + /// - name: The name of the pointer authentication scheme to check. + /// - Returns: Whether the code generated for the target will use the given + /// pointer authentication scheme. + public func isActiveTargetPointerAuthentication(name: String) -> Bool { + targetPointerAuthenticationSchemes.contains(name) + } + + /// Determine whether the given name is the active target object file format (e.g., ELF). + /// + /// The target object file format can only be queried by an experimental + /// syntax `_objectFileFormat()`, e.g., + /// + /// ```swift + /// #if _objectFileFormat(ELF) + /// // Special logic for ELF object file formats + /// #endif + /// ``` + /// - Parameters: + /// - name: The name of the object file format. + /// - Returns: Whether the target object file format matches the given name. + @_spi(ExperimentalLanguageFeatures) + public func isActiveTargetObjectFileFormat(name: String) -> Bool { + targetObjectFileFormats.contains(name) + } +} + +/// Information about a potentially-versioned import of a given module. +/// +/// Each instance of this struct is associated with a top-level module of some +/// form. When the submodule path is empty, it refers to the top-level module +/// itself. +public struct VersionedImportModule: Codable { + /// The submodule path (which may be empty) from the top-level module to + /// this specific import. + public var submodulePath: [String] = [] + + /// The version that was imported, if known. + public var version: VersionTuple? = nil + + /// The version of the underlying Clang module, if there is one and it is + /// known. + public var underlyingVersion: VersionTuple? = nil +} diff --git a/Sources/SwiftIfConfig/VersionTuple.swift b/Sources/SwiftIfConfig/VersionTuple.swift index d31d544b59b..ce2484c5a3c 100644 --- a/Sources/SwiftIfConfig/VersionTuple.swift +++ b/Sources/SwiftIfConfig/VersionTuple.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// /// Describes a version such as `5.9`. -public struct VersionTuple: Sendable { +public struct VersionTuple: Sendable, Codable { /// The components of the version tuple, start with the major version. public var components: [Int] diff --git a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift index 0642823bc34..b31869357ba 100644 --- a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift +++ b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift @@ -20,12 +20,11 @@ import _SwiftSyntaxGenericTestSupport import _SwiftSyntaxTestSupport public class ActiveRegionTests: XCTestCase { - let linuxBuildConfig = TestingBuildConfiguration( + let linuxBuildConfig = StaticBuildConfiguration( customConditions: ["DEBUG", "ASSERTS"], features: ["ParameterPacks"], attributes: ["available"] ) - func testActiveRegions() throws { try assertActiveCode( """ diff --git a/Tests/SwiftIfConfigTest/EvaluateTests.swift b/Tests/SwiftIfConfigTest/EvaluateTests.swift index 77a677977f4..cf57e052c4f 100644 --- a/Tests/SwiftIfConfigTest/EvaluateTests.swift +++ b/Tests/SwiftIfConfigTest/EvaluateTests.swift @@ -21,7 +21,7 @@ import _SwiftSyntaxTestSupport public class EvaluateTests: XCTestCase { func testLiterals() throws { - let buildConfig = TestingBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) + let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) assertIfConfig("true", .active, configuration: buildConfig) assertIfConfig("false", .inactive, configuration: buildConfig) @@ -71,7 +71,7 @@ public class EvaluateTests: XCTestCase { } func testCustomConfigs() throws { - let buildConfig = TestingBuildConfiguration(customConditions: ["DEBUG", "ASSERTS", "try"]) + let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS", "try"]) assertIfConfig("DEBUG", .active, configuration: buildConfig) assertIfConfig("NODEBUG", .inactive, configuration: buildConfig) @@ -136,7 +136,7 @@ public class EvaluateTests: XCTestCase { } func testBadExpressions() throws { - let buildConfig = TestingBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) + let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) assertIfConfig( "3.14159", @@ -178,14 +178,14 @@ public class EvaluateTests: XCTestCase { } func testFeatures() throws { - let buildConfig = TestingBuildConfiguration(features: ["ParameterPacks"]) + let buildConfig = StaticBuildConfiguration(features: ["ParameterPacks"]) assertIfConfig("hasFeature(ParameterPacks)", .active, configuration: buildConfig) assertIfConfig("hasFeature(HigherKindedGenerics)", .inactive, configuration: buildConfig) } func testAttributes() throws { - let buildConfig = TestingBuildConfiguration(attributes: ["available"]) + let buildConfig = StaticBuildConfiguration(attributes: ["available"]) assertIfConfig("hasAttribute(available)", .active, configuration: buildConfig) assertIfConfig("hasAttribute(unsafeUnavailable)", .inactive, configuration: buildConfig) @@ -533,7 +533,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig( "defined(FOO)", .active, - configuration: TestingBuildConfiguration(customConditions: ["FOO"]), + configuration: StaticBuildConfiguration(customConditions: ["FOO"]), diagnostics: [ DiagnosticSpec( message: message, diff --git a/Tests/SwiftIfConfigTest/VisitorTests.swift b/Tests/SwiftIfConfigTest/VisitorTests.swift index 2c6960c7516..bcd0ea4eb12 100644 --- a/Tests/SwiftIfConfigTest/VisitorTests.swift +++ b/Tests/SwiftIfConfigTest/VisitorTests.swift @@ -45,8 +45,8 @@ class AllActiveVisitor: ActiveSyntaxAnyVisitor { } } -class NameCheckingVisitor: ActiveSyntaxAnyVisitor { - let configuration: TestingBuildConfiguration +class NameCheckingVisitor: ActiveSyntaxAnyVisitor { + let configuration: Configuration /// The set of names we are expected to visit. Any syntax nodes with /// names that aren't here will be rejected, and each of the names listed @@ -54,7 +54,7 @@ class NameCheckingVisitor: ActiveSyntaxAnyVisitor { var expectedNames: Set init( - configuration: TestingBuildConfiguration, + configuration: Configuration, expectedNames: Set, configuredRegions: ConfiguredRegions? = nil ) { From 82cb9bcbd1605610b3d165090e36d2a4fb571925 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 26 Sep 2025 11:47:14 -0700 Subject: [PATCH 02/11] Extend swift-parser-cli with option to apply a static build configuration Introduce the option "--build-configuration" to this test program. The input is JSON for a static build configuration, and that configuration will be applied to the parse tree to remove all inactive #if regions before doing anything else with the parse command (printing diagnostics, printing the tree, etc.). Also add a "print" subcommand that prints back the tree after applying whatever transformations are described above. One can use this to implement a basic "preprocess" step that removes all inactive #if regions from the source code. --- SwiftParserCLI/Package.swift | 1 + .../swift-parser-cli/Commands/Print.swift | 34 +++++++++++ .../Commands/PrintDiags.swift | 8 +-- .../swift-parser-cli/Commands/PrintTree.swift | 13 +---- .../swift-parser-cli/ParseCommand.swift | 57 +++++++++++++++++++ .../swift-parser-cli/swift-parser-cli.swift | 1 + 6 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift diff --git a/SwiftParserCLI/Package.swift b/SwiftParserCLI/Package.swift index c59ec321d32..ef12c957302 100644 --- a/SwiftParserCLI/Package.swift +++ b/SwiftParserCLI/Package.swift @@ -25,6 +25,7 @@ let package = Package( "InstructionCounter", .product(name: "SwiftBasicFormat", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), + .product(name: "SwiftIfConfig", package: "swift-syntax"), .product(name: "SwiftOperators", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift new file mode 100644 index 00000000000..5c6f4bb42eb --- /dev/null +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import SwiftParser +import SwiftSyntax + +struct Print: ParsableCommand, ParseCommand { + static var configuration = CommandConfiguration( + commandName: "print", + abstract: "Print the parsed source file after applying any other arguments" + ) + + @OptionGroup + var arguments: ParseArguments + + @Flag(name: .long, help: "Include trivia in the output") + var includeTrivia: Bool = false + + func run() throws { + try withParsedSourceFile(wantDiagnostics: false) { (tree, _) in + print(tree.description) + } + } +} diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift index 048ff69880c..4bef04255e1 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift @@ -28,13 +28,7 @@ struct PrintDiags: ParsableCommand, ParseCommand { var colorize: Bool = false func run() throws { - try sourceFileContents.withUnsafeBufferPointer { sourceBuffer in - let tree = Parser.parse(source: sourceBuffer) - var diags = ParseDiagnosticsGenerator.diagnostics(for: tree) - if foldSequences { - diags += foldAllSequences(tree).1 - } - + try withParsedSourceFile { (tree, diags) in var group = GroupedDiagnostics() group.addSourceFile(tree: tree, displayName: sourceFileName, diagnostics: diags) let annotatedSource = DiagnosticsFormatter.annotateSources( diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift index fa35da89b62..10f2c1ac623 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift @@ -27,17 +27,8 @@ struct PrintTree: ParsableCommand, ParseCommand { var includeTrivia: Bool = false func run() throws { - try sourceFileContents.withUnsafeBufferPointer { sourceBuffer in - let tree = Parser.parse(source: sourceBuffer) - - let resultTree: Syntax - if foldSequences { - resultTree = foldAllSequences(tree).0 - } else { - resultTree = Syntax(tree) - } - - print(resultTree.debugDescription(includeTrivia: includeTrivia)) + try withParsedSourceFile(wantDiagnostics: false) { (tree, _) in + print(tree.debugDescription(includeTrivia: includeTrivia)) } } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift b/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift index db9a5f4a7cd..b234dc26cb0 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift @@ -11,6 +11,12 @@ //===----------------------------------------------------------------------===// import ArgumentParser +import Foundation +import SwiftDiagnostics +import SwiftIfConfig +import SwiftParser +import SwiftParserDiagnostics +import SwiftSyntax struct ParseArguments: ParsableArguments { @Argument(help: "The source file that should be parsed; if omitted, use stdin") @@ -21,6 +27,9 @@ struct ParseArguments: ParsableArguments { @Flag(name: .long, help: "Perform sequence folding with the standard operators") var foldSequences: Bool = false + + @Option(help: "Apply the given static build configuration to the source text before further processing") + var buildConfiguration: String? } /// A command that has arguments to parse source code @@ -50,4 +59,52 @@ extension ParseCommand { /// Whether sequence folding using standard operators should be performed var foldSequences: Bool { arguments.foldSequences } + + /// Parse the source file, applying any additional configuration options + /// such as sequence folding, and provide it to the given closure. + func withParsedSourceFile( + wantDiagnostics: Bool = true, + body: (SourceFileSyntax, [Diagnostic]) throws -> R + ) throws -> R { + return try sourceFileContents.withUnsafeBufferPointer { sourceBuffer in + // Parse the sources + var tree = Parser.parse(source: sourceBuffer) + + // If we want diagnostics, gather them from the parser. + var diags: [Diagnostic] = [] + if wantDiagnostics { + diags += ParseDiagnosticsGenerator.diagnostics(for: tree) + } + + // If we are supposed to fold sequences, do it now. + if foldSequences { + let (folded, foldDiags) = foldAllSequences(tree) + + tree = folded.cast(SourceFileSyntax.self) + if wantDiagnostics { + diags += foldDiags + } + } + + // If we are supposed to apply a build configuration, do it now. + if let buildConfiguration = arguments.buildConfiguration { + // Load the build configuration. + let buildConfigurationText = try Data(contentsOf: URL(fileURLWithPath: buildConfiguration)) + let staticBuildConfiguration = try JSONDecoder().decode( + StaticBuildConfiguration.self, + from: buildConfigurationText + ) + + // Apply the build configuration. + let (configured, configuredDiags) = tree.removingInactive(in: staticBuildConfiguration) + + tree = configured.cast(SourceFileSyntax.self) + if wantDiagnostics { + diags += configuredDiags + } + } + + return try body(tree, diags) + } + } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/swift-parser-cli.swift b/SwiftParserCLI/Sources/swift-parser-cli/swift-parser-cli.swift index 21f02d924c0..e1266525973 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/swift-parser-cli.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/swift-parser-cli.swift @@ -32,6 +32,7 @@ class SwiftParserCli: ParsableCommand { subcommands: [ BasicFormat.self, PerformanceTest.self, + Print.self, PrintDiags.self, PrintTree.self, Reduce.self, From 315794d0407d25c37696ad6c1f6e3edac99f0c1e Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 26 Sep 2025 12:21:14 -0700 Subject: [PATCH 03/11] Drop language- and compiler-version defaults from StaticBuildConfiguration --- .../StaticBuildConfiguration.swift | 8 ++--- .../SwiftIfConfigTest/ActiveRegionTests.swift | 4 ++- Tests/SwiftIfConfigTest/EvaluateTests.swift | 36 +++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift index 2c799e08c2e..b5e41679b0e 100644 --- a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift +++ b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift @@ -30,8 +30,8 @@ public struct StaticBuildConfiguration: Codable { targetPointerBitWidth: Int = 64, targetAtomicBitWidths: [Int] = [], endianness: Endianness = .little, - languageVersion: VersionTuple = VersionTuple(6), - compilerVersion: VersionTuple = VersionTuple(6, 2) + languageVersion: VersionTuple, + compilerVersion: VersionTuple ) { self.customConditions = customConditions self.features = features @@ -159,7 +159,7 @@ public struct StaticBuildConfiguration: Codable { /// #if swift(>=5.5) /// // Hooray, we can use tasks! /// ``` - public var languageVersion: VersionTuple = VersionTuple(6) + public var languageVersion: VersionTuple /// The version of the compiler (e.g., 5.9). /// @@ -171,7 +171,7 @@ public struct StaticBuildConfiguration: Codable { /// #if compiler(>=5.7) /// // Hoorway, we can implicitly open existentials! /// #endif - public var compilerVersion: VersionTuple = VersionTuple(6, 2) + public var compilerVersion: VersionTuple } extension StaticBuildConfiguration: BuildConfiguration { diff --git a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift index b31869357ba..0f8f5aabbf9 100644 --- a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift +++ b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift @@ -23,7 +23,9 @@ public class ActiveRegionTests: XCTestCase { let linuxBuildConfig = StaticBuildConfiguration( customConditions: ["DEBUG", "ASSERTS"], features: ["ParameterPacks"], - attributes: ["available"] + attributes: ["available"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) ) func testActiveRegions() throws { try assertActiveCode( diff --git a/Tests/SwiftIfConfigTest/EvaluateTests.swift b/Tests/SwiftIfConfigTest/EvaluateTests.swift index cf57e052c4f..35fc7565414 100644 --- a/Tests/SwiftIfConfigTest/EvaluateTests.swift +++ b/Tests/SwiftIfConfigTest/EvaluateTests.swift @@ -21,7 +21,11 @@ import _SwiftSyntaxTestSupport public class EvaluateTests: XCTestCase { func testLiterals() throws { - let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) + let buildConfig = StaticBuildConfiguration( + customConditions: ["DEBUG", "ASSERTS"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ) assertIfConfig("true", .active, configuration: buildConfig) assertIfConfig("false", .inactive, configuration: buildConfig) @@ -71,7 +75,11 @@ public class EvaluateTests: XCTestCase { } func testCustomConfigs() throws { - let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS", "try"]) + let buildConfig = StaticBuildConfiguration( + customConditions: ["DEBUG", "ASSERTS", "try"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ) assertIfConfig("DEBUG", .active, configuration: buildConfig) assertIfConfig("NODEBUG", .inactive, configuration: buildConfig) @@ -136,7 +144,11 @@ public class EvaluateTests: XCTestCase { } func testBadExpressions() throws { - let buildConfig = StaticBuildConfiguration(customConditions: ["DEBUG", "ASSERTS"]) + let buildConfig = StaticBuildConfiguration( + customConditions: ["DEBUG", "ASSERTS"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ) assertIfConfig( "3.14159", @@ -178,14 +190,22 @@ public class EvaluateTests: XCTestCase { } func testFeatures() throws { - let buildConfig = StaticBuildConfiguration(features: ["ParameterPacks"]) + let buildConfig = StaticBuildConfiguration( + features: ["ParameterPacks"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ) assertIfConfig("hasFeature(ParameterPacks)", .active, configuration: buildConfig) assertIfConfig("hasFeature(HigherKindedGenerics)", .inactive, configuration: buildConfig) } func testAttributes() throws { - let buildConfig = StaticBuildConfiguration(attributes: ["available"]) + let buildConfig = StaticBuildConfiguration( + attributes: ["available"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ) assertIfConfig("hasAttribute(available)", .active, configuration: buildConfig) assertIfConfig("hasAttribute(unsafeUnavailable)", .inactive, configuration: buildConfig) @@ -533,7 +553,11 @@ public class EvaluateTests: XCTestCase { assertIfConfig( "defined(FOO)", .active, - configuration: StaticBuildConfiguration(customConditions: ["FOO"]), + configuration: StaticBuildConfiguration( + customConditions: ["FOO"], + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6, 2) + ), diagnostics: [ DiagnosticSpec( message: message, From 02adadee173bb70e29117569063a46bc2512f17c Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 26 Sep 2025 15:11:12 -0700 Subject: [PATCH 04/11] Extend MacroExpansionContext with a build-configuration property The (optional) build configuration provides information about the context in which the macro-generated code will be compiled. It allows one to inspect various aspects of the configuration, including available attributes and features, the target architecture/OS/environment/etc., and any custom settings passed via `-D` on the command line. --- Package.swift | 8 ++++- .../CMakeLists.txt | 1 + .../CompilerPluginMessageHandler.swift | 32 +++++++++++++++++-- .../Macros.swift | 10 ++++-- .../PluginMacroExpansionContext.swift | 18 ++++++++++- .../PluginMessages.swift | 6 ++-- .../BasicMacroExpansionContext.swift | 9 +++++- Sources/SwiftSyntaxMacros/CMakeLists.txt | 1 + .../MacroExpansionContext.swift | 14 ++++++++ 9 files changed, 90 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 9801d35218e..51ea5d1d92e 100644 --- a/Package.swift +++ b/Package.swift @@ -275,7 +275,13 @@ let package = Package( .target( name: "SwiftSyntaxMacros", - dependencies: ["SwiftDiagnostics", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"], + dependencies: [ + "SwiftDiagnostics", + "SwiftIfConfig", + "SwiftParser", + "SwiftSyntax", + "SwiftSyntaxBuilder", + ], exclude: ["CMakeLists.txt"] ), diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index e0323a7abfc..3c6a9f416ab 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -25,6 +25,7 @@ target_link_swift_syntax_libraries(SwiftCompilerPluginMessageHandling PUBLIC SwiftSyntax SwiftBasicFormat SwiftDiagnostics + SwiftIfConfig SwiftParser SwiftSyntaxMacros SwiftSyntaxMacroExpansion diff --git a/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift b/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift index 14f30c50d86..4065a814b71 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift @@ -21,8 +21,10 @@ import _SwiftSyntaxCShims #endif #if compiler(>=6) +internal import SwiftIfConfig public import SwiftSyntaxMacros #else +import SwiftIfConfig import SwiftSyntaxMacros #endif @@ -189,12 +191,25 @@ public class PluginProviderMessageHandler: PluginMessa let macroRole, let discriminator, let expandingSyntax, - let lexicalContext + let lexicalContext, + let staticBuildConfigurationString ): + // Decode the static build configuration. + let staticBuildConfiguration: StaticBuildConfiguration? + if let staticBuildConfigurationString { + var mutableConfigurationString = staticBuildConfigurationString + staticBuildConfiguration = mutableConfigurationString.withUTF8 { + try? JSON.decode(StaticBuildConfiguration.self, from: $0) + } + } else { + staticBuildConfiguration = nil + } + return expandFreestandingMacro( macro: macro, macroRole: macroRole, discriminator: discriminator, + staticBuildConfiguration: staticBuildConfiguration, expandingSyntax: expandingSyntax, lexicalContext: lexicalContext ) @@ -208,12 +223,25 @@ public class PluginProviderMessageHandler: PluginMessa let parentDeclSyntax, let extendedTypeSyntax, let conformanceListSyntax, - let lexicalContext + let lexicalContext, + let staticBuildConfigurationString ): + // Decode the static build configuration. + let staticBuildConfiguration: StaticBuildConfiguration? + if let staticBuildConfigurationString { + var mutableConfigurationString = staticBuildConfigurationString + staticBuildConfiguration = mutableConfigurationString.withUTF8 { + try? JSON.decode(StaticBuildConfiguration.self, from: $0) + } + } else { + staticBuildConfiguration = nil + } + return expandAttachedMacro( macro: macro, macroRole: macroRole, discriminator: discriminator, + staticBuildConfiguration: staticBuildConfiguration, attributeSyntax: attributeSyntax, declSyntax: declSyntax, parentDeclSyntax: parentDeclSyntax, diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index 41f630008ec..c4995e86aa1 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -13,6 +13,7 @@ #if compiler(>=6) internal import SwiftBasicFormat internal import SwiftDiagnostics +internal import SwiftIfConfig internal import SwiftOperators internal import SwiftSyntax @_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) internal import SwiftSyntaxMacroExpansion @@ -20,6 +21,7 @@ internal import SwiftSyntax #else import SwiftBasicFormat import SwiftDiagnostics +import SwiftIfConfig import SwiftOperators import SwiftSyntax @_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacroExpansion @@ -53,6 +55,7 @@ extension PluginProviderMessageHandler { macro: PluginMessage.MacroReference, macroRole pluginMacroRole: PluginMessage.MacroRole?, discriminator: String, + staticBuildConfiguration: StaticBuildConfiguration?, expandingSyntax: PluginMessage.Syntax, lexicalContext: [PluginMessage.Syntax]? ) -> PluginToHostMessage { @@ -67,7 +70,8 @@ extension PluginProviderMessageHandler { operatorTable: .standardOperators, fallbackSyntax: syntax ), - expansionDiscriminator: discriminator + expansionDiscriminator: discriminator, + staticBuildConfiguration: staticBuildConfiguration ) let expandedSource: String? @@ -113,6 +117,7 @@ extension PluginProviderMessageHandler { macro: PluginMessage.MacroReference, macroRole: PluginMessage.MacroRole, discriminator: String, + staticBuildConfiguration: StaticBuildConfiguration?, attributeSyntax: PluginMessage.Syntax, declSyntax: PluginMessage.Syntax, parentDeclSyntax: PluginMessage.Syntax?, @@ -143,7 +148,8 @@ extension PluginProviderMessageHandler { operatorTable: .standardOperators, fallbackSyntax: declarationNode ), - expansionDiscriminator: discriminator + expansionDiscriminator: discriminator, + staticBuildConfiguration: staticBuildConfiguration ) // TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches. diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift index 85b5e5d54ec..348f52705c2 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift @@ -12,12 +12,14 @@ #if compiler(>=6) internal import SwiftDiagnostics +internal import SwiftIfConfig internal import SwiftOperators internal import SwiftParser internal import SwiftSyntax internal import SwiftSyntaxMacros #else import SwiftDiagnostics +import SwiftIfConfig import SwiftOperators import SwiftParser import SwiftSyntax @@ -262,6 +264,10 @@ class PluginMacroExpansionContext { /// to produce unique names. private var expansionDiscriminator: String + /// The static build configuration, if any, that will be used for the + /// macro-expanded code. + private var staticBuildConfiguration: StaticBuildConfiguration? + /// Counter for each of the uniqued names. /// /// Used in conjunction with `expansionDiscriminator`. @@ -271,10 +277,16 @@ class PluginMacroExpansionContext { /// macro. internal private(set) var diagnostics: [Diagnostic] = [] - init(sourceManager: SourceManager, lexicalContext: [Syntax], expansionDiscriminator: String = "") { + init( + sourceManager: SourceManager, + lexicalContext: [Syntax], + expansionDiscriminator: String = "", + staticBuildConfiguration: StaticBuildConfiguration? + ) { self.sourceManager = sourceManager self.lexicalContext = lexicalContext self.expansionDiscriminator = expansionDiscriminator + self.staticBuildConfiguration = staticBuildConfiguration } } @@ -321,4 +333,8 @@ extension PluginMacroExpansionContext: MacroExpansionContext { } return AbstractSourceLocation(location) } + + public var buildConfiguration: (any BuildConfiguration)? { + staticBuildConfiguration + } } diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift index 4d8b5a3bf72..ab55433521a 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift @@ -25,7 +25,8 @@ public enum HostToPluginMessage: Codable { macroRole: PluginMessage.MacroRole? = nil, discriminator: String, syntax: PluginMessage.Syntax, - lexicalContext: [PluginMessage.Syntax]? = nil + lexicalContext: [PluginMessage.Syntax]? = nil, + staticBuildConfiguration: String? = nil ) /// Expand an '@attached' macro. @@ -38,7 +39,8 @@ public enum HostToPluginMessage: Codable { parentDeclSyntax: PluginMessage.Syntax?, extendedTypeSyntax: PluginMessage.Syntax?, conformanceListSyntax: PluginMessage.Syntax?, - lexicalContext: [PluginMessage.Syntax]? = nil + lexicalContext: [PluginMessage.Syntax]? = nil, + staticBuildConfiguration: String? = nil ) /// Optionally implemented message to load a dynamic link library. diff --git a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift index 013531a1507..56efff677ce 100644 --- a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift @@ -12,11 +12,13 @@ #if compiler(>=6) public import SwiftDiagnostics +public import SwiftIfConfig internal import SwiftOperators public import SwiftSyntax public import SwiftSyntaxMacros #else import SwiftDiagnostics +import SwiftIfConfig import SwiftOperators import SwiftSyntax import SwiftSyntaxMacros @@ -62,6 +64,9 @@ public class BasicMacroExpansionContext { /// /// Used in conjunction with `expansionDiscriminator`. var uniqueNames: [String: Int] = [:] + + /// The build configuration that will be applied to the expanded code. + var buildConfiguration: (any BuildConfiguration)? = nil } /// State shared by different instances of the macro expansion context, @@ -82,12 +87,14 @@ public class BasicMacroExpansionContext { public init( lexicalContext: [Syntax] = [], expansionDiscriminator: String = "__macro_local_", - sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] + sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:], + buildConfiguration: (any BuildConfiguration)? = nil ) { self.sharedState = SharedState() self.lexicalContext = lexicalContext self.expansionDiscriminator = expansionDiscriminator self.sharedState.sourceFiles = sourceFiles + self.sharedState.buildConfiguration = buildConfiguration } /// Create a new macro evaluation context that shares most of its global diff --git a/Sources/SwiftSyntaxMacros/CMakeLists.txt b/Sources/SwiftSyntaxMacros/CMakeLists.txt index 1d046f2c35a..b89a2b13826 100644 --- a/Sources/SwiftSyntaxMacros/CMakeLists.txt +++ b/Sources/SwiftSyntaxMacros/CMakeLists.txt @@ -29,5 +29,6 @@ add_swift_syntax_library(SwiftSyntaxMacros ) target_link_swift_syntax_libraries(SwiftSyntaxMacros PUBLIC + SwiftIfConfig SwiftSyntaxBuilder ) diff --git a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift index 608a5760743..5b52528cba6 100644 --- a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift @@ -12,10 +12,12 @@ #if compiler(>=6) public import SwiftDiagnostics +public import SwiftIfConfig public import SwiftSyntax import SwiftSyntaxBuilder #else import SwiftDiagnostics +import SwiftIfConfig import SwiftSyntax import SwiftSyntaxBuilder #endif @@ -70,6 +72,13 @@ public protocol MacroExpansionContext: AnyObject { /// This array can be empty if there is no context, for example when a /// freestanding macro is used at file scope. var lexicalContext: [Syntax] { get } + + /// Returns the build configuration that will be used with the generated + /// source code. Macro implementations can use this information to determine + /// the context into which they are generating code. + /// + /// When the build configuration is not known, this returns nil. + var buildConfiguration: (any BuildConfiguration)? { get } } extension MacroExpansionContext { @@ -187,3 +196,8 @@ public enum SourceLocationFilePathMode { /// e.g., `/home/taylor/alison.swift`. case filePath } + +extension MacroExpansionContext { + /// Default implementation that produces no build configuration. + public var buildConfiguration: (any BuildConfiguration)? { nil } +} From 66662029d742a545cf3896b8c2d233ba7c2d2d41 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 27 Sep 2025 07:38:57 -0700 Subject: [PATCH 05/11] Remove importedModules from StaticBuildConfiguration The set of imported modules in StaticBuildConfiguration cannot really be complete, because canImport can trigger module lookups that only the compiler can do. Remove it from the configuration, and have canImport always throw an error to indicate the issue. --- .../StaticBuildConfiguration.swift | 89 +++---------------- Tests/SwiftIfConfigTest/EvaluateTests.swift | 9 ++ 2 files changed, 22 insertions(+), 76 deletions(-) diff --git a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift index b5e41679b0e..9a08264725e 100644 --- a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift +++ b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift @@ -15,12 +15,14 @@ import SwiftSyntax /// A statically-determined build configuration that can be used with any /// API that requires a build configuration. Static build configurations can /// be (de-)serialized via Codable. +/// +/// Static build configurations can not be used for canImport checks, because +/// such checks require deeper integration with the compiler itself. public struct StaticBuildConfiguration: Codable { public init( customConditions: Set = [], features: Set = [], attributes: Set = [], - importedModules: [String: [VersionedImportModule]] = [:], targetOSNames: Set = [], targetArchitectures: Set = [], targetEnvironments: Set = [], @@ -36,7 +38,6 @@ public struct StaticBuildConfiguration: Codable { self.customConditions = customConditions self.features = features self.attributes = attributes - self.importedModules = importedModules self.targetOSNames = targetOSNames self.targetArchitectures = targetArchitectures self.targetEnvironments = targetEnvironments @@ -88,11 +89,6 @@ public struct StaticBuildConfiguration: Codable { /// ``` public var attributes: Set = [] - /// The set of modules that can be imported, and their version and underlying - /// versions (if known). These are organized by top-level module name, - /// with paths (to submodules) handled internally. - public var importedModules: [String: [VersionedImportModule]] = [:] - /// The active target OS names, e.g., "Windows", "iOS". public var targetOSNames: Set = [] @@ -240,59 +236,10 @@ extension StaticBuildConfiguration: BuildConfiguration { /// Determine whether a module with the given import path can be imported, /// with additional version information. /// - /// The availability of a module for import can be checked with `canImport`, - /// e.g., - /// - /// ```swift - /// #if canImport(UIKit) - /// // ... - /// #endif - /// ``` - /// - /// There is an experimental syntax for providing required module version - /// information, which will translate into the `version` argument. - /// - /// - Parameters: - /// - importPath: A nonempty sequence of (token, identifier) pairs - /// describing the imported module, which was written in source as a - /// dotted sequence, e.g., `UIKit.UIViewController` will be passed in as - /// the import path array `[(token, "UIKit"), (token, "UIViewController")]`. - /// - version: The version restriction on the imported module. For the - /// normal `canImport()` syntax, this will always be - /// `CanImportVersion.unversioned`. - /// - Returns: Whether the module can be imported. - public func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) -> Bool { - // If we don't have any record of the top-level module, we cannot import it. - guard let topLevelModuleName = importPath.first?.1, - let versionedImports = importedModules[topLevelModuleName] - else { - return false - } - - // Match on submodule path. - let submodulePath = Array(importPath.lazy.map(\.1).dropFirst()) - guard let matchingImport = versionedImports.first(where: { $0.submodulePath == submodulePath }) else { - return false - } - - switch version { - case .unversioned: - return true - - case .version(let expectedVersion): - guard let actualVersion = matchingImport.version else { - return false - } - - return actualVersion >= expectedVersion - - case .underlyingVersion(let expectedVersion): - guard let actualVersion = matchingImport.underlyingVersion else { - return false - } - - return actualVersion >= expectedVersion - } + /// This implementation always throws an error, because static build + /// configurations cannot evaluate canImport checks. + public func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool { + throw StaticBuildConfiguration.Error.canImportUnavailable } /// Determine whether the given name is the active target OS (e.g., Linux, iOS). @@ -412,20 +359,10 @@ extension StaticBuildConfiguration: BuildConfiguration { } } -/// Information about a potentially-versioned import of a given module. -/// -/// Each instance of this struct is associated with a top-level module of some -/// form. When the submodule path is empty, it refers to the top-level module -/// itself. -public struct VersionedImportModule: Codable { - /// The submodule path (which may be empty) from the top-level module to - /// this specific import. - public var submodulePath: [String] = [] - - /// The version that was imported, if known. - public var version: VersionTuple? = nil - - /// The version of the underlying Clang module, if there is one and it is - /// known. - public var underlyingVersion: VersionTuple? = nil +extension StaticBuildConfiguration { + enum Error: Swift.Error { + /// Indicates when the static build configuration was asked to evaluate + /// canImport, which it cannot do correctly. + case canImportUnavailable + } } diff --git a/Tests/SwiftIfConfigTest/EvaluateTests.swift b/Tests/SwiftIfConfigTest/EvaluateTests.swift index 35fc7565414..849806dd218 100644 --- a/Tests/SwiftIfConfigTest/EvaluateTests.swift +++ b/Tests/SwiftIfConfigTest/EvaluateTests.swift @@ -612,6 +612,15 @@ public class EvaluateTests: XCTestCase { ] ) } + + func testStaticBuildConfigCanImport() throws { + let config = StaticBuildConfiguration( + languageVersion: VersionTuple(6), + compilerVersion: VersionTuple(6) + ) + + XCTAssertThrowsError(try config.canImport(importPath: [], version: .unversioned)) + } } /// Assert the results of evaluating the condition within an `#if` against the From a8472a94d1a3d7ea9a082a61354c0bd40d06f48a Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 27 Sep 2025 08:12:30 -0700 Subject: [PATCH 06/11] [Macro plugin] Thread language mode and experimental features into the parser The language mode and experimental features are available in the new static build configuration. When it's thread, thread them down into the parser used to create the syntax tree passed along macro implementations. This is the swift-syntax part of rdar://129687671. --- Package.swift | 2 +- .../Macros.swift | 56 +++++++++++++++---- .../PluginMacroExpansionContext.swift | 54 ++++++++++++++---- Sources/SwiftIfConfig/CMakeLists.txt | 3 +- .../StaticBuildConfiguration.swift | 34 +++++++++++ Sources/SwiftParser/Parser.swift | 2 +- .../generated/ExperimentalFeatures.swift | 2 +- 7 files changed, 127 insertions(+), 26 deletions(-) diff --git a/Package.swift b/Package.swift index 51ea5d1d92e..55c4f09fdbb 100644 --- a/Package.swift +++ b/Package.swift @@ -166,7 +166,7 @@ let package = Package( .target( name: "SwiftIfConfig", - dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftDiagnostics", "SwiftOperators"], + dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftDiagnostics", "SwiftOperators", "SwiftParser"], exclude: ["CMakeLists.txt"] ), diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index c4995e86aa1..b9cc98dcb5f 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -13,16 +13,18 @@ #if compiler(>=6) internal import SwiftBasicFormat internal import SwiftDiagnostics -internal import SwiftIfConfig +@_spi(ExperimentalLanguageFeatures) internal import SwiftIfConfig internal import SwiftOperators +@_spi(ExperimentalLanguageFeatures) internal import SwiftParser internal import SwiftSyntax @_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) internal import SwiftSyntaxMacroExpansion @_spi(ExperimentalLanguageFeature) internal import SwiftSyntaxMacros #else import SwiftBasicFormat import SwiftDiagnostics -import SwiftIfConfig +@_spi(ExperimentalLanguageFeatures) import SwiftIfConfig import SwiftOperators +@_spi(ExperimentalLanguageFeatures) import SwiftParser import SwiftSyntax @_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacroExpansion @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros @@ -38,6 +40,8 @@ extension PluginProviderMessageHandler { private static func resolveLexicalContext( _ lexicalContext: [PluginMessage.Syntax]?, sourceManager: SourceManager, + swiftVersion: Parser.SwiftVersion?, + experimentalFeatures: Parser.ExperimentalFeatures?, operatorTable: OperatorTable, fallbackSyntax: some SyntaxProtocol ) -> [Syntax] { @@ -47,7 +51,14 @@ extension PluginProviderMessageHandler { return fallbackSyntax.allMacroLexicalContexts() } - return lexicalContext.map { sourceManager.add($0, foldingWith: operatorTable) } + return lexicalContext.map { + sourceManager.add( + $0, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, + foldingWith: operatorTable + ) + } } /// Expand `@freestainding(XXX)` macros. @@ -60,13 +71,22 @@ extension PluginProviderMessageHandler { lexicalContext: [PluginMessage.Syntax]? ) -> PluginToHostMessage { let sourceManager = SourceManager(syntaxRegistry: syntaxRegistry) - let syntax = sourceManager.add(expandingSyntax, foldingWith: .standardOperators) + let swiftVersion = staticBuildConfiguration?.parserSwiftVersion + let experimentalFeatures = staticBuildConfiguration?.experimentalFeatures + let syntax = sourceManager.add( + expandingSyntax, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, + foldingWith: .standardOperators + ) let context = PluginMacroExpansionContext( sourceManager: sourceManager, lexicalContext: Self.resolveLexicalContext( lexicalContext, sourceManager: sourceManager, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, operatorTable: .standardOperators, fallbackSyntax: syntax ), @@ -126,17 +146,27 @@ extension PluginProviderMessageHandler { lexicalContext: [PluginMessage.Syntax]? ) -> PluginToHostMessage { let sourceManager = SourceManager(syntaxRegistry: syntaxRegistry) - let attributeNode = sourceManager.add( - attributeSyntax, - foldingWith: .standardOperators - ).cast(AttributeSyntax.self) - let declarationNode = sourceManager.add(declSyntax) - let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) } + let swiftVersion = staticBuildConfiguration?.parserSwiftVersion + let experimentalFeatures = staticBuildConfiguration?.experimentalFeatures + + func addToSourceManager(_ syntax: PluginMessage.Syntax) -> Syntax { + sourceManager.add( + attributeSyntax, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, + foldingWith: .standardOperators + ) + } + + let attributeNode = addToSourceManager(attributeSyntax) + .cast(AttributeSyntax.self) + let declarationNode = addToSourceManager(declSyntax) + let parentDeclNode = parentDeclSyntax.map { addToSourceManager($0).cast(DeclSyntax.self) } let extendedType = extendedTypeSyntax.map { - sourceManager.add($0).cast(TypeSyntax.self) + addToSourceManager($0).cast(TypeSyntax.self) } let conformanceList = conformanceListSyntax.map { - let placeholderStruct = sourceManager.add($0).cast(StructDeclSyntax.self) + let placeholderStruct = addToSourceManager($0).cast(StructDeclSyntax.self) return placeholderStruct.inheritanceClause!.inheritedTypes } @@ -145,6 +175,8 @@ extension PluginProviderMessageHandler { lexicalContext: Self.resolveLexicalContext( lexicalContext, sourceManager: sourceManager, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, operatorTable: .standardOperators, fallbackSyntax: declarationNode ), diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift index 348f52705c2..2a53c520f2a 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift @@ -14,14 +14,14 @@ internal import SwiftDiagnostics internal import SwiftIfConfig internal import SwiftOperators -internal import SwiftParser +@_spi(ExperimentalLanguageFeatures) internal import SwiftParser internal import SwiftSyntax internal import SwiftSyntaxMacros #else import SwiftDiagnostics import SwiftIfConfig import SwiftOperators -import SwiftParser +@_spi(ExperimentalLanguageFeatures) import SwiftParser import SwiftSyntax import SwiftSyntaxMacros #endif @@ -31,6 +31,8 @@ class ParsedSyntaxRegistry { struct Key: Hashable { let source: String let kind: PluginMessage.Syntax.Kind + let swiftVersion: Parser.SwiftVersion + let experimentalFeatures: Parser.ExperimentalFeatures } private var storage: LRUCache @@ -39,8 +41,17 @@ class ParsedSyntaxRegistry { self.storage = LRUCache(capacity: cacheCapacity) } - private func parse(source: String, kind: PluginMessage.Syntax.Kind) -> Syntax { - var parser = Parser(source) + private func parse( + source: String, + kind: PluginMessage.Syntax.Kind, + swiftVersion: Parser.SwiftVersion, + experimentalFeatures: Parser.ExperimentalFeatures + ) -> Syntax { + var parser = Parser( + source, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures + ) switch kind { case .declaration: return Syntax(DeclSyntax.parse(from: &parser)) @@ -57,13 +68,30 @@ class ParsedSyntaxRegistry { } } - func get(source: String, kind: PluginMessage.Syntax.Kind) -> Syntax { - let key = Key(source: source, kind: kind) + func get( + source: String, + kind: PluginMessage.Syntax.Kind, + swiftVersion: Parser.SwiftVersion?, + experimentalFeatures: Parser.ExperimentalFeatures? + ) -> Syntax { + let swiftVersion = swiftVersion ?? Parser.defaultSwiftVersion + let experimentalFeatures = experimentalFeatures ?? Parser.ExperimentalFeatures() + let key = Key( + source: source, + kind: kind, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures + ) if let cached = storage[key] { return cached } - let node = parse(source: source, kind: kind) + let node = parse( + source: source, + kind: kind, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures + ) storage[key] = node return node } @@ -126,10 +154,16 @@ class SourceManager { /// are cached in the source manager to provide `location(of:)` et al. func add( _ syntaxInfo: PluginMessage.Syntax, - foldingWith operatorTable: OperatorTable? = nil + swiftVersion: Parser.SwiftVersion?, + experimentalFeatures: Parser.ExperimentalFeatures?, + foldingWith operatorTable: OperatorTable? ) -> Syntax { - - var node = syntaxRegistry.get(source: syntaxInfo.source, kind: syntaxInfo.kind) + var node = syntaxRegistry.get( + source: syntaxInfo.source, + kind: syntaxInfo.kind, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures + ) if let operatorTable { node = operatorTable.foldAll(node, errorHandler: { _ in /*ignore*/ }) } diff --git a/Sources/SwiftIfConfig/CMakeLists.txt b/Sources/SwiftIfConfig/CMakeLists.txt index c9534ce9ec8..90601a0ae59 100644 --- a/Sources/SwiftIfConfig/CMakeLists.txt +++ b/Sources/SwiftIfConfig/CMakeLists.txt @@ -28,4 +28,5 @@ target_link_swift_syntax_libraries(SwiftIfConfig PUBLIC SwiftSyntax SwiftSyntaxBuilder SwiftDiagnostics - SwiftOperators) + SwiftOperators + SwiftParser) diff --git a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift index 9a08264725e..743bbafb9fb 100644 --- a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift +++ b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift @@ -10,7 +10,13 @@ // //===----------------------------------------------------------------------===// +#if compiler(>=6) +@_spi(ExperimentalLanguageFeatures) public import SwiftParser +public import SwiftSyntax +#else +@_spi(ExperimentalLanguageFeatures) import SwiftParser import SwiftSyntax +#endif /// A statically-determined build configuration that can be used with any /// API that requires a build configuration. Static build configurations can @@ -366,3 +372,31 @@ extension StaticBuildConfiguration { case canImportUnavailable } } + +extension StaticBuildConfiguration { + /// The Swift version that can be set for the parser. + public var parserSwiftVersion: Parser.SwiftVersion { + if languageVersion < VersionTuple(5) { + return .v4 + } else if languageVersion < VersionTuple(6) { + return .v5 + } else if languageVersion < VersionTuple(7) { + return .v6 + } else { + return Parser.defaultSwiftVersion + } + } + + /// Determine the set of experimental features that are enabled by this + /// static build configuration. + @_spi(ExperimentalLanguageFeatures) + public var experimentalFeatures: Parser.ExperimentalFeatures { + var result: Parser.ExperimentalFeatures = [] + for feature in features { + if let experimentalFeature = Parser.ExperimentalFeatures(name: feature) { + result.insert(experimentalFeature) + } + } + return result + } +} diff --git a/Sources/SwiftParser/Parser.swift b/Sources/SwiftParser/Parser.swift index d60b37d7533..ef268d21da5 100644 --- a/Sources/SwiftParser/Parser.swift +++ b/Sources/SwiftParser/Parser.swift @@ -137,7 +137,7 @@ public struct Parser { #endif /// The Swift version as which source files should be parsed if no Swift version is explicitly specified in the parser. - static let defaultSwiftVersion: SwiftVersion = .v6 + public static let defaultSwiftVersion: SwiftVersion = .v6 var _emptyRawMultipleTrailingClosureElementListSyntax: RawMultipleTrailingClosureElementListSyntax? diff --git a/Sources/SwiftParser/generated/ExperimentalFeatures.swift b/Sources/SwiftParser/generated/ExperimentalFeatures.swift index 1c12c14a148..fcd6518048b 100644 --- a/Sources/SwiftParser/generated/ExperimentalFeatures.swift +++ b/Sources/SwiftParser/generated/ExperimentalFeatures.swift @@ -15,7 +15,7 @@ extension Parser { @_spi(ExperimentalLanguageFeatures) - public struct ExperimentalFeatures: OptionSet, Sendable { + public struct ExperimentalFeatures: OptionSet, Sendable, Hashable { public let rawValue: UInt public init(rawValue: UInt) { From 523a7b26f7c978f4f82031091726575dae4c193e Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 27 Sep 2025 08:27:34 -0700 Subject: [PATCH 07/11] Properly add the Hashable conformance to ExperimentalFeatures --- .../templates/swiftparser/ExperimentalFeaturesFile.swift | 2 +- Sources/SwiftParser/generated/ExperimentalFeatures.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift index 0879e1a3a41..4ebbaf0dd12 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift @@ -20,7 +20,7 @@ let experimentalFeaturesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) """ extension Parser { @_spi(ExperimentalLanguageFeatures) - public struct ExperimentalFeatures: OptionSet, Sendable { + public struct ExperimentalFeatures: OptionSet, Hashable, Sendable { public let rawValue: UInt public init(rawValue: UInt) { self.rawValue = rawValue diff --git a/Sources/SwiftParser/generated/ExperimentalFeatures.swift b/Sources/SwiftParser/generated/ExperimentalFeatures.swift index fcd6518048b..3051769b6f7 100644 --- a/Sources/SwiftParser/generated/ExperimentalFeatures.swift +++ b/Sources/SwiftParser/generated/ExperimentalFeatures.swift @@ -15,7 +15,7 @@ extension Parser { @_spi(ExperimentalLanguageFeatures) - public struct ExperimentalFeatures: OptionSet, Sendable, Hashable { + public struct ExperimentalFeatures: OptionSet, Hashable, Sendable { public let rawValue: UInt public init(rawValue: UInt) { From c82775b882e489b25481288e2ccbb2c2c300e305 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 29 Sep 2025 10:47:25 -0700 Subject: [PATCH 08/11] Address code review feedback --- .../StaticBuildConfiguration.swift | 84 ++++++++++++++++--- .../MacroExpansionContext.swift | 4 +- .../swift-parser-cli/Commands/Print.swift | 8 +- .../Commands/PrintDiags.swift | 21 +++-- .../swift-parser-cli/Commands/PrintTree.swift | 5 +- .../swift-parser-cli/ParseCommand.swift | 11 ++- 6 files changed, 94 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift index 743bbafb9fb..1af7cffda07 100644 --- a/Sources/SwiftIfConfig/StaticBuildConfiguration.swift +++ b/Sources/SwiftIfConfig/StaticBuildConfiguration.swift @@ -29,7 +29,7 @@ public struct StaticBuildConfiguration: Codable { customConditions: Set = [], features: Set = [], attributes: Set = [], - targetOSNames: Set = [], + targetOSs: Set = [], targetArchitectures: Set = [], targetEnvironments: Set = [], targetRuntimes: Set = [], @@ -44,7 +44,7 @@ public struct StaticBuildConfiguration: Codable { self.customConditions = customConditions self.features = features self.attributes = attributes - self.targetOSNames = targetOSNames + self.targetOSs = targetOSs self.targetArchitectures = targetArchitectures self.targetEnvironments = targetEnvironments self.targetRuntimes = targetRuntimes @@ -53,7 +53,7 @@ public struct StaticBuildConfiguration: Codable { self.targetPointerBitWidth = targetPointerBitWidth self.targetAtomicBitWidths = targetAtomicBitWidths self.endianness = endianness - self.languageVersion = languageVersion + self.languageMode = languageVersion self.compilerVersion = compilerVersion } @@ -95,22 +95,76 @@ public struct StaticBuildConfiguration: Codable { /// ``` public var attributes: Set = [] - /// The active target OS names, e.g., "Windows", "iOS". - public var targetOSNames: Set = [] + /// The active target OS, e.g., "Windows", "iOS". + /// + /// The target operating system can be queried with `os()`, e.g., + /// + /// ```swift + /// #if os(Linux) + /// // Linux-specific implementation + /// #endif + /// ``` + public var targetOSs: Set = [] /// The active target architectures, e.g., "x64_64". + /// + /// The target processor architecture can be queried with `arch()`, e.g., + /// + /// ```swift + /// #if arch(x86_64) + /// // 64-bit x86 Intel-specific code + /// #endif + /// ``` public var targetArchitectures: Set = [] /// The active target environments, e.g., "simulator". + /// + /// The target environment can be queried with `targetEnvironment()`, + /// e.g., + /// + /// ```swift + /// #if targetEnvironment(simulator) + /// // Simulator-specific code + /// #endif + /// ``` public var targetEnvironments: Set = [] /// The active target runtimes, e.g., _ObjC. + /// + /// The target runtime can only be queried by an experimental syntax + /// `_runtime()`, e.g., + /// + /// ```swift + /// #if _runtime(_ObjC) + /// // Code that depends on Swift being built for use with the Objective-C + /// // runtime, e.g., on Apple platforms. + /// #endif + /// ``` public var targetRuntimes: Set = [] /// The active target's pointer authentication schemes, e.g., "arm64e". + /// + /// The target pointer authentication scheme describes how pointers are + /// signed, as a security mitigation. This scheme can only be queried by + /// an experimental syntax `_ptrath()`, e.g., + /// + /// ```swift + /// #if _ptrauth(arm64e) + /// // Special logic for arm64e pointer signing + /// #endif + /// ``` public var targetPointerAuthenticationSchemes: Set = [] /// The active target's object file formats, e.g., "COFF" + /// + /// The target object file format can only be queried by an experimental + /// syntax `_objectFileFormat()`, e.g., + /// + /// ```swift + /// #if _objectFileFormat(ELF) + /// // Special logic for ELF object file formats + /// #endif + /// ``` public var targetObjectFileFormats: Set = [] /// The bit width of a data pointer for the target architecture. @@ -151,17 +205,17 @@ public struct StaticBuildConfiguration: Codable { /// ``` public var endianness: Endianness = .little - /// The effective language version, which can be set by the user (e.g., 5.0). + /// The effective language mode, which can be set by the user (e.g., 5.0). /// /// The language version can be queried with the `swift` directive that checks /// how the supported language version compares, as described by /// [SE-0212](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: /// /// ```swift - /// #if swift(>=5.5) + /// #if swift(>=6.0) /// // Hooray, we can use tasks! /// ``` - public var languageVersion: VersionTuple + public var languageMode: VersionTuple /// The version of the compiler (e.g., 5.9). /// @@ -264,7 +318,7 @@ extension StaticBuildConfiguration: BuildConfiguration { /// - Returns: Whether the given operating system name is the target operating /// system, i.e., the operating system for which code is being generated. public func isActiveTargetOS(name: String) -> Bool { - targetOSNames.contains(name) + targetOSs.contains(name) } /// Determine whether the given name is the active target architecture @@ -363,6 +417,12 @@ extension StaticBuildConfiguration: BuildConfiguration { public func isActiveTargetObjectFileFormat(name: String) -> Bool { targetObjectFileFormats.contains(name) } + + /// Equivalent to `languageMode`, but required for conformance to the + /// `BuildConfiguration` protocol. + public var languageVersion: VersionTuple { + languageMode + } } extension StaticBuildConfiguration { @@ -376,11 +436,11 @@ extension StaticBuildConfiguration { extension StaticBuildConfiguration { /// The Swift version that can be set for the parser. public var parserSwiftVersion: Parser.SwiftVersion { - if languageVersion < VersionTuple(5) { + if languageMode < VersionTuple(5) { return .v4 - } else if languageVersion < VersionTuple(6) { + } else if languageMode < VersionTuple(6) { return .v5 - } else if languageVersion < VersionTuple(7) { + } else if languageMode < VersionTuple(7) { return .v6 } else { return Parser.defaultSwiftVersion diff --git a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift index 5b52528cba6..53bee858e3d 100644 --- a/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/MacroExpansionContext.swift @@ -77,7 +77,9 @@ public protocol MacroExpansionContext: AnyObject { /// source code. Macro implementations can use this information to determine /// the context into which they are generating code. /// - /// When the build configuration is not known, this returns nil. + /// When the build configuration is not known, for example because the + /// compiler has not provided this information to the macro implementation, + /// this returns nil. var buildConfiguration: (any BuildConfiguration)? { get } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift index 5c6f4bb42eb..8f47fc99d5c 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/Print.swift @@ -23,12 +23,8 @@ struct Print: ParsableCommand, ParseCommand { @OptionGroup var arguments: ParseArguments - @Flag(name: .long, help: "Include trivia in the output") - var includeTrivia: Bool = false - func run() throws { - try withParsedSourceFile(wantDiagnostics: false) { (tree, _) in - print(tree.description) - } + let (tree, _) = try parsedSourceFile(wantDiagnostics: false) + print(tree.description) } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift index 4bef04255e1..a98c139ccdf 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintDiags.swift @@ -28,19 +28,18 @@ struct PrintDiags: ParsableCommand, ParseCommand { var colorize: Bool = false func run() throws { - try withParsedSourceFile { (tree, diags) in - var group = GroupedDiagnostics() - group.addSourceFile(tree: tree, displayName: sourceFileName, diagnostics: diags) - let annotatedSource = DiagnosticsFormatter.annotateSources( - in: group, - colorize: colorize || TerminalHelper.isConnectedToTerminal - ) + let (tree, diags) = try parsedSourceFile() + var group = GroupedDiagnostics() + group.addSourceFile(tree: tree, displayName: sourceFileName, diagnostics: diags) + let annotatedSource = DiagnosticsFormatter.annotateSources( + in: group, + colorize: colorize || TerminalHelper.isConnectedToTerminal + ) - print(annotatedSource) + print(annotatedSource) - if diags.isEmpty { - print("No diagnostics produced") - } + if diags.isEmpty { + print("No diagnostics produced") } } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift index 10f2c1ac623..52467f8f29d 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/Commands/PrintTree.swift @@ -27,8 +27,7 @@ struct PrintTree: ParsableCommand, ParseCommand { var includeTrivia: Bool = false func run() throws { - try withParsedSourceFile(wantDiagnostics: false) { (tree, _) in - print(tree.debugDescription(includeTrivia: includeTrivia)) - } + let (tree, _) = try parsedSourceFile(wantDiagnostics: false) + print(tree.debugDescription(includeTrivia: includeTrivia)) } } diff --git a/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift b/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift index b234dc26cb0..61210c97108 100644 --- a/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift +++ b/SwiftParserCLI/Sources/swift-parser-cli/ParseCommand.swift @@ -61,11 +61,10 @@ extension ParseCommand { var foldSequences: Bool { arguments.foldSequences } /// Parse the source file, applying any additional configuration options - /// such as sequence folding, and provide it to the given closure. - func withParsedSourceFile( - wantDiagnostics: Bool = true, - body: (SourceFileSyntax, [Diagnostic]) throws -> R - ) throws -> R { + /// such as sequence folding, and return it with diagnostics. + func parsedSourceFile( + wantDiagnostics: Bool = true + ) throws -> (SourceFileSyntax, [Diagnostic]) { return try sourceFileContents.withUnsafeBufferPointer { sourceBuffer in // Parse the sources var tree = Parser.parse(source: sourceBuffer) @@ -104,7 +103,7 @@ extension ParseCommand { } } - return try body(tree, diags) + return (tree, diags) } } } From 9445fc6db7cc5a9734642b0d13f0383dbe1c4510 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 29 Sep 2025 10:51:20 -0700 Subject: [PATCH 09/11] Extend assertMacroExpansion with an optional build configuration --- Package.swift | 1 + .../SwiftSyntaxMacrosGenericTestSupport/Assertions.swift | 6 +++++- Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 55c4f09fdbb..961508b8807 100644 --- a/Package.swift +++ b/Package.swift @@ -328,6 +328,7 @@ let package = Package( "_SwiftSyntaxGenericTestSupport", "SwiftDiagnostics", "SwiftIDEUtils", + "SwiftIfConfig", "SwiftParser", "SwiftSyntaxMacros", "SwiftSyntaxMacroExpansion", diff --git a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift index 64cd8e7aaa8..3450dbaac06 100644 --- a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift @@ -13,6 +13,7 @@ #if compiler(>=6) import SwiftBasicFormat public import SwiftDiagnostics +public import SwiftIfConfig @_spi(FixItApplier) import SwiftIDEUtils import SwiftParser import SwiftParserDiagnostics @@ -24,6 +25,7 @@ private import _SwiftSyntaxGenericTestSupport import SwiftBasicFormat import SwiftDiagnostics @_spi(FixItApplier) import SwiftIDEUtils +import SwiftIfConfig import SwiftParser import SwiftParserDiagnostics import SwiftSyntax @@ -497,6 +499,7 @@ public func assertMacroExpansion( testModuleName: String = "TestModule", testFileName: String = "test.swift", indentationWidth: Trivia = .spaces(4), + buildConfiguration: (any BuildConfiguration)? = nil, failureHandler: (TestFailureSpec) -> Void, fileID: StaticString = #fileID, filePath: StaticString = #filePath, @@ -509,7 +512,8 @@ public func assertMacroExpansion( // Expand all macros in the source. let context = BasicMacroExpansionContext( - sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)] + sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)], + buildConfiguration: buildConfiguration ) func contextGenerator(_ syntax: Syntax) -> BasicMacroExpansionContext { diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 7c131c62878..b8449e72daf 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -11,12 +11,14 @@ //===----------------------------------------------------------------------===// #if compiler(>=6) +public import SwiftIfConfig public import SwiftSyntax public import SwiftSyntaxMacroExpansion public import SwiftSyntaxMacros @_spi(XCTestFailureLocation) public import SwiftSyntaxMacrosGenericTestSupport private import XCTest #else +import SwiftIfConfig import SwiftSyntax import SwiftSyntaxMacroExpansion import SwiftSyntaxMacros @@ -57,6 +59,7 @@ public func assertMacroExpansion( testModuleName: String = "TestModule", testFileName: String = "test.swift", indentationWidth: Trivia = .spaces(4), + buildConfiguration: (any BuildConfiguration)? = nil, file: StaticString = #filePath, line: UInt = #line ) { @@ -71,6 +74,7 @@ public func assertMacroExpansion( testModuleName: testModuleName, testFileName: testFileName, indentationWidth: indentationWidth, + buildConfiguration: buildConfiguration, file: file, line: line ) @@ -104,6 +108,7 @@ public func assertMacroExpansion( testModuleName: String = "TestModule", testFileName: String = "test.swift", indentationWidth: Trivia = .spaces(4), + buildConfiguration: (any BuildConfiguration)? = nil, file: StaticString = #filePath, line: UInt = #line ) { @@ -117,6 +122,7 @@ public func assertMacroExpansion( testModuleName: testModuleName, testFileName: testFileName, indentationWidth: indentationWidth, + buildConfiguration: buildConfiguration, failureHandler: { XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) }, From f4899626b8ba6b5a21f97644ed293b5766a2a615 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 29 Sep 2025 11:02:53 -0700 Subject: [PATCH 10/11] Fix a silly bug in expanding attached macros in the plugin --- Sources/SwiftCompilerPluginMessageHandling/Macros.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index b9cc98dcb5f..f571f41c1a3 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -151,7 +151,7 @@ extension PluginProviderMessageHandler { func addToSourceManager(_ syntax: PluginMessage.Syntax) -> Syntax { sourceManager.add( - attributeSyntax, + syntax, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures, foldingWith: .standardOperators From e4f73a2a638377f6bed0e471e1cf62f3f06d3d06 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 29 Sep 2025 11:15:39 -0700 Subject: [PATCH 11/11] Update see-also reference to new method name --- Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index b8449e72daf..ffcb4d7e6a6 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -46,8 +46,9 @@ public typealias DiagnosticSpec = SwiftSyntaxMacrosGenericTestSupport.Diagnostic /// - testModuleName: The name of the test module to use. /// - testFileName: The name of the test file name to use. /// - indentationWidth: The indentation width used in the expansion. -/// -/// - SeeAlso: ``assertMacroExpansion(_:expandedSource:diagnostics:macroSpecs:applyFixIts:fixedSource:testModuleName:testFileName:indentationWidth:file:line:)`` +/// - buildConfiguration: a build configuration that will be made available +/// to the macro implementation +/// - SeeAlso: ``assertMacroExpansion(_:expandedSource:diagnostics:macroSpecs:applyFixIts:fixedSource:testModuleName:testFileName:indentationWidth:buildConfiguration:file:line:)`` /// to also specify the list of conformances passed to the macro expansion. public func assertMacroExpansion( _ originalSource: String,