diff --git a/Sources/_StringProcessing/Engine/Backtracking.swift b/Sources/_StringProcessing/Engine/Backtracking.swift index 9078c2c19..9351edb58 100644 --- a/Sources/_StringProcessing/Engine/Backtracking.swift +++ b/Sources/_StringProcessing/Engine/Backtracking.swift @@ -11,51 +11,67 @@ extension Processor { struct SavePoint { + /// The current position in the instruction list. var pc: InstructionAddress - var pos: Position? - // Quantifiers may store a range of positions to restore to - var quantifiedRange: Range? + /// The match position to resume when a save point is restored. + enum SavedPosition { + /// A single position to resume at. + case position(Position) + + /// A range of positions still to explore, from the end of the + /// range to the start, along with the semantic mode to use + /// when consuming the range. + case range(Range, isScalarSemantics: Bool) + + /// No position to restore — only the instruction address matters. + case addressOnly + } - // FIXME: refactor, for now this field is only used for quantifier save - // points. We should try to separate out the concerns better. - var isScalarSemantics: Bool + var savedPosition: SavedPosition - // FIXME: Save minimal info (e.g. stack position and - // perhaps current start) - var captureEnds: [_StoredCapture] + // These properties store indices into the mutable `Processor.Registers` + // undo logs. On backtrack, each log is unwound down to the point saved + // here. - // The int registers store values that can be relevant to - // backtracking, such as the number of trips in a quantification. - var intRegisters: [Int] - // Same with position registers - var posRegisters: [Input.Index] + /// The length of the log of the `captures` register when this save point was created, + /// for backtracking on failure. + var captureLogEnd: UInt32 + /// The length of the log of the `ints` register when this save point was created, + /// for backtracking on failure. + var intLogEnd: UInt32 + /// The length of the log of the `positions` register when this save point was created, + /// for backtracking on failure. + var positionLogEnd: UInt32 - var destructure: ( - pc: InstructionAddress, - pos: Position?, - captureEnds: [_StoredCapture], - intRegisters: [Int], - PositionRegister: [Input.Index] - ) { - return (pc, pos, captureEnds, intRegisters, posRegisters) + /// The single position to resume at, for a save point that isn't + /// quantified or address-only. + var pos: Position? { + if case .position(let p) = savedPosition { return p } + return nil } - // Whether this save point is quantified, meaning it has a range of - // possible positions to explore. + /// Whether this save point is quantified, meaning it has a range of + /// possible positions still to explore. var isQuantified: Bool { - quantifiedRange != nil + if case .range = savedPosition { return true } + return false } - /// Move the next range position into pos, and removing it from the range - mutating func takePositionFromQuantifiedRange(_ input: Input) { - assert(isQuantified) - let range = quantifiedRange! - pos = range.upperBound - if range.isEmpty { - // Becomes a normal save point - quantifiedRange = nil - return + /// Pops the next position to try from a quantified save point's + /// range, returning it and updating the stored state so a future pop + /// (if any) tries the next-most-recent position. + /// + /// If the range is (or becomes) empty, this save point becomes a + /// normal, single-position save point for a final future pop. + mutating func popQuantifiedPosition(_ input: Input) -> Position { + guard case .range(let range, let isScalarSemantics) = savedPosition else { + fatalError("Not a quantified save point") + } + let resumeAt = range.upperBound + guard !range.isEmpty else { + savedPosition = .position(resumeAt) + return resumeAt } // Shrink the range @@ -65,7 +81,8 @@ extension Processor { } else { newUpper = input.index(before: range.upperBound) } - quantifiedRange = range.lowerBound.. SavePoint { SavePoint( pc: pc, - pos: currentPosition, - quantifiedRange: nil, - isScalarSemantics: false, - captureEnds: storedCaptures, - intRegisters: registers.ints, - posRegisters: registers.positions) + savedPosition: .position(currentPosition), + captureLogEnd: .init(asserting: registers.storedCaptures.logCount), + intLogEnd: .init(asserting: registers.ints.logCount), + positionLogEnd: .init(asserting: registers.positions.logCount)) } func makeAddressOnlySavePoint( @@ -87,12 +102,10 @@ extension Processor { ) -> SavePoint { SavePoint( pc: pc, - pos: nil, - quantifiedRange: nil, - isScalarSemantics: false, - captureEnds: storedCaptures, - intRegisters: registers.ints, - posRegisters: registers.positions) + savedPosition: .addressOnly, + captureLogEnd: .init(asserting: registers.storedCaptures.logCount), + intLogEnd: .init(asserting: registers.ints.logCount), + positionLogEnd: .init(asserting: registers.positions.logCount)) } func makeQuantifiedSavePoint( @@ -101,12 +114,10 @@ extension Processor { ) -> SavePoint { SavePoint( pc: controller.pc + 1, - pos: nil, - quantifiedRange: range, - isScalarSemantics: isScalarSemantics, - captureEnds: storedCaptures, - intRegisters: registers.ints, - posRegisters: registers.positions) + savedPosition: .range(range, isScalarSemantics: isScalarSemantics), + captureLogEnd: .init(asserting: registers.storedCaptures.logCount), + intLogEnd: .init(asserting: registers.ints.logCount), + positionLogEnd: .init(asserting: registers.positions.logCount)) } } diff --git a/Sources/_StringProcessing/Engine/MEBuilder.swift b/Sources/_StringProcessing/Engine/MEBuilder.swift index 1a26421eb..6e34b9b63 100644 --- a/Sources/_StringProcessing/Engine/MEBuilder.swift +++ b/Sources/_StringProcessing/Engine/MEBuilder.swift @@ -428,12 +428,10 @@ extension MEProgram.Builder { matcherFunctions: matcherFunctions, numInts: nextIntRegister.rawValue, numValues: nextValueRegister.rawValue, - numPositions: nextPositionRegister.rawValue + numPositions: nextPositionRegister.rawValue, + numCaptures: nextCaptureRegister.rawValue ) - let storedCaps = Array( - repeating: Processor._StoredCapture(), count: nextCaptureRegister.rawValue) - let meProgram = MEProgram( instructions: InstructionList(instructions), wholeMatchValueRegister: wholeMatchValue, @@ -443,8 +441,7 @@ extension MEProgram.Builder { referencedCaptureOffsets: referencedCaptureOffsets, initialOptions: initialOptions, canOnlyMatchAtStart: canOnlyMatchAtStart, - registers: regs, - storedCaptures: storedCaps) + registers: regs) return meProgram } diff --git a/Sources/_StringProcessing/Engine/MECapture.swift b/Sources/_StringProcessing/Engine/MECapture.swift index e18365d66..82fe5208a 100644 --- a/Sources/_StringProcessing/Engine/MECapture.swift +++ b/Sources/_StringProcessing/Engine/MECapture.swift @@ -11,25 +11,6 @@ internal import _RegexParser -/* - - TODO: Specialized data structure for all captures: - - - We want to be able to refer to COW prefixes for which - simple appends do not invalidate - - We want a compact save-point representation - - TODO: Conjectures: - - - We should be able to remove the entire capture history, - lazily recomputing it on-request from the initial stored - save point - - We should be able to keep these flat and simple, lazily - constructing structured types on-request - - */ - - extension Processor { struct _StoredCapture { var range: Range? = nil diff --git a/Sources/_StringProcessing/Engine/MEProgram.swift b/Sources/_StringProcessing/Engine/MEProgram.swift index a9df6bedd..b274d00e0 100644 --- a/Sources/_StringProcessing/Engine/MEProgram.swift +++ b/Sources/_StringProcessing/Engine/MEProgram.swift @@ -36,8 +36,6 @@ struct MEProgram { // processors can be spun up quicker (useful for running same regex // over many, many smaller inputs). var registers: Processor.Registers - var storedCaptures: [Processor._StoredCapture] - } extension MEProgram: CustomStringConvertible { @@ -45,6 +43,18 @@ extension MEProgram: CustomStringConvertible { // TODO: Re-instate better pretty-printing functionality var result = """ + Capture list: \(captureList) + Instructions: \(instructions.count) + Register counts: + ints: \(registers.ints.count) + positions: \(registers.positions.count) + values: \(registers.values.count) + elements: \(registers.elements.count) + bitsets: \(registers.bitsets.count) + consumeFunctions: \(registers.consumeFunctions.count) + transformFunctions: \(registers.transformFunctions.count) + matcherFunctions: \(registers.matcherFunctions.count) + canOnlyMatchAtStart: \(canOnlyMatchAtStart) """ // TODO: Extract into formatting code diff --git a/Sources/_StringProcessing/Engine/Processor.swift b/Sources/_StringProcessing/Engine/Processor.swift index 0bf19b829..96ec8915d 100644 --- a/Sources/_StringProcessing/Engine/Processor.swift +++ b/Sources/_StringProcessing/Engine/Processor.swift @@ -22,7 +22,9 @@ struct Controller: Equatable { var pc: InstructionAddress mutating func step() { - pc.rawValue += 1 + // `pc` is always kept in `0.. - var state: State = .inProgress var failureReason: Error? = nil @@ -125,11 +125,10 @@ extension Processor { // Initialize registers from stored starting state self.registers = program.registers - self.storedCaptures = program.storedCaptures - _checkInvariants() } + @inline(always) mutating func reset( currentPosition: Position, searchBounds: Range @@ -145,10 +144,6 @@ extension Processor { self.savePoints.removeAll(keepingCapacity: true) } - for idx in storedCaptures.indices { - storedCaptures[idx] = .init() - } - self.state = .inProgress self.failureReason = nil @@ -162,7 +157,7 @@ extension Processor { _checkInvariants() guard self.controller == Controller(pc: 0), self.savePoints.isEmpty, - self.storedCaptures.allSatisfy({ $0.range == nil }), + !self.registers.isDirty, self.state == .inProgress, self.failureReason == nil else { @@ -377,40 +372,60 @@ extension Processor { state = .fail return } - let (pc, pos, capEnds, intRegisters, posRegisters): ( - pc: InstructionAddress, - pos: Position?, - captureEnds: [_StoredCapture], - intRegisters: [Int], - PositionRegister: [Input.Index] - ) let idx = savePoints.index(before: savePoints.endIndex) - // If we have a quantifier save point, move the next range position into - // pos instead of removing it + // If we have a quantifier save point, pop the next range position + // instead of removing it + let sp: SavePoint + let resumePosition: Position? if savePoints[idx].isQuantified { - savePoints[idx].takePositionFromQuantifiedRange(input) - (pc, pos, capEnds, intRegisters, posRegisters) = savePoints[idx].destructure + resumePosition = savePoints[idx].popQuantifiedPosition(input) + sp = savePoints[idx] } else { - (pc, pos, capEnds, intRegisters, posRegisters) = savePoints.removeLast().destructure + sp = savePoints.removeLast() + resumePosition = sp.pos } - assert(capEnds.count == storedCaptures.count) + controller.pc = sp.pc + currentPosition = resumePosition ?? currentPosition - controller.pc = pc - currentPosition = pos ?? currentPosition - registers.ints = intRegisters - registers.positions = posRegisters + registers.ints.undo(to: Int(sp.intLogEnd)) + registers.positions.undo(to: Int(sp.positionLogEnd)) + registers.isDirty = true if !preservingCaptures { - // Reset all capture information - storedCaptures = capEnds + registers.storedCaptures.undo(to: Int(sp.captureLogEnd)) } + // If preserving captures, leave the capture log entries recorded since + // this save point untouched (rather than replaying or discarding them): + // `storedCaptures` keeps the values from the successful sub-match, and + // the log entries remain available so that an older, still-live save + // point can still correctly undo them on its own future backtrack. metrics.addBacktrack() } + // MARK: Capture mutation + + mutating func setCapture(_ capNum: Int, startingAt pos: Position) { + updateRegister(at: CaptureRegister(capNum)) { + $0.startCapture(pos) + } + } + + mutating func setCapture(_ capNum: Int, endingAt pos: Position) { + updateRegister(at: CaptureRegister(capNum)) { + $0.endCapture(pos) + } + } + + mutating func setCaptureValue(_ capNum: Int, _ value: Any) { + updateRegister(at: CaptureRegister(capNum)) { + $0.registerValue(value) + } + } + mutating func abort(_ e: Error? = nil) { if let e = e { self.failureReason = e @@ -459,12 +474,11 @@ extension Processor { let (imm, reg) = payload.pairedImmediateInt let int = Int(asserting: imm) assert(int == imm) - - registers[reg] = int + updateRegister(at: reg, to: int) controller.step() case .moveCurrentPosition: let reg = payload.position - registers[reg] = currentPosition + updateRegister(at: reg, to: currentPosition) controller.step() case .restorePosition: let reg = payload.position @@ -478,7 +492,9 @@ extension Processor { if registers[int] == 0 { controller.pc = addr } else { - registers[int] -= 1 + // Only reached when `registers[int] != 0`, so this can never + // underflow in practice. + updateRegister(at: int) { $0 &-= 1 } controller.step() } case .condBranchSamePosition: @@ -622,7 +638,7 @@ extension Processor { signalFailure() return } - registers[valReg] = val + updateRegister(at: valReg, to: val) resume(at: nextIdx) controller.step() } catch { @@ -634,13 +650,13 @@ extension Processor { let (isScalarMode, capture) = payload.captureAndMode let capNum = Int( asserting: capture.rawValue) - guard capNum < storedCaptures.count else { + guard capNum < registers.storedCaptures.count else { fatalError("Should this be an assert?") } // TODO: // Should we assert it's not finished yet? // What's the behavior there? - let cap = storedCaptures[capNum] + let cap = registers.storedCaptures[capture] guard let range = cap.range else { signalFailure() return @@ -652,13 +668,13 @@ extension Processor { case .beginCapture: let capNum = Int( asserting: payload.capture.rawValue) - storedCaptures[capNum].startCapture(currentPosition) + setCapture(capNum, startingAt: currentPosition) controller.step() case .endCapture: let capNum = Int( asserting: payload.capture.rawValue) - storedCaptures[capNum].endCapture(currentPosition) + setCapture(capNum, endingAt: currentPosition) controller.step() case .transformCapture: @@ -668,11 +684,11 @@ extension Processor { do { // FIXME: Pass input or the slice? - guard let value = try transform(input, storedCaptures[capNum]) else { + guard let value = try transform(input, registers.storedCaptures[cap]) else { signalFailure() return } - storedCaptures[capNum].registerValue(value) + setCaptureValue(capNum, value) controller.step() } catch { abort(error) @@ -683,7 +699,7 @@ extension Processor { let (val, cap) = payload.pairedValueCapture let value = registers[val] let capNum = Int(asserting: cap.rawValue) - storedCaptures[capNum].registerValue(value) + setCaptureValue(capNum, value) controller.step() } } diff --git a/Sources/_StringProcessing/Engine/Registers.swift b/Sources/_StringProcessing/Engine/Registers.swift index b586baecf..9aa751516 100644 --- a/Sources/_StringProcessing/Engine/Registers.swift +++ b/Sources/_StringProcessing/Engine/Registers.swift @@ -36,18 +36,21 @@ extension Processor { // Value-constructing matchers var matcherFunctions: [MEProgram.MatcherFunction] + + // MARK: writeable - // MARK: writeable, resettable + var values: UndoableArray - var isDirty = false + // MARK: writeable, resettable - // currently, useful for range-based quantification - var ints: [Int] + var ints: UndoableArray - var values: [Any] + var positions: UndoableArray - var positions: [Input.Index] + var storedCaptures: UndoableArray + var isDirty = false + init( elements: [Element], utf8Contents: [[UInt8]], @@ -55,10 +58,10 @@ extension Processor { consumeFunctions: [MEProgram.ConsumeFunction], transformFunctions: [MEProgram.TransformFunction], matcherFunctions: [MEProgram.MatcherFunction], - isDirty: Bool = false, numInts: Int, numValues: Int, - numPositions: Int + numPositions: Int, + numCaptures: Int ) { self.elements = elements self.utf8Contents = utf8Contents @@ -66,39 +69,67 @@ extension Processor { self.consumeFunctions = consumeFunctions self.transformFunctions = transformFunctions self.matcherFunctions = matcherFunctions - self.isDirty = isDirty - self.ints = Array(repeating: 0, count: numInts) - self.values = Array(repeating: SentinelValue(), count: numValues) - self.positions = Array( + self.ints = UndoableArray(repeating: 0, count: numInts) + self.values = UndoableArray(repeating: SentinelValue(), count: numValues) + self.positions = UndoableArray( repeating: Self.sentinelIndex, count: numPositions) + self.storedCaptures = UndoableArray( + repeating: Processor._StoredCapture(), count: numCaptures) } } } +extension Processor { + @inline(always) + mutating func updateRegister(at i: IntRegister, to newValue: Int) { + registers.ints.set(i, to: newValue, logging: !savePoints.isEmpty) + registers.isDirty = true + } + + @inline(always) + mutating func updateRegister(at i: IntRegister, body: (inout Int) -> ()) { + registers.ints.update(i, logging: !savePoints.isEmpty, body) + registers.isDirty = true + } + + @inline(always) + mutating func updateRegister(at i: PositionRegister, to newValue: Input.Index) { + registers.positions.set(i, to: newValue, logging: !savePoints.isEmpty) + registers.isDirty = true + } + + @inline(always) + mutating func updateRegister(at i: ValueRegister, to newValue: Any) { + registers.values.set(i, to: newValue, logging: false) + registers.isDirty = true + } + + @inline(always) + mutating func updateRegister(at i: CaptureRegister, body: (inout _StoredCapture) -> ()) { + registers.storedCaptures.update(i, logging: !savePoints.isEmpty, body) + registers.isDirty = true + } +} + extension Processor.Registers { typealias Input = String subscript(_ i: IntRegister) -> Int { - get { ints[i.rawValue] } - set { - isDirty = true - ints[i.rawValue] = newValue - } + ints[i] } + subscript(_ i: ValueRegister) -> Any { - get { values[i.rawValue] } - set { - isDirty = true - values[i.rawValue] = newValue - } + values[i] } + subscript(_ i: PositionRegister) -> Input.Index { - get { positions[i.rawValue] } - set { - isDirty = true - positions[i.rawValue] = newValue - } + positions[i] + } + + subscript(_ i: CaptureRegister) -> Processor._StoredCapture { + storedCaptures[i] } + subscript(_ i: ElementRegister) -> Input.Element { elements[i.rawValue] } @@ -127,21 +158,12 @@ extension Processor.Registers { } mutating func reset() { - guard isDirty else { - return - } - self.ints._setAll(to: 0) - self.values._setAll(to: SentinelValue()) - self.positions._setAll(to: Processor.Registers.sentinelIndex) - } -} - -// TODO: Productize into general algorithm -extension MutableCollection { - mutating func _setAll(to e: Element) { - for idx in self.indices { - self[idx] = e - } + guard isDirty else { return } + ints.reset(to: 0) + values.reset(to: SentinelValue()) + positions.reset(to: Processor.Registers.sentinelIndex) + storedCaptures.reset(to: Processor._StoredCapture()) + isDirty = false } } @@ -158,9 +180,9 @@ extension Processor.Registers: CustomStringConvertible { return """ \(formatRegisters("elements", elements))\ - \(formatRegisters("ints", ints))\ + \(formatRegisters("ints", ints.values))\ - """ + """ } } diff --git a/Sources/_StringProcessing/Engine/Tracing.swift b/Sources/_StringProcessing/Engine/Tracing.swift index b67cbb6a5..0a3fef162 100644 --- a/Sources/_StringProcessing/Engine/Tracing.swift +++ b/Sources/_StringProcessing/Engine/Tracing.swift @@ -115,17 +115,15 @@ extension Instruction: CustomStringConvertible { extension Processor.SavePoint { func describe(in input: String) -> String { let posStr: String - if let p = self.pos { + switch savedPosition { + case .position(let p): posStr = "\(input.distance(from: input.startIndex, to: p))" - } else { - if !isQuantified { - posStr = "" - } else { - let range = quantifiedRange! - let startStr = "\(input.distance(from: input.startIndex, to: range.lowerBound))" - let endStr = "\(input.distance(from: input.startIndex, to: range.upperBound))" - posStr = "\(startStr)...\(endStr)" - } + case .range(let range, _): + let startStr = "\(input.distance(from: input.startIndex, to: range.lowerBound))" + let endStr = "\(input.distance(from: input.startIndex, to: range.upperBound))" + posStr = "\(startStr)...\(endStr)" + case .addressOnly: + posStr = "" } return """ pc: \(self.pc), pos: \(posStr) diff --git a/Sources/_StringProcessing/Executor.swift b/Sources/_StringProcessing/Executor.swift index 07e058b8f..0f1d7ac8f 100644 --- a/Sources/_StringProcessing/Executor.swift +++ b/Sources/_StringProcessing/Executor.swift @@ -201,7 +201,7 @@ extension Executor { let aroElements = Executor.createExistentialElements( program, matchRange: startPosition.. where Register.RawValue == Int { + /// The current value of each register slot. + private(set) var values: [Value] + + /// The prior value of each logged mutation, most recent last. + private var log: [(slot: Register, oldValue: Value)] = [] + + init(repeating initialValue: Value, count: Int) { + self.values = Array(repeating: initialValue, count: count) + } + + subscript(_ i: Register) -> Value { + values[i.rawValue] + } + + var count: Int { values.count } + + /// The current length of the undo log. + /// + /// Save this value to later undo every mutation logged after it via + /// `undo(to:)`. + var logCount: Int { log.count } + + /// Sets the value of `i`'s register slot, recording its previous value + /// in the undo log when `logging` is true. + @inline(always) + mutating func set(_ i: Register, to newValue: Value, logging: Bool) { + if logging { + log.append((i, values[i.rawValue])) + } + values[i.rawValue] = newValue + } + + /// Mutates the value of `i`'s register slot in place, recording its + /// previous value in the undo log when `logging` is true. + @inline(always) + mutating func update( + _ i: Register, logging: Bool, _ body: (inout Value) -> Void + ) { + if logging { + log.append((i, values[i.rawValue])) + } + body(&values[i.rawValue]) + } + + /// Reverts register slots to the values they held when the undo log + /// contained `mark` entries, replaying the log in reverse. + @inline(always) + mutating func undo(to mark: Int) { + if log.count == mark { return } + let toRemove = log.count - mark + assert(toRemove > 0) + values.withUnsafeMutableBufferPointer { buffer in + var i = log.count + while i > mark { + i -= 1 + let entry = log[i] + buffer[entry.slot.rawValue] = entry.oldValue + } + } + log.removeLast(toRemove) + } + + /// Resets every register slot to `initialValue` and discards the undo + /// log. + @inline(always) + mutating func reset(to initialValue: Value) { + if !log.isEmpty { + log.removeAll(keepingCapacity: true) + } + for idx in values.indices { + values[idx] = initialValue + } + } +} diff --git a/Tests/MatchingEngineTests/UndoableArrayTests.swift b/Tests/MatchingEngineTests/UndoableArrayTests.swift new file mode 100644 index 000000000..ef81ad948 --- /dev/null +++ b/Tests/MatchingEngineTests/UndoableArrayTests.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +@testable import _StringProcessing + +class UndoableArrayTests: XCTestCase { + func testInitialState() { + let a = UndoableArray(repeating: 0, count: 4) + XCTAssertEqual(a.count, 4) + XCTAssertEqual(a.logCount, 0) + for i in 0..<4 { + XCTAssertEqual(a[IntRegister(i)], 0) + } + } + + func testSetWithoutLogging() { + var a = UndoableArray(repeating: 0, count: 4) + a.set(IntRegister(0), to: 42, logging: false) + a.set(IntRegister(1), to: 7, logging: false) + + XCTAssertEqual(a[IntRegister(0)], 42) + XCTAssertEqual(a[IntRegister(1)], 7) + // No log entries were recorded, so there's nothing to undo. + XCTAssertEqual(a.logCount, 0) + } + + func testUpdateWithoutLogging() { + var a = UndoableArray(repeating: 10, count: 2) + a.update(IntRegister(0), logging: false) { $0 += 5 } + + XCTAssertEqual(a[IntRegister(0)], 15) + XCTAssertEqual(a.logCount, 0) + } + + func testSetWithLoggingRecordsUndoEntry() { + var a = UndoableArray(repeating: 0, count: 4) + a.set(IntRegister(0), to: 1, logging: true) + XCTAssertEqual(a.logCount, 1) + a.set(IntRegister(0), to: 2, logging: true) + XCTAssertEqual(a.logCount, 2) + XCTAssertEqual(a[IntRegister(0)], 2) + } + + func testUpdateWithLoggingRecordsUndoEntry() { + var a = UndoableArray(repeating: 0, count: 4) + a.update(IntRegister(0), logging: true) { $0 += 1 } + XCTAssertEqual(a.logCount, 1) + XCTAssertEqual(a[IntRegister(0)], 1) + } + + func testUndoRevertsToMark() { + var a = UndoableArray(repeating: 0, count: 2) + + let mark0 = a.logCount + a.set(IntRegister(0), to: 1, logging: true) + let mark1 = a.logCount + a.set(IntRegister(1), to: 2, logging: true) + let mark2 = a.logCount + a.set(IntRegister(0), to: 99, logging: true) + + XCTAssertEqual(a[IntRegister(0)], 99) + XCTAssertEqual(a[IntRegister(1)], 2) + + // Undo the last mutation only. + a.undo(to: mark2) + XCTAssertEqual(a[IntRegister(0)], 1) + XCTAssertEqual(a[IntRegister(1)], 2) + XCTAssertEqual(a.logCount, mark2) + + // Undo back to after the first mutation. + a.undo(to: mark1) + XCTAssertEqual(a[IntRegister(0)], 1) + XCTAssertEqual(a[IntRegister(1)], 0) + XCTAssertEqual(a.logCount, mark1) + + // Undo everything. + a.undo(to: mark0) + XCTAssertEqual(a[IntRegister(0)], 0) + XCTAssertEqual(a[IntRegister(1)], 0) + XCTAssertEqual(a.logCount, mark0) + } + + func testUndoToCurrentMarkIsANoOp() { + var a = UndoableArray(repeating: 0, count: 2) + a.set(IntRegister(0), to: 5, logging: true) + let mark = a.logCount + a.undo(to: mark) + XCTAssertEqual(a[IntRegister(0)], 5) + XCTAssertEqual(a.logCount, mark) + } + + func testUndoWithInterleavedUnloggedMutationsRestoresLoggedValue() { + // A mutation made with `logging: false` after a logged mutation isn't + // itself undoable, but undoing back past the logged mutation should + // still restore the value it recorded. + var a = UndoableArray(repeating: 0, count: 1) + let mark = a.logCount + a.set(IntRegister(0), to: 1, logging: true) + a.set(IntRegister(0), to: 2, logging: false) + XCTAssertEqual(a[IntRegister(0)], 2) + + a.undo(to: mark) + XCTAssertEqual(a[IntRegister(0)], 0) + } + + func testUpdateBodyCanReadPriorValue() { + var a = UndoableArray(repeating: 3, count: 1) + let mark = a.logCount + a.update(IntRegister(0), logging: true) { $0 *= 10 } + XCTAssertEqual(a[IntRegister(0)], 30) + + a.undo(to: mark) + XCTAssertEqual(a[IntRegister(0)], 3) + } + + func testResetRestoresAllSlotsAndClearsLog() { + var a = UndoableArray(repeating: 0, count: 3) + a.set(IntRegister(0), to: 1, logging: true) + a.set(IntRegister(1), to: 2, logging: true) + a.set(IntRegister(2), to: 3, logging: false) + XCTAssertEqual(a.logCount, 2) + + a.reset(to: 0) + + for i in 0..<3 { + XCTAssertEqual(a[IntRegister(i)], 0) + } + XCTAssertEqual(a.logCount, 0) + } +} diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index 34fff7465..129bc7400 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2036,6 +2036,22 @@ extension RegexTests { ("bacada", nil), (":a:boco", ["a"]) // this matches only the ':a:' prefix ) + + // A capture made inside a successful lookahead/atomic group must be kept + // if that branch is the one that ultimately matches, but must NOT leak + // into the result if an outer, older backtrack abandons that branch in + // favor of a different alternative. This exercises save points nested + // inside a still-live, older save point (the outer alternation). + flatCaptureTest( + #"(?=(a+))a+b|(a)c"#, + ("ac", [nil, "a"]), + ("aab", ["aa", nil]) + ) + flatCaptureTest( + #"(?>(a+))b|(a)c"#, + ("ac", [nil, "a"]), + ("aab", ["aa", nil]) + ) } func testMatchReferences() {