Skip to main content

bloq_ir/
edit.rs

1//! Post-compile seam edits.
2//!
3//! Edge subdivision is verified against persisted template boundary flows
4//! before the graph is mutated, so failed verification leaves it unchanged.
5
6use std::cmp::Ordering;
7
8use bloq_circuit::{ComposedChain, DetectorCoords, DetectorTerm, FlowEngine, OffsetFlows, Op};
9use glam::IVec2;
10use petgraph::Direction;
11use petgraph::stable_graph::NodeIndex;
12use petgraph::visit::EdgeRef;
13use thiserror::Error;
14
15use crate::{
16    Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BloqTemplate, BloqTemplatePool,
17    BoundaryFace, ClassicalNode, FxMap, FxSet, InstanceMeasurement, LevelPath, NodeDetector,
18    NodeRestart, PipePadding, PipeSeam, QuantumEdge, SubGraph, TemplateId, TemplateInstance,
19    TemplateInstanceId, TemporalPipeRef,
20};
21
22/// Why a post-compile graph edit was rejected.
23#[derive(Debug, Error, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum EditError {
26    /// A detector bundle use is malformed.
27    #[error("{0}")]
28    DetectorBundle(#[from] crate::DetectorBundleError),
29    /// Conditional membership must be pinned first.
30    #[error("memory edits require pinned component membership; call Bloq::pin_membership first")]
31    MembershipSelectionRequired,
32    /// Boolean membership analysis failed.
33    #[error("{0}")]
34    MembershipAnalysis(#[from] crate::NodeTemplateInstanceMergeError),
35    /// A level path no longer resolves.
36    #[error("memory-round target has invalid level path {path:?}")]
37    InvalidLevelPath {
38        /// Invalid level path.
39        path: LevelPath,
40    },
41    /// The target node is missing.
42    #[error("memory-round target node {node:?} does not exist at {path:?}")]
43    MemoryRoundTargetMissing {
44        /// Target level.
45        path: LevelPath,
46        /// Missing node.
47        node: BloqNodeId,
48    },
49    /// The target node is not quantum.
50    #[error("memory-round target node {node:?} at {path:?} is not quantum")]
51    MemoryRoundTargetNotQuantum {
52        /// Target level.
53        path: LevelPath,
54        /// Non-quantum node.
55        node: BloqNodeId,
56    },
57    /// The endpoints do not identify one editable quantum seam.
58    #[error(
59        "quantum edge {from:?} -> {to:?} is not a single non-empty seam between quantum nodes or into a region"
60    )]
61    InvalidQuantumEdge {
62        /// Source node.
63        from: BloqNodeId,
64        /// Target node.
65        to: BloqNodeId,
66    },
67    /// Requested padding is empty.
68    #[error("padding must be non-empty")]
69    EmptyPadding,
70    /// No template-instance id remains.
71    #[error("not enough template-instance ids remain for padding")]
72    TemplateInstanceIdExhausted,
73    /// A referenced template is absent.
74    #[error("padding references unknown template {template:?}")]
75    UnknownTemplate {
76        /// Missing template id.
77        template: TemplateId,
78    },
79    /// Boundary-flow composition failed.
80    #[error("seam flow composition failed: {source}")]
81    SeamCompositionFailed {
82        /// Underlying flow error.
83        #[source]
84        source: bloq_circuit::FlowError,
85    },
86    /// A quantum seam lacks padding provenance.
87    #[error("quantum edge {from:?} -> {to:?} has no padding provenance")]
88    EdgePaddingMissing {
89        /// Source node.
90        from: BloqNodeId,
91        /// Target node.
92        to: BloqNodeId,
93    },
94    /// A looped padding template has no single repeat.
95    #[error("looped padding template {template:?} must contain one top-level REPEAT")]
96    PaddingTemplateNotLooped {
97        /// Invalid template.
98        template: TemplateId,
99    },
100    /// A template does not contain exactly one top-level repeat.
101    #[error("template {template:?} has {repeats} top-level REPEATs, expected exactly one")]
102    TemplateNotSingleLooped {
103        /// Invalid template.
104        template: TemplateId,
105        /// Number of top-level repeats found.
106        repeats: usize,
107    },
108    /// Stored seam data disagrees with its templates.
109    #[error("stored seam is inconsistent with template boundary flows")]
110    InvalidStoredSeam,
111    /// Replacement padding does not reproduce the seam.
112    #[error("padding does not reproduce the stored seam")]
113    SeamRecompositionMismatch,
114    /// Appending after a top-level node is unsupported.
115    #[error("memory rounds can only be appended after a node inside a region")]
116    AfterTargetTopLevel,
117    /// The append target is not terminal.
118    #[error("node {node:?} is not terminal: it already has an outgoing quantum edge")]
119    AfterTargetNotTerminal {
120        /// Nonterminal node.
121        node: BloqNodeId,
122    },
123    /// The enclosing boundary has no matching padding.
124    #[error("node {node:?} has no matching padding provenance on its enclosing region boundary")]
125    AfterPaddingMissing {
126        /// Terminal node.
127        node: BloqNodeId,
128    },
129    /// Several terminal instances occupy the same offset.
130    #[error("node {node:?} has ambiguous terminal instance placement at offset {offset:?}")]
131    AfterPaddingAmbiguous {
132        /// Ambiguous node.
133        node: BloqNodeId,
134        /// Ambiguous placement.
135        offset: IVec2,
136    },
137    /// A boundary target is not quantum.
138    #[error("region boundary target {node:?} is not quantum")]
139    AfterBoundaryTargetNotQuantum {
140        /// Non-quantum boundary node.
141        node: BloqNodeId,
142    },
143    /// Terminal padding does not reproduce its boundary.
144    #[error("terminal padding does not reproduce the enclosing region boundary")]
145    TerminalRecompositionMismatch,
146}
147
148/// Inline only bundle uses whose bound owners meet an edited seam. The whole
149/// use has one activation, which transfers to every new inline row.
150fn demote_touched_bundles(
151    program: &Bloq,
152    quantum: &mut crate::QuantumNode,
153    touched: &FxSet<TemplateInstanceId>,
154) -> Result<(), EditError> {
155    if quantum.detector_bundles.is_empty() {
156        return Ok(());
157    }
158    program.node_detector_count(quantum)?;
159    let mut registered = FxSet::default();
160    for guard in &quantum.guards {
161        for &index in &guard.detector_bundles {
162            if index as usize >= quantum.detector_bundles.len() || !registered.insert(index) {
163                return Err(
164                    crate::NodeTemplateInstanceMergeError::InvalidMembership(format!(
165                        "bundle use {index} is missing or registered more than once"
166                    ))
167                    .into(),
168                );
169            }
170        }
171    }
172    let mut remap = Vec::with_capacity(quantum.detector_bundles.len());
173    let mut inline = Vec::with_capacity(quantum.detector_bundles.len());
174    let mut kept = Vec::with_capacity(quantum.detector_bundles.len());
175    for use_ in &quantum.detector_bundles {
176        let bundle = program
177            .detector_bundles()
178            .get(use_.bundle)
179            .ok_or(crate::DetectorBundleError::UnknownBundle(use_.bundle))?;
180        if bundle
181            .used_owners()?
182            .iter()
183            .any(|&owner| touched.contains(&use_.instances[owner as usize]))
184        {
185            let one = crate::QuantumNode {
186                detector_bundles: vec![use_.clone()],
187                ..Default::default()
188            };
189            let start = u32::try_from(quantum.detectors.len())
190                .map_err(|_| crate::DetectorBundleError::CountOverflow)?;
191            let end = start
192                .checked_add(
193                    u32::try_from(bundle.detectors().len())
194                        .map_err(|_| crate::DetectorBundleError::CountOverflow)?,
195                )
196                .ok_or(crate::DetectorBundleError::CountOverflow)?;
197            let indices = start..end;
198            inline.push(Some(indices));
199            quantum
200                .detectors
201                .extend(program.node_detectors(&one)?.map(|row| row.to_owned()));
202            remap.push(None);
203        } else {
204            remap.push(Some(kept.len() as u32));
205            inline.push(None);
206            kept.push(use_.clone());
207        }
208    }
209    quantum.detector_bundles = kept;
210    for guard in &mut quantum.guards {
211        let mut uses = Vec::new();
212        for &index in &guard.detector_bundles {
213            if let Some(indices) = &inline[index as usize] {
214                guard.detectors.extend(indices.clone());
215            }
216            if let Some(index) = remap[index as usize] {
217                uses.push(index);
218            }
219        }
220        guard.detector_bundles = uses;
221    }
222    Ok(())
223}
224
225/// Program-unique location for physical memory-round insertion.
226#[derive(Debug, Clone, PartialEq, Eq, Hash)]
227#[non_exhaustive]
228pub enum MemoryRoundTarget {
229    /// Subdivide one quantum seam in `path`.
230    Edge {
231        /// Graph level containing the seam.
232        path: LevelPath,
233        /// Seam source.
234        from: BloqNodeId,
235        /// Seam target.
236        to: BloqNodeId,
237    },
238    /// Append after a terminal quantum node inside a region body.
239    After {
240        /// Region-body level.
241        path: LevelPath,
242        /// Terminal quantum node.
243        node: BloqNodeId,
244    },
245}
246
247/// A template and offset to splice into a seam.
248///
249/// The edit allocates its fresh, program-unique instance id.
250#[derive(Debug, Clone, Copy)]
251pub struct PaddingInstance {
252    /// Template to instantiate.
253    pub template: TemplateId,
254    /// Placement within the seam.
255    pub offset: IVec2,
256}
257
258/// Memo key for `Bloq`'s `specialized_padding` cache: the recorded template the
259/// specialization derives from and the number of rounds it implements.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub(crate) struct PaddingVariant {
262    pub(crate) base: TemplateId,
263    pub(crate) rounds: u32,
264}
265
266#[derive(Clone, Copy)]
267struct TerminalMember {
268    source: TemplateInstance,
269    pipe: TemporalPipeRef,
270    padding: PipePadding,
271}
272
273struct BoundaryRewrite {
274    target: BloqNodeId,
275    quantum: crate::QuantumNode,
276}
277
278struct QuantumBoundaryEntry {
279    path: LevelPath,
280    target: BloqNodeId,
281}
282
283struct QuantumBoundaryRewrite {
284    path: LevelPath,
285    target: BloqNodeId,
286    quantum: crate::QuantumNode,
287}
288
289/// Completed seam entries keyed by node position.
290#[derive(Debug, Default)]
291struct SeamComposition {
292    detectors: Vec<(usize, NodeDetector)>,
293    restarts: Vec<(usize, NodeRestart)>,
294}
295
296impl SeamComposition {
297    /// Separate the first node's entries from later nodes, retaining their order.
298    fn split_first(self) -> [(Vec<NodeDetector>, Vec<NodeRestart>); 2] {
299        let mut entries = [(Vec::new(), Vec::new()), (Vec::new(), Vec::new())];
300        for (position, detector) in self.detectors {
301            entries[usize::from(position != 0)].0.push(detector);
302        }
303        for (position, restart) in self.restarts {
304            entries[usize::from(position != 0)].1.push(restart);
305        }
306        entries
307    }
308
309    /// The entries owned by nodes strictly after `position`, dropping the
310    /// position key. A composed seam's upper node owns everything past the
311    /// lower node it was composed against, and no consumer needs the key for
312    /// anything but this bucketing.
313    fn after(self, position: usize) -> (Vec<NodeDetector>, Vec<NodeRestart>) {
314        let keep = |candidate: usize| candidate > position;
315        (
316            self.detectors
317                .into_iter()
318                .filter_map(|(at, detector)| keep(at).then_some(detector))
319                .collect(),
320            self.restarts
321                .into_iter()
322                .filter_map(|(at, restart)| keep(at).then_some(restart))
323                .collect(),
324        )
325    }
326}
327
328/// Recompose a seam from persisted [`crate::BloqTemplate::boundary_flows`].
329///
330/// This is partial: unmatched inputs and creators left open are omitted because
331/// their peers may be outside the supplied nodes. Callers must verify the
332/// resulting composition.
333fn compose_seam_detectors(
334    templates: &BloqTemplatePool,
335    nodes: &[&[TemplateInstance]],
336) -> Result<SeamComposition, EditError> {
337    let mut engine: FlowEngine<'_, DetectorTerm<InstanceMeasurement>> = FlowEngine::new();
338    let mut composition = SeamComposition::default();
339    for (position, instances) in nodes.iter().enumerate() {
340        for instance in *instances {
341            let template =
342                templates
343                    .get(instance.template_id)
344                    .ok_or(EditError::UnknownTemplate {
345                        template: instance.template_id,
346                    })?;
347            if template.boundary_flows.is_empty() {
348                continue;
349            }
350            let parts = [OffsetFlows::new(&template.boundary_flows, instance.offset)];
351            engine
352                .append_group_skipping_unmatched_inputs(&parts, |_, &measurement| {
353                    DetectorTerm::Measurement(InstanceMeasurement {
354                        instance: instance.id,
355                        measurement,
356                    })
357                })
358                .map_err(|source| EditError::SeamCompositionFailed { source })?;
359            let (detectors, restarts) = drain_composed_chains(&mut engine);
360            composition
361                .detectors
362                .extend(detectors.into_iter().map(|detector| (position, detector)));
363            composition
364                .restarts
365                .extend(restarts.into_iter().map(|restart| (position, restart)));
366        }
367    }
368    // Open creators may close against nodes outside this partial seam.
369    Ok(composition)
370}
371
372/// Find the stored multiset entries replaced by a recomposed seam.
373fn removal_mask<T: PartialEq>(stored: &[T], seam: &[T]) -> Option<Vec<bool>> {
374    let mut remove = vec![false; stored.len()];
375    for expected in seam {
376        let index = stored
377            .iter()
378            .enumerate()
379            .find_map(|(index, item)| (!remove[index] && item == expected).then_some(index))?;
380        remove[index] = true;
381    }
382    Some(remove)
383}
384
385/// Total order on detector centers, so sorting makes a `Vec` comparison a
386/// multiset comparison. `None` — a chain carrying no center — sorts before
387/// every `Some` and stays distinct from `Some([])`.
388fn compare_centers(left: &Option<DetectorCoords>, right: &Option<DetectorCoords>) -> Ordering {
389    match (left, right) {
390        (None, None) => Ordering::Equal,
391        (None, Some(_)) => Ordering::Less,
392        (Some(_), None) => Ordering::Greater,
393        (Some(left), Some(right)) => left.len().cmp(&right.len()).then_with(|| {
394            std::iter::zip(left, right)
395                .map(|(left, right)| left.total_cmp(right))
396                .find(|order| order.is_ne())
397                .unwrap_or(Ordering::Equal)
398        }),
399    }
400}
401
402/// Whether a recomposed seam reproduces the one it replaces,
403/// plaquette-for-plaquette.
404///
405/// Padding continues the source patch's memory rounds, so the chains that close
406/// against the node above compare the same plaquettes as before — one layer
407/// later, against different measurement indices. Centers are therefore the
408/// invariant: comparing parities would compare indices that legitimately moved.
409///
410/// The comparison is over the center **multiset**, not the set: sorting both
411/// sides and comparing the `Vec`s tests length first, so a seam that dropped or
412/// duplicated a chain fails even though the distinct centers still agree.
413fn seam_reproduced(original: &[NodeDetector], replacement: &[NodeDetector]) -> bool {
414    fn centers(detectors: &[NodeDetector]) -> Vec<Option<DetectorCoords>> {
415        let mut centers: Vec<_> = detectors
416            .iter()
417            .map(|detector| detector.coords.clone())
418            .collect();
419        centers.sort_by(compare_centers);
420        centers
421    }
422    centers(original) == centers(replacement)
423}
424
425/// Rewrite the one top-level repeat whose cardinality the caller checked.
426fn set_single_repeat_count(template: &mut BloqTemplate, repetitions: u32) {
427    let entry = template.circuit.entry_body();
428    for op in template
429        .circuit
430        .body_mut(entry)
431        .expect("every circuit has an entry body")
432        .ops_mut()
433    {
434        if let Op::Repeat { repetitions: r, .. } = op {
435            *r = repetitions;
436        }
437    }
438}
439
440fn retain_unmasked<T>(stored: &mut Vec<T>, remove: &[bool]) {
441    debug_assert_eq!(stored.len(), remove.len());
442    let mut remove = remove.iter();
443    stored.retain(|_| !*remove.next().expect("removal mask matches side table"));
444}
445
446/// Move every [`BoundaryFace::Output`] operator that named a replaced instance
447/// onto its replacement.
448///
449/// The surface a program's observable ends on now ends after the padding, so an
450/// operator stating where the program exits has to follow it.
451fn retarget_output_operators(
452    level: &mut SubGraph,
453    replacements: &FxMap<TemplateInstanceId, TemplateInstanceId>,
454) {
455    for node in level.graph_mut().node_weights_mut() {
456        match &mut node.kind {
457            BloqNodeKind::Classical(data)
458                if data.operators().iter().any(|operator| {
459                    operator.face == BoundaryFace::Output
460                        && replacements.contains_key(&operator.instance)
461                }) =>
462            {
463                let ClassicalNode::Observable { operators, .. } = std::sync::Arc::make_mut(data)
464                else {
465                    unreachable!("classical output checked above")
466                };
467                for operator in operators {
468                    if operator.face == BoundaryFace::Output
469                        && let Some(&replacement) = replacements.get(&operator.instance)
470                    {
471                        operator.instance = replacement;
472                    }
473                }
474            }
475            BloqNodeKind::Region(region) => {
476                for (_, body) in region.bodies_mut() {
477                    retarget_output_operators(body, replacements);
478                }
479            }
480            BloqNodeKind::Quantum(_) | BloqNodeKind::Classical(_) => {}
481        }
482    }
483}
484
485/// Keep observable bindings and logical-output metadata on the same new cut.
486fn retarget_outputs(bloq: &mut Bloq, replacements: &FxMap<TemplateInstanceId, TemplateInstanceId>) {
487    retarget_output_operators(bloq.top_mut(), replacements);
488    let mut outputs = bloq.logical_outputs().to_vec();
489    for output in &mut outputs {
490        if let Some(&replacement) = replacements.get(&output.instance) {
491            output.instance = replacement;
492        }
493    }
494    bloq.set_logical_outputs(outputs);
495}
496
497/// Drain completed chains into detector and restart side tables.
498///
499/// Classification is shared with compile-time detector lowering.
500pub fn drain_composed_chains(
501    engine: &mut FlowEngine<'_, DetectorTerm<InstanceMeasurement>>,
502) -> (Vec<NodeDetector>, Vec<NodeRestart>) {
503    let chains = engine.drain_composed_chains();
504    let mut detectors = Vec::with_capacity(chains.size_hint().1.unwrap_or(0));
505    let mut restarts = Vec::new();
506    for chain in chains {
507        match chain {
508            ComposedChain::Restart { parity } => restarts.push(NodeRestart { parity }),
509            ComposedChain::Detector { parity, center } => {
510                detectors.push(NodeDetector {
511                    parity,
512                    coords: center
513                        .map(|coords| [coords.x as f64, coords.y as f64].into_iter().collect()),
514                });
515            }
516        }
517    }
518    (detectors, restarts)
519}
520
521impl Bloq {
522    /// Insert physical memory rounds at a level-qualified quantum edge or
523    /// after a region body's terminal quantum node.
524    ///
525    /// Templates come from compiler-recorded padding provenance, so the edit
526    /// works on a deserialized program without its source graph. The edit is
527    /// transactional: any error leaves `self` unchanged.
528    ///
529    /// # Errors
530    ///
531    /// Returns [`EditError`] when the target or stored seam provenance is
532    /// invalid, membership is unresolved, or `rounds` is zero.
533    pub fn insert_memory_rounds(
534        &mut self,
535        target: MemoryRoundTarget,
536        rounds: u32,
537    ) -> Result<BloqNodeId, EditError> {
538        Ok(self.insert_memory_rounds_batch(&[target], rounds)?[0])
539    }
540
541    fn insert_memory_rounds_in_place(
542        &mut self,
543        target: &MemoryRoundTarget,
544        rounds: u32,
545    ) -> Result<BloqNodeId, EditError> {
546        if self.timing_requires_pins()? {
547            return Err(EditError::MembershipSelectionRequired);
548        }
549        if rounds == 0 {
550            return Err(EditError::EmptyPadding);
551        }
552        match target {
553            MemoryRoundTarget::Edge { path, from, to } => {
554                self.splice_seam_node_at(path, *from, *to, rounds)
555            }
556            MemoryRoundTarget::After { path, node } => {
557                self.append_memory_rounds_at(path, *node, rounds)
558            }
559        }
560    }
561
562    /// Resolve the seam's recorded padding into instantiable templates and
563    /// splice `node` in, shared by every edge-target insertion.
564    fn splice_seam_node_at(
565        &mut self,
566        path: &LevelPath,
567        from: BloqNodeId,
568        to: BloqNodeId,
569        rounds: u32,
570    ) -> Result<BloqNodeId, EditError> {
571        let level = self
572            .level_at(path)
573            .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
574        let seams = level
575            .edges_between(from, to)
576            .find_map(|edge| match edge.edge {
577                BloqEdge::Quantum(edge) => Some(edge.pipes.clone()),
578                BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
579            })
580            .ok_or(EditError::InvalidQuantumEdge { from, to })?;
581        // Subdivision replaces the seam whole, so every pipe on it must
582        // carry provenance — a partially recorded edge is not editable.
583        let padding_refs: Option<Vec<_>> = seams.iter().map(|seam| seam.padding).collect();
584        let padding_refs = padding_refs
585            .filter(|refs| !refs.is_empty())
586            .ok_or(EditError::EdgePaddingMissing { from, to })?;
587        let padding = self.padding_for_refs(&padding_refs, rounds)?;
588        self.subdivide_quantum_edge_at(path, from, to, &padding, rounds)
589    }
590
591    /// Retune a template's bulk duration: rewrite the repetition count of its
592    /// single top-level `REPEAT`.
593    ///
594    /// This is how a compiled program's memory-round budget is changed without
595    /// recompiling, and it is a *structural* rewrite: neither the template's
596    /// side tables nor its boundary flows depend on the repetition count (a
597    /// loop's recurrence is stated per iteration), so nothing else has to move.
598    /// Measurement ids are per body, not per iteration, so the program's
599    /// measurement registry is unaffected too.
600    ///
601    /// Templates are pooled and shared, so **every** instance of `template`
602    /// changes with it. That is usually the point — one cube kind, one bulk
603    /// duration — but a caller that means to retune only some placements must
604    /// check the sharing itself; this call cannot tell the two intents apart.
605    ///
606    /// # Errors
607    ///
608    /// Returns [`EditError::UnknownTemplate`] for an id outside the pool, and
609    /// [`EditError::TemplateNotSingleLooped`] when the template's entry body
610    /// has anything other than exactly one top-level `REPEAT` — with zero
611    /// there is no loop to retune, and with several the call would silently
612    /// mean "all of them".
613    ///
614    /// # Panics
615    ///
616    /// Panics only if a template disappears after it is read from the pool.
617    pub fn set_template_repetitions(
618        &mut self,
619        template: TemplateId,
620        repetitions: u32,
621    ) -> Result<(), EditError> {
622        let repeats = self
623            .templates()
624            .get(template)
625            .ok_or(EditError::UnknownTemplate { template })?
626            .circuit
627            .entry_top_level_repeats()
628            .len();
629        if repeats != 1 {
630            return Err(EditError::TemplateNotSingleLooped { template, repeats });
631        }
632        let template = self
633            .templates_mut()
634            .make_mut(template)
635            .expect("the template was just read from the pool");
636        set_single_repeat_count(template, repetitions);
637        self.specialized_padding.clear();
638        Ok(())
639    }
640
641    /// Insert memory rounds at every target or at none, returning node ids in
642    /// target order. Targets may mix edges and terminal nodes at any level.
643    /// An empty target list is a no-op, but zero rounds is always rejected.
644    ///
645    /// # Errors
646    ///
647    /// Returns [`EditError`] if membership is unresolved, any target or seam
648    /// is invalid, or `rounds` is zero. Any error leaves `self` unchanged.
649    ///
650    /// Editing a clone and swapping on success costs one clone for the whole
651    /// batch, where looping over the public [`Bloq::insert_memory_rounds`]
652    /// would clone once per target *and* leave the earlier targets padded when
653    /// a later one fails. Targets are applied in order. A duplicate or overlapping
654    /// target that becomes invalid also rolls back the whole batch.
655    pub fn insert_memory_rounds_batch(
656        &mut self,
657        targets: &[MemoryRoundTarget],
658        rounds: u32,
659    ) -> Result<Vec<BloqNodeId>, EditError> {
660        if rounds == 0 {
661            return Err(EditError::EmptyPadding);
662        }
663        if self.timing_requires_pins()? {
664            return Err(EditError::MembershipSelectionRequired);
665        }
666        let mut edited = self.clone();
667        let padding = targets
668            .iter()
669            .map(|target| edited.insert_memory_rounds_in_place(target, rounds))
670            .collect::<Result<Vec<_>, _>>()?;
671        *self = edited;
672        Ok(padding)
673    }
674
675    fn padding_for_refs(
676        &mut self,
677        padding_refs: &[PipePadding],
678        rounds: u32,
679    ) -> Result<Vec<PaddingInstance>, EditError> {
680        padding_refs
681            .iter()
682            .map(|entry| {
683                let template = self.padding_template_for_rounds(entry, rounds)?;
684                Ok(PaddingInstance {
685                    template,
686                    offset: entry.offset,
687                })
688            })
689            .collect()
690    }
691
692    /// Resolve or memoize the template implementing `rounds` memory rounds.
693    /// Only the repetition count varies; side tables and boundary flows are
694    /// count-independent.
695    fn padding_template_for_rounds(
696        &mut self,
697        entry: &PipePadding,
698        rounds: u32,
699    ) -> Result<TemplateId, EditError> {
700        if rounds == 0 {
701            return Err(EditError::EmptyPadding);
702        }
703        if rounds == 1 {
704            return Ok(entry.one_round);
705        }
706        let repetitions = rounds - 1;
707        let template = self
708            .templates()
709            .get(entry.looped)
710            .ok_or(EditError::UnknownTemplate {
711                template: entry.looped,
712            })?;
713        let [current] = template.circuit.entry_top_level_repeats()[..] else {
714            return Err(EditError::PaddingTemplateNotLooped {
715                template: entry.looped,
716            });
717        };
718        if current == repetitions {
719            return Ok(entry.looped);
720        }
721        let key = PaddingVariant {
722            base: entry.looped,
723            rounds,
724        };
725        if let Some(&specialized) = self.specialized_padding.get(&key) {
726            return Ok(specialized);
727        }
728
729        let mut specialized = template.clone();
730        set_single_repeat_count(&mut specialized, repetitions);
731        let specialized = self.add_template(specialized);
732        self.specialized_padding.insert(key, specialized);
733        Ok(specialized)
734    }
735
736    /// Splice padding into `from -> to`, producing `from -> padding -> to`.
737    ///
738    /// Before mutation, this allocates fresh instance ids and verifies the
739    /// original stored seam entries before recomposing both replacement
740    /// halves. Partial seams leaving a merge may move closures between nodes
741    /// and retarget output-face operators; full-node seams preserve side-table
742    /// entries one-for-one. `rounds` is recorded as memory-padding provenance
743    /// on the new node.
744    ///
745    /// Returns the new padding node id. Re-resolve node ids held across this
746    /// call because vacant slots may be recycled.
747    ///
748    /// # Errors
749    ///
750    /// Returns [`EditError`] when the seam, padding instances, or stored
751    /// detector composition is invalid.
752    pub fn subdivide_quantum_edge(
753        &mut self,
754        from: BloqNodeId,
755        to: BloqNodeId,
756        padding: &[PaddingInstance],
757        rounds: u32,
758    ) -> Result<BloqNodeId, EditError> {
759        self.subdivide_quantum_edge_at(&LevelPath::default(), from, to, padding, rounds)
760    }
761
762    fn subdivide_quantum_edge_at(
763        &mut self,
764        path: &LevelPath,
765        from: BloqNodeId,
766        to: BloqNodeId,
767        padding: &[PaddingInstance],
768        rounds: u32,
769    ) -> Result<BloqNodeId, EditError> {
770        if self.timing_requires_pins()? {
771            return Err(EditError::MembershipSelectionRequired);
772        }
773        if rounds == 0 || padding.is_empty() {
774            return Err(EditError::EmptyPadding);
775        }
776
777        // Verify before mutation.
778        let (from_index, to_index) = (
779            NodeIndex::new(from.0 as usize),
780            NodeIndex::new(to.0 as usize),
781        );
782        let level = self
783            .level_at(path)
784            .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
785        let mut quantum_edges = level
786            .graph()
787            .edges_connecting(from_index, to_index)
788            .filter(|edge| matches!(edge.weight(), BloqEdge::Quantum(_)));
789        let edge = quantum_edges
790            .next()
791            .ok_or(EditError::InvalidQuantumEdge { from, to })?;
792        // Removal masks require this to be the sole quantum interface.
793        if quantum_edges.next().is_some() {
794            return Err(EditError::InvalidQuantumEdge { from, to });
795        }
796        let edge_id = edge.id();
797        let BloqEdge::Quantum(quantum_edge) = edge.weight() else {
798            unreachable!("filtered to quantum edges")
799        };
800        let quantum_edge = quantum_edge.clone();
801        if quantum_edge.pipes.is_empty() {
802            return Err(EditError::InvalidQuantumEdge { from, to });
803        }
804
805        let lower = level
806            .node(from)
807            .expect("a graph edge's source node is live");
808        let upper = level.node(to).expect("a graph edge's target node is live");
809        let lower_quantum = lower
810            .try_quantum()
811            .ok_or(EditError::InvalidQuantumEdge { from, to })?;
812        if let Some(region) = upper.try_region() {
813            let mut entries = Vec::new();
814            for (body_selector, body) in region.bodies() {
815                let mut covered = vec![0usize; padding.len()];
816                for (target, node) in body.nodes() {
817                    let Some(quantum) = node.try_quantum() else {
818                        continue;
819                    };
820                    if body
821                        .incoming(target)
822                        .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
823                    {
824                        continue;
825                    }
826                    let mut matched = false;
827                    for (index, padding) in padding.iter().enumerate() {
828                        if quantum
829                            .instances
830                            .iter()
831                            .any(|instance| instance.offset == padding.offset)
832                        {
833                            covered[index] += 1;
834                            matched = true;
835                        }
836                    }
837                    if matched {
838                        entries.push(QuantumBoundaryEntry {
839                            path: path.child(to, body_selector),
840                            target,
841                        });
842                    }
843                }
844                if covered.iter().any(|&count| count != 1) {
845                    return Err(EditError::InvalidQuantumEdge { from, to });
846                }
847            }
848            let lower_quantum = lower_quantum.clone();
849            return self.subdivide_quantum_boundary_input_at(
850                path,
851                from,
852                to,
853                padding,
854                rounds,
855                quantum_edge,
856                lower_quantum,
857                entries,
858            );
859        }
860        let upper_quantum = upper
861            .try_quantum()
862            .ok_or(EditError::InvalidQuantumEdge { from, to })?;
863        if !upper_quantum.guards.is_empty() {
864            let entries = vec![QuantumBoundaryEntry {
865                path: path.clone(),
866                target: to,
867            }];
868            let lower_quantum = lower_quantum.clone();
869            return self.subdivide_quantum_boundary_input_at(
870                path,
871                from,
872                to,
873                padding,
874                rounds,
875                quantum_edge,
876                lower_quantum,
877                entries,
878            );
879        }
880
881        // Only instances carried by this edge are adjacent to `upper`; other
882        // members of the same node may reach it through later seams.
883        let lower_before: Vec<_> = lower_quantum
884            .instances
885            .iter()
886            .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
887            .copied()
888            .collect();
889
890        // Recomposition catches Hadamard-aligned chains that parity alone cannot.
891        let original =
892            compose_seam_detectors(self.templates(), &[&lower_before, &upper_quantum.instances])?;
893        let [
894            (lower_detectors, lower_restarts),
895            (original_detectors, original_restarts),
896        ] = original.split_first();
897        let touched = lower_before
898            .iter()
899            .map(|instance| instance.id)
900            .chain(
901                upper_quantum
902                    .instances
903                    .iter()
904                    .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
905                    .map(|instance| instance.id),
906            )
907            .collect::<FxSet<_>>();
908        let mut lower_quantum = lower_quantum.clone();
909        let mut upper_quantum = upper_quantum.clone();
910        demote_touched_bundles(self, &mut lower_quantum, &touched)?;
911        demote_touched_bundles(self, &mut upper_quantum, &touched)?;
912        if removal_mask(&lower_quantum.detectors, &lower_detectors).is_none()
913            || removal_mask(&lower_quantum.restarts, &lower_restarts).is_none()
914        {
915            return Err(EditError::InvalidStoredSeam);
916        }
917        let detector_remove = removal_mask(&upper_quantum.detectors, &original_detectors)
918            .ok_or(EditError::InvalidStoredSeam)?;
919        let restart_remove = removal_mask(&upper_quantum.restarts, &original_restarts)
920            .ok_or(EditError::InvalidStoredSeam)?;
921
922        let padding_instances = self.allocate_padding_instances(padding)?;
923        let mut replacements = FxMap::default();
924        for (padding, instance) in padding.iter().zip(&padding_instances) {
925            let mut sources = lower_quantum
926                .instances
927                .iter()
928                .filter(|source| source.offset == padding.offset);
929            let Some(source) = sources.next() else {
930                return Err(EditError::InvalidQuantumEdge { from, to });
931            };
932            if sources.next().is_some() || replacements.insert(source.id, *instance).is_some() {
933                return Err(EditError::InvalidQuantumEdge { from, to });
934            }
935        }
936        let lower_after: Vec<_> = lower_before
937            .iter()
938            .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
939            .collect();
940
941        let internal =
942            compose_seam_detectors(self.templates(), &[&lower_before, &padding_instances])?;
943        let [
944            (composed_lower_detectors, composed_lower_restarts),
945            (mid_detectors, mid_restarts),
946        ] = internal.split_first();
947        if removal_mask(&lower_quantum.detectors, &composed_lower_detectors).is_none()
948            || removal_mask(&lower_quantum.restarts, &composed_lower_restarts).is_none()
949        {
950            return Err(EditError::InvalidStoredSeam);
951        }
952
953        let replacement =
954            compose_seam_detectors(self.templates(), &[&lower_after, &upper_quantum.instances])?;
955        let (upper_detectors, upper_restarts) = replacement.after(0);
956
957        // Only the seam against `upper` has a counterpart to reproduce. The
958        // `lower -> padding` seam is new, and its size is set by the padding's
959        // own boundary rather than by what it displaced: a resolved Y-basis
960        // target pairs its plaquettes into composite seam chains, so an
961        // ordinary memory seam underneath it legitimately carries twice the
962        // detectors the original seam did.
963        if !seam_reproduced(&original_detectors, &upper_detectors)
964            || upper_restarts.len() != original_restarts.len()
965        {
966            return Err(EditError::SeamRecompositionMismatch);
967        }
968        let partial_seam = padding_instances.len() < lower_quantum.instances.len();
969
970        // Apply only after all checks pass. Any pipe gives the shared seam layer.
971        let mut node = BloqNode::memory_padding(quantum_edge.pipes[0].pipe, rounds);
972        {
973            let quantum = node.expect_quantum_mut();
974            quantum.instances = padding_instances;
975            quantum.detectors = mid_detectors;
976            quantum.restarts = mid_restarts;
977        }
978        let upper_work = upper_quantum;
979        let level = self
980            .level_at_mut(path)
981            .expect("validated level path remains live during insertion");
982        let mid = level.add_node(node);
983        let graph = level.graph_mut();
984        graph.remove_edge(edge_id);
985        *graph[from_index].expect_quantum_mut() = lower_quantum;
986        let upper_quantum = graph[to_index].expect_quantum_mut();
987        *upper_quantum = upper_work;
988        retain_unmasked(&mut upper_quantum.detectors, &detector_remove);
989        retain_unmasked(&mut upper_quantum.restarts, &restart_remove);
990        upper_quantum.detectors.extend(upper_detectors);
991        upper_quantum.restarts.extend(upper_restarts);
992
993        level.add_edge(from, mid, BloqEdge::Quantum(quantum_edge.clone()));
994        level.add_edge(mid, to, BloqEdge::Quantum(quantum_edge));
995        if partial_seam {
996            let output_replacements = replacements
997                .into_iter()
998                .map(|(source, padding)| (source, padding.id))
999                .collect();
1000            retarget_outputs(self, &output_replacements);
1001        }
1002        Ok(mid)
1003    }
1004
1005    #[expect(
1006        clippy::too_many_arguments,
1007        reason = "the seam edit needs both endpoints, payloads, and replacement state"
1008    )]
1009    fn subdivide_quantum_boundary_input_at(
1010        &mut self,
1011        path: &LevelPath,
1012        from: BloqNodeId,
1013        to: BloqNodeId,
1014        padding: &[PaddingInstance],
1015        rounds: u32,
1016        quantum_edge: Box<QuantumEdge>,
1017        mut source: crate::QuantumNode,
1018        entries: Vec<QuantumBoundaryEntry>,
1019    ) -> Result<BloqNodeId, EditError> {
1020        let source_before: Vec<_> = source
1021            .instances
1022            .iter()
1023            .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
1024            .copied()
1025            .collect();
1026        let padding_instances = self.allocate_padding_instances(padding)?;
1027        let mut replacements = FxMap::default();
1028        for (padding, instance) in padding.iter().zip(&padding_instances) {
1029            let mut sources = source
1030                .instances
1031                .iter()
1032                .filter(|source| source.offset == padding.offset);
1033            let Some(source) = sources.next() else {
1034                return Err(EditError::InvalidQuantumEdge { from, to });
1035            };
1036            if sources.next().is_some() || replacements.insert(source.id, *instance).is_some() {
1037                return Err(EditError::InvalidQuantumEdge { from, to });
1038            }
1039        }
1040        let lower_after: Vec<_> = source_before
1041            .iter()
1042            .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
1043            .collect();
1044
1045        let internal =
1046            compose_seam_detectors(self.templates(), &[&source_before, &padding_instances])?;
1047        let [
1048            (source_detectors, source_restarts),
1049            (padding_detectors, padding_restarts),
1050        ] = internal.split_first();
1051        let touched = source_before.iter().map(|instance| instance.id).collect();
1052        demote_touched_bundles(self, &mut source, &touched)?;
1053        if removal_mask(&source.detectors, &source_detectors).is_none()
1054            || removal_mask(&source.restarts, &source_restarts).is_none()
1055        {
1056            return Err(EditError::InvalidStoredSeam);
1057        }
1058
1059        let mut rewrites = Vec::with_capacity(entries.len());
1060        for entry in entries {
1061            let quantum = self.recompose_boundary_at(
1062                &entry.path,
1063                entry.target,
1064                &source_before,
1065                &lower_after,
1066            )?;
1067            rewrites.push(QuantumBoundaryRewrite {
1068                path: entry.path,
1069                target: entry.target,
1070                quantum,
1071            });
1072        }
1073
1074        for rewrite in rewrites {
1075            *self
1076                .level_at_mut(&rewrite.path)
1077                .expect("validated level remains live")
1078                .node_mut(rewrite.target)
1079                .expect("validated boundary target remains live")
1080                .expect_quantum_mut() = rewrite.quantum;
1081        }
1082
1083        let mut node = BloqNode::memory_padding(quantum_edge.pipes[0].pipe, rounds);
1084        {
1085            let quantum = node.expect_quantum_mut();
1086            quantum.instances = padding_instances;
1087            quantum.detectors = padding_detectors;
1088            quantum.restarts = padding_restarts;
1089        }
1090        let level = self
1091            .level_at_mut(path)
1092            .expect("validated level path remains live during insertion");
1093        *level
1094            .node_mut(from)
1095            .expect("validated source remains live")
1096            .expect_quantum_mut() = source;
1097        let mid = level.add_node(node);
1098        let edge = level
1099            .graph()
1100            .edges_connecting(
1101                NodeIndex::new(from.0 as usize),
1102                NodeIndex::new(to.0 as usize),
1103            )
1104            .find(|edge| matches!(edge.weight(), BloqEdge::Quantum(_)))
1105            .expect("validated quantum edge remains live")
1106            .id();
1107        level.graph_mut().remove_edge(edge);
1108        level.add_edge(from, mid, BloqEdge::Quantum(quantum_edge.clone()));
1109        level.add_edge(mid, to, BloqEdge::Quantum(quantum_edge));
1110        Ok(mid)
1111    }
1112
1113    /// Recompose each mutually exclusive payload independently. Guarded rows
1114    /// retain their indices and input dependencies; only their parity deltas
1115    /// change under the choice that owns the replaced seam.
1116    fn recompose_boundary_at(
1117        &self,
1118        path: &LevelPath,
1119        target: BloqNodeId,
1120        before: &[TemplateInstance],
1121        after: &[TemplateInstance],
1122    ) -> Result<crate::QuantumNode, EditError> {
1123        let level = self.level_at(path).expect("validated boundary level");
1124        let mut node = level[target].clone();
1125        let touched = before
1126            .iter()
1127            .chain(after)
1128            .map(|instance| instance.id)
1129            .chain(
1130                node.expect_quantum()
1131                    .instances
1132                    .iter()
1133                    .filter(|instance| before.iter().any(|source| source.offset == instance.offset))
1134                    .map(|instance| instance.id),
1135            )
1136            .collect::<FxSet<_>>();
1137        demote_touched_bundles(self, node.expect_quantum_mut(), &touched)?;
1138        let quantum = node.expect_quantum();
1139        let mut updated = quantum.clone();
1140        let choices = if quantum.guards.is_empty() {
1141            vec![crate::membership::QuantumSelection {
1142                input: 0,
1143                values: FxMap::default(),
1144            }]
1145        } else {
1146            crate::membership::PredicateAnalysis::new(level)
1147                .quantum_selections(target)?
1148                .ok_or(EditError::MembershipSelectionRequired)?
1149        };
1150        for choice in choices {
1151            let selected = node.select_quantum_members(|slot| choice.values.get(&slot).copied())?;
1152            let selected = selected.expect_quantum();
1153            let original =
1154                compose_seam_detectors(self.templates(), &[before, &selected.instances])?;
1155            let (original_detectors, original_restarts) = original.after(0);
1156            let detector_remove = removal_mask(&selected.detectors, &original_detectors)
1157                .ok_or(EditError::InvalidStoredSeam)?;
1158            let restart_remove = removal_mask(&selected.restarts, &original_restarts)
1159                .ok_or(EditError::InvalidStoredSeam)?;
1160            let replacement =
1161                compose_seam_detectors(self.templates(), &[after, &selected.instances])?;
1162            let (mut detectors, restarts) = replacement.after(0);
1163            if !seam_reproduced(&original_detectors, &detectors)
1164                || restarts.len() != original_restarts.len()
1165            {
1166                return Err(EditError::SeamRecompositionMismatch);
1167            }
1168            if quantum.guards.is_empty() {
1169                retain_unmasked(&mut updated.detectors, &detector_remove);
1170                retain_unmasked(&mut updated.restarts, &restart_remove);
1171                updated.detectors.extend(detectors);
1172                updated.restarts.extend(restarts);
1173                continue;
1174            }
1175            let detector_indices = (0..quantum.detectors.len() as u32).filter(|index| {
1176                !quantum
1177                    .guards
1178                    .iter()
1179                    .any(|guard| !choice.values[&guard.input] && guard.detectors.contains(index))
1180            });
1181            let mut changed = detector_indices
1182                .zip(&selected.detectors)
1183                .zip(detector_remove)
1184                .filter_map(|((index, row), remove)| remove.then_some((index, row)))
1185                .collect::<Vec<_>>();
1186            changed.sort_by(|(_, left), (_, right)| compare_centers(&left.coords, &right.coords));
1187            detectors.sort_by(|left, right| compare_centers(&left.coords, &right.coords));
1188            let guard = updated
1189                .guards
1190                .iter_mut()
1191                .find(|guard| guard.input == choice.input)
1192                .expect("a choice is a member registration");
1193            for ((index, old), new) in changed.into_iter().zip(detectors) {
1194                let mut delta = old.parity.clone();
1195                delta.xor_assign(&new.parity);
1196                if !delta.is_empty() {
1197                    guard.detector_parities.push((index, delta));
1198                }
1199            }
1200            let restart_indices = (0..quantum.restarts.len() as u32).filter(|index| {
1201                !quantum
1202                    .guards
1203                    .iter()
1204                    .any(|guard| !choice.values[&guard.input] && guard.restarts.contains(index))
1205            });
1206            let changed = restart_indices
1207                .zip(&selected.restarts)
1208                .zip(restart_remove)
1209                .filter_map(|((index, row), remove)| remove.then_some((index, row)));
1210            for ((index, old), new) in changed.zip(restarts) {
1211                let mut delta = old.parity.clone();
1212                delta.xor_assign(&new.parity);
1213                if !delta.is_empty() {
1214                    guard.restart_parities.push((index, delta));
1215                }
1216            }
1217        }
1218        Ok(updated)
1219    }
1220
1221    fn append_memory_rounds_at(
1222        &mut self,
1223        path: &LevelPath,
1224        source: BloqNodeId,
1225        rounds: u32,
1226    ) -> Result<BloqNodeId, EditError> {
1227        let level = self
1228            .level_at(path)
1229            .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
1230        let source_node =
1231            level
1232                .node(source)
1233                .ok_or_else(|| EditError::MemoryRoundTargetMissing {
1234                    path: path.clone(),
1235                    node: source,
1236                })?;
1237        let source_quantum =
1238            source_node
1239                .try_quantum()
1240                .ok_or_else(|| EditError::MemoryRoundTargetNotQuantum {
1241                    path: path.clone(),
1242                    node: source,
1243                })?;
1244        if !source_quantum.guards.is_empty() {
1245            return Err(EditError::MembershipSelectionRequired);
1246        }
1247        if level
1248            .outgoing(source)
1249            .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
1250        {
1251            return Err(EditError::AfterTargetNotTerminal { node: source });
1252        }
1253        let source_instances = source_quantum.instances.clone();
1254
1255        let Some((segment, parent_path)) = path.segments().split_last() else {
1256            return Err(EditError::AfterTargetTopLevel);
1257        };
1258        let region = segment.region;
1259        let (mut members, mut boundary_targets) = (Vec::<TerminalMember>::new(), Vec::new());
1260        {
1261            let parent = self
1262                .level_at_segments(parent_path)
1263                .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
1264            for edge_ref in parent.outgoing(region) {
1265                let BloqEdge::Quantum(edge) = edge_ref.edge else {
1266                    continue;
1267                };
1268                let mut matched = false;
1269                for seam in &edge.pipes {
1270                    let Some(padding) = seam.padding else {
1271                        continue;
1272                    };
1273                    let mut instances = source_instances
1274                        .iter()
1275                        .filter(|instance| instance.offset == padding.offset);
1276                    let Some(&instance) = instances.next() else {
1277                        continue;
1278                    };
1279                    if instances.next().is_some()
1280                        || members.iter().any(|member| member.source.id == instance.id)
1281                    {
1282                        return Err(EditError::AfterPaddingAmbiguous {
1283                            node: source,
1284                            offset: padding.offset,
1285                        });
1286                    }
1287                    members.push(TerminalMember {
1288                        source: instance,
1289                        pipe: seam.pipe,
1290                        padding,
1291                    });
1292                    matched = true;
1293                }
1294                if matched {
1295                    if parent
1296                        .node(edge_ref.target)
1297                        .and_then(BloqNode::try_quantum)
1298                        .is_none()
1299                    {
1300                        return Err(EditError::AfterBoundaryTargetNotQuantum {
1301                            node: edge_ref.target,
1302                        });
1303                    }
1304                    if boundary_targets.contains(&edge_ref.target) {
1305                        return Err(EditError::TerminalRecompositionMismatch);
1306                    }
1307                    boundary_targets.push(edge_ref.target);
1308                }
1309            }
1310        }
1311        if members.is_empty() {
1312            return Err(EditError::AfterPaddingMissing { node: source });
1313        }
1314        let mut source_quantum = source_quantum.clone();
1315        let touched = members.iter().map(|member| member.source.id).collect();
1316        demote_touched_bundles(self, &mut source_quantum, &touched)?;
1317        let source_detectors = source_quantum.detectors.clone();
1318        let source_restarts = source_quantum.restarts.clone();
1319        let layer = members[0].pipe.src.z.min(members[0].pipe.dst.z);
1320        if members
1321            .iter()
1322            .any(|member| member.pipe.src.z.min(member.pipe.dst.z) != layer)
1323        {
1324            return Err(EditError::TerminalRecompositionMismatch);
1325        }
1326
1327        let refs: Vec<_> = members.iter().map(|member| member.padding).collect();
1328        let padding = self.padding_for_refs(&refs, rounds)?;
1329        let padding_instances = self.allocate_padding_instances(&padding)?;
1330        let replacements: FxMap<_, _> = members
1331            .iter()
1332            .zip(&padding_instances)
1333            .map(|(member, &padding)| (member.source.id, padding))
1334            .collect();
1335        let lower_after: Vec<_> = source_instances
1336            .iter()
1337            .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
1338            .collect();
1339
1340        let internal =
1341            compose_seam_detectors(self.templates(), &[&source_instances, &padding_instances])?;
1342        let [
1343            (source_composed_detectors, source_composed_restarts),
1344            (padding_detectors, padding_restarts),
1345        ] = internal.split_first();
1346        if removal_mask(&source_detectors, &source_composed_detectors).is_none()
1347            || removal_mask(&source_restarts, &source_composed_restarts).is_none()
1348        {
1349            return Err(EditError::InvalidStoredSeam);
1350        }
1351
1352        let mut boundary_rewrites = Vec::with_capacity(boundary_targets.len());
1353        let boundary_path = parent_path
1354            .iter()
1355            .fold(LevelPath::default(), |path, segment| {
1356                path.child(segment.region, segment.body)
1357            });
1358        for target in boundary_targets {
1359            boundary_rewrites.push(BoundaryRewrite {
1360                target,
1361                quantum: self.recompose_boundary_at(
1362                    &boundary_path,
1363                    target,
1364                    &source_instances,
1365                    &lower_after,
1366                )?,
1367            });
1368        }
1369
1370        // Every fallible check is complete. Apply parent seam repairs first.
1371        {
1372            let parent = self
1373                .level_at_segments_mut(parent_path)
1374                .expect("validated parent level remains live during insertion");
1375            for rewrite in boundary_rewrites {
1376                let upper = parent
1377                    .node_mut(rewrite.target)
1378                    .expect("validated boundary target remains live")
1379                    .expect_quantum_mut();
1380                *upper = rewrite.quantum;
1381            }
1382        }
1383
1384        let mut padding_node = BloqNode::memory_padding(members[0].pipe, rounds);
1385        {
1386            let quantum = padding_node.expect_quantum_mut();
1387            quantum.instances = padding_instances;
1388            quantum.detectors = padding_detectors;
1389            quantum.restarts = padding_restarts;
1390        }
1391        let padding_node = {
1392            let level = self
1393                .level_at_mut(path)
1394                .expect("validated target level remains live during insertion");
1395            *level
1396                .node_mut(source)
1397                .expect("validated source remains live")
1398                .expect_quantum_mut() = source_quantum;
1399            let order_edges: Vec<_> = level
1400                .graph()
1401                .edges_directed(NodeIndex::new(source.0 as usize), Direction::Outgoing)
1402                .filter(|edge| matches!(edge.weight(), BloqEdge::Order))
1403                .map(|edge| (edge.id(), BloqNodeId(edge.target().index() as u32)))
1404                .collect();
1405            let padding_node = level.add_node(padding_node);
1406            for (edge, _) in &order_edges {
1407                level.graph_mut().remove_edge(*edge);
1408            }
1409            level.add_edge(
1410                source,
1411                padding_node,
1412                BloqEdge::Quantum(Box::new(QuantumEdge {
1413                    guard: None,
1414                    pipes: members
1415                        .iter()
1416                        .map(|member| PipeSeam {
1417                            pipe: member.pipe,
1418                            padding: Some(member.padding),
1419                        })
1420                        .collect(),
1421                })),
1422            );
1423            for (_, target) in order_edges {
1424                level.add_edge(padding_node, target, BloqEdge::Order);
1425            }
1426            padding_node
1427        };
1428
1429        let output_replacements: FxMap<_, _> = replacements
1430            .into_iter()
1431            .map(|(source, padding)| (source, padding.id))
1432            .collect();
1433        retarget_outputs(self, &output_replacements);
1434        Ok(padding_node)
1435    }
1436
1437    fn allocate_padding_instances(
1438        &self,
1439        padding: &[PaddingInstance],
1440    ) -> Result<Vec<TemplateInstance>, EditError> {
1441        let base = match self.max_template_instance_id() {
1442            Some(id) => {
1443                id.0.checked_add(1)
1444                    .ok_or(EditError::TemplateInstanceIdExhausted)?
1445            }
1446            None => 0,
1447        };
1448        let last_index =
1449            u32::try_from(padding.len() - 1).map_err(|_| EditError::TemplateInstanceIdExhausted)?;
1450        let last = base
1451            .checked_add(last_index)
1452            .ok_or(EditError::TemplateInstanceIdExhausted)?;
1453        Ok((base..=last)
1454            .zip(padding)
1455            .map(|(id, instance)| {
1456                TemplateInstance::new(TemplateInstanceId(id), instance.template, instance.offset)
1457            })
1458            .collect())
1459    }
1460
1461    /// Largest template-instance id across all nested regions.
1462    fn max_template_instance_id(&self) -> Option<TemplateInstanceId> {
1463        self.levels()
1464            .flat_map(|(_, level)| level.quantum_nodes())
1465            .flat_map(|(_, quantum)| quantum.instances.iter().map(|i| i.id))
1466            .max()
1467    }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472    use bloq_circuit::CoordCircuit;
1473
1474    use super::*;
1475    use crate::{
1476        BodySelector, ClassicalExpr, InstanceBoundaryOperator, NodeDetectorParity, RegionNode,
1477    };
1478    use glam::IVec3;
1479
1480    /// A detector carrying only a center; `seam_reproduced` ignores parities,
1481    /// which legitimately move to the padding's measurement indices.
1482    fn centered(center: Option<[f64; 2]>) -> NodeDetector {
1483        NodeDetector {
1484            parity: NodeDetectorParity::default(),
1485            coords: center.map(|center| center.into_iter().collect()),
1486        }
1487    }
1488
1489    #[test]
1490    fn touched_bundle_demotion_transfers_guard_and_preserves_shared_snapshot() {
1491        use crate::{
1492            BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse, QuantumGuard,
1493        };
1494
1495        let mut program = Bloq::new();
1496        let bundle = program.add_detector_bundle(DetectorBundle::new(
1497            vec![TemplateId(0)],
1498            vec![BundleDetector {
1499                parity: bloq_circuit::DetectorParity::from_measurements([BundleMeasurement {
1500                    owner: 0,
1501                    measurement: 0,
1502                }]),
1503                coords: Some([1.0, 2.0].into_iter().collect()),
1504            }],
1505        ));
1506        let mut quantum = crate::QuantumNode {
1507            detector_bundles: vec![
1508                DetectorBundleUse {
1509                    bundle,
1510                    instances: vec![TemplateInstanceId(5)],
1511                    offset: IVec2::ZERO,
1512                },
1513                DetectorBundleUse {
1514                    bundle,
1515                    instances: vec![TemplateInstanceId(6)],
1516                    offset: IVec2::ZERO,
1517                },
1518            ],
1519            guards: vec![QuantumGuard {
1520                input: 7,
1521                detector_bundles: vec![0, 1],
1522                ..Default::default()
1523            }],
1524            ..Default::default()
1525        };
1526        let snapshot = quantum.clone();
1527        demote_touched_bundles(
1528            &program,
1529            &mut quantum,
1530            &FxSet::from_iter([TemplateInstanceId(5)]),
1531        )
1532        .unwrap();
1533        assert_eq!(
1534            quantum.detectors[0]
1535                .parity
1536                .measurements()
1537                .next()
1538                .unwrap()
1539                .instance,
1540            TemplateInstanceId(5)
1541        );
1542        assert_eq!(quantum.guards[0].detectors, vec![0]);
1543        assert_eq!(quantum.guards[0].detector_bundles, vec![0]);
1544        assert_eq!(
1545            quantum.detector_bundles[0].instances,
1546            vec![TemplateInstanceId(6)]
1547        );
1548        assert_eq!(snapshot.detector_bundles.len(), 2);
1549        assert!(snapshot.detectors.is_empty());
1550    }
1551
1552    /// The seam check compares center *multisets*: a replacement that keeps
1553    /// every distinct plaquette but loses or gains a chain must be rejected,
1554    /// which is what makes a short padding fail rather than slip through.
1555    #[test]
1556    fn seam_reproduction_counts_repeated_centers() {
1557        let original = [
1558            centered(Some([2.0, 0.0])),
1559            centered(Some([2.0, 0.0])),
1560            centered(Some([4.0, 2.0])),
1561        ];
1562
1563        assert!(seam_reproduced(&original, &original));
1564        // Order must not matter.
1565        assert!(seam_reproduced(
1566            &original,
1567            &[
1568                centered(Some([4.0, 2.0])),
1569                centered(Some([2.0, 0.0])),
1570                centered(Some([2.0, 0.0])),
1571            ]
1572        ));
1573        // Same distinct centers, one chain short.
1574        assert!(!seam_reproduced(
1575            &original,
1576            &[centered(Some([2.0, 0.0])), centered(Some([4.0, 2.0]))]
1577        ));
1578        // Same length, one plaquette swapped.
1579        assert!(!seam_reproduced(
1580            &original,
1581            &[
1582                centered(Some([2.0, 0.0])),
1583                centered(Some([2.0, 0.0])),
1584                centered(Some([6.0, 2.0])),
1585            ]
1586        ));
1587        // A center-less chain is distinct from one centered at the origin.
1588        assert!(!seam_reproduced(
1589            &[centered(None)],
1590            &[centered(Some([0.0, 0.0]))]
1591        ));
1592    }
1593
1594    fn plain_pipe() -> TemporalPipeRef {
1595        TemporalPipeRef {
1596            src: IVec3::new(0, 0, 0),
1597            dst: IVec3::new(0, 0, 1),
1598            hadamard: false,
1599        }
1600    }
1601
1602    /// A flowless two-node seam for testing edit guards.
1603    fn seam_bloq(pipe: TemporalPipeRef) -> (Bloq, BloqNodeId, BloqNodeId, TemplateId) {
1604        let mut bloq = Bloq::new();
1605        let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1606        let mut lower = BloqNode::from_members(vec![]);
1607        lower
1608            .expect_quantum_mut()
1609            .instances
1610            .push(TemplateInstance::new(
1611                TemplateInstanceId(0),
1612                template,
1613                IVec2::ZERO,
1614            ));
1615        let lower = bloq.add_node(lower);
1616        let upper = bloq.add_node(BloqNode::from_members(vec![]));
1617        bloq.add_edge(lower, upper, BloqEdge::quantum(vec![pipe]));
1618        (bloq, lower, upper, template)
1619    }
1620
1621    #[test]
1622    fn empty_padding_is_rejected() {
1623        let (mut bloq, lower, upper, _) = seam_bloq(plain_pipe());
1624        assert!(matches!(
1625            bloq.subdivide_quantum_edge(lower, upper, &[], 1),
1626            Err(EditError::EmptyPadding)
1627        ));
1628    }
1629
1630    #[test]
1631    fn exhausted_template_instance_ids_are_rejected_without_mutation() {
1632        for (existing_id, padding_count) in [(u32::MAX, 1), (u32::MAX - 1, 2)] {
1633            let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1634            bloq.node_mut(lower).unwrap().expect_quantum_mut().instances[0].id =
1635                TemplateInstanceId(existing_id);
1636            let padding = vec![
1637                PaddingInstance {
1638                    template,
1639                    offset: IVec2::ZERO,
1640                };
1641                padding_count
1642            ];
1643
1644            assert!(matches!(
1645                bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1646                Err(EditError::TemplateInstanceIdExhausted)
1647            ));
1648            assert_eq!(bloq.node_count(), 2, "failed edit leaves graph unchanged");
1649            assert_eq!(
1650                bloq.graph()
1651                    .edges_connecting(
1652                        NodeIndex::new(lower.0 as usize),
1653                        NodeIndex::new(upper.0 as usize),
1654                    )
1655                    .count(),
1656                1,
1657                "failed edit preserves the original seam"
1658            );
1659        }
1660    }
1661
1662    #[test]
1663    fn quantum_edge_without_pipes_is_rejected() {
1664        let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1665        let edge = bloq
1666            .graph()
1667            .edges_connecting(
1668                NodeIndex::new(lower.0 as usize),
1669                NodeIndex::new(upper.0 as usize),
1670            )
1671            .next()
1672            .unwrap()
1673            .id();
1674        *bloq.graph_mut_internal().edge_weight_mut(edge).unwrap() = BloqEdge::quantum(vec![]);
1675        let padding = [PaddingInstance {
1676            template,
1677            offset: IVec2::ZERO,
1678        }];
1679
1680        assert!(matches!(
1681            bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1682            Err(EditError::InvalidQuantumEdge { .. })
1683        ));
1684        assert_eq!(bloq.node_count(), 2, "failed edit leaves graph unchanged");
1685    }
1686
1687    #[test]
1688    fn missing_quantum_edge_is_rejected() {
1689        let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1690        let padding = [PaddingInstance {
1691            template,
1692            offset: IVec2::ZERO,
1693        }];
1694        // Reversed direction: no Quantum edge upper -> lower.
1695        assert!(matches!(
1696            bloq.subdivide_quantum_edge(upper, lower, &padding, 1),
1697            Err(EditError::InvalidQuantumEdge { .. })
1698        ));
1699    }
1700
1701    #[test]
1702    fn hadamard_pipe_can_be_subdivided() {
1703        let (mut bloq, lower, upper, template) = seam_bloq(TemporalPipeRef {
1704            hadamard: true,
1705            ..plain_pipe()
1706        });
1707        let padding = [PaddingInstance {
1708            template,
1709            offset: IVec2::ZERO,
1710        }];
1711        bloq.subdivide_quantum_edge(lower, upper, &padding, 1)
1712            .expect("Hadamard provenance on an edge does not block subdivision");
1713    }
1714
1715    #[test]
1716    fn parallel_quantum_edges_are_rejected() {
1717        let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1718        bloq.add_edge(lower, upper, BloqEdge::quantum(vec![plain_pipe()]));
1719        let padding = [PaddingInstance {
1720            template,
1721            offset: IVec2::ZERO,
1722        }];
1723        assert!(matches!(
1724            bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1725            Err(EditError::InvalidQuantumEdge { .. })
1726        ));
1727    }
1728
1729    #[test]
1730    fn self_closing_lower_template_is_rejected() {
1731        use bloq_circuit::{Flow, Pauli, PauliMap};
1732
1733        // Compile-time templates never self-close within one node.
1734        let key: PauliMap = [(IVec2::ZERO, Pauli::Z)].into_iter().collect();
1735        let mut creator = BloqTemplate::new(CoordCircuit::new());
1736        creator.boundary_flows =
1737            vec![Flow::new(PauliMap::empty(), key.clone()).with_measurements([0])];
1738        let mut consumer = BloqTemplate::new(CoordCircuit::new());
1739        consumer.boundary_flows = vec![Flow::new(key, PauliMap::empty()).with_measurements([0])];
1740
1741        let mut bloq = Bloq::new();
1742        let creator = bloq.add_template(creator);
1743        let consumer = bloq.add_template(consumer);
1744        let empty = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1745        let mut lower = BloqNode::from_members(vec![]);
1746        lower.expect_quantum_mut().instances.extend([
1747            TemplateInstance::new(TemplateInstanceId(0), creator, IVec2::ZERO),
1748            TemplateInstance::new(TemplateInstanceId(1), consumer, IVec2::ZERO),
1749        ]);
1750        let lower = bloq.add_node(lower);
1751        let upper = bloq.add_node(BloqNode::from_members(vec![]));
1752        bloq.add_edge(lower, upper, BloqEdge::quantum(vec![plain_pipe()]));
1753
1754        assert!(matches!(
1755            bloq.subdivide_quantum_edge(
1756                lower,
1757                upper,
1758                &[PaddingInstance {
1759                    template: empty,
1760                    offset: IVec2::ZERO,
1761                }],
1762                1,
1763            ),
1764            Err(EditError::InvalidStoredSeam)
1765        ));
1766    }
1767
1768    /// A flowless padding template with one top-level repeat.
1769    fn looped_template(repetitions: u32) -> BloqTemplate {
1770        let mut circuit = CoordCircuit::new();
1771        let body = circuit.add_body(bloq_circuit::CircuitBody::new());
1772        let entry = circuit.entry_body();
1773        circuit
1774            .body_mut(entry)
1775            .expect("entry body exists")
1776            .ops_mut()
1777            .push(Op::Repeat { body, repetitions });
1778        BloqTemplate::new(circuit)
1779    }
1780
1781    /// A seam with one-round and three-round padding templates.
1782    fn seam_bloq_with_padding() -> (Bloq, BloqNodeId, BloqNodeId) {
1783        let (mut bloq, lower, upper, one_round) = seam_bloq(plain_pipe());
1784        let looped = bloq.add_template(looped_template(2));
1785        assert!(bloq.set_quantum_edge_padding(
1786            lower,
1787            upper,
1788            vec![crate::PipePadding {
1789                offset: IVec2::ZERO,
1790                one_round,
1791                looped,
1792            }],
1793        ));
1794        (bloq, lower, upper)
1795    }
1796
1797    #[test]
1798    fn selective_padding_preserves_shared_rows_and_both_record_choices() {
1799        let mut bloq = Bloq::from_text(
1800            "BLOQIR 1
1801template t0 {
1802  circuit {
1803    M (0,0):m0
1804  }
1805  flow _ -> Z(0,0) meas m0
1806}
1807template t1 {
1808  circuit {
1809    M (0,0):m0
1810  }
1811  flow Z(0,0) -> _ meas m0
1812}
1813template t2 {
1814  circuit {
1815    M (0,0):m0
1816    TICK
1817    M (0,0):m1
1818  }
1819  flow Z(0,0) -> _ meas m1
1820}
1821template t3 {
1822  circuit {
1823    M (0,0):m0
1824  }
1825  flow Z(0,0) -> _ meas m0
1826  flow _ -> Z(0,0) meas m0
1827}
1828graph {
1829  n0 quantum {
1830    instance i0 t0 @ (0,0)
1831  }
1832  n1 quantum {
1833    instance i1 t1 @ (0,0)
1834    instance i2 t2 @ (0,0)
1835    detector i0:m0
1836    guard 0 i1 xd0=i1:m0
1837    guard 1 i2 xd0=i2:m1
1838  }
1839  n2 observable fragment measurements i0:m0
1840  n3 compute !in0
1841  n0 -> n1 quantum (0,0,0)>(0,0,1)
1842  n0 -> n2 order
1843  n2 -> n3 value 0
1844  n2 -> n1 value 0
1845  n3 -> n1 value 1
1846}
1847",
1848        )
1849        .unwrap();
1850        bloq.validate().unwrap();
1851        let selection = BloqNodeId(1);
1852        let padding = bloq
1853            .subdivide_quantum_edge(
1854                BloqNodeId(0),
1855                selection,
1856                &[PaddingInstance {
1857                    template: TemplateId(3),
1858                    offset: IVec2::ZERO,
1859                }],
1860                1,
1861            )
1862            .unwrap();
1863        bloq.validate().unwrap();
1864        assert_eq!(bloq[selection].expect_quantum().detectors.len(), 1);
1865        assert_eq!(bloq.value_inputs(selection).count(), 2);
1866        for value in [false, true] {
1867            let selected = bloq[selection]
1868                .select_quantum_members(|slot| Some(if slot == 0 { value } else { !value }))
1869                .unwrap();
1870            let selected = selected.expect_quantum();
1871            let (expected, _) = compose_seam_detectors(
1872                bloq.templates(),
1873                &[
1874                    &bloq[padding].expect_quantum().instances,
1875                    &selected.instances,
1876                ],
1877            )
1878            .unwrap()
1879            .after(0);
1880            assert_eq!(selected.detectors, expected);
1881            assert!(
1882                !selected.detectors[0]
1883                    .parity
1884                    .measurements()
1885                    .any(|term| term.instance == TemplateInstanceId(0))
1886            );
1887        }
1888    }
1889
1890    fn body_path(bloq: &Bloq, region: BloqNodeId) -> LevelPath {
1891        bloq.levels()
1892            .find_map(|(path, _)| {
1893                (path == LevelPath::default().child(region, BodySelector::Body)).then_some(path)
1894            })
1895            .expect("region body path exists")
1896    }
1897
1898    /// The `REPEAT` count of the single padding instance's template on `node`.
1899    fn spliced_repetitions(bloq: &Bloq, node: BloqNodeId) -> Option<u32> {
1900        let instance = &bloq
1901            .node(node)
1902            .expect("node exists")
1903            .expect_quantum()
1904            .instances[0];
1905        bloq.templates()[instance.template_id]
1906            .circuit
1907            .entry_top_level_repeats()
1908            .first()
1909            .copied()
1910    }
1911
1912    #[test]
1913    fn memory_round_insertion_of_zero_rounds_is_rejected() {
1914        let (mut bloq, lower, upper) = seam_bloq_with_padding();
1915        assert!(matches!(
1916            bloq.insert_memory_rounds(
1917                MemoryRoundTarget::Edge {
1918                    path: LevelPath::default(),
1919                    from: lower,
1920                    to: upper
1921                },
1922                0
1923            ),
1924            Err(EditError::EmptyPadding)
1925        ));
1926    }
1927
1928    #[test]
1929    fn memory_round_edge_target_supports_nested_levels() {
1930        let mut bloq = Bloq::new();
1931        let one_round = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1932        let looped = bloq.add_template(looped_template(2));
1933        let pipe = plain_pipe();
1934        let mut body = SubGraph::new();
1935        let mut lower = BloqNode::from_members(vec![]);
1936        lower
1937            .expect_quantum_mut()
1938            .instances
1939            .push(TemplateInstance::new(
1940                TemplateInstanceId(0),
1941                one_round,
1942                IVec2::ZERO,
1943            ));
1944        let lower = body.add_node(lower);
1945        let upper = body.add_node(BloqNode::from_members(vec![]));
1946        body.add_edge(
1947            lower,
1948            upper,
1949            BloqEdge::Quantum(Box::new(QuantumEdge {
1950                guard: None,
1951                pipes: vec![PipeSeam {
1952                    pipe,
1953                    padding: Some(PipePadding {
1954                        offset: IVec2::ZERO,
1955                        one_round,
1956                        looped,
1957                    }),
1958                }],
1959            })),
1960        );
1961        let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
1962            restart_source: None,
1963            restart_condition: ClassicalExpr::Const(false),
1964            body,
1965        }));
1966        let path = body_path(&bloq, region);
1967
1968        let padding = bloq
1969            .insert_memory_rounds(
1970                MemoryRoundTarget::Edge {
1971                    path: path.clone(),
1972                    from: lower,
1973                    to: upper,
1974                },
1975                2,
1976            )
1977            .expect("nested seam padding");
1978
1979        let body = bloq.level_at(&path).expect("body remains addressable");
1980        assert_eq!(body[padding].memory_rounds(), Some(2));
1981        assert!(
1982            body.edges_between(lower, padding)
1983                .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
1984        );
1985        assert!(
1986            body.edges_between(padding, upper)
1987                .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
1988        );
1989
1990        // Activation suppresses physical work, including the retry body.
1991        let guard = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1992            expr: ClassicalExpr::Const(true),
1993        }));
1994        bloq.node_mut(region).unwrap().activation = Some(0);
1995        bloq.add_edge(guard, region, BloqEdge::value(0));
1996        let unchanged = bloq.to_binary();
1997        assert!(matches!(
1998            bloq.insert_memory_rounds(
1999                MemoryRoundTarget::Edge {
2000                    path,
2001                    from: lower,
2002                    to: padding,
2003                },
2004                1,
2005            ),
2006            Err(EditError::MembershipSelectionRequired)
2007        ));
2008        assert_eq!(bloq.to_binary(), unchanged);
2009    }
2010
2011    #[test]
2012    fn memory_round_target_rejects_a_stale_level_path() {
2013        let mut bloq = Bloq::new();
2014        let mut body = SubGraph::new();
2015        let source = body.add_node(BloqNode::from_members(vec![]));
2016        let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2017            restart_source: None,
2018            restart_condition: ClassicalExpr::Const(false),
2019            body,
2020        }));
2021        let path = body_path(&bloq, region);
2022        bloq.remove_node(region);
2023
2024        assert!(matches!(
2025            bloq.insert_memory_rounds(MemoryRoundTarget::After { path, node: source }, 1),
2026            Err(EditError::InvalidLevelPath { .. })
2027        ));
2028    }
2029
2030    #[test]
2031    fn memory_round_after_target_rejects_malformed_nodes_without_mutation() {
2032        let mut bloq = Bloq::new();
2033        let mut body = SubGraph::new();
2034        let classical = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2035            expr: ClassicalExpr::Const(true),
2036        }));
2037        let missing = body.add_node(BloqNode::from_members(vec![]));
2038        body.remove_node(missing);
2039        let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2040            restart_source: None,
2041            restart_condition: ClassicalExpr::Const(false),
2042            body,
2043        }));
2044        let path = body_path(&bloq, region);
2045        let unchanged = bloq.to_binary();
2046
2047        assert!(matches!(
2048            bloq.insert_memory_rounds(
2049                MemoryRoundTarget::After {
2050                    path: path.clone(),
2051                    node: missing,
2052                },
2053                1,
2054            ),
2055            Err(EditError::MemoryRoundTargetMissing { .. })
2056        ));
2057        assert!(matches!(
2058            bloq.insert_memory_rounds(
2059                MemoryRoundTarget::After {
2060                    path,
2061                    node: classical,
2062                },
2063                1,
2064            ),
2065            Err(EditError::MemoryRoundTargetNotQuantum { .. })
2066        ));
2067        assert_eq!(bloq.to_binary(), unchanged);
2068    }
2069
2070    #[test]
2071    fn memory_round_after_target_appends_a_real_terminal_node() {
2072        let mut bloq = Bloq::new();
2073        let one_round = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2074        let looped = bloq.add_template(looped_template(2));
2075        let pipe = plain_pipe();
2076        let mut body = SubGraph::new();
2077        let mut source_node = BloqNode::from_members(vec![]);
2078        source_node
2079            .expect_quantum_mut()
2080            .instances
2081            .push(TemplateInstance::new(
2082                TemplateInstanceId(0),
2083                one_round,
2084                IVec2::ZERO,
2085            ));
2086        let source = body.add_node(source_node);
2087        let include = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2088            index: None,
2089            measurements: Vec::new(),
2090            operators: vec![InstanceBoundaryOperator {
2091                instance: TemplateInstanceId(0),
2092                face: BoundaryFace::Output,
2093                operator: bloq_circuit::PauliMap::empty(),
2094            }],
2095        }));
2096        let guard = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2097            expr: ClassicalExpr::Const(false),
2098        }));
2099        body.node_mut(include).unwrap().activation = Some(0);
2100        body.add_edge(guard, include, BloqEdge::value(0));
2101        body.add_edge(source, include, BloqEdge::Order);
2102        let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2103            restart_source: None,
2104            restart_condition: ClassicalExpr::Const(false),
2105            body,
2106        }));
2107        let upper = bloq.add_node(BloqNode::from_members(vec![]));
2108        bloq.add_edge(
2109            region,
2110            upper,
2111            BloqEdge::Quantum(Box::new(QuantumEdge {
2112                guard: None,
2113                pipes: vec![PipeSeam {
2114                    pipe,
2115                    padding: Some(PipePadding {
2116                        offset: IVec2::ZERO,
2117                        one_round,
2118                        looped,
2119                    }),
2120                }],
2121            })),
2122        );
2123        let path = body_path(&bloq, region);
2124        bloq.set_logical_outputs(vec![crate::LogicalOutput {
2125            port: IVec3::ZERO,
2126            instance: TemplateInstanceId(0),
2127            x: bloq_circuit::PauliMap::empty(),
2128            z: bloq_circuit::PauliMap::empty(),
2129        }]);
2130        let original = bloq.clone();
2131
2132        let padding = bloq
2133            .insert_memory_rounds(
2134                MemoryRoundTarget::After {
2135                    path: path.clone(),
2136                    node: source,
2137                },
2138                2,
2139            )
2140            .expect("terminal padding");
2141
2142        let body = bloq.level_at(&path).expect("body remains addressable");
2143        let padding_instance = body[padding].expect_quantum().instances[0].id;
2144        assert_eq!(body[padding].memory_rounds(), Some(2));
2145        assert!(
2146            body.edges_between(source, padding)
2147                .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
2148        );
2149        assert!(
2150            body.edges_between(padding, include)
2151                .any(|edge| matches!(edge.edge, BloqEdge::Order))
2152        );
2153        assert!(
2154            !body
2155                .edges_between(source, include)
2156                .any(|edge| matches!(edge.edge, BloqEdge::Order))
2157        );
2158        let Some(ClassicalNode::Observable { operators, .. }) = body[include].try_classical()
2159        else {
2160            panic!("include remains classical")
2161        };
2162        assert_eq!(operators[0].instance, padding_instance);
2163        assert_eq!(body[include].activation, Some(0));
2164        assert!(
2165            body.value_inputs(include)
2166                .any(|input| input.producer == guard)
2167        );
2168        assert_eq!(bloq.logical_outputs()[0].instance, padding_instance);
2169        let original_body = original.level_at(&path).unwrap();
2170        let Some(ClassicalNode::Observable { operators, .. }) =
2171            original_body[include].try_classical()
2172        else {
2173            panic!("shared observable remains classical")
2174        };
2175        assert_eq!(operators[0].instance, TemplateInstanceId(0));
2176        assert_eq!(original_body[include].activation, Some(0));
2177        assert_eq!(
2178            original.logical_outputs()[0].instance,
2179            TemplateInstanceId(0)
2180        );
2181    }
2182
2183    #[test]
2184    fn partial_seam_padding_moves_only_its_logical_output_owner() {
2185        let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
2186        bloq.node_mut(lower)
2187            .unwrap()
2188            .expect_quantum_mut()
2189            .instances
2190            .push(TemplateInstance::new(
2191                TemplateInstanceId(1),
2192                template,
2193                IVec2::X,
2194            ));
2195        bloq.set_logical_outputs(
2196            (0..2)
2197                .map(|instance| crate::LogicalOutput {
2198                    port: IVec3::new(instance as i32, 0, 0),
2199                    instance: TemplateInstanceId(instance),
2200                    x: bloq_circuit::PauliMap::empty(),
2201                    z: bloq_circuit::PauliMap::empty(),
2202                })
2203                .collect(),
2204        );
2205        let padding = bloq
2206            .subdivide_quantum_edge(
2207                lower,
2208                upper,
2209                &[PaddingInstance {
2210                    template,
2211                    offset: IVec2::ZERO,
2212                }],
2213                1,
2214            )
2215            .unwrap();
2216        let owner = bloq[padding].expect_quantum().instances[0].id;
2217        assert_eq!(bloq.logical_outputs()[0].instance, owner);
2218        assert_eq!(bloq.logical_outputs()[1].instance, TemplateInstanceId(1));
2219    }
2220
2221    #[test]
2222    fn memory_round_insertion_without_padding_provenance_is_rejected() {
2223        let (mut bloq, lower, upper, _) = seam_bloq(plain_pipe());
2224        assert!(matches!(
2225            bloq.insert_memory_rounds(
2226                MemoryRoundTarget::Edge {
2227                    path: LevelPath::default(),
2228                    from: lower,
2229                    to: upper
2230                },
2231                2
2232            ),
2233            Err(EditError::EdgePaddingMissing { .. })
2234        ));
2235    }
2236
2237    #[test]
2238    fn memory_round_insertion_uses_recorded_templates_without_cloning() {
2239        // Both recorded template forms avoid growing the pool.
2240        for (rounds, expect_repeat) in [(1, None), (3, Some(2))] {
2241            let (mut bloq, lower, upper) = seam_bloq_with_padding();
2242            let pool_before = bloq.templates().len();
2243            let mid = bloq
2244                .insert_memory_rounds(
2245                    MemoryRoundTarget::Edge {
2246                        path: LevelPath::default(),
2247                        from: lower,
2248                        to: upper,
2249                    },
2250                    rounds,
2251                )
2252                .expect("flowless decoder wait splices");
2253            assert_eq!(
2254                bloq.node(mid).expect("mid exists").memory_rounds(),
2255                Some(rounds)
2256            );
2257            assert_eq!(spliced_repetitions(&bloq, mid), expect_repeat);
2258            assert_eq!(bloq.templates().len(), pool_before);
2259        }
2260    }
2261
2262    #[test]
2263    fn template_edits_invalidate_cached_padding_specializations() {
2264        let (mut bloq, lower, upper) = seam_bloq_with_padding();
2265        let pool_before = bloq.templates().len();
2266        let mid = bloq
2267            .insert_memory_rounds(
2268                MemoryRoundTarget::Edge {
2269                    path: LevelPath::default(),
2270                    from: lower,
2271                    to: upper,
2272                },
2273                5,
2274            )
2275            .expect("specialized decoder wait splices");
2276        assert_eq!(spliced_repetitions(&bloq, mid), Some(4));
2277        assert_eq!(bloq.templates().len(), pool_before + 1);
2278
2279        let specialized = bloq[mid].expect_quantum().instances[0].template_id;
2280        bloq.set_template_repetitions(specialized, 99)
2281            .expect("retuning succeeds");
2282
2283        // The stale cached template is not reused.
2284        let second = bloq
2285            .insert_memory_rounds(
2286                MemoryRoundTarget::Edge {
2287                    path: LevelPath::default(),
2288                    from: mid,
2289                    to: upper,
2290                },
2291                5,
2292            )
2293            .expect("second decoder wait splices");
2294        assert_eq!(spliced_repetitions(&bloq, second), Some(4));
2295        assert_eq!(bloq.templates().len(), pool_before + 2);
2296    }
2297
2298    #[test]
2299    fn memory_round_insertion_on_unlooped_looped_template_is_rejected() {
2300        // Corrupt provenance: the looped template has no repeat.
2301        let (mut bloq, lower, upper, flowless) = seam_bloq(plain_pipe());
2302        assert!(bloq.set_quantum_edge_padding(
2303            lower,
2304            upper,
2305            vec![crate::PipePadding {
2306                offset: IVec2::ZERO,
2307                one_round: flowless,
2308                looped: flowless,
2309            }],
2310        ));
2311        assert!(matches!(
2312            bloq.insert_memory_rounds(
2313                MemoryRoundTarget::Edge {
2314                    path: LevelPath::default(),
2315                    from: lower,
2316                    to: upper
2317                },
2318                2
2319            ),
2320            Err(EditError::PaddingTemplateNotLooped { .. })
2321        ));
2322    }
2323
2324    /// Every landmark the seam-locating padding helpers address, in one
2325    /// program: a `RepeatUntilSuccess` whose body escapes into a quantum node,
2326    /// two selective selections, and a plain block-to-block seam.
2327    ///
2328    /// `padded_second_selection` drops the padding provenance from the second
2329    /// selection's seam, which is what makes a mid-batch insertion failure — and
2330    /// so the all-or-nothing contract — reachable.
2331    fn seam_landmarks(padded_second_selection: bool) -> Bloq {
2332        let second = match padded_second_selection {
2333            true => " padding offset (4,0) one t0 loop t1",
2334            false => "",
2335        };
2336        Bloq::from_text(&format!(
2337            "\
2338BLOQIR 1
2339
2340template t0 {{
2341  circuit {{
2342    MPP X(0,0):m0
2343  }}
2344}}
2345
2346template t1 {{
2347  circuit {{
2348    REPEAT 2 b1
2349  }}
2350  body b1 {{
2351    MPP X(0,0):m0
2352  }}
2353}}
2354
2355graph {{
2356  n0 rus in0 source n2 {{
2357    body {{
2358      n0 quantum {{
2359        instance i0 t0 @ (0,0)
2360      }}
2361      n1 quantum {{
2362        instance i1 t0 @ (0,0)
2363      }}
2364      n2 observable fragment measurements i1:m0 from generator 0
2365      n0 -> n1 quantum (0,0,0)>(0,0,1)
2366      n1 -> n2 order
2367    }}
2368  }}
2369  n1 quantum {{
2370    instance i2 t0 @ (0,0)
2371    from blocks (0,0,2)
2372  }}
2373  n2 quantum {{
2374    instance i3 t0 @ (0,0)
2375    instance i9 t0 @ (4,0)
2376    instance i4 t0 @ (0,0)
2377    instance i11 t0 @ (0,0)
2378    instance i10 t0 @ (4,0)
2379    guard 0 i3 i9
2380    guard 1 i4 i11 i10
2381  }}
2382  n6 compute 0
2383  n7 compute !in0
2384  n6 -> n7 value 0
2385  n6 -> n2 value 0
2386  n7 -> n2 value 1
2387  n3 quantum {{
2388    instance i5 t0 @ (4,0)
2389    from blocks (1,0,0)
2390  }}
2391  n4 quantum {{
2392    instance i6 t0 @ (4,0)
2393    from blocks (1,0,1)
2394  }}
2395  n5 quantum {{
2396    instance i7 t0 @ (4,0)
2397    instance i8 t0 @ (4,0)
2398    guard 0 i7
2399    guard 1 i8
2400  }}
2401  n6 -> n5 value 0
2402  n7 -> n5 value 1
2403  n0 -> n1 quantum (0,0,1)>(0,0,2) padding offset (0,0) one t0 loop t1
2404  n1 -> n2 quantum (0,0,2)>(0,0,3) padding offset (0,0) one t0 loop t1
2405  n4 -> n2 quantum (1,0,1)>(1,0,2) padding offset (4,0) one t0 loop t1
2406  n3 -> n4 quantum (1,0,0)>(1,0,1) padding offset (4,0) one t0 loop t1
2407  n4 -> n5 quantum (1,0,1)>(1,0,2){second}
2408}}
2409"
2410        ))
2411        .expect("valid .bloqir text")
2412    }
2413
2414    fn selection_targets(bloq: &Bloq) -> Vec<MemoryRoundTarget> {
2415        bloq.selection_seams()
2416            .unwrap()
2417            .into_iter()
2418            .map(|(from, to)| MemoryRoundTarget::Edge {
2419                path: LevelPath::default(),
2420                from,
2421                to,
2422            })
2423            .collect()
2424    }
2425
2426    #[test]
2427    fn memory_round_batch_subdivides_every_selected_seam() {
2428        let mut bloq = seam_landmarks(true);
2429        let targets = selection_targets(&bloq);
2430        let padding = bloq.insert_memory_rounds_batch(&targets, 3).unwrap();
2431
2432        assert_eq!(padding.len(), 3, "one padding node per selection seam");
2433        for (&padding, selection) in
2434            padding
2435                .iter()
2436                .zip([BloqNodeId(2), BloqNodeId(2), BloqNodeId(5)])
2437        {
2438            assert_eq!(bloq[padding].memory_rounds(), Some(3));
2439            assert!(
2440                bloq.edges_between(padding, selection)
2441                    .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
2442            );
2443        }
2444    }
2445
2446    #[test]
2447    fn memory_round_batch_is_all_or_nothing() {
2448        let mut bloq = seam_landmarks(false);
2449        let targets = selection_targets(&bloq);
2450        let before = bloq.to_binary();
2451        // Earlier seams are valid, but the last has no padding provenance.
2452        assert!(matches!(
2453            bloq.insert_memory_rounds_batch(&targets, 1),
2454            Err(EditError::EdgePaddingMissing { .. })
2455        ));
2456        assert_eq!(bloq.to_binary(), before);
2457    }
2458
2459    #[test]
2460    fn memory_round_batch_mixes_edges_and_region_terminals() {
2461        let mut bloq = seam_landmarks(true);
2462        let path = LevelPath::default().child(BloqNodeId(0), BodySelector::Body);
2463        let escape = bloq.level_at(&path).unwrap().quantum_tail().unwrap();
2464        let mut targets = selection_targets(&bloq);
2465        targets.push(MemoryRoundTarget::After {
2466            path: path.clone(),
2467            node: escape,
2468        });
2469        let padding = bloq.insert_memory_rounds_batch(&targets, 2).unwrap();
2470
2471        assert_eq!(padding.len(), 4);
2472        for &node in &padding[..3] {
2473            assert_eq!(bloq[node].memory_rounds(), Some(2));
2474        }
2475        let body = bloq.level_at(&path).unwrap();
2476        let inserted = padding[3];
2477        assert_eq!(body[inserted].memory_rounds(), Some(2));
2478        assert!(
2479            body.edges_between(escape, inserted)
2480                .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
2481        );
2482        assert_eq!(body.quantum_tail(), Ok(inserted));
2483    }
2484
2485    #[test]
2486    fn memory_round_batch_rolls_back_a_region_terminal_edit() {
2487        let mut bloq = seam_landmarks(false);
2488        let path = LevelPath::default().child(BloqNodeId(0), BodySelector::Body);
2489        let escape = bloq.level_at(&path).unwrap().quantum_tail().unwrap();
2490        let bad_edge = selection_targets(&bloq).pop().unwrap();
2491        let targets = [MemoryRoundTarget::After { path, node: escape }, bad_edge];
2492        let before = bloq.to_binary();
2493        assert!(matches!(
2494            bloq.insert_memory_rounds_batch(&targets, 2),
2495            Err(EditError::EdgePaddingMissing { .. })
2496        ));
2497        assert_eq!(bloq.to_binary(), before);
2498    }
2499
2500    #[test]
2501    fn memory_round_batch_handles_empty_zero_and_duplicate_targets() {
2502        let mut bloq = seam_landmarks(true);
2503        let before = bloq.to_binary();
2504        assert_eq!(bloq.insert_memory_rounds_batch(&[], 1), Ok(Vec::new()));
2505        assert_eq!(bloq.to_binary(), before);
2506        assert_eq!(
2507            bloq.insert_memory_rounds_batch(&[], 0),
2508            Err(EditError::EmptyPadding)
2509        );
2510        let target = selection_targets(&bloq).remove(0);
2511        assert!(matches!(
2512            bloq.insert_memory_rounds_batch(&[target.clone(), target], 1),
2513            Err(EditError::InvalidQuantumEdge { .. })
2514        ));
2515        assert_eq!(bloq.to_binary(), before);
2516    }
2517
2518    #[test]
2519    fn template_repetitions_retune_in_place_without_touching_a_shared_pool() {
2520        let mut bloq = Bloq::new();
2521        let looped = bloq.add_shared_template(std::sync::Arc::new(looped_template(8)));
2522        // A second program sharing the same `Arc` stands in for a template
2523        // handed to a sibling program; the retune must not reach it.
2524        let mut shared = Bloq::new();
2525        let shared_id = shared.add_shared_template(std::sync::Arc::clone(
2526            bloq.templates().iter_shared().next().unwrap().1,
2527        ));
2528        assert!(std::ptr::eq(
2529            &bloq.templates()[looped],
2530            &shared.templates()[shared_id]
2531        ));
2532
2533        bloq.set_template_repetitions(looped, 3)
2534            .expect("a single-loop template retunes");
2535        assert!(!std::ptr::eq(
2536            &bloq.templates()[looped],
2537            &shared.templates()[shared_id]
2538        ));
2539
2540        assert_eq!(
2541            bloq.templates()[looped].circuit.entry_top_level_repeats(),
2542            [3]
2543        );
2544        assert_eq!(
2545            shared.templates()[shared_id]
2546                .circuit
2547                .entry_top_level_repeats(),
2548            [8],
2549            "retuning is copy-on-write"
2550        );
2551    }
2552
2553    #[test]
2554    fn retuning_rejects_a_template_with_no_single_loop() {
2555        let mut bloq = Bloq::new();
2556        let flat = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2557        let mut twice = looped_template(2);
2558        let body = twice.circuit.entry_body();
2559        let repeat = twice.circuit.body(body).expect("entry body").ops()[0].clone();
2560        twice
2561            .circuit
2562            .body_mut(body)
2563            .expect("entry body")
2564            .ops_mut()
2565            .push(repeat);
2566        let twice = bloq.add_template(twice);
2567
2568        assert_eq!(
2569            bloq.set_template_repetitions(flat, 1),
2570            Err(EditError::TemplateNotSingleLooped {
2571                template: flat,
2572                repeats: 0,
2573            })
2574        );
2575        assert_eq!(
2576            bloq.set_template_repetitions(twice, 1),
2577            Err(EditError::TemplateNotSingleLooped {
2578                template: twice,
2579                repeats: 2,
2580            })
2581        );
2582        assert_eq!(
2583            bloq.set_template_repetitions(TemplateId(9), 1),
2584            Err(EditError::UnknownTemplate {
2585                template: TemplateId(9),
2586            })
2587        );
2588    }
2589
2590    #[test]
2591    fn splice_rewires_edge_and_allocates_fresh_instance_id() {
2592        let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
2593        let padding = [PaddingInstance {
2594            template,
2595            offset: IVec2::ZERO,
2596        }];
2597        let mid = bloq
2598            .subdivide_quantum_edge(lower, upper, &padding, 1)
2599            .expect("flowless seam splice succeeds");
2600
2601        // The original edge is gone.
2602        assert!(matches!(
2603            bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
2604            Err(EditError::InvalidQuantumEdge { .. })
2605        ));
2606        let order = bloq
2607            .deterministic_emit_order()
2608            .expect("spliced program is acyclic");
2609        assert_eq!(order, vec![lower, mid, upper]);
2610
2611        let mid_node = bloq.node(mid).expect("padding node exists");
2612        assert_eq!(mid_node.layer(), 1, "padding sits on the pipe layer");
2613        assert_eq!(
2614            mid_node.memory_rounds(),
2615            Some(1),
2616            "padding is identifiable and carries its round count"
2617        );
2618        // The lower node already uses id 0.
2619        assert_eq!(
2620            mid_node.expect_quantum().instances[0].id,
2621            TemplateInstanceId(1)
2622        );
2623        bloq.validate().expect("spliced program validates");
2624    }
2625}