Make NdbBlock ~Copyable for better lifetime safety
Changelog-None Closes: https://github.com/damus-io/damus/issues/3127 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -51,7 +51,7 @@ extension ndb_invoice_block {
|
||||
}
|
||||
}
|
||||
|
||||
enum NdbBlock {
|
||||
enum NdbBlock: ~Copyable {
|
||||
case text(ndb_str_block)
|
||||
case mention(ndb_mention_bech32_block)
|
||||
case hashtag(ndb_str_block)
|
||||
@@ -107,10 +107,6 @@ struct NdbBlockGroup: ~Copyable {
|
||||
fileprivate let metadata: MaybeTxn<BlocksMetadata>
|
||||
/// The raw text content of the note
|
||||
fileprivate let rawTextContent: String
|
||||
/// An iterable list of blocks that make up this object
|
||||
var blocks: [NdbBlock] {
|
||||
return self.collectBlocks()
|
||||
}
|
||||
var words: Int {
|
||||
return metadata.borrow { $0.words }
|
||||
}
|
||||
@@ -149,12 +145,12 @@ enum MaybeTxn<T: ~Copyable>: ~Copyable {
|
||||
case pure(T)
|
||||
case txn(SafeNdbTxn<T>)
|
||||
|
||||
func borrow<Y>(_ borrowFunction: (borrowing T) -> Y) -> Y {
|
||||
func borrow<Y>(_ borrowFunction: (borrowing T) throws -> Y) rethrows -> Y {
|
||||
switch self {
|
||||
case .pure(let item):
|
||||
return borrowFunction(item)
|
||||
return try borrowFunction(item)
|
||||
case .txn(let txn):
|
||||
return borrowFunction(txn.val)
|
||||
return try borrowFunction(txn.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,35 +235,60 @@ extension NdbBlockGroup {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Enumeration support
|
||||
|
||||
extension NdbBlockGroup {
|
||||
/// Collects all blocks in the group into an array without using Iterator/Sequence protocols
|
||||
typealias NdbBlockList = NonCopyableLinkedList<NdbBlock>
|
||||
|
||||
/// Borrows all blocks in the group one by one and runs a function defined by the caller.
|
||||
///
|
||||
/// **Implementation note:**
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it does seem to be possible to conform to both `Sequence` and `~Copyable` at the same time.
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable`
|
||||
///
|
||||
/// - Returns: An array of all blocks in the group
|
||||
fileprivate func collectBlocks() -> [NdbBlock] {
|
||||
var blocks = [NdbBlock]()
|
||||
/// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself.
|
||||
/// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)`
|
||||
@discardableResult
|
||||
func forEachBlock<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.forEachItem(borrowingFunction) })
|
||||
}
|
||||
|
||||
/// Borrows all blocks in the group one by one and runs a function defined by the caller, in reverse order
|
||||
///
|
||||
/// **Implementation note:**
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable`
|
||||
///
|
||||
/// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself.
|
||||
/// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)`
|
||||
@discardableResult
|
||||
func forEachBlockReversed<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.forEachItemReversed(borrowingFunction) })
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list, updating a final value, and returns the final result at the end.
|
||||
func reduce<Y>(initialResult: Y, _ borrowingFunction: ((_ index: Int, _ partialResult: Y, _ item: borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.reduce(initialResult: initialResult, borrowingFunction) })
|
||||
}
|
||||
|
||||
/// Borrows the block list for processing
|
||||
func withList<Y>(_ borrowingFunction: (borrowing NdbBlockList) throws -> Y) rethrows -> Y {
|
||||
var linkedList: NdbBlockList = .init()
|
||||
|
||||
// Ensure the C string remains valid for the entire operation by keeping
|
||||
// all operations using it within the withCString closure
|
||||
self.rawTextContent.withCString { cptr in
|
||||
return try self.rawTextContent.withCString { cptr in
|
||||
var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil)
|
||||
|
||||
// Start the iteration
|
||||
self.metadata.borrow { value in
|
||||
return try self.metadata.borrow { value in
|
||||
ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
|
||||
|
||||
// Collect blocks into array
|
||||
while let ptr = ndb_blocks_iterate_next(&iter),
|
||||
outerLoop: while let ptr = ndb_blocks_iterate_next(&iter),
|
||||
let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
|
||||
blocks.append(block)
|
||||
linkedList.add(item: block)
|
||||
}
|
||||
|
||||
return try borrowingFunction(linkedList)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
160
nostrdb/NonCopyableLinkedList.swift
Normal file
160
nostrdb/NonCopyableLinkedList.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// NonCopyableLinkedList.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-07-04.
|
||||
//
|
||||
|
||||
/// A linked list to help with iteration of non-copyable elements
|
||||
///
|
||||
/// This is needed to provide an array-like abstraction or iterators since swift arrays or iterator protocols require the element to be "copyable"
|
||||
struct NonCopyableLinkedList<T: ~Copyable>: ~Copyable {
|
||||
private var head: Node<T>? = nil
|
||||
private var tail: Node<T>? = nil
|
||||
private(set) var count: Int = 0
|
||||
|
||||
/// Iterates over each item of the list, with enumeration support.
|
||||
func forEachItem<Y>(_ borrowingFunction: ((_ index: Int, _ item: borrowing T) throws -> LoopCommand<Y>)) rethrows -> Y? {
|
||||
var indexCounter = 0
|
||||
|
||||
var cursor: Node? = self.head
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, nextItem.value)
|
||||
indexCounter += 1
|
||||
cursor = nextItem.next
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list in reverse, with enumeration support.
|
||||
func forEachItemReversed<Y, E: Error>(_ borrowingFunction: ((_ index: Int, _ item: borrowing T) throws(E) -> LoopCommand<Y>)) throws(E) -> Y? {
|
||||
var indexCounter = count
|
||||
var cursor: Node? = self.tail
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, nextItem.value)
|
||||
indexCounter -= 1
|
||||
cursor = nextItem.previous
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list, with enumeration support, updating some value in each iteration and returning the final value at the end.
|
||||
func reduce<Y>(initialResult: Y, _ borrowingFunction: ((_ index: Int, _ partialResult: Y, _ item: borrowing T) throws -> LoopCommand<Y>)) throws -> Y {
|
||||
var indexCounter = 0
|
||||
var currentResult = initialResult
|
||||
|
||||
var cursor: Node? = self.head
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, currentResult, nextItem.value)
|
||||
indexCounter += 1
|
||||
cursor = nextItem.next
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
currentResult = result
|
||||
continue outerLoop
|
||||
}
|
||||
}
|
||||
|
||||
return currentResult
|
||||
}
|
||||
|
||||
/// Uses a specific item of the list based on a provided index.
|
||||
///
|
||||
/// O(N/2) worst case scenario
|
||||
///
|
||||
/// Returns `nil` if nothing was found
|
||||
func useItem<Y>(at index: Int, _ borrowingFunction: ((_ item: borrowing T) throws -> Y)) rethrows -> Y? {
|
||||
if index < 0 || index >= self.count {
|
||||
return nil
|
||||
}
|
||||
else if index < self.count / 2 {
|
||||
return try self.forEachItem({ i, item in
|
||||
if i == index {
|
||||
return .loopReturn(try borrowingFunction(item))
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
}
|
||||
else {
|
||||
return try self.forEachItemReversed({ i, item in
|
||||
if i == index {
|
||||
return .loopReturn(try borrowingFunction(item))
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an item to the tail end list
|
||||
mutating func add(item: consuming T) {
|
||||
guard self.head != nil, let currentTail = self.tail else {
|
||||
let firstNode = Node(value: item, next: nil, previous: nil)
|
||||
self.head = firstNode
|
||||
self.tail = firstNode
|
||||
self.count = 1
|
||||
return
|
||||
}
|
||||
let newTail = Node(value: item, next: nil, previous: currentTail)
|
||||
currentTail.next = newTail
|
||||
self.tail = newTail
|
||||
self.count += 1
|
||||
}
|
||||
|
||||
/// A node of the linked list
|
||||
///
|
||||
/// Should be `~Copyable` but that would require using a value type such as a struct or enum, and the Swift compiler does not support recursive enums with non-copyable objects for some reason. Example:
|
||||
/// ```swift
|
||||
/// enum List<Y: ~Copyable>: ~Copyable {
|
||||
/// indirect case node(value: Y, next: NewList<Y>) // <-- ERROR: Noncopyable enum 'List' cannot be marked indirect or have indirect cases yet
|
||||
/// case empty
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Therefore, we make it `private` to make sure we contain the exposure of this unsafe object to only this class. Outside users of the linked list can access objects via the iterator functions.
|
||||
private class Node<Item: ~Copyable> {
|
||||
let value: Item
|
||||
var next: Node?
|
||||
var previous: Node?
|
||||
|
||||
init(value: consuming Item, next: consuming Node?, previous: consuming Node?) {
|
||||
self.value = value
|
||||
self.next = next
|
||||
self.previous = previous
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop command to allow closures to control the loop they are in.
|
||||
enum LoopCommand<Y> {
|
||||
/// Breaks out of the loop
|
||||
case loopBreak
|
||||
/// Continues to the next iteration of the loop
|
||||
case loopContinue
|
||||
/// Stops iterating and return a value
|
||||
case loopReturn(Y)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user