From d61178b418f790e69df7263290e8c30c28f71ae1 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 9 Jun 2022 16:11:36 -0500 Subject: [PATCH 1/2] Allow CustomConsuming types to match w/ zero width We previously asserted if a custom consuming type matches with zero width, but that isn't necessary or good. A custom type can implement a lookaround assertion or act as a tracer. --- .../_StringProcessing/Engine/Processor.swift | 2 +- Tests/RegexBuilderTests/RegexDSLTests.swift | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Sources/_StringProcessing/Engine/Processor.swift b/Sources/_StringProcessing/Engine/Processor.swift index 8f777ad33..5865e7a09 100644 --- a/Sources/_StringProcessing/Engine/Processor.swift +++ b/Sources/_StringProcessing/Engine/Processor.swift @@ -120,7 +120,7 @@ extension Processor { mutating func advance(to nextIndex: Input.Index) { assert(nextIndex >= bounds.lowerBound) assert(nextIndex <= bounds.upperBound) - assert(nextIndex > currentPosition) + assert(nextIndex >= currentPosition) currentPosition = nextIndex } diff --git a/Tests/RegexBuilderTests/RegexDSLTests.swift b/Tests/RegexBuilderTests/RegexDSLTests.swift index f325b579f..c1aa996f6 100644 --- a/Tests/RegexBuilderTests/RegexDSLTests.swift +++ b/Tests/RegexBuilderTests/RegexDSLTests.swift @@ -950,6 +950,42 @@ class RegexDSLTests: XCTestCase { XCTAssertEqual(str.wholeMatch(of: parser)?.output, version) } } + + func testZeroWidthConsumer() throws { + struct Trace: CustomConsumingRegexComponent { + typealias RegexOutput = Void + var label: String + init(_ label: String) { self.label = label } + + static var traceOutput = "" + + func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Void)? { + print("Matching '\(label)'", to: &Self.traceOutput) + print(input, to: &Self.traceOutput) + let dist = input.distance(from: input.startIndex, to: index) + print(String(repeating: " ", count: dist) + "^", to: &Self.traceOutput) + return (index, ()) + } + } + + let regex = Regex { + OneOrMore(.word) + Trace("end of key") + ":" + Trace("start of value") + OneOrMore(.word) + } + XCTAssertNotNil("hello:goodbye".firstMatch(of: regex)) + XCTAssertEqual(Trace.traceOutput, """ + Matching 'end of key' + hello:goodbye + ^ + Matching 'start of value' + hello:goodbye + ^ + + """) + } } extension Unicode.Scalar { From 5a981ec541546822e2baa2eaf643db86f8872ae9 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 16 Jun 2022 20:17:51 -0500 Subject: [PATCH 2/2] Rename Processor.advance(to:) to resume(at:) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the given index doesn’t need to advance, this name is less misleading. --- .../_StringProcessing/Engine/Processor.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/_StringProcessing/Engine/Processor.swift b/Sources/_StringProcessing/Engine/Processor.swift index 85cd27206..d6c1306f0 100644 --- a/Sources/_StringProcessing/Engine/Processor.swift +++ b/Sources/_StringProcessing/Engine/Processor.swift @@ -117,11 +117,15 @@ extension Processor { return true } - mutating func advance(to nextIndex: Input.Index) { - assert(nextIndex >= bounds.lowerBound) - assert(nextIndex <= bounds.upperBound) - assert(nextIndex >= currentPosition) - currentPosition = nextIndex + /// Continue matching at the specified index. + /// + /// - Precondition: `bounds.contains(index) || index == bounds.upperBound` + /// - Precondition: `index >= currentPosition` + mutating func resume(at index: Input.Index) { + assert(index >= bounds.lowerBound) + assert(index <= bounds.upperBound) + assert(index >= currentPosition) + currentPosition = index } func doPrint(_ s: String) { @@ -358,7 +362,7 @@ extension Processor { signalFailure() return } - advance(to: nextIndex) + resume(at: nextIndex) controller.step() case .assertBy: @@ -386,7 +390,7 @@ extension Processor { return } registers[valReg] = val - advance(to: nextIdx) + resume(at: nextIdx) controller.step() } catch { abort(error)