Skip to main content

bloq_graph/
program.rs

1//! Block graph hierarchy, interfaces, linking, and BLOG emission.
2
3use 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/// Direction of a quantum module port.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum PortDirection {
23    /// Quantum data enters the module.
24    Input,
25    /// Quantum data leaves the module.
26    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/// A child-module quarter turn applied before translation.
39///
40/// BLOG writes `rotate <axis> <degrees>`; validation accepts only multiples of
41/// 90 degrees and block kinds that support the resulting orientation.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct ModuleRotation {
44    axis: UDirection,
45    quarter_turns: u8,
46}
47
48impl ModuleRotation {
49    /// Rotation that leaves every lattice axis unchanged.
50    pub const IDENTITY: Self = Self {
51        axis: UDirection::Z,
52        quarter_turns: 0,
53    };
54
55    /// Constructs a rotation, reducing `quarter_turns` modulo four.
56    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    /// Constructs a rotation from degrees, or returns `None` for a non-quarter-turn angle.
69    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    /// Returns the unsigned rotation axis.
78    pub const fn axis(self) -> UDirection {
79        self.axis
80    }
81
82    /// Returns the clockwise quarter-turn count modulo four.
83    pub const fn quarter_turns(self) -> u8 {
84        self.quarter_turns
85    }
86
87    /// Returns the normalized angle in degrees.
88    pub const fn degrees(self) -> u16 {
89        self.quarter_turns as u16 * 90
90    }
91
92    /// Returns whether this rotation leaves the lattice unchanged.
93    pub const fn is_identity(self) -> bool {
94        self.quarter_turns == 0
95    }
96
97    /// The full cubic orientation represented by this axis rotation.
98    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    /// Rotates a lattice position around the origin.
109    ///
110    /// # Errors
111    ///
112    /// Returns a coordinate-overflow error if the rotated position is unrepresentable.
113    pub fn try_rotate_position(self, position: IVec3) -> Result<IVec3, crate::BlockGraphError> {
114        self.orientation().try_rotate_position(position)
115    }
116
117    /// Rotates an axis-aligned direction.
118    ///
119    /// # Panics
120    ///
121    /// Panics if an internally constructed cubic rotation stops being axis-aligned.
122    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/// A composed proper orientation of the cubic lattice.
148#[doc(hidden)]
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub struct ModuleOrientation {
151    /// Images of the local positive X, Y, and Z axes.
152    axes: [Direction; 3],
153}
154
155impl ModuleOrientation {
156    pub const IDENTITY: Self = Self {
157        axes: [Direction::XPLUS, Direction::YPLUS, Direction::ZPLUS],
158    };
159
160    /// Composes this parent orientation with one child-local rotation.
161    pub fn then(self, rotation: ModuleRotation) -> Self {
162        self.then_orientation(rotation.orientation())
163    }
164
165    /// Composes this parent orientation with an already-composed child orientation.
166    pub fn then_orientation(self, child: Self) -> Self {
167        Self {
168            axes: child.axes.map(|axis| self.rotate_direction(axis)),
169        }
170    }
171
172    /// Rotates a lattice position with this orientation.
173    ///
174    /// # Errors
175    ///
176    /// Returns a coordinate-overflow error if a negated component is unrepresentable.
177    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    /// Rotates, then translates, a lattice position.
199    ///
200    /// # Errors
201    ///
202    /// Returns a coordinate-overflow error if either operation is unrepresentable.
203    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/// One named quantum boundary of a module.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct QuantumPort {
256    /// Public port name.
257    pub name: String,
258    /// Definition-local port-block position.
259    pub position: IVec3,
260    /// Whether quantum data enters or leaves the module.
261    pub direction: PortDirection,
262    /// Logical resource type carried by the port.
263    pub resource_type: String,
264}
265
266/// One exported Boolean expression.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct BitOutput {
269    /// Public output name.
270    pub name: String,
271    /// Definition-local Boolean expression being exported.
272    pub expr: Expr,
273}
274
275/// Public quantum and classical interface of a module.
276#[derive(Debug, Clone, Default, PartialEq, Eq)]
277pub struct ModuleInterface {
278    /// Quantum ports in declaration order.
279    pub quantum_ports: Vec<QuantumPort>,
280    /// Classical bit-input names in declaration order.
281    pub bit_inputs: Vec<String>,
282    /// Classical bit outputs in declaration order.
283    pub bit_outputs: Vec<BitOutput>,
284}
285
286/// One rotated, then translated, use of another module definition.
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct ModuleInstance {
289    /// Parent-local instance name.
290    pub name: String,
291    /// Referenced module definition name.
292    pub definition: String,
293    /// Rotation applied before translation.
294    pub rotation: ModuleRotation,
295    /// Translation from definition-local into parent coordinates.
296    pub translation: IVec3,
297}
298
299impl ModuleInstance {
300    /// Returns the instance's cubic lattice orientation.
301    pub fn orientation(&self) -> ModuleOrientation {
302        self.rotation.orientation()
303    }
304
305    /// Maps a definition-local position into this instance.
306    ///
307    /// # Errors
308    ///
309    /// Returns a coordinate-overflow error if the transformed position is unrepresentable.
310    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/// A named port on one child instance.
317#[derive(Debug, Clone, PartialEq, Eq, Hash)]
318pub struct InstancePort {
319    /// Parent-local child instance name.
320    pub instance: String,
321    /// Public port name on that child definition.
322    pub port: String,
323}
324
325/// Explicit quantum composition authored by the parent module.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub enum QuantumConnection {
328    /// Connects a parent block to a child quantum input.
329    Input {
330        /// Parent-local port-block position.
331        block: IVec3,
332        /// Child input endpoint.
333        input: InstancePort,
334        /// Whether the seam changes basis by Hadamard.
335        hadamard: bool,
336    },
337    /// Connects a child quantum output to a parent block.
338    Output {
339        /// Child output endpoint.
340        output: InstancePort,
341        /// Parent-local port-block position.
342        block: IVec3,
343        /// Whether the seam changes basis by Hadamard.
344        hadamard: bool,
345    },
346    /// Connects one child output directly to another child input.
347    Pipe {
348        /// Child output endpoint.
349        output: InstancePort,
350        /// Child input endpoint.
351        input: InstancePort,
352        /// Whether the seam changes basis by Hadamard.
353        hadamard: bool,
354    },
355}
356
357/// A Boolean value in the parent or exported by a child.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct BitRef {
360    /// Child instance qualifier, or `None` for a parent-local value.
361    pub instance: Option<String>,
362    /// Classical bit name.
363    pub bit: String,
364}
365
366/// One explicit binding to a child Boolean input.
367#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct BitBinding {
369    /// Parent-local or child-exported source value.
370    pub source: BitRef,
371    /// Child instance receiving the value.
372    pub target_instance: String,
373    /// Classical input name on the target child.
374    pub target_bit: String,
375}
376
377/// Origin of one block in a materialized module hierarchy.
378#[doc(hidden)]
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct MaterializedModuleSite {
381    /// Owning module definition name.
382    pub definition: String,
383    /// Dot-qualified path from the root instance.
384    pub instance_path: String,
385    /// Block position inside its owning definition.
386    pub local_position: IVec3,
387    /// Composed definition-to-root orientation.
388    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/// Limits applied before and during leaf-module certification.
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
411pub struct ModuleCertificationLimits {
412    /// Maximum blocks in one expanded definition, including both branch arms
413    /// and child Ports before seam removal. Also caps the aggregate blocks
414    /// retained by one guarded topology's projection cache and local views.
415    pub max_expanded_blocks: usize,
416    /// Maximum module instances in one expanded definition, including its root.
417    pub max_expanded_instances: usize,
418    /// Maximum sum of occupied footprint cells in one expanded definition.
419    /// Overlapping branch alternatives count separately. Also caps the aggregate
420    /// retained footprints in one guarded topology's projection cache and local views.
421    pub max_occupied_cells: usize,
422    /// Maximum ZX columns in one leaf definition.
423    pub max_local_columns: usize,
424    /// Maximum allocated nodes in one Boolean arena, including dead intermediates
425    /// until explicit garbage collection.
426    pub max_boolean_nodes: usize,
427    /// Maximum cumulative Boolean, row, and query work in one decision arena.
428    /// Unlimited by default; set a finite value for an explicit work budget.
429    pub max_boolean_steps: usize,
430    /// Maximum accounted dense matrix storage, in 64-bit words.
431    pub max_matrix_words: usize,
432    /// Maximum public interface or live composition width. Independent parent
433    /// connectors are projected and joined separately under the same limit.
434    pub max_frontier_width: usize,
435    /// Maximum nodes in one correlation-witness proof arena.
436    /// Guarded readout planning reclaims dead functions at reduction boundaries
437    /// before checking this limit; transient operations may allocate more.
438    pub max_witness_nodes: usize,
439    /// Maximum attempts in one normalization pass. Internal prefixes, rejected
440    /// candidates, and terminal solves are charged before affine work.
441    pub max_normalization_states: usize,
442    /// Maximum distinct patterns in one guarded local topology/Pauli query.
443    /// Continuing branches never enumerate the joint selector assignment space.
444    pub max_guarded_domain_size: usize,
445}
446
447impl ModuleCertificationLimits {
448    /// Production limits. Cumulative Boolean work is unlimited; allocation,
449    /// materialization, and normalization-search limits remain finite.
450    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    /// Disables all explicit certification limits.
465    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    /// Field names accepted by [`set`](Self::set).
480    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    /// Configuration guidance included in certification resource diagnostics.
495    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    /// Sets a certification limit by its public field name.
498    ///
499    /// `usize::MAX` disables that cap; zero refuses any use of that resource.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`UnknownCertificationLimit`] for a name outside [`FIELD_NAMES`](Self::FIELD_NAMES),
504    /// without changing any limit.
505    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    /// Returns the Boolean arena limits used during certification.
525    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/// A configuration names no field in [`ModuleCertificationLimits`].
540#[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    /// Unrecognized limit field name.
547    pub String,
548);
549
550/// A certified signed boundary projection for one leaf module.
551#[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    /// Returns the certified module definition name.
560    pub fn module(&self) -> &str {
561        &self.module
562    }
563
564    /// Exact Pauli rows over quantum ports in interface declaration order.
565    pub fn boundary_rows(&self) -> impl ExactSizeIterator<Item = &PhasedPauliString> {
566        self.table.boundary_rows.iter().map(|row| &row.signed)
567    }
568
569    /// The independently certified public stabilizers for this definition.
570    ///
571    /// Module lowering reuses the named measurement rows instead of asking a
572    /// flattened parent graph to choose new representatives across the child
573    /// boundary.
574    pub fn stabilizers(&self) -> &crate::StabilizerGenerators {
575        &self.stabilizers
576    }
577
578    /// Expands one retained witness into the leaf ZX graph's dense column order.
579    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/// Error from certifying one independently authored module.
586#[derive(Debug, Error)]
587#[non_exhaustive]
588pub enum ModuleCertificationError {
589    /// The requested definition does not exist.
590    #[error("unknown module '{0}'")]
591    UnknownModule(String),
592    /// Leaf certification was requested for a definition with children.
593    #[error("module '{0}' contains child instances")]
594    NotLeaf(String),
595    /// A definition uses a feature unsupported by leaf certification.
596    #[error("module '{module}' uses unsupported {feature}")]
597    Unsupported {
598        /// Definition being certified.
599        module: String,
600        /// Unsupported feature name.
601        feature: &'static str,
602    },
603    /// An authored quantum connection is invalid.
604    #[error("module '{module}': {message}")]
605    InvalidConnection {
606        /// Definition containing the connection.
607        module: String,
608        /// Connection validation diagnostic.
609        message: String,
610    },
611    /// Definition-local graph construction failed.
612    #[error("module '{module}': {source}")]
613    Graph {
614        /// Definition being certified.
615        module: String,
616        /// Underlying graph failure.
617        #[source]
618        source: crate::BlockGraphError,
619    },
620    /// Runtime-basis projection failed.
621    #[error("module '{module}': {source}")]
622    Runtime {
623        /// Definition being certified.
624        module: String,
625        /// Underlying runtime-basis failure.
626        #[source]
627        source: RuntimeBasisError,
628    },
629    /// Certification exceeded an explicit resource limit.
630    #[error(
631        "module '{module}' exceeded {phase} limit: {observed} > {limit}; {help}",
632        help = ModuleCertificationLimits::RESOURCE_LIMIT_HELP
633    )]
634    ResourceLimited {
635        /// Definition being certified.
636        module: String,
637        /// Certification phase or resource name.
638        phase: &'static str,
639        /// Resource amount required or observed.
640        observed: usize,
641        /// Configured maximum.
642        limit: usize,
643    },
644}
645
646/// Error while loading or validating a module program.
647#[derive(Debug, Error)]
648#[non_exhaustive]
649pub enum ModuleError {
650    /// BLOG parsing or graph lowering failed.
651    #[error("{0}")]
652    Parse(#[from] crate::ParseError),
653    /// An imported source could not be loaded.
654    #[error("cannot load {path}: {message}")]
655    Load {
656        /// Import path that could not be loaded.
657        path: String,
658        /// Loader diagnostic.
659        message: String,
660    },
661    /// Imports form a recursive load cycle.
662    #[error("import cycle at {path}")]
663    ImportCycle {
664        /// Path that closes the cycle.
665        path: String,
666    },
667    /// Source with imports was parsed without a loader.
668    #[error("imports require BlockGraph::load or an explicit source resolver")]
669    ImportsRequireLoader,
670    /// Two definitions use the same module name.
671    #[error("duplicate module '{0}'")]
672    DuplicateModule(String),
673    /// The program does not define its required `main` entry module.
674    #[error("program must define `module main`")]
675    MissingMain,
676    /// A module declaration, interface, or connection is invalid.
677    #[error("module '{module}': {message}")]
678    InvalidModule {
679        /// Definition containing the error.
680        module: String,
681        /// Validation diagnostic.
682        message: String,
683        /// Responsible source range, when known.
684        span: Option<Span>,
685    },
686    /// Module geometry or certification is invalid.
687    #[error("{source}")]
688    InvalidGeometry {
689        /// Definition containing the error.
690        module: String,
691        /// Underlying certification failure.
692        #[source]
693        source: ModuleCertificationError,
694        /// Responsible source range, when known.
695        span: Option<Span>,
696    },
697}
698
699impl ModuleError {
700    /// Source range responsible for this error, when parsed from BLOG text.
701    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    /// Required name of the executable root module.
729    pub const ENTRY_MODULE: &'static str = "main";
730
731    /// Installs one definition's interface and child composition on its local graph.
732    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    /// Adds inferred quantum and classical ports to a procedurally authored graph.
750    ///
751    /// # Errors
752    ///
753    /// Returns a typed interface error when Port directions cannot be inferred.
754    ///
755    /// # Panics
756    ///
757    /// Panics only if the generated Port-name sequence is internally exhausted.
758    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    /// Constructs a hierarchy rooted at `main`, preserving each definition once.
800    ///
801    /// # Errors
802    ///
803    /// Returns a typed declaration, geometry, dependency, or resource-limit error.
804    pub fn from_definitions(definitions: Vec<Self>) -> Result<Self, ModuleError> {
805        Self::from_definitions_with_limits(definitions, ModuleCertificationLimits::DEFAULT)
806    }
807
808    /// Constructs a hierarchy with explicit source and expansion limits.
809    ///
810    /// # Errors
811    ///
812    /// Returns a typed declaration, geometry, dependency, or resource-limit error.
813    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    /// Borrows every named definition, including this root, without copying topology.
837    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    /// Mutably borrows one named definition for edits checked by validation.
848    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    /// Returns the executable root; its local topology is stored directly here.
859    pub fn root(&self) -> &Self {
860        self
861    }
862
863    /// Borrows one named definition, including the root.
864    pub fn module(&self, name: &str) -> Option<&Self> {
865        self.modules().find(|module| module.name == name)
866    }
867
868    /// Borrows this definition's local topology and actions.
869    pub fn local_body(&self) -> &Self {
870        self
871    }
872
873    /// Copies one definition without copying the root's helper library.
874    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    /// Whether explicitly authored interfaces or child composition are present.
886    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    /// Explicitly materializes the authored hierarchy as one flat projection.
897    ///
898    /// # Errors
899    ///
900    /// Returns an expansion or graph-construction error.
901    pub fn flatten(&self) -> Result<Self, ModuleCertificationError> {
902        self.flatten_with_limits(ModuleCertificationLimits::DEFAULT)
903    }
904
905    /// Materializes one flat projection without resetting explicit source budgets.
906    ///
907    /// # Errors
908    ///
909    /// Returns a typed invalid-hierarchy, graph, or resource-limit error.
910    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    /// Materializes the root hierarchy as one transient block graph.
923    ///
924    /// This is the topology bridge for consumers whose output remains flat.
925    /// Definitions are expanded once per instance path, child actions are
926    /// namespaced, connected child Ports are removed, and explicit connectors
927    /// become ordinary pipes. Logical module certification remains on the
928    /// summary path; this method does not derive stabilizers.
929    ///
930    /// Classical bindings become explicit namespaced `Let` actions. Child-owned
931    /// structural branches retain both arms with names qualified by instance
932    /// path.
933    ///
934    /// # Errors
935    ///
936    /// Returns [`ModuleCertificationError::Graph`] when expansion cannot
937    /// construct a valid flat topology.
938    #[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    /// Materializes a module hierarchy for consumers that still need one flat graph.
949    #[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    /// Checks expanded definition sizes without materializing or revalidating geometry.
960    ///
961    /// # Errors
962    ///
963    /// Returns an error when an expanded resource count exceeds `limits`.
964    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    /// Validates all authored definitions, interfaces, seams, and dependency cycles.
998    ///
999    /// # Errors
1000    ///
1001    /// Returns typed source, module, geometry, or resource-limit errors.
1002    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    /// Extracts an independently compilable graph rooted at one named definition.
1026    ///
1027    /// Includes its transitive helper definitions and renames the selected root
1028    /// to `main`, resolving any collision with a reachable original root.
1029    ///
1030    /// # Errors
1031    ///
1032    /// Returns a typed missing-definition, invalid-hierarchy, or resource error.
1033    pub fn extract_definition(&self, name: &str) -> Result<Self, ModuleError> {
1034        self.extract_definition_with_limits(name, ModuleCertificationLimits::DEFAULT)
1035    }
1036
1037    /// Extracts one definition and its dependencies with explicit source budgets.
1038    ///
1039    /// # Errors
1040    ///
1041    /// Returns a typed missing-definition, invalid-hierarchy, or resource error.
1042    ///
1043    /// # Panics
1044    ///
1045    /// Panics if validated dependency bookkeeping is internally inconsistent.
1046    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    /// Certifies one definition without flattening its ZX rows.
1105    ///
1106    /// # Errors
1107    ///
1108    /// Returns an error if the definition is missing or cannot be certified within `limits`.
1109    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    /// Canonical BLOG 1.0 text. Resolved imports are written inline.
1220    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    /// Collision-free implementation keys for independently cacheable
1234    /// definitions. Each key includes every transitive dependency once in
1235    /// canonical order, so changing a child invalidates its parents without
1236    /// expanding shared definitions into an exponential tree of strings.
1237    #[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    /// Global orientations in which each reachable definition is instantiated.
1277    #[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    /// Definition-local block faces whose Hadamard bit is supplied by a
1306    /// consumer-side direct module seam. Physical objects carry both variants
1307    /// for these faces and let the linker select one.
1308    #[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                    // A composite may forward this public Port directly into a
1317                    // child. The descendant definition owns the mutable face.
1318                    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/// One linked definition instance before its parent consumes public ports.
1350/// Physical object linkers use this record to compose a definition DAG without
1351/// invoking whole-program materialization.
1352#[doc(hidden)]
1353#[derive(Debug, Clone)]
1354pub struct LinkedModuleDefinition {
1355    pub graph: BlockGraph,
1356    /// Fully qualified module scope for each linked action, in action order.
1357    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/// Links `module` and every descendant instance into one flat
1364/// graph, qualifying names under `prefix`.
1365#[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/// A transient linker index for the regions whose arm block or cut a new pipe
1411/// can update. Ordinary graph edits keep the full branch scan.
1412#[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        // A new cut made during linking touches an indexed shown-arm block;
1427        // arm membership does not change here. Existing cut keys still need
1428        // indexing for Hadamard repair even without a shown-arm endpoint.
1429        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        // Keep the ordinary typed error for an invalid or missing endpoint.
1469        graph.try_add_pipe(pipe)
1470    }
1471}
1472
1473/// Deterministically link one definition from resolved child object records.
1474/// No stabilizer solve, signature discovery, or template compilation occurs.
1475fn 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        // Consumed Ports are replaced by parent bindings below. Their pipes
1565        // may be branch cuts, so this temporary removal must retain the arms.
1566        let child_branches = std::mem::take(&mut child.branches);
1567        for (endpoint, position, neighbor, pipe) in child_ports {
1568            // A consumed Port has one wire, selected by either target syntax.
1569            // Move its correction to the retained endpoint in the source frame,
1570            // before the parent binding can add another Hadamard.
1571            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                    // A bound parent block occupies the removed Port position.
1606                    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
1902/// Joins a module instance prefix and a local name with the `__` separator
1903/// used throughout module linking. An empty prefix returns `name` unchanged.
1904pub 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    // Index only definitions used by this module. Repeated instance ports
2043    // then require one lookup rather than another scan of the child interface.
2044    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            // A parent Port can inherit its pipe from a bound child interface.
2382            // Definition geometry validation checks that complete connection.
2383            [] => 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                // The child output X crosses its original H to become Z at the
2924                // retained endpoint. The later binding H must not change this action.
2925                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}