From 194feb702279e052e77fca0a753025a85aaef70c Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 11 Feb 2026 12:40:54 -0600 Subject: [PATCH] Fix forward progress detection When a quantified concatenation begins with a non-forward progress guaranteed node, the forward progress checker is failing skip over the node to properly check other nodes, leading to false positives. This change resolves the issue. rdar://169999954 --- .../ByteCodeGen+DSLList.swift | 9 ++- Sources/_StringProcessing/Regex/DSLList.swift | 58 ++++++++++--------- Tests/RegexTests/CompileTests.swift | 2 + Tests/RegexTests/MatchTests.swift | 3 + 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift index 3394b319f..03f4910a2 100644 --- a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift +++ b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift @@ -391,8 +391,13 @@ fileprivate extension Compiler.ByteCodeGen { return ccc.guaranteesForwardProgress case .quantification(let amount, _, _): let (atLeast, _) = amount.ast.bounds - guard let atLeast, atLeast > 0 else { return false } - return _guaranteesForwardProgressImpl(list, position: &position) + if let atLeast, atLeast > 0 { + return _guaranteesForwardProgressImpl(list, position: &position) + } else { + list.skipNode(&position) + position += 1 + return false + } case .limitCaptureNesting, .ignoreCapturesInTypedOutput: return _guaranteesForwardProgressImpl(list, position: &position) default: return false diff --git a/Sources/_StringProcessing/Regex/DSLList.swift b/Sources/_StringProcessing/Regex/DSLList.swift index 8e53c87d1..98e478de4 100644 --- a/Sources/_StringProcessing/Regex/DSLList.swift +++ b/Sources/_StringProcessing/Regex/DSLList.swift @@ -134,37 +134,43 @@ extension DSLTree { } } -extension DSLList { - internal func skipNode(_ position: inout Int) { - guard position < nodes.count else { - return - } - switch nodes[position] { - case let .orderedChoice(children): - let n = children.count - for _ in 0.. { + internal func skipNode(_ position: inout Int) { + guard position < endIndex else { + return } - - case let .concatenation(children): - let n = children.count - for _ in 0.. Int? { switch nodes[position] { case .concatenation(let children): diff --git a/Tests/RegexTests/CompileTests.swift b/Tests/RegexTests/CompileTests.swift index d31291303..62812087a 100644 --- a/Tests/RegexTests/CompileTests.swift +++ b/Tests/RegexTests/CompileTests.swift @@ -529,6 +529,8 @@ extension RegexTests { expectProgram(for: #"(?:\w|(?#comment))+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:\w|(?#comment)(?i-i:))+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(?:\w|(?i))+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:A*(?:b|c*))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:[^/]*(?:/|$))*"#, 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 387a71d62..97532ff34 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2853,6 +2853,9 @@ extension RegexTests { expectCompletion(regex: #"(a?)*"#, in: "aa") expectCompletion(regex: #"(a{,4})*"#, in: "aa") expectCompletion(regex: #"((|)+)*"#, in: "aa") + + expectCompletion(regex: #"(?:A*(?:b|c*))*"#, in: "ABC") + expectCompletion(regex: #"^(?:(?:[^/]*(?:/|$))*)(?:[^/]*)$"#, in: "Sources/main.swift") } func testQuantifyOptimization() throws {