Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -413,16 +413,24 @@ public abstract class AhoCorasickBase<T> @JvmOverloads constructor(options: Set<
*/
private fun calculateNextState(startingNode: Int, offset: Int): Int {

// Read the post-build backing arrays directly to keep this loop free of SafeIndexable virtual dispatch. The
// bounds check against parentCount reproduces safeGetParent: an out-of-range child yields no parent, and
// RESERVED_VALUE can never equal a (non-negative) node index, so the check below stays correct.
val baseOffsets = store.runtimeBaseOffsets
val parents = store.runtimeParents
val failureIndices = store.runtimeFailureIndices
val parentCount = parents.size

var currentNode = startingNode

var childNode = store.getBaseOffset(currentNode) + offset
var hasValidChild = store.safeGetParent(childNode) == currentNode
var childNode = baseOffsets[currentNode] + offset
var hasValidChild = childNode < parentCount && parents[childNode] == currentNode

while (!hasValidChild && currentNode != ROOT_NODE) {

currentNode = store.getFailureIndex(currentNode)
childNode = store.getBaseOffset(currentNode) + offset
hasValidChild = store.safeGetParent(childNode) == currentNode
currentNode = failureIndices[currentNode]
childNode = baseOffsets[currentNode] + offset
hasValidChild = childNode < parentCount && parents[childNode] == currentNode
}

// The current node will always be ROOT_NODE if no valid child is found.
Expand Down Expand Up @@ -873,23 +881,28 @@ public abstract class AhoCorasickBase<T> @JvmOverloads constructor(options: Set<

var nextNode: Int = RESERVED_VALUE

// Captured once so the per-result value and prefix reads avoid SafeIndexable virtual dispatch. The spliterator
// is only created post-build (parse requires isBuilt), so these arrays are always initialized here.
private val values = store.runtimeValues
private val prefixIndices = store.runtimePrefixIndices

override fun tryAdvance(action: Consumer<in AhoCorasickResult<T>>): Boolean {

while (currentInputIndex < input.length || nextNode != RESERVED_VALUE) {

var nextValue: Int
if (nextNode == RESERVED_VALUE) {
currentNode = calculateNextState(currentNode, input[currentInputIndex++].normalize().code)
nextNode = store.getPrefixIndex(currentNode)
nextValue = store.getValue(currentNode)
nextNode = prefixIndices[currentNode]
nextValue = values[currentNode]

// Determine if the current node is the end of a string.
if (nextValue == RESERVED_VALUE) continue
} else {

// Add any other values whose keys are equal to a suffix of the path from the root to current node.
nextValue = store.getValue(nextNode)
nextNode = store.getPrefixIndex(nextNode)
nextValue = values[nextNode]
nextNode = prefixIndices[nextNode]
}

val result = generateResult(currentInputIndex, nextValue, input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ internal class AhoCorasickStore {
*/
private var store2: SafeIndexable = DoubleArrayIntList(RESERVED_VALUE)

/**
* Raw backing arrays for the match hot path, captured by [buildRuntimeStructures].
*
* The matcher reads transitions directly as `int[]` through these, bypassing the [SafeIndexable] virtual dispatch
* that the build-time accessors go through. They alias the post-build [UnboxedIntList] backing arrays, so they are
* only valid after [buildRuntimeStructures] runs; reading them before then throws. Named by their post-build
* meaning: [runtimeFailureIndices] aliases [store1], [runtimePrefixIndices] aliases [store2].
*/
lateinit var runtimeBaseOffsets: IntArray
private set

lateinit var runtimeParents: IntArray
private set

lateinit var runtimeValues: IntArray
private set

lateinit var runtimeFailureIndices: IntArray
private set

lateinit var runtimePrefixIndices: IntArray
private set

/**
* Sets the base offset of a node at the given index.
*
Expand Down Expand Up @@ -229,6 +252,14 @@ internal class AhoCorasickStore {
values = values.toIntList()
store1 = store1.toIntList()
store2 = store2.toIntList()

// Capture the backing arrays so the match path can index transitions directly, skipping SafeIndexable
// dispatch. Each list above is now an UnboxedIntList whose backing array length equals its logical size.
runtimeBaseOffsets = (baseOffsets as UnboxedIntList).backingArray
runtimeParents = (parents as UnboxedIntList).backingArray
runtimeValues = (values as UnboxedIntList).backingArray
runtimeFailureIndices = (store1 as UnboxedIntList).backingArray
runtimePrefixIndices = (store2 as UnboxedIntList).backingArray
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ internal class UnboxedIntList(
*/
private var array = IntArray(max(initialCapacity, 1)) { defaultValue }

/**
* The raw backing array, exposed so the Aho-Corasick match path can read transitions directly as `int[]` instead
* of through the [SafeIndexable] virtual dispatch.
*
* For a list produced by [AhoCorasickStore.buildRuntimeStructures] the backing array's length equals [size], so
* callers may index it directly. A structural change ([add], [safeSet], or any resize) may replace this array, so
* callers must re-fetch it after any mutation.
*/
val backingArray: IntArray get() = array

override operator fun get(index: Int) = array[index]

override operator fun set(index: Int, value: Int): Unit = array.set(index, value)
Expand Down
Loading