From 4f283628ec85e9408c3fcd42d15032ec8bf3b859 Mon Sep 17 00:00:00 2001 From: Anthony Latsis Date: Wed, 30 Sep 2026 01:12:55 +0100 Subject: [PATCH] Revert "[un-revert] Replace per-save-point array snapshots with an undo trail (#881)" This reverts commit 2d0b2db77af2522c67ef43172199459d5af16aa9. The commit broke Windows PR testing. --- .../Engine/Backtracking.swift | 123 +++++------ .../_StringProcessing/Engine/MEBuilder.swift | 9 +- .../_StringProcessing/Engine/MECapture.swift | 19 ++ .../_StringProcessing/Engine/MEProgram.swift | 14 +- .../_StringProcessing/Engine/Processor.swift | 114 ++++------ .../_StringProcessing/Engine/Registers.swift | 120 ++++------- .../_StringProcessing/Engine/Tracing.swift | 18 +- Sources/_StringProcessing/Executor.swift | 2 +- .../Utility/UndoableArray.swift | 115 ---------- .../UndoableArrayTests.swift | 198 ------------------ Tests/RegexTests/MatchTests.swift | 16 -- 11 files changed, 174 insertions(+), 574 deletions(-) delete mode 100644 Sources/_StringProcessing/Utility/UndoableArray.swift delete mode 100644 Tests/MatchingEngineTests/UndoableArrayTests.swift diff --git a/Sources/_StringProcessing/Engine/Backtracking.swift b/Sources/_StringProcessing/Engine/Backtracking.swift index 78ce21248..9078c2c19 100644 --- a/Sources/_StringProcessing/Engine/Backtracking.swift +++ b/Sources/_StringProcessing/Engine/Backtracking.swift @@ -9,75 +9,53 @@ // //===----------------------------------------------------------------------===// -internal import _RegexParser - extension Processor { struct SavePoint { - /// The current position in the instruction list. var pc: InstructionAddress + var pos: Position? - /// 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 - } + // Quantifiers may store a range of positions to restore to + var quantifiedRange: Range? - var savedPosition: SavedPosition + // 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 - // These properties store indices into the mutable `Processor.Registers` - // undo logs. On backtrack, each log is unwound down to the point saved - // here. - // - // NOTE: These are `UInt32` to keep `SavePoint` small. A log can only fill - // up if a single match attempt has 2^32 register mutations without - // clearing the registers, beyond what would already fill up memory. + // FIXME: Save minimal info (e.g. stack position and + // perhaps current start) + var captureEnds: [_StoredCapture] - /// 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 + // 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 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 + var destructure: ( + pc: InstructionAddress, + pos: Position?, + captureEnds: [_StoredCapture], + intRegisters: [Int], + PositionRegister: [Input.Index] + ) { + return (pc, pos, captureEnds, intRegisters, posRegisters) } - /// Whether this save point is quantified, meaning it has a range of - /// possible positions still to explore. + // Whether this save point is quantified, meaning it has a range of + // possible positions to explore. var isQuantified: Bool { - if case .range = savedPosition { return true } - return false + quantifiedRange != nil } - /// 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 + /// 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 } // Shrink the range @@ -87,8 +65,7 @@ extension Processor { } else { newUpper = input.index(before: range.upperBound) } - savedPosition = .range(range.lowerBound.. SavePoint { SavePoint( pc: pc, - savedPosition: .position(currentPosition), - captureLogEnd: .init(asserting: registers.storedCaptures.logCount), - intLogEnd: .init(asserting: registers.ints.logCount), - positionLogEnd: .init(asserting: registers.positions.logCount)) + pos: currentPosition, + quantifiedRange: nil, + isScalarSemantics: false, + captureEnds: storedCaptures, + intRegisters: registers.ints, + posRegisters: registers.positions) } func makeAddressOnlySavePoint( @@ -108,10 +87,12 @@ extension Processor { ) -> SavePoint { SavePoint( pc: pc, - savedPosition: .addressOnly, - captureLogEnd: .init(asserting: registers.storedCaptures.logCount), - intLogEnd: .init(asserting: registers.ints.logCount), - positionLogEnd: .init(asserting: registers.positions.logCount)) + pos: nil, + quantifiedRange: nil, + isScalarSemantics: false, + captureEnds: storedCaptures, + intRegisters: registers.ints, + posRegisters: registers.positions) } func makeQuantifiedSavePoint( @@ -120,10 +101,12 @@ extension Processor { ) -> SavePoint { SavePoint( pc: controller.pc + 1, - savedPosition: .range(range, isScalarSemantics: isScalarSemantics), - captureLogEnd: .init(asserting: registers.storedCaptures.logCount), - intLogEnd: .init(asserting: registers.ints.logCount), - positionLogEnd: .init(asserting: registers.positions.logCount)) + pos: nil, + quantifiedRange: range, + isScalarSemantics: isScalarSemantics, + captureEnds: storedCaptures, + intRegisters: registers.ints, + posRegisters: registers.positions) } } diff --git a/Sources/_StringProcessing/Engine/MEBuilder.swift b/Sources/_StringProcessing/Engine/MEBuilder.swift index 6e34b9b63..1a26421eb 100644 --- a/Sources/_StringProcessing/Engine/MEBuilder.swift +++ b/Sources/_StringProcessing/Engine/MEBuilder.swift @@ -428,10 +428,12 @@ extension MEProgram.Builder { matcherFunctions: matcherFunctions, numInts: nextIntRegister.rawValue, numValues: nextValueRegister.rawValue, - numPositions: nextPositionRegister.rawValue, - numCaptures: nextCaptureRegister.rawValue + numPositions: nextPositionRegister.rawValue ) + let storedCaps = Array( + repeating: Processor._StoredCapture(), count: nextCaptureRegister.rawValue) + let meProgram = MEProgram( instructions: InstructionList(instructions), wholeMatchValueRegister: wholeMatchValue, @@ -441,7 +443,8 @@ extension MEProgram.Builder { referencedCaptureOffsets: referencedCaptureOffsets, initialOptions: initialOptions, canOnlyMatchAtStart: canOnlyMatchAtStart, - registers: regs) + registers: regs, + storedCaptures: storedCaps) return meProgram } diff --git a/Sources/_StringProcessing/Engine/MECapture.swift b/Sources/_StringProcessing/Engine/MECapture.swift index 82fe5208a..e18365d66 100644 --- a/Sources/_StringProcessing/Engine/MECapture.swift +++ b/Sources/_StringProcessing/Engine/MECapture.swift @@ -11,6 +11,25 @@ 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 b274d00e0..a9df6bedd 100644 --- a/Sources/_StringProcessing/Engine/MEProgram.swift +++ b/Sources/_StringProcessing/Engine/MEProgram.swift @@ -36,6 +36,8 @@ 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 { @@ -43,18 +45,6 @@ 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 049ecaf23..0bf19b829 100644 --- a/Sources/_StringProcessing/Engine/Processor.swift +++ b/Sources/_StringProcessing/Engine/Processor.swift @@ -22,9 +22,7 @@ struct Controller: Equatable { var pc: InstructionAddress mutating func step() { - // `pc` is always kept in `0.. + var state: State = .inProgress var failureReason: Error? = nil @@ -125,10 +125,11 @@ 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 @@ -144,6 +145,10 @@ extension Processor { self.savePoints.removeAll(keepingCapacity: true) } + for idx in storedCaptures.indices { + storedCaptures[idx] = .init() + } + self.state = .inProgress self.failureReason = nil @@ -157,7 +162,7 @@ extension Processor { _checkInvariants() guard self.controller == Controller(pc: 0), self.savePoints.isEmpty, - !self.registers.isDirty, + self.storedCaptures.allSatisfy({ $0.range == nil }), self.state == .inProgress, self.failureReason == nil else { @@ -372,74 +377,40 @@ 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, pop the next range position - // instead of removing it - let sp: SavePoint - let resumePosition: Position? + // If we have a quantifier save point, move the next range position into + // pos instead of removing it if savePoints[idx].isQuantified { - resumePosition = savePoints[idx].popQuantifiedPosition(input) - sp = savePoints[idx] + savePoints[idx].takePositionFromQuantifiedRange(input) + (pc, pos, capEnds, intRegisters, posRegisters) = savePoints[idx].destructure } else { - sp = savePoints.removeLast() - resumePosition = sp.pos + (pc, pos, capEnds, intRegisters, posRegisters) = savePoints.removeLast().destructure } - controller.pc = sp.pc - currentPosition = resumePosition ?? currentPosition + assert(capEnds.count == storedCaptures.count) - // Note: no need to mark the registers dirty here. `undo` only rewrites - // slots that were previously logged, and logging a slot goes through - // `updateRegister`, which already set `isDirty`. - registers.ints.undo(to: Int(sp.intLogEnd)) - registers.positions.undo(to: Int(sp.positionLogEnd)) + controller.pc = pc + currentPosition = pos ?? currentPosition + registers.ints = intRegisters + registers.positions = posRegisters if !preservingCaptures { - registers.storedCaptures.undo(to: Int(sp.captureLogEnd)) - } else { - // 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. - // Once there are no save points left, though, they're unreachable. - discardUnreachableUndoLogs() + // Reset all capture information + storedCaptures = capEnds } metrics.addBacktrack() } - /// Drops the register undo logs once there are no save points left, since - /// nothing can backtrack into them any more. - @inline(__always) - mutating func discardUnreachableUndoLogs() { - if savePoints.isEmpty { - registers.discardUndoLogs() - } - } - - // 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 @@ -464,7 +435,6 @@ extension Processor { mutating func clearThrough(_ address: InstructionAddress) { while let sp = savePoints.popLast() { if sp.pc == address { - discardUnreachableUndoLogs() controller.step() return } @@ -489,11 +459,12 @@ extension Processor { let (imm, reg) = payload.pairedImmediateInt let int = Int(asserting: imm) assert(int == imm) - updateRegister(at: reg, to: int) + + registers[reg] = int controller.step() case .moveCurrentPosition: let reg = payload.position - updateRegister(at: reg, to: currentPosition) + registers[reg] = currentPosition controller.step() case .restorePosition: let reg = payload.position @@ -507,9 +478,7 @@ extension Processor { if registers[int] == 0 { controller.pc = addr } else { - // Only reached when `registers[int] != 0`, so this can never - // underflow in practice. - updateRegister(at: int) { $0 &-= 1 } + registers[int] -= 1 controller.step() } case .condBranchSamePosition: @@ -539,7 +508,6 @@ extension Processor { case .clear: if let _ = savePoints.popLast() { - discardUnreachableUndoLogs() controller.step() } else { // TODO: What should we do here? @@ -654,7 +622,7 @@ extension Processor { signalFailure() return } - updateRegister(at: valReg, to: val) + registers[valReg] = val resume(at: nextIdx) controller.step() } catch { @@ -666,13 +634,13 @@ extension Processor { let (isScalarMode, capture) = payload.captureAndMode let capNum = Int( asserting: capture.rawValue) - guard capNum < registers.storedCaptures.count else { + guard capNum < 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 = registers.storedCaptures[capture] + let cap = storedCaptures[capNum] guard let range = cap.range else { signalFailure() return @@ -684,13 +652,13 @@ extension Processor { case .beginCapture: let capNum = Int( asserting: payload.capture.rawValue) - setCapture(capNum, startingAt: currentPosition) + storedCaptures[capNum].startCapture(currentPosition) controller.step() case .endCapture: let capNum = Int( asserting: payload.capture.rawValue) - setCapture(capNum, endingAt: currentPosition) + storedCaptures[capNum].endCapture(currentPosition) controller.step() case .transformCapture: @@ -700,11 +668,11 @@ extension Processor { do { // FIXME: Pass input or the slice? - guard let value = try transform(input, registers.storedCaptures[cap]) else { + guard let value = try transform(input, storedCaptures[capNum]) else { signalFailure() return } - setCaptureValue(capNum, value) + storedCaptures[capNum].registerValue(value) controller.step() } catch { abort(error) @@ -715,7 +683,7 @@ extension Processor { let (val, cap) = payload.pairedValueCapture let value = registers[val] let capNum = Int(asserting: cap.rawValue) - setCaptureValue(capNum, value) + storedCaptures[capNum].registerValue(value) controller.step() } } diff --git a/Sources/_StringProcessing/Engine/Registers.swift b/Sources/_StringProcessing/Engine/Registers.swift index ece7be4f1..b586baecf 100644 --- a/Sources/_StringProcessing/Engine/Registers.swift +++ b/Sources/_StringProcessing/Engine/Registers.swift @@ -36,21 +36,18 @@ extension Processor { // Value-constructing matchers var matcherFunctions: [MEProgram.MatcherFunction] - - // MARK: writeable - - var values: UndoableArray // MARK: writeable, resettable - var ints: UndoableArray + var isDirty = false + + // currently, useful for range-based quantification + var ints: [Int] - var positions: UndoableArray + var values: [Any] - var storedCaptures: UndoableArray + var positions: [Input.Index] - var isDirty = false - init( elements: [Element], utf8Contents: [[UInt8]], @@ -58,10 +55,10 @@ extension Processor { consumeFunctions: [MEProgram.ConsumeFunction], transformFunctions: [MEProgram.TransformFunction], matcherFunctions: [MEProgram.MatcherFunction], + isDirty: Bool = false, numInts: Int, numValues: Int, - numPositions: Int, - numCaptures: Int + numPositions: Int ) { self.elements = elements self.utf8Contents = utf8Contents @@ -69,70 +66,39 @@ extension Processor { self.consumeFunctions = consumeFunctions self.transformFunctions = transformFunctions self.matcherFunctions = matcherFunctions - self.ints = UndoableArray(repeating: 0, count: numInts) - self.values = UndoableArray(repeating: SentinelValue(), count: numValues) - self.positions = UndoableArray( + self.isDirty = isDirty + self.ints = Array(repeating: 0, count: numInts) + self.values = Array(repeating: SentinelValue(), count: numValues) + self.positions = Array( 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 - } - - // NOTE: Value registers are deliberately never logged, matching the - // behavior from before the undo log was introduced. Values are only - // really read when consuming matches, so they don't need to be unwound. - @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 { - ints[i] + get { ints[i.rawValue] } + set { + isDirty = true + ints[i.rawValue] = newValue + } } - subscript(_ i: ValueRegister) -> Any { - values[i] + get { values[i.rawValue] } + set { + isDirty = true + values[i.rawValue] = newValue + } } - subscript(_ i: PositionRegister) -> Input.Index { - positions[i] - } - - subscript(_ i: CaptureRegister) -> Processor._StoredCapture { - storedCaptures[i] + get { positions[i.rawValue] } + set { + isDirty = true + positions[i.rawValue] = newValue + } } - subscript(_ i: ElementRegister) -> Input.Element { elements[i.rawValue] } @@ -161,23 +127,21 @@ extension Processor.Registers { } mutating func reset() { - 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 + guard isDirty else { + return + } + self.ints._setAll(to: 0) + self.values._setAll(to: SentinelValue()) + self.positions._setAll(to: Processor.Registers.sentinelIndex) } +} - /// Discards every register's undo log, leaving the current values in place. - /// - /// Only valid when no save points remain, since the logged values can no - /// longer be reached by backtracking at that point. - @inline(__always) - mutating func discardUndoLogs() { - ints.discardLog() - positions.discardLog() - storedCaptures.discardLog() +// TODO: Productize into general algorithm +extension MutableCollection { + mutating func _setAll(to e: Element) { + for idx in self.indices { + self[idx] = e + } } } @@ -194,9 +158,9 @@ extension Processor.Registers: CustomStringConvertible { return """ \(formatRegisters("elements", elements))\ - \(formatRegisters("ints", ints.values))\ + \(formatRegisters("ints", ints))\ - """ + """ } } diff --git a/Sources/_StringProcessing/Engine/Tracing.swift b/Sources/_StringProcessing/Engine/Tracing.swift index 0a3fef162..b67cbb6a5 100644 --- a/Sources/_StringProcessing/Engine/Tracing.swift +++ b/Sources/_StringProcessing/Engine/Tracing.swift @@ -115,15 +115,17 @@ extension Instruction: CustomStringConvertible { extension Processor.SavePoint { func describe(in input: String) -> String { let posStr: String - switch savedPosition { - case .position(let p): + if let p = self.pos { posStr = "\(input.distance(from: input.startIndex, to: p))" - 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 = "" + } 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)" + } } return """ pc: \(self.pc), pos: \(posStr) diff --git a/Sources/_StringProcessing/Executor.swift b/Sources/_StringProcessing/Executor.swift index 0f1d7ac8f..07e058b8f 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 { - private typealias Entry = (slot: Register, oldValue: Value) - - /// 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: [Entry] = [] - - /// 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 } - - 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 } - - /// 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) { - // Only inline this early exit check, so we aren't inflating the backtracking - // caller unnecessarily. - if logCount == mark { return } - _undo(to: mark) - } - - @inline(never) - private mutating func _undo(to mark: Int) { - assert(mark < logCount) - // Restore values from the log in reverse order. - let end = logCount - log.withUnsafeBufferPointer { log in - values.withUnsafeMutableBufferPointer { values in - var i = end - while i > mark { - i -= 1 - values[log[i].slot.rawValue] = log[i].oldValue - } - } - } - // Truncate the log; old values are overwritten as needed. - log.removeLast(log.count - mark) - } - - /// Discards the undo log without applying it, leaving every register slot - /// at its current value. - /// - /// Only valid when no save point refers to the log any longer, i.e. when - /// there is nothing left that could backtrack. - @inline(__always) - mutating func discardLog() { - if !log.isEmpty { - log.removeAll(keepingCapacity: true) - } - } - - /// Resets every register slot to `initialValue` and discards the undo - /// log. - @inline(always) - mutating func reset(to initialValue: Value) { - discardLog() - values.withUnsafeMutableBufferPointer { values in - for idx in values.indices { - values[idx] = initialValue - } - } - } -} diff --git a/Tests/MatchingEngineTests/UndoableArrayTests.swift b/Tests/MatchingEngineTests/UndoableArrayTests.swift deleted file mode 100644 index 15e3dbbd9..000000000 --- a/Tests/MatchingEngineTests/UndoableArrayTests.swift +++ /dev/null @@ -1,198 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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) - } - - func testDiscardLogKeepsCurrentValues() { - var a = UndoableArray(repeating: 0, count: 2) - a.set(IntRegister(0), to: 1, logging: true) - a.set(IntRegister(1), to: 2, logging: true) - XCTAssertEqual(a.logCount, 2) - - a.discardLog() - - XCTAssertEqual(a.logCount, 0) - XCTAssertEqual(a[IntRegister(0)], 1) - XCTAssertEqual(a[IntRegister(1)], 2) - } - - // The log keeps its storage when it's truncated, so new entries overwrite - // stale ones. Make sure a later undo only replays the live entries. - func testLogStorageIsReusedAfterTruncation() { - var a = UndoableArray(repeating: 0, count: 2) - - a.set(IntRegister(0), to: 1, logging: true) - a.set(IntRegister(1), to: 2, logging: true) - a.set(IntRegister(0), to: 3, logging: true) - XCTAssertEqual(a.logCount, 3) - - a.undo(to: 0) - XCTAssertEqual(a[IntRegister(0)], 0) - XCTAssertEqual(a[IntRegister(1)], 0) - - // Reuse the three stale log slots, plus one fresh one. - a.set(IntRegister(1), to: 10, logging: true) - let mark = a.logCount - a.set(IntRegister(0), to: 20, logging: true) - a.set(IntRegister(1), to: 30, logging: true) - a.set(IntRegister(0), to: 40, logging: true) - XCTAssertEqual(a.logCount, 4) - - a.undo(to: mark) - XCTAssertEqual(a[IntRegister(0)], 0) - XCTAssertEqual(a[IntRegister(1)], 10) - - a.undo(to: 0) - XCTAssertEqual(a[IntRegister(0)], 0) - XCTAssertEqual(a[IntRegister(1)], 0) - } - - func testUndoAfterDiscardLogOnlyReplaysNewEntries() { - var a = UndoableArray(repeating: 0, count: 1) - a.set(IntRegister(0), to: 1, logging: true) - a.discardLog() - - let mark = a.logCount - a.set(IntRegister(0), to: 2, logging: true) - a.undo(to: mark) - - // The discarded entry is gone, so `1` is the restored value, not `0`. - XCTAssertEqual(a[IntRegister(0)], 1) - XCTAssertEqual(a.logCount, 0) - } -} diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index d75dbde64..73b0a7b91 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -2040,22 +2040,6 @@ 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() {