diff --git a/Sources/RegexBuilder/DSL.swift b/Sources/RegexBuilder/DSL.swift index e8dffaa8e..f3b0fd702 100644 --- a/Sources/RegexBuilder/DSL.swift +++ b/Sources/RegexBuilder/DSL.swift @@ -41,18 +41,14 @@ extension _BuiltinRegexComponent { extension String: RegexComponent { public typealias Output = Substring - public var regex: Regex { - .init(node: .quotedLiteral(self)) - } + public var regex: Regex { .init(verbatim: self) } } @available(SwiftStdlib 5.7, *) extension Substring: RegexComponent { public typealias Output = Substring - public var regex: Regex { - .init(node: .quotedLiteral(String(self))) - } + public var regex: Regex { String(self).regex } } @available(SwiftStdlib 5.7, *) diff --git a/Sources/_StringProcessing/Capture.swift b/Sources/_StringProcessing/Capture.swift index fe00bdc0f..6f87fa625 100644 --- a/Sources/_StringProcessing/Capture.swift +++ b/Sources/_StringProcessing/Capture.swift @@ -49,7 +49,7 @@ extension AnyRegexOutput.Element { from: input, in: range, value: value, - optionalCount: optionalDepth + optionalCount: representation.optionalDepth ) } diff --git a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift index 1b5ce346f..df70e9fc7 100644 --- a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift +++ b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift @@ -11,90 +11,31 @@ @_implementationOnly import _RegexParser -@available(SwiftStdlib 5.7, *) -extension Regex where Output == AnyRegexOutput { - /// Parses and compiles a regular expression, resulting in an existentially-typed capture list. - /// - /// - Parameter pattern: The regular expression. - public init(_ pattern: String) throws { - self.init(ast: try parse(pattern, .semantic, .traditional)) - } -} - -@available(SwiftStdlib 5.7, *) -extension Regex { - /// Parses and compiles a regular expression. - /// - /// - Parameter pattern: The regular expression. - /// - Parameter as: The desired type for the output. - public init( - _ pattern: String, - as: Output.Type = Output.self - ) throws { - self.init(ast: try parse(pattern, .semantic, .traditional)) - } -} - -@available(SwiftStdlib 5.7, *) -extension Regex.Match where Output == AnyRegexOutput { - /// Accesses the whole match using the `.0` syntax. - public subscript( - dynamicMember keyPath: KeyPath<(Substring, _doNotUse: ()), Substring> - ) -> Substring { - anyRegexOutput.input[range] - } - - public subscript(name: String) -> AnyRegexOutput.Element? { - anyRegexOutput.first { - $0.name == name - } - } -} - /// A type-erased regex output. @available(SwiftStdlib 5.7, *) public struct AnyRegexOutput { - let input: String - let _elements: [ElementRepresentation] - - /// The underlying representation of the element of a type-erased regex - /// output. - internal struct ElementRepresentation { - /// The depth of `Optioals`s wrapping the underlying value. For example, - /// `Substring` has optional depth `0`, and `Int??` has optional depth `2`. - let optionalDepth: Int - - /// The bounds of the output element. - let bounds: Range? - - /// The name of the capture. - var name: String? = nil - - /// The capture reference this element refers to. - var referenceID: ReferenceID? = nil - - /// If the output vaule is strongly typed, then this will be set. - var value: Any? = nil - } + internal let input: String + internal let _elements: [ElementRepresentation] } @available(SwiftStdlib 5.7, *) extension AnyRegexOutput { - /// Creates a type-erased regex output from an existing output. + /// Creates a type-erased regex output from an existing match. /// - /// Use this initializer to fit a regex with strongly typed captures into the - /// use site of a dynamic regex, like one that was created from a string. + /// Use this initializer to fit a strongly-typed regex match into the + /// use site of a type-erased regex output. public init(_ match: Regex.Match) { self = match.anyRegexOutput } - /// Returns a typed output by converting the underlying value to the specified - /// type. + /// Returns a strongly-typed output by converting type-erased values to the specified type. /// /// - Parameter type: The expected output type. /// - Returns: The output, if the underlying value can be converted to the /// output type; otherwise `nil`. - public func `as`(_ type: Output.Type = Output.self) -> Output? { + public func extractValues( + as type: Output.Type = Output.self + ) -> Output? { let elements = map { $0.existentialOutputComponent(from: input[...]) } @@ -102,57 +43,18 @@ extension AnyRegexOutput { } } -@available(SwiftStdlib 5.7, *) -extension AnyRegexOutput { - internal init(input: String, elements: [ElementRepresentation]) { - self.init( - input: input, - _elements: elements - ) - } -} - -@available(SwiftStdlib 5.7, *) -extension AnyRegexOutput.ElementRepresentation { - func value(forInput input: String) -> Any { - // Ok for now because `existentialMatchComponent` - // wont slice the input if there's no range to slice with - // - // FIXME: This is ugly :-/ - let input = bounds.map { input[$0] } ?? "" - - return constructExistentialOutputComponent( - from: input, - in: bounds, - value: nil, - optionalCount: optionalDepth - ) - } -} - @available(SwiftStdlib 5.7, *) extension AnyRegexOutput: RandomAccessCollection { + /// An individual type-erased output value. public struct Element { - fileprivate let representation: ElementRepresentation - let input: String - - var optionalDepth: Int { - representation.optionalDepth - } - - var name: String? { - representation.name - } - + internal let representation: ElementRepresentation + internal let input: String + /// The range over which a value was captured. `nil` for no-capture. public var range: Range? { representation.bounds } - - var referenceID: ReferenceID? { - representation.referenceID - } - + /// The slice of the input over which a value was captured. `nil` for no-capture. public var substring: Substring? { range.map { input[$0] } @@ -160,8 +62,23 @@ extension AnyRegexOutput: RandomAccessCollection { /// The captured value, `nil` for no-capture public var value: Any? { + // FIXME: Should this return the substring for default-typed + // values? representation.value } + + /// The name of this capture, if it has one, otherwise `nil`. + public var name: String? { + representation.name + } + + // TODO: Consider making API, and figure out how + // DSL and this would work together... + /// Whether this capture is considered optional by the regex. I.e., + /// whether it is inside an alternation or zero-or-n quantification. + var isOptional: Bool { + representation.optionalDepth != 0 + } } public var startIndex: Int { @@ -191,6 +108,7 @@ extension AnyRegexOutput: RandomAccessCollection { @available(SwiftStdlib 5.7, *) extension AnyRegexOutput { + /// Access a capture by name. Returns `nil` if no capture with that name was present in the Regex. public subscript(name: String) -> Element? { first { $0.name == name @@ -200,21 +118,52 @@ extension AnyRegexOutput { @available(SwiftStdlib 5.7, *) extension Regex.Match where Output == AnyRegexOutput { - /// Creates a type-erased regex match from an existing match. + /// Accesses the whole match using the `.0` syntax. + public subscript( + dynamicMember keyPath: KeyPath<(Substring, _doNotUse: ()), Substring> + ) -> Substring { + anyRegexOutput.input[range] + } + + /// Access a capture by name. Returns `nil` if there's no capture with that name. + public subscript(name: String) -> AnyRegexOutput.Element? { + anyRegexOutput.first { + $0.name == name + } + } +} + +// MARK: - Run-time regex creation and queries + +@available(SwiftStdlib 5.7, *) +extension Regex where Output == AnyRegexOutput { + /// Parses and compiles a regular expression, resulting in a type-erased capture list. /// - /// Use this initializer to fit a regex match with strongly typed captures into the - /// use site of a dynamic regex match, like one that was created from a string. - public init(_ match: Regex.Match) { - self.init( - anyRegexOutput: match.anyRegexOutput, - range: match.range, - value: match.value - ) + /// - Parameter pattern: The regular expression. + public init(_ pattern: String) throws { + self.init(ast: try parse(pattern, .semantic, .traditional)) } } @available(SwiftStdlib 5.7, *) extension Regex { + /// Parses and compiles a regular expression. + /// + /// - Parameter pattern: The regular expression. + /// - Parameter as: The desired type for the output. + public init( + _ pattern: String, + as: Output.Type = Output.self + ) throws { + self.init(ast: try parse(pattern, .semantic, .traditional)) + } + + /// Produces a regex that matches `verbatim` exactly, as though every + /// metacharacter in it was escaped. + public init(verbatim: String) { + self.init(node: .quotedLiteral(verbatim)) + } + /// Returns whether a named-capture with `name` exists public func contains(captureNamed name: String) -> Bool { program.tree.root._captureList.captures.contains(where: { @@ -223,30 +172,95 @@ extension Regex { } } +// MARK: - Converting to/from ARO + @available(SwiftStdlib 5.7, *) extension Regex where Output == AnyRegexOutput { /// Creates a type-erased regex from an existing regex. /// - /// Use this initializer to fit a regex with strongly typed captures into the - /// use site of a dynamic regex, i.e. one that was created from a string. + /// Use this initializer to fit a regex with strongly-typed captures into the + /// use site of a type-erased regex, i.e. one that was created from a string. public init(_ regex: Regex) { self.init(node: regex.root) } +} - /// Returns a typed regex by converting the underlying types. +@available(SwiftStdlib 5.7, *) +extension Regex.Match where Output == AnyRegexOutput { + /// Creates a type-erased regex match from an existing match. /// - /// - Parameter type: The expected output type. - /// - Returns: A regex generic over the output type if the underlying types can be converted. - /// Returns `nil` otherwise. - public func `as`( - _ type: Output.Type = Output.self - ) -> Regex? { - let result = Regex(node: root) - - guard result._verifyType() else { + /// Use this initializer to fit a regex match with strongly-typed captures into the + /// use site of a type-erased regex match. + public init(_ match: Regex.Match) { + self.init( + anyRegexOutput: match.anyRegexOutput, + range: match.range, + value: match.value + ) + } +} + +@available(SwiftStdlib 5.7, *) +extension Regex { + /// Creates a strongly-typed regex from a type-erased regex. + /// + /// Use this initializer to create a strongly-typed regex from + /// one that was created from a string. Returns `nil` if the types + /// don't match. + public init?( + _ erased: Regex, + as: Output.Type = Output.self + ) { + self.init(node: erased.root) + guard self._verifyType() else { return nil } - - return result + } +} + +// MARK: - Internals + +@available(SwiftStdlib 5.7, *) +extension AnyRegexOutput { + /// The underlying representation of the element of a type-erased regex + /// output. + internal struct ElementRepresentation { + /// The depth of `Optioals`s wrapping the underlying value. For example, + /// `Substring` has optional depth `0`, and `Int??` has optional depth `2`. + let optionalDepth: Int + + /// The bounds of the output element. + let bounds: Range? + + /// The name of the capture. + var name: String? = nil + + /// The capture reference this element refers to. + var referenceID: ReferenceID? = nil + + /// If the output vaule is strongly typed, then this will be set. + var value: Any? = nil + } + + internal init(input: String, elements: [ElementRepresentation]) { + self.init(input: input, _elements: elements) + } +} + +@available(SwiftStdlib 5.7, *) +extension AnyRegexOutput.ElementRepresentation { + fileprivate func value(forInput input: String) -> Any { + // Ok for now because `existentialMatchComponent` + // wont slice the input if there's no range to slice with + // + // FIXME: This is ugly :-/ + let input = bounds.map { input[$0] } ?? "" + + return constructExistentialOutputComponent( + from: input, + in: bounds, + value: nil, + optionalCount: optionalDepth + ) } } diff --git a/Sources/_StringProcessing/Regex/Match.swift b/Sources/_StringProcessing/Regex/Match.swift index 78c9c8c9f..a9234020f 100644 --- a/Sources/_StringProcessing/Regex/Match.swift +++ b/Sources/_StringProcessing/Regex/Match.swift @@ -38,7 +38,7 @@ extension Regex.Match { let output = AnyRegexOutput( input: anyRegexOutput.input, - _elements: [wholeMatchCapture] + anyRegexOutput._elements + elements: [wholeMatchCapture] + anyRegexOutput._elements ) return output as! Output @@ -77,7 +77,7 @@ extension Regex.Match { @_spi(RegexBuilder) public subscript(_ id: ReferenceID) -> Capture { guard let element = anyRegexOutput.first( - where: { $0.referenceID == id } + where: { $0.representation.referenceID == id } ) else { preconditionFailure("Reference did not capture any match in the regex") } diff --git a/Tests/RegexBuilderTests/AnyRegexOutputTests.swift b/Tests/RegexBuilderTests/AnyRegexOutputTests.swift new file mode 100644 index 000000000..9b32a86fd --- /dev/null +++ b/Tests/RegexBuilderTests/AnyRegexOutputTests.swift @@ -0,0 +1,256 @@ + +import XCTest +import _StringProcessing +import RegexBuilder + +private let enablePrinting = false + +extension RegexDSLTests { + + func testContrivedAROExample() { + // Find and extract potential IDs. IDs are 8 bytes encoded as + // 16 hexadecimal numbers, with an optional `-` between every + // double-byte (i.e. 4 hex digits). + // + // AAAA-BBBB-CCCC-DDDD + // AAAABBBBCCCCDDDD + // AAAABBBBCCCC-DDDD + // + // IDs are converted to uppercase and hyphen separated + // + // The regex can have special capture names which affect replacement + // behavior + // - "salient": presented uppercase in square brackets after + // - "note": presented lowercase in parens + // - none: nothing + // - no captures: "" + // + let input = """ + Machine 1234-5678-90ab-CDEF connected + Session FEDCAB0987654321 ended + Artiface 0011deff-2231-abcd contrived + """ + let noCapOutput = """ + Machine connected + Session ended + Artiface contrived + """ + let unnamedOutput = """ + Machine 1234-5678-90AB-CDEF connected + Session FEDC-AB09-8765-4321 ended + Artiface 0011-DEFF-2231-ABCD contrived + """ + let salientOutput = """ + Machine 1234-5678-90AB-CDEF [5678] connected + Session FEDC-AB09-8765-4321 [AB09] ended + Artiface 0011-DEFF-2231-ABCD [DEFF] contrived + """ + let noteOutput = """ + Machine 1234-5678-90AB-CDEF (5678) connected + Session FEDC-AB09-8765-4321 (ab09) ended + Artiface 0011-DEFF-2231-ABCD (deff) contrived + """ + + enum Kind { + case none + case unnamed + case salient + case note + + func contains(captureNamed s: String) -> Bool { + switch self { + case .none: return false + case .unnamed: return false + case .salient: return s == "salient" + case .note: return s == "note" + } + } + + var expected: String { + switch self { + case .none: return """ + Machine connected + Session ended + Artiface contrived + """ + case .unnamed: return """ + Machine 1234-5678-90AB-CDEF connected + Session FEDC-AB09-8765-4321 ended + Artiface 0011-DEFF-2231-ABCD contrived + """ + case .salient: return """ + Machine 1234-5678-90AB-CDEF [5678] connected + Session FEDC-AB09-8765-4321 [AB09] ended + Artiface 0011-DEFF-2231-ABCD [DEFF] contrived + """ + case .note: return """ + Machine 1234-5678-90AB-CDEF (5678) connected + Session FEDC-AB09-8765-4321 (ab09) ended + Artiface 0011-DEFF-2231-ABCD (deff) contrived + """ + } + } + } + + func checkContains( + _ re: Regex, _ kind: Kind + ) { + for name in ["", "salient", "note", "other"] { + XCTAssertEqual( + kind.contains(captureNamed: name), re.contains(captureNamed: name)) + } + } + func checkAROReplacing( + _ re: Regex, _ kind: Kind + ) { + let aro = Regex(re) + let output = input.replacing(aro) { + (match: Regex.Match) -> String in + + if match.count < 5 { return "" } + + let suffix: String + if re.contains(captureNamed: "salient") { + let body = match["salient"]!.substring?.uppercased() ?? "" + suffix = " [\(body)]" + } else if re.contains(captureNamed: "note") { + let body = match["note"]!.substring?.lowercased() ?? "" + suffix = " (\(body))" + } else { + suffix = "" + } + + return match.output.dropFirst().lazy.map { + $0.substring!.uppercased() + }.joined(separator: "-") + suffix + } + + XCTAssertEqual(output, kind.expected) + + if enablePrinting { + print("---") + print(output) + print(kind) + } + } + func check( + _ re: Regex, _ kind: Kind, _ expected: String + ) { + let aro = Regex(re) + + // FIXME: The below fatal errors + let casted = aro//try! XCTUnwrap(Regex(aro, as: Output.self)) + + // contains(captureNamed:) + checkContains(re, kind) + checkContains(aro, kind) + checkContains(casted, kind) + + // replacing + checkAROReplacing(re, kind) + checkAROReplacing(aro, kind) + checkAROReplacing(casted, kind) + } + + // Literals (mocked up via explicit `as` types) + check(try! Regex(#""" + (?x) + \p{hexdigit}{4} -? \p{hexdigit}{4} -? + \p{hexdigit}{4} -? \p{hexdigit}{4} + """#, as: Substring.self), + .none, + noCapOutput + ) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#, as: (Substring, Substring, Substring, Substring, Substring).self), + .unnamed, + unnamedOutput + ) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (?\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#, as: (Substring, Substring, Substring, Substring, Substring).self), + .salient, + salientOutput + ) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (?\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#, as: (Substring, Substring, Substring, Substring, Substring).self), + .note, + noteOutput + ) + + // Run-time strings (ARO) + check(try! Regex(#""" + (?x) + \p{hexdigit}{4} -? \p{hexdigit}{4} -? + \p{hexdigit}{4} -? \p{hexdigit}{4} + """#), + .none, + noCapOutput) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#), + .unnamed, + unnamedOutput + ) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (?\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#), + .salient, + salientOutput + ) + check(try! Regex(#""" + (?x) + (\p{hexdigit}{4}) -? (?\p{hexdigit}{4}) -? + (\p{hexdigit}{4}) -? (\p{hexdigit}{4}) + """#), + .note, + noteOutput + ) + + // Builders + check( + Regex { + let doublet = Repeat(.hexDigit, count: 4) + doublet + Optionally { "-" } + doublet + Optionally { "-" } + doublet + Optionally { "-" } + doublet + }, + .none, + noCapOutput + ) + check( + Regex { + let doublet = Repeat(.hexDigit, count: 4) + Capture { doublet } + Optionally { "-" } + Capture { doublet } + Optionally { "-" } + Capture { doublet } + Optionally { "-" } + Capture { doublet } + }, + .unnamed, + unnamedOutput + ) + + // FIXME: `salient` and `note` builders using a semantically rich + // `mapOutput` + + } +} diff --git a/Tests/RegexTests/AnyRegexOutputTests.swift b/Tests/RegexTests/AnyRegexOutputTests.swift index 8d91c0ec8..10bb6a061 100644 --- a/Tests/RegexTests/AnyRegexOutputTests.swift +++ b/Tests/RegexTests/AnyRegexOutputTests.swift @@ -103,13 +103,13 @@ extension RegexTests { // TODO: ARO init from concrete match tuple - let concreteOutputCasted = output.as( - (Substring, fieldA: Substring, fieldB: Substring).self + let concreteOutputCasted = output.extractValues( + as: (Substring, fieldA: Substring, fieldB: Substring).self )! checkSame(output, concreteOutputCasted) var concreteOutputCopy = concreteOutput - concreteOutputCopy = output.as()! + concreteOutputCopy = output.extractValues()! checkSame(output, concreteOutputCopy) // TODO: Regex.Match: init from tuple match and as to tuple match @@ -146,12 +146,23 @@ extension RegexTests { XCTAssertTrue(output["upper"]?.substring == "A6F1") XCTAssertTrue(output[3].substring == "Extend") XCTAssertTrue(output["desc"]?.substring == "Extend") - let typedOutput = try XCTUnwrap(output.as( - (Substring, lower: Substring, upper: Substring?, Substring).self)) + let typedOutput = try XCTUnwrap( + output.extractValues( + as: (Substring, lower: Substring, upper: Substring?, Substring).self)) XCTAssertEqual(typedOutput.0, line[...]) XCTAssertTrue(typedOutput.lower == "A6F0") XCTAssertTrue(typedOutput.upper == "A6F1") XCTAssertTrue(typedOutput.3 == "Extend") + + // Extracting as different argument labels is allowed + let typedOutput2 = try XCTUnwrap( + output.extractValues( + as: (Substring, first: Substring, Substring?, third: Substring).self)) + XCTAssertEqual(typedOutput2.0, line[...]) + XCTAssertTrue(typedOutput2.first == "A6F0") + XCTAssertTrue(typedOutput2.2 == "A6F1") + XCTAssertTrue(typedOutput2.third == "Extend") + } } } diff --git a/Tests/RegexTests/CaptureTests.swift b/Tests/RegexTests/CaptureTests.swift index ac4d8b87c..582d6ae92 100644 --- a/Tests/RegexTests/CaptureTests.swift +++ b/Tests/RegexTests/CaptureTests.swift @@ -58,13 +58,13 @@ extension CaptureList { extension AnyRegexOutput.Element { func formatStringCapture(input: String) -> String { - var res = String(repeating: "some(", count: optionalDepth) + var res = String(repeating: "some(", count: representation.optionalDepth) if let r = range { res += input[r] } else { res += "none" } - res += String(repeating: ")", count: optionalDepth) + res += String(repeating: ")", count: representation.optionalDepth) return res } } @@ -122,7 +122,7 @@ extension StringCapture { to structCap: AnyRegexOutput.Element, in input: String ) -> Bool { - guard optionalCount == structCap.optionalDepth else { + guard optionalCount == structCap.representation.optionalDepth else { return false } guard let r = structCap.range else { @@ -462,29 +462,29 @@ extension RegexTests { func testTypeVerification() throws { let opaque1 = try Regex("abc") - _ = try XCTUnwrap(opaque1.as(Substring.self)) - XCTAssertNil(opaque1.as((Substring, Substring).self)) - XCTAssertNil(opaque1.as(Int.self)) + _ = try XCTUnwrap(Regex(opaque1)) + XCTAssertNil(Regex<(Substring, Substring)>(opaque1)) + XCTAssertNil(Regex(opaque1)) let opaque2 = try Regex("(abc)") - _ = try XCTUnwrap(opaque2.as((Substring, Substring).self)) - XCTAssertNil(opaque2.as(Substring.self)) - XCTAssertNil(opaque2.as((Substring, Int).self)) + _ = try XCTUnwrap(Regex<(Substring, Substring)>(opaque2)) + XCTAssertNil(Regex(opaque2)) + XCTAssertNil(Regex<(Substring, Int)>(opaque2)) let opaque3 = try Regex("(?abc)") - _ = try XCTUnwrap(opaque3.as((Substring, someLabel: Substring).self)) - XCTAssertNil(opaque3.as((Substring, Substring).self)) - XCTAssertNil(opaque3.as(Substring.self)) + _ = try XCTUnwrap(Regex<(Substring, someLabel: Substring)>(opaque3)) + XCTAssertNil(Regex<(Substring, Substring)>(opaque3)) + XCTAssertNil(Regex(opaque3)) let opaque4 = try Regex("(?abc)?") - _ = try XCTUnwrap(opaque4.as((Substring, somethingHere: Substring?).self)) - XCTAssertNil(opaque4.as((Substring, somethignHere: Substring).self)) - XCTAssertNil(opaque4.as((Substring, Substring?).self)) + _ = try XCTUnwrap(Regex<(Substring, somethingHere: Substring?)>(opaque4)) + XCTAssertNil(Regex<(Substring, somethignHere: Substring)>(opaque4)) + XCTAssertNil(Regex<(Substring, Substring?)>(opaque4)) let opaque5 = try Regex("((a)?bc)?") - _ = try XCTUnwrap(opaque5.as((Substring, Substring?, Substring??).self)) - XCTAssertNil(opaque5.as((Substring, somethingHere: Substring?, here: Substring??).self)) - XCTAssertNil(opaque5.as((Substring, Substring?, Substring?).self)) + _ = try XCTUnwrap(Regex<(Substring, Substring?, Substring??)>(opaque5)) + XCTAssertNil(Regex<(Substring, somethingHere: Substring?, here: Substring??)>(opaque5)) + XCTAssertNil(Regex<(Substring, Substring?, Substring?)>(opaque5)) } }