Skip to content

Commit e0352a2

Browse files
authored
Fix range-based quantification fast path (#653)
The fast path for quantification incorrectly discards the last save position when the quantification used up all possible trips, which is only possible with range-based quantifications (e.g. `{0,3}`). This bug shows up when a range-based quantifier matches the maximum - 1 repetitions of the preceding pattern. For example, the regex `/a{0,2}a/` should succeed as a full match any of the strings "aa", "aaa", or "aaaa". However, the pattern fails to match "aaa", since the save point allowing a single "a" to match the first `a{0,2}` part of the regex is discarded. This change only discards the last save position when advancing the quantifier fails due to a failure to match, not maxing out the number of trips.
1 parent e01e43d commit e0352a2

File tree

2 files changed

+41
-7
lines changed

2 files changed

+41
-7
lines changed

Sources/_StringProcessing/Engine/MEQuantify.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ extension Processor {
4040
}
4141
}
4242
let next = _doQuantifyMatch(payload)
43-
guard let idx = next else { break }
43+
guard let idx = next else {
44+
if !savePoint.rangeIsEmpty {
45+
// The last save point has saved the current, non-matching position,
46+
// so it's unneeded.
47+
savePoint.shrinkRange(input)
48+
}
49+
break
50+
}
4451
currentPosition = idx
4552
trips += 1
4653
}
@@ -50,12 +57,8 @@ extension Processor {
5057
return false
5158
}
5259

53-
if payload.quantKind == .eager && !savePoint.rangeIsEmpty {
54-
// The last save point has saved the current position, so it's unneeded
55-
savePoint.shrinkRange(input)
56-
if !savePoint.rangeIsEmpty {
57-
savePoints.append(savePoint)
58-
}
60+
if !savePoint.rangeIsEmpty {
61+
savePoints.append(savePoint)
5962
}
6063
return true
6164
}

Tests/RegexTests/MatchTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2574,4 +2574,35 @@ extension RegexTests {
25742574
func testFuzzerArtifacts() throws {
25752575
expectCompletion(regex: #"(b?)\1*"#, in: "a")
25762576
}
2577+
2578+
func testIssue640() throws {
2579+
// Original report from https://github.com/apple/swift-experimental-string-processing/issues/640
2580+
let original = try Regex("[1-9][0-9]{0,2}(?:,?[0-9]{3})*")
2581+
XCTAssertNotNil("36,769".wholeMatch(of: original))
2582+
XCTAssertNotNil("36769".wholeMatch(of: original))
2583+
2584+
// Simplified case
2585+
let simplified = try Regex("a{0,2}a")
2586+
XCTAssertNotNil("aaa".wholeMatch(of: simplified))
2587+
2588+
for max in 1...8 {
2589+
let patternEager = "a{0,\(max)}a"
2590+
let regexEager = try Regex(patternEager)
2591+
let patternReluctant = "a{0,\(max)}?a"
2592+
let regexReluctant = try Regex(patternReluctant)
2593+
for length in 1...(max + 1) {
2594+
let str = String(repeating: "a", count: length)
2595+
if str.wholeMatch(of: regexEager) == nil {
2596+
XCTFail("Didn't match '\(patternEager)' in '\(str)' (\(max),\(length)).")
2597+
}
2598+
if str.wholeMatch(of: regexReluctant) == nil {
2599+
XCTFail("Didn't match '\(patternReluctant)' in '\(str)' (\(max),\(length)).")
2600+
}
2601+
}
2602+
2603+
let possessiveRegex = try Regex("a{0,\(max)}+a")
2604+
let str = String(repeating: "a", count: max + 1)
2605+
XCTAssertNotNil(str.wholeMatch(of: possessiveRegex))
2606+
}
2607+
}
25772608
}

0 commit comments

Comments
 (0)