Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions Sources/_StringProcessing/ByteCodeGen+DSLList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<DSLTree.Node>, 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):
Expand All @@ -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)
Expand All @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions Tests/RegexTests/CompileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions Tests/RegexTests/MatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading