RegexBuilder infinite loop when nullable capture starts with NegativeLookahead

In Swift 6.4 or later, a RegexBuilder pattern can hang when an unbounded quantifier repeats a body that can match the empty string, where that body begins with NegativeLookahead.

I've opened a corresponding issue and PR to resolve the issue in swift-experimental-string-processing. See below for a reproduction and a workaround.

The regression affects apps running on OS 27 built with Xcode 27, which includes Swift 6.4. Running apps built with Xcode 27 on OS 26 or earlier demonstrates the expected behavior.

Links:

Reproduction

In the reducer below, matching "A" repeatedly invokes the capture transform with an empty substring without advancing through the input.

import RegexBuilder

let regex = Regex {
    ZeroOrMore {
        Capture {
            NegativeLookahead { "a" }
            ZeroOrMore(.digit)
        } transform: { String($0) }   // invoked repeatedly with ""
    }
}

_ = "A".matches(of: regex)   // never returns

Reduced string form:

_ = try! Regex(#"(?:(?!a)\d*)*"#).firstMatch(in: "A")   // never returns

The issue is in the same forward-progress class as PR #851, which skips a nullable quantification's child subtree. Lookaround groups need the same treatment.

The regression first appears in Swift 6.4-dev toolchains. I observed the issue in code running on iOS 27 beta 1 (24A5355q), then traced the regression to PR #849 in swift-experimental-string-processing.

Workaround

In the meantime, wrap the capture contents in Optionally { }:

import RegexBuilder

let digits = Regex {
    NegativeLookahead { "a" }
    ZeroOrMore(.digit)
}

let regex = Regex {
    ZeroOrMore {
        Capture {
            Optionally {
                digits
            }
        } transform: { String($0) }
    }
}

_ = "A".matches(of: regex)
RegexBuilder infinite loop when nullable capture starts with NegativeLookahead
 
 
Q