1use std::collections::{BTreeSet, HashMap, HashSet};
4use std::fmt::Write as _;
5
6use glam::IVec3;
7use thiserror::Error;
8
9use crate::ast::Span;
10use crate::graph::LinkBranchOccupancy;
11use crate::zx::{
12 ProjectedExternalTable, ProjectionError, ProjectionLimits, RuntimeBasisError, StabilizerError,
13 ZXGraph,
14};
15use crate::{
16 Action, Block, BlockGraph, BlockKind, Direction, Expr, PhasedPauliString, Pipe, PortRole,
17 UDirection, checked_add_position,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum PortDirection {
23 Input,
25 Output,
27}
28
29impl PortDirection {
30 fn keyword(self) -> &'static str {
31 match self {
32 Self::Input => "in",
33 Self::Output => "out",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct ModuleRotation {
44 axis: UDirection,
45 quarter_turns: u8,
46}
47
48impl ModuleRotation {
49 pub const IDENTITY: Self = Self {
51 axis: UDirection::Z,
52 quarter_turns: 0,
53 };
54
55 pub const fn new(axis: UDirection, quarter_turns: i32) -> Self {
57 let quarter_turns = quarter_turns.rem_euclid(4) as u8;
58 Self {
59 axis: if quarter_turns == 0 {
60 UDirection::Z
61 } else {
62 axis
63 },
64 quarter_turns,
65 }
66 }
67
68 pub const fn from_degrees(axis: UDirection, degrees: i32) -> Option<Self> {
70 if degrees % 90 == 0 {
71 Some(Self::new(axis, degrees / 90))
72 } else {
73 None
74 }
75 }
76
77 pub const fn axis(self) -> UDirection {
79 self.axis
80 }
81
82 pub const fn quarter_turns(self) -> u8 {
84 self.quarter_turns
85 }
86
87 pub const fn degrees(self) -> u16 {
89 self.quarter_turns as u16 * 90
90 }
91
92 pub const fn is_identity(self) -> bool {
94 self.quarter_turns == 0
95 }
96
97 pub fn orientation(self) -> ModuleOrientation {
99 ModuleOrientation {
100 axes: [
101 self.rotate_direction(Direction::XPLUS),
102 self.rotate_direction(Direction::YPLUS),
103 self.rotate_direction(Direction::ZPLUS),
104 ],
105 }
106 }
107
108 pub fn try_rotate_position(self, position: IVec3) -> Result<IVec3, crate::BlockGraphError> {
114 self.orientation().try_rotate_position(position)
115 }
116
117 pub fn rotate_direction(self, direction: Direction) -> Direction {
123 let mut position = direction.to_ivec3();
124 for _ in 0..self.quarter_turns {
125 position = match self.axis {
126 UDirection::X => IVec3::new(position.x, -position.z, position.y),
127 UDirection::Y => IVec3::new(position.z, position.y, -position.x),
128 UDirection::Z => IVec3::new(-position.y, position.x, position.z),
129 };
130 }
131 Direction::try_from(position).expect("a cubic rotation preserves axis directions")
132 }
133}
134
135impl Default for ModuleRotation {
136 fn default() -> Self {
137 Self::IDENTITY
138 }
139}
140
141impl std::fmt::Display for ModuleRotation {
142 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(formatter, "{} {}", self.axis, self.degrees())
144 }
145}
146
147#[doc(hidden)]
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub struct ModuleOrientation {
151 axes: [Direction; 3],
153}
154
155impl ModuleOrientation {
156 pub const IDENTITY: Self = Self {
157 axes: [Direction::XPLUS, Direction::YPLUS, Direction::ZPLUS],
158 };
159
160 pub fn then(self, rotation: ModuleRotation) -> Self {
162 self.then_orientation(rotation.orientation())
163 }
164
165 pub fn then_orientation(self, child: Self) -> Self {
167 Self {
168 axes: child.axes.map(|axis| self.rotate_direction(axis)),
169 }
170 }
171
172 pub fn try_rotate_position(self, position: IVec3) -> Result<IVec3, crate::BlockGraphError> {
178 let mut rotated = [0; 3];
179 for (component, direction) in position.to_array().into_iter().zip(self.axes) {
180 let value = if matches!(
181 direction,
182 Direction::XMINUS | Direction::YMINUS | Direction::ZMINUS
183 ) {
184 component.checked_neg().ok_or(
185 crate::BlockGraphError::CoordinateRotationOverflow {
186 position,
187 axis: direction.as_udirection(),
188 },
189 )?
190 } else {
191 component
192 };
193 rotated[direction.as_udirection().index()] = value;
194 }
195 Ok(IVec3::from_array(rotated))
196 }
197
198 pub fn try_transform_position(
204 self,
205 position: IVec3,
206 translation: IVec3,
207 ) -> Result<IVec3, crate::BlockGraphError> {
208 checked_add_position(self.try_rotate_position(position)?, translation)
209 }
210
211 pub fn rotate_direction(self, direction: Direction) -> Direction {
212 let mapped = self.axes[direction.as_udirection().index()];
213 if matches!(
214 direction,
215 Direction::XMINUS | Direction::YMINUS | Direction::ZMINUS
216 ) {
217 mapped.negate()
218 } else {
219 mapped
220 }
221 }
222
223 pub(crate) fn rotate_axis_values<T: Copy>(self, values: [T; 3]) -> [T; 3] {
224 let mut rotated = values;
225 for (local, direction) in self.axes.into_iter().enumerate() {
226 rotated[direction.as_udirection().index()] = values[local];
227 }
228 rotated
229 }
230
231 pub(crate) const fn preserves_time_direction(self) -> bool {
232 matches!(self.axes[2], Direction::ZPLUS)
233 }
234
235 pub(crate) fn is_coordinate_half_turn(self) -> bool {
236 self.axes
237 .iter()
238 .enumerate()
239 .all(|(axis, direction)| direction.as_udirection().index() == axis)
240 }
241
242 fn sort_key(self) -> [usize; 3] {
243 self.axes.map(Direction::index)
244 }
245}
246
247impl Default for ModuleOrientation {
248 fn default() -> Self {
249 Self::IDENTITY
250 }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct QuantumPort {
256 pub name: String,
258 pub position: IVec3,
260 pub direction: PortDirection,
262 pub resource_type: String,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct BitOutput {
269 pub name: String,
271 pub expr: Expr,
273}
274
275#[derive(Debug, Clone, Default, PartialEq, Eq)]
277pub struct ModuleInterface {
278 pub quantum_ports: Vec<QuantumPort>,
280 pub bit_inputs: Vec<String>,
282 pub bit_outputs: Vec<BitOutput>,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct ModuleInstance {
289 pub name: String,
291 pub definition: String,
293 pub rotation: ModuleRotation,
295 pub translation: IVec3,
297}
298
299impl ModuleInstance {
300 pub fn orientation(&self) -> ModuleOrientation {
302 self.rotation.orientation()
303 }
304
305 pub fn try_transform_position(&self, position: IVec3) -> Result<IVec3, crate::BlockGraphError> {
311 self.orientation()
312 .try_transform_position(position, self.translation)
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Hash)]
318pub struct InstancePort {
319 pub instance: String,
321 pub port: String,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub enum QuantumConnection {
328 Input {
330 block: IVec3,
332 input: InstancePort,
334 hadamard: bool,
336 },
337 Output {
339 output: InstancePort,
341 block: IVec3,
343 hadamard: bool,
345 },
346 Pipe {
348 output: InstancePort,
350 input: InstancePort,
352 hadamard: bool,
354 },
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct BitRef {
360 pub instance: Option<String>,
362 pub bit: String,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct BitBinding {
369 pub source: BitRef,
371 pub target_instance: String,
373 pub target_bit: String,
375}
376
377#[doc(hidden)]
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct MaterializedModuleSite {
381 pub definition: String,
383 pub instance_path: String,
385 pub local_position: IVec3,
387 pub orientation: ModuleOrientation,
389}
390
391impl std::fmt::Display for MaterializedModuleSite {
392 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 if self.instance_path.is_empty() {
394 write!(
395 formatter,
396 "definition '{}' local block {}",
397 self.definition, self.local_position
398 )
399 } else {
400 write!(
401 formatter,
402 "instance '{}' (definition '{}') local block {}",
403 self.instance_path, self.definition, self.local_position
404 )
405 }
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
411pub struct ModuleCertificationLimits {
412 pub max_expanded_blocks: usize,
416 pub max_expanded_instances: usize,
418 pub max_occupied_cells: usize,
422 pub max_local_columns: usize,
424 pub max_boolean_nodes: usize,
427 pub max_boolean_steps: usize,
430 pub max_matrix_words: usize,
432 pub max_frontier_width: usize,
435 pub max_witness_nodes: usize,
439 pub max_normalization_states: usize,
442 pub max_guarded_domain_size: usize,
445}
446
447impl ModuleCertificationLimits {
448 pub const DEFAULT: Self = Self {
451 max_expanded_blocks: 1_000_000,
452 max_expanded_instances: 1_000_000,
453 max_occupied_cells: 4_000_000,
454 max_local_columns: 1_000_000,
455 max_boolean_nodes: 4_000_000,
456 max_boolean_steps: usize::MAX,
457 max_matrix_words: 16_777_216,
458 max_frontier_width: 4_096,
459 max_witness_nodes: 1_000_000,
460 max_normalization_states: 1_000_000,
461 max_guarded_domain_size: 4_096,
462 };
463
464 pub const UNLIMITED: Self = Self {
466 max_expanded_blocks: usize::MAX,
467 max_expanded_instances: usize::MAX,
468 max_occupied_cells: usize::MAX,
469 max_local_columns: usize::MAX,
470 max_boolean_nodes: usize::MAX,
471 max_boolean_steps: usize::MAX,
472 max_matrix_words: usize::MAX,
473 max_frontier_width: usize::MAX,
474 max_witness_nodes: usize::MAX,
475 max_normalization_states: usize::MAX,
476 max_guarded_domain_size: usize::MAX,
477 };
478
479 pub const FIELD_NAMES: [&'static str; 11] = [
481 "max_expanded_blocks",
482 "max_expanded_instances",
483 "max_occupied_cells",
484 "max_local_columns",
485 "max_boolean_nodes",
486 "max_boolean_steps",
487 "max_matrix_words",
488 "max_frontier_width",
489 "max_witness_nodes",
490 "max_normalization_states",
491 "max_guarded_domain_size",
492 ];
493
494 pub const RESOURCE_LIMIT_HELP: &'static str = "increase the matching ModuleCertificationLimits field (or set it to usize::MAX); compilation accepts these through CompileConfig::with_certification_limits; see https://bloqec.com/docs/dev/api/rust/bloq_graph/struct.ModuleCertificationLimits.html";
496
497 pub fn set(&mut self, field: &str, value: usize) -> Result<(), UnknownCertificationLimit> {
506 let target = match field {
507 "max_expanded_blocks" => &mut self.max_expanded_blocks,
508 "max_expanded_instances" => &mut self.max_expanded_instances,
509 "max_occupied_cells" => &mut self.max_occupied_cells,
510 "max_local_columns" => &mut self.max_local_columns,
511 "max_boolean_nodes" => &mut self.max_boolean_nodes,
512 "max_boolean_steps" => &mut self.max_boolean_steps,
513 "max_matrix_words" => &mut self.max_matrix_words,
514 "max_frontier_width" => &mut self.max_frontier_width,
515 "max_witness_nodes" => &mut self.max_witness_nodes,
516 "max_normalization_states" => &mut self.max_normalization_states,
517 "max_guarded_domain_size" => &mut self.max_guarded_domain_size,
518 _ => return Err(UnknownCertificationLimit(field.to_owned())),
519 };
520 *target = value;
521 Ok(())
522 }
523
524 pub const fn boolean_limits(self) -> bloq_utils::boolean::BooleanLimits {
526 bloq_utils::boolean::BooleanLimits {
527 max_nodes: self.max_boolean_nodes,
528 max_steps: self.max_boolean_steps,
529 }
530 }
531}
532
533impl Default for ModuleCertificationLimits {
534 fn default() -> Self {
535 Self::DEFAULT
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Error)]
541#[error(
542 "unknown compilation limit '{0}'; expected one of {expected}",
543 expected = ModuleCertificationLimits::FIELD_NAMES.join(", ")
544)]
545pub struct UnknownCertificationLimit(
546 pub String,
548);
549
550#[derive(Debug, Clone)]
552pub struct LeafModuleCertificate {
553 module: String,
554 pub(crate) table: ProjectedExternalTable,
555 pub(crate) stabilizers: crate::StabilizerGenerators,
556}
557
558impl LeafModuleCertificate {
559 pub fn module(&self) -> &str {
561 &self.module
562 }
563
564 pub fn boundary_rows(&self) -> impl ExactSizeIterator<Item = &PhasedPauliString> {
566 self.table.boundary_rows.iter().map(|row| &row.signed)
567 }
568
569 pub fn stabilizers(&self) -> &crate::StabilizerGenerators {
575 &self.stabilizers
576 }
577
578 pub fn materialize_boundary_row(&self, index: usize) -> Option<PhasedPauliString> {
580 let row = self.table.boundary_rows.get(index)?;
581 Some(self.table.materialize(row))
582 }
583}
584
585#[derive(Debug, Error)]
587#[non_exhaustive]
588pub enum ModuleCertificationError {
589 #[error("unknown module '{0}'")]
591 UnknownModule(String),
592 #[error("module '{0}' contains child instances")]
594 NotLeaf(String),
595 #[error("module '{module}' uses unsupported {feature}")]
597 Unsupported {
598 module: String,
600 feature: &'static str,
602 },
603 #[error("module '{module}': {message}")]
605 InvalidConnection {
606 module: String,
608 message: String,
610 },
611 #[error("module '{module}': {source}")]
613 Graph {
614 module: String,
616 #[source]
618 source: crate::BlockGraphError,
619 },
620 #[error("module '{module}': {source}")]
622 Runtime {
623 module: String,
625 #[source]
627 source: RuntimeBasisError,
628 },
629 #[error(
631 "module '{module}' exceeded {phase} limit: {observed} > {limit}; {help}",
632 help = ModuleCertificationLimits::RESOURCE_LIMIT_HELP
633 )]
634 ResourceLimited {
635 module: String,
637 phase: &'static str,
639 observed: usize,
641 limit: usize,
643 },
644}
645
646#[derive(Debug, Error)]
648#[non_exhaustive]
649pub enum ModuleError {
650 #[error("{0}")]
652 Parse(#[from] crate::ParseError),
653 #[error("cannot load {path}: {message}")]
655 Load {
656 path: String,
658 message: String,
660 },
661 #[error("import cycle at {path}")]
663 ImportCycle {
664 path: String,
666 },
667 #[error("imports require BlockGraph::load or an explicit source resolver")]
669 ImportsRequireLoader,
670 #[error("duplicate module '{0}'")]
672 DuplicateModule(String),
673 #[error("program must define `module main`")]
675 MissingMain,
676 #[error("module '{module}': {message}")]
678 InvalidModule {
679 module: String,
681 message: String,
683 span: Option<Span>,
685 },
686 #[error("{source}")]
688 InvalidGeometry {
689 module: String,
691 #[source]
693 source: ModuleCertificationError,
694 span: Option<Span>,
696 },
697}
698
699impl ModuleError {
700 pub fn span(&self) -> Option<Span> {
702 match self {
703 Self::Parse(error) => error.span(),
704 Self::InvalidModule { span, .. } | Self::InvalidGeometry { span, .. } => *span,
705 _ => None,
706 }
707 }
708
709 pub(crate) fn with_module_span(mut self, module: &str, span: Span) -> Self {
710 match &mut self {
711 Self::InvalidModule {
712 module: name,
713 span: current,
714 ..
715 }
716 | Self::InvalidGeometry {
717 module: name,
718 span: current,
719 ..
720 } if name == module && current.is_none() => *current = Some(span),
721 _ => {}
722 }
723 self
724 }
725}
726
727impl BlockGraph {
728 pub const ENTRY_MODULE: &'static str = "main";
730
731 pub fn definition(
733 name: impl Into<String>,
734 mut body: Self,
735 interface: ModuleInterface,
736 instances: Vec<ModuleInstance>,
737 quantum_connections: Vec<QuantumConnection>,
738 bit_bindings: Vec<BitBinding>,
739 ) -> Self {
740 body.name = name.into();
741 body.interface = interface;
742 body.instances = instances;
743 body.quantum_connections = quantum_connections;
744 body.bit_bindings = bit_bindings;
745 body.interface_declared = true;
746 body
747 }
748
749 pub fn with_inferred_interface(mut self) -> Result<Self, ModuleError> {
759 if self.has_module_structure() {
760 return Ok(self);
761 }
762 let bit_inputs = self
763 .action_graph()
764 .inputs()
765 .map(str::to_string)
766 .collect::<Vec<_>>();
767 let mut port_names = (0..)
768 .map(|index| format!("q{index}"))
769 .filter(|name| !bit_inputs.contains(name));
770 let quantum_ports = self
771 .blocks()
772 .filter(|block| block.kind() == BlockKind::Port)
773 .map(|block| {
774 Ok(QuantumPort {
775 name: port_names
776 .next()
777 .expect("finite bit names leave fresh port names"),
778 position: block.pos,
779 direction: port_direction(&self, block.pos).map_err(|message| {
780 ModuleError::InvalidModule {
781 module: self.name.clone(),
782 message,
783 span: None,
784 }
785 })?,
786 resource_type: "data".to_string(),
787 })
788 })
789 .collect::<Result<Vec<_>, ModuleError>>()?;
790 self.interface = ModuleInterface {
791 quantum_ports,
792 bit_inputs,
793 bit_outputs: Vec::new(),
794 };
795 self.interface_declared = true;
796 Ok(self)
797 }
798
799 pub fn from_definitions(definitions: Vec<Self>) -> Result<Self, ModuleError> {
805 Self::from_definitions_with_limits(definitions, ModuleCertificationLimits::DEFAULT)
806 }
807
808 pub fn from_definitions_with_limits(
814 mut definitions: Vec<Self>,
815 limits: ModuleCertificationLimits,
816 ) -> Result<Self, ModuleError> {
817 let mut names = HashSet::new();
818 for definition in &definitions {
819 if !definition.definitions.is_empty() {
820 return Err(ModuleError::InvalidModule { module: definition.name.clone(), message: "from_definitions requires local definitions without nested helper libraries; use clone_local_definition on each source.modules() entry".to_string(), span: None });
821 }
822 if !names.insert(definition.name.as_str()) {
823 return Err(ModuleError::DuplicateModule(definition.name.clone()));
824 }
825 }
826 let entry = definitions
827 .iter()
828 .position(|definition| definition.name == Self::ENTRY_MODULE)
829 .ok_or(ModuleError::MissingMain)?;
830 let mut root = definitions.remove(entry);
831 root.definitions.extend(definitions);
832 root.validate_with_limits(limits)?;
833 Ok(root)
834 }
835
836 pub fn modules(&self) -> impl ExactSizeIterator<Item = &Self> + DoubleEndedIterator + Clone {
838 (0..self.definitions.len() + 1).map(move |index| {
839 if index == self.definitions.len() {
840 self
841 } else {
842 &self.definitions[index]
843 }
844 })
845 }
846
847 pub fn module_mut(&mut self, name: &str) -> Option<&mut Self> {
849 if self.name == name {
850 Some(self)
851 } else {
852 self.definitions
853 .iter_mut()
854 .find(|module| module.name == name)
855 }
856 }
857
858 pub fn root(&self) -> &Self {
860 self
861 }
862
863 pub fn module(&self, name: &str) -> Option<&Self> {
865 self.modules().find(|module| module.name == name)
866 }
867
868 pub fn local_body(&self) -> &Self {
870 self
871 }
872
873 pub fn clone_local_definition(&self) -> Self {
875 let mut graph = self.copy_local_geometry();
876 graph.name = self.name.clone();
877 graph.interface = self.interface.clone();
878 graph.instances = self.instances.clone();
879 graph.quantum_connections = self.quantum_connections.clone();
880 graph.bit_bindings = self.bit_bindings.clone();
881 graph.interface_declared = self.interface_declared;
882 graph
883 }
884
885 pub fn has_module_structure(&self) -> bool {
887 self.interface_declared
888 || self.name != Self::ENTRY_MODULE
889 || self.interface != ModuleInterface::default()
890 || !self.instances.is_empty()
891 || !self.definitions.is_empty()
892 || !self.quantum_connections.is_empty()
893 || !self.bit_bindings.is_empty()
894 }
895
896 pub fn flatten(&self) -> Result<Self, ModuleCertificationError> {
902 self.flatten_with_limits(ModuleCertificationLimits::DEFAULT)
903 }
904
905 pub fn flatten_with_limits(
911 &self,
912 limits: ModuleCertificationLimits,
913 ) -> Result<Self, ModuleCertificationError> {
914 self.validate_with_limits(limits)
915 .map_err(|error| ModuleCertificationError::Graph {
916 module: self.name.clone(),
917 source: crate::BlockGraphError::ModuleSource(std::sync::Arc::new(error)),
918 })?;
919 self.materialize_root_graph()
920 }
921
922 #[doc(hidden)]
939 pub fn materialize_root_graph(&self) -> Result<BlockGraph, ModuleCertificationError> {
940 self.materialize_flat_graph()
941 .map(|graph| graph.fix_shadowed_faces())
942 .map_err(|source| ModuleCertificationError::Graph {
943 module: self.name.clone(),
944 source,
945 })
946 }
947
948 #[doc(hidden)]
950 pub fn materialize_flat_graph(&self) -> Result<BlockGraph, crate::BlockGraphError> {
951 if !self.has_module_structure() {
952 return Ok(self.copy_local_geometry());
953 }
954 self.validate_definition_structure()
955 .map_err(|error| crate::BlockGraphError::ModuleSource(std::sync::Arc::new(error)))?;
956 flatten_module_definition(self, self.root(), "").map(|module| module.graph)
957 }
958
959 pub(crate) fn validate_hierarchy_resource_limits(
965 &self,
966 limits: ModuleCertificationLimits,
967 ) -> Result<(), ModuleError> {
968 self.validate_definition_structure()?;
969 crate::summary::preflight_program_expansion(self, limits)
970 }
971
972 fn validate_definition_structure(&self) -> Result<HashMap<&str, &Self>, ModuleError> {
973 let mut definitions = HashMap::new();
974 for module in self.modules() {
975 if !module.definitions.is_empty() && !std::ptr::eq(module, self) {
976 return Err(ModuleError::InvalidModule {
977 module: module.name.clone(),
978 message: "nested helper libraries must be resolved into the executable root"
979 .to_string(),
980 span: None,
981 });
982 }
983 if definitions.insert(module.name.as_str(), module).is_some() {
984 return Err(ModuleError::DuplicateModule(module.name.clone()));
985 }
986 }
987 if self.name != Self::ENTRY_MODULE {
988 return Err(ModuleError::MissingMain);
989 }
990 for module in self.modules() {
991 validate_module(module, &definitions)?;
992 }
993 validate_acyclic(&self.modules().collect::<Vec<_>>(), &definitions)?;
994 Ok(definitions)
995 }
996
997 pub fn validate_with_limits(
1003 &self,
1004 limits: ModuleCertificationLimits,
1005 ) -> Result<(), ModuleError> {
1006 if !self.has_module_structure() {
1007 self.validate_local_resource_limits(limits)
1008 .and_then(|()| self.validate_source())
1009 .map_err(|source| ModuleError::InvalidGeometry {
1010 module: self.name.clone(),
1011 source: ModuleCertificationError::Graph {
1012 module: self.name.clone(),
1013 source,
1014 },
1015 span: None,
1016 })?;
1017 return Ok(());
1018 }
1019 let definitions = self.validate_definition_structure()?;
1020 crate::summary::preflight_program_expansion(self, limits)?;
1021 validate_classical_cycles(&self.modules().collect::<Vec<_>>(), &definitions)?;
1022 crate::summary::validate_program_geometry(self)
1023 }
1024
1025 pub fn extract_definition(&self, name: &str) -> Result<Self, ModuleError> {
1034 self.extract_definition_with_limits(name, ModuleCertificationLimits::DEFAULT)
1035 }
1036
1037 pub fn extract_definition_with_limits(
1047 &self,
1048 name: &str,
1049 limits: ModuleCertificationLimits,
1050 ) -> Result<Self, ModuleError> {
1051 self.validate_with_limits(limits)?;
1052 if self.module(name).is_none() {
1053 return Err(ModuleError::InvalidModule {
1054 module: name.to_string(),
1055 message: "unknown graph definition".to_string(),
1056 span: None,
1057 });
1058 }
1059 if !self.has_module_structure() {
1060 return self.clone_local_definition().with_inferred_interface();
1061 }
1062 let mut reachable = BTreeSet::new();
1063 let mut pending = vec![name];
1064 while let Some(definition) = pending.pop() {
1065 if reachable.insert(definition) {
1066 pending.extend(
1067 self.module(definition)
1068 .expect("checked definition exists")
1069 .instances
1070 .iter()
1071 .map(|instance| instance.definition.as_str()),
1072 );
1073 }
1074 }
1075 let mut names = reachable
1076 .iter()
1077 .map(|name| ((*name).to_string(), (*name).to_string()))
1078 .collect::<HashMap<_, _>>();
1079 if name != Self::ENTRY_MODULE && reachable.contains(Self::ENTRY_MODULE) {
1080 let mut fresh = "source_main".to_string();
1081 let mut index = 0;
1082 while reachable.contains(fresh.as_str()) {
1083 index += 1;
1084 fresh = format!("source_main_{index}");
1085 }
1086 names.insert(Self::ENTRY_MODULE.to_string(), fresh);
1087 }
1088 names.insert(name.to_string(), Self::ENTRY_MODULE.to_string());
1089 let definitions = self
1090 .modules()
1091 .filter(|module| reachable.contains(module.name.as_str()))
1092 .map(|module| {
1093 let mut graph = module.clone_local_definition();
1094 graph.name = names[&module.name].clone();
1095 for instance in &mut graph.instances {
1096 instance.definition = names[&instance.definition].clone();
1097 }
1098 graph
1099 })
1100 .collect();
1101 Self::from_definitions_with_limits(definitions, limits)
1102 }
1103
1104 pub fn certify_leaf(
1110 &self,
1111 name: &str,
1112 limits: ModuleCertificationLimits,
1113 ) -> Result<LeafModuleCertificate, ModuleCertificationError> {
1114 if !self.has_module_structure() {
1115 let inferred = self
1116 .clone_local_definition()
1117 .with_inferred_interface()
1118 .map_err(|error| ModuleCertificationError::Graph {
1119 module: self.name.clone(),
1120 source: crate::BlockGraphError::ModuleSource(std::sync::Arc::new(error)),
1121 })?;
1122 return inferred.certify_leaf(name, limits);
1123 }
1124 self.validate_with_limits(limits)
1125 .map_err(|error| ModuleCertificationError::Graph {
1126 module: self.name.clone(),
1127 source: crate::BlockGraphError::ModuleSource(std::sync::Arc::new(error)),
1128 })?;
1129
1130 let module = self
1131 .module(name)
1132 .ok_or_else(|| ModuleCertificationError::UnknownModule(name.to_string()))?;
1133 if !module.instances.is_empty() {
1134 return Err(ModuleCertificationError::NotLeaf(name.to_string()));
1135 }
1136
1137 certify_leaf_body(
1138 name,
1139 module.local_body(),
1140 &module.interface.quantum_ports,
1141 limits,
1142 )
1143 }
1144}
1145
1146pub(crate) fn certify_leaf_body(
1147 name: &str,
1148 source: &BlockGraph,
1149 ports: &[QuantumPort],
1150 limits: ModuleCertificationLimits,
1151) -> Result<LeafModuleCertificate, ModuleCertificationError> {
1152 let graph_error = |source| ModuleCertificationError::Graph {
1153 module: name.to_string(),
1154 source,
1155 };
1156 let body = source.fix_shadowed_faces();
1157 let zx = ZXGraph::from_block_graph_for_analysis(&body)
1158 .map_err(|source| graph_error(source.into()))?;
1159 if zx.total_ids() > limits.max_local_columns {
1160 return Err(resource_limit(
1161 name,
1162 "local ZX columns",
1163 zx.total_ids(),
1164 limits.max_local_columns,
1165 ));
1166 }
1167
1168 let columns = ports
1169 .iter()
1170 .map(|port| {
1171 zx.node_at(port.position)
1172 .expect("validated module port is a ZX node")
1173 .id
1174 })
1175 .collect::<Vec<_>>();
1176 let table = zx
1177 .projected_external_table(
1178 &columns,
1179 ProjectionLimits {
1180 max_frontier_width: limits.max_frontier_width,
1181 max_witness_nodes: limits.max_witness_nodes,
1182 },
1183 )
1184 .map_err(|error| projection_limit(name, error))?;
1185 let external_basis = table.flow_witness_basis().to_external_basis();
1186 let stabilizers = zx
1187 .stabilizers_from_external_basis_with_limits(&external_basis, limits)
1188 .map_err(|source| match source {
1189 StabilizerError::ResourceLimited {
1190 phase,
1191 observed,
1192 limit,
1193 } => resource_limit(name, phase, observed, limit),
1194 source => graph_error(source.into()),
1195 })?;
1196 body.with_analyzed_action_graph(&stabilizers)
1197 .map_err(graph_error)?;
1198 stabilizers
1199 .validate_measurements_close_before_outputs_with_limits(limits)
1200 .map_err(|source| match source {
1201 RuntimeBasisError::Stabilizer(StabilizerError::ResourceLimited {
1202 phase,
1203 observed,
1204 limit,
1205 }) => resource_limit(name, phase, observed, limit),
1206 source => ModuleCertificationError::Runtime {
1207 module: name.to_string(),
1208 source,
1209 },
1210 })?;
1211 Ok(LeafModuleCertificate {
1212 module: name.to_string(),
1213 table,
1214 stabilizers,
1215 })
1216}
1217
1218impl BlockGraph {
1219 pub fn to_blog_text(&self) -> String {
1221 if !self.has_module_structure() {
1222 return self
1223 .to_program_blog_text()
1224 .unwrap_or_else(|_| self.to_blog_body_text());
1225 }
1226 let mut out = String::from("BLOG 1.0\n\n");
1227 for module in self.modules() {
1228 write_definition(&mut out, module);
1229 }
1230 out
1231 }
1232
1233 #[doc(hidden)]
1238 pub fn module_definition_cache_keys(&self) -> HashMap<String, String> {
1239 fn collect<'a>(program: &'a BlockGraph, name: &'a str, names: &mut BTreeSet<&'a str>) {
1240 let mut pending = vec![name];
1241 while let Some(name) = pending.pop() {
1242 if names.insert(name) {
1243 let module = program
1244 .module(name)
1245 .expect("validated instance names a definition");
1246 pending.extend(
1247 module
1248 .instances
1249 .iter()
1250 .map(|instance| instance.definition.as_str()),
1251 );
1252 }
1253 }
1254 }
1255
1256 let mut reachable = BTreeSet::new();
1257 collect(self, Self::ENTRY_MODULE, &mut reachable);
1258 reachable
1259 .into_iter()
1260 .map(|name| {
1261 let mut definitions = BTreeSet::new();
1262 collect(self, name, &mut definitions);
1263 let mut key = format!("root {name}\n");
1264 for definition in definitions {
1265 write_definition(
1266 &mut key,
1267 self.module(definition)
1268 .expect("reachable definition exists"),
1269 );
1270 }
1271 (name.to_string(), key)
1272 })
1273 .collect()
1274 }
1275
1276 #[doc(hidden)]
1278 pub fn definition_orientations(&self) -> HashMap<String, Vec<ModuleOrientation>> {
1279 let mut seen = HashSet::new();
1280 let mut orientations = HashMap::<String, Vec<ModuleOrientation>>::new();
1281 let mut pending = vec![(Self::ENTRY_MODULE, ModuleOrientation::IDENTITY)];
1282 while let Some((name, orientation)) = pending.pop() {
1283 if seen.insert((name, orientation)) {
1284 orientations
1285 .entry(name.to_string())
1286 .or_default()
1287 .push(orientation);
1288 let module = self
1289 .module(name)
1290 .expect("validated instance names a definition");
1291 pending.extend(module.instances.iter().rev().map(|instance| {
1292 (
1293 instance.definition.as_str(),
1294 orientation.then(instance.rotation),
1295 )
1296 }));
1297 }
1298 }
1299 for values in orientations.values_mut() {
1300 values.sort_unstable_by_key(|orientation| orientation.sort_key());
1301 }
1302 orientations
1303 }
1304
1305 #[doc(hidden)]
1309 pub fn module_public_seam_faces(&self) -> HashMap<(String, IVec3), Vec<Direction>> {
1310 let mut faces = HashMap::<(String, IVec3), Vec<Direction>>::new();
1311 for module in self.modules() {
1312 let body = module.copy_local_geometry().fix_shadowed_faces();
1313 for port in &module.interface.quantum_ports {
1314 let neighbors = body.neighbor_positions(port.position);
1315 let [neighbor] = neighbors.as_slice() else {
1316 continue;
1319 };
1320 let block = body
1321 .get_endpoint_block(*neighbor)
1322 .expect("validated module Port neighbour belongs to a block");
1323 let direction = Direction::try_from(port.position - *neighbor)
1324 .expect("validated module Port pipe is axial and unit length");
1325 faces
1326 .entry((module.name.clone(), block.pos()))
1327 .or_default()
1328 .push(direction);
1329 }
1330 }
1331 for directions in faces.values_mut() {
1332 directions.sort_unstable_by_key(|direction| direction.index());
1333 directions.dedup();
1334 }
1335 faces
1336 }
1337}
1338
1339fn write_definition(out: &mut String, module: &BlockGraph) {
1340 let ids = module.local_body().blog_block_ids().into_iter().collect();
1341 writeln!(out, "module {} {{", module.name).expect("writing to a String cannot fail");
1342 write_interface(out, module, &ids);
1343 write_instances(out, module);
1344 write_body(out, module.local_body());
1345 write_connections(out, module, &ids);
1346 out.push_str("}\n\n");
1347}
1348
1349#[doc(hidden)]
1353#[derive(Debug, Clone)]
1354pub struct LinkedModuleDefinition {
1355 pub graph: BlockGraph,
1356 pub action_scopes: Vec<String>,
1358 pub bit_outputs: HashMap<String, Expr>,
1359 pub sites: HashMap<IVec3, MaterializedModuleSite>,
1360 pub quantum_ports: Vec<QuantumPort>,
1361}
1362
1363#[doc(hidden)]
1366pub fn flatten_module_definition(
1367 program: &BlockGraph,
1368 module: &BlockGraph,
1369 prefix: &str,
1370) -> Result<LinkedModuleDefinition, crate::BlockGraphError> {
1371 struct Frame<'a> {
1372 module: &'a BlockGraph,
1373 prefix: String,
1374 children: Vec<LinkedModuleDefinition>,
1375 }
1376 let definitions = program
1377 .modules()
1378 .map(|module| (module.name.as_str(), module))
1379 .collect::<HashMap<_, _>>();
1380 let mut pending = vec![Frame {
1381 module,
1382 prefix: prefix.to_string(),
1383 children: Vec::new(),
1384 }];
1385 loop {
1386 let frame = pending.last_mut().expect("root remains until returned");
1387 if let Some(instance) = frame.module.instances.get(frame.children.len()) {
1388 let prefix = qualified_name(&frame.prefix, &instance.name);
1389 pending.push(Frame {
1390 module: definitions[instance.definition.as_str()],
1391 prefix,
1392 children: Vec::new(),
1393 });
1394 continue;
1395 }
1396 let frame = pending.pop().expect("finished frame exists");
1397 let mut children = frame.children.into_iter();
1398 let linked = link_module_definition_with(frame.module, &frame.prefix, |_, _| {
1399 Ok(children
1400 .next()
1401 .expect("each child was linked in instance order"))
1402 })?;
1403 match pending.last_mut() {
1404 Some(parent) => parent.children.push(linked),
1405 None => return Ok(linked),
1406 }
1407 }
1408}
1409
1410#[derive(Default)]
1413struct LinkBranchPipeIndex {
1414 by_shown_block: HashMap<IVec3, Vec<usize>>,
1415 by_cut: HashMap<(IVec3, IVec3), Vec<usize>>,
1416}
1417
1418impl LinkBranchPipeIndex {
1419 fn add_region(&mut self, index: usize, region: &crate::BranchRegion) {
1420 for block in region.shown_arm().blocks() {
1421 self.by_shown_block
1422 .entry(block.pos())
1423 .or_default()
1424 .push(index);
1425 }
1426 for cut in region
1430 .arm_incoming(false)
1431 .iter()
1432 .chain(region.arm_incoming(true))
1433 {
1434 self.by_cut
1435 .entry(crate::branch::pipe_key(&cut.pipe))
1436 .or_default()
1437 .push(index);
1438 }
1439 }
1440
1441 fn candidates(&self, graph: &BlockGraph, pipe: &Pipe) -> Option<Vec<usize>> {
1442 let (src, dst) = pipe.try_endpoints().ok()?;
1443 let src_owner = graph.get_endpoint_block(src)?.pos();
1444 let dst_owner = graph.get_endpoint_block(dst)?.pos();
1445 let mut candidates = Vec::new();
1446 for owner in [src_owner, dst_owner] {
1447 if let Some(regions) = self.by_shown_block.get(&owner) {
1448 candidates.extend(regions);
1449 }
1450 }
1451 if let Some(regions) = self.by_cut.get(&crate::branch::pipe_key(pipe)) {
1452 candidates.extend(regions);
1453 }
1454 candidates.sort_unstable();
1455 candidates.dedup();
1456 Some(candidates)
1457 }
1458}
1459
1460fn add_link_pipe(
1461 graph: &mut BlockGraph,
1462 index: &LinkBranchPipeIndex,
1463 pipe: Pipe,
1464) -> Result<(), crate::BlockGraphError> {
1465 if let Some(candidates) = index.candidates(graph, &pipe) {
1466 graph.try_add_pipe_with_branch_candidates(pipe, Some(&candidates))
1467 } else {
1468 graph.try_add_pipe(pipe)
1470 }
1471}
1472
1473fn link_module_definition_with(
1476 module: &BlockGraph,
1477 prefix: &str,
1478 mut link_child: impl FnMut(
1479 &ModuleInstance,
1480 &str,
1481 ) -> Result<LinkedModuleDefinition, crate::BlockGraphError>,
1482) -> Result<LinkedModuleDefinition, crate::BlockGraphError> {
1483 let mut graph = module.copy_local_geometry();
1484 let mut sites = module_sites(module, prefix);
1485 for branch in &mut graph.branches {
1486 branch.name = qualified_name(prefix, &branch.name);
1487 }
1488 let mut branch_pipe_index = LinkBranchPipeIndex::default();
1489 let mut branch_occupancy = LinkBranchOccupancy::default();
1490 for (index, branch) in graph.branches.iter().enumerate() {
1491 branch_pipe_index.add_region(index, branch);
1492 branch_occupancy.add_region(branch)?;
1493 }
1494 let local_actions = graph.actions();
1495 graph.clear_actions();
1496 let mut exposed = HashMap::<InstancePort, IVec3>::new();
1497 let mut child_actions = Vec::new();
1498 let mut child_outputs = HashMap::<(String, String), Expr>::new();
1499 let mut connections_by_port = HashMap::<&InstancePort, &QuantumConnection>::new();
1500 for connection in &module.quantum_connections {
1501 match connection {
1502 QuantumConnection::Input { input, .. } => {
1503 connections_by_port.entry(input).or_insert(connection);
1504 }
1505 QuantumConnection::Output { output, .. } => {
1506 connections_by_port.entry(output).or_insert(connection);
1507 }
1508 QuantumConnection::Pipe { output, input, .. } => {
1509 connections_by_port.entry(output).or_insert(connection);
1510 connections_by_port.entry(input).or_insert(connection);
1511 }
1512 }
1513 }
1514
1515 for instance in &module.instances {
1516 let child_prefix = qualified_name(prefix, &instance.name);
1517 let flattened = link_child(instance, &child_prefix)?;
1518 let mut quantum_ports = flattened.quantum_ports.clone();
1519 for port in &mut quantum_ports {
1520 port.position = instance.try_transform_position(port.position)?;
1521 }
1522 for (name, expr) in flattened.bit_outputs {
1523 child_outputs.insert((instance.name.clone(), name), expr);
1524 }
1525 let orientation = instance.orientation();
1526 let mut child = flattened
1527 .graph
1528 .with_orientation_lenient(orientation)?
1529 .shift_positions(instance.translation)?;
1530 let mut child_sites = flattened
1531 .sites
1532 .into_iter()
1533 .map(|(position, mut site)| {
1534 site.orientation = orientation.then_orientation(site.orientation);
1535 instance
1536 .try_transform_position(position)
1537 .map(|position| (position, site))
1538 })
1539 .collect::<Result<HashMap<_, _>, _>>()?;
1540 let mut actions = child.actions();
1541 child.clear_actions();
1542 let mut bound_pipes = Vec::new();
1543
1544 let child_ports = quantum_ports
1545 .iter()
1546 .map(|port| {
1547 let endpoint = InstancePort {
1548 instance: instance.name.clone(),
1549 port: port.name.clone(),
1550 };
1551 let position = port.position;
1552 let neighbors = child.neighbor_positions(position);
1553 let [neighbor] = neighbors.as_slice() else {
1554 unreachable!("validated module port has one incident pipe")
1555 };
1556 let pipe = child
1557 .get_pipe(position, *neighbor)
1558 .expect("module port neighbor has a pipe")
1559 .clone();
1560 (endpoint, position, *neighbor, pipe)
1561 })
1562 .collect::<Vec<_>>();
1563
1564 let child_branches = std::mem::take(&mut child.branches);
1567 for (endpoint, position, neighbor, pipe) in child_ports {
1568 for action in &mut actions {
1572 if let Action::Feedback { targets, .. } = action {
1573 for target in targets
1574 .iter_mut()
1575 .filter(|target| target.target == position)
1576 {
1577 target.target = neighbor;
1578 target.direction = Some(if pipe.src == neighbor {
1579 pipe.dir()
1580 } else {
1581 pipe.dir().negate()
1582 });
1583 if pipe.hadamard {
1584 target.pauli = match target.pauli {
1585 crate::PauliBasis::X => crate::PauliBasis::Z,
1586 crate::PauliBasis::Z => crate::PauliBasis::X,
1587 crate::PauliBasis::Y => crate::PauliBasis::Y,
1588 };
1589 }
1590 }
1591 }
1592 }
1593 let connection = connections_by_port
1594 .get(&endpoint)
1595 .copied()
1596 .expect("validated child port is connected");
1597 child.remove_block(position);
1598 child_sites.remove(&position);
1599 match connection {
1600 QuantumConnection::Pipe { .. } => {
1601 exposed.insert(endpoint, neighbor);
1602 }
1603 QuantumConnection::Input { hadamard, .. }
1604 | QuantumConnection::Output { hadamard, .. } => {
1605 let mut pipe = pipe;
1607 pipe.hadamard ^= *hadamard;
1608 bound_pipes.push(pipe);
1609 }
1610 }
1611 }
1612 child_actions.push((instance.name.clone(), actions, flattened.action_scopes));
1613 child.branches = child_branches;
1614
1615 for block in child.blocks() {
1616 graph.try_add_block_with_branch_occupancy(block.clone(), Some(&branch_occupancy))?;
1617 }
1618 for pipe in child.pipes() {
1619 add_link_pipe(&mut graph, &branch_pipe_index, pipe.clone())?;
1620 }
1621 sites.extend(child_sites);
1622 let first_branch = graph.branches.len();
1623 graph.branches.extend(child.branches.iter().cloned());
1624 for (index, branch) in graph.branches.iter().enumerate().skip(first_branch) {
1625 branch_pipe_index.add_region(index, branch);
1626 branch_occupancy.add_region(branch)?;
1627 }
1628 for pipe in bound_pipes {
1629 add_link_pipe(&mut graph, &branch_pipe_index, pipe)?;
1630 }
1631 }
1632
1633 for connection in &module.quantum_connections {
1634 let QuantumConnection::Pipe {
1635 output,
1636 input,
1637 hadamard,
1638 } = connection
1639 else {
1640 continue;
1641 };
1642 let output = exposed[output];
1643 let input = exposed[input];
1644 let direction = Direction::iter()
1645 .find(|direction| {
1646 checked_add_position(output, direction.to_ivec3()).ok() == Some(input)
1647 })
1648 .expect("validated module seam endpoints are adjacent");
1649 let pipe = Pipe::new(output, direction);
1650 add_link_pipe(
1651 &mut graph,
1652 &branch_pipe_index,
1653 if *hadamard {
1654 pipe.with_hadamard()
1655 } else {
1656 pipe
1657 },
1658 )?;
1659 }
1660
1661 let bindings = module
1662 .bit_bindings
1663 .iter()
1664 .map(|binding| {
1665 let child_prefix = qualified_name(prefix, &binding.target_instance);
1666 (
1667 binding.target_instance.clone(),
1668 Action::Let {
1669 name: qualified_name(&child_prefix, &binding.target_bit),
1670 expr: resolve_bit_source(&binding.source, module, prefix, &child_outputs),
1671 },
1672 )
1673 })
1674 .collect::<Vec<_>>();
1675 let (actions, action_scopes) = order_linked_actions(
1676 bindings,
1677 child_actions,
1678 rename_module_actions(local_actions, prefix, &child_outputs),
1679 prefix,
1680 )?;
1681 graph.set_actions_deferred_with_inputs(
1682 actions,
1683 module
1684 .interface
1685 .bit_inputs
1686 .iter()
1687 .map(|name| qualified_name(prefix, name)),
1688 )?;
1689 let bit_outputs = module
1690 .interface
1691 .bit_outputs
1692 .iter()
1693 .map(|output| {
1694 (
1695 output.name.clone(),
1696 resolve_module_expr(output.expr.clone(), prefix, &child_outputs),
1697 )
1698 })
1699 .collect();
1700 Ok(LinkedModuleDefinition {
1701 graph,
1702 action_scopes,
1703 bit_outputs,
1704 sites,
1705 quantum_ports: module.interface.quantum_ports.clone(),
1706 })
1707}
1708
1709fn module_sites(module: &BlockGraph, prefix: &str) -> HashMap<IVec3, MaterializedModuleSite> {
1710 let positions = module
1711 .local_body()
1712 .blocks()
1713 .map(Block::pos)
1714 .chain(
1715 module
1716 .local_body()
1717 .branch_definitions()
1718 .iter()
1719 .flat_map(|region| {
1720 region
1721 .on_false()
1722 .blocks()
1723 .chain(region.on_true().blocks())
1724 .map(Block::pos)
1725 }),
1726 )
1727 .collect::<HashSet<_>>();
1728 positions
1729 .into_iter()
1730 .map(|local_position| {
1731 (
1732 local_position,
1733 MaterializedModuleSite {
1734 definition: module.name.clone(),
1735 instance_path: prefix.to_string(),
1736 local_position,
1737 orientation: ModuleOrientation::IDENTITY,
1738 },
1739 )
1740 })
1741 .collect()
1742}
1743
1744fn rename_module_actions(
1745 actions: Vec<Action>,
1746 prefix: &str,
1747 child_outputs: &HashMap<(String, String), Expr>,
1748) -> Vec<Action> {
1749 let rename = |name: String| qualified_name(prefix, &name);
1750 actions
1751 .into_iter()
1752 .map(|action| match action {
1753 Action::Let { name, expr } => Action::Let {
1754 name: rename(name),
1755 expr: resolve_module_expr(expr, prefix, child_outputs),
1756 },
1757 Action::Measure { target, name } => Action::Measure {
1758 target,
1759 name: rename(name),
1760 },
1761 Action::DiscardIf(expr) => {
1762 Action::DiscardIf(resolve_module_expr(expr, prefix, child_outputs))
1763 }
1764 Action::Resolve { target, condition } => Action::Resolve {
1765 target,
1766 condition: resolve_module_expr(condition, prefix, child_outputs),
1767 },
1768 Action::Branch { target, condition } => Action::Branch {
1769 target,
1770 condition: resolve_module_expr(condition, prefix, child_outputs),
1771 },
1772 Action::Feedback { targets, condition } => Action::Feedback {
1773 targets,
1774 condition: condition.map(|expr| resolve_module_expr(expr, prefix, child_outputs)),
1775 },
1776 })
1777 .collect()
1778}
1779
1780fn order_linked_actions(
1781 bindings: Vec<(String, Action)>,
1782 child_actions: Vec<(String, Vec<Action>, Vec<String>)>,
1783 local_actions: Vec<Action>,
1784 prefix: &str,
1785) -> Result<(Vec<Action>, Vec<String>), crate::BlockGraphError> {
1786 let mut bindings_by_target = HashMap::<String, Vec<Action>>::new();
1787 for (target, binding) in bindings {
1788 bindings_by_target.entry(target).or_default().push(binding);
1789 }
1790
1791 let mut actions = Vec::new();
1792 let mut action_scopes = Vec::new();
1793 let mut ordered_ranges = Vec::new();
1794 for (instance, child, child_scopes) in child_actions {
1795 let bindings = bindings_by_target.remove(&instance).unwrap_or_default();
1796 action_scopes.extend(std::iter::repeat_n(
1797 qualified_name(prefix, &instance),
1798 bindings.len(),
1799 ));
1800 actions.extend(bindings);
1801 let start = actions.len();
1802 debug_assert_eq!(child.len(), child_scopes.len());
1803 actions.extend(child);
1804 action_scopes.extend(child_scopes);
1805 ordered_ranges.push(start..actions.len());
1806 }
1807 debug_assert!(bindings_by_target.is_empty());
1808 let start = actions.len();
1809 action_scopes.extend(std::iter::repeat_n(prefix.to_string(), local_actions.len()));
1810 actions.extend(local_actions);
1811 ordered_ranges.push(start..actions.len());
1812
1813 let mut successors = vec![BTreeSet::new(); actions.len()];
1814 for (predecessor, consumer, _) in crate::ActionDag::from_actions(&actions).dependencies() {
1815 successors[predecessor].insert(consumer);
1816 }
1817 for range in ordered_ranges {
1818 for (predecessor, consumers) in successors
1819 .iter_mut()
1820 .enumerate()
1821 .take(range.end.saturating_sub(1))
1822 .skip(range.start)
1823 {
1824 consumers.insert(predecessor + 1);
1825 }
1826 }
1827 let mut incoming = vec![0usize; actions.len()];
1828 for consumer in successors.iter().flatten() {
1829 incoming[*consumer] += 1;
1830 }
1831 let mut ready = incoming
1832 .iter()
1833 .enumerate()
1834 .filter_map(|(index, &count)| (count == 0).then_some(index))
1835 .collect::<BTreeSet<_>>();
1836 let mut order = Vec::with_capacity(actions.len());
1837 while let Some(index) = ready.pop_first() {
1838 order.push(index);
1839 for &consumer in &successors[index] {
1840 incoming[consumer] -= 1;
1841 if incoming[consumer] == 0 {
1842 ready.insert(consumer);
1843 }
1844 }
1845 }
1846 if order.len() != actions.len() {
1847 let ordinal = incoming
1848 .iter()
1849 .position(|&count| count != 0)
1850 .expect("unsorted actions retain an incoming dependency");
1851 return Err(crate::InvalidActionError::DependencyCycle { ordinal }.into());
1852 }
1853 Ok(order
1854 .into_iter()
1855 .map(|index| (actions[index].clone(), action_scopes[index].clone()))
1856 .unzip())
1857}
1858
1859fn resolve_module_expr(
1860 expr: Expr,
1861 prefix: &str,
1862 child_outputs: &HashMap<(String, String), Expr>,
1863) -> Expr {
1864 match expr {
1865 Expr::Var(name) => split_member(&name)
1866 .and_then(|(instance, bit)| {
1867 child_outputs
1868 .get(&(instance.to_string(), bit.to_string()))
1869 .cloned()
1870 })
1871 .unwrap_or_else(|| Expr::Var(qualified_name(prefix, &name))),
1872 Expr::Not(expr) => Expr::Not(Box::new(resolve_module_expr(*expr, prefix, child_outputs))),
1873 Expr::Binary(op, lhs, rhs) => Expr::Binary(
1874 op,
1875 Box::new(resolve_module_expr(*lhs, prefix, child_outputs)),
1876 Box::new(resolve_module_expr(*rhs, prefix, child_outputs)),
1877 ),
1878 }
1879}
1880
1881fn resolve_bit_source(
1882 source: &BitRef,
1883 module: &BlockGraph,
1884 prefix: &str,
1885 child_outputs: &HashMap<(String, String), Expr>,
1886) -> Expr {
1887 if let Some(instance) = &source.instance {
1888 return child_outputs[&(instance.clone(), source.bit.clone())].clone();
1889 }
1890 if module.interface.bit_inputs.contains(&source.bit) {
1891 return Expr::Var(qualified_name(prefix, &source.bit));
1892 }
1893 let output = module
1894 .interface
1895 .bit_outputs
1896 .iter()
1897 .find(|output| output.name == source.bit)
1898 .expect("validated bit source names a local export");
1899 resolve_module_expr(output.expr.clone(), prefix, child_outputs)
1900}
1901
1902pub fn qualified_name(prefix: &str, name: &str) -> String {
1905 if prefix.is_empty() {
1906 name.to_string()
1907 } else {
1908 format!("{prefix}__{name}")
1909 }
1910}
1911
1912pub(crate) fn resource_limit(
1913 module: &str,
1914 phase: &'static str,
1915 observed: usize,
1916 limit: usize,
1917) -> ModuleCertificationError {
1918 ModuleCertificationError::ResourceLimited {
1919 module: module.to_string(),
1920 phase,
1921 observed,
1922 limit,
1923 }
1924}
1925
1926pub(crate) fn projection_limit(module: &str, error: ProjectionError) -> ModuleCertificationError {
1927 match error {
1928 ProjectionError::FrontierLimit { observed, limit } => {
1929 resource_limit(module, "frontier columns", observed, limit)
1930 }
1931 ProjectionError::WitnessLimit { observed, limit } => {
1932 resource_limit(module, "witness nodes", observed, limit)
1933 }
1934 }
1935}
1936
1937fn validate_module(
1938 module: &BlockGraph,
1939 definitions: &HashMap<&str, &BlockGraph>,
1940) -> Result<(), ModuleError> {
1941 let fail = |message| ModuleError::InvalidModule {
1942 module: module.name.clone(),
1943 message,
1944 span: None,
1945 };
1946 if !crate::parser::is_valid_simple_identifier(&module.name) {
1947 return Err(fail("invalid module name".to_string()));
1948 }
1949 if let Some(branch) = module
1950 .local_body()
1951 .branch_definitions()
1952 .iter()
1953 .find(|branch| branch.name.contains("__"))
1954 {
1955 return Err(fail(format!("invalid branch name '{}'", branch.name)));
1956 }
1957
1958 let mut names = HashSet::new();
1959 let mut port_positions = HashSet::new();
1960 for port in &module.interface.quantum_ports {
1961 if !crate::parser::is_valid_simple_identifier(&port.name) {
1962 return Err(fail(format!("invalid port name '{}'", port.name)));
1963 }
1964 if !names.insert(port.name.as_str()) {
1965 return Err(fail(format!("duplicate interface name '{}'", port.name)));
1966 }
1967 if !crate::parser::is_valid_resource_type(&port.resource_type) {
1968 return Err(fail(format!(
1969 "invalid resource type '{}'",
1970 port.resource_type
1971 )));
1972 }
1973 if !port_positions.insert(port.position) {
1974 return Err(fail(format!(
1975 "multiple interface ports use {}",
1976 port.position
1977 )));
1978 }
1979 let actual = port_direction(module.local_body(), port.position)
1980 .map_err(|message| fail(format!("port '{}': {message}", port.name)))?;
1981 if actual != port.direction {
1982 return Err(fail(format!(
1983 "port '{}' is {:?}, declared {:?}",
1984 port.name, actual, port.direction
1985 )));
1986 }
1987 }
1988 for name in &module.interface.bit_inputs {
1989 if !crate::parser::is_valid_simple_identifier(name) || name.contains("__") {
1990 return Err(fail(format!("invalid in name '{name}'")));
1991 }
1992 if !names.insert(name) {
1993 return Err(fail(format!("duplicate interface name '{name}'")));
1994 }
1995 }
1996 for output in &module.interface.bit_outputs {
1997 if !crate::parser::is_valid_simple_identifier(&output.name) || output.name.contains("__") {
1998 return Err(fail(format!("invalid bit output name '{}'", output.name)));
1999 }
2000 if !names.insert(&output.name) {
2001 return Err(fail(format!("duplicate interface name '{}'", output.name)));
2002 }
2003 }
2004 for block in module
2005 .local_body()
2006 .blocks()
2007 .filter(|block| block.kind().is_port())
2008 {
2009 if !port_positions.contains(&block.pos()) {
2010 return Err(fail(format!("undeclared Port at {}", block.pos())));
2011 }
2012 }
2013
2014 let mut instances = HashMap::new();
2015 for instance in &module.instances {
2016 if !crate::parser::is_valid_simple_identifier(&instance.name)
2017 || instance.name.contains("__")
2018 {
2019 return Err(fail(format!("invalid instance name '{}'", instance.name)));
2020 }
2021 if instances.insert(instance.name.as_str(), instance).is_some() {
2022 return Err(fail(format!("duplicate instance '{}'", instance.name)));
2023 }
2024 if !definitions.contains_key(instance.definition.as_str()) {
2025 return Err(fail(format!(
2026 "instance '{}' uses unknown module '{}'",
2027 instance.name, instance.definition
2028 )));
2029 }
2030 }
2031
2032 validate_quantum_connections(module, definitions, &instances, &fail)?;
2033 validate_bits(module, definitions, &instances, &fail)
2034}
2035
2036fn validate_quantum_connections(
2037 module: &BlockGraph,
2038 definitions: &HashMap<&str, &BlockGraph>,
2039 instances: &HashMap<&str, &ModuleInstance>,
2040 fail: &impl Fn(String) -> ModuleError,
2041) -> Result<(), ModuleError> {
2042 let mut ports = HashMap::new();
2045 let mut indexed = HashSet::new();
2046 for instance in instances.values() {
2047 let definition = instance.definition.as_str();
2048 if indexed.insert(definition) {
2049 for port in &definitions[definition].interface.quantum_ports {
2050 ports
2051 .entry((definition, port.name.as_str(), port.direction))
2052 .or_insert(port);
2053 }
2054 }
2055 }
2056 let mut used = HashSet::<(&str, &str)>::new();
2057 let mut block_types = HashMap::<IVec3, &str>::new();
2058 let common_blocks = module
2059 .local_body()
2060 .blog_block_ids()
2061 .into_iter()
2062 .map(|(position, _)| position)
2063 .collect::<HashSet<_>>();
2064 for connection in &module.quantum_connections {
2065 let bind = match connection {
2066 QuantumConnection::Input { block, input, .. } => {
2067 Some((*block, input, PortDirection::Input))
2068 }
2069 QuantumConnection::Output { output, block, .. } => {
2070 Some((*block, output, PortDirection::Output))
2071 }
2072 QuantumConnection::Pipe { output, input, .. } => {
2073 let output_port =
2074 child_port(output, PortDirection::Output, &ports, instances, fail)?;
2075 let input_port = child_port(input, PortDirection::Input, &ports, instances, fail)?;
2076 if output_port.resource_type != input_port.resource_type {
2077 return Err(fail(format!(
2078 "pipe joins resource types '{}' and '{}'",
2079 output_port.resource_type, input_port.resource_type
2080 )));
2081 }
2082 use_port(output, &mut used, fail)?;
2083 use_port(input, &mut used, fail)?;
2084 None
2085 }
2086 };
2087 let Some((block, endpoint, direction)) = bind else {
2088 continue;
2089 };
2090 let port = child_port(endpoint, direction, &ports, instances, fail)?;
2091 validate_bind(
2092 module,
2093 block,
2094 endpoint,
2095 port,
2096 instances,
2097 &common_blocks,
2098 fail,
2099 )?;
2100 use_port(endpoint, &mut used, fail)?;
2101 if let Some(resource_type) = block_types.insert(block, &port.resource_type)
2102 && resource_type != port.resource_type
2103 {
2104 return Err(fail(format!(
2105 "block {block} joins resource types '{resource_type}' and '{}'",
2106 port.resource_type
2107 )));
2108 }
2109 }
2110
2111 for instance in &module.instances {
2112 let child = definitions[instance.definition.as_str()];
2113 for port in &child.interface.quantum_ports {
2114 if !used.contains(&(instance.name.as_str(), port.name.as_str())) {
2115 return Err(fail(format!(
2116 "unconnected port '{}.{}'",
2117 instance.name, port.name
2118 )));
2119 }
2120 }
2121 }
2122 Ok(())
2123}
2124
2125fn child_port<'a>(
2126 endpoint: &InstancePort,
2127 direction: PortDirection,
2128 ports: &HashMap<(&str, &str, PortDirection), &'a QuantumPort>,
2129 instances: &HashMap<&str, &ModuleInstance>,
2130 fail: &impl Fn(String) -> ModuleError,
2131) -> Result<&'a QuantumPort, ModuleError> {
2132 let instance = instances
2133 .get(endpoint.instance.as_str())
2134 .ok_or_else(|| fail(format!("unknown instance '{}'", endpoint.instance)))?;
2135 ports
2136 .get(&(
2137 instance.definition.as_str(),
2138 endpoint.port.as_str(),
2139 direction,
2140 ))
2141 .copied()
2142 .ok_or_else(|| {
2143 fail(format!(
2144 "unknown {:?} port '{}.{}'",
2145 direction, endpoint.instance, endpoint.port
2146 ))
2147 })
2148}
2149
2150fn validate_bind(
2151 module: &BlockGraph,
2152 block: IVec3,
2153 endpoint: &InstancePort,
2154 port: &QuantumPort,
2155 instances: &HashMap<&str, &ModuleInstance>,
2156 common_blocks: &HashSet<IVec3>,
2157 fail: &impl Fn(String) -> ModuleError,
2158) -> Result<(), ModuleError> {
2159 if !common_blocks.contains(&block) {
2160 return Err(fail(format!(
2161 "bind references missing or branch-local block {block}"
2162 )));
2163 }
2164 let instance = instances[endpoint.instance.as_str()];
2165 let translated = instance
2166 .try_transform_position(port.position)
2167 .map_err(|error| fail(error.to_string()))?;
2168 if translated != block {
2169 return Err(fail(format!(
2170 "port '{}.{}' is at {translated}, not {block}",
2171 endpoint.instance, endpoint.port
2172 )));
2173 }
2174 if let Some(parent_port) = module
2175 .interface
2176 .quantum_ports
2177 .iter()
2178 .find(|candidate| candidate.position == block)
2179 && (parent_port.direction != port.direction
2180 || parent_port.resource_type != port.resource_type)
2181 {
2182 return Err(fail(format!(
2183 "bound parent port '{}' has a different interface",
2184 parent_port.name
2185 )));
2186 }
2187 Ok(())
2188}
2189
2190fn use_port<'a>(
2191 port: &'a InstancePort,
2192 used: &mut HashSet<(&'a str, &'a str)>,
2193 fail: &impl Fn(String) -> ModuleError,
2194) -> Result<(), ModuleError> {
2195 if used.insert((port.instance.as_str(), port.port.as_str())) {
2196 Ok(())
2197 } else {
2198 Err(fail(format!(
2199 "port '{}.{}' is connected more than once",
2200 port.instance, port.port
2201 )))
2202 }
2203}
2204
2205fn validate_bits(
2206 module: &BlockGraph,
2207 definitions: &HashMap<&str, &BlockGraph>,
2208 instances: &HashMap<&str, &ModuleInstance>,
2209 fail: &impl Fn(String) -> ModuleError,
2210) -> Result<(), ModuleError> {
2211 let local_actions = module
2212 .local_body()
2213 .action_graph()
2214 .ordered_nodes()
2215 .filter_map(|node| match &node.action {
2216 Action::Measure { name, .. } | Action::Let { name, .. } => Some(name.as_str()),
2217 _ => None,
2218 })
2219 .collect::<HashSet<_>>();
2220 if let Some(name) = local_actions.iter().find(|name| {
2221 !crate::parser::is_valid_identifier(name) || name.contains('.') || name.contains("__")
2222 }) {
2223 return Err(fail(format!("invalid local action name '{name}'")));
2224 }
2225 let bit_inputs = module
2226 .interface
2227 .bit_inputs
2228 .iter()
2229 .map(String::as_str)
2230 .collect::<HashSet<_>>();
2231 if let Some(name) = bit_inputs.intersection(&local_actions).next() {
2232 return Err(fail(format!("bit input '{name}' is also a local action")));
2233 }
2234 let exported_bits = bit_inputs
2235 .iter()
2236 .copied()
2237 .chain(
2238 module
2239 .interface
2240 .bit_outputs
2241 .iter()
2242 .map(|output| output.name.as_str()),
2243 )
2244 .collect::<HashSet<_>>();
2245
2246 for output in &module.interface.bit_outputs {
2247 for name in expr_names(&output.expr) {
2248 validate_bit_ref(
2249 &name,
2250 &bit_inputs,
2251 &local_actions,
2252 definitions,
2253 instances,
2254 fail,
2255 )?;
2256 }
2257 }
2258 for name in module.local_body().action_graph().inputs() {
2259 validate_bit_ref(
2260 name,
2261 &bit_inputs,
2262 &local_actions,
2263 definitions,
2264 instances,
2265 fail,
2266 )?;
2267 }
2268
2269 let mut bound = HashSet::new();
2270 for binding in &module.bit_bindings {
2271 validate_bit_source(
2272 &binding.source,
2273 &exported_bits,
2274 definitions,
2275 instances,
2276 fail,
2277 )?;
2278 let target = instances
2279 .get(binding.target_instance.as_str())
2280 .ok_or_else(|| fail(format!("unknown instance '{}'", binding.target_instance)))?;
2281 let child = definitions[target.definition.as_str()];
2282 if !child.interface.bit_inputs.contains(&binding.target_bit) {
2283 return Err(fail(format!(
2284 "unknown bit input '{}.{}'",
2285 binding.target_instance, binding.target_bit
2286 )));
2287 }
2288 if !bound.insert((
2289 binding.target_instance.as_str(),
2290 binding.target_bit.as_str(),
2291 )) {
2292 return Err(fail(format!(
2293 "bit input '{}.{}' is bound more than once",
2294 binding.target_instance, binding.target_bit
2295 )));
2296 }
2297 }
2298 for instance in &module.instances {
2299 for input in &definitions[instance.definition.as_str()]
2300 .interface
2301 .bit_inputs
2302 {
2303 if !bound.contains(&(instance.name.as_str(), input.as_str())) {
2304 return Err(fail(format!(
2305 "unbound bit input '{}.{}'",
2306 instance.name, input
2307 )));
2308 }
2309 }
2310 }
2311 Ok(())
2312}
2313
2314fn validate_bit_ref(
2315 name: &str,
2316 local_bits: &HashSet<&str>,
2317 local_actions: &HashSet<&str>,
2318 definitions: &HashMap<&str, &BlockGraph>,
2319 instances: &HashMap<&str, &ModuleInstance>,
2320 fail: &impl Fn(String) -> ModuleError,
2321) -> Result<(), ModuleError> {
2322 if let Some((instance, bit)) = split_member(name) {
2323 return validate_child_bit_output(instance, bit, definitions, instances, fail);
2324 }
2325 if local_bits.contains(name) || local_actions.contains(name) {
2326 Ok(())
2327 } else {
2328 Err(fail(format!("unknown bit '{name}'")))
2329 }
2330}
2331
2332fn validate_bit_source(
2333 source: &BitRef,
2334 local_bits: &HashSet<&str>,
2335 definitions: &HashMap<&str, &BlockGraph>,
2336 instances: &HashMap<&str, &ModuleInstance>,
2337 fail: &impl Fn(String) -> ModuleError,
2338) -> Result<(), ModuleError> {
2339 match &source.instance {
2340 Some(instance) => {
2341 validate_child_bit_output(instance, &source.bit, definitions, instances, fail)
2342 }
2343 None if local_bits.contains(source.bit.as_str()) => Ok(()),
2344 None => Err(fail(format!("unknown exported bit '{}'", source.bit))),
2345 }
2346}
2347
2348fn validate_child_bit_output(
2349 instance_name: &str,
2350 bit: &str,
2351 definitions: &HashMap<&str, &BlockGraph>,
2352 instances: &HashMap<&str, &ModuleInstance>,
2353 fail: &impl Fn(String) -> ModuleError,
2354) -> Result<(), ModuleError> {
2355 let instance = instances
2356 .get(instance_name)
2357 .ok_or_else(|| fail(format!("unknown instance '{instance_name}'")))?;
2358 if definitions[instance.definition.as_str()]
2359 .interface
2360 .bit_outputs
2361 .iter()
2362 .any(|output| output.name == bit)
2363 {
2364 Ok(())
2365 } else {
2366 Err(fail(format!("unknown bit output '{instance_name}.{bit}'")))
2367 }
2368}
2369
2370fn port_direction(graph: &BlockGraph, position: IVec3) -> Result<PortDirection, String> {
2371 let block = graph
2372 .get_block(position)
2373 .ok_or_else(|| format!("no block at {position}"))?;
2374 if block.kind() != BlockKind::Port {
2375 return Err("interface block is not a Port".to_string());
2376 }
2377 let role = block.port_role().expect("Port has a role");
2378 let neighbors = graph.neighbor_positions(position);
2379 if role == PortRole::Multiplex {
2380 return match neighbors.as_slice() {
2381 [] => Ok(PortDirection::Input),
2384 [neighbor] if neighbor.z == position.z => Ok(PortDirection::Input),
2385 _ => Err("multiplex Port must have one spatial pipe".to_string()),
2386 };
2387 }
2388 if neighbors.is_empty() {
2389 return match role {
2390 PortRole::Input => Ok(PortDirection::Input),
2391 PortRole::Output => Ok(PortDirection::Output),
2392 PortRole::Auto => Err("automatic Port must have one temporal pipe".to_string()),
2393 PortRole::Multiplex => unreachable!("multiplex ports returned above"),
2394 };
2395 }
2396 let [neighbor] = neighbors.as_slice() else {
2397 return Err("module Port must have one pipe".to_string());
2398 };
2399 if neighbor.z == position.z {
2400 return match role {
2401 PortRole::Input => Ok(PortDirection::Input),
2402 PortRole::Output => Ok(PortDirection::Output),
2403 PortRole::Auto => Err("spatial Port needs an explicit role".to_string()),
2404 PortRole::Multiplex => unreachable!("multiplex ports returned above"),
2405 };
2406 }
2407 let inferred = if neighbor.z > position.z {
2408 PortDirection::Input
2409 } else {
2410 PortDirection::Output
2411 };
2412 match role {
2413 PortRole::Auto => Ok(inferred),
2414 PortRole::Input if inferred == PortDirection::Input => Ok(inferred),
2415 PortRole::Output if inferred == PortDirection::Output => Ok(inferred),
2416 PortRole::Input | PortRole::Output => {
2417 Err("explicit role disagrees with temporal pipe".to_string())
2418 }
2419 PortRole::Multiplex => unreachable!("multiplex ports returned above"),
2420 }
2421}
2422
2423fn validate_acyclic(
2424 modules: &[&BlockGraph],
2425 definitions: &HashMap<&str, &BlockGraph>,
2426) -> Result<(), ModuleError> {
2427 let mut active = HashSet::new();
2428 let mut done = HashSet::new();
2429 for module in modules {
2430 let mut pending = vec![(module.name.as_str(), false)];
2431 while let Some((name, exiting)) = pending.pop() {
2432 if exiting {
2433 active.remove(name);
2434 done.insert(name);
2435 } else if !done.contains(name) {
2436 if !active.insert(name) {
2437 return Err(ModuleError::InvalidModule {
2438 module: name.to_string(),
2439 message: "module dependency cycle".to_string(),
2440 span: None,
2441 });
2442 }
2443 pending.push((name, true));
2444 pending.extend(
2445 definitions[name]
2446 .instances
2447 .iter()
2448 .rev()
2449 .map(|instance| (instance.definition.as_str(), false)),
2450 );
2451 }
2452 }
2453 }
2454 Ok(())
2455}
2456
2457type BitDependencies = HashMap<String, HashSet<String>>;
2458
2459fn validate_classical_cycles(
2460 modules: &[&BlockGraph],
2461 definitions: &HashMap<&str, &BlockGraph>,
2462) -> Result<(), ModuleError> {
2463 let mut memo = HashMap::new();
2464 let mut seen = HashSet::new();
2465 for module in modules {
2466 let mut pending = vec![(*module, false)];
2467 while let Some((module, exiting)) = pending.pop() {
2468 if exiting {
2469 module_bit_dependencies(&module.name, definitions, &mut memo)?;
2470 } else if seen.insert(module.name.as_str()) {
2471 pending.push((module, true));
2472 pending.extend(
2473 module
2474 .instances
2475 .iter()
2476 .rev()
2477 .map(|instance| (definitions[instance.definition.as_str()], false)),
2478 );
2479 }
2480 }
2481 }
2482 Ok(())
2483}
2484
2485fn module_bit_dependencies(
2486 name: &str,
2487 definitions: &HashMap<&str, &BlockGraph>,
2488 memo: &mut HashMap<String, BitDependencies>,
2489) -> Result<BitDependencies, ModuleError> {
2490 if let Some(dependencies) = memo.get(name) {
2491 return Ok(dependencies.clone());
2492 }
2493 let module = definitions[name];
2494 let mut child_dependencies = HashMap::new();
2495 for instance in &module.instances {
2496 child_dependencies.insert(instance.name.as_str(), memo[&instance.definition].clone());
2497 }
2498
2499 let mut dependencies = BitDependencies::new();
2500 let actions = module.local_body().action_graph();
2501 for node in actions.ordered_nodes() {
2502 let action = action_node(node.ordinal);
2503 dependencies.insert(
2504 action.clone(),
2505 action_expression(&node.action)
2506 .map(expr_names)
2507 .unwrap_or_default()
2508 .into_iter()
2509 .collect(),
2510 );
2511 if let Action::Let { name, .. } | Action::Measure { name, .. } = &node.action {
2512 dependencies.insert(name.clone(), HashSet::from([action]));
2513 }
2514 }
2515 for (predecessor, consumer, _) in actions.dependencies() {
2516 dependencies
2517 .entry(action_node(consumer))
2518 .or_default()
2519 .insert(action_node(predecessor));
2520 }
2521 for output in &module.interface.bit_outputs {
2522 dependencies.insert(
2523 output_node(&output.name),
2524 expr_names(&output.expr).into_iter().collect(),
2525 );
2526 }
2527 for instance in &module.instances {
2528 for (output, inputs) in &child_dependencies[instance.name.as_str()] {
2529 dependencies.insert(
2530 format!("{}.{}", instance.name, output),
2531 inputs
2532 .iter()
2533 .map(|input| format!("{}.{}", instance.name, input))
2534 .collect(),
2535 );
2536 }
2537 }
2538 for binding in &module.bit_bindings {
2539 let source = match &binding.source.instance {
2540 Some(instance) => format!("{instance}.{}", binding.source.bit),
2541 None if module
2542 .interface
2543 .bit_outputs
2544 .iter()
2545 .any(|output| output.name == binding.source.bit) =>
2546 {
2547 output_node(&binding.source.bit)
2548 }
2549 None => binding.source.bit.clone(),
2550 };
2551 dependencies.insert(
2552 format!("{}.{}", binding.target_instance, binding.target_bit),
2553 HashSet::from([source]),
2554 );
2555 }
2556
2557 let mut active = HashSet::new();
2558 let mut done = HashSet::new();
2559 let mut nodes = dependencies.keys().cloned().collect::<Vec<_>>();
2560 nodes.sort();
2561 for node in nodes {
2562 if let Some(cycle) = dependency_cycle(&node, &dependencies, &mut active, &mut done) {
2563 return Err(ModuleError::InvalidModule {
2564 module: module.name.clone(),
2565 message: format!(
2566 "classical binding cycle at '{}'",
2567 cycle.strip_prefix("@output:").unwrap_or(&cycle)
2568 ),
2569 span: None,
2570 });
2571 }
2572 }
2573
2574 let inputs = module
2575 .interface
2576 .bit_inputs
2577 .iter()
2578 .cloned()
2579 .collect::<HashSet<_>>();
2580 let mut output_dependencies = BitDependencies::new();
2581 for output in &module.interface.bit_outputs {
2582 let mut found = HashSet::new();
2583 collect_input_dependencies(
2584 &output_node(&output.name),
2585 &dependencies,
2586 &inputs,
2587 &mut HashSet::new(),
2588 &mut found,
2589 );
2590 output_dependencies.insert(output.name.clone(), found);
2591 }
2592 memo.insert(name.to_string(), output_dependencies.clone());
2593 Ok(output_dependencies)
2594}
2595
2596fn output_node(name: &str) -> String {
2597 format!("@output:{name}")
2598}
2599
2600fn action_node(ordinal: usize) -> String {
2601 format!("@action:{ordinal}")
2602}
2603
2604fn action_expression(action: &Action) -> Option<&Expr> {
2605 match action {
2606 Action::Let { expr, .. } | Action::DiscardIf(expr) => Some(expr),
2607 Action::Resolve { condition, .. } | Action::Branch { condition, .. } => Some(condition),
2608 Action::Feedback { condition, .. } => condition.as_ref(),
2609 Action::Measure { .. } => None,
2610 }
2611}
2612
2613fn dependency_cycle(
2614 node: &str,
2615 dependencies: &BitDependencies,
2616 active: &mut HashSet<String>,
2617 done: &mut HashSet<String>,
2618) -> Option<String> {
2619 if done.contains(node) {
2620 return None;
2621 }
2622 if !active.insert(node.to_string()) {
2623 return Some(node.to_string());
2624 }
2625 if let Some(next) = dependencies.get(node) {
2626 let mut next = next.iter().collect::<Vec<_>>();
2627 next.sort();
2628 for dependency in next {
2629 if let Some(cycle) = dependency_cycle(dependency, dependencies, active, done) {
2630 return Some(cycle);
2631 }
2632 }
2633 }
2634 active.remove(node);
2635 done.insert(node.to_string());
2636 None
2637}
2638
2639fn collect_input_dependencies(
2640 node: &str,
2641 dependencies: &BitDependencies,
2642 inputs: &HashSet<String>,
2643 seen: &mut HashSet<String>,
2644 found: &mut HashSet<String>,
2645) {
2646 if inputs.contains(node) {
2647 found.insert(node.to_string());
2648 return;
2649 }
2650 if !seen.insert(node.to_string()) {
2651 return;
2652 }
2653 if let Some(next) = dependencies.get(node) {
2654 for dependency in next {
2655 collect_input_dependencies(dependency, dependencies, inputs, seen, found);
2656 }
2657 }
2658}
2659
2660fn expr_names(expr: &Expr) -> BTreeSet<String> {
2661 fn collect(expr: &Expr, names: &mut BTreeSet<String>) {
2662 match expr {
2663 Expr::Var(name) => {
2664 names.insert(name.clone());
2665 }
2666 Expr::Not(inner) => collect(inner, names),
2667 Expr::Binary(_, left, right) => {
2668 collect(left, names);
2669 collect(right, names);
2670 }
2671 }
2672 }
2673 let mut names = BTreeSet::new();
2674 collect(expr, &mut names);
2675 names
2676}
2677
2678pub(crate) fn split_member(name: &str) -> Option<(&str, &str)> {
2679 let (instance, member) = name.split_once('.')?;
2680 (!instance.is_empty() && !member.is_empty() && !member.contains('.'))
2681 .then_some((instance, member))
2682}
2683
2684fn write_interface(out: &mut String, module: &BlockGraph, ids: &HashMap<IVec3, u32>) {
2685 if module.interface == ModuleInterface::default() {
2686 return;
2687 }
2688 for port in &module.interface.quantum_ports {
2689 let id = ids[&port.position];
2690 writeln!(
2691 out,
2692 " {} {}: {} = {}",
2693 port.direction.keyword(),
2694 port.name,
2695 port.resource_type,
2696 id
2697 )
2698 .expect("writing to a String cannot fail");
2699 }
2700 for input in &module.interface.bit_inputs {
2701 writeln!(out, " in {input}").expect("writing to a String cannot fail");
2702 }
2703 for output in &module.interface.bit_outputs {
2704 writeln!(out, " out {} = {}", output.name, output.expr)
2705 .expect("writing to a String cannot fail");
2706 }
2707 out.push('\n');
2708}
2709
2710fn write_instances(out: &mut String, module: &BlockGraph) {
2711 if module.instances.is_empty() {
2712 return;
2713 }
2714 for instance in &module.instances {
2715 write!(
2716 out,
2717 " {}: {} @ {}",
2718 instance.name, instance.definition, instance.translation
2719 )
2720 .expect("writing to a String cannot fail");
2721 if !instance.rotation.is_identity() {
2722 write!(out, " rotate {}", instance.rotation).expect("writing to a String cannot fail");
2723 }
2724 out.push('\n');
2725 }
2726 out.push('\n');
2727}
2728
2729fn write_body(out: &mut String, body: &BlockGraph) {
2730 let text = body.to_blog_body_text();
2731 let body = text
2732 .strip_prefix("BLOG 1.0\n\n")
2733 .expect("BlockGraph writer emits BLOG 1.0")
2734 .trim_end();
2735 if body.is_empty() {
2736 return;
2737 }
2738 for line in body.lines() {
2739 if line.is_empty() {
2740 out.push('\n');
2741 } else {
2742 writeln!(out, " {line}").expect("writing to a String cannot fail");
2743 }
2744 }
2745 out.push('\n');
2746}
2747
2748fn write_connections(out: &mut String, module: &BlockGraph, ids: &HashMap<IVec3, u32>) {
2749 if module.quantum_connections.is_empty() && module.bit_bindings.is_empty() {
2750 return;
2751 }
2752 let id = |block: &IVec3| {
2753 ids.get(block)
2754 .expect("validated connection block has a BLOG ID")
2755 };
2756 let arrow = |hadamard| if hadamard { "-H>" } else { "->" };
2757 for connection in &module.quantum_connections {
2758 match connection {
2759 QuantumConnection::Input {
2760 block,
2761 input,
2762 hadamard,
2763 } => writeln!(
2764 out,
2765 " {} {} {}.{}",
2766 id(block),
2767 arrow(*hadamard),
2768 input.instance,
2769 input.port
2770 )
2771 .expect("writing to a String cannot fail"),
2772 QuantumConnection::Output {
2773 output,
2774 block,
2775 hadamard,
2776 } => writeln!(
2777 out,
2778 " {}.{} {} {}",
2779 output.instance,
2780 output.port,
2781 arrow(*hadamard),
2782 id(block)
2783 )
2784 .expect("writing to a String cannot fail"),
2785 QuantumConnection::Pipe {
2786 output,
2787 input,
2788 hadamard,
2789 } => writeln!(
2790 out,
2791 " {}.{} {} {}.{}",
2792 output.instance,
2793 output.port,
2794 arrow(*hadamard),
2795 input.instance,
2796 input.port
2797 )
2798 .expect("writing to a String cannot fail"),
2799 }
2800 }
2801 for binding in &module.bit_bindings {
2802 let source = match &binding.source.instance {
2803 Some(instance) => format!("{instance}.{}", binding.source.bit),
2804 None => binding.source.bit.clone(),
2805 };
2806 writeln!(
2807 out,
2808 " {source} => {}.{}",
2809 binding.target_instance, binding.target_bit
2810 )
2811 .expect("writing to a String cannot fail");
2812 }
2813}
2814
2815#[cfg(test)]
2816mod tests {
2817 use super::*;
2818 use crate::{
2819 CubeKind, FeedbackTarget, GalleryItem, Pauli, PauliBasis, SelectiveFixingTarget,
2820 SelectiveKind, Stabilizer, StabilizerGenerator, StabilizerGenerators, StabilizerRowKind,
2821 parse_inline_graph,
2822 };
2823
2824 #[test]
2825 fn boolean_work_default_is_unlimited_but_explicit_limits_still_apply() {
2826 use bloq_utils::boolean::{BooleanDecisionDiagram, BooleanLimits};
2827
2828 let defaults = ModuleCertificationLimits::DEFAULT;
2829 assert_eq!(defaults.max_boolean_steps, usize::MAX);
2830 assert_eq!(defaults.max_boolean_nodes, 4_000_000);
2831 assert_eq!(BooleanLimits::DEFAULT.max_steps, 64_000_000);
2832 let mut diagram = BooleanDecisionDiagram::with_limits(defaults.boolean_limits());
2833 diagram.charge(512_000_001).unwrap();
2834 assert_eq!(diagram.steps(), 512_000_001);
2835
2836 let mut limits = defaults;
2837 limits.set("max_boolean_steps", 512_000_000).unwrap();
2838 let mut diagram = BooleanDecisionDiagram::with_limits(limits.boolean_limits());
2839 let error = diagram.charge(512_000_001).unwrap_err();
2840 assert_eq!((error.observed, error.limit), (512_000_001, 512_000_000));
2841 assert_eq!(diagram.steps(), 0);
2842 }
2843
2844 #[test]
2845 fn named_limit_overrides_select_each_field_and_reject_unknown_names() {
2846 let mut limits = ModuleCertificationLimits::DEFAULT;
2847 for (value, field) in ModuleCertificationLimits::FIELD_NAMES
2848 .into_iter()
2849 .enumerate()
2850 {
2851 limits.set(field, value).unwrap();
2852 }
2853 assert_eq!(
2854 limits,
2855 ModuleCertificationLimits {
2856 max_expanded_blocks: 0,
2857 max_expanded_instances: 1,
2858 max_occupied_cells: 2,
2859 max_local_columns: 3,
2860 max_boolean_nodes: 4,
2861 max_boolean_steps: 5,
2862 max_matrix_words: 6,
2863 max_frontier_width: 7,
2864 max_witness_nodes: 8,
2865 max_normalization_states: 9,
2866 max_guarded_domain_size: 10,
2867 }
2868 );
2869 let previous = limits;
2870 let error = limits.set("max_booleen_steps", 0).unwrap_err();
2871 assert_eq!(error, UnknownCertificationLimit("max_booleen_steps".into()));
2872 assert_eq!(limits, previous);
2873 assert!(error.to_string().contains("max_boolean_steps"));
2874 }
2875
2876 #[test]
2877 fn linked_port_feedback_preserves_its_wire_and_hadamard_frame() {
2878 let source = "BLOG 1.0
2879 module Wire {
2880 in input: data = 0
2881 out output: data = 2
2882 0: Port [0,0,-1] role=input
2883 1: ZXZ [0,0,0]
2884 2: Port [0,0,1] role=output
2885 0 -> +Z
2886 1 -H> +Z
2887 feedback X 2
2888 }
2889 module main {
2890 in input: data = 0
2891 out a: data = 2
2892 out b: data = 3
2893 0: Port [0,0,-1] role=input
2894 1: XZX [0,0,1]
2895 2: Port [0,0,2] role=output
2896 3: Port [1,0,1] role=output
2897 wire: Wire @ [0,0,0]
2898 0 -> wire.input
2899 wire.output -> 1
2900 1 -> +Z
2901 1 -> +X
2902 }";
2903 for direction in ["", " -> -Z"] {
2904 for binding_hadamard in [false, true] {
2905 let source = source.replace("feedback X 2", &format!("feedback X 2{direction}"));
2906 let source = if binding_hadamard {
2907 source
2908 .replace("wire.output -> 1", "wire.output -H> 1")
2909 .replace("1: XZX [0,0,1]", "1: ZXZ [0,0,1]")
2910 } else {
2911 source
2912 };
2913 let program = parse_inline_graph(&source).unwrap();
2914 let graph = program.materialize_flat_graph().unwrap();
2915 let expected = Action::Feedback {
2916 targets: vec![FeedbackTarget {
2917 pauli: PauliBasis::Z,
2918 target: IVec3::ZERO,
2919 direction: Some(Direction::ZPLUS),
2920 }],
2921 condition: None,
2922 };
2923 assert_eq!(
2926 graph.actions(),
2927 vec![expected.clone()],
2928 "direction={direction:?}, binding_hadamard={binding_hadamard}",
2929 );
2930 let round_trip = crate::parse_blog_to_graph(&graph.to_blog_text()).unwrap();
2931 assert_eq!(round_trip.actions(), vec![expected]);
2932 let zx = crate::ZXGraph::try_from(&graph).unwrap();
2933 let center = zx.node_at(IVec3::Z).unwrap();
2934 let mut row = crate::PauliString::new(zx.total_ids());
2935 for pos in [IVec3::new(0, 0, 2), IVec3::new(1, 0, 1)] {
2936 let output = zx.node_at(pos).unwrap();
2937 row.set(output.id, Pauli::Z);
2938 row.set(zx.edge_id(center.id, output.id).unwrap(), Pauli::Z);
2939 row.set(zx.edge_id(output.id, center.id).unwrap(), Pauli::Z);
2940 }
2941 let surface = zx.materialize_stabilizer_with_sign(row, false);
2942 let Action::Feedback { targets, .. } = &graph.actions()[0] else {
2943 unreachable!()
2944 };
2945 assert!(
2946 !surface.odd_anticommutes_feedback(targets, Some(&zx)),
2947 "X on the incoming wire preserves the two-output ZZ relation"
2948 );
2949 assert!(
2950 surface.odd_anticommutes_feedback(
2951 &[FeedbackTarget {
2952 pauli: PauliBasis::X,
2953 target: center.pos,
2954 direction: None
2955 }],
2956 Some(&zx)
2957 ),
2958 "the former junction target has different semantics"
2959 );
2960 }
2961 }
2962 }
2963
2964 const STAGE: &str = r#"module Stage {
2965 in q_in: data = 0
2966 out q_out: data = 2
2967 0: Port [0, 0, -1] <q_in>
2968 1: ZXZ [0, 0, 0]
2969 2: Port [0, 0, 1] <q_out>
2970 [0, 0, -1] -> +Z
2971 [0, 0, 0] -> +Z
2972}
2973"#;
2974
2975 const MEASURED_STAGE: &str = r#"module MeasuredStage {
2976 in q_in: data = 0
2977 out q_out: data = 2
2978 0: Port [0, 0, 0] role=input <q_in>
2979 1: XZX [0, 0, 1]
2980 2: Port [0, 0, 2] role=output <q_out>
2981 3: ZXZ [1, 0, 0]
2982 4: Z [1, 0, 1]
2983 0 -> +Z
2984 1 -> +Z
2985 3 -> +Z
2986 m = measure 4
2987}
2988"#;
2989
2990 const PAIR: &str = r#"module main {
2991 in q_in: data = 100
2992 out q_out: data = 103
2993 lower: Stage @ [0, 0, 0]
2994 upper: Stage @ [0, 0, 2]
2995 100: Port [0, 0, -1] role=input <q_in>
2996 101: ZXZ [0, 0, 1]
2997 103: Port [0, 0, 3] role=output <q_out>
2998 100 -> lower.q_in
2999 lower.q_out -> 101
3000 101 -> upper.q_in
3001 upper.q_out -> 103
3002}
3003"#;
3004
3005 const DIRECT_PAIR: &str = r#"module main {
3006 in q_in: data = 100
3007 out q_out: data = 101
3008 lower: Stage @ [0, 0, 0]
3009 upper: StageDual @ [0, 0, 1]
3010 100: Port [0, 0, -1] role=input <q_in>
3011 101: Port [0, 0, 2] role=output <q_out>
3012 100 -> lower.q_in
3013 lower.q_out -H> upper.q_in
3014 upper.q_out -> 101
3015}
3016"#;
3017
3018 fn pair_source(stage: &str) -> String {
3019 format!("BLOG 1.0\n\n{stage}\n{PAIR}")
3020 }
3021
3022 fn direct_pair_source() -> String {
3023 let dual = STAGE
3024 .replacen("module Stage", "module StageDual", 1)
3025 .replacen("1: ZXZ", "1: XZX", 1);
3026 format!("BLOG 1.0\n\n{STAGE}\n{dual}\n{DIRECT_PAIR}")
3027 }
3028
3029 fn child_loop(body: BlockGraph) -> Result<BlockGraph, ModuleError> {
3030 let child = BlockGraph::definition(
3031 "Child".to_string(),
3032 body,
3033 ModuleInterface {
3034 bit_inputs: vec!["i".to_string()],
3035 bit_outputs: vec![BitOutput {
3036 name: "o".to_string(),
3037 expr: Expr::Var("m".to_string()),
3038 }],
3039 ..ModuleInterface::default()
3040 },
3041 Vec::new(),
3042 Vec::new(),
3043 Vec::new(),
3044 );
3045 let root = BlockGraph::definition(
3046 "main".to_string(),
3047 BlockGraph::new(),
3048 ModuleInterface::default(),
3049 vec![ModuleInstance {
3050 name: "child".to_string(),
3051 definition: "Child".to_string(),
3052 rotation: ModuleRotation::IDENTITY,
3053 translation: IVec3::ZERO,
3054 }],
3055 Vec::new(),
3056 vec![BitBinding {
3057 source: BitRef {
3058 instance: Some("child".to_string()),
3059 bit: "o".to_string(),
3060 },
3061 target_instance: "child".to_string(),
3062 target_bit: "i".to_string(),
3063 }],
3064 );
3065 BlockGraph::from_definitions(vec![child, root])
3066 }
3067
3068 fn analyzed_body(
3069 mut body: BlockGraph,
3070 actions: Vec<Action>,
3071 generators: Vec<StabilizerGenerator>,
3072 ) -> BlockGraph {
3073 body.set_actions_deferred_with_inputs(actions, ["i".to_string()])
3074 .unwrap();
3075 let zx = body.to_zx_graph().unwrap();
3076 body.with_analyzed_action_graph(&StabilizerGenerators::new(zx, generators))
3077 .unwrap()
3078 }
3079
3080 fn y_seam_source() -> String {
3081 r#"BLOG 1.0
3082
3083module YCap {
3084 in q: data = 0
3085 0: Port [0, 0, 0] role=input <q>
3086 1: Y [0, 0, 1]
3087 [0, 0, 0] -> +Z
3088}
3089
3090module main {
3091 in q: data = 10
3092 y: YCap @ [0, 0, 0]
3093 10: Port [0, 0, 0] role=input <q>
3094 10 -> y.q
3095}
3096
3097"#
3098 .to_string()
3099 }
3100
3101 fn direct_y_seam_source() -> String {
3102 r#"BLOG 1.0
3103
3104module YPrep {
3105 out q: data = 1
3106 0: Y [0, 0, 0]
3107 1: Port [0, 0, 1] role=output <q>
3108 [0, 0, 0] -> +Z
3109}
3110
3111module Wire {
3112 in q_in: data = 0
3113 out q_out: data = 2
3114 0: Port [0, 0, -1] role=input <q_in>
3115 1: ZXZ [0, 0, 0]
3116 2: Port [0, 0, 1] role=output <q_out>
3117 [0, 0, -1] -> +Z
3118 [0, 0, 0] -> +Z
3119}
3120
3121module main {
3122 out q: data = 10
3123 y: YPrep @ [0, 0, 0]
3124 wire: Wire @ [0, 0, 1]
3125 10: Port [0, 0, 2] role=output <q>
3126 y.q -> wire.q_in
3127 wire.q_out -> 10
3128}
3129
3130module FlatY {
3131 out q: data = 10
3132 0: Y [0, 0, 0]
3133 1: ZXZ [0, 0, 1]
3134 10: Port [0, 0, 2] role=output <q>
3135 [0, 0, 0] -> +Z
3136 [0, 0, 1] -> +Z
3137}
3138
3139"#
3140 .to_string()
3141 }
3142
3143 fn direct_chain_source(len: usize) -> String {
3144 let mut instances = String::new();
3145 let mut connections = String::from(" 100 -> s0.q_in\n");
3146 for index in 0..len {
3147 writeln!(instances, " s{index}: Stage @ [0, 0, {index}]").unwrap();
3148 if index + 1 < len {
3149 writeln!(connections, " s{index}.q_out -> s{}.q_in", index + 1).unwrap();
3150 }
3151 }
3152 writeln!(connections, " s{}.q_out -> 101", len - 1).unwrap();
3153 format!(
3154 "BLOG 1.0\n\n{STAGE}\nmodule main {{\n in q_in: data = 100\n out q_out: data = 101\n{instances} 100: Port [0, 0, -1] role=input <q_in>\n 101: Port [0, 0, {len}] role=output <q_out>\n{connections}}}\n"
3155 )
3156 }
3157
3158 fn measured_chain_source(len: usize) -> String {
3159 let mut instances = String::new();
3160 let mut connectors = String::new();
3161 let mut connections = String::from(" 100 -> s0.q_in\n");
3162 for index in 0..len {
3163 writeln!(
3164 instances,
3165 " s{index}: MeasuredStage @ [0, 0, {}]",
3166 index * 2
3167 )
3168 .unwrap();
3169 if index + 1 < len {
3170 writeln!(
3171 connectors,
3172 " {}: XZX [0, 0, {}]",
3173 200 + index,
3174 (index + 1) * 2
3175 )
3176 .unwrap();
3177 writeln!(connections, " s{index}.q_out -> {}", 200 + index).unwrap();
3178 writeln!(connections, " {} -> s{}.q_in", 200 + index, index + 1).unwrap();
3179 }
3180 }
3181 writeln!(connections, " s{}.q_out -> 101", len - 1).unwrap();
3182 format!(
3183 "BLOG 1.0\n\n{MEASURED_STAGE}\nmodule main {{\n in q_in: data = 100\n out q_out: data = 101\n{instances} 100: Port [0, 0, 0] role=input <q_in>\n{connectors} 101: Port [0, 0, {}] role=output <q_out>\n{connections}}}\n",
3184 len * 2
3185 )
3186 }
3187
3188 #[test]
3189 fn module_program_round_trips_shared_replacement_block() {
3190 let program = parse_inline_graph(&pair_source(STAGE)).unwrap();
3191 assert_eq!(program.modules().len(), 2);
3192 assert_eq!(program.root().name, "main");
3193
3194 let written = program.to_blog_text();
3195 let reparsed = parse_inline_graph(&written).unwrap();
3196 assert_eq!(reparsed.root().quantum_connections.len(), 4);
3197 assert_eq!(reparsed.to_blog_text(), written);
3198 }
3199
3200 #[test]
3201 fn linking_a_branch_input_port_preserves_its_authored_cuts() {
3202 let source = r"BLOG 1.0
3203module Child {
3204 in q: data = 0
3205 in enabled
3206 0: Port [0, 0, 0] role=input
3207 branch choice {
3208 false {
3209 1: X [0, 0, 1]
3210 0 -> 1
3211 }
3212 true {
3213 2: ZXZ [0, 0, 1]
3214 0 -> 2
3215 }
3216 }
3217 resolve choice if enabled
3218}
3219module main {
3220 in enabled
3221 0: ZXZ [0, 0, 0]
3222 child: Child @ [0, 0, 0]
3223 0 -> child.q
3224 enabled => child.enabled
3225}
3226";
3227 for hadamard in [false, true] {
3228 let source = if hadamard {
3229 source
3230 .replace("0 -> child.q", "0 -H> child.q")
3231 .replace("0: ZXZ [0, 0, 0]", "0: XZX [0, 0, 0]")
3232 } else {
3233 source.to_string()
3234 };
3235 let program = parse_inline_graph(&source).unwrap();
3236 let mut graph = program.materialize_flat_graph().unwrap();
3237 let region = graph.branch_by_name("child__choice").unwrap();
3238 for arm in [region.on_true(), region.on_false()] {
3239 let pipes = arm.pipes().collect::<Vec<_>>();
3240 assert_eq!(pipes.len(), 1);
3241 assert_eq!(pipes[0].is_hadamard(), hadamard);
3242 }
3243 graph.validate().unwrap();
3244 graph.set_shown_branch_arm("child__choice", false).unwrap();
3245 assert_eq!(graph.pipes().next().unwrap().is_hadamard(), hadamard);
3246 let pipe = graph.pipes().next().unwrap().clone();
3247 let (src, dst) = pipe.endpoints();
3248 let mut ordinary = graph.clone();
3249 let mut indexed = graph.clone();
3250 ordinary.remove_pipe(src, dst).unwrap();
3251 indexed.remove_pipe(src, dst).unwrap();
3252 let mut index = LinkBranchPipeIndex::default();
3253 for (ordinal, region) in indexed.branches.iter().enumerate() {
3254 index.add_region(ordinal, region);
3255 }
3256 ordinary.try_add_pipe(pipe.clone()).unwrap();
3257 add_link_pipe(&mut indexed, &index, pipe).unwrap();
3258 assert_eq!(indexed.branch_definitions(), ordinary.branch_definitions());
3259 let exported = graph.to_program_blog_text().unwrap();
3260 let reparsed = parse_inline_graph(&exported).unwrap();
3261 reparsed
3262 .materialize_flat_graph()
3263 .unwrap()
3264 .validate()
3265 .unwrap();
3266 assert_eq!(reparsed.to_blog_text(), exported);
3267 }
3268 }
3269
3270 #[test]
3271 fn linked_occupancy_rejects_a_hidden_arm_footprint() {
3272 let source = r#"BLOG 1.0
3273module main {
3274 0: ZXZ [0,0,0]
3275 9: Z [2,0,0]
3276 branch b {
3277 false {
3278 1: ZXZ [0,0,1]
3279 2: ZXZ [1,0,1]
3280 0 -> +Z
3281 1 -> +X
3282 }
3283 true {
3284 3: ZXZ [0,0,1]
3285 4: ZXZ [-1,0,1]
3286 0 -> +Z
3287 3 -> -X
3288 }
3289 }
3290 m = measure 9
3291 resolve b if m
3292}
3293"#;
3294 let graph = parse_inline_graph(source)
3295 .unwrap()
3296 .root()
3297 .copy_local_geometry();
3298 let hidden = IVec3::new(1, 0, 1);
3299 assert!(graph.get_block(hidden).is_none());
3300 let mut index = LinkBranchOccupancy::default();
3301 for region in graph.branch_definitions() {
3302 index.add_region(region).unwrap();
3303 }
3304 let block = Block::new(hidden, BlockKind::Cube(CubeKind::ZXZ));
3305 let ordinary = graph.clone().try_add_block(block.clone()).unwrap_err();
3306 let indexed = graph
3307 .clone()
3308 .try_add_block_with_branch_occupancy(block, Some(&index))
3309 .unwrap_err();
3310 assert_eq!(ordinary.to_string(), indexed.to_string());
3311 assert!(matches!(
3312 indexed,
3313 crate::BlockGraphError::BlockPositionOccupied(position) if position == hidden
3314 ));
3315 }
3316
3317 #[test]
3318 fn definition_cache_keys_keep_shared_dependencies_once() {
3319 let mut modules = Vec::new();
3320 for index in 0_usize..21 {
3321 modules.push(BlockGraph::definition(
3322 if index == 20 {
3323 "main".into()
3324 } else {
3325 format!("m{index}")
3326 },
3327 BlockGraph::new(),
3328 ModuleInterface::default(),
3329 (index.saturating_sub(2)..index)
3330 .map(|child| ModuleInstance {
3331 name: format!("c{child}"),
3332 definition: format!("m{child}"),
3333 rotation: ModuleRotation::IDENTITY,
3334 translation: IVec3::ZERO,
3335 })
3336 .collect(),
3337 Vec::new(),
3338 Vec::new(),
3339 ));
3340 }
3341 let program = BlockGraph::from_definitions(modules).unwrap();
3342 let before = program.module_definition_cache_keys();
3343 assert_eq!(before.len(), 21);
3344 assert_eq!(before["main"].matches("module m0 {").count(), 1);
3345 assert!(before["main"].len() <= program.to_blog_text().len() + 20);
3346
3347 let mut changed = program;
3348 changed.module_mut("m1").unwrap().instances[0].rotation =
3349 ModuleRotation::new(UDirection::Z, 1);
3350 changed.validate().unwrap();
3351 let after = changed.module_definition_cache_keys();
3352 assert_eq!(before["m0"], after["m0"]);
3353 for name in ["m1", "m2", "m19", "main"] {
3354 assert_ne!(
3355 before[name], after[name],
3356 "a changed shared dependency must invalidate {name}"
3357 );
3358 }
3359 changed.definitions.reverse();
3360 assert_eq!(after, changed.module_definition_cache_keys());
3361 }
3362
3363 #[test]
3364 fn module_rotations_round_trip_and_transform_all_axes() {
3365 let source = "BLOG 1.0\n\nmodule Cell {\n 0: XZZ [0,1,2]\n}\nmodule main {\n x: Cell @ [10,0,0] rotate X 90\n y: Cell @ [20,0,0] rotate Y -90\n z: Cell @ [30,0,0] rotate Z 180\n}\n";
3366 let program = parse_inline_graph(source).unwrap();
3367 let graph = program.materialize_flat_graph().unwrap();
3368
3369 assert!(graph.has_block_at(IVec3::new(10, -2, 1)));
3370 assert!(graph.has_block_at(IVec3::new(18, 1, 0)));
3371 assert!(graph.has_block_at(IVec3::new(30, -1, 2)));
3372 let written = program.to_blog_text();
3373 assert!(written.contains("rotate X 90"));
3374 assert!(written.contains("rotate Y 270"));
3375 assert!(written.contains("rotate Z 180"));
3376 assert_eq!(
3377 parse_inline_graph(&written).unwrap().to_blog_text(),
3378 written
3379 );
3380
3381 let nested = ModuleRotation::new(UDirection::Y, -1)
3382 .orientation()
3383 .then(ModuleRotation::new(UDirection::X, 1));
3384 assert_eq!(
3385 nested.try_rotate_position(IVec3::new(1, 2, 3)).unwrap(),
3386 IVec3::new(-2, -3, 1)
3387 );
3388 }
3389
3390 #[test]
3391 fn module_rotation_constraints_are_checked_during_validation() {
3392 let source = |axis, degrees| {
3393 format!(
3394 "BLOG 1.0\n\nmodule Magic {{\n 0: T [0,0,0]\n}}\nmodule main {{\n magic: Magic @ [0,0,0] rotate {axis} {degrees}\n}}\n"
3395 )
3396 };
3397
3398 parse_inline_graph(&source("Z", 90)).expect("time-axis rotation is valid");
3399 for axis in ["X", "Y"] {
3400 assert!(matches!(
3401 parse_inline_graph(&source(axis, 90)),
3402 Err(ModuleError::InvalidGeometry {
3403 source: ModuleCertificationError::Graph {
3404 source: crate::BlockGraphError::RotationUnsupportedDynamicBlocks,
3405 ..
3406 },
3407 ..
3408 })
3409 ));
3410 }
3411 assert!(matches!(
3412 parse_inline_graph(&source("Z", 45)),
3413 Err(ModuleError::InvalidModule { .. })
3414 ));
3415 }
3416
3417 #[test]
3418 fn module_program_requires_main() {
3419 assert!(matches!(
3420 parse_inline_graph("BLOG 1.0\nmodule Helper {\n}\n"),
3421 Err(ModuleError::MissingMain)
3422 ));
3423 }
3424
3425 #[test]
3426 fn graph_program_serializes_main() {
3427 let mut graph = BlockGraph::new();
3428 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Port));
3429 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
3430 graph.add_pipe(crate::Pipe::new(IVec3::NEG_Z, crate::Direction::ZPLUS));
3431
3432 let text = (graph).with_inferred_interface().unwrap().to_blog_text();
3433
3434 assert!(text.contains("module main {"));
3435 assert!(text.contains("in q0: data"));
3436 parse_inline_graph(&text).unwrap();
3437 }
3438
3439 #[test]
3440 fn module_bit_bindings_round_trip() {
3441 let source = "BLOG 1.0\n\nmodule Child {\n in enable\n out done = enable\n}\n\nmodule main {\n in enable\n out done = child.done\n child: Child @ [0,0,0]\n enable => child.enable\n}\n";
3442 let program = parse_inline_graph(source).unwrap();
3443
3444 assert_eq!(program.root().bit_bindings.len(), 1);
3445 let written = program.to_blog_text();
3446 assert_eq!(
3447 parse_inline_graph(&written).unwrap().to_blog_text(),
3448 written
3449 );
3450 }
3451
3452 #[test]
3453 fn explicit_temporal_port_role_must_match_geometry() {
3454 let source = "BLOG 1.0\n\nmodule main {\n in q: data = 0\n 0: Port [0,0,1] role=input <q>\n 1: ZXZ [0,0,0]\n 1 -> +Z\n}\n";
3455
3456 assert!(matches!(
3457 parse_inline_graph(source),
3458 Err(ModuleError::InvalidModule { .. })
3459 ));
3460 }
3461
3462 #[test]
3463 fn explicit_spatial_port_role_is_valid() {
3464 let source = "BLOG 1.0\n\nmodule main {\n in q: data = 0\n 0: Port [0,0,0] role=input <q>\n 1: ZXZ [1,0,0]\n 0 -> +X\n}\n";
3465
3466 parse_inline_graph(source).expect("spatial interface has explicit direction");
3467 }
3468
3469 #[test]
3470 fn explicit_port_role_still_requires_one_local_pipe() {
3471 let source = "BLOG 1.0\n\nmodule main {\n in q: data = 0\n 0: Port [0,0,0] role=input <q>\n 1: ZXZ [1,0,0]\n 2: ZXZ [0,1,0]\n 0 -> +X\n 0 -> +Y\n}\n";
3472
3473 assert!(matches!(
3474 parse_inline_graph(source),
3475 Err(ModuleError::InvalidModule { .. })
3476 ));
3477 }
3478
3479 #[test]
3480 fn multiplex_interface_can_bind_a_child_port() {
3481 let source = "BLOG 1.0\nmodule Wire {\n\
3482 in q: data = 0\nout result: data = 2\n\
3483 0: Port [1,0,0] role=multiplex\n1: ZXZ [0,0,0]\n2: Port [0,0,1]\n\
3484 1 -> +X\n1 -> +Z\n}\nmodule main {\n\
3485 in q: data = 10\nout result: data = 11\n\
3486 wire: Wire @ [0,0,0]\n\
3487 10: Port [1,0,0] role=multiplex\n11: Port [0,0,1] role=output\n\
3488 10 -> wire.q\nwire.result -> 11\n}\n";
3489 let program = parse_inline_graph(source).unwrap();
3490 let graph = program.materialize_root_graph().unwrap();
3491 graph.validate().unwrap();
3492 assert_eq!(
3493 graph.get_block(IVec3::X).unwrap().port_role(),
3494 Some(PortRole::Multiplex)
3495 );
3496 parse_inline_graph(&source.replace("10 -> wire.q\n", "")).unwrap_err();
3497 parse_inline_graph(
3498 "BLOG 1.0\nmodule main {\nin q: data = 0\n\
3499 0: Port [0,0,0] role=multiplex\n}\n",
3500 )
3501 .unwrap_err();
3502 }
3503
3504 #[test]
3505 fn direct_seams_are_geometry_validated_during_parse() {
3506 let distant = direct_pair_source().replace(
3507 "upper: StageDual @ [0, 0, 1]",
3508 "upper: StageDual @ [0, 0, 10]",
3509 );
3510 assert!(matches!(
3511 parse_inline_graph(&distant),
3512 Err(ModuleError::InvalidModule { .. })
3513 ));
3514
3515 let wrong_basis =
3516 direct_pair_source().replace("lower.q_out -H> upper.q_in", "lower.q_out -> upper.q_in");
3517 let error = parse_inline_graph(&wrong_basis).unwrap_err();
3518 assert!(matches!(&error, ModuleError::InvalidGeometry { .. }));
3519 let span = error.span().expect("geometry error keeps its module span");
3520 assert!(wrong_basis[span.start as usize..span.end as usize].starts_with("module main"));
3521 }
3522
3523 #[test]
3524 fn module_bind_rejects_branch_local_blocks_before_writing() {
3525 let mut child = parse_inline_graph(
3526 "BLOG 1.0\n\nmodule main {\n in q: data = 0\n 0: Port [0,0,0] role=input <q>\n 1: ZXZ [0,0,1]\n 0 -> +Z\n}\n",
3527 )
3528 .unwrap()
3529 .root()
3530 .clone();
3531 child.name = "Child".to_string();
3532 let body = crate::parse_blog_to_graph(
3533 "BLOG 1.0\n\n9: Z [3,0,0]\n10: ZXZ [0,0,-1]\n[0,0,-1] -> +Z\nbranch b {\n false {\n 1: ZXZ [0,0,0]\n }\n true {\n 2: ZXZ [0,0,0]\n }\n}\nm = measure 9\nresolve b if m\n",
3534 )
3535 .unwrap();
3536 let parent = BlockGraph::definition(
3537 "main".to_string(),
3538 body,
3539 ModuleInterface::default(),
3540 vec![ModuleInstance {
3541 name: "child".to_string(),
3542 definition: "Child".to_string(),
3543 rotation: ModuleRotation::IDENTITY,
3544 translation: IVec3::ZERO,
3545 }],
3546 vec![QuantumConnection::Input {
3547 block: IVec3::ZERO,
3548 input: InstancePort {
3549 instance: "child".to_string(),
3550 port: "q".to_string(),
3551 },
3552 hadamard: false,
3553 }],
3554 Vec::new(),
3555 );
3556
3557 assert!(matches!(
3558 BlockGraph::from_definitions(vec![child, parent]),
3559 Err(ModuleError::InvalidModule { .. })
3560 ));
3561 }
3562
3563 #[test]
3564 fn classical_parent_output_cycle_is_rejected() {
3565 let source = "BLOG 1.0\n\nmodule Invert {\n in i\n out o = !i\n}\n\nmodule main {\n out x = child.o\n child: Invert @ [0,0,0]\n x => child.i\n}\n";
3566
3567 assert!(matches!(
3568 parse_inline_graph(source),
3569 Err(ModuleError::InvalidModule { .. })
3570 ));
3571 }
3572
3573 #[test]
3574 fn classical_sibling_cycle_is_rejected() {
3575 let source = "BLOG 1.0\n\nmodule Pass {\n in i\n out o = i\n}\n\nmodule main {\n a: Pass @ [0,0,0]\n b: Pass @ [0,0,0]\n a.o => b.i\n b.o => a.i\n}\n";
3576
3577 assert!(matches!(
3578 parse_inline_graph(source),
3579 Err(ModuleError::InvalidModule { .. })
3580 ));
3581 }
3582
3583 #[test]
3584 fn input_independent_child_output_does_not_create_a_cycle() {
3585 let source = "BLOG 1.0\n\nmodule Independent {\n in i\n out o = m\n 0: Z [0,0,0]\n m = measure 0\n}\n\nmodule main {\n child: Independent @ [0,0,0]\n child.o => child.i\n}\n";
3586
3587 parse_inline_graph(source).expect("child output does not depend on its input");
3588 }
3589
3590 #[test]
3591 fn feedback_dependency_closes_a_child_binding_cycle() {
3592 let target = IVec3::ZERO;
3593 let mut body = BlockGraph::new();
3594 body.add_block(Block::new(target, BlockKind::Cube(CubeKind::ZXZ)));
3595 body.add_block(Block::new(IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
3596 body.add_pipe(crate::Pipe::new(target, crate::Direction::ZPLUS));
3597 let zx = body.to_zx_graph().unwrap();
3598 let row = crate::PauliString::from_terms(
3599 zx.total_ids(),
3600 (0..zx.total_ids()).map(|column| (column, Pauli::Z)),
3601 );
3602 let surface = zx.materialize_stabilizer(row);
3603 let body = analyzed_body(
3604 body,
3605 vec![
3606 Action::Feedback {
3607 targets: vec![FeedbackTarget {
3608 pauli: PauliBasis::X,
3609 target,
3610 direction: None,
3611 }],
3612 condition: Some(Expr::Var("i".to_string())),
3613 },
3614 Action::Measure {
3615 target: crate::MeasureTarget::Node(target),
3616 name: "m".to_string(),
3617 },
3618 ],
3619 vec![StabilizerGenerator::new(
3620 surface,
3621 StabilizerRowKind::Measurement {
3622 name: "m".to_string(),
3623 },
3624 )],
3625 );
3626
3627 assert!(matches!(
3628 child_loop(body),
3629 Err(ModuleError::InvalidModule { .. })
3630 ));
3631 }
3632
3633 #[test]
3634 fn selective_dependency_closes_a_child_binding_cycle() {
3635 let target = IVec3::ZERO;
3636 let mut body = BlockGraph::new();
3637 body.add_block(Block::new(IVec3::NEG_Z, BlockKind::Cube(CubeKind::ZXZ)));
3638 body.add_block(Block::new(target, BlockKind::Selective(SelectiveKind::XY)));
3639 body.add_pipe(crate::Pipe::new(IVec3::NEG_Z, crate::Direction::ZPLUS));
3640 let body = analyzed_body(
3641 body,
3642 vec![
3643 Action::Resolve {
3644 target,
3645 condition: Expr::Var("i".to_string()),
3646 },
3647 Action::Measure {
3648 target: crate::MeasureTarget::Node(target),
3649 name: "m".to_string(),
3650 },
3651 ],
3652 vec![
3653 StabilizerGenerator::new(
3654 Stabilizer::from_interior_nodes([(target, Pauli::X)]),
3655 StabilizerRowKind::Measurement {
3656 name: "m".to_string(),
3657 },
3658 ),
3659 StabilizerGenerator::new(
3660 Stabilizer::from_interior_nodes([(target, Pauli::Y)]),
3661 StabilizerRowKind::SelectiveFixing {
3662 targets: vec![SelectiveFixingTarget {
3663 pos: target,
3664 forbidden: Pauli::Y,
3665 }],
3666 },
3667 ),
3668 ],
3669 );
3670
3671 assert!(matches!(
3672 child_loop(body),
3673 Err(ModuleError::InvalidModule { .. })
3674 ));
3675 }
3676
3677 #[test]
3678 fn output_can_export_a_same_named_local_action() {
3679 let source = "BLOG 1.0\n\nmodule main {\n out m = m\n 0: Z [0,0,0]\n m = measure 0\n}\n";
3680
3681 parse_inline_graph(source).expect("output and local action have separate nodes");
3682 }
3683
3684 #[test]
3685 fn programmatic_bit_input_cannot_shadow_a_local_action() {
3686 let body = crate::parse_blog_to_graph("BLOG 1.0\n\n0: Z [0,0,0]\nm = measure 0\n").unwrap();
3687 let module = BlockGraph::definition(
3688 "main".to_string(),
3689 body,
3690 ModuleInterface {
3691 bit_inputs: vec!["m".to_string()],
3692 ..ModuleInterface::default()
3693 },
3694 Vec::new(),
3695 Vec::new(),
3696 Vec::new(),
3697 );
3698
3699 assert!(matches!(
3700 BlockGraph::from_definitions(vec![module]),
3701 Err(ModuleError::InvalidModule { message, .. })
3702 if message.contains("also a local action")
3703 ));
3704 }
3705
3706 #[test]
3707 fn module_local_actions_use_contextual_identifier_grammar() {
3708 let source =
3709 "BLOG 1.0\n\nmodule main {\n in value\n module = value\n path/name = module\n}\n";
3710
3711 parse_inline_graph(source).expect("contextual and slash action names are valid");
3712 }
3713
3714 #[test]
3715 fn module_namespaces_reserve_the_instance_path_separator() {
3716 for source in [
3717 "BLOG 1.0\n\nmodule Leaf {\n}\nmodule main {\n bad__child: Leaf @ [0,0,0]\n}\n",
3718 "BLOG 1.0\n\nmodule main {\n in bad__input\n}\n",
3719 "BLOG 1.0\n\nmodule main {\n in value\n out bad__output = value\n}\n",
3720 "BLOG 1.0\n\nmodule main {\n in value\n bad__local = value\n}\n",
3721 ] {
3722 let error = parse_inline_graph(source).unwrap_err();
3723 assert!(error.to_string().contains("invalid"), "{error}");
3724 }
3725
3726 let mut program = GalleryItem::CCZGateTeleport.build();
3727 program.branches.first_mut().unwrap().name = "bad__branch".into();
3728 let error = program.validate().unwrap_err();
3729 assert!(error.to_string().contains("invalid branch name"), "{error}");
3730 }
3731
3732 #[test]
3733 fn leaf_certification_projects_rows_and_reports_limits() {
3734 let program = parse_inline_graph(&pair_source(STAGE)).unwrap();
3735 let certificate = program
3736 .certify_leaf("Stage", ModuleCertificationLimits::UNLIMITED)
3737 .unwrap();
3738 assert_eq!(certificate.boundary_rows().len(), 2);
3739 let row = certificate.materialize_boundary_row(0).unwrap();
3740 assert!(row.paulis.len() > 2);
3741
3742 let error = program
3743 .certify_leaf(
3744 "Stage",
3745 ModuleCertificationLimits {
3746 max_local_columns: 0,
3747 ..ModuleCertificationLimits::UNLIMITED
3748 },
3749 )
3750 .unwrap_err();
3751 assert!(matches!(
3752 error,
3753 ModuleCertificationError::ResourceLimited {
3754 phase: "local ZX columns",
3755 ..
3756 }
3757 ));
3758 }
3759
3760 #[test]
3761 fn leaf_certification_bounds_closure_searches() {
3762 let source = r#"BLOG 1.0
3763
3764module main {
3765 out out: data = 2
3766 1: XZX [0, 0, 1]
3767 2: Port [0, 0, 2] role=output <out>
3768 3: T [1, 0, 0]
3769 4: XZX [1, 0, 1]
3770 5: YX [1, 0, 2]
3771 [0, 0, 1] -> +Z
3772 [0, 0, 1] -> +X
3773 [1, 0, 0] -> +Z
3774 [1, 0, 2] -> -Z
3775 mzz = measure 1 -> +X
3776 resolve 5 if mzz
3777}
3778
3779"#;
3780 let program = parse_inline_graph(source).unwrap();
3781 let error = program
3782 .certify_leaf(
3783 "main",
3784 ModuleCertificationLimits {
3785 max_guarded_domain_size: 1,
3786 ..ModuleCertificationLimits::UNLIMITED
3787 },
3788 )
3789 .unwrap_err();
3790 assert!(matches!(
3791 error,
3792 ModuleCertificationError::ResourceLimited {
3793 phase: "guarded-domain branches",
3794 observed: 2,
3795 limit: 1,
3796 ..
3797 }
3798 ));
3799
3800 let error = program
3801 .certify_leaf(
3802 "main",
3803 ModuleCertificationLimits {
3804 max_normalization_states: 0,
3805 ..ModuleCertificationLimits::UNLIMITED
3806 },
3807 )
3808 .unwrap_err();
3809 assert!(matches!(
3810 error,
3811 ModuleCertificationError::ResourceLimited {
3812 phase: "selective fixing setup states",
3813 observed: 1,
3814 limit: 0,
3815 ..
3816 }
3817 ));
3818 }
3819
3820 #[test]
3821 fn child_summaries_compose_bind_and_direct_seams() {
3822 let summary = parse_inline_graph(&pair_source(STAGE))
3823 .unwrap()
3824 .summarize_root(ModuleCertificationLimits::UNLIMITED)
3825 .unwrap();
3826 assert_eq!(summary.quantum_ports().len(), 2);
3827 assert_eq!(summary.boundary_rows().len(), 2);
3828 for row in summary.boundary_rows() {
3829 assert_eq!(row.phase(), 0);
3830 assert_eq!(row.paulis.get(0), row.paulis.get(1));
3831 assert_ne!(row.paulis.get(0), Pauli::I);
3832 }
3833
3834 let direct = parse_inline_graph(&direct_pair_source())
3835 .unwrap()
3836 .summarize_root(ModuleCertificationLimits::UNLIMITED)
3837 .unwrap();
3838 let rows = direct
3839 .boundary_rows()
3840 .map(|row| ((row.paulis.get(0), row.paulis.get(1)), row.phase()))
3841 .collect::<Vec<_>>();
3842 assert_eq!(
3843 rows,
3844 vec![((Pauli::X, Pauli::Z), 0), ((Pauli::Z, Pauli::X), 0)]
3845 );
3846 }
3847
3848 #[test]
3849 fn module_summary_jobs_preserve_output() {
3850 let program = parse_inline_graph(&direct_pair_source()).unwrap();
3851 let rows = |jobs| {
3852 program
3853 .summarize_root_with_jobs(ModuleCertificationLimits::UNLIMITED, jobs)
3854 .unwrap()
3855 .boundary_rows()
3856 .cloned()
3857 .collect::<Vec<_>>()
3858 };
3859
3860 assert_eq!(
3861 rows(std::num::NonZeroUsize::MIN),
3862 rows(std::num::NonZeroUsize::new(4).unwrap())
3863 );
3864 }
3865
3866 #[test]
3867 fn y_supported_seams_preserve_flattened_signs() {
3868 let bound = parse_inline_graph(&y_seam_source()).unwrap();
3869 let leaf = bound
3870 .certify_leaf("YCap", ModuleCertificationLimits::UNLIMITED)
3871 .unwrap()
3872 .boundary_rows()
3873 .cloned()
3874 .collect::<Vec<_>>();
3875 let composed = bound
3876 .summarize_root(ModuleCertificationLimits::UNLIMITED)
3877 .unwrap()
3878 .boundary_rows()
3879 .cloned()
3880 .collect::<Vec<_>>();
3881 assert_eq!(composed, leaf);
3882
3883 let direct = parse_inline_graph(&direct_y_seam_source()).unwrap();
3884 let composed = direct
3885 .summarize_root(ModuleCertificationLimits::UNLIMITED)
3886 .unwrap()
3887 .boundary_rows()
3888 .cloned()
3889 .collect::<Vec<_>>();
3890 let flat = direct
3891 .certify_leaf("FlatY", ModuleCertificationLimits::UNLIMITED)
3892 .unwrap()
3893 .boundary_rows()
3894 .cloned()
3895 .collect::<Vec<_>>();
3896 assert_eq!(composed, flat);
3897 }
3898
3899 #[test]
3900 fn module_program_rejects_overlapping_translated_child_bodies() {
3901 const CLOSED: &str = "module Closed {\n 0: ZXZ [0, 0, 0]\n}";
3902 let root = "module main {\n child: Closed @ [0, 0, 0]\n 0: ZXZ [0, 0, 0]\n}";
3903 let source = format!("BLOG 1.0\n\n{CLOSED}\n{root}\n");
3904 let error = parse_inline_graph(&source).unwrap_err();
3905 assert!(matches!(
3906 error,
3907 ModuleError::InvalidGeometry {
3908 source: ModuleCertificationError::Graph {
3909 source: crate::BlockGraphError::ModuleBlockOverlap { .. },
3910 ..
3911 },
3912 ..
3913 }
3914 ));
3915 }
3916
3917 #[test]
3918 fn module_overlap_error_attributes_both_instances() {
3919 let source = "BLOG 1.0\n\nmodule Closed {\n 0: ZXZ [0, 0, 0]\n}\nmodule main {\n a: Closed @ [0, 0, 0]\n b: Closed @ [0, 0, 0]\n}\n";
3920 let error = parse_inline_graph(source).unwrap_err();
3921 let message = error.to_string();
3922 let ModuleError::InvalidGeometry {
3923 source:
3924 ModuleCertificationError::Graph {
3925 source:
3926 crate::BlockGraphError::ModuleBlockOverlap {
3927 position,
3928 first,
3929 second,
3930 },
3931 ..
3932 },
3933 ..
3934 } = error
3935 else {
3936 panic!("unexpected error: {message}");
3937 };
3938 let mut paths = [first.instance_path.as_str(), second.instance_path.as_str()];
3939 paths.sort_unstable();
3940
3941 assert_eq!(position, IVec3::ZERO);
3942 assert_eq!(paths, ["a", "b"]);
3943 assert_eq!(first.definition, "Closed");
3944 assert_eq!(second.definition, "Closed");
3945 assert_eq!(first.local_position, IVec3::ZERO);
3946 assert_eq!(second.local_position, IVec3::ZERO);
3947 assert!(message.contains("instance 'a'"), "{message}");
3948 assert!(message.contains("instance 'b'"), "{message}");
3949 }
3950
3951 #[test]
3952 fn module_reserved_overlap_names_the_footprint_cell() {
3953 let source = "BLOG 1.0\n\nmodule Tall {\n 0: ZXZ [0, 0, 0] height=3d\n}\nmodule main {\n a: Tall @ [0, 0, 0]\n b: Tall @ [0, 0, 2]\n}\n";
3954 let error = parse_inline_graph(source).unwrap_err();
3955 let message = error.to_string();
3956
3957 assert!(matches!(
3958 error,
3959 ModuleError::InvalidGeometry {
3960 source: ModuleCertificationError::Graph {
3961 source: crate::BlockGraphError::ModuleBlockOverlap { position, .. },
3962 ..
3963 },
3964 ..
3965 } if position == IVec3::new(0, 0, 2)
3966 ));
3967 assert!(
3968 message.contains("block footprints conflict at cell [0, 0, 2]"),
3969 "{message}"
3970 );
3971 }
3972
3973 #[test]
3974 fn nested_geometry_error_names_the_invalid_child() {
3975 let source = "BLOG 1.0\n\nmodule main {\n child: Child @ [0,0,0]\n}\n\nmodule Child {\n a: Leaf @ [0,0,0]\n b: Leaf @ [0,0,0]\n}\n\nmodule Leaf {\n 0: ZXZ [0,0,0]\n}\n";
3976 let error = parse_inline_graph(source).unwrap_err();
3977
3978 assert!(matches!(
3979 &error,
3980 ModuleError::InvalidGeometry { module, .. } if module == "Child"
3981 ));
3982 let span = error.span().unwrap();
3983 assert!(source[span.start as usize..span.end as usize].starts_with("module Child"));
3984 }
3985
3986 #[test]
3987 fn composite_geometry_checks_hidden_branch_arms() {
3988 let source = r#"BLOG 1.0
3989
3990module Branched {
3991 0: ZXZ [0,0,0]
3992 9: Z [2,0,0]
3993 branch b {
3994 false {
3995 1: ZXZ [0,0,1]
3996 2: ZXZ [1,0,1]
3997 0 -> +Z
3998 1 -> +X
3999 }
4000 true {
4001 3: ZXZ [0,0,1]
4002 4: ZXZ [-1,0,1]
4003 0 -> +Z
4004 3 -> -X
4005 }
4006 }
4007 m = measure 9
4008 resolve b if m
4009}
4010
4011module main {
4012 child: Branched @ [0,0,0]
4013 0: ZXZ [1,0,1]
4014}
4015
4016"#;
4017
4018 assert!(matches!(
4019 parse_inline_graph(source),
4020 Err(ModuleError::InvalidGeometry {
4021 source: ModuleCertificationError::Graph {
4022 source: crate::BlockGraphError::ModuleBlockOverlap { position, .. },
4023 ..
4024 },
4025 ..
4026 }) if position == IVec3::new(1, 0, 1)
4027 ));
4028 }
4029
4030 #[test]
4031 fn direct_chains_keep_a_fixed_composition_frontier() {
4032 for len in [10, 100, 1_000] {
4033 let summary = parse_inline_graph(&direct_chain_source(len))
4034 .unwrap()
4035 .summarize_root(ModuleCertificationLimits {
4036 max_frontier_width: 4,
4037 ..ModuleCertificationLimits::UNLIMITED
4038 })
4039 .unwrap();
4040 assert_eq!(summary.boundary_rows().len(), 2);
4041 assert!(
4042 summary
4043 .boundary_rows()
4044 .all(|row| row.phase() == 0 && row.paulis.get(0) == row.paulis.get(1))
4045 );
4046 }
4047 }
4048
4049 #[test]
4050 fn binding_from_child_let_is_emitted_after_its_definition() {
4051 let source = r#"BLOG 1.0
4052
4053module Producer {
4054 out fire = t
4055 0: ZXZ [0, 0, 0]
4056 m = measure 0
4057 t = !m
4058}
4059
4060module Consumer {
4061 in enable
4062 discard if enable
4063}
4064
4065module main {
4066 p: Producer @ [0, 0, 0]
4067 c: Consumer @ [0, 0, 0]
4068 p.fire => c.enable
4069}
4070"#;
4071 let program = parse_inline_graph(source).unwrap();
4072 let linked = flatten_module_definition(&program, program.root(), "").unwrap();
4073 let actions = linked.graph.actions();
4074 assert!(matches!(&actions[0], Action::Measure { name, .. } if name == "p__m"));
4075 assert!(matches!(&actions[1], Action::Let { name, .. } if name == "p__t"));
4076 assert!(matches!(&actions[2], Action::Let { name, .. } if name == "c__enable"));
4077 assert!(matches!(&actions[3], Action::DiscardIf(_)));
4078 assert_eq!(linked.action_scopes, ["p", "p", "c", "c"]);
4079 }
4080
4081 #[test]
4082 fn discharged_measurements_still_materialize() {
4083 let program = parse_inline_graph(&measured_chain_source(32)).unwrap();
4084 let summary = program
4085 .summarize_root(ModuleCertificationLimits {
4086 max_frontier_width: 6,
4087 ..ModuleCertificationLimits::UNLIMITED
4088 })
4089 .unwrap();
4090 let graph = program
4091 .materialize_root_graph()
4092 .unwrap()
4093 .fix_shadowed_faces();
4094 let zx = crate::ZXGraph::try_from(&graph).unwrap();
4095 let stabilizers = summary.materialize_stabilizers(&zx, IVec3::ZERO).unwrap();
4096
4097 assert_eq!(
4098 stabilizers
4099 .generators
4100 .iter()
4101 .filter(|generator| generator.measurement_name().is_some())
4102 .count(),
4103 32
4104 );
4105 }
4106
4107 #[test]
4108 fn independent_connectors_preserve_named_parent_ports_at_the_public_width_limit() {
4109 let source = format!(
4110 "BLOG 1.0\n{STAGE}\n{}",
4111 r#"module main {
4112 out o1: data = 3
4113 in i0: data = 0
4114 out o0: data = 1
4115 in i1: data = 2
4116 a: Stage @ [0,0,0]
4117 b: Stage @ [3,0,0]
4118 0: Port [0,0,-1] role=input
4119 1: Port [0,0,1] role=output
4120 2: Port [3,0,-1] role=input
4121 3: Port [3,0,1] role=output
4122 b.q_out -> 3
4123 0 -> a.q_in
4124 a.q_out -> 1
4125 2 -> b.q_in
4126}"#
4127 );
4128 let summary = parse_inline_graph(&source)
4129 .unwrap()
4130 .summarize_root(ModuleCertificationLimits {
4131 max_frontier_width: 4,
4132 ..ModuleCertificationLimits::UNLIMITED
4133 })
4134 .unwrap();
4135 let column = |name| {
4136 summary
4137 .quantum_ports()
4138 .iter()
4139 .position(|port| port.name == name)
4140 .unwrap()
4141 };
4142 let expected = [("i0", "o0"), ("i1", "o1")]
4143 .into_iter()
4144 .flat_map(|(input, output)| {
4145 [crate::Pauli::X, crate::Pauli::Z].map(|pauli| {
4146 crate::PhasedPauliString::positive(crate::PauliString::from_terms(
4147 4,
4148 [(column(input), pauli), (column(output), pauli)],
4149 ))
4150 })
4151 })
4152 .collect::<Vec<_>>();
4153 assert_eq!(summary.boundary_rows().len(), expected.len());
4154 assert!(
4155 expected
4156 .iter()
4157 .all(|row| summary.boundary_rows().any(|actual| actual == row))
4158 );
4159 }
4160
4161 #[test]
4162 fn bit_input_is_an_external_action_variable() {
4163 let source = r#"BLOG 1.0
4164
4165module main {
4166 in enabled
4167 0: ZXZ [0, 0, 0]
4168 1: ZXZ [0, 0, 1]
4169 0 -> +Z
4170 feedback X 0 if enabled
4171}
4172
4173"#;
4174 let program = parse_inline_graph(source).unwrap();
4175 assert_eq!(
4176 program
4177 .root()
4178 .local_body()
4179 .action_graph()
4180 .inputs()
4181 .collect::<Vec<_>>(),
4182 ["enabled"]
4183 );
4184 }
4185
4186 #[test]
4187 fn loader_resolves_relative_import_root() {
4188 let stage = format!(
4189 "BLOG 1.0\n\n{}",
4190 STAGE.replacen("module Stage", "module main", 1)
4191 );
4192 let root = pair_source("import \"stage.blog\" as Stage");
4193 let program = crate::load_graph_with_resolver_and_limits(
4194 "root.blog",
4195 |path| match path.to_str() {
4196 Some("root.blog") => Ok(root.clone()),
4197 Some("stage.blog") => Ok(stage.to_string()),
4198 _ => Err("missing"),
4199 },
4200 ModuleCertificationLimits::DEFAULT,
4201 )
4202 .unwrap();
4203 assert!(program.module("Stage").is_some());
4204 assert_eq!(program.root().name, "main");
4205 }
4206
4207 #[test]
4208 fn loader_rejects_import_cycle() {
4209 let error = crate::load_graph_with_resolver_and_limits(
4210 "a.blog",
4211 |path| match path.to_str() {
4212 Some("a.blog") => {
4213 Ok("BLOG 1.0\nimport \"b.blog\" as B\nmodule main {\n}\n".to_string())
4214 }
4215 Some("b.blog") => {
4216 Ok("BLOG 1.0\nimport \"./a.blog\" as A\nmodule main {\n}\n".to_string())
4217 }
4218 _ => Err("missing"),
4219 },
4220 ModuleCertificationLimits::DEFAULT,
4221 )
4222 .unwrap_err();
4223 assert!(matches!(error, ModuleError::ImportCycle { .. }));
4224 }
4225
4226 #[test]
4227 fn compact_hierarchy_is_bounded_before_geometry_expansion() {
4228 let make_modules = |body: &str, depth| {
4229 let leaf = crate::parse_blog_to_graph(body).unwrap();
4230 (0..=depth)
4231 .map(|level| {
4232 BlockGraph::definition(
4233 if level == depth {
4234 "main".into()
4235 } else {
4236 format!("L{level}")
4237 },
4238 if level == 0 {
4239 leaf.clone()
4240 } else {
4241 BlockGraph::default()
4242 },
4243 ModuleInterface::default(),
4244 if level == 0 {
4245 Vec::new()
4246 } else {
4247 ["a", "b"]
4248 .map(|name| ModuleInstance {
4249 name: name.into(),
4250 definition: format!("L{}", level - 1),
4251 rotation: ModuleRotation::default(),
4252 translation: IVec3::ZERO,
4253 })
4254 .to_vec()
4255 },
4256 Vec::new(),
4257 Vec::new(),
4258 )
4259 })
4260 .collect::<Vec<_>>()
4261 };
4262 for (body, limits, phase, expected) in [
4263 (
4264 "BLOG 1.0\n0: ZXZ [0,0,0]\n",
4265 ModuleCertificationLimits {
4266 max_expanded_blocks: 1_000_000,
4267 ..ModuleCertificationLimits::UNLIMITED
4268 },
4269 "expanded blocks",
4270 1_048_576,
4271 ),
4272 (
4273 "BLOG 1.0\n0: ZXZ [0,0,0] height=4d\n",
4274 ModuleCertificationLimits {
4275 max_occupied_cells: 4_000_000,
4276 ..ModuleCertificationLimits::UNLIMITED
4277 },
4278 "occupied footprint cells",
4279 4_194_304,
4280 ),
4281 (
4282 "BLOG 1.0\n",
4283 ModuleCertificationLimits::DEFAULT,
4284 "expanded module instances",
4285 1_048_575,
4286 ),
4287 ] {
4288 let error = BlockGraph::from_definitions_with_limits(make_modules(body, 24), limits)
4289 .unwrap_err();
4290 assert!(matches!(error, ModuleError::InvalidGeometry {
4291 source: ModuleCertificationError::ResourceLimited { phase: actual, observed, .. }, ..
4292 } if actual == phase && observed == expected));
4293 }
4294 let overflow = BlockGraph::from_definitions_with_limits(
4295 make_modules("BLOG 1.0\n", 80),
4296 ModuleCertificationLimits::UNLIMITED,
4297 )
4298 .unwrap_err();
4299 assert!(matches!(
4300 overflow,
4301 ModuleError::InvalidGeometry {
4302 source: ModuleCertificationError::ResourceLimited {
4303 phase: "expanded module instances",
4304 observed: usize::MAX,
4305 limit: usize::MAX,
4306 ..
4307 },
4308 ..
4309 }
4310 ));
4311 }
4312
4313 #[test]
4314 fn module_instances_must_be_acyclic() {
4315 let source = "BLOG 1.0\nmodule main {\n b: B @ [0,0,0]\n}\n\
4316 module B {\n a: main @ [0,0,0]\n}\n";
4317 let error = parse_inline_graph(source).unwrap_err();
4318 assert!(matches!(
4319 error,
4320 ModuleError::InvalidModule { message, .. }
4321 if message == "module dependency cycle"
4322 ));
4323 }
4324}