Skip to main content

bloq_graph/
composition.rs

1//! Validated module placement and docking shared with the editor.
2use crate::{
3    Basis, BitBinding, BitOutput, BitRef, Block, BlockGraph, BlockGraphError, BlockKind, Expr,
4    InstancePort, ModuleCertificationError, ModuleCertificationLimits, ModuleError, ModuleInstance,
5    ModuleRotation, PortDirection, PortRole, QuantumConnection, QuantumPort, checked_add_position,
6    flatten_module_definition,
7};
8use glam::IVec3;
9use std::collections::{BTreeSet, HashSet};
10use std::sync::Arc;
11
12fn invalid(message: impl Into<String>) -> BlockGraphError {
13    source_error(ModuleError::InvalidModule {
14        module: BlockGraph::ENTRY_MODULE.into(),
15        message: message.into(),
16        span: None,
17    })
18}
19fn source_error(error: ModuleError) -> BlockGraphError {
20    BlockGraphError::ModuleSource(Arc::new(error))
21}
22fn materialization(error: ModuleCertificationError) -> BlockGraphError {
23    BlockGraphError::ModuleMaterialization(Arc::new(error))
24}
25macro_rules! fail { ($($arg:tt)*) => { return Err(invalid(format!($($arg)*))) }; }
26macro_rules! ensure { ($condition:expr, $($arg:tt)*) => { if !$condition { fail!($($arg)*); } }; }
27
28/// Docking preferences for module composition.
29#[derive(Debug, Clone, Copy)]
30pub struct ModuleJoinOptions {
31    /// Align the input instance's connected group to the output port.
32    pub align: bool,
33    /// Prefer direct seams; retain connection cubes when compaction is obstructed.
34    pub compact: bool,
35    /// Apply an extra Hadamard at the explicitly selected seam.
36    pub hadamard: bool,
37}
38impl Default for ModuleJoinOptions {
39    fn default() -> Self {
40        Self {
41            align: true,
42            compact: true,
43            hadamard: false,
44        }
45    }
46}
47
48impl BlockGraph {
49    /// Places a child beside the current geometry and exposes its public interface.
50    /// Quantum ports, classical inputs, and classical outputs receive instance-qualified names.
51    /// The graph is unchanged on failure.
52    ///
53    /// # Errors
54    /// Returns a declaration, geometry, coordinate, or certification error.
55    pub fn place_module(&mut self, name: &str, definition: &str) -> Result<IVec3, BlockGraphError> {
56        self.place_module_with(name, definition, None, ModuleRotation::IDENTITY)
57    }
58
59    /// Places a child with optional explicit translation and a local rotation.
60    /// When translation is absent, the child is placed beside the current geometry.
61    ///
62    /// # Errors
63    /// Returns a declaration, geometry, coordinate, or certification error without changing the graph.
64    pub fn place_module_with(
65        &mut self,
66        name: &str,
67        definition: &str,
68        translation: Option<IVec3>,
69        rotation: ModuleRotation,
70    ) -> Result<IVec3, BlockGraphError> {
71        let (next, position) = place_module(self, name, definition, translation, rotation)?;
72        *self = next;
73        Ok(position)
74    }
75
76    /// Aligns and docks two named child ports, joining all other touching compatible ports.
77    /// Names use `instance.port`. Connected input-side instances move together.
78    /// Direct seams preserve the endpoint Hadamards; obstructed compaction retains connection cubes.
79    ///
80    /// # Errors
81    /// Returns a declaration, geometry, coordinate, or certification error without changing the graph.
82    pub fn connect_modules(&mut self, output: &str, input: &str) -> Result<usize, BlockGraphError> {
83        self.connect_modules_with(output, input, ModuleJoinOptions::default())
84    }
85
86    /// Docks named child ports with explicit alignment, compaction, and Hadamard preferences.
87    ///
88    /// # Errors
89    /// Returns a declaration, geometry, coordinate, or certification error without changing the graph.
90    pub fn connect_modules_with(
91        &mut self,
92        output: &str,
93        input: &str,
94        options: ModuleJoinOptions,
95    ) -> Result<usize, BlockGraphError> {
96        self.validate_with_limits(ModuleCertificationLimits::DEFAULT)
97            .map_err(source_error)?;
98        let result = connect_ports(
99            self,
100            endpoint(output)?,
101            endpoint(input)?,
102            options.hadamard,
103            options.align,
104            options.compact,
105        )?;
106        let count = result.connections;
107        *self = result.program;
108        Ok(count)
109    }
110
111    /// Binds a parent value or child bit output to a child's classical input.
112    /// Replaces its existing binding and removes an unused automatically exposed parent input.
113    ///
114    /// # Errors
115    /// Returns a declaration, dependency, or certification error without changing the graph.
116    pub fn bind_modules(&mut self, source: &str, target: &str) -> Result<(), BlockGraphError> {
117        self.validate_with_limits(ModuleCertificationLimits::DEFAULT)
118            .map_err(source_error)?;
119        let target = endpoint(target)?;
120        let source = if source.contains('.') {
121            let source = endpoint(source)?;
122            BitRef {
123                instance: Some(source.instance),
124                bit: source.port,
125            }
126        } else {
127            BitRef {
128                instance: None,
129                bit: source.into(),
130            }
131        };
132        let mut modules = self
133            .modules()
134            .map(BlockGraph::clone_local_definition)
135            .collect::<Vec<_>>();
136        let root = modules
137            .iter_mut()
138            .find(|m| m.name == Self::ENTRY_MODULE)
139            .expect("validated program has main");
140        let binding = root
141            .bit_bindings
142            .iter_mut()
143            .find(|b| b.target_instance == target.instance && b.target_bit == target.port)
144            .ok_or_else(|| invalid("unknown child classical input"))?;
145        let old = std::mem::replace(&mut binding.source, source);
146        prune_unused_input(root, &old)?;
147        *self = BlockGraph::from_definitions(modules).map_err(source_error)?;
148        Ok(())
149    }
150
151    /// Translates an instance's connected group and joins touching compatible free ports.
152    /// Returns the applied offset, including any successful seam compaction.
153    ///
154    /// # Errors
155    /// Returns a declaration, geometry, coordinate, or certification error without changing the graph.
156    pub fn translate_module(
157        &mut self,
158        name: &str,
159        offset: IVec3,
160        compact: bool,
161    ) -> Result<IVec3, BlockGraphError> {
162        self.validate_with_limits(ModuleCertificationLimits::DEFAULT)
163            .map_err(source_error)?;
164        let result = translate_module(self, name, offset, compact)?;
165        let offset = result.offset;
166        *self = result.program;
167        Ok(offset)
168    }
169}
170fn endpoint(text: &str) -> Result<InstancePort, BlockGraphError> {
171    let (instance, port) = text
172        .split_once('.')
173        .filter(|(instance, port)| !instance.is_empty() && !port.is_empty() && !port.contains('.'))
174        .ok_or_else(|| invalid("expected a quantum port name 'instance.port'"))?;
175    Ok(InstancePort {
176        instance: instance.into(),
177        port: port.into(),
178    })
179}
180
181pub fn place_module(
182    program: &BlockGraph,
183    name: &str,
184    definition: &str,
185    translation: Option<IVec3>,
186    rotation: ModuleRotation,
187) -> Result<(BlockGraph, IVec3), BlockGraphError> {
188    program
189        .validate_with_limits(ModuleCertificationLimits::DEFAULT)
190        .map_err(source_error)?;
191    ensure!(
192        definition != BlockGraph::ENTRY_MODULE,
193        "A module cannot contain itself"
194    );
195    let mut modules = program
196        .modules()
197        .map(BlockGraph::clone_local_definition)
198        .collect::<Vec<_>>();
199    let child = program
200        .module(definition)
201        .ok_or_else(|| invalid("Module no longer exists"))?;
202    let child_graph = flatten_module_definition(program, child, "")?.graph;
203    ensure!(
204        !child_graph.is_empty(),
205        "Build some geometry before placing this module"
206    );
207    let translation = match translation {
208        Some(position) => position,
209        None => position_beside(
210            &program.flatten().map_err(materialization)?,
211            &child_graph,
212            rotation,
213        )?,
214    };
215    let root = modules
216        .iter_mut()
217        .find(|m| m.name == BlockGraph::ENTRY_MODULE)
218        .expect("validated program has main");
219    root.interface_declared = true;
220    ensure!(
221        !root.instances.iter().any(|i| i.name == name),
222        "Instance '{name}' already exists"
223    );
224    let instance = ModuleInstance {
225        name: name.into(),
226        definition: definition.into(),
227        translation,
228        rotation,
229    };
230    for port in &child.interface.quantum_ports {
231        expose_port(program, root, &instance, port)?;
232    }
233    for input in &child.interface.bit_inputs {
234        let name = fresh_interface_name(root, &format!("{}_{}", instance.name, input));
235        root.interface.bit_inputs.push(name.clone());
236        root.bit_bindings.push(BitBinding {
237            source: BitRef {
238                instance: None,
239                bit: name,
240            },
241            target_instance: instance.name.clone(),
242            target_bit: input.clone(),
243        });
244    }
245    for output in &child.interface.bit_outputs {
246        root.interface.bit_outputs.push(BitOutput {
247            name: fresh_interface_name(root, &format!("{}_{}", instance.name, output.name)),
248            expr: Expr::Var(format!("{}.{}", instance.name, output.name)),
249        });
250    }
251    root.instances.push(instance);
252
253    Ok((
254        BlockGraph::from_definitions(modules).map_err(source_error)?,
255        translation,
256    ))
257}
258
259#[derive(Debug)]
260pub struct ModuleTranslation {
261    pub program: BlockGraph,
262    pub offset: IVec3,
263    pub connections: usize,
264    pub compacted: bool,
265}
266
267#[derive(Clone, Debug)]
268pub struct ConnectionSeam {
269    pub output: InstancePort,
270    pub input: InstancePort,
271    pub hadamard: bool,
272    pub cube: Option<IVec3>,
273}
274
275/// Recognize ordinary direct seams and isolated parent cubes joining two ports.
276/// Authored junctions and local pipes are deliberately left to the definition editor.
277pub fn connection_seams(root: &BlockGraph) -> Vec<ConnectionSeam> {
278    let mut seams = Vec::new();
279    for connection in &root.quantum_connections {
280        match connection {
281            QuantumConnection::Pipe {
282                output,
283                input,
284                hadamard,
285            } => seams.push(ConnectionSeam {
286                output: output.clone(),
287                input: input.clone(),
288                hadamard: *hadamard,
289                cube: None,
290            }),
291            QuantumConnection::Output {
292                output,
293                block,
294                hadamard,
295            } => {
296                if !root
297                    .get_block(*block)
298                    .is_some_and(|block| block.kind().is_cube())
299                    || !root.neighbor_positions(*block).is_empty()
300                    || root.shown_branch_at(*block).is_some()
301                {
302                    continue;
303                }
304                let bindings = root
305                    .quantum_connections
306                    .iter()
307                    .filter(|connection| bound_block(connection) == Some(*block))
308                    .collect::<Vec<_>>();
309                if bindings.len() == 2
310                    && let Some((input, input_h)) =
311                        bindings.iter().find_map(|binding| match binding {
312                            QuantumConnection::Input {
313                                input, hadamard, ..
314                            } => Some((input, *hadamard)),
315                            _ => None,
316                        })
317                {
318                    seams.push(ConnectionSeam {
319                        output: output.clone(),
320                        input: input.clone(),
321                        hadamard: *hadamard ^ input_h,
322                        cube: Some(*block),
323                    });
324                }
325            }
326            _ => {}
327        }
328    }
329    seams
330}
331
332pub fn connected_instances(root: &BlockGraph, start: &str) -> BTreeSet<String> {
333    let seams = connection_seams(root);
334    let mut group = BTreeSet::from([start.to_owned()]);
335    let mut pending = vec![start.to_owned()];
336    while let Some(name) = pending.pop() {
337        for seam in &seams {
338            let other = if seam.output.instance == name {
339                &seam.input.instance
340            } else if seam.input.instance == name {
341                &seam.output.instance
342            } else {
343                continue;
344            };
345            if group.insert(other.clone()) {
346                pending.push(other.clone());
347            }
348        }
349    }
350    group
351}
352
353struct OpenPort {
354    endpoint: InstancePort,
355    port: QuantumPort,
356    neighbor: IVec3,
357    bases: [Option<Basis>; 3],
358    hadamard: bool,
359}
360
361struct PortJoin<'a> {
362    output: &'a OpenPort,
363    input: &'a OpenPort,
364    position: IVec3,
365    compact_step: Option<IVec3>,
366    hadamard: bool,
367}
368
369impl PortJoin<'_> {
370    fn cube_kind(&self) -> Result<BlockKind, BlockGraphError> {
371        for kind in BlockKind::all_kinds() {
372            let BlockKind::Cube(cube) = kind else {
373                continue;
374            };
375            let matches = [(self.output, false), (self.input, self.hadamard)]
376                .into_iter()
377                .all(|(port, flip)| {
378                    port.bases
379                        .iter()
380                        .zip(cube.bases())
381                        .all(|(face, cube_face)| {
382                            face.is_none_or(
383                                |face| if flip { face.flip() } else { face } == cube_face,
384                            )
385                        })
386                });
387            if matches {
388                return Ok(kind);
389            }
390        }
391        fail!("Port faces do not match any connection cube; rotate the module or choose Hadamard")
392    }
393}
394
395fn open_ports(program: &BlockGraph) -> Result<Vec<OpenPort>, BlockGraphError> {
396    let graph = program.flatten().map_err(materialization)?;
397    let mut ports = Vec::new();
398    for (endpoint, public) in exposed_ports(program) {
399        let position = public.position;
400        let neighbors = graph.neighbor_positions(position);
401        let [neighbor] = neighbors.as_slice() else {
402            fail!("Port needs one incident pipe")
403        };
404        let pipe = graph
405            .get_pipe(position, *neighbor)
406            .expect("the neighbor came from this position's incident pipes");
407        ports.push(OpenPort {
408            endpoint,
409            port: public.clone(),
410            neighbor: *neighbor,
411            bases: graph.infer_pipe_basis_from_endpoint(pipe, position),
412            hadamard: pipe.is_hadamard(),
413        });
414    }
415    Ok(ports)
416}
417
418pub fn translate_module(
419    program: &BlockGraph,
420    name: &str,
421    offset: IVec3,
422    compact: bool,
423) -> Result<ModuleTranslation, BlockGraphError> {
424    translate_and_connect(program, name, offset, compact, None)
425}
426
427pub fn connect_ports(
428    program: &BlockGraph,
429    output: InstancePort,
430    input: InstancePort,
431    hadamard: bool,
432    align: bool,
433    compact: bool,
434) -> Result<ModuleTranslation, BlockGraphError> {
435    ensure!(
436        output.instance != input.instance,
437        "Choose ports on different instances"
438    );
439    let output_port = exposed_port(program.root(), &output)
440        .ok_or_else(|| invalid("Output is already connected"))?;
441    let input_port = exposed_port(program.root(), &input)
442        .ok_or_else(|| invalid("Input is already connected"))?;
443    ensure!(
444        output_port.direction == PortDirection::Output
445            && input_port.direction == PortDirection::Input,
446        "Connect an output to an input"
447    );
448    let offset = if align {
449        checked_difference(output_port.position, input_port.position)?
450    } else {
451        IVec3::ZERO
452    };
453    let name = input.instance.clone();
454    translate_and_connect(
455        program,
456        &name,
457        offset,
458        compact,
459        Some((output, input, hadamard)),
460    )
461}
462
463fn translate_and_connect(
464    program: &BlockGraph,
465    name: &str,
466    offset: IVec3,
467    compact: bool,
468    requested: Option<(InstancePort, InstancePort, bool)>,
469) -> Result<ModuleTranslation, BlockGraphError> {
470    ensure!(
471        program
472            .root()
473            .instances
474            .iter()
475            .any(|instance| instance.name == name),
476        "Instance no longer exists"
477    );
478    let group = connected_instances(program.root(), name);
479    let ports = open_ports(program)?;
480    let mut pairs = Vec::new();
481    for moving in ports
482        .iter()
483        .filter(|port| group.contains(&port.endpoint.instance))
484    {
485        let position = checked_add_position(moving.port.position, offset)?;
486        for fixed in ports
487            .iter()
488            .filter(|port| !group.contains(&port.endpoint.instance))
489        {
490            if position != fixed.port.position {
491                continue;
492            }
493            ensure!(
494                moving.port.direction != fixed.port.direction,
495                "Ports '{}.{}' and '{}.{}' must join an output to an input",
496                moving.endpoint.instance,
497                moving.endpoint.port,
498                fixed.endpoint.instance,
499                fixed.endpoint.port
500            );
501            ensure!(
502                moving.port.resource_type == fixed.port.resource_type,
503                "Touching ports have different resource types"
504            );
505            let (output, input) = if moving.port.direction == PortDirection::Output {
506                (moving, fixed)
507            } else {
508                (fixed, moving)
509            };
510            let hadamard = requested
511                .as_ref()
512                .is_some_and(|(a, b, h)| *h && a == &output.endpoint && b == &input.endpoint);
513            let step = checked_difference(moving.port.position, moving.neighbor)?;
514            pairs.push(PortJoin {
515                output,
516                input,
517                hadamard,
518                position: fixed.port.position,
519                compact_step: (fixed.neighbor - fixed.port.position == step).then_some(step),
520            });
521        }
522    }
523    if let Some((output, input, _)) = requested {
524        ensure!(
525            pairs
526                .iter()
527                .any(|join| join.output.endpoint == output && join.input.endpoint == input),
528            "Ports do not meet; enable Align input instance or move them together first"
529        );
530    }
531    let build = |delta: IVec3, compacted: bool| -> Result<BlockGraph, BlockGraphError> {
532        let mut modules = program
533            .modules()
534            .map(BlockGraph::clone_local_definition)
535            .collect::<Vec<_>>();
536        let root = modules
537            .iter_mut()
538            .find(|module| module.name == BlockGraph::ENTRY_MODULE)
539            .expect("validated program contains the entry module");
540        for join in &pairs {
541            consume_exposed_port(root, &join.output.endpoint)?;
542            consume_exposed_port(root, &join.input.endpoint)?;
543        }
544        translate_group(root, &group, delta)?;
545        for join in &pairs {
546            let PortJoin {
547                output,
548                input,
549                hadamard,
550                ..
551            } = join;
552            if compacted {
553                root.quantum_connections.push(QuantumConnection::Pipe {
554                    output: output.endpoint.clone(),
555                    input: input.endpoint.clone(),
556                    hadamard: output.hadamard ^ input.hadamard ^ *hadamard,
557                });
558            } else {
559                let block = join.position;
560                root.try_add_block(Block::new(block, join.cube_kind()?))?;
561                root.quantum_connections.extend([
562                    QuantumConnection::Output {
563                        output: output.endpoint.clone(),
564                        block,
565                        hadamard: false,
566                    },
567                    QuantumConnection::Input {
568                        block,
569                        input: input.endpoint.clone(),
570                        hadamard: *hadamard,
571                    },
572                ]);
573            }
574        }
575        ensure!(
576            root.actions() == program.root().local_body().actions()
577                && root.branch_definitions() == program.root().local_body().branch_definitions(),
578            "Move would change parent actions or branches; edit the owning definition instead"
579        );
580        BlockGraph::from_definitions(modules).map_err(source_error)
581    };
582    if compact
583        && let Some(step) = pairs.first().and_then(|join| join.compact_step)
584        && pairs.iter().all(|join| join.compact_step == Some(step))
585        && let Ok(compact_offset) = checked_add_position(offset, step)
586    {
587        match build(compact_offset, true) {
588            Ok(program) => {
589                return Ok(ModuleTranslation {
590                    program,
591                    offset: compact_offset,
592                    connections: pairs.len(),
593                    compacted: true,
594                });
595            }
596            Err(error) => {
597                let mut cause: Option<&dyn std::error::Error> = Some(&error);
598                while let Some(current) = cause {
599                    if matches!(
600                        current.downcast_ref::<ModuleCertificationError>(),
601                        Some(ModuleCertificationError::ResourceLimited { .. })
602                    ) {
603                        return Err(error);
604                    }
605                    cause = current.source();
606                }
607            }
608        }
609    }
610    Ok(ModuleTranslation {
611        program: build(offset, false)?,
612        offset,
613        connections: pairs.len(),
614        compacted: false,
615    })
616}
617
618/// Move a connected group and its free public ports/shared connector cubes in
619/// one transaction, so old positions cannot collide with each other mid-move.
620///
621/// The graph is unchanged if a shifted coordinate or placement is invalid.
622pub fn translate_group(
623    graph: &mut BlockGraph,
624    group: &BTreeSet<String>,
625    offset: IVec3,
626) -> Result<(), BlockGraphError> {
627    let mut root = graph.clone();
628    let mut positions = connection_seams(&root)
629        .iter()
630        .filter(|seam| {
631            group.contains(&seam.output.instance) && group.contains(&seam.input.instance)
632        })
633        .filter_map(|seam| seam.cube)
634        .collect::<HashSet<_>>();
635    for connection in &root.quantum_connections {
636        let endpoint = match connection {
637            QuantumConnection::Input {
638                input: endpoint, ..
639            }
640            | QuantumConnection::Output {
641                output: endpoint, ..
642            } => endpoint,
643            _ => continue,
644        };
645        if group.contains(&endpoint.instance)
646            && let Some(port) = exposed_port(&root, endpoint)
647        {
648            positions.insert(port.position);
649        }
650    }
651    let mut blocks = positions
652        .iter()
653        .map(|position| {
654            root.get_block(*position)
655                .expect("validated interface ports and binds sit on body blocks")
656                .clone()
657        })
658        .collect::<Vec<_>>();
659    blocks.sort_by_key(|block| block.pos().to_array());
660    for position in &positions {
661        root.remove_block(*position);
662    }
663    for block in blocks {
664        root.try_add_block(block.try_with_shift(offset)?)?;
665    }
666    for port in &mut root.interface.quantum_ports {
667        if positions.contains(&port.position) {
668            port.position = checked_add_position(port.position, offset)?;
669        }
670    }
671    for connection in &mut root.quantum_connections {
672        if let QuantumConnection::Input { block, .. } | QuantumConnection::Output { block, .. } =
673            connection
674            && positions.contains(block)
675        {
676            *block = checked_add_position(*block, offset)?;
677        }
678    }
679    for instance in &mut root.instances {
680        if group.contains(&instance.name) {
681            instance.translation = checked_add_position(instance.translation, offset)?;
682        }
683    }
684    *graph = root;
685    Ok(())
686}
687
688pub fn unique_name(base: &str, used: &HashSet<String>) -> String {
689    if !used.contains(base) {
690        return base.to_owned();
691    }
692    (2..)
693        .map(|i| format!("{base}_{i}"))
694        .find(|name| !used.contains(name))
695        .expect("an unbounded counter always yields a name outside a finite set")
696}
697
698pub fn fresh_interface_name(module: &BlockGraph, base: &str) -> String {
699    let used = module
700        .interface
701        .quantum_ports
702        .iter()
703        .map(|p| p.name.clone())
704        .chain(module.interface.bit_inputs.iter().cloned())
705        .chain(module.interface.bit_outputs.iter().map(|p| p.name.clone()))
706        .collect();
707    unique_name(base, &used)
708}
709
710pub fn expose_port(
711    program: &BlockGraph,
712    root: &mut BlockGraph,
713    instance: &ModuleInstance,
714    port: &QuantumPort,
715) -> Result<(), BlockGraphError> {
716    let mut public = port.clone();
717    public.position = instance.try_transform_position(port.position)?;
718    public.name = fresh_interface_name(root, &format!("{}_{}", instance.name, port.name));
719    let prototype = program
720        .module(&instance.definition)
721        .expect("validated program resolves every instance definition")
722        .get_block(port.position)
723        .expect("validated interface port sits on a body block");
724    let role = if prototype.port_role() == Some(PortRole::Multiplex) {
725        PortRole::Multiplex
726    } else {
727        match port.direction {
728            PortDirection::Input => PortRole::Input,
729            PortDirection::Output => PortRole::Output,
730        }
731    };
732    root.try_add_block(
733        prototype
734            .try_with_shift(checked_difference(public.position, port.position)?)?
735            .with_port_role(role)?
736            .with_tag(public.name.clone())?,
737    )?;
738    let endpoint = InstancePort {
739        instance: instance.name.clone(),
740        port: port.name.clone(),
741    };
742    root.quantum_connections.push(match port.direction {
743        PortDirection::Input => QuantumConnection::Input {
744            block: public.position,
745            input: endpoint,
746            hadamard: false,
747        },
748        PortDirection::Output => QuantumConnection::Output {
749            block: public.position,
750            output: endpoint,
751            hadamard: false,
752        },
753    });
754    root.interface.quantum_ports.push(public);
755    Ok(())
756}
757
758pub fn exposed_port<'a>(root: &'a BlockGraph, endpoint: &InstancePort) -> Option<&'a QuantumPort> {
759    let position = root
760        .quantum_connections
761        .iter()
762        .find_map(|connection| match connection {
763            QuantumConnection::Input {
764                block,
765                input: port,
766                hadamard: false,
767            }
768            | QuantumConnection::Output {
769                block,
770                output: port,
771                hadamard: false,
772            } if port == endpoint => Some(*block),
773            _ => None,
774        })?;
775    (root.neighbor_positions(position).is_empty()
776        && root
777            .quantum_connections
778            .iter()
779            .filter(|c| bound_block(c) == Some(position))
780            .count()
781            == 1)
782        .then(|| {
783            root.interface
784                .quantum_ports
785                .iter()
786                .find(|p| p.position == position)
787        })
788        .flatten()
789}
790
791/// Free instance ports in declaration order, paired with their public interface.
792pub fn exposed_ports(program: &BlockGraph) -> impl Iterator<Item = (InstancePort, &QuantumPort)> {
793    program.root().instances.iter().flat_map(move |instance| {
794        program
795            .module(&instance.definition)
796            .expect("validated program resolves every instance definition")
797            .interface
798            .quantum_ports
799            .iter()
800            .filter_map(move |port| {
801                let endpoint = InstancePort {
802                    instance: instance.name.clone(),
803                    port: port.name.clone(),
804                };
805                exposed_port(program.root(), &endpoint).map(|port| (endpoint, port))
806            })
807    })
808}
809
810fn consume_exposed_port(
811    root: &mut BlockGraph,
812    endpoint: &InstancePort,
813) -> Result<(), BlockGraphError> {
814    let position = exposed_port(root, endpoint)
815        .ok_or_else(|| invalid("Port is already connected or bound to local geometry"))?
816        .position;
817    root.quantum_connections
818        .retain(|c| bound_block(c) != Some(position));
819    remove_public_port(root, position);
820    Ok(())
821}
822
823pub fn remove_public_port(root: &mut BlockGraph, position: IVec3) {
824    if root
825        .interface
826        .quantum_ports
827        .iter()
828        .any(|p| p.position == position)
829    {
830        root.interface
831            .quantum_ports
832            .retain(|p| p.position != position);
833        root.remove_block(position);
834    }
835}
836
837pub fn bound_block(connection: &QuantumConnection) -> Option<IVec3> {
838    match connection {
839        QuantumConnection::Input { block, .. } | QuantumConnection::Output { block, .. } => {
840            Some(*block)
841        }
842        QuantumConnection::Pipe { .. } => None,
843    }
844}
845
846fn checked_difference(a: IVec3, b: IVec3) -> Result<IVec3, BlockGraphError> {
847    let error = || BlockGraphError::CoordinateOverflow {
848        position: a,
849        offset: b,
850    };
851    Ok(IVec3::new(
852        a.x.checked_sub(b.x).ok_or_else(error)?,
853        a.y.checked_sub(b.y).ok_or_else(error)?,
854        a.z.checked_sub(b.z).ok_or_else(error)?,
855    ))
856}
857
858fn position_beside(
859    graph: &BlockGraph,
860    child: &BlockGraph,
861    rotation: ModuleRotation,
862) -> Result<IVec3, BlockGraphError> {
863    let Some((x, _, _)) = graph.spans() else {
864        return Ok(IVec3::ZERO);
865    };
866    let (child_x, child_y, child_z) = child
867        .spans()
868        .ok_or_else(|| invalid("Module has no geometry"))?;
869    let mut min_x = i32::MAX;
870    for cx in [*child_x.start(), *child_x.end()] {
871        for cy in [*child_y.start(), *child_y.end()] {
872            for cz in [*child_z.start(), *child_z.end()] {
873                min_x = min_x.min(rotation.try_rotate_position(IVec3::new(cx, cy, cz))?.x);
874            }
875        }
876    }
877    let x = x
878        .end()
879        .checked_sub(min_x)
880        .and_then(|x| x.checked_add(3))
881        .ok_or_else(|| invalid("No coordinate space beside this composition"))?;
882    Ok(IVec3::new(x, 0, 0))
883}
884
885pub fn prune_unused_input(root: &mut BlockGraph, source: &BitRef) -> Result<(), BlockGraphError> {
886    if source.instance.is_none()
887        && !root.bit_bindings.iter().any(|b| b.source == *source)
888        && !root
889            .actions()
890            .iter()
891            .any(|action| action.referenced_names().contains(source.bit.as_str()))
892        && !root
893            .interface
894            .bit_outputs
895            .iter()
896            .any(|output| expr_uses(&output.expr, &source.bit))
897    {
898        let inputs = root
899            .action_graph()
900            .inputs()
901            .filter(|name| *name != source.bit)
902            .map(str::to_owned)
903            .collect::<Vec<_>>();
904        root.set_actions_deferred_with_inputs(root.actions(), inputs)?;
905        root.interface.bit_inputs.retain(|name| name != &source.bit);
906    }
907    Ok(())
908}
909
910fn expr_uses(expr: &Expr, name: &str) -> bool {
911    match expr {
912        Expr::Var(value) => value == name,
913        Expr::Not(value) => expr_uses(value, name),
914        Expr::Binary(_, a, b) => expr_uses(a, name) || expr_uses(b, name),
915    }
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921    use crate::{GalleryItem, UDirection};
922
923    fn cnot_program() -> BlockGraph {
924        let mut definition = GalleryItem::CNOT.build();
925        definition.name = "CNOT".into();
926        BlockGraph::from_definitions(vec![definition, BlockGraph::new()]).unwrap()
927    }
928
929    #[test]
930    fn placement_docking_and_group_translation_preserve_the_library() {
931        let mut graph = cnot_program();
932        let original = graph.module("CNOT").unwrap().to_blog_text();
933        for index in 0..3 {
934            graph
935                .place_module(&format!("child{index}"), "CNOT")
936                .unwrap();
937        }
938        assert_eq!(
939            graph
940                .connect_modules("child0.control_out", "child1.control_in")
941                .unwrap(),
942            2
943        );
944        assert_eq!(
945            graph
946                .connect_modules("child1.control_out", "child2.control_in")
947                .unwrap(),
948            2
949        );
950        assert_eq!(graph.interface.quantum_ports.len(), 4);
951        assert_eq!(
952            graph
953                .instances
954                .iter()
955                .map(|i| i.translation)
956                .collect::<Vec<_>>(),
957            [IVec3::ZERO, IVec3::new(0, 0, 2), IVec3::new(0, 0, 4)]
958        );
959        assert_eq!(graph.module("CNOT").unwrap().to_blog_text(), original);
960        assert_eq!(
961            graph
962                .translate_module("child0", IVec3::Y * 3, true)
963                .unwrap(),
964            IVec3::Y * 3
965        );
966        assert!(graph.instances.iter().all(|i| i.translation.y == 3));
967        assert_eq!(graph.module("CNOT").unwrap().to_blog_text(), original);
968        let restored = BlockGraph::from_text(&graph.to_blog_text()).unwrap();
969        assert_eq!(restored.interface, graph.interface);
970        assert_eq!(restored.instances, graph.instances);
971        assert_eq!(restored.quantum_connections, graph.quantum_connections);
972        for block in graph.blocks() {
973            assert_eq!(restored.get_block(block.pos()), Some(block));
974        }
975    }
976
977    #[test]
978    fn failed_group_translation_preserves_the_graph() {
979        let mut graph = cnot_program();
980        graph.place_module("child", "CNOT").unwrap();
981        graph.add_block(Block::new(IVec3::X * 10, BlockKind::Port));
982        let before = graph.to_blog_text();
983        let group = BTreeSet::from(["child".to_owned()]);
984        for offset in [IVec3::splat(i32::MAX), IVec3::X * 10] {
985            translate_group(&mut graph, &group, offset)
986                .expect_err("shifted boundary coordinates overflow or collide");
987            assert_eq!(graph.to_blog_text(), before);
988        }
989    }
990
991    #[test]
992    fn failed_edits_are_atomic_and_rotated_placement_stays_beside_the_graph() {
993        let mut graph = cnot_program();
994        graph.place_module("child0", "CNOT").unwrap();
995        graph
996            .place_module_with(
997                "child1",
998                "CNOT",
999                None,
1000                ModuleRotation::new(UDirection::Z, 2),
1001            )
1002            .unwrap();
1003        let before = graph.to_blog_text();
1004        for (a, b) in [
1005            ("child0.control_in", "child1.control_in"),
1006            ("child0.control_out", "missing.control_in"),
1007            ("child0", "child1.control_in"),
1008        ] {
1009            graph
1010                .connect_modules(a, b)
1011                .expect_err("invalid endpoints must fail");
1012            assert_eq!(graph.to_blog_text(), before);
1013        }
1014        graph
1015            .place_module("child0", "CNOT")
1016            .expect_err("duplicate instance must fail");
1017        graph
1018            .place_module("new", "missing")
1019            .expect_err("missing definition must fail");
1020        graph
1021            .place_module_with(
1022                "new",
1023                "CNOT",
1024                Some(IVec3::splat(i32::MAX)),
1025                ModuleRotation::IDENTITY,
1026            )
1027            .expect_err("coordinate overflow must fail");
1028        assert_eq!(graph.to_blog_text(), before);
1029    }
1030}