///|
priv struct CLSourceHazardReaderIndex {
  types : Array[@lib.ValType]
  readers_by_group : Array[Array[Int32]]
}

///|
fn cl_source_reader_type_group(value : @lib.ValType) -> Int32 {
  match value {
    @lib.NumTypeValType(@lib.I32NumType) => 0
    @lib.NumTypeValType(@lib.I64NumType) => 1
    @lib.NumTypeValType(@lib.F32NumType) => 2
    @lib.NumTypeValType(@lib.F64NumType) => 3
    @lib.VecTypeValType => 4
    @lib.RefTypeValType(_) => 5
    @lib.BotValType => 6
  }
}

///|
fn cl_source_hazard_reader_index(
  types : Array[@lib.ValType],
  readers : Array[Bool],
  local_visits~ : Array[Int32]? = None,
) -> CLSourceHazardReaderIndex {
  let readers_by_group = Array::makei(7, fn(_) { [] })
  // RefType equality accepts shorthand and expanded references with different
  // derived hashes. Keep all references in one coarse bucket and retain the
  // old exact equality filter at lookup. Fixed kinds keep construction linear
  // even when every reference has a distinct heap type. The index lives only
  // through one read-only hazard analysis.
  for local in 0.. visits[0] += 1
      None => ()
    }
    if local < readers.length() && readers[local] {
      // Appending in local order preserves the old destination order.
      let group = cl_source_reader_type_group(types[local])
      readers_by_group[group].push(local)
    }
  }
  { types, readers_by_group, }
}

///|
fn cl_source_write_dest_read_candidate_dests_indexed(
  source : Int32,
  index : CLSourceHazardReaderIndex,
  interferences : CLBitMatrix,
  first_effective_write : Array[Int32],
  latest_get : Array[Int32],
  candidate_visits~ : Array[Int32]? = None,
) -> Array[Int32] {
  let candidates : Array[Int32] = []
  if source < 0 ||
    source >= index.types.length() ||
    source >= interferences.length() ||
    source >= first_effective_write.length() ||
    first_effective_write[source] < 0 {
    return candidates
  }
  let group = cl_source_reader_type_group(index.types[source])
  for dest in index.readers_by_group[group] {
    match candidate_visits {
      Some(visits) => visits[0] += 1
      None => ()
    }
    if source != dest &&
      dest < latest_get.length() &&
      latest_get[dest] > first_effective_write[source] &&
      index.types[source] == index.types[dest] &&
      !cl_bit_matrix_get(interferences, source, dest) {
      candidates.push(dest)
    }
  }
  candidates
}

///|
// The index filters readers and coarse type groups before the old exact test.
// A complete one-group reader row gives every source the full destination row.
// Sparse rows or a second group keep the existing index path.
fn cl_source_hazard_reader_index_can_filter(
  types : Array[@lib.ValType],
  readers : Array[Bool],
) -> Bool {
  if types.is_empty() {
    return false
  }
  if readers.is_empty() || !readers[0] {
    return true
  }
  let group = cl_source_reader_type_group(types[0])
  for local in 1..= readers.length() || !readers[local] {
      return true
    }
    if cl_source_reader_type_group(types[local]) != group {
      return true
    }
  }
  false
}