quil_rs/program/scheduling/
graph.rs

1//! Utilities for analysis of the dependency graph of a Quil Program
2
3// Copyright 2021 Rigetti Computing
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use std::collections::{HashMap, HashSet};
18
19use petgraph::graphmap::GraphMap;
20use petgraph::Directed;
21
22use self::dependency_queue::DependencyQueue;
23use crate::instruction::{
24    ExternSignatureMap, FrameIdentifier, Instruction, InstructionHandler, Target,
25};
26use crate::program::analysis::{
27    BasicBlock, BasicBlockOwned, BasicBlockTerminator, ControlFlowGraph,
28};
29use crate::{instruction::InstructionRole, program::Program, quil::Quil};
30
31pub use crate::program::memory::MemoryAccessType;
32
33mod dependency_queue;
34
35#[derive(Debug, Clone, Copy)]
36pub enum ScheduleErrorVariant {
37    DuplicateLabel,
38    Extern,
39    UncalibratedInstruction,
40    UnresolvedCallInstruction,
41    ControlFlowNotBlockTerminator,
42    UnschedulableInstruction,
43}
44
45#[derive(Debug, Clone, thiserror::Error)]
46#[error(
47    "Error scheduling {}: {}: {variant:?}",
48    .instruction_node.map_or_else(||  "an instruction".to_string(), |node| node.to_string()),
49    .instruction.to_quil_or_debug(),
50)]
51pub struct ScheduleError {
52    pub instruction_node: Option<ScheduledGraphNode>,
53    pub instruction: Instruction,
54    pub variant: ScheduleErrorVariant,
55}
56
57pub type ScheduleResult<T> = Result<T, ScheduleError>;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Ord)]
60pub enum ScheduledGraphNode {
61    BlockStart,
62    InstructionIndex(usize),
63    BlockEnd,
64}
65
66impl std::fmt::Display for ScheduledGraphNode {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::BlockStart => write!(f, "the start of the block"),
70            Self::InstructionIndex(ix) => write!(f, "instruction {ix}"),
71            Self::BlockEnd => write!(f, "the end-of-block terminator"),
72        }
73    }
74}
75
76/// A MemoryAccessDependency expresses a dependency that one node has on another to complete
77/// some type of memory access prior to the dependent node's execution.
78#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
79struct MemoryAccessDependency {
80    /// What type of memory access must complete prior to the downstream instruction.
81    // NOTE: This must remain the first field for ordering to work as expected.
82    pub access_type: MemoryAccessType,
83
84    /// Which node is using the given `access_type`.
85    pub node_id: ScheduledGraphNode,
86}
87
88#[derive(Clone, Debug, Eq, PartialEq, Hash)]
89pub enum ExecutionDependency {
90    /// The downstream instruction must wait for the given operation to complete.
91    AwaitMemoryAccess(MemoryAccessType),
92
93    /// The schedule of the downstream instruction depends on the upstream instruction.
94    /// Per the Quil-T specification, the downstream instruction begins execution at
95    /// the time that its latest upstream neighbor completes.
96    Scheduled,
97
98    /// The ordering between these two instructions must remain unchanged
99    StableOrdering,
100}
101
102/// Add a dependency to an edge on the graph, whether that edge currently exists or not.
103macro_rules! add_dependency {
104    ($graph:expr, $source:expr => $target:expr, $dependency:expr) => {{
105        let source = $source;
106        let target = $target;
107        let dependency = $dependency;
108        match $graph.edge_weight_mut(source, target) {
109            Some(edge) => {
110                edge.insert(dependency);
111            }
112            None => {
113                let mut edge = HashSet::new();
114                edge.insert(dependency);
115                $graph.add_edge(source, target, edge);
116            }
117        }
118    }};
119}
120
121pub type DependencyGraph = GraphMap<ScheduledGraphNode, HashSet<ExecutionDependency>, Directed>;
122
123/// A [`ScheduledBasicBlock`] is a wrapper around a [`BasicBlock`] which includes a graph expressing the vector clock
124/// among the instructions according to the Quil specification.
125///
126/// If instruction A blocks instruction B (because of shared use of a frame), then there will be an edge from A to B
127/// in the graph.
128#[derive(Clone, Debug)]
129pub struct ScheduledBasicBlock<'a> {
130    basic_block: BasicBlock<'a>,
131    pub(super) graph: DependencyGraph,
132}
133
134/// How an instruction affects a frame.
135///
136/// An instruction may do one of two things:
137///
138/// 1. It may [*block*][Self::Blocking] the frame, which indicates that while it does not play on
139///    that frame itself, it must prevent any other instructions from playing on it.
140///
141/// 2. It may [*use*][Self::Using] the frame, which indicates that the instruction play on or
142///    modifies that frame.
143///
144/// These may be thought of as reads and writes of a frame, respectively: blocking does not have an
145/// affect on the frame but must be ordered with respect to uses, while uses must happen in a
146/// specific order.  This is relevant for [`dependency_queue::Access`].
147///
148/// See also [`crate::program::MatchedFrames`] for the type that keeps track of the specific frames
149/// that are blocked or used by an instruction.
150#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
151pub enum InstructionFrameInteraction {
152    Blocking,
153    Using,
154}
155
156impl<'a> ScheduledBasicBlock<'a> {
157    /// Build a scheduled basic block from a basic block and a program.
158    pub fn build<H: InstructionHandler>(
159        basic_block: BasicBlock<'a>,
160        program: &'a Program,
161        handler: &H,
162    ) -> ScheduleResult<Self> {
163        let mut graph: DependencyGraph = GraphMap::new();
164        // Root node
165        graph.add_node(ScheduledGraphNode::BlockStart);
166
167        // The set of classical instructions that do not have outgoing edges (i.e. there are no
168        // downstream instructions that depend on them). After iterating over all instructions,
169        // the set of trailing classical instructions will need an outgoing edge to the block end.
170        let mut trailing_classical_instructions: HashSet<ScheduledGraphNode> = HashSet::new();
171
172        // Store the instruction index of the last instruction to block that frame
173        let mut last_instruction_by_frame: HashMap<
174            FrameIdentifier,
175            DependencyQueue<InstructionFrameInteraction>,
176        > = HashMap::new();
177
178        let mut last_timed_instruction_by_frame: HashMap<
179            FrameIdentifier,
180            DependencyQueue<InstructionFrameInteraction>,
181        > = HashMap::new();
182
183        // Keep track of memory accesses (reads/writes) to each region, so that they can be
184        // serialized.  This serialization is done by memory region; that is, writes to `region[0]`
185        // are not allowed to be concurrent with writes to `region[1]`.
186        //
187        // This could be refined to serialize writes to each memory region *and offset*, so writes
188        // to `region[0]` could happen in parallel with writes to `region[1]`, although this would
189        // require further refinement of [`Instruction::get_memory_accesses`] and the
190        // [`crate::program::memory::MemoryAccesses`] type.  In particular, we would need to capture
191        // accesses that read from/write to potentially an entire region, such as `LOAD` and
192        // `STORE`, as well as accesses that only access a statically known index.
193        let mut pending_memory_access: HashMap<String, DependencyQueue<MemoryAccessType>> =
194            HashMap::new();
195
196        let extern_signature_map = ExternSignatureMap::try_from(program.extern_pragma_map.clone())
197            .map_err(|(pragma, _)| ScheduleError {
198                instruction_node: None,
199                instruction: Instruction::Pragma(pragma),
200                variant: ScheduleErrorVariant::Extern,
201            })?;
202
203        let terminator = basic_block.terminator().clone().into_instruction();
204        let terminator_ref = terminator.as_ref();
205
206        let instructions_iter = basic_block
207            .instructions()
208            .iter()
209            .enumerate()
210            .map(|(index, instr)| (ScheduledGraphNode::InstructionIndex(index), *instr))
211            .chain(terminator_ref.map(|instr| (ScheduledGraphNode::BlockEnd, instr)));
212
213        for (node, instruction) in instructions_iter {
214            graph.add_node(node);
215
216            let accesses = handler
217                .memory_accesses(&extern_signature_map, instruction)
218                .map_err(|_| ScheduleError {
219                    instruction_node: Some(node),
220                    instruction: instruction.clone(),
221                    variant: ScheduleErrorVariant::UnresolvedCallInstruction,
222                })?;
223
224            let memory_dependencies = [
225                (accesses.reads, MemoryAccessType::Read),
226                (accesses.writes, MemoryAccessType::Write),
227                (accesses.captures, MemoryAccessType::Capture),
228            ]
229            .into_iter()
230            .flat_map(|(regions, access_type)| {
231                regions
232                    .into_iter()
233                    .flat_map(|region| {
234                        pending_memory_access
235                            .entry(region)
236                            .or_default()
237                            // NOTE: This mutates the underlying `DependencyQueue` by registering
238                            // the instruction node.
239                            .record_access_and_get_dependencies(node, access_type)
240                    })
241                    // We have to `collect` into a `Vec` to finish our accesses to
242                    // `pending_memory_access`.
243                    .collect::<Vec<_>>()
244            })
245            .collect::<Vec<_>>();
246
247            // Does this instruction have no incoming edges?  (Only used if this instruction is
248            // classical.)
249            let mut leading_instruction = true;
250
251            for memory_dependency in memory_dependencies {
252                // Test to make sure that no instructions depend directly on themselves
253                if memory_dependency.node_id != node {
254                    let execution_dependency =
255                        ExecutionDependency::AwaitMemoryAccess(memory_dependency.access_type);
256
257                    add_dependency!(graph, memory_dependency.node_id => node, execution_dependency);
258
259                    // This instruction now has an incoming edge, so it is not leading.
260                    leading_instruction = false;
261
262                    // This memory dependency now has an outgoing edge, so it is no longer a
263                    // trailing classical instruction. If the memory dependency is not a classical
264                    // instruction, this has no effect.
265                    trailing_classical_instructions.remove(&memory_dependency.node_id);
266                }
267            }
268
269            // We're done defining `leading_instruction`, it's no longer `mut`.
270            let leading_instruction = leading_instruction;
271
272            match handler.role(instruction) {
273                // Classical instructions must be ordered by appearance in the program
274                InstructionRole::ClassicalCompute => {
275                    // All classical instructions must occur after the block start.
276                    if leading_instruction {
277                        add_dependency!(graph, ScheduledGraphNode::BlockStart => node, ExecutionDependency::StableOrdering);
278                    }
279                    trailing_classical_instructions.insert(node);
280                    Ok(())
281                }
282                InstructionRole::RFControl => {
283                    let matched_frames = handler.matching_frames(program, instruction);
284                    let is_scheduled = handler.is_scheduled(instruction);
285
286                    if let Some(matched_frames) = matched_frames {
287                        for frame in matched_frames.used {
288                            if is_scheduled {
289                                let previous_node_ids = last_timed_instruction_by_frame
290                                    .entry((*frame).clone())
291                                    .or_default()
292                                    .record_access_and_get_dependencies(
293                                        node,
294                                        InstructionFrameInteraction::Using,
295                                    );
296
297                                for previous_node_id in previous_node_ids {
298                                    add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
299                                }
300                            }
301
302                            let previous_node_ids = last_instruction_by_frame
303                                .entry((*frame).clone())
304                                .or_default()
305                                .record_access_and_get_dependencies(
306                                    node,
307                                    InstructionFrameInteraction::Using,
308                                );
309
310                            for previous_node_id in previous_node_ids {
311                                add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
312                            }
313                        }
314
315                        for frame in matched_frames.blocked {
316                            if is_scheduled {
317                                let previous_node_ids = last_timed_instruction_by_frame
318                                    .entry((*frame).clone())
319                                    .or_default()
320                                    .record_access_and_get_dependencies(
321                                        node,
322                                        InstructionFrameInteraction::Blocking,
323                                    );
324                                for previous_node_id in previous_node_ids {
325                                    add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
326                                }
327                            }
328
329                            let previous_node_ids = last_instruction_by_frame
330                                .entry((*frame).clone())
331                                .or_default()
332                                .record_access_and_get_dependencies(
333                                    node,
334                                    InstructionFrameInteraction::Blocking,
335                                );
336                            for previous_node_id in previous_node_ids {
337                                add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
338                            }
339                        }
340                    }
341
342                    Ok(())
343                }
344                InstructionRole::ControlFlow => {
345                    if let ScheduledGraphNode::BlockEnd = node {
346                        Ok(())
347                    } else {
348                        Err(ScheduleError {
349                            instruction_node: Some(node),
350                            instruction: instruction.clone(),
351                            variant: ScheduleErrorVariant::ControlFlowNotBlockTerminator,
352                        })
353                    }
354                }
355                InstructionRole::ProgramComposition => Err(ScheduleError {
356                    instruction_node: Some(node),
357                    instruction: instruction.clone(),
358                    variant: ScheduleErrorVariant::UnschedulableInstruction,
359                }),
360            }?;
361        }
362
363        // Link all pending dependency nodes to the end of the block, to ensure that the block
364        // does not terminate until these are complete
365        for trailing_classical_instruction in trailing_classical_instructions {
366            add_dependency!(graph, trailing_classical_instruction => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
367        }
368
369        for node in last_timed_instruction_by_frame
370            .into_values()
371            .flat_map(DependencyQueue::into_pending_dependencies)
372        {
373            add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::Scheduled);
374        }
375
376        for node in last_instruction_by_frame
377            .into_values()
378            .flat_map(DependencyQueue::into_pending_dependencies)
379        {
380            add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
381        }
382
383        // Maintain the invariant that the block start node has a connecting path to the block end node.
384        if basic_block.instructions().is_empty() {
385            add_dependency!(graph, ScheduledGraphNode::BlockStart => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
386        }
387
388        Ok(ScheduledBasicBlock { graph, basic_block })
389    }
390
391    pub fn get_dependency_graph(&self) -> &DependencyGraph {
392        &self.graph
393    }
394
395    pub fn instructions(&'a self) -> &'a [&'a Instruction] {
396        self.basic_block.instructions()
397    }
398
399    /// Return a particular-indexed instruction (if present).
400    pub fn get_instruction(&self, node_id: usize) -> Option<&Instruction> {
401        self.instructions().get(node_id).copied()
402    }
403
404    pub fn label(&self) -> Option<&Target> {
405        self.basic_block.label()
406    }
407
408    /// Return the count of executable instructions in this block.
409    pub fn len(&self) -> usize {
410        self.instructions().len()
411    }
412
413    /// Return true if this block contains no executable instructions.
414    pub fn is_empty(&self) -> bool {
415        self.instructions().is_empty()
416    }
417
418    pub fn terminator(&self) -> &BasicBlockTerminator<'a> {
419        self.basic_block.terminator()
420    }
421
422    pub fn basic_block(&self) -> &BasicBlock<'a> {
423        &self.basic_block
424    }
425}
426
427/// A program broken down into its [`ScheduledBasicBlock`]s.  All instruction-level scheduling in a
428/// program is intra-block; the only dependencies between basic blocks are those resulting from
429/// execution flow.  For instance, we do *not* track memory dependencies from a write in one block to
430/// a read in a subsequent block.
431#[derive(Clone, Debug)]
432pub struct ScheduledProgram<'a> {
433    basic_blocks: Vec<ScheduledBasicBlock<'a>>,
434}
435
436impl<'a> ScheduledProgram<'a> {
437    /// Structure a sequential program
438    pub fn from_program<H: InstructionHandler>(
439        program: &'a Program,
440        handler: &H,
441    ) -> ScheduleResult<Self> {
442        let control_flow_graph = ControlFlowGraph::from(program);
443        Ok(Self {
444            basic_blocks: control_flow_graph
445                .into_blocks()
446                .into_iter()
447                .map(|block| ScheduledBasicBlock::build(block, program, handler))
448                .collect::<ScheduleResult<Vec<_>>>()?,
449        })
450    }
451
452    pub fn basic_blocks(&self) -> &[ScheduledBasicBlock<'_>] {
453        self.basic_blocks.as_ref()
454    }
455
456    pub fn into_basic_blocks(self) -> Vec<ScheduledBasicBlock<'a>> {
457        self.basic_blocks
458    }
459}
460
461#[derive(Clone, Debug)]
462pub struct ScheduledBasicBlockOwned {
463    basic_block: BasicBlockOwned,
464    graph: DependencyGraph,
465}
466
467impl<'a> From<&'a ScheduledBasicBlockOwned> for ScheduledBasicBlock<'a> {
468    fn from(block: &'a ScheduledBasicBlockOwned) -> Self {
469        Self {
470            basic_block: (&block.basic_block).into(),
471            graph: block.graph.clone(),
472        }
473    }
474}
475
476impl From<ScheduledBasicBlock<'_>> for ScheduledBasicBlockOwned {
477    fn from(block: ScheduledBasicBlock) -> Self {
478        Self {
479            basic_block: block.basic_block.into(),
480            graph: block.graph.clone(),
481        }
482    }
483}
484
485#[cfg(all(test, feature = "graphviz-dot"))]
486mod graphviz_dot_tests {
487    use super::*;
488
489    use crate::program::scheduling::graphviz_dot::tests::build_dot_format_snapshot_test_case;
490
491    mod custom_handler {
492        use super::*;
493
494        use crate::instruction::DefaultHandler;
495        use crate::instruction::Pragma;
496        use crate::instruction::PragmaArgument;
497        use crate::program::frame::FrameMatchCondition;
498        use crate::program::MemoryAccessesError;
499        use crate::program::{MatchedFrames, MemoryAccesses};
500
501        /// A custom [`InstructionHandler`] that specially handles two `PRAGMA` instructions:
502        ///
503        /// - `NO-OP` is considered a `ClassicalCompute` instruction that does nothing
504        /// - `RAW-INSTRUCTION` is an `RFControl` instruction that is scheduled on all frames by default
505        ///   or the frame names specified as arguments, and reads from `ro`.
506        ///
507        /// Note that any program being tested must define at least one frame for `RAW-INSTRUCTION` to
508        /// have any effect.
509        struct CustomHandler;
510
511        const NO_OP: &str = "NO-OP";
512        const RAW_INSTRUCTION: &str = "RAW-INSTRUCTION";
513
514        impl InstructionHandler for CustomHandler {
515            fn is_scheduled(&self, instruction: &Instruction) -> bool {
516                match instruction {
517                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => false,
518                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => true,
519                    _ => DefaultHandler.is_scheduled(instruction),
520                }
521            }
522
523            fn role(&self, instruction: &Instruction) -> InstructionRole {
524                match instruction {
525                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => {
526                        InstructionRole::ClassicalCompute
527                    }
528                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => {
529                        InstructionRole::RFControl
530                    }
531                    _ => DefaultHandler.role(instruction),
532                }
533            }
534
535            fn matching_frames<'p>(
536                &self,
537                program: &'p Program,
538                instruction: &Instruction,
539            ) -> Option<MatchedFrames<'p>> {
540                match instruction {
541                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => None,
542                    Instruction::Pragma(Pragma {
543                        name, arguments, ..
544                    }) if name == RAW_INSTRUCTION => {
545                        let frame_condition = if arguments.is_empty() {
546                            FrameMatchCondition::All
547                        } else {
548                            FrameMatchCondition::AnyOfNames(
549                                arguments
550                                    .iter()
551                                    .filter_map(|arg| match arg {
552                                        PragmaArgument::Identifier(name) => Some(name.as_str()),
553                                        PragmaArgument::Integer(_) => None,
554                                    })
555                                    .collect(),
556                            )
557                        };
558
559                        let used = program
560                            .frames
561                            .get_matching_keys_for_condition(frame_condition);
562
563                        Some(MatchedFrames {
564                            used,
565                            blocked: HashSet::new(),
566                        })
567                    }
568                    _ => DefaultHandler.matching_frames(program, instruction),
569                }
570            }
571
572            fn memory_accesses(
573                &self,
574                extern_signature_map: &ExternSignatureMap,
575                instruction: &Instruction,
576            ) -> Result<MemoryAccesses, MemoryAccessesError> {
577                match instruction {
578                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => {
579                        Ok(MemoryAccesses::none())
580                    }
581                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => {
582                        Ok(MemoryAccesses {
583                            reads: ["ro".to_owned()].into(),
584                            writes: HashSet::new(),
585                            captures: HashSet::new(),
586                        })
587                    }
588                    _ => DefaultHandler.memory_accesses(extern_signature_map, instruction),
589                }
590            }
591        }
592
593        build_dot_format_snapshot_test_case! {
594            only_pragmas_without_frames,
595            r#"
596DEFFRAME 0 "quux":
597    SAMPLE-RATE: 1.0
598    INITIAL-FREQUENCY: 1e8
599PRAGMA NO-OP
600PRAGMA RAW-INSTRUCTION
601PRAGMA RAW-INSTRUCTION
602PRAGMA NO-OP
603PRAGMA RAW-INSTRUCTION
604"#,
605            &CustomHandler,
606        }
607
608        build_dot_format_snapshot_test_case! {
609            only_pragmas_with_frames,
610            r#"
611DEFFRAME 0 "foo":
612    SAMPLE-RATE: 1.0
613    INITIAL-FREQUENCY: 1e8
614DEFFRAME 1 "bar":
615    SAMPLE-RATE: 1.0
616    INITIAL-FREQUENCY: 1e8
617
618PRAGMA NO-OP
619PRAGMA RAW-INSTRUCTION foo
620PRAGMA RAW-INSTRUCTION bar
621PRAGMA NO-OP
622PRAGMA RAW-INSTRUCTION foo bar
623PRAGMA NO-OP
624PRAGMA RAW-INSTRUCTION foo
625"#,
626            &CustomHandler,
627        }
628
629        build_dot_format_snapshot_test_case! {
630            mixed_pragmas_and_pulses,
631            r#"
632DEFFRAME 0 "foo":
633    SAMPLE-RATE: 1.0
634    INITIAL-FREQUENCY: 1e8
635DEFFRAME 1 "bar":
636    SAMPLE-RATE: 1.0
637    INITIAL-FREQUENCY: 1e8
638
639PRAGMA NO-OP
640PRAGMA RAW-INSTRUCTION foo
641PULSE 1 "bar" gaussian(duration: 1, fwhm: 2, t0: 3)
642PRAGMA RAW-INSTRUCTION foo bar
643PRAGMA NO-OP
644PULSE 0 "foo" gaussian(duration: 1, fwhm: 2, t0: 3)
645PRAGMA RAW-INSTRUCTION bar
646PULSE 0 "foo" gaussian(duration: 1, fwhm: 2, t0: 3)
647PULSE 1 "bar" gaussian(duration: 1, fwhm: 2, t0: 3)
648PRAGMA NO-OP
649PRAGMA RAW-INSTRUCTION foo
650"#,
651            &CustomHandler,
652        }
653    }
654
655    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
656    // we expect an edge from the first load to the second (0 -> 1).
657    build_dot_format_snapshot_test_case! {
658        classical_write_read_load_load,
659        r#"
660DECLARE params1 REAL[1]
661DECLARE params2 REAL[1]
662DECLARE params3 REAL[1]
663DECLARE integers INTEGER[1]
664LOAD params2[0] params3 integers[0] # writes params2 
665LOAD params1[0] params2 integers[0] # reads params2
666"#
667    }
668
669    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
670    // we expect an edge from the mul to the load (0 -> 1).
671    build_dot_format_snapshot_test_case! {
672        classical_write_read_mul_load,
673        r#"
674DECLARE params1 REAL[1]
675DECLARE params2 REAL[1]
676DECLARE integers INTEGER[1]
677
678MUL params2[0] 2 # reads and writes params2
679LOAD params1[0] params2 integers[0] # just reads params2
680"#
681    }
682
683    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
684    // we expect an edge from the mul to the add (0 -> 1).
685    build_dot_format_snapshot_test_case! {
686        classical_write_read_add_mul,
687        r#"
688DECLARE params1 REAL[1]
689DECLARE params2 REAL[1]
690DECLARE integers INTEGER[1]
691
692ADD params1[0] 1 # this reads and writes params1
693MUL params1[0] 2 # this reads and writes params1
694"#
695    }
696
697    // Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
698    // we expect an edge from the first load to the second (0, 1).
699    build_dot_format_snapshot_test_case! {
700        classical_read_write_load_load,
701        r#"
702DECLARE params1 REAL[1]
703DECLARE params2 REAL[1]
704DECLARE integers INTEGER[1]
705
706LOAD params1[0] params2 integers[0] # reads params2
707LOAD params2[0] params3 integers[0] # writes params2
708"#
709    }
710
711    // Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
712    // we expect an edge from the load to the mul (0, 1).
713    build_dot_format_snapshot_test_case! {
714        classical_read_write_load_mul,
715        r#"
716DECLARE params1 REAL[1]
717DECLARE params2 REAL[1]
718DECLARE integers INTEGER[1]
719
720LOAD params1[0] params2 integers[0] # reads params2
721MUL params2[0] 2                    # reads and writes params2
722"#
723    }
724
725    // Because memory reading and writing dependencies also apply to RfControl instructions, we
726    // expect edges from the first load to the first shift-phase (0 -> 1) as well as to the
727    // second load (0 -> 2), from the first shift-phase to the second load (1 -> 2), and from the
728    // second load to the second shift-phase (2 -> 3).
729    build_dot_format_snapshot_test_case! {
730        quantum_write_parameterized_operations,
731        r#"
732DEFFRAME 0 "rx":
733    INITIAL-FREQUENCY: 1e8
734DEFFRAME 1 "rx":
735    INITIAL-FREQUENCY: 1e8
736
737DECLARE params1 REAL[1]
738DECLARE params2 REAL[1]
739DECLARE integers INTEGER[1]
740
741LOAD params2[0] params1 integers[0] # writes params2
742SHIFT-PHASE 0 "rf" params2[0]       # reads params2
743LOAD params2[0] params1 integers[1] # writes params2
744SHIFT-PHASE 1 "rf" params2[0]       # reads params2
745"#
746    }
747
748    // Because a pragma by default will have no memory accesses, it should only have edges from the block start and to the block end.
749    build_dot_format_snapshot_test_case! {
750        classical_no_memory_pragma,
751        r#"PRAGMA example"#
752    }
753
754    build_dot_format_snapshot_test_case! {
755        write_capture_read,
756        r#"
757DECLARE bits BIT[1]
758DECLARE integers INTEGER[1]
759LOAD bits[0] bits2 integers[0] # write
760NONBLOCKING CAPTURE 0 "Transmon-0_readout_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) bits[0] # capture
761LOAD bits3[0] bits integers[0] # read
762"#
763    }
764
765    build_dot_format_snapshot_test_case! {
766        write_write_read,
767        r#"
768DECLARE bits BIT[1]
769DECLARE bits2 BIT[1]
770DECLARE bits3 BIT[1]
771DECLARE integers INTEGER[1]
772LOAD bits[0] bits2 integers[0] # write
773LOAD bits[0] bits3 integers[0] # write
774LOAD bits4[0] bits integers[0] # read
775"#
776    }
777
778    build_dot_format_snapshot_test_case! {
779        memory_dependency_not_in_block_terminator,
780        r#"
781DECLARE ro BIT
782DECLARE depends_on_ro BIT
783
784NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
785MOVE depends_on_ro ro
786JUMP @eq
787LABEL @eq
788PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
789"#
790    }
791
792    build_dot_format_snapshot_test_case! {
793        memory_dependency_in_block_terminator,
794        r#"
795DECLARE ro BIT
796
797NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
798JUMP-WHEN @eq ro
799LABEL @eq
800PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
801"#
802    }
803
804    build_dot_format_snapshot_test_case! {
805        no_memory_dependency_across_blocks,
806        r#"
807DECLARE ro BIT
808DECLARE depends_on_ro BIT
809
810NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
811JUMP @eq
812LABEL @eq
813MOVE depends_on_ro ro
814PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
815"#
816    }
817
818    build_dot_format_snapshot_test_case! {
819        memory_dependency_one_variable,
820        r#"DEFFRAME 0 "frame0":
821    DIRECTION: "tx"
822    INITIAL-FREQUENCY: 6864214214.214214
823    CENTER-FREQUENCY: 7250000000.0
824    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
825    SAMPLE-RATE: 1000000000.0
826DEFFRAME 1 "frame1":
827    DIRECTION: "tx"
828    INITIAL-FREQUENCY: 6864214214.214214
829    CENTER-FREQUENCY: 7250000000.0
830    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
831    SAMPLE-RATE: 1000000000.0
832
833DELAY 2e-8
834
835DECLARE phase REAL
836MOVE phase 0.1
837
838SET-PHASE 0 "frame0" 2*pi*phase
839SET-PHASE 1 "frame1" 2*pi*phase
840
841PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
842PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
843"#
844    }
845
846    build_dot_format_snapshot_test_case! {
847        memory_dependency_array,
848        r#"DEFFRAME 0 "frame0":
849    DIRECTION: "tx"
850    INITIAL-FREQUENCY: 6864214214.214214
851    CENTER-FREQUENCY: 7250000000.0
852    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
853    SAMPLE-RATE: 1000000000.0
854DEFFRAME 1 "frame1":
855    DIRECTION: "tx"
856    INITIAL-FREQUENCY: 6864214214.214214
857    CENTER-FREQUENCY: 7250000000.0
858    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
859    SAMPLE-RATE: 1000000000.0
860
861DELAY 2e-8
862
863DECLARE phase REAL[2]
864MOVE phase[0] 0.1
865MOVE phase[1] 0.1
866
867SET-PHASE 0 "frame0" 2*pi*phase[0]
868SET-PHASE 1 "frame1" 2*pi*phase[1]
869
870PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
871PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
872"#
873    }
874
875    build_dot_format_snapshot_test_case! {
876        memory_dependency_two_variables,
877        r#"DEFFRAME 0 "frame0":
878    DIRECTION: "tx"
879    INITIAL-FREQUENCY: 6864214214.214214
880    CENTER-FREQUENCY: 7250000000.0
881    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
882    SAMPLE-RATE: 1000000000.0
883DEFFRAME 1 "frame1":
884    DIRECTION: "tx"
885    INITIAL-FREQUENCY: 6864214214.214214
886    CENTER-FREQUENCY: 7250000000.0
887    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
888    SAMPLE-RATE: 1000000000.0
889
890DELAY 2e-8
891
892DECLARE phase0 REAL
893DECLARE phase1 REAL
894MOVE phase0 0.1
895MOVE phase1 0.1
896
897SET-PHASE 0 "frame0" 2*pi*phase0
898SET-PHASE 1 "frame1" 2*pi*phase1
899
900PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
901PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
902"#
903    }
904}