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