Skip to content
Merged
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
40 changes: 22 additions & 18 deletions Sources/FuzzyMatch/FuzzyMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,16 @@ public struct FuzzyMatcher: Sendable {
) -> ScoredMatch? {
switch query.config.algorithm {
case .smithWaterman(let swConfig):
return scoreSmithWatermanImpl(
candidateUTF8,
against: query,
swConfig: swConfig,
candidateStorage: &buffer.candidateStorage,
smithWatermanState: &buffer.smithWatermanState,
wordInitials: &buffer.wordInitials
)
return buffer.withSmithWatermanBuffers { candidateStorage, smithWatermanState, wordInitials in
scoreSmithWatermanImpl(
candidateUTF8,
against: query,
swConfig: swConfig,
candidateStorage: &candidateStorage,
smithWatermanState: &smithWatermanState,
wordInitials: &wordInitials
)
}

case .editDistance(let edConfig):
// Fast path for 1-character queries: single scan, no buffer needed.
Expand All @@ -366,16 +368,18 @@ public struct FuzzyMatcher: Sendable {
minScore: query.config.minScore
)
}
return scoreImpl(
candidateUTF8,
against: query,
edConfig: edConfig,
candidateStorage: &buffer.candidateStorage,
editDistanceState: &buffer.editDistanceState,
matchPositions: &buffer.matchPositions,
alignmentState: &buffer.alignmentState,
wordInitials: &buffer.wordInitials
)
return buffer.withEditDistanceBuffers { candidateStorage, editDistanceState, matchPositions, alignmentState, wordInitials in
scoreImpl(
candidateUTF8,
against: query,
edConfig: edConfig,
candidateStorage: &candidateStorage,
editDistanceState: &editDistanceState,
matchPositions: &matchPositions,
alignmentState: &alignmentState,
wordInitials: &wordInitials
)
}
}
}

Expand Down
52 changes: 50 additions & 2 deletions Sources/FuzzyMatch/ScoringBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,21 @@ internal struct CandidateStorage: Sendable {
mutating func withMutableBuffers<R>(
_ body: (UnsafeMutablePointer<UInt8>, UnsafeMutablePointer<Int32>) -> R
) -> R {
bytes.withUnsafeMutableBufferPointer { bytesPtr in
bonus.withUnsafeMutableBufferPointer { bonusPtr in
// Swap arrays into locals so the nested withUnsafeMutableBufferPointer
// calls operate on independent variables, avoiding overlapping exclusive
// accesses to `self` in library evolution mode. swap() is O(1) for arrays.
var localBytes: [UInt8] = []
var localBonus: [Int32] = []
swap(&localBytes, &bytes)
swap(&localBonus, &bonus)
let result = localBytes.withUnsafeMutableBufferPointer { bytesPtr in
localBonus.withUnsafeMutableBufferPointer { bonusPtr in
body(bytesPtr.baseAddress!, bonusPtr.baseAddress!)
}
}
swap(&localBytes, &bytes)
swap(&localBonus, &bonus)
return result
Comment on lines +81 to +88

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a defer block to swap the arrays back would make this code more robust and idiomatic. While the current implementation is correct because the body closure doesn't throw, defer guarantees that the cleanup logic (swapping back the arrays) is executed right before the function returns. This is especially useful if the function's logic becomes more complex or if the closure's signature is changed to be throwing in the future.

        defer {
            swap(&localBytes, &bytes)
            swap(&localBonus, &bonus)
        }
        return localBytes.withUnsafeMutableBufferPointer { bytesPtr in
            localBonus.withUnsafeMutableBufferPointer { bonusPtr in
                body(bytesPtr.baseAddress!, bonusPtr.baseAddress!)
            }
        }

}
}

Expand Down Expand Up @@ -392,4 +402,42 @@ public struct ScoringBuffer: Sendable {
highWaterQueryLength = 0
callsSinceLastCheck = 0
}

// MARK: - Disjoint Property Access

/// Provides simultaneous `inout` access to buffers needed by the edit distance pipeline.
///
/// In library evolution mode, passing `&buffer.candidateStorage` and `&buffer.editDistanceState`
/// as separate `inout` arguments triggers an overlapping-access error. This method provides
/// a single exclusive access to `self` and projects its stored properties through a closure.
@inline(__always)
@inlinable
mutating func withEditDistanceBuffers<R>(
_ body: (
inout CandidateStorage,
inout EditDistanceState,
inout [Int],
inout AlignmentState,
inout [UInt8]
) -> R
) -> R {
body(&candidateStorage, &editDistanceState, &matchPositions, &alignmentState, &wordInitials)
}

/// Provides simultaneous `inout` access to buffers needed by the Smith-Waterman pipeline.
///
/// In library evolution mode, passing `&buffer.candidateStorage` and `&buffer.smithWatermanState`
/// as separate `inout` arguments triggers an overlapping-access error. This method provides
/// a single exclusive access to `self` and projects its stored properties through a closure.
@inline(__always)
@inlinable
mutating func withSmithWatermanBuffers<R>(
_ body: (
inout CandidateStorage,
inout SmithWatermanState,
inout [UInt8]
) -> R
) -> R {
body(&candidateStorage, &smithWatermanState, &wordInitials)
}
}
Loading