use std::collections::HashMap;
use rucc_base::{Interner, Symbol};
use rucc_ir::{Extra, Inst, Module, Opcode};
use crate::Counts;
use crate::boundary::Sites;
use crate::wrap::INTERPOSED;
pub const SCHEMA: u32 = 1;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Class {
pub emitted: usize,
pub remaining: usize,
}
impl Class {
#[must_use]
pub const fn discharged(self) -> usize {
self.emitted.saturating_sub(self.remaining)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Frames {
pub elided: usize,
pub checked: usize,
pub outside: usize,
pub unknown: usize,
pub pointerless: usize,
}
impl Frames {
#[must_use]
pub const fn wanted(self) -> usize {
self.elided + self.checked + self.outside + self.unknown
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Summary {
pub unit: String,
pub tier: &'static str,
pub bounds: Class,
pub lifetime: Class,
pub derivation: Class,
pub effective_type: Class,
pub initialization: Class,
pub restrict: Class,
pub unchecked: usize,
pub interposed: usize,
pub rows: usize,
pub external: Vec<String>,
pub indirect: usize,
pub exposed: usize,
pub synthesized: usize,
pub asm: usize,
pub crossings: Sites,
pub frames: Frames,
}
impl Summary {
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
out.push_str("{\n");
out.push_str(&format!(" \"schema\": {SCHEMA},\n"));
out.push_str(&format!(" \"unit\": {},\n", quoted(&self.unit)));
out.push_str(&format!(" \"tier\": {},\n", quoted(self.tier)));
out.push_str(" \"checks\": {\n");
out.push_str(&format!(" \"bounds\": {},\n", class(self.bounds)));
out.push_str(&format!(" \"lifetime\": {},\n", class(self.lifetime)));
out.push_str(&format!(" \"derivation\": {},\n", class(self.derivation)));
out.push_str(&format!(" \"type\": {},\n", class(self.effective_type)));
out.push_str(&format!(" \"init\": {},\n", class(self.initialization)));
out.push_str(&format!(" \"restrict\": {},\n", class(self.restrict)));
out.push_str(&format!(" \"unchecked\": {}\n", self.unchecked));
out.push_str(" },\n");
out.push_str(" \"trust\": {\n");
out.push_str(&format!(" \"interposed\": {},\n", self.interposed));
out.push_str(&format!(" \"rows\": {},\n", self.rows));
out.push_str(&format!(" \"external\": {},\n", list(&self.external)));
out.push_str(&format!(" \"indirect\": {},\n", self.indirect));
out.push_str(&format!(" \"exposed\": {},\n", self.exposed));
out.push_str(&format!(" \"synthesized\": {},\n", self.synthesized));
out.push_str(&format!(" \"asm\": {},\n", self.asm));
out.push_str(&format!(
" \"crossings\": {{ \"entered\": {}, \"returned\": {} }}\n",
self.crossings.entered, self.crossings.returned
));
out.push_str(" },\n");
out.push_str(" \"frames\": {\n");
out.push_str(&format!(" \"elided\": {},\n", self.frames.elided));
out.push_str(&format!(" \"checked\": {},\n", self.frames.checked));
out.push_str(&format!(" \"outside\": {},\n", self.frames.outside));
out.push_str(&format!(" \"unknown\": {},\n", self.frames.unknown));
out.push_str(&format!(" \"pointerless\": {},\n", self.frames.pointerless));
out.push_str(&format!(" \"wanted\": {}\n", self.frames.wanted()));
out.push_str(" },\n");
out.push_str(" \"at_run_time\": [\n");
out.push_str(" \"__rucc_safety_recovered\",\n");
out.push_str(" \"__rucc_safety_recovered_wide\"\n");
out.push_str(" ]\n");
out.push_str("}\n");
out
}
}
#[must_use]
pub fn summarize(
module: &Module,
names: &Interner,
unit: &str,
tier: &'static str,
emitted: Counts,
interposed: usize,
crossings: Sites,
) -> Summary {
let mut summary = Summary {
unit: unit.to_string(),
tier,
bounds: Class { emitted: emitted.checked, remaining: 0 },
lifetime: Class { emitted: emitted.live, remaining: 0 },
derivation: Class { emitted: emitted.derived, remaining: 0 },
effective_type: Class { emitted: emitted.asked, remaining: 0 },
initialization: Class { emitted: emitted.filled, remaining: 0 },
restrict: Class { emitted: emitted.promised, remaining: 0 },
unchecked: emitted.skipped,
interposed,
rows: INTERPOSED.len(),
crossings,
..Summary::default()
};
let left: HashMap<Symbol, usize> = module
.funcs()
.filter(|&id| !module[id].is_declaration())
.map(|id| (module[id].name, checks_left(&module[id])))
.collect();
let mut external: Vec<Symbol> = Vec::new();
for id in module.funcs() {
if module[id].is_declaration() {
continue;
}
let func = &module[id];
let insts: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in insts {
match func[inst].opcode {
Opcode::CheckBounds => summary.bounds.remaining += 1,
Opcode::CheckLive => summary.lifetime.remaining += 1,
Opcode::CheckDeriv => summary.derivation.remaining += 1,
Opcode::CheckType => summary.effective_type.remaining += 1,
Opcode::CheckInit => summary.initialization.remaining += 1,
Opcode::CheckRestrictRead | Opcode::CheckRestrictWrite => {
summary.restrict.remaining += 1;
}
Opcode::PtrToInt => summary.exposed += 1,
Opcode::IntToPtr => summary.synthesized += 1,
Opcode::InlineAsm => summary.asm += 1,
Opcode::Call | Opcode::TailCall | Opcode::CallIndirect => {
let indirect = func[inst].opcode == Opcode::CallIndirect;
if indirect {
summary.indirect += 1;
}
let callee = match func[inst].extra {
Extra::Call(at) if !indirect => func[at].callee,
_ => None,
};
match callee {
Some(callee)
if !left.contains_key(&callee)
&& !external.contains(&callee)
&& !ours(names.resolve(callee)) =>
{
external.push(callee);
}
Some(_) => {}
None if !indirect => summary.indirect += 1,
None => {}
}
let skip = usize::from(indirect);
let hands_over = func[func[inst].args]
.iter()
.skip(skip)
.any(|&value| func[value].ty.is_ptr());
if !hands_over {
summary.frames.pointerless += 1;
} else {
match callee.and_then(|callee| left.get(&callee)) {
None => {
if indirect || callee.is_none() {
summary.frames.unknown += 1;
} else {
summary.frames.outside += 1;
}
}
Some(0) => summary.frames.elided += 1,
Some(_) => summary.frames.checked += 1,
}
}
}
_ => {}
}
}
}
summary.external = external.iter().map(|&s| names.resolve(s).to_string()).collect();
summary.external.sort_unstable();
summary
}
fn checks_left(func: &rucc_ir::Func) -> usize {
let insts: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
insts
.into_iter()
.filter(|&inst| {
matches!(
func[inst].opcode,
Opcode::CheckBounds
| Opcode::CheckLive
| Opcode::CheckDeriv
| Opcode::CheckType
| Opcode::CheckInit
)
})
.count()
}
fn ours(name: &str) -> bool {
name.starts_with("__rucc_")
}
fn class(class: Class) -> String {
format!(
"{{ \"emitted\": {}, \"remaining\": {}, \"discharged\": {} }}",
class.emitted,
class.remaining,
class.discharged()
)
}
fn list(names: &[String]) -> String {
if names.is_empty() {
return "[]".to_string();
}
let mut out = String::from("[\n");
for (at, name) in names.iter().enumerate() {
let comma = if at + 1 == names.len() { "" } else { "," };
out.push_str(&format!(" {}{comma}\n", quoted(name)));
}
out.push_str(" ]");
out
}
fn quoted(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use rucc_ir::{Builder, Func, InstData, Linkage, MemInfo, MemOrder, Restrict, Signature, Type};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::*;
use crate::insert;
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
fn guarded(names: &mut Interner) -> Func {
let i32_ = Type::int(32);
let mut func = Func::new(
names.intern("guarded"),
Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]),
);
let entry = func.create_block();
let p = func.append_param(entry, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let mut b = Builder::new(&mut func, entry);
let args = b.func().push_values(&[p]);
let extra = Extra::Mem(b.func().add_mem(info));
let loaded = b.value(InstData { args, extra, ..InstData::new(Opcode::Load) }, i32_);
b.ret(&[loaded]);
let mut elsewhere = Module::new(names.intern("reader.c"), &target());
insert(
&mut func,
&crate::Plane::build(&mut elsewhere),
8,
crate::Subobject::Off,
crate::Promise::Off,
);
func
}
fn clean(names: &mut Interner) -> Func {
let i32_ = Type::int(32);
let mut func = Func::new(
names.intern("clean"),
Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]),
);
let entry = func.create_block();
func.append_param(entry, Type::PTR);
let mut b = Builder::new(&mut func, entry);
let zero = b.iconst(i32_, 0);
b.ret(&[zero]);
func
}
fn calls(names: &mut Interner) -> Module {
let i32_ = Type::int(32);
let taking = Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]);
let nothing = Signature::new().with_returns(&[i32_]);
let mut func = Func::new(names.intern("main"), taking.clone());
let entry = func.create_block();
let p = func.append_param(entry, Type::PTR);
let taking = func.add_signature(taking);
let nothing = func.add_signature(nothing);
let no_checks = names.intern("clean");
let has_checks = names.intern("guarded");
let elsewhere = names.intern("puts");
let ticks = names.intern("ticks");
let mut b = Builder::new(&mut func, entry);
b.call(no_checks, taking, &[p]);
b.call(has_checks, taking, &[p]);
b.call(elsewhere, taking, &[p]);
b.call(ticks, nothing, &[]);
let zero = b.iconst(i32_, 0);
b.ret(&[zero]);
let mut module = Module::new(names.intern("calls.c"), &target());
module.add_func(clean(names));
module.add_func(guarded(names));
module.add_func(func);
let mut declared = Func::new(elsewhere, Signature::new().with_params(&[Type::PTR]));
declared.linkage = Linkage::External;
module.add_func(declared);
module
}
fn frames_of(module: &Module, names: &Interner) -> Frames {
summarize(module, names, "calls.c", "detect", Counts::default(), 0, Sites::default()).frames
}
fn filled() -> Summary {
Summary {
unit: "a.c".to_string(),
tier: "detect",
bounds: Class { emitted: 12, remaining: 5 },
lifetime: Class { emitted: 12, remaining: 11 },
derivation: Class { emitted: 3, remaining: 3 },
effective_type: Class { emitted: 7, remaining: 7 },
initialization: Class { emitted: 9, remaining: 8 },
restrict: Class { emitted: 4, remaining: 4 },
unchecked: 1,
interposed: 2,
rows: 27,
external: vec!["printf".to_string(), "qsort".to_string()],
indirect: 1,
exposed: 0,
synthesized: 0,
asm: 1,
crossings: Sites { entered: 2, returned: 1 },
frames: Frames { elided: 4, checked: 2, outside: 3, unknown: 1, pointerless: 5 },
}
}
#[test]
fn a_discharged_check_is_one_that_went_in_and_is_not_there_now() {
assert_eq!(Class { emitted: 12, remaining: 5 }.discharged(), 7);
assert_eq!(Class { emitted: 0, remaining: 0 }.discharged(), 0);
}
#[test]
fn a_pass_that_somehow_added_checks_discharges_none_rather_than_panicking() {
assert_eq!(Class { emitted: 1, remaining: 4 }.discharged(), 0);
}
#[test]
fn the_summary_says_the_schema_it_is_written_in() {
let text = filled().render();
assert!(text.contains("\"schema\": 1"), "{text}");
}
#[test]
fn every_class_reports_all_three_numbers() {
let text = filled().render();
assert!(
text.contains("\"bounds\": { \"emitted\": 12, \"remaining\": 5, \"discharged\": 7 }"),
"{text}"
);
assert!(
text.contains(
"\"derivation\": { \"emitted\": 3, \"remaining\": 3, \"discharged\": 0 }"
),
"{text}"
);
assert!(
text.contains("\"type\": { \"emitted\": 7, \"remaining\": 7, \"discharged\": 0 }"),
"{text}"
);
assert!(
text.contains("\"init\": { \"emitted\": 9, \"remaining\": 8, \"discharged\": 1 }"),
"{text}"
);
}
#[test]
fn the_unwrapped_calls_are_named_rather_than_counted() {
let text = filled().render();
assert!(text.contains("\"printf\""), "{text}");
assert!(text.contains("\"qsort\""), "{text}");
}
#[test]
fn a_unit_with_nothing_to_hide_says_so_with_an_empty_list() {
let text = Summary { external: Vec::new(), ..filled() }.render();
assert!(text.contains("\"external\": [],"), "{text}");
}
#[test]
fn the_boundary_crossings_are_counted_by_direction() {
let text = filled().render();
assert!(text.contains("\"crossings\": { \"entered\": 2, \"returned\": 1 }"), "{text}");
}
#[test]
fn the_run_time_counts_are_named_rather_than_guessed_at() {
let text = filled().render();
assert!(text.contains("__rucc_safety_recovered"), "{text}");
assert!(!text.contains("\"recovered\": 0"), "{text}");
}
#[test]
fn a_name_with_a_quote_in_it_comes_out_as_json_rather_than_as_two_strings() {
let text = Summary { unit: "a\"b\\c.c".to_string(), ..filled() }.render();
assert!(text.contains(r#""unit": "a\"b\\c.c""#), "{text}");
}
#[test]
fn a_call_into_a_function_with_no_checks_left_is_one_whose_frame_goes_away() {
let mut names = Interner::new();
let module = calls(&mut names);
let frames = frames_of(&module, &names);
assert_eq!(frames.elided, 1, "{frames:?}");
assert_eq!(frames.checked, 1, "{frames:?}");
assert_eq!(frames.outside, 1, "{frames:?}");
assert_eq!(frames.unknown, 0, "{frames:?}");
}
#[test]
fn a_call_that_hands_no_pointer_over_never_wanted_a_frame() {
let mut names = Interner::new();
let module = calls(&mut names);
let frames = frames_of(&module, &names);
assert_eq!(frames.pointerless, 1, "{frames:?}");
assert_eq!(frames.wanted(), 3, "{frames:?}");
}
#[test]
fn the_five_buckets_account_for_every_call_in_the_unit() {
let mut names = Interner::new();
let module = calls(&mut names);
let frames = frames_of(&module, &names);
assert_eq!(frames.wanted() + frames.pointerless, 4, "{frames:?}");
}
#[test]
fn the_rate_is_reported_with_its_denominator_beside_it() {
let text = filled().render();
assert!(text.contains("\"elided\": 4"), "{text}");
assert!(text.contains("\"wanted\": 10"), "{text}");
}
#[test]
fn the_whole_thing_is_one_object_and_ends_in_a_newline() {
let text = filled().render();
assert!(text.starts_with("{\n"), "{text}");
assert!(text.ends_with("}\n"), "{text}");
assert_eq!(text.matches('{').count(), text.matches('}').count(), "{text}");
}
}