From 7443f985b14f4d7e48ce4ea30d2ab55542d4508a Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 17 Sep 2025 11:47:37 -0500 Subject: [PATCH 1/2] Include optionality in AnyRegexOutput.Element.type When reconstructing the type of a captured element for access through the `AnyRegexOutput.Element.type` API, any optionality is being omitted (e.g. `Int?` is returned as `Int`). This is both observable through that API and results in a bug when accessing part of a match's output through the dynamic member subscript (e.g. `match.1`). In that case, the lack of optionality causes an incorrect calculation of the output tuple member's position, resulting in the wrong member being loaded and returned. This change adds the missing optionality to the type. Fixes https://github.com/swiftlang/swift/issues/83022 --- .../Regex/AnyRegexOutput.swift | 9 +++- Tests/RegexBuilderTests/RegexDSLTests.swift | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift index 0a76f2d86..ae8193804 100644 --- a/Sources/_StringProcessing/Regex/AnyRegexOutput.swift +++ b/Sources/_StringProcessing/Regex/AnyRegexOutput.swift @@ -381,7 +381,12 @@ extension AnyRegexOutput.ElementRepresentation { } var type: Any.Type { - content?.value.map { Swift.type(of: $0) } - ?? TypeConstruction.optionalType(of: Substring.self, depth: optionalDepth) + func wrapIfNecessary(_: U.Type) -> Any.Type { + TypeConstruction.optionalType(of: U.self, depth: optionalDepth) + } + + return content?.value.map { + _openExistential(Swift.type(of: $0), do: wrapIfNecessary) + } ?? TypeConstruction.optionalType(of: Substring.self, depth: optionalDepth) } } diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index c6f5fdaf5..507e893de 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -1962,6 +1962,59 @@ extension RegexDSLTests { XCTAssertNotNil(clip.contains(pattern)) XCTAssertNotNil(clip2.contains(pattern)) } + + func testIssue83022() throws { + // Original report from https://github.com/swiftlang/swift/issues/83022 + // rdar://155710126 + let mixedNumberRegex = Regex { + // whole number + Optionally { + Capture { + OneOrMore(.digit) + } transform: { Int($0)! } + OneOrMore { " " } + } + // numerator + Capture { + OneOrMore(.digit) + } transform: { Int($0)! } + "/" + // denominator + Capture { + OneOrMore(.digit) + } transform: { Int($0)! } + } + + do { + let match = try XCTUnwrap(mixedNumberRegex.wholeMatch(in: "1 3/4")) + XCTAssertEqual(match.1, 1) + XCTAssertEqual(match.2, 3) + XCTAssertEqual(match.3, 4) + + let erasedMatch = Regex.Match(match) + XCTAssert(erasedMatch.output[0].type == Substring.self) + XCTAssert(erasedMatch.output[1].type == Int?.self) + XCTAssert(erasedMatch.output[2].type == Int.self) + XCTAssert(erasedMatch.output[3].type == Int.self) + } + + do { + let match = try XCTUnwrap(mixedNumberRegex.wholeMatch(in: "3/4")) + XCTAssertNil(match.1) + XCTAssertEqual(match.2, 3) + XCTAssertEqual(match.3, 4) + + let erasedMatch = Regex.Match(match) + XCTAssert(erasedMatch.output[0].type == Substring.self) + XCTAssert(erasedMatch.output[2].type == Int.self) + XCTAssert(erasedMatch.output[3].type == Int.self) + + XCTExpectFailure { + // `nil` value is interpreted as `Substring?` instead of `Int?` + XCTAssert(erasedMatch.output[1].type == Int?.self) + } + } + } } extension Unicode.Scalar { From 07994b7aaf89ba256028a6cf198826b4efd24b83 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 25 Sep 2025 22:14:31 -0500 Subject: [PATCH 2/2] Update test to include double Optional --- Tests/RegexBuilderTests/RegexDSLTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index 507e893de..3611ee2c0 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -1979,10 +1979,10 @@ extension RegexDSLTests { OneOrMore(.digit) } transform: { Int($0)! } "/" - // denominator + // denominator (modified to test for double optional) Capture { OneOrMore(.digit) - } transform: { Int($0)! } + } transform: { Optional.some(Int($0)) } } do { @@ -1995,7 +1995,7 @@ extension RegexDSLTests { XCTAssert(erasedMatch.output[0].type == Substring.self) XCTAssert(erasedMatch.output[1].type == Int?.self) XCTAssert(erasedMatch.output[2].type == Int.self) - XCTAssert(erasedMatch.output[3].type == Int.self) + XCTAssert(erasedMatch.output[3].type == Int??.self) } do { @@ -2007,7 +2007,7 @@ extension RegexDSLTests { let erasedMatch = Regex.Match(match) XCTAssert(erasedMatch.output[0].type == Substring.self) XCTAssert(erasedMatch.output[2].type == Int.self) - XCTAssert(erasedMatch.output[3].type == Int.self) + XCTAssert(erasedMatch.output[3].type == Int??.self) XCTExpectFailure { // `nil` value is interpreted as `Substring?` instead of `Int?`