diff --git a/Package.swift b/Package.swift index 72e17e362..71641ae28 100644 --- a/Package.swift +++ b/Package.swift @@ -42,13 +42,22 @@ let package = Package( .target( name: "_StringProcessing", dependencies: ["_MatchingEngine", "_CUnicode"], + swiftSettings: [ + .unsafeFlags(["-enable-library-evolution"]), + ]), + .target( + name: "RegexBuilder", + dependencies: ["_StringProcessing", "_MatchingEngine"], swiftSettings: [ .unsafeFlags(["-enable-library-evolution"]), .unsafeFlags(["-Xfrontend", "-enable-experimental-pairwise-build-block"]) ]), .testTarget( name: "RegexTests", - dependencies: ["_StringProcessing"], + dependencies: ["_StringProcessing"]), + .testTarget( + name: "RegexBuilderTests", + dependencies: ["_StringProcessing", "RegexBuilder"], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-enable-experimental-pairwise-build-block"]) ]), @@ -73,7 +82,7 @@ let package = Package( // MARK: Exercises .target( name: "Exercises", - dependencies: ["_MatchingEngine", "Prototypes", "_StringProcessing"], + dependencies: ["_MatchingEngine", "Prototypes", "_StringProcessing", "RegexBuilder"], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-enable-experimental-pairwise-build-block"]) ]), diff --git a/Sources/Exercises/Participants/RegexParticipant.swift b/Sources/Exercises/Participants/RegexParticipant.swift index bae3aed42..731b9b6f6 100644 --- a/Sources/Exercises/Participants/RegexParticipant.swift +++ b/Sources/Exercises/Participants/RegexParticipant.swift @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// import _StringProcessing +import RegexBuilder /* diff --git a/Sources/_StringProcessing/RegexDSL/Anchor.swift b/Sources/RegexBuilder/Anchor.swift similarity index 98% rename from Sources/_StringProcessing/RegexDSL/Anchor.swift rename to Sources/RegexBuilder/Anchor.swift index 57d8f2ffa..ea2dde382 100644 --- a/Sources/_StringProcessing/RegexDSL/Anchor.swift +++ b/Sources/RegexBuilder/Anchor.swift @@ -10,6 +10,7 @@ //===----------------------------------------------------------------------===// import _MatchingEngine +@_spi(RegexBuilder) import _StringProcessing public struct Anchor { internal enum Kind { diff --git a/Sources/_StringProcessing/RegexDSL/Builder.swift b/Sources/RegexBuilder/Builder.swift similarity index 95% rename from Sources/_StringProcessing/RegexDSL/Builder.swift rename to Sources/RegexBuilder/Builder.swift index 78c122828..8921c8f25 100644 --- a/Sources/_StringProcessing/RegexDSL/Builder.swift +++ b/Sources/RegexBuilder/Builder.swift @@ -9,6 +9,8 @@ // //===----------------------------------------------------------------------===// +@_spi(RegexBuilder) import _StringProcessing + @resultBuilder public enum RegexComponentBuilder { public static func buildBlock() -> Regex { diff --git a/Sources/_StringProcessing/RegexDSL/DSL.swift b/Sources/RegexBuilder/DSL.swift similarity index 75% rename from Sources/_StringProcessing/RegexDSL/DSL.swift rename to Sources/RegexBuilder/DSL.swift index 4c3c382cc..816668b67 100644 --- a/Sources/_StringProcessing/RegexDSL/DSL.swift +++ b/Sources/RegexBuilder/DSL.swift @@ -10,6 +10,15 @@ //===----------------------------------------------------------------------===// import _MatchingEngine +@_spi(RegexBuilder) import _StringProcessing + +extension Regex { + public init( + @RegexComponentBuilder _ content: () -> Content + ) where Content.Output == Output { + self.init(content()) + } +} // A convenience protocol for builtin regex components that are initialized with // a `DSLTree` node. @@ -23,51 +32,6 @@ extension _BuiltinRegexComponent { } } -// MARK: - Primitives - -extension String: RegexComponent { - public typealias Output = Substring - - public var regex: Regex { - .init(node: .quotedLiteral(self)) - } -} - -extension Substring: RegexComponent { - public typealias Output = Substring - - public var regex: Regex { - .init(node: .quotedLiteral(String(self))) - } -} - -extension Character: RegexComponent { - public typealias Output = Substring - - public var regex: Regex { - .init(node: .atom(.char(self))) - } -} - -extension UnicodeScalar: RegexComponent { - public typealias Output = Substring - - public var regex: Regex { - .init(node: .atom(.scalar(self))) - } -} - -extension CharacterClass: RegexComponent { - public typealias Output = Substring - - public var regex: Regex { - guard let ast = self.makeAST() else { - fatalError("FIXME: extended AST?") - } - return Regex(ast: ast) - } -} - // MARK: - Combinators // MARK: Concatenation @@ -96,9 +60,9 @@ public struct QuantificationBehavior { case reluctantly case possessively } - + var kind: Kind - + internal var astKind: AST.Quantification.Kind { switch kind { case .eagerly: return .eager @@ -108,19 +72,49 @@ public struct QuantificationBehavior { } } +extension DSLTree.Node { + /// Generates a DSLTree node for a repeated range of the given DSLTree node. + /// Individual public API functions are in the generated Variadics.swift file. + static func repeating( + _ range: Range, + _ behavior: QuantificationBehavior, + _ node: DSLTree.Node + ) -> DSLTree.Node { + // TODO: Throw these as errors + assert(range.lowerBound >= 0, "Cannot specify a negative lower bound") + assert(!range.isEmpty, "Cannot specify an empty range") + + switch (range.lowerBound, range.upperBound) { + case (0, Int.max): // 0... + return .quantification(.zeroOrMore, behavior.astKind, node) + case (1, Int.max): // 1... + return .quantification(.oneOrMore, behavior.astKind, node) + case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1 + // Note: `behavior` is ignored in this case + return .quantification(.exactly(.init(faking: range.lowerBound)), .eager, node) + case (0, _): // 0..: _BuiltinRegexComponent { // MARK: - Backreference -struct ReferenceID: Hashable, Equatable { - private static var counter: Int = 0 - var base: Int - - init() { - base = Self.counter - Self.counter += 1 - } -} - public struct Reference: RegexComponent { let id = ReferenceID() - + public init(_ captureType: Capture.Type = Capture.self) {} public var regex: Regex { .init(node: .atom(.symbolicReference(id))) } } + +extension Regex.Match { + public subscript(_ reference: Reference) -> Capture { + self[reference.id] + } +} diff --git a/Sources/RegexBuilder/Match.swift b/Sources/RegexBuilder/Match.swift new file mode 100644 index 000000000..3f86f9498 --- /dev/null +++ b/Sources/RegexBuilder/Match.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021-2022 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 +// +//===----------------------------------------------------------------------===// + +import _StringProcessing + +extension String { + public func match( + @RegexComponentBuilder _ content: () -> R + ) -> Regex.Match? { + match(content()) + } +} + +extension Substring { + public func match( + @RegexComponentBuilder _ content: () -> R + ) -> Regex.Match? { + match(content()) + } +} diff --git a/Sources/_StringProcessing/RegexDSL/Variadics.swift b/Sources/RegexBuilder/Variadics.swift similarity index 99% rename from Sources/_StringProcessing/RegexDSL/Variadics.swift rename to Sources/RegexBuilder/Variadics.swift index c81f8b555..60292252a 100644 --- a/Sources/_StringProcessing/RegexDSL/Variadics.swift +++ b/Sources/RegexBuilder/Variadics.swift @@ -12,6 +12,7 @@ // BEGIN AUTO-GENERATED CONTENT import _MatchingEngine +@_spi(RegexBuilder) import _StringProcessing extension RegexComponentBuilder { public static func buildPartialBlock( diff --git a/Sources/VariadicsGenerator/VariadicsGenerator.swift b/Sources/VariadicsGenerator/VariadicsGenerator.swift index 683d45e6e..1f41e68d6 100644 --- a/Sources/VariadicsGenerator/VariadicsGenerator.swift +++ b/Sources/VariadicsGenerator/VariadicsGenerator.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -// swift run VariadicsGenerator --max-arity 10 > Sources/_StringProcessing/RegexDSL/Variadics.swift +// swift run VariadicsGenerator --max-arity 10 > Sources/RegexBuilder/Variadics.swift import ArgumentParser #if os(macOS) @@ -121,6 +121,7 @@ struct VariadicsGenerator: ParsableCommand { // BEGIN AUTO-GENERATED CONTENT import _MatchingEngine + @_spi(RegexBuilder) import _StringProcessing """) diff --git a/Sources/_StringProcessing/CharacterClass.swift b/Sources/_StringProcessing/CharacterClass.swift index d72ecf06c..7989c0943 100644 --- a/Sources/_StringProcessing/CharacterClass.swift +++ b/Sources/_StringProcessing/CharacterClass.swift @@ -178,6 +178,17 @@ public struct CharacterClass: Hashable { } } +extension CharacterClass: RegexComponent { + public typealias Output = Substring + + public var regex: Regex { + guard let ast = self.makeAST() else { + fatalError("FIXME: extended AST?") + } + return Regex(ast: ast) + } +} + extension RegexComponent where Self == CharacterClass { public static var any: CharacterClass { .init(cc: .any, matchLevel: .graphemeCluster) diff --git a/Sources/_StringProcessing/RegexDSL/ASTConversion.swift b/Sources/_StringProcessing/Regex/ASTConversion.swift similarity index 100% rename from Sources/_StringProcessing/RegexDSL/ASTConversion.swift rename to Sources/_StringProcessing/Regex/ASTConversion.swift diff --git a/Sources/_StringProcessing/RegexDSL/AnyRegexOutput.swift b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift similarity index 100% rename from Sources/_StringProcessing/RegexDSL/AnyRegexOutput.swift rename to Sources/_StringProcessing/Regex/AnyRegexOutput.swift diff --git a/Sources/_StringProcessing/RegexDSL/Core.swift b/Sources/_StringProcessing/Regex/Core.swift similarity index 78% rename from Sources/_StringProcessing/RegexDSL/Core.swift rename to Sources/_StringProcessing/Regex/Core.swift index 236888c77..c6433ef3e 100644 --- a/Sources/_StringProcessing/RegexDSL/Core.swift +++ b/Sources/_StringProcessing/Regex/Core.swift @@ -36,6 +36,7 @@ public struct Regex: RegexComponent { init(ast: AST) { self.tree = ast.dslTree } + init(tree: DSLTree) { self.tree = tree } @@ -44,7 +45,8 @@ public struct Regex: RegexComponent { let program: Program // var ast: AST { program.ast } - var root: DSLTree.Node { + @_spi(RegexBuilder) + public var root: DSLTree.Node { program.tree.root } @@ -59,7 +61,8 @@ public struct Regex: RegexComponent { self.program = Program(ast: .init(ast, globalOptions: nil)) } - init(node: DSLTree.Node) { + @_spi(RegexBuilder) + public init(node: DSLTree.Node) { self.program = Program(tree: .init(node, options: nil)) } @@ -84,17 +87,46 @@ public struct Regex: RegexComponent { self = content.regex } - public init( - @RegexComponentBuilder _ content: () -> Content - ) where Content.Output == Output { - self.init(content()) + public var regex: Regex { + self } +} + +// MARK: - Primitive regex components + +extension String: RegexComponent { + public typealias Output = Substring public var regex: Regex { - self + .init(node: .quotedLiteral(self)) + } +} + +extension Substring: RegexComponent { + public typealias Output = Substring + + public var regex: Regex { + .init(node: .quotedLiteral(String(self))) + } +} + +extension Character: RegexComponent { + public typealias Output = Substring + + public var regex: Regex { + .init(node: .atom(.char(self))) + } +} + +extension UnicodeScalar: RegexComponent { + public typealias Output = Substring + + public var regex: Regex { + .init(node: .atom(.scalar(self))) } } +// MARK: - Testing public struct MockRegexLiteral: RegexComponent { public typealias MatchValue = Substring diff --git a/Sources/_StringProcessing/RegexDSL/DSLConsumers.swift b/Sources/_StringProcessing/Regex/DSLConsumers.swift similarity index 100% rename from Sources/_StringProcessing/RegexDSL/DSLConsumers.swift rename to Sources/_StringProcessing/Regex/DSLConsumers.swift diff --git a/Sources/_StringProcessing/RegexDSL/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift similarity index 81% rename from Sources/_StringProcessing/RegexDSL/DSLTree.swift rename to Sources/_StringProcessing/Regex/DSLTree.swift index 25a5943c0..e579828d2 100644 --- a/Sources/_StringProcessing/RegexDSL/DSLTree.swift +++ b/Sources/_StringProcessing/Regex/DSLTree.swift @@ -11,7 +11,8 @@ import _MatchingEngine -struct DSLTree { +@_spi(RegexBuilder) +public struct DSLTree { var root: Node var options: Options? @@ -22,7 +23,8 @@ struct DSLTree { } extension DSLTree { - indirect enum Node: _TreeNode { + @_spi(RegexBuilder) + public indirect enum Node: _TreeNode { /// Try to match each node in order /// /// ... | ... | ... @@ -101,7 +103,8 @@ extension DSLTree { } extension DSLTree { - struct CustomCharacterClass { + @_spi(RegexBuilder) + public struct CustomCharacterClass { var members: [Member] var isInverted: Bool @@ -120,7 +123,8 @@ extension DSLTree { } } - enum Atom { + @_spi(RegexBuilder) + public enum Atom { case char(Character) case scalar(Unicode.Scalar) case any @@ -134,18 +138,21 @@ extension DSLTree { } // CollectionConsumer -typealias _ConsumerInterface = ( +@_spi(RegexBuilder) +public typealias _ConsumerInterface = ( String, Range ) -> String.Index? // Type producing consume // TODO: better name -typealias _MatcherInterface = ( +@_spi(RegexBuilder) +public typealias _MatcherInterface = ( String, String.Index, Range ) -> (String.Index, Any)? // Character-set (post grapheme segmentation) -typealias _CharacterPredicateInterface = ( +@_spi(RegexBuilder) +public typealias _CharacterPredicateInterface = ( (Character) -> Bool ) @@ -161,7 +168,8 @@ typealias _CharacterPredicateInterface = ( */ extension DSLTree.Node { - var children: [DSLTree.Node]? { + @_spi(RegexBuilder) + public var children: [DSLTree.Node]? { switch self { case let .orderedChoice(v): return v @@ -256,7 +264,8 @@ extension DSLTree { } } extension DSLTree.Node { - func _captureStructure( + @_spi(RegexBuilder) + public func _captureStructure( _ constructor: inout CaptureStructure.Constructor ) -> CaptureStructure { switch self { @@ -323,14 +332,18 @@ extension DSLTree.Node { } extension DSLTree.Node { - func appending(_ newNode: DSLTree.Node) -> DSLTree.Node { + @_spi(RegexBuilder) + public func appending(_ newNode: DSLTree.Node) -> DSLTree.Node { if case .concatenation(let components) = self { return .concatenation(components + [newNode]) } return .concatenation([self, newNode]) } - func appendingAlternationCase(_ newNode: DSLTree.Node) -> DSLTree.Node { + @_spi(RegexBuilder) + public func appendingAlternationCase( + _ newNode: DSLTree.Node + ) -> DSLTree.Node { if case .orderedChoice(let components) = self { return .orderedChoice(components + [newNode]) } @@ -338,32 +351,13 @@ extension DSLTree.Node { } } -extension DSLTree.Node { - /// Generates a DSLTree node for a repeated range of the given DSLTree node. - /// Individual public API functions are in the generated Variadics.swift file. - static func repeating( - _ range: Range, - _ behavior: QuantificationBehavior, - _ node: DSLTree.Node - ) -> DSLTree.Node { - // TODO: Throw these as errors - assert(range.lowerBound >= 0, "Cannot specify a negative lower bound") - assert(!range.isEmpty, "Cannot specify an empty range") - - switch (range.lowerBound, range.upperBound) { - case (0, Int.max): // 0... - return .quantification(.zeroOrMore, behavior.astKind, node) - case (1, Int.max): // 1... - return .quantification(.oneOrMore, behavior.astKind, node) - case _ where range.count == 1: // ..<1 or ...0 or any range with count == 1 - // Note: `behavior` is ignored in this case - return .quantification(.exactly(.init(faking: range.lowerBound)), .eager, node) - case (0, _): // 0..(_ reference: Reference) -> Capture { - guard let offset = referencedCaptureOffsets[reference.id] else { + @_spi(RegexBuilder) + public subscript(_ id: ReferenceID) -> Capture { + guard let offset = referencedCaptureOffsets[id] else { preconditionFailure( "Reference did not capture any match in the regex") } @@ -98,21 +99,9 @@ extension String { public func match(_ regex: R) -> Regex.Match? { regex.match(in: self) } - - public func match( - @RegexComponentBuilder _ content: () -> R - ) -> Regex.Match? { - match(content()) - } } extension Substring { public func match(_ regex: R) -> Regex.Match? { regex.match(in: self) } - - public func match( - @RegexComponentBuilder _ content: () -> R - ) -> Regex.Match? { - match(content()) - } } diff --git a/Sources/_StringProcessing/RegexDSL/Options.swift b/Sources/_StringProcessing/Regex/Options.swift similarity index 100% rename from Sources/_StringProcessing/RegexDSL/Options.swift rename to Sources/_StringProcessing/Regex/Options.swift diff --git a/Tests/RegexBuilderTests/AlgorithmsTests.swift b/Tests/RegexBuilderTests/AlgorithmsTests.swift new file mode 100644 index 000000000..183d247a7 --- /dev/null +++ b/Tests/RegexBuilderTests/AlgorithmsTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021-2022 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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import _StringProcessing +@testable import RegexBuilder + +class RegexConsumerTests: XCTestCase { + func testMatches() { + let regex = Capture(OneOrMore(.digit)) { 2 * Int($0)! } + let str = "foo 160 bar 99 baz" + XCTAssertEqual(str.matches(of: regex).map(\.result.1), [320, 198]) + } + + func testMatchReplace() { + func replaceTest( + _ regex: R, + input: String, + result: String, + _ replace: (_MatchResult>) -> String, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertEqual(input.replacing(regex, with: replace), result) + } + + let int = Capture(OneOrMore(.digit)) { Int($0)! } + + replaceTest( + int, + input: "foo 160 bar 99 baz", + result: "foo 240 bar 143 baz", + { match in String(match.result.1, radix: 8) }) + + replaceTest( + Regex { int; "+"; int }, + input: "9+16, 0+3, 5+5, 99+1", + result: "25, 3, 10, 100", + { match in "\(match.result.1 + match.result.2)" }) + + // TODO: Need to support capture history + // replaceTest( + // OneOrMore { int; "," }, + // input: "3,5,8,0, 1,0,2,-5,x8,8,", + // result: "16 3-5x16", + // { match in "\(match.result.1.reduce(0, +))" }) + + replaceTest( + Regex { int; "x"; int; Optionally { "x"; int } }, + input: "2x3 5x4x3 6x0 1x2x3x4", + result: "6 60 0 6x4", + { match in "\(match.result.1 * match.result.2 * (match.result.3 ?? 1))" }) + } +} diff --git a/Tests/RegexTests/CustomTests.swift b/Tests/RegexBuilderTests/CustomTests.swift similarity index 84% rename from Tests/RegexTests/CustomTests.swift rename to Tests/RegexBuilderTests/CustomTests.swift index 12d4ad6cd..b405a5399 100644 --- a/Tests/RegexTests/CustomTests.swift +++ b/Tests/RegexBuilderTests/CustomTests.swift @@ -1,5 +1,17 @@ -import _StringProcessing +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021-2022 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 +// +//===----------------------------------------------------------------------===// + import XCTest +import _StringProcessing +@testable import RegexBuilder // A nibbler processes a single character from a string private protocol Nibbler: CustomRegexComponent { @@ -58,8 +70,7 @@ func customTest( } } -extension RegexTests { - +class CustomRegexComponentTests: XCTestCase { // TODO: Refactor below into more exhaustive, declarative // tests. func testCustomRegexComponents() { @@ -127,5 +138,4 @@ extension RegexTests { XCTAssertEqual(res4.result.0, "123") XCTAssertEqual(res4.result.1, 3) } - } diff --git a/Tests/RegexTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift similarity index 99% rename from Tests/RegexTests/RegexDSLTests.swift rename to Tests/RegexBuilderTests/RegexDSLTests.swift index 4ca446b32..a7d307b9b 100644 --- a/Tests/RegexTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -10,7 +10,8 @@ //===----------------------------------------------------------------------===// import XCTest -@testable import _StringProcessing +import _StringProcessing +@testable import RegexBuilder class RegexDSLTests: XCTestCase { func _testDSLCaptures( diff --git a/Tests/RegexTests/AlgorithmsTests.swift b/Tests/RegexTests/AlgorithmsTests.swift index 6a7bf646b..b51f12100 100644 --- a/Tests/RegexTests/AlgorithmsTests.swift +++ b/Tests/RegexTests/AlgorithmsTests.swift @@ -114,52 +114,6 @@ class RegexConsumerTests: XCTestCase { expectReplace("aab", "a+", "X", "Xb") expectReplace("aab", "a*", "X", "XXbX") } - - func testMatches() { - let regex = Capture(OneOrMore(.digit)) { 2 * Int($0)! } - let str = "foo 160 bar 99 baz" - XCTAssertEqual(str.matches(of: regex).map(\.result.1), [320, 198]) - } - - func testMatchReplace() { - func replaceTest( - _ regex: R, - input: String, - result: String, - _ replace: (_MatchResult>) -> String, - file: StaticString = #file, - line: UInt = #line - ) { - XCTAssertEqual(input.replacing(regex, with: replace), result) - } - - let int = Capture(OneOrMore(.digit)) { Int($0)! } - - replaceTest( - int, - input: "foo 160 bar 99 baz", - result: "foo 240 bar 143 baz", - { match in String(match.result.1, radix: 8) }) - - replaceTest( - Regex { int; "+"; int }, - input: "9+16, 0+3, 5+5, 99+1", - result: "25, 3, 10, 100", - { match in "\(match.result.1 + match.result.2)" }) - - // TODO: Need to support capture history - // replaceTest( - // OneOrMore { int; "," }, - // input: "3,5,8,0, 1,0,2,-5,x8,8,", - // result: "16 3-5x16", - // { match in "\(match.result.1.reduce(0, +))" }) - - replaceTest( - Regex { int; "x"; int; Optionally { "x"; int } }, - input: "2x3 5x4x3 6x0 1x2x3x4", - result: "6 60 0 6x4", - { match in "\(match.result.1 * match.result.2 * (match.result.3 ?? 1))" }) - } func testAdHoc() { let r = try! Regex("a|b+") diff --git a/Tests/RegexTests/CaptureTests.swift b/Tests/RegexTests/CaptureTests.swift index cc3568c1d..258aea86d 100644 --- a/Tests/RegexTests/CaptureTests.swift +++ b/Tests/RegexTests/CaptureTests.swift @@ -1,6 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021-2022 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 +// +//===----------------------------------------------------------------------===// import XCTest -@testable import _StringProcessing +@testable @_spi(RegexBuilder) import _StringProcessing import _MatchingEngine extension StructuredCapture {