mod common;
use std::path::PathBuf;
fn erases_a_hasher(mangled: &str) -> bool {
(mangled.contains("ValueRepr") || mangled.contains("InlineValue")) && mangled.contains("4hash")
}
fn emit_llvm_ir() -> String {
let target_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("codegen-ir");
let status = common::nested_cargo()
.args(["rustc", "--lib", "--release", "--target-dir"])
.arg(&target_dir)
.args(["--", "--emit=llvm-ir", "-Cdebuginfo=0"])
.status()
.expect("failed to run `cargo rustc`");
assert!(status.success(), "`cargo rustc --emit=llvm-ir` failed");
let ir_file = std::fs::read_dir(target_dir.join("release/deps"))
.expect("read the build's deps directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.extension().is_some_and(|ext| ext == "ll")
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("ijson-"))
})
.max_by_key(|path| path.metadata().and_then(|meta| meta.modified()).ok())
.expect("no `ijson-*.ll` was emitted");
std::fs::read_to_string(&ir_file).expect("read the emitted LLVM IR")
}
#[test]
#[cfg_attr(miri, ignore = "shells out to a compiler, which Miri cannot run")]
fn value_dispatch_is_devirtualized() {
let ir = emit_llvm_ir();
let mut function = String::new();
let mut indirect = 0usize;
let mut ijson_functions = 0usize;
let mut saw_the_clone_dispatch = false;
let mut saw_a_hasher_erasure = false;
let mut offenders: Vec<String> = Vec::new();
for line in ir.lines() {
if line.starts_with("define") {
function = line
.split_once('@')
.map(|(_, tail)| tail.trim_start_matches('"'))
.and_then(|tail| tail.split('(').next())
.unwrap_or_default()
.trim_end_matches('"')
.to_owned();
indirect = 0;
} else if line == "}" {
if function.contains("ijson") {
ijson_functions += 1;
saw_the_clone_dispatch |= function.contains("IValue") && function.contains("clone");
saw_a_hasher_erasure |= erases_a_hasher(&function);
if indirect > 0 && !erases_a_hasher(&function) {
offenders.push(format!(" {indirect} indirect call(s) in {function}"));
}
}
function.clear();
} else if !function.is_empty() && common::is_indirect_call(line) {
indirect += 1;
}
}
assert!(
ijson_functions > 50,
"only {} ijson functions found in the emitted IR — the scan is not seeing the \
library, so this test is not checking anything",
ijson_functions
);
assert!(
saw_the_clone_dispatch,
"did not find `IValue`'s `Clone` impl among the {} ijson functions scanned — \
the name matching is stale, so this test is not checking the dispatch",
ijson_functions
);
assert!(
saw_a_hasher_erasure,
"did not recognise a single `hash` impl among the {} ijson functions scanned — \
`erases_a_hasher` no longer matches the symbol mangling, so every legitimate \
`hash` would be reported as an offender below",
ijson_functions
);
assert!(
offenders.is_empty(),
"the value dispatch is no longer devirtualized: these ijson functions call \
through a function pointer (i.e. a vtable), which should only happen in the \
`hash` impls that erase `&mut dyn Hasher`.\n\n{}\n\nIf a `&dyn ValueRepr` was \
reintroduced (e.g. a `repr()` returning one from a `match`), every arm's \
vtable merges into a phi the optimizer cannot fold, and each operation pays an \
indirect call. Dispatch by handing the *concrete* representation to a closure \
(`ReprTag::with`) instead.",
offenders.join("\n")
);
}