Skip to content
Open
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
54 changes: 44 additions & 10 deletions Sources/_StringProcessing/ByteCodeGen+DSLList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,10 @@ fileprivate extension Compiler.ByteCodeGen {
}
switch (low, high) {
case (_, 0):
try skipNode(&list)
// `{0}` / `{0,0}` never runs the operand. Drop the body, but still
// allocate any nested captures so they remain unset (nil) rather than
// recording an empty match at the current position.
try skipNode(&list, preservingCaptures: false, allocatingCaptures: true)
return
case let (n, m?) where n > m:
// TODO: Should error out earlier, maybe DSL and parser
Expand Down Expand Up @@ -848,18 +851,25 @@ fileprivate extension Compiler.ByteCodeGen {
extension Compiler.ByteCodeGen {
mutating func skipNode(
_ list: inout ArraySlice<DSLTree.Node>,
preservingCaptures: Bool = true
preservingCaptures: Bool = true,
allocatingCaptures: Bool = false
) throws {
guard let node = list.popFirst() else { return }
switch node {
case let .orderedChoice(n):
for _ in 0..<n {
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)
}

case let .concatenation(n):
for _ in 0..<n {
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)
}

case let .capture(name, refId, _):
Expand All @@ -869,23 +879,47 @@ extension Compiler.ByteCodeGen {
if preservingCaptures {
let cap = builder.makeCapture(id: refId, name: name)
builder.buildBeginCapture(cap)
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)
builder.buildEndCapture(cap)
} else {
try skipNode(&list, preservingCaptures: preservingCaptures)
// Capture registers are numbered sequentially from the AST. A group
// that never participates still needs a register, left at the default
// unset/nil state.
if allocatingCaptures {
_ = builder.makeCapture(id: refId, name: name)
}
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)
}

case .nonCapturingGroup:
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)

case .ignoreCapturesInTypedOutput:
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)

case .limitCaptureNesting:
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)

case .quantification:
try skipNode(&list, preservingCaptures: preservingCaptures)
try skipNode(
&list,
preservingCaptures: preservingCaptures,
allocatingCaptures: allocatingCaptures)

case .customCharacterClass, .atom, .quotedLiteral, .matcher:
break
Expand Down
21 changes: 21 additions & 0 deletions Tests/RegexTests/MatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,27 @@ extension RegexTests {
("ac", [nil, "a"]),
("aab", ["aa", nil])
)

// A group quantified `{0}` / `{0,0}` never participates, so its capture
// must be nil (not an empty match). See swiftlang/swift-experimental-string-processing#902.
flatCaptureTest(
#"(a){0}b"#,
("b", [nil]),
("ab", [nil]))
flatCaptureTest(
#"(a){0,0}b"#,
("b", [nil]),
("ab", [nil]))
// Nested captures inside a never-run `{0}` body must also stay nil,
// while a later capture still gets the correct register.
flatCaptureTest(
#"((a)){0}(b)"#,
("b", [nil, nil, "b"]))
// `{0,1}` already reports nil when the group does not run.
flatCaptureTest(
#"(a){0,1}b"#,
("b", [nil]),
("ab", ["a"]))
}

func testMatchReferences() {
Expand Down