use dot_writer::{Attributes, DotWriter, Shape, Style};
use crate::{
instruction::Target,
program::{
analysis::BasicBlockTerminator,
scheduling::graph::{
ExecutionDependency, MemoryAccessType, ScheduledBasicBlock, ScheduledGraphNode,
},
},
quil::Quil,
};
use super::graph::ScheduledProgram;
impl ScheduledBasicBlock<'_> {
fn write_dot_format(&self, cluster: &mut dot_writer::Scope, node_prefix: &str) {
self.graph.nodes().for_each(|node| {
let node_id = get_node_id(&node, node_prefix);
match &node {
ScheduledGraphNode::BlockEnd => {
cluster
.node_named(node_id)
.set_shape(Shape::Circle)
.set_label("end");
}
ScheduledGraphNode::BlockStart => {
cluster
.node_named(node_id)
.set_shape(Shape::Circle)
.set_label("start");
}
ScheduledGraphNode::InstructionIndex(index) => {
cluster
.node_named(node_id)
.set_shape(Shape::Rectangle)
.set_label(&escape_label(&format!(
"[{}] {}",
index,
self.instructions().get(*index).unwrap().to_quil_or_debug()
)));
}
};
self.graph.edges(node).for_each(|(src, dest, edge)| {
let source = get_node_id(&src, node_prefix);
let target = get_node_id(&dest, node_prefix);
let mut labels = edge
.iter()
.map(|dependency| match dependency {
ExecutionDependency::AwaitMemoryAccess(access_type) => match access_type {
MemoryAccessType::Read => "await read",
MemoryAccessType::Write => "await write",
MemoryAccessType::Capture => "await capture",
},
ExecutionDependency::Scheduled => "timing",
ExecutionDependency::StableOrdering => "ordering",
})
.collect::<Vec<&str>>();
labels.sort_unstable();
let label = labels.join("\n");
cluster
.edge(source, target)
.attributes()
.set_label(label.as_str());
})
});
}
}
#[derive(Debug, PartialEq)]
enum DotFormatBlockLabel<'p> {
Unlabeled { index: usize },
Labeled(&'p Target),
}
impl<'p> DotFormatBlockLabel<'p> {
fn from_label_or_index(label: Option<&'p Target>, index: usize) -> Self {
label
.map(DotFormatBlockLabel::Labeled)
.unwrap_or(DotFormatBlockLabel::Unlabeled { index })
}
}
impl<'p> From<&'p Target> for DotFormatBlockLabel<'p> {
fn from(target: &'p Target) -> Self {
DotFormatBlockLabel::Labeled(target)
}
}
impl From<usize> for DotFormatBlockLabel<'_> {
fn from(index: usize) -> Self {
DotFormatBlockLabel::Unlabeled { index }
}
}
impl std::fmt::Display for DotFormatBlockLabel<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DotFormatBlockLabel::Unlabeled { index } => write!(f, "block_{index}"),
DotFormatBlockLabel::Labeled(label) => write!(f, "{}", label.to_quil_or_debug()),
}
}
}
impl ScheduledProgram<'_> {
pub fn get_dot_format(&self) -> Vec<u8> {
let mut output_bytes = Vec::new();
{
let mut writer = DotWriter::from(&mut output_bytes);
writer.set_pretty_print(true);
let mut digraph = writer.digraph();
let blocks = self.basic_blocks();
println!("blocks: {blocks:?}");
let mut iter = blocks.iter().enumerate().peekable();
if let Some((index, first_block)) = iter.peek() {
let block_node_label = first_block
.basic_block()
.label()
.map(DotFormatBlockLabel::from)
.unwrap_or(DotFormatBlockLabel::from(*index));
digraph.edge(
"entry",
get_node_id(
&ScheduledGraphNode::BlockStart,
&block_node_label.to_string(),
),
);
}
digraph.node_named("entry").set_label("Entry Point");
while let Some((index, scheduled_basic_block)) = iter.next() {
let block_node_label = scheduled_basic_block
.basic_block()
.label()
.map(DotFormatBlockLabel::from)
.unwrap_or(DotFormatBlockLabel::from(index));
let string_label = block_node_label.to_string();
{
let mut cluster = digraph.cluster();
cluster.set_label(&string_label);
cluster.node_attributes().set_style(Style::Filled);
scheduled_basic_block.write_dot_format(&mut cluster, &string_label);
}
let next_index_and_block = iter.peek();
let terminator_source_label =
get_node_id(&ScheduledGraphNode::BlockEnd, &string_label);
match &scheduled_basic_block.terminator() {
BasicBlockTerminator::ConditionalJump {
condition,
target,
jump_if_condition_zero,
} => {
let (condition_met, condition_unmet) = if *jump_if_condition_zero {
("== 0", "!= 0")
} else {
("!= 0", "== 0")
};
digraph
.edge(
&terminator_source_label,
get_node_id(
&ScheduledGraphNode::BlockStart,
&DotFormatBlockLabel::from(*target).to_string(),
),
)
.attributes()
.set_label(
format!("if {} {}", condition.to_quil_or_debug(), condition_met)
.as_str(),
);
if let Some((next_index, next_block)) = next_index_and_block {
let next_block_prefix = DotFormatBlockLabel::from_label_or_index(
next_block.label(),
*next_index,
)
.to_string();
digraph
.edge(
&terminator_source_label,
get_node_id(
&ScheduledGraphNode::BlockStart,
&next_block_prefix,
),
)
.attributes()
.set_label(
format!(
"if {} {}",
condition.to_quil_or_debug(),
condition_unmet
)
.as_str(),
);
};
}
BasicBlockTerminator::Jump { target } => {
digraph
.edge(
&terminator_source_label,
get_node_id(
&ScheduledGraphNode::BlockStart,
&DotFormatBlockLabel::from(*target).to_string(),
),
)
.attributes()
.set_label("always");
}
BasicBlockTerminator::Continue => {
if let Some((next_index, next_block)) = next_index_and_block {
let node_label: DotFormatBlockLabel<'_> =
DotFormatBlockLabel::from_label_or_index(
next_block.label(),
*next_index,
);
digraph
.edge(
&terminator_source_label,
get_node_id(
&ScheduledGraphNode::BlockStart,
&node_label.to_string(),
),
)
.attributes()
.set_label("always");
};
}
BasicBlockTerminator::Halt => {}
}
}
}
output_bytes
}
}
fn escape_label(original: &str) -> String {
original.replace('\\', "\\\\").replace('"', "\\\"")
}
fn get_node_id(node: &ScheduledGraphNode, prefix: &str) -> String {
match node {
ScheduledGraphNode::BlockEnd => {
format!("\"{prefix}_end\"")
}
ScheduledGraphNode::BlockStart => {
format!("\"{prefix}_start\"")
}
ScheduledGraphNode::InstructionIndex(index) => {
format!("\"{prefix}_{index}\"")
}
}
}
#[cfg(test)]
pub(crate) mod tests {
pub(crate) use graph::build_dot_format_snapshot_test_case;
mod graph {
use crate::program::Program;
macro_rules! build_dot_format_snapshot_test_case {
($name: ident, $input: expr) => {
crate::program::scheduling::graphviz_dot::tests::build_dot_format_snapshot_test_case!(
$name,
$input,
&crate::instruction::DefaultHandler,
);
};
($name: ident, $input:expr, $handler: expr $(,)?) => {
#[test]
fn $name() {
use crate::program::scheduling::graph::ScheduledProgram;
const FRAME_DEFINITIONS: &'static str = "
DEFFRAME 0 \"rf\":
INITIAL-FREQUENCY: 1e6
DEFFRAME 1 \"rf\":
INITIAL-FREQUENCY: 1e6
DEFFRAME 2 \"rf\":
INITIAL-FREQUENCY: 1e6
DEFFRAME 0 \"ro_rx\":
INITIAL-FREQUENCY: 1e6
DEFFRAME 0 \"ro_tx\":
INITIAL-FREQUENCY: 1e6
DEFFRAME 0 1 \"cz\":
INITIAL-FREQUENCY: 1e6
";
let program = format!("{}\n{}", FRAME_DEFINITIONS, $input)
.parse::<Program>()
.unwrap();
let scheduled_program =
ScheduledProgram::from_program(&program, $handler).unwrap();
for block in scheduled_program.basic_blocks() {
let graph = block.get_dependency_graph();
assert!(
!petgraph::algo::is_cyclic_directed(graph),
"cycle in graph: {:?}",
graph
);
}
let dot_format_bytes = scheduled_program.get_dot_format();
let dot_format = String::from_utf8_lossy(&dot_format_bytes);
insta::assert_snapshot!(dot_format);
}
};
}
pub(crate) use build_dot_format_snapshot_test_case;
build_dot_format_snapshot_test_case!(
single_instruction,
"PULSE 0 \"rf\" test(duration: 1e6)"
);
build_dot_format_snapshot_test_case!(
single_dependency,
"
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
chained_pulses,
"
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
different_frames_blocking,
"
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 1 \"rf\" test(duration: 1e6)
PULSE 2 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
different_frames_nonblocking,
"
NONBLOCKING PULSE 0 \"rf\" test(duration: 1e6)
NONBLOCKING PULSE 1 \"rf\" test(duration: 1e6)
NONBLOCKING PULSE 2 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
blocking_pulses_wrap_nonblocking,
"
PULSE 0 \"rf\" test(duration: 1e6)
NONBLOCKING PULSE 0 \"ro_tx\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
FENCE 0
FENCE 0
"
);
build_dot_format_snapshot_test_case!(
blocking_pulses_after_nonblocking,
"
NONBLOCKING PULSE 0 \"ro_tx\" test(duration: 1e6)
PULSE 0 \"rf\" test(duration: 1e6)
PULSE 0 \"ro_rx\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
blocking_2q_pulse,
"
PULSE 0 \"rf\" test(duration: 1e-6)
PULSE 1 \"rf\" test(duration: 1e-6)
PULSE 0 1 \"cz\" test(duration: 1e-6)
"
);
build_dot_format_snapshot_test_case!(
fence_all_with_nonblocking_pulses,
"
NONBLOCKING PULSE 0 \"rf\" test(duration: 1e6)
NONBLOCKING PULSE 1 \"rf\" test(duration: 1e6)
FENCE
NONBLOCKING PULSE 0 \"rf\" test(duration: 1e6)
NONBLOCKING PULSE 1 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(fence_all, "FENCE");
build_dot_format_snapshot_test_case!(
fence_wrapper,
"
FENCE
NONBLOCKING PULSE 0 1 \"cz\" test(duration: 1e-6)
NONBLOCKING PULSE 1 \"rf\" test(duration: 1e-6)
FENCE 1
"
);
build_dot_format_snapshot_test_case!(
fence_one_wrapper,
r#"
FENCE 0
NONBLOCKING PULSE 0 "rf" flat(iq: 1, duration: 4e-7)
FENCE 0
"#
);
build_dot_format_snapshot_test_case!(
fence_three_frame_stagger,
"
PULSE 0 \"rf\" test(duration: 1e-6)
PULSE 1 \"rf\" test(duration: 1e-6)
PULSE 2 \"rf\" test(duration: 1e-6)
FENCE
DELAY 0 100e-6
FENCE 0 1
FENCE 0 2
PULSE 2 \"rf\" test(duration: 1e-6)
"
);
build_dot_format_snapshot_test_case!(
jump,
"DECLARE ro BIT
LABEL @first-block
PULSE 0 \"rf\" test(duration: 1e6)
JUMP-UNLESS @third-block ro[0]
LABEL @second-block
PULSE 0 \"rf\" test(duration: 1e6)
LABEL @third-block
PULSE 0 \"rf\" test(duration: 1e6)
"
);
build_dot_format_snapshot_test_case!(
active_reset_single_frame,
"DECLARE ro BIT
LABEL @measure
NONBLOCKING PULSE 0 \"ro_tx\" test(duration: 1e6)
NONBLOCKING CAPTURE 0 \"ro_rx\" test(duration: 1e6) ro
JUMP-WHEN @end ro[0]
LABEL @feedback
PULSE 0 \"rf\" test(duration: 1e6)
JUMP @measure
LABEL @end
"
);
build_dot_format_snapshot_test_case!(
labels_only,
"LABEL @a
LABEL @b
LABEL @c
"
);
build_dot_format_snapshot_test_case!(
simple_memory_access,
"DECLARE a INTEGER
DECLARE b INTEGER
MOVE a 1
MOVE b 2
ADD a b
"
);
build_dot_format_snapshot_test_case!(
simple_capture,
"DECLARE ro BIT
CAPTURE 0 \"ro_rx\" test ro"
);
build_dot_format_snapshot_test_case!(
pulse_after_capture,
"DECLARE ro BIT
CAPTURE 0 \"ro_rx\" test ro
PULSE 0 \"rf\" test"
);
build_dot_format_snapshot_test_case!(
pulse_after_set_frequency,
r#"DECLARE ro BIT
SET-FREQUENCY 0 "rf" 3e9
PULSE 0 "rf" test"#
);
build_dot_format_snapshot_test_case!(
parametric_pulse,
"DECLARE ro BIT
DECLARE param REAL
PULSE 0 \"rf\" test(a: param[0])
CAPTURE 0 \"ro_rx\" test(a: param[0]) ro"
);
build_dot_format_snapshot_test_case!(
parametric_pulses_using_capture_results,
"DECLARE ro BIT
DECLARE param REAL
CAPTURE 0 \"ro_rx\" test(a: param[0]) ro
NONBLOCKING PULSE 0 \"rf\" test(a: ro[0])
NONBLOCKING PULSE 1 \"rf\" test(a: ro[0])
CAPTURE 0 \"ro_rx\" test(a: param[0]) ro
NONBLOCKING PULSE 0 \"rf\" test(a: ro[0])
NONBLOCKING PULSE 1 \"rf\" test(a: ro[0])"
);
build_dot_format_snapshot_test_case!(
multiple_classical_instructions,
"DECLARE ro INTEGER[2]\nMOVE ro[0] 1\nMOVE ro[1] 0\nADD ro[0] 5\nSUB ro[1] ro[0]"
);
}
}