From 1d44a43dcba995302c308f6a2a252c6718ae88b8 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 6 Jul 2026 15:52:53 -0700 Subject: [PATCH] [SwiftParser] Make ExperimentalFeatures public and reduce overloads Make `Parser.ExperimentalFeatures` a public type so it can appear in the non-SPI parser API, while keeping the individual feature flags behind `@_spi(ExperimentalLanguageFeatures)` since they remain unstable. Clients that don't enable experimental features only ever use the empty set. Now that the type is public, the overloads that existed only to accept `experimentalFeatures` can be collapsed: `Parser.init`, `parse`, and `withParser` take `experimentalFeatures` as a defaulted argument instead of separate SPI overloads. `withParser` and the arena-based initializer keep `@_spi(RawSyntax)` but no longer carry `@_spi(ExperimentalLanguageFeatures)`. Update the in-tree SPI callers (NameMatcher, the string-interpolation parsable conformance, and the plugin macro expansion context) to import SwiftParser with `@_spi(RawSyntax)` as needed. --- .../ExperimentalFeaturesFile.swift | 7 +- .../PluginMacroExpansionContext.swift | 4 +- Sources/SwiftParser/ParseSourceFile.swift | 26 +--- Sources/SwiftParser/Parser.swift | 137 ++++++------------ .../generated/ExperimentalFeatures.swift | 18 ++- 5 files changed, 73 insertions(+), 119 deletions(-) diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift index 4ebbaf0dd12..9a87b4af08c 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftparser/ExperimentalFeaturesFile.swift @@ -19,7 +19,10 @@ let experimentalFeaturesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) DeclSyntax( """ extension Parser { - @_spi(ExperimentalLanguageFeatures) + /// The type is public so it can appear in the (non-SPI) parser API, but + /// the individual features are `@_spi(ExperimentalLanguageFeatures)` since + /// they are unstable. Clients that don't enable experimental features only + /// ever use the empty set. public struct ExperimentalFeatures: OptionSet, Hashable, Sendable { public let rawValue: UInt public init(rawValue: UInt) { @@ -35,6 +38,7 @@ let experimentalFeaturesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) DeclSyntax( """ /// Whether to enable the parsing of \(raw: feature.documentationDescription). + @_spi(ExperimentalLanguageFeatures) public static let \(feature.token) = Self(rawValue: 1 << \(raw: i)) """ ) @@ -44,6 +48,7 @@ let experimentalFeaturesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) """ /// Creates a new value representing the experimental feature with the /// given name, or returns nil if the name is not recognized. + @_spi(ExperimentalLanguageFeatures) public init?(name: String) """ ) { diff --git a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift index e00dcfcad00..aae830fec0d 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift @@ -14,14 +14,14 @@ internal import SwiftDiagnostics internal import SwiftIfConfig internal import SwiftOperators -@_spi(ExperimentalLanguageFeatures) internal import SwiftParser +@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) internal import SwiftParser internal import SwiftSyntax internal import SwiftSyntaxMacros #else import SwiftDiagnostics import SwiftIfConfig import SwiftOperators -@_spi(ExperimentalLanguageFeatures) import SwiftParser +@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftParser import SwiftSyntax import SwiftSyntaxMacros #endif diff --git a/Sources/SwiftParser/ParseSourceFile.swift b/Sources/SwiftParser/ParseSourceFile.swift index 6f5a713d901..805d021c317 100644 --- a/Sources/SwiftParser/ParseSourceFile.swift +++ b/Sources/SwiftParser/ParseSourceFile.swift @@ -20,27 +20,14 @@ extension Parser { /// Parse the source code in the given string as Swift source file. See /// `Parser.init` for more details. public static func parse( - source: String - ) -> SourceFileSyntax { - return withParser( - source: source, - maximumNestingLevel: nil, - parseTransition: nil, - swiftVersion: nil, - experimentalFeatures: [] - ) { SourceFileSyntax.parse(from: &$0) } - } - - /// A compiler interface that allows the enabling of experimental features. - @_spi(ExperimentalLanguageFeatures) - public static func parse( - source: UnsafeBufferPointer, + source: String, + maximumNestingLevel: Int? = nil, swiftVersion: SwiftVersion? = nil, - experimentalFeatures: ExperimentalFeatures + experimentalFeatures: ExperimentalFeatures = [] ) -> SourceFileSyntax { return withParser( source: source, - maximumNestingLevel: nil, + maximumNestingLevel: maximumNestingLevel, parseTransition: nil, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures @@ -52,14 +39,15 @@ extension Parser { public static func parse( source: UnsafeBufferPointer, maximumNestingLevel: Int? = nil, - swiftVersion: SwiftVersion? = nil + swiftVersion: SwiftVersion? = nil, + experimentalFeatures: ExperimentalFeatures = [] ) -> SourceFileSyntax { return withParser( source: source, maximumNestingLevel: maximumNestingLevel, parseTransition: nil, swiftVersion: swiftVersion, - experimentalFeatures: [] + experimentalFeatures: experimentalFeatures ) { SourceFileSyntax.parse(from: &$0) } } diff --git a/Sources/SwiftParser/Parser.swift b/Sources/SwiftParser/Parser.swift index f5a90f6fc29..4f0eeae8312 100644 --- a/Sources/SwiftParser/Parser.swift +++ b/Sources/SwiftParser/Parser.swift @@ -274,13 +274,25 @@ public struct Parser { } } - /// Private initializer for creating a ``Parser`` from the given string. - private init( - string input: String, - maximumNestingLevel: Int?, - parseTransition: IncrementalParseTransition?, - swiftVersion: SwiftVersion?, - experimentalFeatures: ExperimentalFeatures + /// Initializes a ``Parser`` from the given string. + /// + /// - Parameters: + /// - input: The source text to parse. + /// - maximumNestingLevel: To avoid overflowing the stack, the parser will + /// stop if a nesting level greater than this value + /// is reached. `defaultMaximumNestingLevel` is used + /// if this is `nil`. + /// - parseTransition: The previously recorded state for an incremental + /// parse, or `nil`. + /// - swiftVersion: The version of Swift to parse as, or `nil` for the + /// default. + /// - experimentalFeatures: The experimental features to enable. + public init( + _ input: String, + maximumNestingLevel: Int? = nil, + parseTransition: IncrementalParseTransition? = nil, + swiftVersion: SwiftVersion? = nil, + experimentalFeatures: ExperimentalFeatures = [] ) { var input = input input.makeContiguousUTF8() @@ -300,45 +312,30 @@ public struct Parser { } } - /// Initializes a ``Parser`` from the given string. - public init( - _ input: String, - maximumNestingLevel: Int? = nil, - parseTransition: IncrementalParseTransition? = nil, - swiftVersion: SwiftVersion? = nil - ) { - // Chain to the private String initializer. - self.init( - string: input, - maximumNestingLevel: maximumNestingLevel, - parseTransition: parseTransition, - swiftVersion: swiftVersion, - experimentalFeatures: [] - ) - } - /// Initializes a ``Parser`` from the given input buffer. /// - /// - Parameters + /// - Parameters: /// - input: An input buffer containing Swift source text. It is copied into /// a parser-owned buffer, so it can be freed after the initializer /// has been called. /// - maximumNestingLevel: To avoid overflowing the stack, the parser will /// stop if a nesting level greater than this value - /// is reached. The nesting level is increased - /// whenever a bracketed expression like `(` or `{` - /// is started. `defaultMaximumNestingLevel` is used + /// is reached. `defaultMaximumNestingLevel` is used /// if this is `nil`. /// - parseTransition: The previously recorded state for an incremental /// parse, or `nil`. + /// - swiftVersion: The version of Swift to parse as, or `nil` for the + /// default. + /// - experimentalFeatures: The experimental features to enable. public init( _ input: UnsafeBufferPointer, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, - swiftVersion: SwiftVersion? = nil + swiftVersion: SwiftVersion? = nil, + experimentalFeatures: ExperimentalFeatures = [] ) { - // Chain to the private buffer initializer. Copy the source so the caller may - // free `input` after this initializer returns. + // Copy the source so the caller may free `input` after this initializer + // returns. self.init( buffer: input, maximumNestingLevel: maximumNestingLevel, @@ -346,44 +343,24 @@ public struct Parser { arena: nil, copySource: true, swiftVersion: swiftVersion, - experimentalFeatures: [] - ) - } - - /// Initializes a ``Parser`` from the given input string, with a given set - /// of experimental language features. - @_spi(ExperimentalLanguageFeatures) - public init( - _ input: String, - maximumNestingLevel: Int? = nil, - parseTransition: IncrementalParseTransition? = nil, - swiftVersion: SwiftVersion? = nil, - experimentalFeatures: ExperimentalFeatures - ) { - // Chain to the private String initializer. - self.init( - string: input, - maximumNestingLevel: maximumNestingLevel, - parseTransition: parseTransition, - swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures ) } - /// Initializes a ``Parser`` from the given input buffer, with a given set - /// of experimental language features. - @_spi(ExperimentalLanguageFeatures) + /// Initializes a ``Parser`` from the given input buffer, allocating the parsed + /// syntax into the given arena. + @_spi(RawSyntax) public init( _ input: UnsafeBufferPointer, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, - arena: ParsingRawSyntaxArena? = nil, + arena: ParsingRawSyntaxArena, swiftVersion: SwiftVersion? = nil, - experimentalFeatures: ExperimentalFeatures + experimentalFeatures: ExperimentalFeatures = [] ) { - // Chain to the private buffer initializer. Copy the source so the caller may - // free `input` after this initializer returns, and so the resulting tree - // does not depend on `input` (its tokens are interned into `arena`). + // Copy the source so the caller may free `input` after this initializer + // returns, and so the resulting tree does not depend on `input` (its tokens + // are interned into `arena`). self.init( buffer: input, maximumNestingLevel: maximumNestingLevel, @@ -413,38 +390,8 @@ public struct Parser { source input: UnsafeBufferPointer, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, - body: (inout Parser) -> T - ) -> T { - return withParser( - source: input, - maximumNestingLevel: maximumNestingLevel, - parseTransition: parseTransition, - swiftVersion: nil, - experimentalFeatures: [], - body: body - ) - } - - /// Runs `body` with a `Parser` that lexes directly over `input` without - /// copying it into a parser-owned buffer, with a given Swift version and set - /// of experimental language features. - /// - /// This is the no-copy fast path: the entire parse runs inside this scope, - /// where `input` is guaranteed valid, and each token's text is interned into - /// the arena so the resulting tree is self-contained once `body` returns. - /// - /// - Important: `input` must remain valid for the entire duration of `body`. - /// It is *not* referenced after `body` returns. - /// - Important: The `Parser` passed to `body` must not escape the closure. It - /// lexes directly over `input`, so it is only valid within this scope. - @_spi(RawSyntax) - @_spi(ExperimentalLanguageFeatures) - public static func withParser( - source input: UnsafeBufferPointer, - maximumNestingLevel: Int? = nil, - parseTransition: IncrementalParseTransition? = nil, - swiftVersion: SwiftVersion?, - experimentalFeatures: ExperimentalFeatures, + swiftVersion: SwiftVersion? = nil, + experimentalFeatures: ExperimentalFeatures = [], body: (inout Parser) -> T ) -> T { var parser = Parser( @@ -459,8 +406,7 @@ public struct Parser { return body(&parser) } - /// Runs `body` with a no-copy `Parser` over the UTF-8 of `input`, with a given - /// Swift version and set of experimental language features. + /// Runs `body` with a no-copy `Parser` over the UTF-8 of `input`. /// /// The entire parse runs inside `String.withUTF8`, where the contiguous UTF-8 /// storage is valid, so no copy of the source is needed. @@ -468,13 +414,12 @@ public struct Parser { /// - Important: The `Parser` passed to `body` must not escape the closure. It /// lexes directly over `input`, so it is only valid within this scope. @_spi(RawSyntax) - @_spi(ExperimentalLanguageFeatures) public static func withParser( source input: String, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, - swiftVersion: SwiftVersion?, - experimentalFeatures: ExperimentalFeatures, + swiftVersion: SwiftVersion? = nil, + experimentalFeatures: ExperimentalFeatures = [], body: (inout Parser) -> T ) -> T { var input = input diff --git a/Sources/SwiftParser/generated/ExperimentalFeatures.swift b/Sources/SwiftParser/generated/ExperimentalFeatures.swift index c19b3694059..6ccefda5856 100644 --- a/Sources/SwiftParser/generated/ExperimentalFeatures.swift +++ b/Sources/SwiftParser/generated/ExperimentalFeatures.swift @@ -14,7 +14,10 @@ // swift-format-ignore-file extension Parser { - @_spi(ExperimentalLanguageFeatures) + /// The type is public so it can appear in the (non-SPI) parser API, but + /// the individual features are `@_spi(ExperimentalLanguageFeatures)` since + /// they are unstable. Clients that don't enable experimental features only + /// ever use the empty set. public struct ExperimentalFeatures: OptionSet, Hashable, Sendable { public let rawValue: UInt @@ -26,43 +29,56 @@ extension Parser { extension Parser.ExperimentalFeatures { /// Whether to enable the parsing of reference bindings. + @_spi(ExperimentalLanguageFeatures) public static let referenceBindings = Self (rawValue: 1 << 0) /// Whether to enable the parsing of 'then' statements. + @_spi(ExperimentalLanguageFeatures) public static let thenStatements = Self (rawValue: 1 << 1) /// Whether to enable the parsing of 'do' expressions. + @_spi(ExperimentalLanguageFeatures) public static let doExpressions = Self (rawValue: 1 << 2) /// Whether to enable the parsing of non-escapable types. + @_spi(ExperimentalLanguageFeatures) public static let nonescapableTypes = Self (rawValue: 1 << 3) /// Whether to enable the parsing of trailing commas. + @_spi(ExperimentalLanguageFeatures) public static let trailingComma = Self (rawValue: 1 << 4) /// Whether to enable the parsing of coroutine accessors. + @_spi(ExperimentalLanguageFeatures) public static let coroutineAccessors = Self (rawValue: 1 << 5) /// Whether to enable the parsing of keypaths with method members. + @_spi(ExperimentalLanguageFeatures) public static let keypathWithMethodMembers = Self (rawValue: 1 << 6) /// Whether to enable the parsing of `_move` and `_borrow` as ownership operators. + @_spi(ExperimentalLanguageFeatures) public static let oldOwnershipOperatorSpellings = Self (rawValue: 1 << 7) /// Whether to enable the parsing of set default actor isolation for a file. + @_spi(ExperimentalLanguageFeatures) public static let defaultIsolationPerFile = Self (rawValue: 1 << 8) /// Whether to enable the parsing of borrow and mutate accessors. + @_spi(ExperimentalLanguageFeatures) public static let borrowAndMutateAccessors = Self (rawValue: 1 << 9) /// Whether to enable the parsing of constant-foldable literal expressions. + @_spi(ExperimentalLanguageFeatures) public static let literalExpressions = Self (rawValue: 1 << 10) /// Whether to enable the parsing of a test feature that parses everything as unexpected. + @_spi(ExperimentalLanguageFeatures) public static let _test_EverythingUnexpected = Self (rawValue: 1 << 11) /// Creates a new value representing the experimental feature with the /// given name, or returns nil if the name is not recognized. + @_spi(ExperimentalLanguageFeatures) public init?(name: String) { switch name { case "ReferenceBindings":