Skip to main content

bloq_compile/lower/
qubit_layout.rs

1use std::collections::BTreeMap;
2use std::ops::Bound::Excluded;
3
4use bloq_graph::BlockGraph;
5use bloq_ir::lowering::{NodeTemplateInstanceMergeError, PredicateAnalysis, TemplateInstance};
6use bloq_ir::{
7    Bloq, BloqNode, BloqNodeId, BloqValidationError, NodeProvenance, SubGraph, TemplateId, ValueRef,
8};
9use glam::{IVec2, IVec3};
10
11use crate::CompileError;
12
13/// Validate same-layer physical-qubit ownership in `bloq`.
14///
15/// Two nodes may overlap where source adjacency permits it or their predicates
16/// are mutually exclusive. Validation runs at every graph level, so nodes inside
17/// RUS retry bodies are covered. Each
18/// level is checked in isolation: separate `SubGraph`s never cross-check, which
19/// is exactly the intended semantics — a region reports its whole footprint as
20/// one node at the parent level, and each body has its own execution scope.
21/// Hierarchical source graphs are explicitly projected to their flat topology
22/// for this audit; native compilation retains its definition ownership.
23///
24/// # Errors
25///
26/// Returns [`CompileError::OverlappingNodeQubit`] for the first disallowed overlap.
27pub fn validate_bloq_qubit_layout_for_source(
28    bloq: &Bloq,
29    source: &BlockGraph,
30) -> Result<(), CompileError> {
31    let flat = source
32        .has_module_structure()
33        .then(|| source.flatten())
34        .transpose()?;
35    validate_bloq_qubit_layout_with_limits(
36        bloq,
37        flat.as_ref().unwrap_or(source),
38        bloq_utils::boolean::BooleanLimits::DEFAULT,
39    )
40}
41
42pub(crate) fn validate_bloq_qubit_layout_with_limits(
43    bloq: &Bloq,
44    source: &BlockGraph,
45    limits: bloq_utils::boolean::BooleanLimits,
46) -> Result<(), CompileError> {
47    let mut template_runs = crate::FxMap::default();
48    for (path, level) in bloq.levels() {
49        // Inside a region body, same-layer nodes may legitimately share a
50        // physical footprint when they are temporally *sequential* rather than
51        // concurrent — a T block's cultivation and escape stages sit at the same
52        // layer and qubits, joined by a temporal seam edge. So bodies skip pairs
53        // with a directed path between them and flag only genuinely concurrent
54        // (no path either way) same-layer overlaps. Top-level Region nodes use
55        // the same ordered sharing with adjacent lowering nodes.
56        let skip_sequential = !path.segments().is_empty();
57        validate_level_qubit_layout(
58            bloq,
59            source,
60            level,
61            skip_sequential,
62            limits,
63            &mut template_runs,
64        )?;
65    }
66    Ok(())
67}
68
69/// Runs the same-layer overlap check within a single graph level. When
70/// `skip_sequential` is set (region bodies), pairs joined by a directed path are
71/// exempt because they occupy the shared footprint at different times.
72fn validate_level_qubit_layout(
73    bloq: &Bloq,
74    source: &BlockGraph,
75    level: &SubGraph,
76    skip_sequential: bool,
77    limits: bloq_utils::boolean::BooleanLimits,
78    template_runs: &mut crate::FxMap<TemplateId, Vec<QubitRun>>,
79) -> Result<(), CompileError> {
80    let mut layers = BTreeMap::<_, Vec<_>>::new();
81    let mut guarded = false;
82    for (id, node) in level.nodes() {
83        if node.try_quantum().is_none() && node.try_region().is_none() {
84            continue;
85        }
86        guarded |= node.activation.is_some()
87            || node
88                .try_quantum()
89                .is_some_and(|quantum| !quantum.guards.is_empty());
90        layers.entry(node.layer()).or_default().push((id, node));
91    }
92    if guarded && !level.is_acyclic() {
93        return Err(BloqValidationError::CyclicGraph.into());
94    }
95    let mut predicates = PredicateAnalysis::with_limits(level, limits);
96    let mut allowed_pairs = crate::FxMap::default();
97    let mut scratch = level.path_scratch();
98    for nodes in layers.values() {
99        // One interval index per row and x parity. A stride-2 run stays one
100        // interval regardless of code distance.
101        let mut occupied = crate::FxMap::<(i32, i32), BTreeMap<i64, OccupiedRun>>::default();
102        for &(right_id, right) in nodes {
103            let footprints = guarded_footprints(bloq, level, right_id, right, template_runs)?;
104            let mut conflict = None;
105            for footprint in &footprints {
106                for run in &footprint.runs {
107                    let Some(row) = occupied.get(&(run.y, run.parity)) else {
108                        continue;
109                    };
110                    let candidates = row
111                        .range(..=run.start)
112                        .next_back()
113                        .into_iter()
114                        .chain(row.range((Excluded(run.start), Excluded(run.end))));
115                    for (&start, interval) in candidates {
116                        let x_index = run.start.max(start);
117                        if x_index >= interval.end {
118                            continue;
119                        }
120                        let x = (2 * x_index + i64::from(run.parity)) as i32;
121                        for &(left_id, left_guard) in &interval.users {
122                            let allowed =
123                                *allowed_pairs.entry((left_id, right_id)).or_insert_with(|| {
124                                    let left = &level[left_id];
125                                    let region_pair =
126                                        left.try_region().is_some() || right.try_region().is_some();
127                                    allows_same_layer_qubit_overlap(source, left, right)
128                                        || ((skip_sequential || region_pair)
129                                            && (level.has_path_with_scratch(
130                                                left_id,
131                                                right_id,
132                                                &mut scratch,
133                                            ) || level.has_path_with_scratch(
134                                                right_id,
135                                                left_id,
136                                                &mut scratch,
137                                            )))
138                                });
139                            if allowed
140                                || !predicates
141                                    .overlap_values(left_guard, footprint.guard)
142                                    .map_err(|source| match source {
143                                        NodeTemplateInstanceMergeError::BooleanResource(
144                                            resource,
145                                        ) => CompileError::from(resource),
146                                        source => {
147                                            BloqValidationError::InvalidInstanceMergeStructure {
148                                                node: right_id,
149                                                source,
150                                            }
151                                            .into()
152                                        }
153                                    })?
154                            {
155                                continue;
156                            }
157                            let key = (left_id.0, x, run.y);
158                            if conflict.is_none_or(|previous| key < previous) {
159                                conflict = Some(key);
160                            }
161                        }
162                    }
163                }
164            }
165            if let Some((left, x, y)) = conflict {
166                return Err(CompileError::OverlappingNodeQubit {
167                    first: BloqNodeId(left),
168                    second: right_id,
169                    qubit: IVec2::new(x, y),
170                });
171            }
172            for footprint in footprints {
173                for run in footprint.runs {
174                    insert_occupied(
175                        occupied.entry((run.y, run.parity)).or_default(),
176                        run,
177                        (right_id, footprint.guard),
178                    );
179                }
180            }
181        }
182    }
183    Ok(())
184}
185
186struct GuardedFootprint {
187    guard: Option<ValueRef>,
188    runs: Vec<QubitRun>,
189}
190
191#[derive(Clone, Copy, Debug)]
192pub(super) struct QubitRun {
193    pub(super) y: i32,
194    pub(super) parity: i32,
195    pub(super) start: i64,
196    pub(super) end: i64,
197}
198
199impl QubitRun {
200    pub(super) fn translated(
201        self,
202        offset: IVec2,
203    ) -> Result<Self, bloq_circuit::CoordinateOverflowError> {
204        let first = IVec2::new((2 * self.start + i64::from(self.parity)) as i32, self.y);
205        let last = IVec2::new((2 * (self.end - 1) + i64::from(self.parity)) as i32, self.y);
206        let first = bloq_circuit::checked_translate_coordinate(first, offset)?;
207        let last = bloq_circuit::checked_translate_coordinate(last, offset)?;
208        Ok(Self {
209            y: first.y,
210            parity: first.x.rem_euclid(2),
211            start: i64::from(first.x.div_euclid(2)),
212            end: i64::from(last.x.div_euclid(2)) + 1,
213        })
214    }
215}
216
217struct OccupiedRun {
218    end: i64,
219    users: Vec<(BloqNodeId, Option<ValueRef>)>,
220}
221
222fn merge_runs(mut runs: Vec<QubitRun>) -> Vec<QubitRun> {
223    runs.sort_unstable_by_key(|run| (run.y, run.parity, run.start, run.end));
224    let mut merged: Vec<QubitRun> = Vec::with_capacity(runs.len());
225    for run in runs {
226        if let Some(last) = merged.last_mut()
227            && last.y == run.y
228            && last.parity == run.parity
229            && run.start <= last.end
230        {
231            last.end = last.end.max(run.end);
232        } else {
233            merged.push(run);
234        }
235    }
236    merged
237}
238
239pub(super) fn template_qubit_runs(qubits: &[IVec2]) -> Vec<QubitRun> {
240    merge_runs(
241        qubits
242            .iter()
243            .map(|qubit| QubitRun {
244                y: qubit.y,
245                parity: qubit.x.rem_euclid(2),
246                start: i64::from(qubit.x.div_euclid(2)),
247                end: i64::from(qubit.x.div_euclid(2)) + 1,
248            })
249            .collect(),
250    )
251}
252
253fn append_instance_runs(
254    bloq: &Bloq,
255    node: &BloqNode,
256    instance: &TemplateInstance,
257    template_runs: &mut crate::FxMap<TemplateId, Vec<QubitRun>>,
258    runs: &mut Vec<QubitRun>,
259) -> Result<(), CompileError> {
260    let Some(template) = bloq.templates().get(instance.template_id) else {
261        return Ok(());
262    };
263    let local = template_runs
264        .entry(instance.template_id)
265        .or_insert_with(|| template_qubit_runs(template.qubits()));
266    for run in local {
267        let run = match run.translated(instance.offset) {
268            Ok(run) => run,
269            Err(_) => {
270                // Template qubits are stored in arbitrary order. Re-scan that
271                // order to keep the old first-overflow diagnostic exactly.
272                return Err(bloq
273                    .node_qubits(node)
274                    .expect_err("a run endpoint overflowed")
275                    .into());
276            }
277        };
278        runs.push(run);
279    }
280    Ok(())
281}
282
283fn append_node_runs(
284    bloq: &Bloq,
285    node: &BloqNode,
286    template_runs: &mut crate::FxMap<TemplateId, Vec<QubitRun>>,
287    runs: &mut Vec<QubitRun>,
288) -> Result<(), CompileError> {
289    if let Some(region) = node.try_region() {
290        for (_, body) in region.bodies() {
291            for (_, body_node) in body.nodes() {
292                append_node_runs(bloq, body_node, template_runs, runs)?;
293            }
294        }
295    } else if let Some(quantum) = node.try_quantum() {
296        let mut seen = crate::FxSet::default();
297        for instance in &quantum.instances {
298            if seen.insert((instance.template_id, instance.offset)) {
299                append_instance_runs(bloq, node, instance, template_runs, runs)?;
300            }
301        }
302    }
303    Ok(())
304}
305
306fn split_occupied_at(row: &mut BTreeMap<i64, OccupiedRun>, x: i64) {
307    if let Some((&start, interval)) = row.range(..x).next_back()
308        && x < interval.end
309    {
310        let end = interval.end;
311        let users = interval.users.clone();
312        row.get_mut(&start).expect("occupied interval exists").end = x;
313        row.insert(x, OccupiedRun { end, users });
314    }
315}
316
317fn insert_occupied(
318    row: &mut BTreeMap<i64, OccupiedRun>,
319    run: QubitRun,
320    user: (BloqNodeId, Option<ValueRef>),
321) {
322    split_occupied_at(row, run.start);
323    split_occupied_at(row, run.end);
324    let mut gaps = Vec::new();
325    let mut cursor = run.start;
326    for (&start, interval) in row.range_mut(run.start..run.end) {
327        if cursor < start {
328            gaps.push((cursor, start));
329        }
330        interval.users.push(user);
331        cursor = interval.end;
332    }
333    if cursor < run.end {
334        gaps.push((cursor, run.end));
335    }
336    for (start, end) in gaps {
337        row.insert(
338            start,
339            OccupiedRun {
340                end,
341                users: vec![user],
342            },
343        );
344    }
345}
346
347fn guarded_footprints(
348    bloq: &Bloq,
349    level: &SubGraph,
350    id: BloqNodeId,
351    node: &BloqNode,
352    template_runs: &mut crate::FxMap<TemplateId, Vec<QubitRun>>,
353) -> Result<Vec<GuardedFootprint>, CompileError> {
354    let Some(quantum) = node
355        .try_quantum()
356        .filter(|quantum| !quantum.guards.is_empty())
357    else {
358        let guard = node
359            .activation
360            .map(|slot| {
361                level
362                    .value_inputs(id)
363                    .find(|input| input.slot == slot)
364                    .map(|input| input.value_ref().expect("value input"))
365                    .ok_or(BloqValidationError::MissingClassicalInput { node: id, slot })
366            })
367            .transpose()?;
368        let mut runs = Vec::new();
369        append_node_runs(bloq, node, template_runs, &mut runs)?;
370        return Ok(vec![GuardedFootprint {
371            guard,
372            runs: merge_runs(runs),
373        }]);
374    };
375    // Keep the first producer for each slot, as the previous per-guard search did.
376    let mut producers = crate::FxMap::default();
377    for input in level.value_inputs(id) {
378        producers
379            .entry(input.slot)
380            .or_insert(input.value_ref().expect("value input"));
381    }
382    let producer = |slot| {
383        producers
384            .get(&slot)
385            .copied()
386            .ok_or(BloqValidationError::MissingClassicalInput { node: id, slot })
387    };
388    let instance_ids = quantum
389        .instances
390        .iter()
391        .map(|instance| instance.id)
392        .collect::<crate::FxSet<_>>();
393    let mut guards = crate::FxMap::default();
394    for guard in &quantum.guards {
395        let predicate = producer(guard.input)?;
396        for &instance in &guard.instances {
397            if !instance_ids.contains(&instance) || guards.insert(instance, predicate).is_some() {
398                return Err(BloqValidationError::InvalidInstanceMergeStructure {
399                    node: id,
400                    source: NodeTemplateInstanceMergeError::InvalidMembership(format!(
401                        "instance i{} is missing or registered more than once",
402                        instance.0,
403                    )),
404                }
405                .into());
406            }
407        }
408    }
409    let mut groups = crate::FxMap::<Option<ValueRef>, Vec<QubitRun>>::default();
410    let mut seen = crate::FxSet::default();
411    for instance in &quantum.instances {
412        let guard = guards.get(&instance.id).copied();
413        let runs = groups.entry(guard).or_default();
414        if seen.insert((guard, instance.template_id, instance.offset)) {
415            append_instance_runs(bloq, node, instance, template_runs, runs)?;
416        }
417    }
418    Ok(groups
419        .into_iter()
420        .map(|(guard, runs)| GuardedFootprint {
421            guard,
422            runs: merge_runs(runs),
423        })
424        .collect())
425}
426
427fn allows_same_layer_qubit_overlap(source: &BlockGraph, left: &BloqNode, right: &BloqNode) -> bool {
428    let NodeProvenance::BlockComponent {
429        members: left_members,
430    } = &left.provenance
431    else {
432        return false;
433    };
434    let NodeProvenance::BlockComponent {
435        members: right_members,
436    } = &right.provenance
437    else {
438        return false;
439    };
440    if left_members.is_empty() || right_members.is_empty() {
441        return false;
442    }
443
444    left_members.iter().all(|left| {
445        right_members
446            .iter()
447            .all(|right| walking_soft_corridor_overlap_allowed(source, left.pos, right.pos))
448    })
449}
450
451fn walking_soft_corridor_overlap_allowed(source: &BlockGraph, left: IVec3, right: IVec3) -> bool {
452    if left == right {
453        return false;
454    }
455    let Some(left_block) = source.get_block(left) else {
456        return false;
457    };
458    let Some(right_block) = source.get_block(right) else {
459        return false;
460    };
461    let left_kind = left_block.kind();
462    let right_kind = right_block.kind();
463    let right_offsets = right_kind.reserved_offsets();
464    let mut has_allowed_overlap = false;
465
466    for left_offset in left_kind.reserved_offsets() {
467        let overlap_pos = left + left_offset;
468        if right_offsets
469            .iter()
470            .any(|&right_offset| right + right_offset == overlap_pos)
471        {
472            if !left_kind.allows_reserved_overlap(left, right_kind, right, overlap_pos) {
473                return false;
474            }
475            has_allowed_overlap = true;
476        }
477    }
478
479    has_allowed_overlap
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use bloq_circuit::{CoordCircuit, GateType};
486    use bloq_graph::{Block, BlockKind, WalkingBoundaryKind, WalkingKind};
487    use bloq_ir::{
488        BloqEdge, ClassicalExpr, RegionNode, SourceBlockRef,
489        lowering::{BloqTemplate, TemplateInstanceId},
490    };
491    use glam::{ivec2, ivec3};
492
493    fn single_qubit_node(
494        bloq: &mut Bloq,
495        pos: glam::IVec3,
496        gate: GateType,
497        instance_id: TemplateInstanceId,
498    ) -> BloqNode {
499        let mut circuit = CoordCircuit::new();
500        circuit.do_gate(gate, [ivec2(0, 0)]).unwrap();
501        let template = bloq.add_template(BloqTemplate::new(circuit));
502        let mut node = BloqNode::from_members(vec![SourceBlockRef { pos }]);
503        node.expect_quantum_mut()
504            .instances
505            .push(TemplateInstance::new(instance_id, template, ivec2(0, 0)));
506        node
507    }
508
509    fn same_layer_overlap_bloq(left: glam::IVec3, right: glam::IVec3) -> Bloq {
510        let mut bloq = Bloq::new();
511        let first = single_qubit_node(&mut bloq, left, GateType::H, TemplateInstanceId(0));
512        bloq.add_node(first);
513        let second = single_qubit_node(&mut bloq, right, GateType::X, TemplateInstanceId(1));
514        bloq.add_node(second);
515        bloq
516    }
517
518    #[test]
519    fn interval_layout_matches_coordinate_oracle() {
520        let cases = [
521            vec![
522                vec![(
523                    vec![ivec2(-5, -1), ivec2(-3, -1), ivec2(-1, -1), ivec2(1, -1)],
524                    IVec2::ZERO,
525                )],
526                vec![(vec![ivec2(-5, -1), ivec2(0, -1)], IVec2::X)],
527            ],
528            vec![
529                vec![(vec![ivec2(-5, -2), ivec2(-3, -2), ivec2(-1, -2)], IVec2::X)],
530                vec![(vec![ivec2(-5, -2), ivec2(-3, -2)], IVec2::ZERO)],
531            ],
532            vec![
533                vec![
534                    (vec![ivec2(0, 0), ivec2(2, 0), ivec2(4, 0)], IVec2::ZERO),
535                    (vec![ivec2(0, 0), ivec2(2, 0)], ivec2(2, 0)),
536                ],
537                vec![(vec![ivec2(6, 0)], IVec2::ZERO)],
538            ],
539            vec![
540                vec![(vec![ivec2(i32::MAX, 1)], IVec2::ZERO)],
541                vec![(vec![ivec2(i32::MIN, 1)], IVec2::ZERO)],
542                vec![(vec![ivec2(i32::MIN, 1), ivec2(i32::MAX, 1)], IVec2::ZERO)],
543            ],
544        ];
545        for placements in cases {
546            let mut bloq = Bloq::new();
547            let mut coordinates = Vec::new();
548            for (node_index, instances) in placements.into_iter().enumerate() {
549                let mut node = BloqNode::from_members(vec![SourceBlockRef {
550                    pos: ivec3(node_index as i32, 0, 0),
551                }]);
552                for (instance_index, (qubits, offset)) in instances.into_iter().enumerate() {
553                    let mut circuit = CoordCircuit::new();
554                    for qubit in qubits {
555                        circuit.do_gate(GateType::H, [qubit]).unwrap();
556                    }
557                    let template = bloq.add_template(BloqTemplate::new(circuit));
558                    node.expect_quantum_mut()
559                        .instances
560                        .push(TemplateInstance::new(
561                            TemplateInstanceId(instance_index as u32),
562                            template,
563                            offset,
564                        ));
565                }
566                coordinates.push(bloq.node_qubits(&node).unwrap());
567                bloq.add_node(node);
568            }
569            let expected = (0..coordinates.len()).find_map(|right| {
570                let mut conflict = None;
571                for left in 0..right {
572                    for qubit in coordinates[left].intersection(&coordinates[right]) {
573                        let key = (left as u32, qubit.x, qubit.y);
574                        if conflict.is_none_or(|previous| key < previous) {
575                            conflict = Some(key);
576                        }
577                    }
578                }
579                conflict
580                    .map(|(left, x, y)| (BloqNodeId(left), BloqNodeId(right as u32), ivec2(x, y)))
581            });
582            let actual = validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new());
583            match (expected, actual) {
584                (None, Ok(())) => {}
585                (
586                    Some((first, second, qubit)),
587                    Err(CompileError::OverlappingNodeQubit {
588                        first: actual_first,
589                        second: actual_second,
590                        qubit: actual_qubit,
591                    }),
592                ) => assert_eq!(
593                    (actual_first, actual_second, actual_qubit),
594                    (first, second, qubit)
595                ),
596                (expected, actual) => panic!("coordinate oracle {expected:?}, layout {actual:?}"),
597            }
598        }
599    }
600
601    #[test]
602    fn interval_layout_preserves_first_coordinate_overflow() {
603        let mut bloq = Bloq::new();
604        let mut circuit = CoordCircuit::new();
605        circuit.do_gate(GateType::H, [ivec2(i32::MAX, 0)]).unwrap();
606        circuit.do_gate(GateType::H, [ivec2(i32::MIN, 0)]).unwrap();
607        let template = bloq.add_template(BloqTemplate::new(circuit));
608        let mut node = BloqNode::from_members(vec![SourceBlockRef { pos: IVec3::ZERO }]);
609        node.expect_quantum_mut()
610            .instances
611            .push(TemplateInstance::new(
612                TemplateInstanceId(0),
613                template,
614                IVec2::X,
615            ));
616        let expected = bloq.node_qubits(&node).unwrap_err();
617        bloq.add_node(node);
618        assert!(matches!(
619            validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()),
620            Err(CompileError::CoordinateOverflow(actual)) if actual == expected
621        ));
622    }
623
624    #[test]
625    fn interval_insert_matches_coordinate_users_after_overlaps() {
626        let mut row = BTreeMap::new();
627        let mut oracle = BTreeMap::<i64, Vec<_>>::new();
628        for (id, start, end) in [(0, 0, 8), (1, 2, 6), (2, -2, 3), (3, 5, 10), (4, 0, 8)] {
629            let user = (BloqNodeId(id), None);
630            insert_occupied(
631                &mut row,
632                QubitRun {
633                    y: 0,
634                    parity: 0,
635                    start,
636                    end,
637                },
638                user,
639            );
640            for x in start..end {
641                oracle.entry(x).or_default().push(user);
642            }
643        }
644        for x in -3..11 {
645            let actual = row
646                .range(..=x)
647                .next_back()
648                .filter(|(_, interval)| x < interval.end)
649                .map(|(_, interval)| interval.users.as_slice());
650            assert_eq!(actual, oracle.get(&x).map(Vec::as_slice), "x={x}");
651        }
652    }
653
654    #[test]
655    fn guarded_overlap_inserts_both_users_for_later_node() {
656        use bloq_ir::{ClassicalNode, QuantumGuard};
657
658        fn row_node(bloq: &mut Bloq, xs: &[i32], id: u32) -> BloqNode {
659            let mut circuit = CoordCircuit::new();
660            for &x in xs {
661                circuit.do_gate(GateType::H, [ivec2(x, 0)]).unwrap();
662            }
663            let template = bloq.add_template(BloqTemplate::new(circuit));
664            let mut node = BloqNode::from_members(vec![SourceBlockRef {
665                pos: ivec3(id as i32, 0, 0),
666            }]);
667            node.expect_quantum_mut()
668                .instances
669                .push(TemplateInstance::new(
670                    TemplateInstanceId(id),
671                    template,
672                    IVec2::ZERO,
673                ));
674            node
675        }
676
677        let mut bloq = Bloq::new();
678        let yes = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
679            expr: ClassicalExpr::Const(true),
680        }));
681        let no = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
682            expr: ClassicalExpr::Const(false),
683        }));
684        let mut first = row_node(&mut bloq, &[0, 2, 4, 6], 0);
685        first.expect_quantum_mut().guards.push(QuantumGuard {
686            input: 0,
687            instances: vec![TemplateInstanceId(0)],
688            ..Default::default()
689        });
690        let first = bloq.add_node(first);
691        bloq.add_edge(yes, first, BloqEdge::value(0));
692        let mut second = row_node(&mut bloq, &[2, 4, 6, 8], 1);
693        second.expect_quantum_mut().guards.push(QuantumGuard {
694            input: 0,
695            instances: vec![TemplateInstanceId(1)],
696            ..Default::default()
697        });
698        let second = bloq.add_node(second);
699        bloq.add_edge(no, second, BloqEdge::value(0));
700        let third_node = row_node(&mut bloq, &[6, 8], 2);
701        let third = bloq.add_node(third_node);
702        let error = validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()).unwrap_err();
703        assert!(matches!(
704            error,
705            CompileError::OverlappingNodeQubit {
706                first: actual_first,
707                second: actual_second,
708                qubit,
709            } if actual_first == first && actual_second == third && qubit == ivec2(6, 0)
710        ));
711    }
712
713    #[test]
714    fn guarded_layout_preserves_member_and_region_availability() {
715        use bloq_ir::{ClassicalNode, QuantumGuard};
716
717        let mut bloq = Bloq::new();
718        let raw = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
719        let guard = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
720            expr: ClassicalExpr::In(0),
721        }));
722        bloq.add_edge(raw, guard, BloqEdge::value(0));
723        let inverse = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
724            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
725        }));
726        bloq.add_edge(guard, inverse, BloqEdge::value(0));
727        let mut left =
728            single_qubit_node(&mut bloq, IVec3::ZERO, GateType::H, TemplateInstanceId(0));
729        let extra = single_qubit_node(&mut bloq, IVec3::ZERO, GateType::X, TemplateInstanceId(1));
730        left.expect_quantum_mut()
731            .instances
732            .extend(extra.expect_quantum().instances.iter().copied());
733        left.expect_quantum_mut().instances[1].offset = IVec2::X;
734        left.expect_quantum_mut().guards.push(QuantumGuard {
735            input: 0,
736            instances: vec![TemplateInstanceId(0)],
737            ..Default::default()
738        });
739        let left = bloq.add_node(left);
740        bloq.add_edge(guard, left, BloqEdge::value(0));
741        let mut right =
742            single_qubit_node(&mut bloq, IVec3::ZERO, GateType::H, TemplateInstanceId(2));
743        right.expect_quantum_mut().guards.push(QuantumGuard {
744            input: 0,
745            instances: vec![TemplateInstanceId(2)],
746            ..Default::default()
747        });
748        let right = bloq.add_node(right);
749        bloq.add_edge(inverse, right, BloqEdge::value(0));
750
751        validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()).unwrap();
752        let error = validate_bloq_qubit_layout_with_limits(
753            &bloq,
754            &BlockGraph::new(),
755            bloq_utils::boolean::BooleanLimits {
756                max_steps: 0,
757                ..bloq_utils::boolean::BooleanLimits::UNLIMITED
758            },
759        )
760        .unwrap_err();
761        assert!(matches!(&error, CompileError::BooleanResource(resource) if resource.limit == 0));
762        assert!(error.resource_limit_help().is_some());
763        bloq.node_mut(left).unwrap().expect_quantum_mut().instances[1].offset = IVec2::ZERO;
764        assert!(matches!(
765            validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()),
766            Err(CompileError::OverlappingNodeQubit { .. })
767        ));
768
769        bloq.node_mut(left).unwrap().expect_quantum_mut().instances[1].offset = IVec2::X;
770        let mut child = bloq[right].clone();
771        child.expect_quantum_mut().guards.clear();
772        let mut body = SubGraph::new();
773        body.add_node(child);
774        let mut region = BloqNode::region(RegionNode::RepeatUntilSuccess {
775            restart_condition: ClassicalExpr::Const(false),
776            restart_source: None,
777            body,
778        });
779        region.activation = Some(0);
780        *bloq.node_mut(right).unwrap() = region;
781        validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()).unwrap();
782
783        bloq.node_mut(inverse).unwrap().kind = bloq_ir::BloqNodeKind::Classical(
784            ClassicalNode::Compute {
785                expr: ClassicalExpr::Or(Box::new([
786                    ClassicalExpr::In(0),
787                    ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
788                ])),
789            }
790            .into(),
791        );
792        assert!(matches!(
793            validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new()),
794            Err(CompileError::OverlappingNodeQubit { .. })
795        ));
796    }
797
798    #[test]
799    fn guarded_layout_keeps_missing_input_before_membership_errors() {
800        use bloq_ir::{ClassicalNode, QuantumGuard};
801
802        let mut bloq = Bloq::new();
803        let mut node =
804            single_qubit_node(&mut bloq, IVec3::ZERO, GateType::H, TemplateInstanceId(0));
805        node.expect_quantum_mut().guards.push(QuantumGuard {
806            input: 7,
807            instances: vec![TemplateInstanceId(99)],
808            ..Default::default()
809        });
810        let id = bloq.add_node(node);
811        assert!(matches!(
812            guarded_footprints(&bloq, bloq.top(), id, &bloq[id], &mut crate::FxMap::default()),
813            Err(CompileError::BloqValidation(
814                BloqValidationError::MissingClassicalInput { node, slot: 7 }
815            )) if node == id
816        ));
817
818        let predicate = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
819            expr: ClassicalExpr::Const(true),
820        }));
821        bloq.add_edge(predicate, id, BloqEdge::value(7));
822        assert!(matches!(
823            guarded_footprints(&bloq, bloq.top(), id, &bloq[id], &mut crate::FxMap::default()),
824            Err(CompileError::BloqValidation(
825                BloqValidationError::InvalidInstanceMergeStructure {
826                    node,
827                    source: NodeTemplateInstanceMergeError::InvalidMembership(_)
828                }
829            )) if node == id
830        ));
831
832        bloq.node_mut(id).unwrap().expect_quantum_mut().guards[0].instances =
833            vec![TemplateInstanceId(0), TemplateInstanceId(0)];
834        assert!(matches!(
835            guarded_footprints(&bloq, bloq.top(), id, &bloq[id], &mut crate::FxMap::default()),
836            Err(CompileError::BloqValidation(
837                BloqValidationError::InvalidInstanceMergeStructure {
838                    node,
839                    source: NodeTemplateInstanceMergeError::InvalidMembership(_)
840                }
841            )) if node == id
842        ));
843
844        bloq.node_mut(id).unwrap().expect_quantum_mut().guards[0].instances =
845            vec![TemplateInstanceId(0)];
846        let other = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
847            expr: ClassicalExpr::Const(false),
848        }));
849        bloq.add_edge(other, id, BloqEdge::value(7));
850        let first = bloq.value_inputs(id).find(|input| input.slot == 7).unwrap();
851        let footprints = guarded_footprints(
852            &bloq,
853            bloq.top(),
854            id,
855            &bloq[id],
856            &mut crate::FxMap::default(),
857        )
858        .unwrap();
859        assert_eq!(footprints.len(), 1);
860        assert_eq!(footprints[0].guard, first.value_ref());
861    }
862
863    #[test]
864    fn source_aware_qubit_layout_allows_walking_soft_corridor_overlap() {
865        let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap();
866        let mut source = BlockGraph::new();
867        source.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Walking(walking)));
868        source.add_block(Block::new(
869            ivec3(0, 1, 0),
870            BlockKind::Walking(walking.with_boundary(WalkingBoundaryKind::XZX)),
871        ));
872
873        let bloq = same_layer_overlap_bloq(ivec3(0, 0, 0), ivec3(0, 1, 0));
874
875        validate_bloq_qubit_layout_for_source(&bloq, &source)
876            .expect("parallel walking soft-corridor source overlap is valid");
877    }
878
879    #[test]
880    fn source_aware_qubit_layout_rejects_unrelated_walking_overlap() {
881        let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::X).unwrap();
882        let mut source = BlockGraph::new();
883        source.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Walking(walking)));
884        source.add_block(Block::new(ivec3(3, 0, 0), BlockKind::Walking(walking)));
885
886        let bloq = same_layer_overlap_bloq(ivec3(0, 0, 0), ivec3(3, 0, 0));
887
888        let error = validate_bloq_qubit_layout_for_source(&bloq, &source)
889            .expect_err("distant walking blocks do not justify overlapping qubits");
890
891        assert!(
892            error.to_string().contains("overlapping physical qubit"),
893            "{error}"
894        );
895    }
896
897    #[test]
898    fn qubit_layout_check_descends_into_region_bodies() {
899        // Two genuinely parallel (no connecting edge) same-layer nodes sharing a
900        // physical qubit inside a retry body must be rejected — proof the check
901        // reaches region-body nodes, which the old top-level-only scan missed.
902        let mut bloq = Bloq::new();
903        let mut body = SubGraph::new();
904        let first = single_qubit_node(
905            &mut bloq,
906            ivec3(0, 0, 0),
907            GateType::H,
908            TemplateInstanceId(0),
909        );
910        body.add_node(first);
911        let second = single_qubit_node(
912            &mut bloq,
913            ivec3(0, 0, 0),
914            GateType::X,
915            TemplateInstanceId(1),
916        );
917        body.add_node(second);
918        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
919            restart_condition: ClassicalExpr::Const(false),
920            restart_source: None,
921            body,
922        }));
923
924        // Empty source: no walking soft-corridor exemption, so the identical
925        // footprint at the same layer is a genuine overlap.
926        let source = BlockGraph::new();
927        let error = validate_bloq_qubit_layout_for_source(&bloq, &source)
928            .expect_err("parallel overlapping nodes in a region body must be rejected");
929        assert!(
930            error.to_string().contains("overlapping physical qubit"),
931            "{error}"
932        );
933    }
934
935    #[test]
936    fn qubit_layout_check_allows_sequential_region_body_overlap() {
937        // Two same-layer nodes sharing a footprint but joined by a temporal edge
938        // (the cultivation -> escape shape) are sequential, not concurrent, so
939        // the shared qubit is legitimate and must NOT be flagged.
940        let mut bloq = Bloq::new();
941        let mut body = SubGraph::new();
942        let first = single_qubit_node(
943            &mut bloq,
944            ivec3(0, 0, 0),
945            GateType::H,
946            TemplateInstanceId(0),
947        );
948        let first_id = body.add_node(first);
949        let second = single_qubit_node(
950            &mut bloq,
951            ivec3(0, 0, 0),
952            GateType::X,
953            TemplateInstanceId(1),
954        );
955        let second_id = body.add_node(second);
956        body.add_edge(first_id, second_id, BloqEdge::Order);
957        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
958            restart_condition: ClassicalExpr::Const(false),
959            restart_source: None,
960            body,
961        }));
962
963        let source = BlockGraph::new();
964        validate_bloq_qubit_layout_for_source(&bloq, &source)
965            .expect("sequential same-layer footprint sharing in a body is valid");
966    }
967
968    #[test]
969    fn region_footprint_does_not_get_vacuous_block_overlap_exemption() {
970        let mut bloq = Bloq::new();
971        let mut body = SubGraph::new();
972        let body_node = single_qubit_node(
973            &mut bloq,
974            ivec3(0, 0, 0),
975            GateType::H,
976            TemplateInstanceId(0),
977        );
978        body.add_node(body_node);
979        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
980            restart_condition: ClassicalExpr::Const(false),
981            restart_source: None,
982            body,
983        }));
984        let sibling = single_qubit_node(
985            &mut bloq,
986            ivec3(1, 0, 0),
987            GateType::X,
988            TemplateInstanceId(1),
989        );
990        bloq.add_node(sibling);
991
992        let error = validate_bloq_qubit_layout_for_source(&bloq, &BlockGraph::new())
993            .expect_err("a region and sibling quantum node cannot overlap concurrently");
994        assert!(error.to_string().contains("overlapping physical qubit"));
995    }
996}