use error::*;
use intern::{Symbol, UNKNOWN_SYMBOL};
use raw::*;
use report::{self, Report};
use utils::*;
use fixedbitset::FixedBitSet;
use petgraph::Direction;
use petgraph::graph::{DiGraph, EdgeIndex, EdgeReference, NodeIndex};
use petgraph::visit::{Dfs, EdgeFiltered, EdgeRef, IntoNodeReferences};
use std::{cmp, io, mem, usize};
use std::borrow::Cow;
use std::collections::{BTreeMap, Bound, HashSet};
use std::collections::hash_map::{Entry, HashMap};
use std::ops::{Index, IndexMut};
#[derive(Default, Debug, Clone)]
pub struct Graph {
version: Version,
functions: Vec<FunctionInfo>,
gcno_index: HashMap<GcnoFunctionIdentity, FunctionIndex>,
gcda_index: HashMap<GcdaFunctionIdentity, FunctionIndex>,
graph: DiGraph<BlockInfo, ArcInfo>,
}
impl Graph {
pub fn new() -> Graph {
Graph::default()
}
pub fn merge(&mut self, mut gcov: Gcov) -> Result<()> {
let source_location = match gcov.src.take() {
Some(path) => Location::File(path),
None => Location::None,
};
source_location.wrap(|| {
match self.version {
INVALID_VERSION => self.version = gcov.version,
v => ensure!(v == gcov.version, ErrorKind::VersionMismatch(v, gcov.version)),
}
match gcov.ty {
Type::Gcno => self.merge_gcno(gcov),
Type::Gcda => self.merge_gcda(gcov),
}
})
}
fn merge_gcno(&mut self, gcno: Gcov) -> Result<()> {
let checksum = gcno.stamp;
let mut fis = Vec::new();
for (index, record) in gcno.records.into_iter().enumerate() {
macro_rules! last_fi {
() => {
match fis.last_mut() {
Some(fi) => &mut fi.1,
None => bail!(Location::RecordIndex(index).wrap_error(ErrorKind::RecordWithoutFunction)),
}
}
}
match record {
Record::Function(ident, function) => fis.push((ident, GcnoFunctionIdentity::new(function))),
Record::Blocks(blocks) => last_fi!().blocks = blocks,
Record::Arcs(arcs) => last_fi!().arcs.push(arcs),
Record::Lines(lines) => last_fi!().lines.push(lines),
_ => trace!("gcno-unknown-record: {:?}", record),
}
}
let mut gcno_index = mem::replace(&mut self.gcno_index, HashMap::new());
for (ident, fi) in fis {
let gcda_identity = GcdaFunctionIdentity::new(checksum, ident, &fi.function);
match gcno_index.entry(fi) {
Entry::Occupied(entry) => {
let _ = self.gcda_index.insert(gcda_identity, *entry.get());
},
Entry::Vacant(entry) => {
let new_index = self.add_function(entry.key());
let _ = self.gcda_index.insert(gcda_identity, new_index);
let _ = entry.insert(new_index);
},
}
}
debug_assert!(self.gcno_index.is_empty());
self.gcno_index = gcno_index;
Ok(())
}
fn merge_gcda(&mut self, gcda: Gcov) -> Result<()> {
let mut cur = INVALID_FUNCTION_INDEX;
let checksum = gcda.stamp;
for (index, record) in gcda.records.into_iter().enumerate() {
match record {
Record::Function(ident, function) => cur = Location::RecordIndex(index).wrap(|| self.find_function(checksum, ident, function))?,
Record::ArcCounts(ac) => self.add_arc_counts(cur, ac)?,
Record::Summary(_) => {},
_ => trace!("gcda-unknown-record: {:?}", record),
}
}
Ok(())
}
pub fn analyze(&mut self) {
self.mark_catch_blocks();
self.mark_unconditional_arcs();
self.mark_exceptional_blocks();
self.propagate_counts();
if cfg!(debug_assertions) {
self.verify_counts();
}
self.mark_exceptional_blocks();
}
pub fn report(&self) -> Report {
let mut r = Report::default();
for function in &self.functions {
self.report_function(function, &mut r);
}
for (src, block) in self.graph.node_references() {
if let Some(last_line) = self.report_block(block, &mut r) {
let function = &self[block.index];
let exit_block = function.exit_block(self.version);
let file = r.files.get_mut(&last_line.0).unwrap();
let branches = &mut file.lines.get_mut(&last_line.1).unwrap().branches;
for edge_ref in self.graph.edges(src) {
if edge_ref.target() == exit_block && edge_ref.weight().count == Some(0) {
continue;
}
let branch = self.report_arc(edge_ref);
branches.extend(branch);
}
}
}
r
}
fn report_function(&self, function: &FunctionInfo, r: &mut Report) {
let source = function.source.unwrap_or_default();
let entry_block = function.entry_block();
let exit_block = function.exit_block(self.version);
let blocks_count = function.nodes.len();
let blocks_executed = function.nodes.iter().filter(|ni| self.graph[**ni].count > Some(0)).count();
let (branches_count, branches_executed, branches_taken) = function
.nodes
.iter()
.flat_map(|ni| self.graph.edges(*ni))
.filter_map(|er| {
let arc = er.weight();
if arc.attr.intersects(ArcAttr::UNCONDITIONAL | ArcAttr::FAKE) {
return None;
}
let arc_taken = arc.count > Some(0);
let src = er.source();
let src_executed = self.graph[src].count > Some(0);
Some((1, src_executed as usize, arc_taken as usize))
})
.fold((0, 0, 0), tuple_3_add);
let entry_count = self.graph[entry_block].count.unwrap_or(0);
let mut exit_count = self.graph[exit_block].count.unwrap_or(0);
exit_count -= self.graph
.edges_directed(exit_block, Direction::Incoming)
.filter_map(|er| {
let arc = er.weight();
if arc.attr.contains(ArcAttr::FAKE) {
arc.count
} else {
None
}
})
.sum::<u64>();
let report_function = report::Function {
name: source.name,
line: source.line,
column: 0,
summary: report::FunctionSummary {
blocks_count,
blocks_executed,
entry_count,
exit_count,
branches_count,
branches_executed,
branches_taken,
},
};
r.files.entry(source.filename).or_default_().functions.push(report_function);
}
fn report_block(&self, block: &BlockInfo, r: &mut Report) -> Option<(Symbol, u32)> {
let block_count = block.count.unwrap_or(0);
let mut last_line = None;
for (filename, line_number) in block.iter_lines() {
let file = r.files.entry(filename).or_default_();
let line = file.lines.entry(line_number).or_default_();
line.count = cmp::max(line.count, block_count);
line.attr |= block.attr;
last_line = Some((filename, line_number));
}
last_line
}
fn report_arc(&self, edge_ref: EdgeReference<ArcInfo>) -> Option<report::Branch> {
let arc = edge_ref.weight();
let attr = arc.attr;
if attr.contains(ArcAttr::UNCONDITIONAL) && !attr.contains(ArcAttr::CALL_NON_RETURN) {
return None;
}
let dest = &self.graph[edge_ref.target()];
let (filename, line) = dest.iter_lines().next().unwrap_or((UNKNOWN_SYMBOL, 0));
Some(report::Branch {
count: arc.count.unwrap_or(0),
attr: arc.attr,
filename,
line,
column: 0,
})
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct FunctionIndex(usize);
const INVALID_FUNCTION_INDEX: FunctionIndex = FunctionIndex(usize::MAX);
impl Index<FunctionIndex> for Graph {
type Output = FunctionInfo;
fn index(&self, index: FunctionIndex) -> &FunctionInfo {
&self.functions[index.0]
}
}
impl IndexMut<FunctionIndex> for Graph {
fn index_mut(&mut self, index: FunctionIndex) -> &mut FunctionInfo {
&mut self.functions[index.0]
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct GcdaFunctionIdentity {
file_checksum: u32,
ident: Ident,
lineno_checksum: u32,
cfg_checksum: u32,
}
impl GcdaFunctionIdentity {
fn new(file_checksum: u32, ident: Ident, function: &Function) -> GcdaFunctionIdentity {
GcdaFunctionIdentity {
file_checksum,
ident,
lineno_checksum: function.lineno_checksum,
cfg_checksum: function.cfg_checksum,
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct GcnoFunctionIdentity {
function: Function,
blocks: Blocks,
arcs: Vec<Arcs>,
lines: Vec<Lines>,
}
impl GcnoFunctionIdentity {
fn new(function: Function) -> GcnoFunctionIdentity {
GcnoFunctionIdentity {
function,
blocks: Blocks { flags: Vec::new() },
arcs: Vec::new(),
lines: Vec::new(),
}
}
}
impl Graph {
fn mark_catch_blocks(&mut self) {
let graph = &mut self.graph;
for src in graph.node_indices() {
let edges = graph.edges(src).map(|er| (er.weight().attr, er.id(), er.target())).collect::<Vec<_>>();
let mut mark_throw = false;
for &(_, ei, dest) in edges.iter().filter(|&&(a, _, _)| a.contains(ArcAttr::FAKE)) {
let (ni, block_attr, arc_attr) = if graph[src].is_entry_block() {
(dest, BlockAttr::NONLOCAL_RETURN, ArcAttr::NONLOCAL_RETURN)
} else {
mark_throw = true;
(src, BlockAttr::CALL_SITE, ArcAttr::CALL_NON_RETURN)
};
graph[ni].attr |= block_attr;
graph[ei].attr |= arc_attr;
}
if mark_throw {
let edges_iter = edges.into_iter().filter(|&(a, _, _)| !a.intersects(ArcAttr::FAKE | ArcAttr::FALLTHROUGH));
for (_, ei, _) in edges_iter {
graph[ei].attr |= ArcAttr::THROW;
}
}
}
}
fn mark_unconditional_arcs(&mut self) {
let graph = &mut self.graph;
let unconditional_edges = graph
.node_indices()
.filter_map(|src| {
let mut non_fake_edges = graph.edges(src).filter(|edge_ref| !edge_ref.weight().attr.contains(ArcAttr::FAKE));
if let Some(er) = non_fake_edges.next() {
if non_fake_edges.next().is_none() {
return Some((er.source(), er.target(), er.id(), er.weight().attr));
}
}
None
})
.collect::<Vec<_>>();
for (src, dest, ei, arc_attr) in unconditional_edges {
graph[ei].attr |= ArcAttr::UNCONDITIONAL;
if arc_attr.contains(ArcAttr::FALLTHROUGH) && graph[src].attr.contains(BlockAttr::CALL_SITE) {
graph[dest].attr |= BlockAttr::CALL_RETURN;
}
}
}
fn propagate_counts(&mut self) {
let mut block_status = self.create_block_status();
let mut old_green_blocks = FixedBitSet::with_capacity(self.graph.node_count());
let mut green_blocks = old_green_blocks.clone();
let mut red_blocks = old_green_blocks.clone();
red_blocks.set_range(.., true);
let mut should_process = true;
while should_process {
should_process = false;
for ni in red_blocks.ones() {
should_process = true;
match self.process_red_block(NodeIndex::new(ni), &block_status) {
BlockColor::White => {},
BlockColor::Red => unreachable!(),
BlockColor::Green => green_blocks.insert(ni),
}
}
red_blocks.clear();
mem::swap(&mut green_blocks, &mut old_green_blocks); for src in old_green_blocks.ones() {
should_process = true;
let src = NodeIndex::new(src);
for dir in &[Direction::Outgoing, Direction::Incoming] {
let dest = self.process_green_block(src, *dir, &mut block_status);
if let Some((dest, ac)) = dest {
match self.process_green_block_dest(dest, ac, *dir, &mut block_status) {
BlockColor::White => {},
BlockColor::Red => red_blocks.insert(dest.index()),
BlockColor::Green => green_blocks.insert(dest.index()),
}
}
}
}
old_green_blocks.clear();
}
}
fn create_block_status(&self) -> Vec<BlockStatus> {
let mut block_status = vec![BlockStatus::default(); self.graph.node_count()];
for edge_ref in self.graph.edge_references() {
let weight = edge_ref.weight();
let src = edge_ref.source().index();
let dest = edge_ref.target().index();
if let Some(count) = weight.count {
block_status[src].outgoing_total_count += count;
block_status[dest].incoming_total_count += count;
} else {
block_status[src].outgoing_invalid_arcs += 1;
block_status[dest].incoming_invalid_arcs += 1;
}
}
for function in &self.functions {
let entry_block = function.entry_block();
let exit_block = function.exit_block(self.version);
block_status[entry_block.index()].incoming_invalid_arcs = usize::MAX;
block_status[exit_block.index()].outgoing_invalid_arcs = usize::MAX;
}
block_status
}
fn process_red_block(&mut self, ni: NodeIndex, bs: &[BlockStatus]) -> BlockColor {
let status = &bs[ni.index()];
let total = if status.outgoing_invalid_arcs == 0 {
status.outgoing_total_count
} else if status.incoming_invalid_arcs == 0 {
status.incoming_total_count
} else {
return BlockColor::White;
};
self.graph[ni].count = Some(total);
BlockColor::Green
}
fn process_green_block(&mut self, src: NodeIndex, direction: Direction, bs: &mut [BlockStatus]) -> Option<(NodeIndex, u64)> {
let dest;
let arc_count;
{
let status = &mut bs[src.index()];
let (src_ia, src_tc) = status.totals_mut(direction);
if *src_ia != 1 {
return None;
}
let (invalid_arc_id, d) = self.graph
.edges_directed(src, direction)
.filter_map(|edge_ref| if edge_ref.weight().count.is_some() {
None
} else {
Some((
edge_ref.id(),
match direction {
Direction::Outgoing => edge_ref.target(),
Direction::Incoming => edge_ref.source(),
},
))
})
.next()
.expect("An arc without any count yet");
let (block, edge) = self.graph.index_twice_mut(src, invalid_arc_id);
let block_count = block.count.expect("Block count");
dest = d;
arc_count = block_count - *src_tc;
edge.count = Some(arc_count);
*src_tc = block_count;
*src_ia -= 1;
}
Some((dest, arc_count))
}
fn process_green_block_dest(&self, dest: NodeIndex, arc_count: u64, direction: Direction, bs: &mut [BlockStatus]) -> BlockColor {
let status = &mut bs[dest.index()];
let (dest_ia, dest_tc) = status.totals_mut(direction.opposite());
*dest_tc += arc_count;
*dest_ia -= 1;
match (self.graph[dest].count, *dest_ia) {
(Some(_), 1) => BlockColor::Green,
(None, 0) => BlockColor::Red,
_ => BlockColor::White,
}
}
fn verify_counts(&self) {
for (_, block) in self.graph.node_references() {
assert!(block.count.is_some());
}
for edge_ref in self.graph.edge_references() {
assert!(edge_ref.weight().count.is_some());
}
}
fn mark_exceptional_blocks(&mut self) {
fn is_non_exc_edge(er: EdgeReference<ArcInfo>) -> bool {
!er.weight().attr.intersects(ArcAttr::FAKE | ArcAttr::THROW)
}
let mut stack = Vec::with_capacity(self.functions.len());
for (i, block) in self.graph.node_weights_mut().enumerate() {
if block.is_entry_block() {
stack.push(NodeIndex::new(i));
} else {
block.attr |= BlockAttr::EXCEPTIONAL;
}
}
let mut dfs = Dfs::empty(&EdgeFiltered(&self.graph, is_non_exc_edge));
dfs.stack = stack;
while let Some(non_exc_ni) = dfs.next(&self.graph) {
self.graph[non_exc_ni].attr.remove(BlockAttr::EXCEPTIONAL);
}
}
}
enum BlockColor {
White,
Red,
Green,
}
#[derive(Default, Clone)]
struct BlockStatus {
outgoing_total_count: u64,
outgoing_invalid_arcs: usize,
incoming_total_count: u64,
incoming_invalid_arcs: usize,
}
impl BlockStatus {
fn totals_mut(&mut self, direction: Direction) -> (&mut usize, &mut u64) {
match direction {
Direction::Outgoing => (&mut self.outgoing_invalid_arcs, &mut self.outgoing_total_count),
Direction::Incoming => (&mut self.incoming_invalid_arcs, &mut self.incoming_total_count),
}
}
}
macro_rules! get_function {
($self:expr, $index:expr) => { &mut $self.functions[$index.0] }
}
impl Graph {
fn add_function(&mut self, fi: &GcnoFunctionIdentity) -> FunctionIndex {
let new_index = FunctionIndex(self.functions.len());
trace!("gcno-add-function {:?} -> {:?}", fi.function.source, new_index);
let mut function = FunctionInfo {
arcs: Vec::with_capacity(fi.arcs.iter().map(|a| a.arcs.len()).sum()),
nodes: Vec::with_capacity(fi.blocks.flags.len()),
source: fi.function.source,
};
self.add_blocks(&mut function, new_index, &fi.blocks);
for arcs in &fi.arcs {
self.add_arcs(&mut function, new_index, arcs);
}
let mut block_number_to_lines = BTreeMap::new();
for line in &fi.lines {
let old_lines = block_number_to_lines.insert(line.block_number.into(), &*line.lines);
debug_assert_eq!(old_lines, None);
}
self.add_lines(&function, new_index, block_number_to_lines);
self.functions.push(function);
new_index
}
fn add_blocks(&mut self, function: &mut FunctionInfo, index: FunctionIndex, blocks: &Blocks) {
let count = blocks.flags.len();
trace!("gcno-add-blocks ({}): {} blocks", index.0, count);
let graph = &mut self.graph;
function.nodes = blocks
.flags
.iter()
.enumerate()
.map(move |(block, &attr)| {
graph.add_node(BlockInfo {
index,
block,
attr,
count: None,
lines: Vec::new(),
})
})
.collect();
}
fn add_arcs(&mut self, function: &mut FunctionInfo, index: FunctionIndex, arcs: &Arcs) {
trace!("gcno-add-arcs ({}): {:?} -> {} dests", index.0, arcs.src_block, arcs.arcs.len());
let src_ni = function.node(arcs.src_block);
debug_assert!(self.graph.neighbors(src_ni).next().is_none());
for (local_arc_index, arc) in arcs.arcs.iter().enumerate() {
let dest_ni = function.node(arc.dest_block);
let is_real_arc = !arc.flags.contains(ArcAttr::ON_TREE);
let arc_info = ArcInfo {
index,
arc: local_arc_index,
count: if is_real_arc { Some(0) } else { None },
attr: arc.flags,
};
let ei = self.graph.add_edge(src_ni, dest_ni, arc_info);
if is_real_arc {
function.arcs.push(ei);
}
}
}
fn add_lines(&mut self, function: &FunctionInfo, index: FunctionIndex, all_lines: BTreeMap<usize, &[Line]>) {
trace!("gcno-add-lines ({})", index.0);
for ni in &function.nodes {
let block = &mut self.graph[*ni];
debug_assert!(block.lines.is_empty());
let mut lines_range = all_lines.range((Bound::Unbounded, Bound::Included(block.block)));
block.lines = lines_range
.next_back()
.map(|(&block_number, &lines)| {
if block_number == block.block || lines.is_empty() {
return lines.to_owned();
}
let mut last_line = [Line::FileName(UNKNOWN_SYMBOL), Line::LineNumber(0)];
let mut has_line_number = false;
let mut has_filename = false;
for line in lines.iter().rev() {
match *line {
Line::FileName(_) if !has_filename => {
has_filename = true;
last_line[0] = *line;
},
Line::LineNumber(_) if !has_line_number => {
has_line_number = true;
last_line[1] = *line;
},
_ => {},
}
if has_line_number && has_filename {
break;
}
}
last_line.to_vec()
})
.unwrap_or_default();
}
}
fn find_function(&self, checksum: u32, ident: Ident, function: Function) -> Result<FunctionIndex> {
trace!("gcda-function #{}@{}: {:?}", ident, checksum, function);
let identity = GcdaFunctionIdentity::new(checksum, ident, &function);
self.gcda_index.get(&identity).cloned().ok_or_else(|| ErrorKind::MissingFunction(checksum, ident).into())
}
fn add_arc_counts(&mut self, index: FunctionIndex, ac: ArcCounts) -> Result<()> {
trace!("gcda-arc-counts ({}): {:?}", index.0, ac);
let function = get_function!(self, index);
ensure!(
ac.counts.len() == function.arcs.len(),
ErrorKind::CountsMismatch("arcs", Type::Gcda, ac.counts.len(), function.arcs.len())
);
for (&ei, &new_count) in function.arcs.iter().zip(ac.counts.iter()) {
let count = &mut self.graph[ei].count;
match *count {
None => *count = Some(new_count),
Some(ref mut c) => *c += new_count,
}
}
Ok(())
}
}
#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct ArcInfo {
index: FunctionIndex,
arc: usize,
count: Option<u64>,
attr: ArcAttr,
}
#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct BlockInfo {
index: FunctionIndex,
block: usize,
count: Option<u64>,
attr: BlockAttr,
lines: Vec<Line>,
}
impl BlockInfo {
fn is_entry_block(&self) -> bool {
self.block == 0
}
fn iter_lines(&self) -> IterLines {
IterLines {
filename: UNKNOWN_SYMBOL,
iter: self.lines.iter(),
}
}
}
struct IterLines<'a> {
filename: Symbol,
iter: ::std::slice::Iter<'a, Line>,
}
impl<'a> Iterator for IterLines<'a> {
type Item = (Symbol, u32);
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.iter.next() {
Some(&Line::FileName(filename)) => self.filename = filename,
Some(&Line::LineNumber(ln)) => return Some((self.filename, ln)),
None => return None,
}
}
}
}
#[derive(Default, Debug, Clone)]
struct FunctionInfo {
arcs: Vec<EdgeIndex>,
nodes: Vec<NodeIndex>,
source: Option<Source>,
}
impl FunctionInfo {
fn node(&self, block_index: BlockIndex) -> NodeIndex {
self.nodes[usize::from(block_index)]
}
fn entry_block(&self) -> NodeIndex {
self.nodes[0]
}
fn exit_block(&self, version: Version) -> NodeIndex {
let index = if version >= VERSION_4_7 {
1
} else {
self.nodes.len() - 1
};
self.nodes[index]
}
}
impl Graph {
pub fn write_dot<W: io::Write>(&self, function_name: Symbol, mut writer: W) -> io::Result<()> {
fn count_to_color_label(count: Option<u64>) -> (&'static str, Cow<'static, str>) {
match count {
Some(0) => ("red", Cow::Borrowed("0")),
Some(c) => ("darkgreen", Cow::Owned(c.to_string())),
None => ("gray", Cow::Borrowed("?")),
}
}
writeln!(writer, "digraph {{\n\tnode[shape=plain]")?;
let mut allowed_nodes = HashSet::new();
for (ni, block) in self.graph.node_references() {
let function = &self[block.index];
if function_name != UNKNOWN_SYMBOL && function_name != function.source.map_or(UNKNOWN_SYMBOL, |a| a.name) {
continue;
}
allowed_nodes.insert(ni);
let (color, label) = count_to_color_label(block.count);
let line = if ni == function.entry_block() {
"ENTRY".to_owned()
} else if ni == function.exit_block(self.version) {
"EXIT".to_owned()
} else {
let mut s = String::new();
for (i, (_, line)) in block.iter_lines().enumerate() {
use std::fmt::Write;
write!(s, "{}{}", if i == 0 { '#' } else { ',' }, line).expect(":(");
}
if s.is_empty() {
s.push('?');
}
s
};
writeln!(
writer,
"\t{} [label=<\
<table cellspacing=\"0\">\
<tr>\
<td rowspan=\"2\"><font color=\"{}\">{}</font></td>\
<td><font point-size=\"9\">@{}</font></td>\
</tr>\
<tr>\
<td><font point-size=\"9\">{}</font></td>\
</tr>\
</table>\
>]",
ni.index(),
color,
label,
block.block,
line,
)?;
}
for edge_ref in self.graph.edge_references() {
let src = edge_ref.source();
if !allowed_nodes.contains(&src) {
continue;
}
let src = src.index();
let dest = edge_ref.target().index();
let arc = edge_ref.weight();
let (font_color, label) = count_to_color_label(arc.count);
let (style, color, weight) = if arc.attr.contains(ArcAttr::FAKE) {
("dotted", "green", 0)
} else if arc.attr.contains(ArcAttr::FALLTHROUGH) {
("solid", "blue", 100)
} else {
("solid", "black", 10)
};
writeln!(
writer,
"\t{} -> {} [style={}, color={}, weight={}, constraint={}, fontcolor=\"{}\", label=<{}<font point-size=\"9\">({:02x}h)</font>>]",
src,
dest,
style,
color,
weight,
!arc.attr.contains(ArcAttr::FAKE),
font_color,
label,
arc.attr,
)?;
}
writeln!(writer, "}}")?;
Ok(())
}
}