///|
// KMP carries only the unmatched separator prefix across backward reads.
// Completed matches are nonoverlapping, so at most ceil(block/separator)
// starts are retained. Clearing the array reuses its bounded allocation.
fn reverse_block_matches(
  data : BytesView,
  separator : Bytes,
  failure : FixedArray[Int32],
  carried : Ref[Int32],
  starts : Array[Int32],
) -> Unit {
  starts.clear()
  let mut matched = carried.val
  let first = separator[separator.length() - 1:]
  let mut index = data.length() - 1
  while index >= 0 {
    // In the zero-prefix state only the final separator byte can start a
    // reversed match. Skip other bytes without changing any KMP state.
    if matched == 0 && index >= 32 && data[index] != first[0] {
      guard data[:index].rev_find(first) is Some(next) else { break }
      index = next
    }
    let byte = data[index]
    while matched > 0 && byte != separator[separator.length() - matched - 1] {
      matched = failure[matched - 1]
    }
    if byte == separator[separator.length() - matched - 1] {
      matched += 1
    }
    if matched == separator.length() {
      starts.push(index)
      matched = 0
    }
    index -= 1
  }
  carried.val = matched
}

///|
fn reverse_matched_ranges(
  data : BytesView,
  starts : Array[Int32],
  first : Int32,
  offset : Int32,
  end : Int32,
  output : @buffer.Buffer,
) -> Int32 {
  let mut end = end
  for index in first..