use std::{
collections::{HashMap, HashSet},
fmt::Debug,
};
use eyre::{anyhow, bail, Result};
use wasmparser::{FunctionBody, Operator, RelocationEntry};
use crate::{
read::{GlobalId, InputFuncId, InputModule, MemoryId, SymbolIndex, TableId, TagId},
reloc::{DataSymbol, RelocDetails},
util::shift_range,
};
#[derive(Debug, PartialEq, Eq, Hash, Copy, PartialOrd, Ord, Clone)]
pub enum DepNode {
Function(InputFuncId),
DataSymbol(SymbolIndex),
Global(GlobalId),
Table(TableId),
Tag(TagId),
Memory(MemoryId),
}
pub type DepGraph = HashMap<DepNode, HashSet<DepNode>>;
pub fn get_dependencies(module: &InputModule) -> Result<(DepGraph, HashSet<InputFuncId>)> {
struct Builder<'a, 'm>(DepGraph, &'a InputModule<'m>);
impl Builder<'_, '_> {
fn add_dep(&mut self, a: DepNode, b: DepNode) {
self.0.entry(a).or_default().insert(b);
}
fn add_reloc_dep(&mut self, a: DepNode, relocation: &RelocationEntry) -> Result<()> {
let target = match self.1.reloc_info.expand_relocation(relocation)? {
RelocDetails::TypeIndex { .. } => None,
RelocDetails::MemoryAddr(details) => {
Some(DepNode::DataSymbol(details.symbol_index))
}
RelocDetails::TableIndex(details) => Some(DepNode::Function(details.index)),
RelocDetails::RelTableIndex(details) => Some(DepNode::Function(details.index)),
RelocDetails::FunctionIndex(details) => Some(DepNode::Function(details.index)),
RelocDetails::TableNumber(details) => Some(DepNode::Table(details.index)),
RelocDetails::GlobalIndex(details) => Some(DepNode::Global(details.index)),
RelocDetails::TagIndex(details) => Some(DepNode::Tag(details.index)),
};
if let Some(target) = target {
self.add_dep(a, target);
};
Ok(())
}
}
let mut deps = Builder(DepGraph::new(), module);
let mut fns_with_relocs = HashSet::<InputFuncId>::new();
for dep_entry in iter_functions_with_relocs(module) {
let (func_index, entry) = dep_entry?;
fns_with_relocs.insert(func_index);
deps.add_reloc_dep(DepNode::Function(func_index), entry)?;
}
let mut stub_fns = HashSet::<InputFuncId>::new();
let imported_fns_len = module.imported_funcs.len();
let mut unexpected_ops: HashMap<String, (usize, InputFuncId)> = HashMap::new();
for (i, defined_func) in module.defined_funcs.iter().enumerate() {
let idx = imported_fns_len + i;
if fns_with_relocs.contains(&idx) {
continue;
}
if let Some(targets) = validate_no_reloc_stub(&defined_func.body, idx, &mut unexpected_ops)?
{
stub_fns.insert(idx);
for target in targets {
deps.add_dep(DepNode::Function(idx), DepNode::Function(target));
}
}
}
if !unexpected_ops.is_empty() {
for (op, (count, sample)) in &unexpected_ops {
tracing::warn!(
"skipped {count} func(s) with missing reloc info, {op} looks too complicated for a stub (e.g. func[{sample}] {:?})",
module.names.functions.get(sample).unwrap_or(&"<anon>")
);
}
}
for dep_entry in iter_data_dependencies(module) {
match dep_entry? {
DataDependency::Reloc(data_symbol, entry) => {
deps.add_reloc_dep(DepNode::DataSymbol(data_symbol.symbol_index), entry)?
}
DataDependency::Containment { container, inner } => {
deps.add_dep(
DepNode::DataSymbol(container.symbol_index),
DepNode::DataSymbol(inner.symbol_index),
);
}
DataDependency::DirectedOverlap {
from,
to,
back_to,
container,
} => {
if let Some(container) = container {
deps.add_dep(
DepNode::DataSymbol(container.symbol_index),
DepNode::DataSymbol(to.symbol_index),
);
}
deps.add_dep(
DepNode::DataSymbol(from.symbol_index),
DepNode::DataSymbol(to.symbol_index),
);
deps.add_dep(
DepNode::DataSymbol(to.symbol_index),
DepNode::DataSymbol(back_to.symbol_index),
);
}
}
}
Ok((deps.0, stub_fns))
}
fn iter_functions_with_relocs<'m>(
module: &'m InputModule,
) -> impl Iterator<Item = Result<(InputFuncId, &'m RelocationEntry)>> {
let code_relocs = module.reloc_info.iter_code_relocs();
let code_section_offset = module.reloc_info.code_section_offset();
let mut function_index = 0;
code_relocs.map(move |entry| {
let reloc_file_range = shift_range(entry.relocation_range()?, code_section_offset);
let found_index = crate::util::exponential_partition_point(
&module.defined_funcs[function_index..],
|func| reloc_file_range.end > func.body.range().end,
);
function_index += found_index;
if function_index >= module.defined_funcs.len() {
bail!("Invalid relocation entry {entry:?}, no function contains its relocation range")
}
let func_index = module.imported_funcs.len() + function_index;
Ok((func_index, entry))
})
}
fn validate_no_reloc_stub(
body: &FunctionBody<'_>,
func_id: InputFuncId,
unexpected_ops: &mut HashMap<String, (usize, InputFuncId)>,
) -> Result<Option<Vec<InputFuncId>>> {
fn surely_needs_reloc_information(op: &Operator) -> bool {
matches!(
op, | Operator::Call { .. } | Operator::ReturnCall { .. }
| Operator::CallIndirect { .. }
| Operator::ReturnCallIndirect { .. }
| Operator::RefFunc { .. }
| Operator::GlobalGet { .. }
| Operator::GlobalSet { .. }
| Operator::TableGet { .. }
| Operator::TableSet { .. }
| Operator::TableSize { .. }
| Operator::TableGrow { .. }
| Operator::TableFill { .. }
| Operator::TableInit { .. }
| Operator::TableCopy { .. }
| Operator::MemoryInit { .. }
| Operator::DataDrop { .. }
| Operator::ElemDrop { .. }
)
}
let mut targets = vec![];
let mut ops = body.get_operators_reader()?;
let mut needs_reloc_info = false;
let mut is_simple_stub = true;
while !ops.eof() {
let op = ops.read()?;
needs_reloc_info |= surely_needs_reloc_information(&op);
match op {
Operator::Call { function_index } => targets.push(function_index as usize),
Operator::LocalGet { .. } | Operator::Return | Operator::End => {}
op if needs_reloc_info => {
let name = op_short_name(&op);
let entry = unexpected_ops.entry(name).or_insert((0, func_id));
entry.0 += 1;
return Ok(None);
}
_ => is_simple_stub = false,
}
}
let any_targets = !targets.is_empty();
Ok((is_simple_stub && any_targets).then_some(targets))
}
fn op_short_name(op: &Operator) -> String {
let dbg = format!("{op:?}");
dbg.split(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or("<unknown>")
.to_owned()
}
enum DataDependency<'a> {
Reloc(&'a DataSymbol, &'a RelocationEntry),
Containment {
container: &'a DataSymbol,
inner: &'a DataSymbol,
},
DirectedOverlap {
from: &'a DataSymbol,
to: &'a DataSymbol,
back_to: &'a DataSymbol,
container: Option<&'a DataSymbol>,
},
}
macro_rules! emit_iter_err {
($a:expr) => {
match $a {
Err(e) => return Some(Err(e.into())),
Ok(o) => o,
}
};
}
fn iter_data_dependencies<'m>(
module: &'m InputModule,
) -> impl Iterator<Item = Result<DataDependency<'m>>> {
let data_section_offset = module.reloc_info.data_section_offset();
let mut data_relocs = module.reloc_info.iter_data_relocs().peekable();
let mut data_symbols = module.reloc_info.data_symbols.iter().peekable();
let mut overlap_candidates: Vec<&DataSymbol> = vec![];
std::iter::from_fn(move || loop {
if let Some(&entry) = data_relocs.peek() {
let reloc_file_range = shift_range(
emit_iter_err!(entry.relocation_range()),
data_section_offset,
);
let should_handle_reloc = match data_symbols.peek() {
None => true,
Some(next_symbol) => next_symbol.range.start >= reloc_file_range.end,
};
if should_handle_reloc {
let entry = data_relocs.next().unwrap();
while overlap_candidates.last().is_some_and(|&back| {
!((reloc_file_range.start < back.range.end)
& (back.range.start < reloc_file_range.end))
}) {
let _ = overlap_candidates.pop();
}
let &target = emit_iter_err!(overlap_candidates.last().ok_or_else(|| anyhow!(
"Invalid relocation entry {entry:?} not overlapping any data symbols"
)));
if !(target.range.start <= reloc_file_range.start
&& reloc_file_range.end <= target.range.end)
{
emit_iter_err!(Err(anyhow!("Invalid relocation entry {entry:?} not fully contained inside its data symbol")))
}
return Some(Ok(DataDependency::Reloc(target, entry)));
}
}
let next_symbol = data_symbols.next()?;
while overlap_candidates
.last()
.is_some_and(|&candidate| candidate.range.end <= next_symbol.range.start)
{
overlap_candidates.pop();
}
let mut container = None;
let last = overlap_candidates.last().cloned(); 'candidate_loop: for candidate in &mut overlap_candidates {
let is_contained = next_symbol.range.end <= candidate.range.end;
debug_assert!(
!is_contained || candidate.range.start <= next_symbol.range.start,
"input not sorted"
);
if is_contained {
container = Some(*candidate);
continue 'candidate_loop;
}
let back_to = std::mem::replace(candidate, next_symbol);
return Some(Ok(DataDependency::DirectedOverlap {
from: last.unwrap(),
to: next_symbol,
back_to,
container,
}));
}
if let Some(container) = container {
return Some(Ok(DataDependency::Containment {
container,
inner: next_symbol,
}));
};
overlap_candidates.push(next_symbol);
})
}