use std::{
collections::{BTreeMap, BTreeSet},
ffi::{OsStr, OsString},
path::{Path, PathBuf},
};
use serde::Serialize;
use rllvm_core::{
catalog::ArchiveCache, config::LlvmBindir, error::Error, utils::execute_llvm_tool_in_for_output,
};
use crate::{
Query, QueryResult, QueryResults, Session,
facts::{FunctionId, Linkage},
load::{load_catalog, read_module},
};
const LLVM_EXTRACT: &str = "llvm-extract";
const LLVM_NM: &str = "llvm-nm";
const LOCAL_SYMBOL_TYPES: &[char] = &['t', 'd', 'b', 'r', 's'];
const GLOBAL_PREFIXES: &[&str] = &["", "_"];
const VERBATIM_NAME: char = '\u{1}';
#[derive(Debug, Serialize)]
pub struct EmittedModule {
pub path: PathBuf,
pub modules: usize,
pub functions: usize,
}
pub fn emit_module(
session: &Session,
catalog: &Path,
functions: &[FunctionId],
llvm_link: &Path,
llvm_bindir: &LlvmBindir,
out: &Path,
) -> Result<EmittedModule, Error> {
let llvm_extract = llvm_bindir.tool(LLVM_EXTRACT)?;
let llvm_nm = llvm_bindir.tool(LLVM_NM)?;
let mut plans = plan_pieces(session, functions);
if plans.is_empty() {
return Err(Error::InvalidArguments(
"the slice holds no definition; nothing to emit".to_string(),
));
}
let loaded = load_catalog(catalog)?;
let scratch = tempfile::TempDir::new()?;
let mut archives = ArchiveCache::default();
let mut pieces: Vec<Piece> = Vec::new();
for (index, pending) in loaded.pending.iter().enumerate() {
let Some(plan) = plans.remove(pending.id.as_str()) else {
continue;
};
let analyzed = session
.modules()
.iter()
.find(|report| report.id == pending.id)
.and_then(|report| report.content_sha256.as_ref());
if analyzed.is_some() && analyzed != pending.record.content_sha256.as_ref() {
return Err(Error::InvalidArguments(format!(
"module {} changed in {} since the query read it",
pending.id,
catalog.display()
)));
}
let module = read_module(pending, &mut archives)?;
let piece = Piece {
module: pending.id.clone(),
input: scratch.path().join(format!("{index}.bc")),
output: scratch.path().join(format!("{index}.slice.bc")),
plan,
promoted: BTreeSet::new(),
symbols: BTreeSet::new(),
};
std::fs::write(&piece.input, &module.bytes)?;
pieces.push(piece);
}
drop(archives);
if let Some(module) = plans.into_keys().next() {
return Err(Error::InvalidArguments(format!(
"module {module} holds slice definitions but was not read from {}",
catalog.display()
)));
}
let environment = inherited_environment();
for piece in &mut pieces {
let mut arguments: Vec<OsString> = Vec::new();
for (flag, symbol) in &piece.plan.definitions {
arguments.extend([OsString::from(flag), OsString::from(symbol)]);
}
arguments.extend([
"-o".into(),
piece.output.clone().into(),
piece.input.clone().into(),
]);
run_tool(&llvm_extract, &arguments, scratch.path(), &environment)?;
let list = |file: &Path| {
run_tool(&llvm_nm, &[file.as_os_str()], scratch.path(), &environment)
.map(|stdout| symbol_table(&String::from_utf8_lossy(&stdout)))
};
let mut statics: BTreeSet<String> = list(&piece.input)?
.into_iter()
.filter(|(kind, _)| LOCAL_SYMBOL_TYPES.contains(kind))
.map(|(_, name)| name)
.collect();
piece.symbols = list(&piece.output)?
.into_iter()
.map(|(_, name)| name)
.collect();
let prefix = global_prefix(&piece.plan.cut, &piece.symbols);
for symbol in &piece.plan.internal {
statics.extend(spellings(symbol, prefix));
}
piece.promoted = piece.symbols.intersection(&statics).cloned().collect();
}
refuse_colliding_statics(&pieces)?;
let mut arguments: Vec<OsString> = vec!["-o".into(), std::path::absolute(out)?.into()];
arguments.extend(
pieces
.iter()
.map(|piece| piece.output.clone().into_os_string()),
);
run_tool(llvm_link, &arguments, scratch.path(), &environment)?;
Ok(EmittedModule {
path: out.to_path_buf(),
modules: pieces.len(),
functions: pieces
.iter()
.map(|piece| piece.plan.definitions.len())
.sum(),
})
}
pub fn emit_slice(
session: &Session,
catalog: &Path,
answer: &QueryResult,
llvm_link: &Path,
llvm_bindir: &LlvmBindir,
out: &Path,
) -> Result<EmittedModule, Error> {
match (&answer.query, &answer.results) {
(Query::Slice { from, to, .. }, QueryResults::Slice(slice)) => {
if slice.functions.is_empty() {
return Err(Error::InvalidArguments(format!(
"no path from {from} to {to}; nothing to emit"
)));
}
emit_module(
session,
catalog,
&slice.functions,
llvm_link,
llvm_bindir,
out,
)
}
_ => Err(Error::InvalidArguments(
"only a `slice` answer emits a module".to_string(),
)),
}
}
fn plan_pieces(session: &Session, functions: &[FunctionId]) -> BTreeMap<String, Plan> {
let mut plans: BTreeMap<String, Plan> = BTreeMap::new();
for id in functions {
let Some(function) = session.function(id).filter(|f| f.is_definition) else {
continue;
};
let plan = plans.entry(id.module_id.clone()).or_default();
let mut next = Some(function);
while let Some(function) = next {
let flag = if function.alias_of.is_some() {
"--alias"
} else {
"--func"
};
if !plan.cut.insert(function.id.symbol.clone()) {
break;
}
plan.definitions.push((flag, function.id.symbol.clone()));
next = function
.alias_of
.as_ref()
.and_then(|target| session.function(target));
}
}
for function in session.functions() {
if function.linkage == Linkage::Internal
&& let Some(plan) = plans.get_mut(&function.id.module_id)
{
plan.internal.insert(function.id.symbol.clone());
}
}
plans
}
#[derive(Default)]
struct Plan {
definitions: Vec<(&'static str, String)>,
cut: BTreeSet<String>,
internal: BTreeSet<String>,
}
struct Piece {
module: String,
input: PathBuf,
output: PathBuf,
plan: Plan,
symbols: BTreeSet<String>,
promoted: BTreeSet<String>,
}
fn refuse_colliding_statics(pieces: &[Piece]) -> Result<(), Error> {
for piece in pieces {
for symbol in &piece.promoted {
if let Some(other) = pieces
.iter()
.find(|other| other.module != piece.module && other.symbols.contains(symbol))
{
return Err(Error::InvalidArguments(format!(
"cannot emit the slice as one module: `{symbol}` is static in module {} \
and also named by module {}'s part, and llvm-extract makes a static it \
keeps external",
piece.module, other.module
)));
}
}
}
Ok(())
}
fn symbol_table(listing: &str) -> Vec<(char, String)> {
let one_letter = |field: &str| {
let mut letters = field.chars();
match (letters.next(), letters.next()) {
(Some(kind), None) => Some(kind),
_ => None,
}
};
listing
.lines()
.filter_map(|line| {
let (first, rest) = line.trim().split_once(char::is_whitespace)?;
let (kind, name) = match one_letter(first) {
Some(kind) => (kind, rest),
None => {
let (letter, name) = rest.trim_start().split_once(char::is_whitespace)?;
(one_letter(letter)?, name)
}
};
let name = name.trim_start();
(!name.is_empty()).then(|| (kind, name.to_string()))
})
.collect()
}
fn global_prefix(cut: &BTreeSet<String>, symbols: &BTreeSet<String>) -> Option<&'static str> {
let mut fitting = GLOBAL_PREFIXES.iter().copied().filter(|prefix| {
cut.iter()
.filter(|symbol| !symbol.starts_with(VERBATIM_NAME))
.all(|symbol| symbols.contains(&format!("{prefix}{symbol}")))
});
match (fitting.next(), fitting.next()) {
(Some(prefix), None) => Some(prefix),
_ => None,
}
}
fn spellings(symbol: &str, prefix: Option<&str>) -> Vec<String> {
if let Some(verbatim) = symbol.strip_prefix(VERBATIM_NAME) {
return vec![verbatim.to_string()];
}
match prefix {
Some(prefix) => vec![format!("{prefix}{symbol}")],
None => GLOBAL_PREFIXES
.iter()
.map(|prefix| format!("{prefix}{symbol}"))
.collect(),
}
}
fn run_tool<S: AsRef<OsStr>>(
tool: &Path,
arguments: &[S],
directory: &Path,
environment: &BTreeMap<String, String>,
) -> Result<Vec<u8>, Error> {
let output = execute_llvm_tool_in_for_output(tool, arguments, directory, environment)?;
if output.status.success() {
return Ok(output.stdout);
}
Err(Error::ExecutionFailure(format!(
"{} failed ({}): {}",
tool.display(),
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)))
}
fn inherited_environment() -> BTreeMap<String, String> {
std::env::vars_os()
.filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_keeps_its_spaces_and_an_address_is_optional() {
let listing = "---------------- t -[Foo bar:baz:]\nU -[Foo other]\n U _leaf \n";
assert_eq!(
symbol_table(listing),
[
('t', "-[Foo bar:baz:]".to_string()),
('U', "-[Foo other]".to_string()),
('U', "_leaf".to_string()),
]
);
}
#[test]
fn the_prefix_is_read_off_the_listing() {
let cut = BTreeSet::from(["fa".to_string(), "\u{1}-[Foo bar:]".to_string()]);
let macho = BTreeSet::from(["_fa".to_string(), "-[Foo bar:]".to_string()]);
assert_eq!(global_prefix(&cut, &macho), Some("_"));
assert_eq!(spellings("pf", Some("_")), ["_pf"]);
let elf = BTreeSet::from(["fa".to_string(), "-[Foo bar:]".to_string()]);
assert_eq!(global_prefix(&cut, &elf), Some(""));
assert_eq!(spellings("\u{1}-[Foo baz]", Some("_")), ["-[Foo baz]"]);
let both = BTreeSet::from(["fa".to_string(), "_fa".to_string()]);
assert_eq!(global_prefix(&cut, &both), None);
assert_eq!(spellings("pf", None), ["pf", "_pf"]);
}
#[test]
fn a_listing_reads_as_type_letters_and_names() {
let listing = "---------------- t _g\n U _leaf\n\
---------------- d _counter\n0000000000000000 T main\n\nlib.a:\n";
assert_eq!(
symbol_table(listing),
[
('t', "_g".to_string()),
('U', "_leaf".to_string()),
('d', "_counter".to_string()),
('T', "main".to_string()),
]
);
}
}