use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, PoisonError};
use rayon::prelude::*;
use super::definition::{Definition, DefinitionKind, Resolver};
use super::flags::SymbolFlags;
use super::name::{InputPosition, SymbolName};
use super::report::{DuplicateSymbol, SymbolReference, UndefinedSymbol};
use super::table::{InternJob, LookupView, SymbolTable};
use super::util::select_mut;
use crate::error::{Error, Result};
use crate::ids::{FileId, SymbolId};
const MIN_PARALLEL_SYMBOLS: usize = 1024;
const MIN_PARALLEL_WORK: usize = 4096;
const MIN_PARALLEL_SIZING: usize = 1 << 20;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SymbolUse {
Reference {
weak: bool,
},
Definition {
kind: DefinitionKind,
aux: u64,
},
Ignore,
}
pub trait ResolveFile<'a>: Send + Sync {
fn position(&self) -> InputPosition;
fn is_live_at_start(&self) -> bool;
fn lazy_names(&self) -> &[SymbolName<'a>];
fn load(&mut self) -> Result<()>;
fn symbol_names(&self) -> &[SymbolName<'a>];
fn symbol_use(&self, index: usize) -> SymbolUse;
fn can_load_early(&self) -> bool {
false
}
fn unload(&mut self) {}
}
#[derive(Debug)]
pub struct RoundFile<'r, F> {
pub id: FileId,
pub file: &'r mut F,
pub rank: Option<u32>,
}
pub trait RoundHook<F> {
fn load_hook(&self) -> Option<&dyn LoadHook<F>> {
None
}
fn after_load(&mut self, round: usize, files: &mut [RoundFile<'_, F>]) -> Result<()> {
let _ = (round, files);
Ok(())
}
}
impl<F> RoundHook<F> for () {}
pub trait LoadHook<F>: Sync {
fn keeps_names(&self, file: &F) -> bool;
fn prepare(&self, id: FileId, file: &mut F) -> Result<()> {
let _ = (id, file);
Ok(())
}
fn on_load(&self, round: usize, id: FileId, rank: u32, file: &mut F) -> Result<()>;
}
#[derive(Debug)]
pub struct Resolution<'a> {
live: Vec<bool>,
symbol_ids: Vec<Vec<SymbolId>>,
extracted: Vec<Vec<FileId>>,
undefined: Vec<UndefinedSymbol<'a>>,
duplicates: Vec<DuplicateSymbol<'a>>,
}
impl<'a> Resolution<'a> {
#[must_use]
pub fn is_live(&self, file: FileId) -> bool {
self.live[file.index()]
}
pub fn live_files(&self) -> impl Iterator<Item = FileId> + '_ {
self.live
.iter()
.enumerate()
.filter(|&(_, &live)| live)
.map(|(index, _)| FileId::new(index))
}
#[must_use]
pub fn symbol_ids(&self, file: FileId) -> &[SymbolId] {
&self.symbol_ids[file.index()]
}
#[must_use]
pub fn extracted(&self) -> &[Vec<FileId>] {
&self.extracted
}
#[must_use]
pub fn undefined(&self) -> &[UndefinedSymbol<'a>] {
&self.undefined
}
#[must_use]
pub fn duplicates(&self) -> &[DuplicateSymbol<'a>] {
&self.duplicates
}
}
#[derive(Clone, Copy)]
enum Step {
Load,
Hook,
Intern,
Insert,
Choose,
Report,
Prefetch,
}
const STEP_NAMES: [&str; 7] = [
"load", "hook", "intern", "insert", "choose", "report", "prefetch",
];
struct Steps {
last: Option<std::time::Instant>,
first: [f64; 7],
later: [f64; 7],
}
impl Steps {
fn new() -> Self {
Self {
last: std::env::var_os("QLD_TIMING").map(|_| std::time::Instant::now()),
first: [0.0; 7],
later: [0.0; 7],
}
}
fn lap(&mut self, round: usize, step: Step) {
let Some(last) = &mut self.last else {
return;
};
let now = std::time::Instant::now();
let ms = now.duration_since(*last).as_secs_f64() * 1000.0;
*last = now;
let totals = if round == 0 {
&mut self.first
} else {
&mut self.later
};
totals[step as usize] += ms;
}
fn print(&self, rounds: usize) {
if self.last.is_none() {
return;
}
for (name, ms) in STEP_NAMES.iter().zip(&self.first) {
if *ms > 0.0 {
eprintln!("qld-lap: resolve round 0 {name}: {ms:.2} ms");
}
}
for (name, ms) in STEP_NAMES.iter().zip(&self.later) {
if *ms > 0.0 {
eprintln!("qld-lap: resolve rounds 1+ {name}: {ms:.2} ms");
}
}
eprintln!("qld-lap: resolve rounds (count): {rounds} ms");
}
}
#[derive(Clone, Copy)]
struct Work<'s> {
file: usize,
ids: &'s [SymbolId],
lazy: bool,
}
fn map_symbols<'s, F, T, W>(files: &[F], work: &[Work<'s>], f: W) -> Vec<T>
where
F: Sync,
T: Send,
W: Fn(&F, Work<'s>, usize, SymbolId) -> Option<T> + Sync,
{
let f = &f;
let sequential = move |w: &Work<'s>| {
let (file, w) = (&files[w.file], *w);
w.ids
.iter()
.enumerate()
.filter_map(move |(symbol, &id)| f(file, w, symbol, id))
};
let total: usize = work.iter().map(|w| w.ids.len()).sum();
if total < MIN_PARALLEL_WORK {
return work.iter().flat_map(sequential).collect();
}
let (large, small): (Vec<Work<'s>>, Vec<Work<'s>>) = work
.iter()
.partition(|w| w.ids.len() >= 2 * MIN_PARALLEL_SYMBOLS);
let small_total: usize = small.iter().map(|w| w.ids.len()).sum();
let files_per_task = (MIN_PARALLEL_SYMBOLS * small.len() / small_total.max(1)).max(1);
small
.par_iter()
.with_min_len(files_per_task)
.flat_map_iter(sequential)
.chain(large.par_iter().flat_map(move |w| {
let (file, w) = (&files[w.file], *w);
w.ids
.par_iter()
.enumerate()
.with_min_len(MIN_PARALLEL_SYMBOLS)
.filter_map(move |(symbol, &id)| f(file, w, symbol, id))
}))
.collect()
}
fn sort_unstable_by_key<T, K, G>(items: &mut [T], key: G)
where
T: Send,
K: Ord,
G: Fn(&T) -> K + Sync,
{
if items.len() < MIN_PARALLEL_WORK {
items.sort_unstable_by_key(key);
} else {
items.par_sort_unstable_by_key(key);
}
}
pub fn resolve_symbols<'a, F, R>(
table: &mut SymbolTable<'a>,
resolver: &R,
files: &mut [F],
) -> Result<Resolution<'a>>
where
F: ResolveFile<'a>,
R: Resolver + ?Sized,
{
resolve_symbols_with(table, resolver, files, &mut ())
}
pub fn resolve_symbols_with<'a, F, R, H>(
table: &mut SymbolTable<'a>,
resolver: &R,
files: &mut [F],
hook: &mut H,
) -> Result<Resolution<'a>>
where
F: ResolveFile<'a>,
R: Resolver + ?Sized,
H: RoundHook<F> + ?Sized,
{
if u32::try_from(files.len()).is_err() {
return Err(Error::Limit(format!(
"{} input files (at most {})",
files.len(),
u32::MAX
)));
}
let count = files.len();
let mut live = vec![false; count];
let mut symbol_ids: Vec<Vec<SymbolId>> = (0..count).map(|_| Vec::new()).collect();
let mut lazy_ids: Vec<Vec<SymbolId>> = (0..count).map(|_| Vec::new()).collect();
let (mut load, mut lazy): (Vec<usize>, Vec<usize>) =
(0..count).partition(|&index| files[index].is_live_at_start());
let mut extracted = Vec::new();
let mut early: Vec<Option<Result<Option<usize>>>> = (0..count).map(|_| None).collect();
let mut loaded_early = Vec::new();
let ranks: Option<Vec<u32>> = {
let mut order: Vec<usize> = (0..count).collect();
order.sort_unstable_by_key(|&index| files[index].position());
order
.windows(2)
.all(|pair| files[pair[0]].position() != files[pair[1]].position())
.then(|| {
let mut ranks = vec![0u32; count];
for (rank, &index) in order.iter().enumerate() {
ranks[index] = rank as u32;
}
ranks
})
};
let with_load_hook = hook.load_hook().is_some() && ranks.is_some();
let mut steps = Steps::new();
for round in 0.. {
let looked_up = with_load_hook && round > 0;
let looked = {
let mut loaded = select_mut(files, &load);
let view = looked_up.then(|| table.lookup_view());
let outputs = looked_up.then(|| select_mut(&mut symbol_ids, &load));
let looked = load_files(
&mut loaded,
&load,
round,
hook.load_hook().zip(ranks.as_deref()),
view.as_ref().zip(outputs),
select_mut(&mut early, &load),
)?;
steps.lap(round, Step::Load);
let mut round_files: Vec<RoundFile<'_, F>> = load
.iter()
.zip(loaded)
.map(|(&index, file)| RoundFile {
id: FileId::new(index),
file,
rank: ranks
.as_ref()
.filter(|_| with_load_hook)
.map(|ranks| ranks[index]),
})
.collect();
round_files.sort_by_key(|entry| (entry.file.position(), entry.id));
hook.after_load(round, &mut round_files)?;
steps.lap(round, Step::Hook);
looked
};
for &index in &load {
live[index] = true;
}
let files_ref = &*files;
if looked_up {
if looked
.iter()
.any(|looked| looked.is_none_or(|missing| missing > 0))
{
let mut jobs: Vec<InternJob<'a, '_>> = load
.iter()
.zip(select_mut(&mut symbol_ids, &load))
.zip(&looked)
.map(|((&index, ids), looked)| {
let names = files_ref[index].symbol_names();
if looked.is_none() {
ids.clear();
ids.resize(names.len(), LookupView::MISSING);
}
InternJob {
position: files_ref[index].position(),
names,
ids: ids.as_mut_slice(),
}
})
.collect();
table.try_intern_missing(&mut jobs)?;
}
} else {
intern_round(
table,
files_ref,
&load,
&lazy,
&mut symbol_ids,
&mut lazy_ids,
)?;
}
steps.lap(round, Step::Intern);
let work: Vec<Work<'_>> = load
.iter()
.map(|&file| Work {
file,
ids: &symbol_ids[file],
lazy: false,
})
.chain(lazy.iter().map(|&file| Work {
file,
ids: &lazy_ids[file],
lazy: true,
}))
.collect();
let table_ref = &*table;
let candidates = insert_and_mark(table_ref, resolver, files_ref, &work);
steps.lap(round, Step::Insert);
let live_ref = &live;
let choose = |&id: &SymbolId| {
let current = table_ref.definition(id);
if !current.is_defined() || !resolver.extracts(¤t) {
return None;
}
let member = current.file.index();
(member < count && !live_ref[member]).then_some(member)
};
let mut members: Vec<usize> = if candidates.len() < MIN_PARALLEL_WORK {
candidates.iter().filter_map(choose).collect()
} else {
candidates.par_iter().filter_map(choose).collect()
};
sort_unstable_by_key(&mut members, |&member| member);
members.dedup();
steps.lap(round, Step::Choose);
if members.is_empty() {
break;
}
if round == 0
&& with_load_hook
&& let Some(load_hook) = hook.load_hook()
{
loaded_early = prefetch(
files,
table,
load_hook,
&live,
&members,
&mut symbol_ids,
&mut early,
);
steps.lap(round, Step::Prefetch);
}
for &member in &members {
lazy_ids[member] = Vec::new();
}
let mut round_members: Vec<FileId> = members.iter().copied().map(FileId::new).collect();
round_members.sort_by_key(|file| (files[file.index()].position(), *file));
extracted.push(round_members);
load = members;
lazy = Vec::new();
}
drop(lazy_ids);
for index in loaded_early {
if !live[index] {
files[index].unload();
symbol_ids[index] = Vec::new();
}
}
let live_work: Vec<Work<'_>> = live
.iter()
.enumerate()
.filter(|&(_, &live)| live)
.map(|(file, _)| Work {
file,
ids: &symbol_ids[file],
lazy: false,
})
.collect();
let undefined = collect_undefined(table, files, &live_work);
let duplicates = collect_duplicates(table, resolver, files, &live_work);
steps.lap(0, Step::Report);
steps.print(extracted.len() + 1);
Ok(Resolution {
live,
symbol_ids,
extracted,
undefined,
duplicates,
})
}
fn prefetch<'a, F>(
files: &mut [F],
table: &mut SymbolTable<'a>,
load_hook: &dyn LoadHook<F>,
live: &[bool],
start: &[usize],
symbol_ids: &mut [Vec<SymbolId>],
early: &mut [Option<Result<Option<usize>>>],
) -> Vec<usize>
where
F: ResolveFile<'a>,
{
let claimed: Vec<AtomicBool> = live.iter().map(|&live| AtomicBool::new(live)).collect();
let prefetcher = Prefetcher {
slots: files
.iter_mut()
.zip(symbol_ids.iter_mut())
.zip(early.iter_mut())
.map(|((file, ids), early)| Mutex::new(Some((file, ids, early))))
.collect(),
claimed,
view: table.lookup_view(),
load_hook,
};
rayon::scope(|scope| {
for &member in start {
if !prefetcher.claimed[member].swap(true, Ordering::Relaxed) {
let prefetcher = &prefetcher;
scope.spawn(move |scope| prefetcher.visit(member, scope));
}
}
});
prefetcher
.slots
.iter()
.enumerate()
.filter(|(_, slot)| {
slot.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.is_some_and(|(_, _, early)| early.is_some())
})
.map(|(index, _)| index)
.collect()
}
type PrefetchSlot<'p, F> = Option<(
&'p mut F,
&'p mut Vec<SymbolId>,
&'p mut Option<Result<Option<usize>>>,
)>;
struct Prefetcher<'p, 'a, F> {
slots: Vec<Mutex<PrefetchSlot<'p, F>>>,
claimed: Vec<AtomicBool>,
view: LookupView<'p, 'a>,
load_hook: &'p dyn LoadHook<F>,
}
impl<'a, F> Prefetcher<'_, 'a, F>
where
F: ResolveFile<'a>,
{
fn visit<'s>(&'s self, index: usize, scope: &rayon::Scope<'s>) {
let mut slot = self.slots[index]
.lock()
.unwrap_or_else(PoisonError::into_inner);
let Some((file, ids, early)) = slot.as_mut() else {
return;
};
if !file.can_load_early() {
return;
}
let loaded = file
.load()
.and_then(|()| self.load_hook.prepare(FileId::new(index), file));
if let Err(error) = loaded {
**early = Some(Err(error));
return;
}
if !self.load_hook.keeps_names(file) {
**early = Some(Ok(None));
return;
}
let names = file.symbol_names();
ids.clear();
ids.resize(names.len(), SymbolId::from_u32(0));
**early = Some(Ok(Some(self.view.find_all(names, ids))));
for (symbol, &id) in ids.iter().enumerate() {
if !LookupView::is_found(id)
|| file.symbol_use(symbol) != (SymbolUse::Reference { weak: false })
{
continue;
}
let Some(member) = self.view.lazy_file(id) else {
continue;
};
let member = member.index();
if self
.claimed
.get(member)
.is_some_and(|claimed| !claimed.swap(true, Ordering::Relaxed))
{
scope.spawn(move |scope| self.visit(member, scope));
}
}
}
}
fn load_files<'a, F: ResolveFile<'a>>(
files: &mut [&mut F],
indices: &[usize],
round: usize,
load_hook: Option<(&dyn LoadHook<F>, &[u32])>,
lookup: Option<(&LookupView<'_, 'a>, Vec<&mut Vec<SymbolId>>)>,
early: Vec<&mut Option<Result<Option<usize>>>>,
) -> Result<Vec<Option<usize>>> {
let (view, outputs): (_, Vec<Option<&mut Vec<SymbolId>>>) = match lookup {
Some((view, outputs)) => (Some(view), outputs.into_iter().map(Some).collect()),
None => (None, (0..files.len()).map(|_| None).collect()),
};
let one = |file: &mut F,
index: usize,
ids: Option<&mut Vec<SymbolId>>,
early: &mut Option<Result<Option<usize>>>|
-> Result<Option<usize>> {
let id = FileId::new(index);
let mut looked = match early.take() {
Some(result) => result?,
None => {
file.load()?;
if let Some((load_hook, _)) = load_hook {
load_hook.prepare(id, file)?;
}
None
}
};
if looked.is_none()
&& let (Some(ids), Some(view), Some((load_hook, _))) = (ids, view, load_hook)
&& load_hook.keeps_names(file)
{
let names = file.symbol_names();
ids.clear();
ids.resize(names.len(), SymbolId::from_u32(0));
looked = Some(view.find_all(names, ids));
}
if let Some((load_hook, ranks)) = load_hook {
load_hook.on_load(round, id, ranks[index], file)?;
}
Ok(looked)
};
let results: Vec<(InputPosition, usize, Result<Option<usize>>)> = files
.par_iter_mut()
.zip(indices.par_iter())
.zip(outputs)
.zip(early)
.map(|(((file, &index), ids), early)| {
(file.position(), index, one(file, index, ids, early))
})
.collect();
let mut looked = Vec::with_capacity(results.len());
let mut first_error: Option<(InputPosition, usize, Error)> = None;
for (position, index, result) in results {
match result {
Ok(count) => looked.push(count),
Err(error) => {
if first_error
.as_ref()
.is_none_or(|(p, i, _)| (position, index) < (*p, *i))
{
first_error = Some((position, index, error));
}
}
}
}
match first_error {
Some((_, _, error)) => Err(error),
None => Ok(looked),
}
}
fn intern_round<'a, F: ResolveFile<'a>>(
table: &mut SymbolTable<'a>,
files: &[F],
load: &[usize],
lazy: &[usize],
symbol_ids: &mut [Vec<SymbolId>],
lazy_ids: &mut [Vec<SymbolId>],
) -> Result<()> {
let names = |index: usize, is_lazy: bool| {
let file = &files[index];
if is_lazy {
file.lazy_names()
} else {
file.symbol_names()
}
};
let repeats = repeated_lazy_files(files, lazy);
let interned: Vec<usize> = lazy
.iter()
.zip(&repeats)
.filter(|(_, repeat)| repeat.is_none())
.map(|(&index, _)| index)
.collect();
let mut outputs: Vec<(usize, bool, &mut Vec<SymbolId>)> = load
.iter()
.zip(select_mut(symbol_ids, load))
.map(|(&index, ids)| (index, false, ids))
.chain(
interned
.iter()
.zip(select_mut(lazy_ids, &interned))
.map(|(&index, ids)| (index, true, ids)),
)
.collect();
let total: usize = outputs
.iter()
.map(|(index, is_lazy, _)| names(*index, *is_lazy).len())
.sum();
let size = |(index, is_lazy, ids): &mut (usize, bool, &mut Vec<SymbolId>)| {
ids.clear();
ids.resize(names(*index, *is_lazy).len(), SymbolId::from_u32(0));
};
if total < MIN_PARALLEL_SIZING {
outputs.iter_mut().for_each(size);
} else {
outputs.par_iter_mut().for_each(size);
}
let mut jobs: Vec<InternJob<'a, '_>> = outputs
.into_iter()
.map(|(index, is_lazy, ids)| InternJob {
position: files[index].position(),
names: names(index, is_lazy),
ids: ids.as_mut_slice(),
})
.collect();
table.try_intern_batch(&mut jobs)?;
drop(jobs);
for (&index, repeat) in lazy.iter().zip(&repeats) {
if let &Some(original) = repeat {
lazy_ids[index] = lazy_ids[original].clone();
}
}
Ok(())
}
fn repeated_lazy_files<'a, F: ResolveFile<'a>>(files: &[F], lazy: &[usize]) -> Vec<Option<usize>> {
let same = |a: &SymbolName<'_>, b: &SymbolName<'_>| {
std::ptr::eq(a.bytes(), b.bytes())
&& a.version().map(<[u8]>::as_ptr) == b.version().map(<[u8]>::as_ptr)
&& a.version().map(<[u8]>::len) == b.version().map(<[u8]>::len)
};
let mut first: hashbrown::HashMap<(usize, usize), usize> = hashbrown::HashMap::new();
let candidates: Vec<Option<usize>> = lazy
.iter()
.map(|&index| {
let names = files[index].lazy_names();
let head = names.first()?;
let key = (names.len(), head.bytes().as_ptr() as usize);
match first.get(&key) {
Some(&original) if files[original].position() < files[index].position() => {
Some(original)
}
Some(_) => None,
None => {
first.insert(key, index);
None
}
}
})
.collect();
candidates
.into_par_iter()
.zip(lazy.par_iter())
.map(|(candidate, &index)| {
candidate.filter(|&original| {
files[original]
.lazy_names()
.iter()
.zip(files[index].lazy_names())
.all(|(a, b)| same(a, b))
})
})
.collect()
}
fn insert_and_mark<'a, F, R>(
table: &SymbolTable<'a>,
resolver: &R,
files: &[F],
work: &[Work<'_>],
) -> Vec<SymbolId>
where
F: ResolveFile<'a>,
R: Resolver + ?Sized,
{
map_symbols(files, work, |file, w, symbol, id| {
let (kind, aux) = if w.lazy {
(DefinitionKind::Lazy, 0)
} else {
match file.symbol_use(symbol) {
SymbolUse::Definition { kind, aux } => (kind, aux),
SymbolUse::Reference { weak: false } => {
let before = table.set_flags(id, SymbolFlags::REFERENCED);
return (!before.contains(SymbolFlags::REFERENCED)).then_some(id);
}
SymbolUse::Reference { weak: true } => {
table.set_flags(id, SymbolFlags::WEAK_REFERENCED);
return None;
}
SymbolUse::Ignore => return None,
}
};
let candidate = Definition {
kind,
file: FileId::new(w.file),
index: u32::try_from(symbol).unwrap_or(u32::MAX),
position: file.position(),
aux,
};
let won = table.insert_definition(resolver, id, &candidate);
(won && resolver.extracts(&candidate) && table.flags(id).contains(SymbolFlags::REFERENCED))
.then_some(id)
})
}
fn collect_undefined<'a, F: ResolveFile<'a>>(
table: &SymbolTable<'a>,
files: &[F],
live_work: &[Work<'_>],
) -> Vec<UndefinedSymbol<'a>> {
let mut references: Vec<(SymbolId, SymbolReference)> =
map_symbols(files, live_work, |file, w, symbol, id| {
if file.symbol_use(symbol) != (SymbolUse::Reference { weak: false }) {
return None;
}
let kind = table.definition_kind(id);
if kind != DefinitionKind::Undefined && kind != DefinitionKind::Lazy {
return None;
}
Some((
id,
SymbolReference {
position: file.position(),
file: FileId::new(w.file),
index: u32::try_from(symbol).unwrap_or(u32::MAX),
},
))
});
sort_unstable_by_key(&mut references, |&entry| entry);
let mut undefined: Vec<UndefinedSymbol<'a>> = references
.chunk_by(|a, b| a.0 == b.0)
.map(|group| UndefinedSymbol {
symbol: group[0].0,
name: table.name(group[0].0),
references: group.iter().map(|&(_, reference)| reference).collect(),
})
.collect();
undefined.sort_unstable_by_key(|entry| (entry.references[0], entry.symbol));
undefined
}
fn collect_duplicates<'a, F, R>(
table: &SymbolTable<'a>,
resolver: &R,
files: &[F],
live_work: &[Work<'_>],
) -> Vec<DuplicateSymbol<'a>>
where
F: ResolveFile<'a>,
R: Resolver + ?Sized,
{
let mut losers: Vec<(SymbolId, Definition)> =
map_symbols(files, live_work, |file, w, symbol, id| {
let SymbolUse::Definition { kind, aux } = file.symbol_use(symbol) else {
return None;
};
if kind == DefinitionKind::Undefined {
return None;
}
let definition = Definition {
kind,
file: FileId::new(w.file),
index: u32::try_from(symbol).unwrap_or(u32::MAX),
position: file.position(),
aux,
};
let winner = table.definition(id);
(winner != definition && resolver.is_duplicate(&winner, &definition))
.then_some((id, definition))
});
sort_unstable_by_key(&mut losers, |(id, definition)| (*id, definition.tie_key()));
let mut duplicates: Vec<DuplicateSymbol<'a>> = losers
.chunk_by(|a, b| a.0 == b.0)
.map(|group| DuplicateSymbol {
symbol: group[0].0,
name: table.name(group[0].0),
winner: table.definition(group[0].0),
others: group.iter().map(|&(_, definition)| definition).collect(),
})
.collect();
duplicates.sort_unstable_by_key(|entry| (entry.winner.tie_key(), entry.symbol));
duplicates
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::symbols::elf_reference::ElfReferenceRules;
struct Mock {
position: InputPosition,
live_at_start: bool,
lazy: Vec<SymbolName<'static>>,
names: Vec<SymbolName<'static>>,
uses: Vec<SymbolUse>,
loads: usize,
fail: bool,
hooked: bool,
early: bool,
unloads: usize,
}
fn parse(spec: &'static str) -> (SymbolName<'static>, SymbolUse) {
let (tag, name) = spec.split_once(':').unwrap();
let use_ = match tag {
"D" => SymbolUse::Definition {
kind: DefinitionKind::Regular,
aux: 0,
},
"W" => SymbolUse::Definition {
kind: DefinitionKind::Weak,
aux: 0,
},
"S" => SymbolUse::Definition {
kind: DefinitionKind::Shared,
aux: 0,
},
"U" => SymbolUse::Reference { weak: false },
"u" => SymbolUse::Reference { weak: true },
common => SymbolUse::Definition {
kind: DefinitionKind::Common,
aux: common[1..].parse().unwrap(),
},
};
(SymbolName::new(name.as_bytes()), use_)
}
fn file(position: InputPosition, live: bool, specs: &[&'static str]) -> Mock {
let (names, uses): (Vec<_>, Vec<_>) = specs.iter().map(|spec| parse(spec)).unzip();
let lazy = names
.iter()
.zip(&uses)
.filter(|(_, use_)| matches!(use_, SymbolUse::Definition { .. }))
.map(|(name, _)| *name)
.collect();
Mock {
position,
live_at_start: live,
lazy,
names,
uses,
loads: 0,
fail: false,
hooked: false,
early: false,
unloads: 0,
}
}
fn object(input: u32, specs: &[&'static str]) -> Mock {
file(InputPosition::new(input, 0), true, specs)
}
fn member(input: u32, member: u32, specs: &[&'static str]) -> Mock {
file(InputPosition::new(input, member), false, specs)
}
impl<'a> ResolveFile<'a> for Mock {
fn position(&self) -> InputPosition {
self.position
}
fn is_live_at_start(&self) -> bool {
self.live_at_start
}
fn lazy_names(&self) -> &[SymbolName<'a>] {
&self.lazy
}
fn load(&mut self) -> Result<()> {
self.loads += 1;
assert_eq!(self.loads, 1, "file loaded twice");
if self.fail {
return Err(Error::malformed(
format!("input{}", self.position.input()),
0,
"test",
));
}
Ok(())
}
fn symbol_names(&self) -> &[SymbolName<'a>] {
assert!(
self.loads == 1 && (self.hooked || self.early),
"symbols read too early"
);
&self.names
}
fn symbol_use(&self, index: usize) -> SymbolUse {
assert!(
self.loads == 1 && (self.hooked || self.early),
"symbols read too early"
);
self.uses[index]
}
fn can_load_early(&self) -> bool {
self.early
}
fn unload(&mut self) {
assert_eq!(self.loads, 1, "unloaded but not loaded");
self.unloads += 1;
}
}
#[derive(Default)]
struct Recorder {
rounds: Vec<Vec<(InputPosition, usize)>>,
fail_in_round: Option<usize>,
}
impl RoundHook<Mock> for Recorder {
fn after_load(&mut self, round: usize, files: &mut [RoundFile<'_, Mock>]) -> Result<()> {
assert_eq!(round, self.rounds.len());
let mut entries = Vec::new();
for entry in files.iter_mut() {
assert_eq!(entry.file.loads, 1, "hook before load");
assert!(!entry.file.hooked, "file handed to the hook twice");
entry.file.hooked = true;
entries.push((entry.file.position, entry.id.index()));
}
self.rounds.push(entries);
if self.fail_in_round == Some(round) {
return Err(Error::Internal(format!("hook failed in round {round}")));
}
Ok(())
}
}
fn run(files: &mut [Mock]) -> (SymbolTable<'static>, Resolution<'static>) {
let mut table = SymbolTable::new();
let resolution = resolve_symbols_with(
&mut table,
&ElfReferenceRules,
files,
&mut Recorder::default(),
)
.unwrap();
(table, resolution)
}
fn id(table: &SymbolTable<'_>, name: &str) -> SymbolId {
table.lookup(&SymbolName::new(name.as_bytes())).unwrap()
}
fn live(resolution: &Resolution<'_>) -> Vec<usize> {
resolution.live_files().map(FileId::index).collect()
}
#[test]
fn repeated_archive_gets_the_ids_of_the_first_copy() {
let specs: [&[&'static str]; 3] = [&["D:f", "U:h"], &["D:g", "D:h"], &["D:x", "U:y"]];
let build = |shared: bool| {
let mut files = vec![object(0, &["U:f", "U:g", "D:main"])];
let first: Vec<Mock> = specs
.iter()
.enumerate()
.map(|(i, s)| member(1, i as u32, s))
.collect();
let mut second: Vec<Mock> = specs
.iter()
.enumerate()
.map(|(i, s)| member(3, i as u32, s))
.collect();
for (copy, original) in second.iter_mut().zip(&first) {
if shared {
copy.lazy = original.lazy.clone();
copy.names = original.names.clone();
} else {
let fresh = |name: &SymbolName<'static>| {
SymbolName::new(Box::leak(name.bytes().to_vec().into_boxed_slice()))
};
copy.lazy = original.lazy.iter().map(fresh).collect();
copy.names = original.names.iter().map(fresh).collect();
}
}
files.extend(first);
files.push(object(2, &["U:x", "D:z"]));
files.extend(second);
files
};
let mut shared = build(true);
let mut copied = build(false);
let repeats = repeated_lazy_files(&shared, &[1, 2, 3, 5, 6, 7]);
assert_eq!(repeats, [None, None, None, Some(1), Some(2), Some(3)]);
let (table_a, resolution_a) = run(&mut shared);
let (table_b, resolution_b) = run(&mut copied);
let names = |table: &SymbolTable<'_>| {
table
.names()
.iter()
.map(|name| name.bytes().to_vec())
.collect::<Vec<_>>()
};
assert_eq!(names(&table_a), names(&table_b));
assert_eq!(live(&resolution_a), live(&resolution_b));
for file in 0..shared.len() {
let file = FileId::new(file);
assert_eq!(resolution_a.symbol_ids(file), resolution_b.symbol_ids(file));
}
}
#[test]
fn extracts_transitively_and_skips_unneeded_members() {
let mut files = [
object(0, &["U:foo", "D:main"]),
member(1, 0, &["D:foo", "U:bar"]),
member(1, 1, &["D:bar"]),
member(1, 2, &["D:unused"]),
];
let (table, resolution) = run(&mut files);
assert_eq!(live(&resolution), [0, 1, 2]);
assert_eq!(
resolution.extracted(),
[vec![FileId::new(1)], vec![FileId::new(2)]]
);
assert!(resolution.undefined().is_empty());
assert!(resolution.duplicates().is_empty());
assert_eq!(
table.definition_file(id(&table, "bar")),
Some(FileId::new(2))
);
assert_eq!(
table.definition_kind(id(&table, "unused")),
DefinitionKind::Lazy
);
assert!(
table
.flags(id(&table, "foo"))
.contains(SymbolFlags::REFERENCED)
);
assert_eq!(resolution.symbol_ids(FileId::new(3)), []);
assert_eq!(
resolution.symbol_ids(FileId::new(1)),
[id(&table, "foo"), id(&table, "bar")]
);
}
#[test]
fn archive_before_its_user_is_still_searched() {
let mut files = [member(0, 0, &["D:foo"]), object(1, &["U:foo"])];
let (_, resolution) = run(&mut files);
assert_eq!(live(&resolution), [0, 1]);
assert!(resolution.undefined().is_empty());
}
#[test]
fn earliest_member_wins() {
let mut files = [
member(5, 0, &["D:foo"]),
object(0, &["U:foo"]),
member(2, 3, &["D:foo"]),
member(2, 4, &["D:foo"]),
];
let (table, resolution) = run(&mut files);
assert_eq!(live(&resolution), [1, 2]);
assert_eq!(
table.definition_file(id(&table, "foo")),
Some(FileId::new(2))
);
}
#[test]
fn weak_references_and_existing_definitions_do_not_extract() {
let mut files = [
object(
0,
&["u:weakref", "U:has_weak", "U:has_shared", "W:has_weak"],
),
object(1, &["S:has_shared"]),
member(2, 0, &["D:weakref"]),
member(2, 1, &["D:has_weak"]),
member(2, 2, &["D:has_shared"]),
];
let (table, resolution) = run(&mut files);
assert_eq!(live(&resolution), [0, 1]);
assert!(resolution.undefined().is_empty());
let weakref = id(&table, "weakref");
assert!(table.flags(weakref).contains(SymbolFlags::WEAK_REFERENCED));
assert!(!table.flags(weakref).contains(SymbolFlags::REFERENCED));
assert_eq!(table.definition_kind(weakref), DefinitionKind::Lazy);
}
#[test]
fn reports_undefined_and_duplicates_in_order() {
let mut files = [
object(3, &["U:zeta", "D:dup", "U:alpha"]),
object(1, &["U:alpha", "D:dup", "C4:common", "u:weak_only"]),
object(2, &["D:dup", "C8:common", "U:alpha"]),
];
let (table, resolution) = run(&mut files);
let undefined: Vec<(String, Vec<usize>)> = resolution
.undefined()
.iter()
.map(|u| {
(
u.name.display().to_string(),
u.references.iter().map(|r| r.file.index()).collect(),
)
})
.collect();
assert_eq!(
undefined,
[
("alpha".to_string(), vec![1, 2, 0]),
("zeta".to_string(), vec![0])
]
);
let [duplicate] = resolution.duplicates() else {
panic!("expected one duplicate: {:?}", resolution.duplicates());
};
assert_eq!(duplicate.symbol, id(&table, "dup"));
assert_eq!(duplicate.winner.file, FileId::new(1));
let others: Vec<usize> = duplicate.others.iter().map(|d| d.file.index()).collect();
assert_eq!(others, [2, 0]);
let common = table.definition(id(&table, "common"));
assert_eq!((common.file.index(), common.aux), (2, 8));
}
#[test]
fn stale_lazy_index_terminates_and_reports() {
let mut lying = member(1, 0, &["D:real"]);
lying.lazy.push(SymbolName::new(b"ghost"));
let mut files = [object(0, &["U:ghost", "U:real"]), lying];
let (_, resolution) = run(&mut files);
assert_eq!(live(&resolution), [0, 1]);
let names: Vec<String> = resolution
.undefined()
.iter()
.map(|u| u.name.display().to_string())
.collect();
assert_eq!(names, ["ghost"]);
}
#[test]
fn load_error_of_earliest_input_is_returned() {
let mut files = [object(4, &["U:foo"]), object(2, &["U:bar"]), object(3, &[])];
files[0].fail = true;
files[2].fail = true;
let mut table = SymbolTable::new();
let error = resolve_symbols(&mut table, &ElfReferenceRules, &mut files).unwrap_err();
assert!(error.to_string().starts_with("input3"), "{error}");
}
#[test]
fn hook_sees_each_rounds_new_files_in_position_order() {
let mut files = [
member(3, 1, &["D:c"]),
object(2, &["U:a", "U:b"]),
member(3, 0, &["D:a", "U:c"]),
object(0, &["D:main"]),
member(1, 0, &["D:b"]),
member(1, 1, &["D:unused"]),
];
let mut recorder = Recorder::default();
let mut table = SymbolTable::new();
let resolution =
resolve_symbols_with(&mut table, &ElfReferenceRules, &mut files, &mut recorder)
.unwrap();
let p = InputPosition::new;
assert_eq!(
recorder.rounds,
[
vec![(p(0, 0), 3), (p(2, 0), 1)],
vec![(p(1, 0), 4), (p(3, 0), 2)],
vec![(p(3, 1), 0)],
]
);
assert_eq!(resolution.extracted().len(), 2);
assert!(!files[5].hooked);
}
#[derive(Default)]
struct EarlyHook {
loaded: std::sync::atomic::AtomicUsize,
rounds: usize,
}
impl LoadHook<Mock> for EarlyHook {
fn keeps_names(&self, _: &Mock) -> bool {
true
}
fn prepare(&self, _: FileId, file: &mut Mock) -> Result<()> {
assert_eq!(file.loads, 1, "hook before load");
assert!(!file.hooked, "file prepared twice");
file.hooked = true;
Ok(())
}
fn on_load(&self, round: usize, _: FileId, _: u32, file: &mut Mock) -> Result<()> {
assert_eq!(round, self.rounds);
assert!(file.hooked, "on_load before prepare");
self.loaded
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
}
impl RoundHook<Mock> for EarlyHook {
fn load_hook(&self) -> Option<&dyn LoadHook<Mock>> {
Some(self)
}
fn after_load(&mut self, round: usize, files: &mut [RoundFile<'_, Mock>]) -> Result<()> {
assert_eq!(round, self.rounds);
assert!(files.iter().all(|entry| entry.file.hooked));
assert!(files.iter().all(|entry| entry.rank.is_some()));
assert!(files.windows(2).all(|pair| pair[0].rank < pair[1].rank));
self.rounds += 1;
Ok(())
}
}
#[test]
fn names_looked_up_while_loading_get_the_same_ids() {
let build = || {
vec![
object(0, &["U:a", "D:main", "U:b"]),
member(1, 0, &["D:a", "U:late1", "U:c", "D:late2"]),
member(1, 1, &["D:b", "U:late2", "U:late3", "U:late1"]),
member(1, 2, &["D:c", "U:d", "u:late4"]),
member(2, 0, &["D:d", "U:late5", "D:late3"]),
member(2, 1, &["D:unused", "U:never"]),
]
};
let mut plain = build();
let (table_a, resolution_a) = run(&mut plain);
for threads in [1, 3] {
let mut early = build();
let mut table_b = SymbolTable::new();
let mut hook = EarlyHook::default();
let resolution_b = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
.install(|| {
resolve_symbols_with(&mut table_b, &ElfReferenceRules, &mut early, &mut hook)
})
.unwrap();
assert_eq!(table_a.names(), table_b.names());
assert_eq!(live(&resolution_a), live(&resolution_b));
assert_eq!(resolution_a.extracted(), resolution_b.extracted());
for file in 0..plain.len() {
let file = FileId::new(file);
assert_eq!(resolution_a.symbol_ids(file), resolution_b.symbol_ids(file));
}
assert_eq!(hook.rounds, resolution_b.extracted().len() + 1);
assert_eq!(
hook.loaded.load(std::sync::atomic::Ordering::Relaxed),
live(&resolution_b).len()
);
}
}
#[test]
fn members_loaded_early_resolve_the_same() {
let build = |early: bool| {
let mut files = vec![
object(0, &["U:a1", "U:c1", "D:main"]),
member(1, 0, &["D:s", "U:bad1"]),
member(1, 1, &["D:a1", "U:s", "U:y1"]),
member(1, 2, &["D:c1", "W:s"]),
member(2, 0, &["D:y1", "U:y2", "U:late"]),
member(2, 1, &["D:y2", "D:late"]),
member(3, 0, &["D:bad1"]),
];
files[6].fail = true;
for file in &mut files {
file.early = early && !file.live_at_start;
}
files
};
let mut plain = build(false);
let (table_a, resolution_a) = run(&mut plain);
assert_eq!(live(&resolution_a), [0, 2, 3, 4, 5]);
for threads in [1, 3] {
let mut early = build(true);
let mut table_b = SymbolTable::new();
let mut hook = EarlyHook::default();
let resolution_b = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap()
.install(|| {
resolve_symbols_with(&mut table_b, &ElfReferenceRules, &mut early, &mut hook)
})
.unwrap();
assert_eq!(table_a.names(), table_b.names());
assert_eq!(live(&resolution_a), live(&resolution_b));
assert_eq!(resolution_a.extracted(), resolution_b.extracted());
for file in 0..plain.len() {
let id = FileId::new(file);
assert_eq!(resolution_a.symbol_ids(id), resolution_b.symbol_ids(id));
}
for id in table_a.ids() {
assert_eq!(table_a.definition(id), table_b.definition(id));
}
for (index, file) in early.iter().enumerate() {
let dropped = usize::from(index == 1 || index == 6);
assert_eq!((file.loads, file.unloads), (1, dropped), "file {index}");
}
}
for early in [false, true] {
let mut files = build(early);
files[0].names.push(SymbolName::new(b"bad1"));
files[0].uses.push(SymbolUse::Reference { weak: false });
let mut table = SymbolTable::new();
let error = resolve_symbols_with(
&mut table,
&ElfReferenceRules,
&mut files,
&mut EarlyHook::default(),
)
.unwrap_err();
assert!(error.to_string().starts_with("input3"), "{error}");
}
}
#[test]
fn hook_error_stops_resolution() {
let mut files = [object(0, &["U:a"]), member(1, 0, &["D:a"])];
let mut recorder = Recorder {
fail_in_round: Some(1),
..Recorder::default()
};
let mut table = SymbolTable::new();
let error = resolve_symbols_with(&mut table, &ElfReferenceRules, &mut files, &mut recorder)
.unwrap_err();
assert!(matches!(error, Error::Internal(_)), "{error}");
assert_eq!(recorder.rounds.len(), 2);
}
#[test]
fn symbol_table_overflow_is_a_limit_error() {
let mut files = [
object(0, &["D:main", "U:a"]),
member(1, 0, &["D:a", "D:b", "U:c", "U:d"]),
];
for (limit, ok) in [(5, true), (4, false), (2, false)] {
for file in &mut files {
file.loads = 0;
file.hooked = false;
}
let mut table = SymbolTable::new();
table.set_limit(limit);
let result = resolve_symbols_with(
&mut table,
&ElfReferenceRules,
&mut files,
&mut Recorder::default(),
);
match result {
Ok(_) => assert!(ok, "limit {limit}"),
Err(Error::Limit(message)) => {
assert!(!ok, "limit {limit}: {message}");
assert!(table.len() <= limit);
}
Err(other) => panic!("limit {limit}: {other}"),
}
}
}
}