diff --git a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift index 4d1290cd0..f11f1f9a7 100644 --- a/Sources/_StringProcessing/ByteCodeGen+DSLList.swift +++ b/Sources/_StringProcessing/ByteCodeGen+DSLList.swift @@ -351,9 +351,20 @@ fileprivate extension Compiler.ByteCodeGen { } } + /// Returns whether the node at `position` is guaranteed to consume input, + /// leaving `position` just past that node's entire subtree, so that a caller + /// walking a list of nodes reads the next sibling. + /// + /// Some of the cases below stop looking at children as soon as the answer is + /// known, and others don't look at them at all, so the end of the subtree is + /// found up front rather than left to the traversal to reach. func _guaranteesForwardProgressImpl(_ list: ArraySlice, position: inout Int) -> Bool { guard position < list.endIndex else { return false } let node = list[position] + var endOfSubtree = position + list.skipNode(&endOfSubtree) + endOfSubtree += 1 + defer { position = endOfSubtree } position += 1 switch node { case .orderedChoice(let count): @@ -369,8 +380,6 @@ 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) @@ -396,8 +405,6 @@ fileprivate extension Compiler.ByteCodeGen { if let atLeast, atLeast > 0 { return _guaranteesForwardProgressImpl(list, position: &position) } else { - list.skipNode(&position) - position += 1 return false } case .limitCaptureNesting, .ignoreCapturesInTypedOutput: diff --git a/Tests/RegexTests/CompileTests.swift b/Tests/RegexTests/CompileTests.swift index 0879a875e..6cc872597 100644 --- a/Tests/RegexTests/CompileTests.swift +++ b/Tests/RegexTests/CompileTests.swift @@ -556,6 +556,24 @@ extension RegexTests { // Inner node is a quantification that guarantees forward progress expectProgram(for: #"(a+)*"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) expectProgram(for: #"(a{1,})*"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) + + // An empty alternative next to one that is a concatenation of more than + // one node. Deciding the concatenation takes only its first child, so the + // walk has to skip the rest of it to find the empty alternative. + // https://github.com/swiftlang/swift-experimental-string-processing/issues/897 + expectProgram(for: #"(?:.c|)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:\sA|)*?"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:[ab]c|)*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + // https://github.com/swiftlang/swift-experimental-string-processing/issues/894 + expectProgram(for: #"(?:.b|)+"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) + // Every alternative progresses, so no check is needed even though one of + // them is a concatenation. + expectProgram(for: #"(?:.c|b)*"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:.c|b|[de]f)*"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) + // A concatenation progresses as soon as one of its children does, wherever + // in the concatenation that child is. + expectProgram(for: #"(?:(?=a)\b.)*"#, doesNotContain: [.moveCurrentPosition, .condBranchSamePosition]) + expectProgram(for: #"(?:(?=a)\b(?:))*"#, contains: [.moveCurrentPosition, .condBranchSamePosition]) } func testCanOnlyMatchAtStart() throws { diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index f492c345d..7a9d9e704 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2917,6 +2917,32 @@ extension RegexTests { expectCompletion(regex: #"(?:(?=\d)\w*)*"#, in: "a") expectCompletion(regex: #"(?:(?!b)\d*)*"#, in: "a") expectCompletion(regex: #"(?:(?!\d)[0-9]*)*"#, in: "a") + + // An empty alternative next to one that is a concatenation of more than + // one node. + // https://github.com/swiftlang/swift-experimental-string-processing/issues/897 + expectCompletion(regex: #"(?:.c|)*x"#, in: "b") + expectCompletion(regex: #"(?:\sA|)*?x"#, in: "b") + expectCompletion(regex: #"(?:[ab]c|)*x"#, in: "b") + // https://github.com/swiftlang/swift-experimental-string-processing/issues/894 + expectCompletion(regex: #"(?:.b|)+."#, in: "a") + } + + func testEmptyAlternativeInQuantification() { + // https://github.com/swiftlang/swift-experimental-string-processing/issues/897 + firstMatchTests( + #"(?:.c|)*x"#, + ("b", nil), ("x", "x"), ("bcx", "bcx"), ("acbcx", "acbcx")) + firstMatchTests( + #"(?:\sA|)*?x"#, + ("b", nil), ("x", "x"), (" Ax", " Ax")) + firstMatchTests( + #"(?:[ab]c|)*x"#, + ("b", nil), ("acbcx", "acbcx")) + // https://github.com/swiftlang/swift-experimental-string-processing/issues/894 + firstMatchTests( + #"(?:.b|)+."#, + ("a", "a"), ("ab", "a"), ("abb", "abb")) } func testQuantifyOptimization() throws {