From 67c56912966160e748ebbc2795f57a91103d73d5 Mon Sep 17 00:00:00 2001 From: Evangelos Kassos Date: Tue, 21 Jul 2026 13:26:36 -0400 Subject: [PATCH 1/2] Fix forward progress detection for lookaround groups (#866) When a quantified concatenation begins with a lookaround group, the forward progress checker fails to skip over the assertion's child subtree, leaving the analysis cursor misaligned and leading to false positives. This change skips the lookaround's child subtree, matching the treatment added for nullable quantifications in #851. --- Sources/_StringProcessing/ByteCodeGen+DSLList.swift | 2 ++ Tests/RegexTests/CompileTests.swift | 1 + Tests/RegexTests/MatchTests.swift | 1 + 3 files changed, 4 insertions(+) diff --git a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift index 4abf2b90e..4d1290cd0 100644 --- a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift +++ b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift @@ -369,6 +369,8 @@ fileprivate extension Compiler.ByteCodeGen { case .nonCapturingGroup(let kind): switch kind.ast { case .lookahead, .negativeLookahead, .lookbehind, .negativeLookbehind: + list.skipNode(&position) + position += 1 return false default: return _guaranteesForwardProgressImpl(list, position: &position) diff --git a/Tests/RegexTests/CompileTests.swift b/Tests/RegexTests/CompileTests.swift index 62812087a..50cd675b4 100644 --- a/Tests/RegexTests/CompileTests.swift +++ b/Tests/RegexTests/CompileTests.swift @@ -531,6 +531,7 @@ extension RegexTests { expectProgram(for: #"(?:\w|(?i))+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:A*(?:b|c*))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:[^/]*(?:/|$))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:(?!a)\d*)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) // Bounded quantification, don't emit position checking expectProgram(for: #"(?:(?=a)){1,4}"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index 97532ff34..e1cfb298b 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2856,6 +2856,7 @@ extension RegexTests { expectCompletion(regex: #"(?:A*(?:b|c*))*"#, in: "ABC") expectCompletion(regex: #"^(?:(?:[^/]*(?:/|$))*)(?:[^/]*)$"#, in: "Sources/main.swift") + expectCompletion(regex: #"(?:(?!a)\d*)*"#, in: "A") } func testQuantifyOptimization() throws { From 3403183df44ef6dfe06ba600b6cbebab414e98b8 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 21 Jul 2026 13:36:25 -0500 Subject: [PATCH 2/2] Add'l testing for lookahead forward prog detection (#867) --- Tests/RegexTests/CompileTests.swift | 2 ++ Tests/RegexTests/MatchTests.swift | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Tests/RegexTests/CompileTests.swift b/Tests/RegexTests/CompileTests.swift index 50cd675b4..1d967c7ed 100644 --- a/Tests/RegexTests/CompileTests.swift +++ b/Tests/RegexTests/CompileTests.swift @@ -531,7 +531,9 @@ extension RegexTests { expectProgram(for: #"(?:\w|(?i))+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:A*(?:b|c*))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:[^/]*(?:/|$))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:(?=a)\d*)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:(?!a)\d*)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:(?!\d)[0-9]*)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) // Bounded quantification, don't emit position checking expectProgram(for: #"(?:(?=a)){1,4}"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index e1cfb298b..34fff7465 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2828,7 +2828,12 @@ extension RegexTests { } } - func expectCompletion(regex: String, in target: String) { + func expectCompletion( + regex: String, + in target: String, + file: StaticString = #file, + line: UInt = #line + ) { let expectation = XCTestExpectation(description: "Run the given regex to completion") Task.init { let r = try! Regex(regex) @@ -2836,7 +2841,14 @@ extension RegexTests { expectation.fulfill() return val } - wait(for: [expectation], timeout: 3.0) + let result = XCTWaiter().wait(for: [expectation], timeout: 3.0) + if result != .completed { + XCTFail(""" + Matching didn't complete within 3 seconds: + regex: \(regex) + input: \(target) + """, file: file, line: line) + } } func testQuantificationForwardProgress() { @@ -2856,7 +2868,12 @@ extension RegexTests { expectCompletion(regex: #"(?:A*(?:b|c*))*"#, in: "ABC") expectCompletion(regex: #"^(?:(?:[^/]*(?:/|$))*)(?:[^/]*)$"#, in: "Sources/main.swift") - expectCompletion(regex: #"(?:(?!a)\d*)*"#, in: "A") + + // Repeated lookahead + expectCompletion(regex: #"(?:(?=\w)\d*)*"#, in: "a") + expectCompletion(regex: #"(?:(?=\d)\w*)*"#, in: "a") + expectCompletion(regex: #"(?:(?!b)\d*)*"#, in: "a") + expectCompletion(regex: #"(?:(?!\d)[0-9]*)*"#, in: "a") } func testQuantifyOptimization() throws {