use core::fmt;
use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
use std::sync::{Mutex, MutexGuard, PoisonError};
use hashbrown::HashTable;
use hashbrown::hash_table::Entry;
use rayon::prelude::*;
use super::definition::{Definition, DefinitionKind, Resolver, takes_precedence};
use super::flags::{self, SymbolFlags};
use super::name::{InputPosition, SymbolName};
use super::util::select_mut;
use crate::error::{Error, Result};
use crate::ids::{FileId, SymbolId};
mod partition;
pub use partition::LookupView;
pub const SHARD_BITS: u32 = 11;
pub const SHARD_COUNT: usize = 1 << SHARD_BITS;
pub const DEFINITION_LOCKS: usize = 1 << 12;
pub const MAX_SYMBOLS: usize = PENDING as usize;
const PENDING: u32 = 1 << 31;
const MIN_PARALLEL_CHUNK: usize = 1024;
const MIN_PARALLEL_LOOKUP: usize = 1 << 16;
const MIN_PARALLEL_LOOKUP_KNOWN: usize = 1 << 14;
const MIN_PARALLEL_SORT: usize = 1 << 17;
const MIN_PARALLEL_LINEAR: usize = 1 << 17;
const MIN_PARALLEL_GROW: usize = 1 << 17;
fn overflow(limit: usize) -> Error {
Error::Limit(format!("more than {limit} distinct symbol names"))
}
type Occurrence = (InputPosition, u32);
#[derive(Clone, Copy)]
struct Slot {
h32: u32,
value: u32,
}
struct PendingName<'a> {
name: SymbolName<'a>,
first: Occurrence,
}
struct Shard<'a> {
table: HashTable<Slot>,
pending: Vec<PendingName<'a>>,
assigned: Vec<AtomicU32>,
}
impl Shard<'_> {
fn new() -> Self {
Self {
table: HashTable::new(),
pending: Vec::new(),
assigned: Vec::new(),
}
}
}
#[inline]
fn shard_of(hash: u64) -> usize {
(hash >> (64 - SHARD_BITS)) as usize
}
#[inline]
fn low32(hash: u64) -> u32 {
hash as u32
}
#[inline]
fn table_hash(h32: u32) -> u64 {
u64::from(h32).wrapping_mul(0x9e37_79b9_7f4a_7c15)
}
#[inline]
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[inline]
fn get_mut<T>(mutex: &mut Mutex<T>) -> &mut T {
mutex.get_mut().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug)]
pub struct InternJob<'a, 's> {
pub position: InputPosition,
pub names: &'s [SymbolName<'a>],
pub ids: &'s mut [SymbolId],
}
pub struct SymbolTable<'a> {
shards: Box<[Mutex<Shard<'a>>]>,
names: Vec<SymbolName<'a>>,
flags: Vec<AtomicU32>,
def_kind: Vec<AtomicU8>,
def_file: Vec<AtomicU32>,
def_index: Vec<AtomicU32>,
def_position: Vec<AtomicU64>,
def_aux: Vec<AtomicU64>,
def_locks: Box<[Mutex<()>]>,
limit: usize,
}
impl Default for SymbolTable<'_> {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for SymbolTable<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SymbolTable")
.field("len", &self.len())
.finish_non_exhaustive()
}
}
impl<'a> SymbolTable<'a> {
#[must_use]
pub fn new() -> Self {
Self::with_capacity(0)
}
#[must_use]
pub fn with_capacity(symbols: usize) -> Self {
let per_shard = symbols.div_ceil(SHARD_COUNT);
let shards = (0..SHARD_COUNT)
.map(|_| {
let mut shard = Shard::new();
shard.table.reserve(per_shard, |slot| table_hash(slot.h32));
Mutex::new(shard)
})
.collect();
let def_locks = (0..DEFINITION_LOCKS).map(|_| Mutex::new(())).collect();
Self {
shards,
names: Vec::with_capacity(symbols),
flags: Vec::with_capacity(symbols),
def_kind: Vec::with_capacity(symbols),
def_file: Vec::with_capacity(symbols),
def_index: Vec::with_capacity(symbols),
def_position: Vec::with_capacity(symbols),
def_aux: Vec::with_capacity(symbols),
def_locks,
limit: MAX_SYMBOLS,
}
}
#[cfg(test)]
pub(crate) fn set_limit(&mut self, limit: usize) {
self.limit = limit.min(MAX_SYMBOLS);
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.names.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn ids(&self) -> impl ExactSizeIterator<Item = SymbolId> + use<> {
(0..self.len()).map(SymbolId::new)
}
#[inline]
#[must_use]
pub fn name(&self, id: SymbolId) -> SymbolName<'a> {
self.names[id.index()]
}
#[inline]
#[must_use]
pub fn names(&self) -> &[SymbolName<'a>] {
&self.names
}
#[must_use]
pub fn lookup(&self, name: &SymbolName<'_>) -> Option<SymbolId> {
let h32 = low32(name.hash());
let shard = lock(&self.shards[shard_of(name.hash())]);
let names = &self.names;
shard
.table
.find(table_hash(h32), |slot| {
slot.h32 == h32 && slot.value & PENDING == 0 && names[slot.value as usize] == *name
})
.map(|slot| SymbolId::from_u32(slot.value))
}
pub fn intern(&mut self, name: SymbolName<'a>) -> SymbolId {
match self.try_intern(name) {
Ok(id) => id,
Err(error) => panic!("{error}"),
}
}
pub fn try_intern(&mut self, name: SymbolName<'a>) -> Result<SymbolId> {
let before = self.names.len();
let id = self.intern_name(name).ok_or_else(|| overflow(self.limit))?;
if self.names.len() > before {
self.grow_state(1);
}
Ok(id)
}
#[inline]
fn intern_name(&mut self, name: SymbolName<'a>) -> Option<SymbolId> {
let h32 = low32(name.hash());
let next = self.names.len();
let shard = get_mut(&mut self.shards[shard_of(name.hash())]);
let names = &self.names;
let entry = shard.table.entry(
table_hash(h32),
|slot| slot.h32 == h32 && names[slot.value as usize] == name,
|slot| table_hash(slot.h32),
);
match entry {
Entry::Occupied(occupied) => Some(SymbolId::from_u32(occupied.get().value)),
Entry::Vacant(vacant) => {
if next >= self.limit {
return None;
}
let value = next as u32;
vacant.insert(Slot { h32, value });
self.names.push(name);
Some(SymbolId::from_u32(value))
}
}
}
fn intern_in_order(&mut self, jobs: &mut [InternJob<'a, '_>], order: &[usize]) -> Result<()> {
let base = self.names.len();
for &j in order {
let job = &mut jobs[j];
for (id, name) in job.ids.iter_mut().zip(job.names) {
match self.intern_name(*name) {
Some(interned) => *id = interned,
None => {
self.forget_names_from(base);
return Err(overflow(self.limit));
}
}
}
}
self.grow_state(self.names.len() - base);
Ok(())
}
fn forget_names_from(&mut self, base: usize) {
for (offset, name) in self.names[base..].iter().enumerate() {
let value = (base + offset) as u32;
let shard = get_mut(&mut self.shards[shard_of(name.hash())]);
if let Ok(entry) = shard
.table
.find_entry(table_hash(low32(name.hash())), |slot| slot.value == value)
{
entry.remove();
}
}
self.names.truncate(base);
}
pub fn intern_batch(&mut self, jobs: &mut [InternJob<'a, '_>]) {
if let Err(error) = self.try_intern_batch(jobs) {
panic!("{error}");
}
}
pub fn try_intern_batch(&mut self, jobs: &mut [InternJob<'a, '_>]) -> Result<()> {
let mut total = 0usize;
for job in jobs.iter() {
assert_eq!(
job.names.len(),
job.ids.len(),
"intern job output length mismatch"
);
total = total.saturating_add(job.names.len());
}
let min_parallel = if self.names.is_empty() {
MIN_PARALLEL_LOOKUP
} else {
MIN_PARALLEL_LOOKUP_KNOWN
};
let mut order: Vec<usize> = (0..jobs.len()).collect();
order.sort_unstable_by_key(|&j| jobs[j].position);
if order
.windows(2)
.all(|pair| jobs[pair[0]].position != jobs[pair[1]].position)
{
if total < min_parallel || rayon::current_num_threads() == 1 {
return self.intern_in_order(jobs, &order);
}
if Self::can_partition(total) {
return self.intern_partitioned(jobs, &order, total);
}
}
let jobs_per_task = (MIN_PARALLEL_CHUNK * jobs.len() / total.max(1)).max(1);
let overflowed = AtomicBool::new(false);
{
let this = &*self;
let intern_job = |job: &mut InternJob<'a, '_>| {
let position = job.position;
let one = |(index, (id, name)): (usize, (&mut SymbolId, &SymbolName<'a>))| {
let index = u32::try_from(index).unwrap_or(u32::MAX);
*id = SymbolId::from_u32(this.lookup_or_pend(
name,
(position, index),
&overflowed,
));
};
if job.ids.len() >= 2 * MIN_PARALLEL_CHUNK && total >= min_parallel {
job.ids
.par_iter_mut()
.zip(job.names.par_iter())
.enumerate()
.with_min_len(MIN_PARALLEL_CHUNK)
.for_each(one);
} else {
job.ids.iter_mut().zip(job.names).enumerate().for_each(one);
}
};
if total >= min_parallel {
jobs.par_iter_mut()
.with_min_len(jobs_per_task)
.for_each(intern_job);
} else {
jobs.iter_mut().for_each(intern_job);
}
}
let active: Vec<usize> = self
.shards
.iter_mut()
.enumerate()
.filter_map(|(index, shard)| (!get_mut(shard).pending.is_empty()).then_some(index))
.collect();
if active.is_empty() {
return Ok(());
}
let new_count: usize = active
.iter()
.map(|&s| get_mut(&mut self.shards[s]).pending.len())
.sum();
let base = self.names.len();
if overflowed.into_inner() || new_count > self.limit.saturating_sub(base) {
self.discard_pending(&active);
return Err(overflow(self.limit));
}
self.assign_pending(&active, base, new_count);
{
let shards: Vec<&Shard<'a>> = self.shards.iter_mut().map(|s| &*get_mut(s)).collect();
let rewrite_job = |job: &mut InternJob<'a, '_>| {
let one = |(id, name): (&mut SymbolId, &SymbolName<'a>)| {
let raw = id.as_u32();
if raw & PENDING != 0 {
let local = (raw & !PENDING) as usize;
let assigned = &shards[shard_of(name.hash())].assigned[local];
*id = SymbolId::from_u32(assigned.load(Ordering::Relaxed));
}
};
if job.ids.len() >= 2 * MIN_PARALLEL_CHUNK && total >= MIN_PARALLEL_LINEAR {
job.ids
.par_iter_mut()
.zip(job.names.par_iter())
.with_min_len(MIN_PARALLEL_CHUNK)
.for_each(one);
} else {
job.ids.iter_mut().zip(job.names).for_each(one);
}
};
if total >= MIN_PARALLEL_LINEAR {
jobs.par_iter_mut()
.with_min_len(jobs_per_task)
.for_each(rewrite_job);
} else {
jobs.iter_mut().for_each(rewrite_job);
}
}
for &s in &active {
get_mut(&mut self.shards[s]).assigned = Vec::new();
}
Ok(())
}
#[inline]
fn lookup_or_pend(
&self,
name: &SymbolName<'a>,
occurrence: Occurrence,
overflowed: &AtomicBool,
) -> u32 {
let h32 = low32(name.hash());
let mut guard = lock(&self.shards[shard_of(name.hash())]);
let Shard { table, pending, .. } = &mut *guard;
let names = &self.names;
let entry = table.entry(
table_hash(h32),
|slot| {
slot.h32 == h32
&& if slot.value & PENDING == 0 {
names[slot.value as usize] == *name
} else {
pending[(slot.value & !PENDING) as usize].name == *name
}
},
|slot| table_hash(slot.h32),
);
match entry {
Entry::Occupied(occupied) => {
let value = occupied.get().value;
if value & PENDING != 0 {
let record = &mut pending[(value & !PENDING) as usize];
if occurrence < record.first {
record.first = occurrence;
}
}
value
}
Entry::Vacant(vacant) => {
let Some(local) = u32::try_from(pending.len())
.ok()
.filter(|&local| local < PENDING)
else {
overflowed.store(true, Ordering::Relaxed);
return u32::MAX;
};
pending.push(PendingName {
name: *name,
first: occurrence,
});
let value = PENDING | local;
vacant.insert(Slot { h32, value });
value
}
}
}
fn discard_pending(&mut self, active: &[usize]) {
for &s in active {
let shard = get_mut(&mut self.shards[s]);
shard.table.retain(|slot| slot.value & PENDING == 0);
shard.pending = Vec::new();
}
}
fn assign_pending(&mut self, active: &[usize], base: usize, new_count: usize) {
let parallel = new_count >= MIN_PARALLEL_LINEAR;
let mut shards: Vec<&mut Shard<'a>> = select_mut(&mut self.shards, active)
.into_iter()
.map(get_mut)
.collect();
let mut order: Vec<(Occurrence, u32, u32)> = Vec::with_capacity(new_count);
{
let view: Vec<&Shard<'a>> = shards.iter().map(|shard| &**shard).collect();
fn entries<'v, 'a>(
(s, shard): (usize, &'v &Shard<'a>),
) -> impl Iterator<Item = (Occurrence, u32, u32)> + 'v {
shard
.pending
.iter()
.enumerate()
.map(move |(l, record)| (record.first, s as u32, l as u32))
}
if parallel {
order.par_extend(view.par_iter().enumerate().flat_map_iter(entries));
} else {
order.extend(view.iter().enumerate().flat_map(entries));
}
let compare = |a: &(Occurrence, u32, u32), b: &(Occurrence, u32, u32)| {
a.0.cmp(&b.0).then_with(|| {
let name_a = &view[a.1 as usize].pending[a.2 as usize].name;
let name_b = &view[b.1 as usize].pending[b.2 as usize].name;
name_a.cmp_contents(name_b)
})
};
if new_count >= MIN_PARALLEL_SORT {
order.par_sort_unstable_by(compare);
} else {
order.sort_unstable_by(compare);
}
let name_of =
|&(_, s, l): &(Occurrence, u32, u32)| view[s as usize].pending[l as usize].name;
if parallel {
self.names.par_extend(order.par_iter().map(name_of));
} else {
self.names.extend(order.iter().map(name_of));
}
}
for shard in &mut shards {
let len = shard.pending.len();
shard.assigned.clear();
shard.assigned.resize_with(len, || AtomicU32::new(u32::MAX));
}
{
let view: Vec<&Shard<'a>> = shards.iter().map(|shard| &**shard).collect();
let assign = |(rank, &(_, s, l)): (usize, &(Occurrence, u32, u32))| {
let id = (base + rank) as u32;
view[s as usize].assigned[l as usize].store(id, Ordering::Relaxed);
};
if parallel {
order
.par_iter()
.enumerate()
.with_min_len(MIN_PARALLEL_CHUNK)
.for_each(assign);
} else {
order.iter().enumerate().for_each(assign);
}
}
drop(order);
let finish = |shard: &mut &mut Shard<'a>| {
let Shard {
table,
pending,
assigned,
} = &mut **shard;
for (local, record) in pending.iter().enumerate() {
let h32 = low32(record.name.hash());
let handle = PENDING | local as u32;
let slot = table.find_mut(table_hash(h32), |slot| slot.value == handle);
debug_assert!(slot.is_some(), "pending slot missing");
if let Some(slot) = slot {
slot.value = assigned[local].load(Ordering::Relaxed);
}
}
*pending = Vec::new();
};
if parallel {
shards.par_iter_mut().for_each(finish);
} else {
shards.iter_mut().for_each(finish);
}
drop(shards);
self.grow_state(new_count);
}
fn grow_state(&mut self, count: usize) {
let Self {
flags,
def_kind,
def_file,
def_index,
def_position,
def_aux,
..
} = self;
if count < MIN_PARALLEL_GROW {
flags.extend((0..count).map(|_| AtomicU32::new(0)));
def_kind.extend((0..count).map(|_| AtomicU8::new(DefinitionKind::Undefined as u8)));
def_file.extend((0..count).map(|_| AtomicU32::new(0)));
def_index.extend((0..count).map(|_| AtomicU32::new(0)));
def_position.extend((0..count).map(|_| AtomicU64::new(0)));
def_aux.extend((0..count).map(|_| AtomicU64::new(0)));
return;
}
fn grow<T: Send>(vec: &mut Vec<T>, count: usize, make: impl Fn() -> T + Sync + Send) {
vec.par_extend((0..count).into_par_iter().map(|_| make()));
}
rayon::join(
|| {
rayon::join(
|| grow(flags, count, || AtomicU32::new(0)),
|| {
grow(def_kind, count, || {
AtomicU8::new(DefinitionKind::Undefined as u8)
});
},
)
},
|| {
rayon::join(
|| grow(def_file, count, || AtomicU32::new(0)),
|| {
rayon::join(
|| grow(def_index, count, || AtomicU32::new(0)),
|| {
rayon::join(
|| grow(def_position, count, || AtomicU64::new(0)),
|| grow(def_aux, count, || AtomicU64::new(0)),
)
},
)
},
)
},
);
}
#[inline]
#[must_use]
pub fn flags(&self, id: SymbolId) -> SymbolFlags {
SymbolFlags::from_bits(self.flags[id.index()].load(Ordering::Relaxed))
}
#[inline]
pub fn set_flags(&self, id: SymbolId, flags: SymbolFlags) -> SymbolFlags {
flags::set(&self.flags[id.index()], flags)
}
#[inline]
pub fn clear_flags(&self, id: SymbolId, flags: SymbolFlags) -> SymbolFlags {
flags::clear(&self.flags[id.index()], flags)
}
pub fn insert_definition<R: Resolver + ?Sized>(
&self,
resolver: &R,
id: SymbolId,
candidate: &Definition,
) -> bool {
let index = id.index();
let _guard = lock(&self.def_locks[index & (DEFINITION_LOCKS - 1)]);
let current = self.load_definition(index);
if !takes_precedence(resolver, candidate, ¤t) {
return false;
}
self.store_definition(index, candidate);
true
}
pub fn replace_definition(&self, id: SymbolId, definition: &Definition) {
let index = id.index();
let _guard = lock(&self.def_locks[index & (DEFINITION_LOCKS - 1)]);
self.store_definition(index, definition);
}
#[inline]
#[must_use]
pub fn definition(&self, id: SymbolId) -> Definition {
self.load_definition(id.index())
}
#[must_use]
pub fn definition_synchronized(&self, id: SymbolId) -> Definition {
let index = id.index();
let _guard = lock(&self.def_locks[index & (DEFINITION_LOCKS - 1)]);
self.load_definition(index)
}
#[inline]
#[must_use]
pub fn definition_kind(&self, id: SymbolId) -> DefinitionKind {
DefinitionKind::from_u8(self.def_kind[id.index()].load(Ordering::Relaxed))
}
#[inline]
#[must_use]
pub fn definition_file(&self, id: SymbolId) -> Option<FileId> {
let index = id.index();
(self.def_kind[index].load(Ordering::Relaxed) != DefinitionKind::Undefined as u8)
.then(|| FileId::from_u32(self.def_file[index].load(Ordering::Relaxed)))
}
#[inline]
fn load_definition(&self, index: usize) -> Definition {
self.definitions().get(index)
}
#[inline]
fn definitions(&self) -> Definitions<'_> {
Definitions {
kind: &self.def_kind,
file: &self.def_file,
index: &self.def_index,
position: &self.def_position,
aux: &self.def_aux,
}
}
#[inline]
fn store_definition(&self, index: usize, definition: &Definition) {
self.def_file[index].store(definition.file.as_u32(), Ordering::Relaxed);
self.def_index[index].store(definition.index, Ordering::Relaxed);
self.def_position[index].store(definition.position.raw(), Ordering::Relaxed);
self.def_aux[index].store(definition.aux, Ordering::Relaxed);
self.def_kind[index].store(definition.kind as u8, Ordering::Relaxed);
}
}
#[derive(Clone, Copy)]
struct Definitions<'t> {
kind: &'t [AtomicU8],
file: &'t [AtomicU32],
index: &'t [AtomicU32],
position: &'t [AtomicU64],
aux: &'t [AtomicU64],
}
impl Definitions<'_> {
#[inline]
fn get(&self, index: usize) -> Definition {
let kind = DefinitionKind::from_u8(self.kind[index].load(Ordering::Relaxed));
if kind == DefinitionKind::Undefined {
return Definition::undefined();
}
Definition {
kind,
file: FileId::from_u32(self.file[index].load(Ordering::Relaxed)),
index: self.index[index].load(Ordering::Relaxed),
position: InputPosition::from_raw(self.position[index].load(Ordering::Relaxed)),
aux: self.aux[index].load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbols::elf_reference::ElfReferenceRules;
fn names_from(strings: &[&'static str]) -> Vec<SymbolName<'static>> {
strings
.iter()
.map(|s| SymbolName::new(s.as_bytes()))
.collect()
}
#[test]
fn sequential_intern_numbers_by_first_sight() {
let mut table = SymbolTable::new();
let a = table.intern(SymbolName::new(b"a"));
let b = table.intern(SymbolName::new(b"b"));
assert_eq!(table.intern(SymbolName::new(b"a")), a);
assert_eq!((a.index(), b.index()), (0, 1));
assert_eq!(table.len(), 2);
assert_eq!(table.name(b).bytes(), b"b");
assert_eq!(table.lookup(&SymbolName::new(b"b")), Some(b));
assert_eq!(table.lookup(&SymbolName::new(b"c")), None);
}
#[test]
fn versions_are_distinct_symbols() {
let mut table = SymbolTable::new();
let plain = table.intern(SymbolName::new(b"foo"));
let v1 = table.intern(SymbolName::with_version(b"foo", Some(b"V1")));
let v2 = table.intern(SymbolName::with_version(b"foo", Some(b"V2")));
assert_ne!(plain, v1);
assert_ne!(v1, v2);
assert_eq!(
table.lookup(&SymbolName::with_version(b"foo", Some(b"V1"))),
Some(v1)
);
}
#[test]
fn batch_ids_follow_first_occurrence_not_job_order() {
let late = names_from(&["shared", "late_only", "zzz"]);
let early = names_from(&["early_only", "shared", "aaa"]);
let mut late_ids = vec![SymbolId::new(0); late.len()];
let mut early_ids = vec![SymbolId::new(0); early.len()];
let mut table = SymbolTable::new();
table.intern(SymbolName::new(b"pre"));
let mut jobs = [
InternJob {
position: InputPosition::new(5, 0),
names: &late,
ids: &mut late_ids,
},
InternJob {
position: InputPosition::new(2, 0),
names: &early,
ids: &mut early_ids,
},
];
table.intern_batch(&mut jobs);
let id = |s: &str| table.lookup(&SymbolName::new(s.as_bytes())).unwrap();
assert_eq!(id("pre").index(), 0);
assert_eq!(id("early_only").index(), 1);
assert_eq!(id("shared").index(), 2);
assert_eq!(id("aaa").index(), 3);
assert_eq!(id("late_only").index(), 4);
assert_eq!(id("zzz").index(), 5);
assert_eq!(late_ids, [id("shared"), id("late_only"), id("zzz")]);
assert_eq!(early_ids, [id("early_only"), id("shared"), id("aaa")]);
let again = names_from(&["new", "shared"]);
let mut again_ids = vec![SymbolId::new(0); 2];
table.intern_batch(&mut [InternJob {
position: InputPosition::new(0, 0),
names: &again,
ids: &mut again_ids,
}]);
let shared = table.lookup(&SymbolName::new(b"shared")).unwrap();
assert_eq!(again_ids, [SymbolId::new(6), shared]);
assert_eq!(table.len(), 7);
}
#[test]
fn equal_positions_fall_back_to_name_order() {
let a = names_from(&["m", "b"]);
let b = names_from(&["k", "a"]);
let mut a_ids = vec![SymbolId::new(0); 2];
let mut b_ids = vec![SymbolId::new(0); 2];
let mut table = SymbolTable::new();
let position = InputPosition::new(1, 0);
table.intern_batch(&mut [
InternJob {
position,
names: &a,
ids: &mut a_ids,
},
InternJob {
position,
names: &b,
ids: &mut b_ids,
},
]);
assert_eq!(b_ids, [SymbolId::new(0), SymbolId::new(2)]);
assert_eq!(a_ids, [SymbolId::new(1), SymbolId::new(3)]);
}
#[test]
fn flags_and_definitions_are_per_symbol() {
let mut table = SymbolTable::new();
let a = table.intern(SymbolName::new(b"a"));
let b = table.intern(SymbolName::new(b"b"));
assert!(table.set_flags(a, SymbolFlags::NEEDS_GOT).is_empty());
assert!(table.flags(b).is_empty());
assert_eq!(table.definition(a), Definition::undefined());
assert_eq!(table.definition_file(a), None);
let rules = ElfReferenceRules;
let weak = Definition {
kind: DefinitionKind::Weak,
file: FileId::new(1),
index: 4,
position: InputPosition::new(1, 0),
aux: 0,
};
let strong = Definition {
kind: DefinitionKind::Regular,
file: FileId::new(2),
index: 9,
position: InputPosition::new(2, 0),
aux: 0,
};
assert!(table.insert_definition(&rules, a, &weak));
assert!(table.insert_definition(&rules, a, &strong));
assert!(!table.insert_definition(&rules, a, &weak));
assert_eq!(table.definition(a), strong);
assert_eq!(table.definition_synchronized(a), strong);
assert_eq!(table.definition_kind(a), DefinitionKind::Regular);
assert_eq!(table.definition_file(a), Some(FileId::new(2)));
table.replace_definition(a, &weak);
assert_eq!(table.definition(a), weak);
assert_eq!(table.definition(b), Definition::undefined());
}
#[test]
fn many_shards_and_growth() {
let storage: Vec<String> = (0..50_000).map(|i| format!("sym{i}")).collect();
let names: Vec<SymbolName<'_>> = storage
.iter()
.map(|s| SymbolName::new(s.as_bytes()))
.collect();
let mut table = SymbolTable::new();
let mut ids = vec![SymbolId::new(0); names.len()];
let (first, second) = names.split_at(25_000);
let (first_ids, second_ids) = ids.split_at_mut(25_000);
table.intern_batch(&mut [
InternJob {
position: InputPosition::new(1, 0),
names: second,
ids: second_ids,
},
InternJob {
position: InputPosition::new(0, 0),
names: first,
ids: first_ids,
},
]);
for (i, id) in ids.iter().enumerate() {
assert_eq!(id.index(), i);
assert_eq!(table.name(*id), names[i]);
assert_eq!(table.lookup(&names[i]), Some(*id));
}
}
fn intern_chunks(
table: &mut SymbolTable<'static>,
names: &[SymbolName<'static>],
chunk: usize,
same_position: bool,
) -> Result<Vec<SymbolId>> {
let mut ids = vec![SymbolId::new(0); names.len()];
let mut jobs: Vec<InternJob<'static, '_>> = names
.chunks(chunk)
.zip(ids.chunks_mut(chunk))
.enumerate()
.map(|(j, (names, ids))| InternJob {
position: InputPosition::new(if same_position { 0 } else { j as u32 }, 0),
names,
ids,
})
.collect();
table.try_intern_batch(&mut jobs)?;
drop(jobs);
Ok(ids)
}
fn leaked_names(prefix: &str, count: usize) -> Vec<SymbolName<'static>> {
(0..count)
.map(|i| {
let text: &'static str = Box::leak(format!("{prefix}{i}").into_boxed_str());
SymbolName::new(text.as_bytes())
})
.collect()
}
fn pool(threads: usize) -> rayon::ThreadPool {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
}
#[test]
fn overflow_is_an_error_and_leaves_the_table_unchanged() {
for (count, chunk, same_position) in [
(100, 7, false),
(100, 7, true),
(100_000, 5000, false),
(100_000, 5000, true),
] {
let what = format!("{count} names, same position: {same_position}");
let first = leaked_names("first", 10);
let names = leaked_names("batch", count);
pool(4).install(|| {
let mut table = SymbolTable::new();
table.set_limit(count);
intern_chunks(&mut table, &first, 3, false).unwrap();
let error = intern_chunks(&mut table, &names, chunk, same_position).unwrap_err();
assert!(matches!(error, Error::Limit(_)), "{what}: {error}");
assert_eq!(table.len(), 10, "{what}");
assert!(table.lookup(&names[0]).is_none(), "{what}");
assert!(table.lookup(&names[count - 1]).is_none(), "{what}");
assert_eq!(table.lookup(&first[9]), Some(SymbolId::new(9)), "{what}");
assert_eq!(table.try_intern(names[0]).unwrap(), SymbolId::new(10));
let rest = &names[1..count - 10];
let ids = intern_chunks(&mut table, rest, chunk, same_position).unwrap();
assert_eq!(table.len(), count, "{what}");
assert_eq!(table.lookup(&rest[0]), Some(SymbolId::new(11)), "{what}");
assert_eq!(table.lookup(&rest[rest.len() - 1]), ids.last().copied());
assert!(matches!(
table.try_intern(names[count - 1]),
Err(Error::Limit(_))
));
assert_eq!(table.try_intern(first[0]).unwrap(), SymbolId::new(0));
});
}
}
#[test]
#[should_panic(expected = "limit exceeded")]
fn intern_panics_on_overflow() {
let mut table = SymbolTable::new();
table.set_limit(1);
table.intern(SymbolName::new(b"a"));
table.intern(SymbolName::new(b"b"));
}
#[test]
fn ordered_and_parallel_paths_assign_the_same_ids() {
let base = leaked_names("n", 30_000);
let names: Vec<SymbolName<'static>> =
(0..90_000).map(|i| base[(i * 7919) % base.len()]).collect();
let sequential = |order: &mut dyn Iterator<Item = &SymbolName<'static>>| {
let mut table = SymbolTable::new();
order.map(|name| table.intern(*name)).collect::<Vec<_>>()
};
let parallel = pool(4).install(|| {
let mut table = SymbolTable::new();
intern_chunks(&mut table, &names, 300, false).unwrap()
});
assert!(parallel == sequential(&mut names.iter()), "parallel path");
const RANK: [usize; 3] = [2, 0, 1];
let mut table = SymbolTable::new();
let mut ordered = vec![SymbolId::new(0); names.len()];
for (batch, (names, ids)) in names.chunks(900).zip(ordered.chunks_mut(900)).enumerate() {
let mut jobs: Vec<InternJob<'static, '_>> = names
.chunks(300)
.zip(ids.chunks_mut(300))
.enumerate()
.map(|(j, (names, ids))| InternJob {
position: InputPosition::new((batch * 3 + RANK[j]) as u32, 0),
names,
ids,
})
.collect();
table.try_intern_batch(&mut jobs).unwrap();
}
let mut model = SymbolTable::new();
let mut expected = vec![SymbolId::new(0); names.len()];
for (names, ids) in names.chunks(900).zip(expected.chunks_mut(900)) {
let mut chunks: Vec<(usize, &[SymbolName<'static>], &mut [SymbolId])> = names
.chunks(300)
.zip(ids.chunks_mut(300))
.enumerate()
.map(|(j, (names, ids))| (RANK[j], names, ids))
.collect();
chunks.sort_by_key(|chunk| chunk.0);
for (_, names, ids) in chunks {
for (name, id) in names.iter().zip(ids) {
*id = model.intern(*name);
}
}
}
assert!(ordered == expected, "ordered path");
}
#[test]
fn partitioned_batches_match_sequential_interning() {
let base = leaked_names("p", 60_000);
let batches: Vec<Vec<SymbolName<'static>>> = (0..3)
.map(|b| {
(0..70_000)
.map(|i| base[(i * 7919 + b * 20_000) % (20_000 * (b + 1))])
.collect()
})
.collect();
let mut model = SymbolTable::new();
let expected: Vec<Vec<SymbolId>> = batches
.iter()
.map(|names| names.iter().map(|name| model.intern(*name)).collect())
.collect();
for threads in [2, 4] {
let got: Vec<Vec<SymbolId>> = pool(threads).install(|| {
let mut table = SymbolTable::new();
batches
.iter()
.map(|names| {
let mut ids = vec![SymbolId::new(0); names.len()];
let cuts = [0, 1, 999, 999, 30_000, names.len()];
let mut jobs: Vec<InternJob<'static, '_>> = Vec::new();
let mut rest: &mut [SymbolId] = &mut ids;
for (j, pair) in cuts.windows(2).enumerate() {
let (head, tail) = rest.split_at_mut(pair[1] - pair[0]);
rest = tail;
jobs.push(InternJob {
position: InputPosition::new(j as u32, 0),
names: &names[pair[0]..pair[1]],
ids: head,
});
}
jobs.reverse();
table.try_intern_batch(&mut jobs).unwrap();
drop(jobs);
ids
})
.collect()
});
assert!(got == expected, "{threads} threads");
}
}
}