use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
};
use crate::{
instruction::{
Instruction, InstructionHandler, Jump, JumpUnless, JumpWhen, Label, MemoryReference, Target,
},
program::{
scheduling::{
schedule::{ComputedScheduleError, ComputedScheduleItem, Schedule, TimeSpan, Zero},
ScheduleError, ScheduledBasicBlock, Seconds,
},
ProgramError,
},
Program,
};
#[derive(Clone, Debug, Default)]
pub struct ControlFlowGraph<'p> {
blocks: Vec<BasicBlock<'p>>,
}
impl<'p> ControlFlowGraph<'p> {
pub fn has_dynamic_control_flow(&self) -> bool {
self.blocks
.iter()
.any(|block| block.terminator().is_dynamic())
}
pub fn into_blocks(self) -> Vec<BasicBlock<'p>> {
self.blocks
}
}
#[derive(Clone, Debug)]
pub struct ControlFlowGraphOwned {
blocks: Vec<BasicBlockOwned>,
}
impl From<ControlFlowGraph<'_>> for ControlFlowGraphOwned {
fn from(value: ControlFlowGraph) -> Self {
let blocks = value
.blocks
.into_iter()
.map(BasicBlockOwned::from)
.collect();
ControlFlowGraphOwned { blocks }
}
}
impl<'p> From<&'p ControlFlowGraphOwned> for ControlFlowGraph<'p> {
fn from(value: &'p ControlFlowGraphOwned) -> Self {
let blocks = value.blocks.iter().map(BasicBlock::from).collect();
ControlFlowGraph { blocks }
}
}
#[derive(Clone, Debug, Default)]
pub struct BasicBlock<'p> {
label: Option<&'p Target>,
instructions: Vec<&'p Instruction>,
instruction_index_offset: usize,
terminator: BasicBlockTerminator<'p>,
}
impl<'p> BasicBlock<'p> {
pub fn label(&self) -> Option<&'p Target> {
self.label
}
pub fn instruction_index_offset(&self) -> usize {
self.instruction_index_offset
}
pub fn instructions(&self) -> &[&'p Instruction] {
self.instructions.as_ref()
}
pub fn terminator(&self) -> &BasicBlockTerminator<'p> {
&self.terminator
}
pub fn as_schedule_seconds(
&self,
program: &Program,
) -> Result<Schedule<Seconds>, BasicBlockScheduleError> {
self.as_schedule(
program,
ScheduledBasicBlock::get_instruction_duration_seconds,
)
}
pub fn as_schedule<F, Time>(
&self,
program: &'p Program,
get_duration: F,
) -> Result<Schedule<Time>, BasicBlockScheduleError>
where
F: Fn(&Program, &Instruction) -> Option<Time>,
Time: Clone
+ Debug
+ PartialOrd
+ std::ops::Add<Time, Output = Time>
+ std::ops::Sub<Time, Output = Time>
+ Zero,
{
let mut calibrated_to_uncalibrated_instruction_source_mapping = BTreeMap::new();
let mut calibrated_block_instructions = Vec::new();
for (uncalibrated_instruction_index, instruction) in self.instructions.iter().enumerate() {
let first_calibrated_instruction_index = calibrated_block_instructions.len();
if let Some(expanded) = program.calibrations.expand(instruction, &[])? {
calibrated_block_instructions.extend(expanded.into_iter());
} else {
calibrated_block_instructions.push((*instruction).clone());
}
calibrated_to_uncalibrated_instruction_source_mapping.insert(
first_calibrated_instruction_index,
uncalibrated_instruction_index,
);
}
let calibrated_block = BasicBlock {
label: self.label,
instructions: calibrated_block_instructions.iter().collect(),
instruction_index_offset: self.instruction_index_offset,
terminator: self.terminator.clone(),
};
let mut instruction_handler = InstructionHandler::default();
let scheduled_self =
ScheduledBasicBlock::build(calibrated_block, program, &mut instruction_handler)?;
let schedule = scheduled_self.as_schedule(program, get_duration)?;
let uncalibrated_schedule_items_by_instruction_index = schedule
.into_items()
.into_iter()
.fold(HashMap::<usize, TimeSpan<Time>>::new(), |mut map, item| {
if let Some((_, uncalibrated_instruction_index)) =
calibrated_to_uncalibrated_instruction_source_mapping
.range(..=item.instruction_index)
.next_back()
{
if let Some(existing_time_span) = map.get_mut(uncalibrated_instruction_index) {
*existing_time_span = existing_time_span.clone().union(item.time_span);
} else {
map.insert(*uncalibrated_instruction_index, item.time_span.clone());
}
}
map
});
let schedule_items = uncalibrated_schedule_items_by_instruction_index
.into_iter()
.map(|(instruction_index, time_span)| ComputedScheduleItem {
instruction_index,
time_span,
})
.collect::<Vec<_>>();
let schedule = Schedule::from(schedule_items);
Ok(schedule)
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, thiserror::Error)]
pub enum BasicBlockScheduleError {
#[error(transparent)]
ScheduleError(#[from] ScheduleError),
#[error(transparent)]
ComputedScheduleError(#[from] ComputedScheduleError),
#[error(transparent)]
ProgramError(#[from] ProgramError),
}
#[derive(Clone, Debug)]
pub struct BasicBlockOwned {
label: Option<Target>,
instructions: Vec<Instruction>,
instruction_index_offset: usize,
terminator: BasicBlockTerminatorOwned,
}
impl From<BasicBlock<'_>> for BasicBlockOwned {
fn from(value: BasicBlock) -> Self {
let label = value.label.cloned();
let instructions = value.instructions.into_iter().cloned().collect();
let instruction_index_offset = value.instruction_index_offset;
let terminator = value.terminator.into();
BasicBlockOwned {
label,
instructions,
instruction_index_offset,
terminator,
}
}
}
impl<'b> From<&'b BasicBlockOwned> for BasicBlock<'b> {
fn from(value: &'b BasicBlockOwned) -> Self {
let label = value.label.as_ref();
let instructions = value.instructions.iter().collect();
let instruction_index_offset = value.instruction_index_offset;
let terminator = (&value.terminator).into();
BasicBlock {
label,
instructions,
instruction_index_offset,
terminator,
}
}
}
#[derive(Clone, Debug, Default)]
pub enum BasicBlockTerminator<'p> {
ConditionalJump {
condition: &'p MemoryReference,
target: &'p Target,
jump_if_condition_zero: bool,
},
#[default]
Continue,
Jump {
target: &'p Target,
},
Halt,
}
impl BasicBlockTerminator<'_> {
pub fn is_dynamic(&self) -> bool {
matches!(self, BasicBlockTerminator::ConditionalJump { .. })
}
pub fn into_instruction(self) -> Option<Instruction> {
match self {
BasicBlockTerminator::ConditionalJump {
condition,
target,
jump_if_condition_zero,
} => {
if jump_if_condition_zero {
Some(Instruction::JumpUnless(JumpUnless {
condition: condition.clone(),
target: target.clone(),
}))
} else {
Some(Instruction::JumpWhen(JumpWhen {
condition: condition.clone(),
target: target.clone(),
}))
}
}
BasicBlockTerminator::Continue => None,
BasicBlockTerminator::Jump { target } => Some(Instruction::Jump(Jump {
target: target.clone(),
})),
BasicBlockTerminator::Halt => Some(Instruction::Halt),
}
}
}
#[derive(Clone, Debug, Default)]
pub enum BasicBlockTerminatorOwned {
ConditionalJump {
condition: MemoryReference,
target: Target,
jump_if_condition_zero: bool,
},
#[default]
Continue,
Jump {
target: Target,
},
Halt,
}
impl<'p> From<BasicBlockTerminator<'p>> for BasicBlockTerminatorOwned {
fn from(value: BasicBlockTerminator) -> Self {
match value {
BasicBlockTerminator::ConditionalJump {
condition,
target,
jump_if_condition_zero,
} => BasicBlockTerminatorOwned::ConditionalJump {
condition: condition.clone(),
target: target.clone(),
jump_if_condition_zero,
},
BasicBlockTerminator::Continue => BasicBlockTerminatorOwned::Continue,
BasicBlockTerminator::Jump { target } => BasicBlockTerminatorOwned::Jump {
target: target.clone(),
},
BasicBlockTerminator::Halt => BasicBlockTerminatorOwned::Halt,
}
}
}
impl<'p> From<&'p BasicBlockTerminatorOwned> for BasicBlockTerminator<'p> {
fn from(value: &'p BasicBlockTerminatorOwned) -> Self {
match value {
BasicBlockTerminatorOwned::ConditionalJump {
condition,
target,
jump_if_condition_zero,
} => BasicBlockTerminator::ConditionalJump {
condition,
target,
jump_if_condition_zero: *jump_if_condition_zero,
},
BasicBlockTerminatorOwned::Continue => BasicBlockTerminator::Continue,
BasicBlockTerminatorOwned::Jump { target } => BasicBlockTerminator::Jump { target },
BasicBlockTerminatorOwned::Halt => BasicBlockTerminator::Halt,
}
}
}
impl<'p> From<&'p Program> for ControlFlowGraph<'p> {
fn from(value: &'p Program) -> Self {
let mut graph = ControlFlowGraph::default();
let mut current_label = None;
let mut current_block_instructions = Vec::new();
let mut instruction_index_offset = 0;
for instruction in &value.instructions {
match instruction {
Instruction::Arithmetic(_)
| Instruction::BinaryLogic(_)
| Instruction::Call(_)
| Instruction::Capture(_)
| Instruction::Convert(_)
| Instruction::Comparison(_)
| Instruction::Delay(_)
| Instruction::Fence(_)
| Instruction::Exchange(_)
| Instruction::Gate(_)
| Instruction::Load(_)
| Instruction::Pragma(_)
| Instruction::Measurement(_)
| Instruction::Move(_)
| Instruction::Nop
| Instruction::Pulse(_)
| Instruction::RawCapture(_)
| Instruction::Reset(_)
| Instruction::SetFrequency(_)
| Instruction::SetPhase(_)
| Instruction::SetScale(_)
| Instruction::ShiftFrequency(_)
| Instruction::ShiftPhase(_)
| Instruction::Store(_)
| Instruction::SwapPhases(_)
| Instruction::UnaryLogic(_)
| Instruction::Wait => current_block_instructions.push(instruction),
Instruction::CalibrationDefinition(_)
| Instruction::CircuitDefinition(_)
| Instruction::Declaration(_)
| Instruction::FrameDefinition(_)
| Instruction::GateDefinition(_)
| Instruction::Include(_)
| Instruction::MeasureCalibrationDefinition(_)
| Instruction::WaveformDefinition(_) => {}
Instruction::Label(Label { target }) => {
if !current_block_instructions.is_empty() || current_label.is_some() {
let block = BasicBlock {
label: current_label.take(),
instructions: std::mem::take(&mut current_block_instructions),
instruction_index_offset,
terminator: BasicBlockTerminator::Continue,
};
instruction_index_offset += block.instructions.len() + 1;
graph.blocks.push(block);
}
current_label = Some(target);
}
Instruction::Jump(_)
| Instruction::JumpUnless(_)
| Instruction::JumpWhen(_)
| Instruction::Halt => {
let terminator = match instruction {
Instruction::Jump(jump) => BasicBlockTerminator::Jump {
target: &jump.target,
},
Instruction::JumpUnless(jump_unless) => {
BasicBlockTerminator::ConditionalJump {
condition: &jump_unless.condition,
target: &jump_unless.target,
jump_if_condition_zero: true,
}
}
Instruction::JumpWhen(jump_when) => BasicBlockTerminator::ConditionalJump {
condition: &jump_when.condition,
target: &jump_when.target,
jump_if_condition_zero: false,
},
Instruction::Halt => BasicBlockTerminator::Halt,
_ => unreachable!(),
};
let block = BasicBlock {
label: current_label.take(),
instructions: std::mem::take(&mut current_block_instructions),
instruction_index_offset,
terminator,
};
let label_instruction_offset = if block.label().is_some() { 1 } else { 0 };
instruction_index_offset +=
block.instructions.len() + 1 + label_instruction_offset;
graph.blocks.push(block);
}
}
}
if !current_block_instructions.is_empty() || current_label.is_some() {
let block = BasicBlock {
label: current_label.take(),
instructions: current_block_instructions,
instruction_index_offset,
terminator: BasicBlockTerminator::Continue,
};
graph.blocks.push(block);
}
graph
}
}
impl<'a> TryFrom<&'a Program> for BasicBlock<'a> {
type Error = ProgramEmptyOrContainsMultipleBasicBlocks;
fn try_from(value: &'a Program) -> Result<Self, Self::Error> {
let blocks = ControlFlowGraph::from(value).blocks;
if blocks.len() == 1 {
Ok(blocks.into_iter().next().unwrap())
} else {
Err(ProgramEmptyOrContainsMultipleBasicBlocks)
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("Program is empty or contains multiple basic blocks")]
pub struct ProgramEmptyOrContainsMultipleBasicBlocks;
#[cfg(test)]
mod tests {
use crate::Program;
use rstest::rstest;
use super::*;
use super::super::test_programs::*;
#[rstest]
#[case(QUIL_AS_TREE)]
#[case(QUIL_AS_INVERSE_TREE)]
#[case(QUIL_AS_LINEAR)]
#[case(QUIL_WITH_DIAMOND)]
#[case(QUIL_WITH_SWAP)]
#[case(KITCHEN_SINK_QUIL)]
fn expect_single_basic_block(#[case] input: &str) {
let program: Program = input.parse().unwrap();
let _: BasicBlock = (&program).try_into().unwrap();
}
#[rstest]
#[case(QUIL_AS_TREE, false)]
#[case(QUIL_AS_INVERSE_TREE, false)]
#[case(QUIL_AS_LINEAR, false)]
#[case(QUIL_WITH_DIAMOND, false)]
#[case(KITCHEN_SINK_QUIL, false)]
#[case(QUIL_WITH_JUMP, false)]
#[case(QUIL_WITH_JUMP_WHEN, true)]
#[case(QUIL_WITH_JUMP_UNLESS, true)]
fn has_dynamic_control_flow(#[case] input: &str, #[case] expected: bool) {
let program: Program = input.parse().unwrap();
let graph = ControlFlowGraph::from(&program);
let dynamic = graph.has_dynamic_control_flow();
assert_eq!(expected, dynamic);
}
#[rstest]
#[case(r#"LABEL @a
JUMP @a
LABEL @b
JUMP @b
LABEL @c
JUMP @c
"#, vec![0, 2, 4])]
#[case(r#"LABEL @a
LABEL @b
LABEL @c
JUMP @c
"#, vec![0, 1, 2])]
#[case(r#"DEFFRAME 0 "rf":
ATTRIBUTE: 1
DEFCAL X 0:
Y 0
X 0
"#, vec![0])]
fn instruction_index_offset(#[case] input: &str, #[case] expected_block_offsets: Vec<usize>) {
let program: Program = input.parse().unwrap();
let graph = ControlFlowGraph::from(&program);
let blocks = graph.into_blocks();
println!("blocks: {:#?}", blocks);
let actual = blocks
.iter()
.map(|block| block.instruction_index_offset())
.collect::<Vec<_>>();
assert_eq!(expected_block_offsets, actual);
}
#[test]
fn schedule_uncalibrated() {
let input = r#"DEFFRAME 0 "a":
ATTRIBUTE: 1
DEFFRAME 0 "b":
ATTRIBUTE: 1
DEFCAL A 0:
NONBLOCKING PULSE 0 "a" flat(duration: 1.0)
NONBLOCKING PULSE 0 "a" flat(duration: 1.0)
NONBLOCKING PULSE 0 "a" flat(duration: 1.0)
DEFCAL B 0:
NONBLOCKING PULSE 0 "b" flat(duration: 10.0)
A 0
B 0
FENCE
B 0
PULSE 0 "a" flat(duration: 1.0)
"#;
let program: Program = input.parse().unwrap();
let graph = ControlFlowGraph::from(&program);
let blocks = graph.into_blocks();
println!("blocks: {:#?}", blocks);
let schedule = blocks[0].as_schedule_seconds(&program).unwrap();
println!("schedule: {:#?}", schedule);
assert_eq!(schedule.duration().0, 21.0);
let schedule_items = schedule.into_items();
let schedule_items_by_instruction_index = schedule_items
.iter()
.map(|item| (item.instruction_index, item.clone()))
.collect::<HashMap<_, _>>();
assert_eq!(
schedule_items_by_instruction_index[&0]
.time_span
.start_time()
.0,
0.0
);
assert_eq!(
schedule_items_by_instruction_index[&0]
.time_span
.duration()
.0,
3.0
);
assert_eq!(
schedule_items_by_instruction_index[&1]
.time_span
.start_time()
.0,
0.0
);
assert_eq!(
schedule_items_by_instruction_index[&1]
.time_span
.duration()
.0,
10.0
);
assert_eq!(
schedule_items_by_instruction_index[&3]
.time_span
.start_time()
.0,
10.0
);
assert_eq!(
schedule_items_by_instruction_index[&3]
.time_span
.duration()
.0,
10.0
);
assert_eq!(
schedule_items_by_instruction_index[&4]
.time_span
.start_time()
.0,
20.0
);
assert_eq!(
schedule_items_by_instruction_index[&4]
.time_span
.duration()
.0,
1.0
);
}
#[test]
fn schedule_uncalibrated_cz() {
let input = r#"DEFFRAME 0 "flux_tx_cz":
TEST: 1
DEFFRAME 1 "flux_tx_iswap":
TEST: 1
DEFFRAME 1 "flux_tx_cz":
TEST: 1
DEFFRAME 1 "flux_tx_iswap":
TEST: 1
DEFFRAME 2 "flux_tx_cz":
TEST: 1
DEFFRAME 2 "flux_tx_iswap":
TEST: 1
DEFFRAME 3 "flux_tx_cz":
TEST: 1
DEFFRAME 3 "flux_tx_iswap":
TEST: 1
# Simplified version
DEFCAL CZ q0 q1:
FENCE q0 q1
SET-PHASE q0 "flux_tx_cz" 0.0
SET-PHASE q1 "flux_tx_iswap" 0.0
NONBLOCKING PULSE q0 "flux_tx_cz" erf_square(duration: 6.000000000000001e-08, pad_left: 0, pad_right: 0)
NONBLOCKING PULSE q1 "flux_tx_iswap" erf_square(duration: 6.000000000000001e-08, pad_left: 0, pad_right: 0)
SHIFT-PHASE q0 "flux_tx_cz" 1.0
SHIFT-PHASE q1 "flux_tx_iswap" 1.0
FENCE q0 q1
CZ 0 1
CZ 2 3
CZ 0 2
"#;
let program: Program = input.parse().unwrap();
let graph = ControlFlowGraph::from(&program);
let blocks = graph.into_blocks();
println!("blocks: {:#?}", blocks);
let schedule = blocks[0].as_schedule_seconds(&program).unwrap();
let mut schedule_items = schedule.into_items();
schedule_items.sort_by_key(|item| item.instruction_index);
assert_eq!(schedule_items.len(), 3);
insta::assert_debug_snapshot!(schedule_items);
}
}