Skip to main content

bloq_graph/
branch.rs

1//! Conditional graph regions, projections, and branch-aware edits.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4
5use bloq_utils::boolean::BooleanLimits;
6use glam::IVec3;
7
8use crate::{
9    Action, Block, BlockGraph, BlockGraphError, Expr, InvalidActionError, MeasureTarget, Pipe,
10    StabilizerGenerators,
11};
12
13/// Blocks and pipes authored as one arm of a named branch region.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct BranchArm {
16    blocks: Vec<Block>,
17    pipes: Vec<Pipe>,
18}
19
20impl BranchArm {
21    /// Creates an arm. Canonical ordering makes equality and BLOG output stable.
22    pub fn new(mut blocks: Vec<Block>, mut pipes: Vec<Pipe>) -> Self {
23        blocks.sort_by_key(|block| block.pos().to_array());
24        pipes.sort_by_key(pipe_sort_key);
25        Self { blocks, pipes }
26    }
27
28    /// Creates an arm from position and BLOG block-kind pairs.
29    /// Use [`Self::new`] when blocks need tags, heights, or other properties.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if a block kind is invalid. Geometry is checked when
34    /// the arm is installed in its parent graph.
35    pub fn try_from_blocks<P: Into<IVec3>, K: AsRef<str>>(
36        blocks: impl IntoIterator<Item = (P, K)>,
37    ) -> Result<Self, BlockGraphError> {
38        let blocks = blocks
39            .into_iter()
40            .map(|(position, kind)| {
41                Ok(Block::new(
42                    position,
43                    kind.as_ref().parse::<crate::BlockKind>()?,
44                ))
45            })
46            .collect::<Result<_, BlockGraphError>>()?;
47        Ok(Self::new(blocks, Vec::new()))
48    }
49
50    /// Adds explicitly authored pipes to this arm, retaining canonical order.
51    #[must_use]
52    pub fn with_pipes(mut self, pipes: impl IntoIterator<Item = Pipe>) -> Self {
53        self.pipes.extend(pipes);
54        self.pipes.sort_by_key(pipe_sort_key);
55        self
56    }
57
58    /// Iterates over arm blocks in canonical position order.
59    pub fn blocks(&self) -> impl Iterator<Item = &Block> {
60        self.blocks.iter()
61    }
62
63    /// Iterates over arm pipes in canonical endpoint order.
64    pub fn pipes(&self) -> impl Iterator<Item = &Pipe> {
65        self.pipes.iter()
66    }
67
68    pub(crate) fn push_pipe(&mut self, pipe: Pipe) {
69        self.pipes.push(pipe);
70        self.pipes.sort_by_key(pipe_sort_key);
71    }
72
73    /// Returns whether the arm contains no blocks.
74    pub fn is_empty(&self) -> bool {
75        self.blocks.is_empty()
76    }
77
78    /// Returns whether the arm owns a block anchored at `position`.
79    pub fn contains_block(&self, position: IVec3) -> bool {
80        self.blocks
81            .binary_search_by_key(&position.to_array(), |block| block.pos().to_array())
82            .is_ok()
83    }
84
85    fn reserved_positions(&self) -> Result<HashSet<IVec3>, BlockGraphError> {
86        self.blocks
87            .iter()
88            .try_fold(HashSet::new(), |mut occupied, block| {
89                occupied.extend(block.checked_reserved_positions()?);
90                Ok(occupied)
91            })
92    }
93
94    /// Translate an unmaterialized arm, including its external cut endpoints.
95    /// Returns an error if a block footprint or pipe endpoint overflows.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`BlockGraphError::CoordinateOverflow`] for an unrepresentable shift.
100    pub fn try_with_shift(&self, offset: IVec3) -> Result<Self, BlockGraphError> {
101        self.try_map_positions(|position| crate::checked_add_position(position, offset))
102    }
103
104    fn try_map_positions(
105        &self,
106        map: impl Fn(IVec3) -> Result<IVec3, BlockGraphError> + Copy,
107    ) -> Result<Self, BlockGraphError> {
108        Ok(Self::new(
109            self.blocks
110                .iter()
111                .map(|block| {
112                    let mut block = block.clone();
113                    block.pos = map(block.pos)?;
114                    block.checked_reserved_positions()?;
115                    Ok::<Block, BlockGraphError>(block)
116                })
117                .collect::<Result<_, _>>()?,
118            self.pipes
119                .iter()
120                .map(|pipe| {
121                    let mut pipe = pipe.clone();
122                    pipe.src = map(pipe.src)?;
123                    pipe.try_endpoints()?;
124                    Ok::<Pipe, BlockGraphError>(pipe)
125                })
126                .collect::<Result<_, _>>()?,
127        ))
128    }
129
130    /// Rotate an unmaterialized arm with the graph's block-orientation rules.
131    /// Returns an error for unsupported orientations or coordinate overflow.
132    ///
133    /// # Errors
134    ///
135    /// Returns an orientation or coordinate error if any arm element cannot rotate.
136    pub fn try_with_orientation(
137        &self,
138        orientation: crate::ModuleOrientation,
139    ) -> Result<Self, BlockGraphError> {
140        crate::graph::validate_block_orientation(self.blocks(), orientation)?;
141        Ok(Self::new(
142            self.blocks
143                .iter()
144                .map(|block| crate::graph::oriented_block(block, orientation))
145                .collect::<Result<_, _>>()?,
146            self.pipes
147                .iter()
148                .map(|pipe| crate::graph::oriented_pipe(pipe, orientation))
149                .collect::<Result<_, _>>()?,
150        ))
151    }
152
153    /// Swap X/Z bases, including Hadamards at fixed T/Y endpoints. The source
154    /// graph supplies block kinds at external cut endpoints outside this arm.
155    #[must_use]
156    pub fn flip_xz_basis(&self, source: &BlockGraph) -> Self {
157        Self::new(
158            self.blocks
159                .iter()
160                .cloned()
161                .map(|mut block| {
162                    block.kind = block.kind.flip_xz_basis();
163                    block
164                })
165                .collect(),
166            self.pipes
167                .iter()
168                .cloned()
169                .map(|mut pipe| {
170                    crate::graph::flip_pipe_xz_basis(&mut pipe, |pos| {
171                        self.is_fixed_resource_at(pos, source)
172                    });
173                    pipe
174                })
175                .collect(),
176        )
177    }
178
179    fn is_fixed_resource_at(&self, pos: IVec3, source: &BlockGraph) -> bool {
180        self.blocks
181            .binary_search_by_key(&pos.to_array(), |block| block.pos().to_array())
182            .map(|index| &self.blocks[index])
183            .ok()
184            .or_else(|| source.get_block(pos))
185            .is_some_and(|block| matches!(block.kind(), crate::BlockKind::T | crate::BlockKind::Y))
186    }
187}
188
189/// A branch ready to attach to a parent graph, including its resolve condition.
190/// Both arms use coordinates in the parent graph. External seams are supplied
191/// once to [`BlockGraph::try_add_branches`].
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Branch {
194    /// Public name of the branch.
195    pub name: String,
196    /// Corrected classical expression selecting the true arm.
197    pub condition: Expr,
198    /// Geometry selected when the condition is false.
199    pub on_false: BranchArm,
200    /// Geometry selected when the condition is true.
201    pub on_true: BranchArm,
202}
203
204impl Branch {
205    /// Creates a branch with explicit false and true arms.
206    pub fn new(
207        name: impl Into<String>,
208        condition: Expr,
209        on_false: BranchArm,
210        on_true: BranchArm,
211    ) -> Self {
212        Self {
213            name: name.into(),
214            condition,
215            on_false,
216            on_true,
217        }
218    }
219}
220
221pub(crate) fn pipe_key(pipe: &Pipe) -> (IVec3, IVec3) {
222    let (src, dst) = pipe.endpoints();
223    if src.to_array() <= dst.to_array() {
224        (src, dst)
225    } else {
226        (dst, src)
227    }
228}
229
230fn pipe_sort_key(pipe: &Pipe) -> ([i32; 3], [i32; 3], bool) {
231    let (low, high) = pipe_key(pipe);
232    (low.to_array(), high.to_array(), pipe.is_hadamard())
233}
234
235/// One temporal pipe crossing from the unconditional prefix into an arm.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct BranchCut {
238    /// Endpoint owned by the unconditional prefix.
239    pub past: IVec3,
240    /// Endpoint owned by the conditional arm.
241    pub inside: IVec3,
242    /// The source pipe exactly as authored, including orientation, Hadamard,
243    /// and tag metadata.
244    pub pipe: Pipe,
245}
246
247/// A named branch region with two explicitly authored static arms.
248///
249/// Exactly one arm is materialized in the ordinary [`BlockGraph`] at a time.
250/// `incoming` retains the canonical true-arm cut used by lowering; the editor
251/// may switch the displayed arm without changing branch semantics.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct BranchRegion {
254    /// Public branch name used by BLOG and action expressions.
255    pub name: String,
256    /// Stable internal anchor, initially the true-arm block owning the first
257    /// cut. Transforms preserve the anchor even when cut sorting changes.
258    pub target: IVec3,
259    /// Complete true-arm boundary attachments. The historical name includes
260    /// outgoing and spatial cuts as well as incoming temporal cuts.
261    pub incoming: Vec<BranchCut>,
262    on_false: BranchArm,
263    on_true: BranchArm,
264    false_incoming: Vec<BranchCut>,
265    shown_true: bool,
266}
267
268impl BranchRegion {
269    #[cfg(test)]
270    pub(crate) fn test_region(target: IVec3, blocks: Vec<IVec3>, incoming: Vec<BranchCut>) -> Self {
271        let on_true = BranchArm::new(
272            blocks
273                .into_iter()
274                .map(|position| Block::new(position, crate::BlockKind::default()))
275                .collect(),
276            Vec::new(),
277        );
278        Self {
279            name: "test_branch".into(),
280            target,
281            incoming: incoming.clone(),
282            on_false: BranchArm::default(),
283            on_true,
284            false_incoming: incoming,
285            shown_true: true,
286        }
287    }
288
289    /// Returns the arm selected when the branch condition is false.
290    pub fn on_false(&self) -> &BranchArm {
291        &self.on_false
292    }
293
294    /// Returns the arm selected when the branch condition is true.
295    pub fn on_true(&self) -> &BranchArm {
296        &self.on_true
297    }
298
299    /// Returns whether the true arm is currently materialized for display.
300    pub fn shown_true(&self) -> bool {
301        self.shown_true
302    }
303
304    /// Returns the arm currently materialized for display.
305    pub fn shown_arm(&self) -> &BranchArm {
306        if self.shown_true {
307            &self.on_true
308        } else {
309            &self.on_false
310        }
311    }
312
313    pub(crate) fn incoming_for(&self, value: bool) -> &[BranchCut] {
314        if value {
315            &self.incoming
316        } else {
317            &self.false_incoming
318        }
319    }
320
321    /// Returns the arm selected by `value`.
322    pub fn arm(&self, value: bool) -> &BranchArm {
323        if value { &self.on_true } else { &self.on_false }
324    }
325
326    /// Returns the boundary cuts for the arm selected by `value`.
327    pub fn arm_incoming(&self, value: bool) -> &[BranchCut] {
328        if value {
329            &self.incoming
330        } else {
331            &self.false_incoming
332        }
333    }
334
335    /// Whether either arm owns a block anchored at `position`.
336    pub fn contains_any_block(&self, position: IVec3) -> bool {
337        self.on_true.contains_block(position) || self.on_false.contains_block(position)
338    }
339
340    /// Whether the canonical true arm owns the block at `position`.
341    pub fn contains_block(&self, position: IVec3) -> bool {
342        self.on_true.contains_block(position)
343    }
344
345    fn contains_any_endpoint(&self, position: IVec3) -> bool {
346        self.on_false
347            .blocks()
348            .chain(self.on_true.blocks())
349            .any(|block| {
350                block
351                    .connectable_offsets()
352                    .into_iter()
353                    .any(|offset| block.pos() + offset == position)
354            })
355    }
356
357    fn try_map_positions(
358        &self,
359        map: impl Fn(IVec3) -> Result<IVec3, BlockGraphError> + Copy,
360    ) -> Result<Self, BlockGraphError> {
361        let map_cut = |cut: &BranchCut| -> Result<BranchCut, BlockGraphError> {
362            let mut pipe = cut.pipe.clone();
363            pipe.src = map(pipe.src)?;
364            Ok(BranchCut {
365                past: map(cut.past)?,
366                inside: map(cut.inside)?,
367                pipe,
368            })
369        };
370        Ok(Self {
371            name: self.name.clone(),
372            target: map(self.target)?,
373            incoming: self
374                .incoming
375                .iter()
376                .map(map_cut)
377                .collect::<Result<_, _>>()?,
378            on_false: self.on_false.try_map_positions(map)?,
379            on_true: self.on_true.try_map_positions(map)?,
380            false_incoming: self
381                .false_incoming
382                .iter()
383                .map(map_cut)
384                .collect::<Result<_, _>>()?,
385            shown_true: self.shown_true,
386        })
387    }
388
389    pub(crate) fn try_with_orientation(
390        &self,
391        orientation: crate::ModuleOrientation,
392    ) -> Result<Self, BlockGraphError> {
393        let map_cut = |cut: &BranchCut| -> Result<BranchCut, BlockGraphError> {
394            Ok(BranchCut {
395                past: orientation.try_rotate_position(cut.past)?,
396                inside: orientation.try_rotate_position(cut.inside)?,
397                pipe: crate::graph::oriented_pipe(&cut.pipe, orientation)?,
398            })
399        };
400        Ok(Self {
401            name: self.name.clone(),
402            target: orientation.try_rotate_position(self.target)?,
403            incoming: self
404                .incoming
405                .iter()
406                .map(map_cut)
407                .collect::<Result<_, _>>()?,
408            on_false: self.on_false.try_with_orientation(orientation)?,
409            on_true: self.on_true.try_with_orientation(orientation)?,
410            false_incoming: self
411                .false_incoming
412                .iter()
413                .map(map_cut)
414                .collect::<Result<_, _>>()?,
415            shown_true: self.shown_true,
416        })
417    }
418
419    pub(crate) fn flip_xz_basis(&self, source: &BlockGraph) -> Self {
420        let flip_cut = |cut: &BranchCut, arm: &BranchArm| {
421            let mut cut = cut.clone();
422            crate::graph::flip_pipe_xz_basis(&mut cut.pipe, |pos| {
423                arm.is_fixed_resource_at(pos, source)
424            });
425            cut
426        };
427        Self {
428            name: self.name.clone(),
429            target: self.target,
430            incoming: self
431                .incoming
432                .iter()
433                .map(|cut| flip_cut(cut, &self.on_true))
434                .collect(),
435            on_false: self.on_false.flip_xz_basis(source),
436            on_true: self.on_true.flip_xz_basis(source),
437            false_incoming: self
438                .false_incoming
439                .iter()
440                .map(|cut| flip_cut(cut, &self.on_false))
441                .collect(),
442            shown_true: self.shown_true,
443        }
444    }
445
446    pub(crate) fn set_cut_hadamard(&mut self, key: (IVec3, IVec3), hadamard: bool) {
447        let Some(previous) = self
448            .arm_incoming(self.shown_true)
449            .iter()
450            .find(|cut| pipe_key(&cut.pipe) == key)
451            .map(|cut| cut.pipe.is_hadamard())
452        else {
453            return;
454        };
455        let flip = previous ^ hadamard;
456        for pipe in self
457            .on_false
458            .pipes
459            .iter_mut()
460            .chain(&mut self.on_true.pipes)
461            .filter(|pipe| pipe_key(pipe) == key)
462        {
463            pipe.hadamard ^= flip;
464        }
465        for cut in self.incoming.iter_mut().chain(&mut self.false_incoming) {
466            if pipe_key(&cut.pipe) == key {
467                cut.pipe.hadamard ^= flip;
468            }
469        }
470    }
471}
472
473/// One jointly reachable structural-branch assignment and its projected
474/// ordinary static graph.
475#[derive(Debug, Clone)]
476pub struct BranchProjection {
477    assignments: Vec<(IVec3, bool)>,
478    graph: BlockGraph,
479}
480
481impl BranchProjection {
482    /// Branch values in canonical resolve-action order.
483    pub fn assignments(&self) -> &[(IVec3, bool)] {
484        &self.assignments
485    }
486
487    /// Returns the projected ordinary block graph.
488    pub fn graph(&self) -> &BlockGraph {
489        &self.graph
490    }
491
492    /// Consumes the projection and returns its ordinary block graph.
493    pub fn into_graph(self) -> BlockGraph {
494        self.graph
495    }
496
497    pub(crate) fn with_analyzed_actions(
498        mut self,
499        stabilizers: &StabilizerGenerators,
500    ) -> Result<Self, BlockGraphError> {
501        self.graph = self.graph.with_analyzed_action_graph(stabilizers)?;
502        Ok(self)
503    }
504}
505
506#[derive(Debug)]
507struct ValidatedArm {
508    incoming: Vec<BranchCut>,
509    target: IVec3,
510}
511
512fn materialize_arm(graph: &mut BlockGraph, arm: &BranchArm) -> Result<(), BlockGraphError> {
513    for block in arm.blocks() {
514        graph.try_add_block(block.clone())?;
515    }
516    for pipe in arm.pipes() {
517        graph.try_add_pipe(pipe.clone())?;
518    }
519    Ok(())
520}
521
522fn remove_arm(graph: &mut BlockGraph, arm: &BranchArm) {
523    for pipe in arm.pipes() {
524        let (src, dst) = pipe.endpoints();
525        graph.remove_pipe(src, dst);
526    }
527    for block in arm.blocks() {
528        graph.remove_block(block.pos());
529    }
530}
531
532fn branchless_projection_base(graph: &BlockGraph) -> BlockGraph {
533    let mut base = graph.copy_local_geometry();
534    base.set_actions_deferred(Vec::new())
535        .expect("an empty action list is valid");
536    for region in std::mem::take(&mut base.branches) {
537        remove_arm(&mut base, region.shown_arm());
538    }
539    base
540}
541
542#[derive(Clone, Copy)]
543enum ProjectionContext {
544    Graph,
545    Definition,
546}
547
548fn materialize_projection<'a>(
549    mut graph: BlockGraph,
550    shared_pipes: &[Pipe],
551    arms: impl IntoIterator<Item = (&'a BranchRegion, bool)>,
552    actions: &[Action],
553    defer_action_analysis: bool,
554    context: ProjectionContext,
555) -> Result<BlockGraph, BlockGraphError> {
556    let arms = arms
557        .into_iter()
558        .map(|(region, value)| region.arm(value))
559        .collect::<Vec<_>>();
560    for arm in &arms {
561        for block in arm.blocks() {
562            graph.try_add_block(block.clone())?;
563        }
564    }
565    for arm in arms {
566        for pipe in arm.pipes() {
567            graph.try_add_pipe(pipe.clone())?;
568        }
569    }
570    for pipe in shared_pipes {
571        if graph.get_pipe(pipe.src(), pipe.dst()).is_none() {
572            graph.try_add_pipe(pipe.clone())?;
573        }
574    }
575    graph = graph.fix_shadowed_faces();
576    if matches!(context, ProjectionContext::Graph) {
577        graph.validate_structure()?;
578    }
579    if defer_action_analysis {
580        graph.set_actions_deferred(actions.to_vec())?;
581    } else {
582        graph.set_actions(actions.to_vec())?;
583    }
584    Ok(graph)
585}
586
587fn invalid_interface(name: &str, reason: impl Into<String>) -> BlockGraphError {
588    InvalidActionError::InvalidBranchInterface {
589        name: name.to_owned(),
590        reason: reason.into(),
591    }
592    .into()
593}
594
595fn validate_arm(
596    candidate: &mut BlockGraph,
597    name: &str,
598    arm: &BranchArm,
599) -> Result<ValidatedArm, BlockGraphError> {
600    let members = arm.blocks().map(Block::pos).collect::<HashSet<_>>();
601    for block in arm.blocks() {
602        if block.kind().is_port() {
603            return Err(invalid_interface(name, "arms cannot contain Port blocks"));
604        }
605        if block.kind().is_dynamic() {
606            return Err(invalid_interface(
607                name,
608                format!("arms cannot contain dynamic {} blocks", block.kind()),
609            ));
610        }
611    }
612
613    materialize_arm(candidate, arm)?;
614
615    let mut incoming = Vec::new();
616    let mut forward = HashMap::<IVec3, Vec<IVec3>>::new();
617    for pipe in arm.pipes() {
618        let (src, dst) = pipe.try_endpoints()?;
619        let src_owner = candidate
620            .get_endpoint_block(src)
621            .ok_or(BlockGraphError::BlockNotFound(src))?
622            .pos();
623        let dst_owner = candidate
624            .get_endpoint_block(dst)
625            .ok_or(BlockGraphError::BlockNotFound(dst))?
626            .pos();
627        let src_inside = members.contains(&src_owner);
628        let dst_inside = members.contains(&dst_owner);
629        match (src_inside, dst_inside) {
630            (false, false) => {
631                return Err(invalid_interface(
632                    name,
633                    "an arm pipe must touch at least one arm block",
634                ));
635            }
636            (true, true) => {
637                if pipe.dir().is_spatial() || src.z == dst.z {
638                    forward.entry(src_owner).or_default().push(dst_owner);
639                    forward.entry(dst_owner).or_default().push(src_owner);
640                } else {
641                    let (past, future) = if src.z < dst.z {
642                        (src_owner, dst_owner)
643                    } else {
644                        (dst_owner, src_owner)
645                    };
646                    forward.entry(past).or_default().push(future);
647                }
648            }
649            _ => {
650                let (past, inside) = if src_inside { (dst, src) } else { (src, dst) };
651                incoming.push(BranchCut {
652                    past,
653                    inside,
654                    pipe: pipe.clone(),
655                });
656            }
657        }
658    }
659    incoming.sort_by_key(|cut| {
660        (
661            cut.past.to_array(),
662            cut.inside.to_array(),
663            pipe_sort_key(&cut.pipe),
664        )
665    });
666    if incoming.is_empty() {
667        return Err(invalid_interface(name, "an arm has no external interface"));
668    }
669
670    let mut reached = HashSet::new();
671    let mut queue = VecDeque::new();
672    for cut in &incoming {
673        let owner = candidate
674            .get_endpoint_block(cut.inside)
675            .expect("validated cut endpoint has an arm owner")
676            .pos();
677        if reached.insert(owner) {
678            queue.push_back(owner);
679        }
680    }
681    while let Some(block) = queue.pop_front() {
682        for &next in forward.get(&block).into_iter().flatten() {
683            if reached.insert(next) {
684                queue.push_back(next);
685            }
686        }
687    }
688    if let Some(unreachable) = members.iter().find(|position| !reached.contains(position)) {
689        return Err(invalid_interface(
690            name,
691            format!("block {unreachable} is not reachable from an incoming cut"),
692        ));
693    }
694
695    let target = candidate
696        .get_endpoint_block(incoming[0].inside)
697        .expect("validated cut endpoint has an arm owner")
698        .pos();
699    remove_arm(candidate, arm);
700    Ok(ValidatedArm { incoming, target })
701}
702
703// Keep all source geometry in one scratch graph. Each successful arm check
704// removes only what it added, and each region restores the displayed geometry.
705struct BranchArmValidation {
706    candidate: BlockGraph,
707    shared: Vec<Pipe>,
708    shared_at: HashMap<IVec3, Vec<usize>>,
709}
710
711impl BranchArmValidation {
712    fn new(source: &BlockGraph) -> Self {
713        let shared = source.branch_shared_pipes();
714        let mut shared_at = HashMap::<_, Vec<_>>::new();
715        for (index, pipe) in shared.iter().enumerate() {
716            for endpoint in [pipe.src(), pipe.dst()] {
717                shared_at.entry(endpoint).or_default().push(index);
718            }
719        }
720        let mut candidate = source.clone();
721        candidate.branches.clear();
722        candidate.clear_actions();
723        Self {
724            candidate,
725            shared,
726            shared_at,
727        }
728    }
729
730    fn with_seams(&self, arm: &BranchArm) -> Result<BranchArm, BlockGraphError> {
731        let positions = arm.reserved_positions()?;
732        let mut seams = positions
733            .iter()
734            .flat_map(|position| self.shared_at.get(position).into_iter().flatten().copied())
735            .collect::<Vec<_>>();
736        seams.sort_unstable();
737        seams.dedup();
738        let mut arm = arm.clone();
739        if !seams.is_empty() {
740            arm.pipes
741                .extend(seams.into_iter().map(|index| self.shared[index].clone()));
742            arm.pipes.sort_by_key(pipe_sort_key);
743        }
744        Ok(arm)
745    }
746
747    fn validate(
748        &mut self,
749        region: &BranchRegion,
750    ) -> Result<(ValidatedArm, ValidatedArm), BlockGraphError> {
751        // Restore the actual displayed graph, including common seams. Reading
752        // adjacency keeps this proportional to the arm, not the complete source.
753        let mut restore = BranchArm::default();
754        let mut pipes = HashSet::new();
755        for block in region.shown_arm().blocks() {
756            if let Some(block) = self.candidate.get_block(block.pos()) {
757                restore.blocks.push(block.clone());
758                for pipe in self.candidate.pipes_at(block.pos()) {
759                    if pipes.insert(pipe_key(pipe)) {
760                        restore.pipes.push(pipe.clone());
761                    }
762                }
763            }
764        }
765        remove_arm(&mut self.candidate, region.shown_arm());
766        let on_false = self.with_seams(region.on_false())?;
767        let on_false = validate_arm(&mut self.candidate, &region.name, &on_false)?;
768        let on_true = self.with_seams(region.on_true())?;
769        let on_true = validate_arm(&mut self.candidate, &region.name, &on_true)?;
770        if !on_false
771            .incoming
772            .iter()
773            .map(cut_interface)
774            .eq(on_true.incoming.iter().map(cut_interface))
775        {
776            return Err(invalid_interface(
777                &region.name,
778                "false and true arms have different external interfaces",
779            ));
780        }
781        materialize_arm(&mut self.candidate, &restore)?;
782        Ok((on_false, on_true))
783    }
784}
785
786fn cut_interface(cut: &BranchCut) -> ([i32; 3], [i32; 3]) {
787    // SEM-TBRANCH: the shared cut precedes any arm-owned realignment.
788    // Its Hadamard is part of the chosen arm, not the common interface.
789    (cut.past.to_array(), cut.inside.to_array())
790}
791
792fn validate_region_action(
793    graph: &BlockGraph,
794    actions: &[Action],
795    condition: &Expr,
796    region: &BranchRegion,
797    forbidden: Option<usize>,
798) -> Result<(), BlockGraphError> {
799    let Some(ordinal) = forbidden else {
800        // Every controller is a Measure action in this same list. If no
801        // action target touches the region, no controller can touch it.
802        return Ok(());
803    };
804    for controller in controlling_measurements(condition, actions) {
805        if measurement_touches_region(graph, controller, region) {
806            return Err(InvalidActionError::BranchControllerInside {
807                target: region.target,
808                controller,
809            }
810            .into());
811        }
812    }
813    Err(InvalidActionError::BranchContainsActionTarget {
814        target: region.target,
815        ordinal,
816    }
817    .into())
818}
819
820struct RegionTargetIndex {
821    blocks: crate::FxHashMap<IVec3, Vec<usize>>,
822    endpoints: crate::FxHashMap<IVec3, Vec<usize>>,
823}
824
825impl RegionTargetIndex {
826    fn new(regions: &[BranchRegion]) -> Self {
827        let mut blocks = crate::FxHashMap::<_, Vec<_>>::default();
828        let mut endpoints = crate::FxHashMap::<_, Vec<_>>::default();
829        for (region, definition) in regions.iter().enumerate() {
830            for block in definition
831                .on_false
832                .blocks()
833                .chain(definition.on_true.blocks())
834            {
835                blocks.entry(block.pos()).or_default().push(region);
836                for offset in block.connectable_offsets() {
837                    // Arm validation checked the footprint first; keep the
838                    // index total even for a directly queried malformed arm.
839                    if let Some(endpoint) = block.pos().checked_add(offset) {
840                        endpoints.entry(endpoint).or_default().push(region);
841                    }
842                }
843            }
844        }
845        Self { blocks, endpoints }
846    }
847
848    fn mark(
849        postings: &crate::FxHashMap<IVec3, Vec<usize>>,
850        position: IVec3,
851        ordinal: usize,
852        earliest: &mut [Option<usize>],
853    ) {
854        for &region in postings.get(&position).into_iter().flatten() {
855            earliest[region].get_or_insert(ordinal);
856        }
857    }
858
859    fn mark_measurement(
860        &self,
861        graph: &BlockGraph,
862        target: MeasureTarget,
863        ordinal: usize,
864        earliest: &mut [Option<usize>],
865    ) {
866        match target {
867            MeasureTarget::Node(position) => {
868                if let Some(owner) = graph.get_endpoint_block(position) {
869                    Self::mark(&self.blocks, owner.pos(), ordinal, earliest);
870                }
871                Self::mark(&self.blocks, position, ordinal, earliest);
872            }
873            MeasureTarget::Edge { src, dir } => {
874                let Some(dst) = src.checked_add(dir.to_ivec3()) else {
875                    return;
876                };
877                for endpoint in [src, dst] {
878                    if let Some(owner) = graph.get_endpoint_block(endpoint) {
879                        Self::mark(&self.blocks, owner.pos(), ordinal, earliest);
880                    }
881                    Self::mark(&self.endpoints, endpoint, ordinal, earliest);
882                }
883            }
884        }
885    }
886}
887
888fn earliest_region_action_targets(graph: &BlockGraph, actions: &[Action]) -> Vec<Option<usize>> {
889    let index = RegionTargetIndex::new(&graph.branches);
890    let mut earliest = vec![None; graph.branches.len()];
891    for (ordinal, action) in actions.iter().enumerate() {
892        match action {
893            Action::Measure { target, .. } => {
894                index.mark_measurement(graph, *target, ordinal, &mut earliest);
895            }
896            Action::Resolve { target, .. } => {
897                RegionTargetIndex::mark(&index.blocks, *target, ordinal, &mut earliest);
898            }
899            Action::Feedback { targets, .. } => {
900                for target in targets {
901                    let measurement =
902                        target
903                            .direction
904                            .map_or(MeasureTarget::Node(target.target), |dir| {
905                                MeasureTarget::Edge {
906                                    src: target.target,
907                                    dir,
908                                }
909                            });
910                    index.mark_measurement(graph, measurement, ordinal, &mut earliest);
911                }
912            }
913            Action::Let { .. } | Action::DiscardIf(_) | Action::Branch { .. } => {}
914        }
915    }
916    earliest
917}
918
919fn controlling_measurements<'a>(condition: &'a Expr, actions: &'a [Action]) -> Vec<MeasureTarget> {
920    let definitions = actions
921        .iter()
922        .filter_map(|action| match action {
923            Action::Measure { name, .. } | Action::Let { name, .. } => {
924                Some((name.as_str(), action))
925            }
926            _ => None,
927        })
928        .collect::<HashMap<_, _>>();
929    let mut names = Vec::new();
930    collect_expr_names(condition, &mut names);
931    let mut seen = HashSet::new();
932    let mut measurements = Vec::new();
933    while let Some(name) = names.pop() {
934        if !seen.insert(name) {
935            continue;
936        }
937        match definitions.get(name).copied() {
938            Some(Action::Measure { target, .. }) => measurements.push(*target),
939            Some(Action::Let { expr, .. }) => collect_expr_names(expr, &mut names),
940            _ => {}
941        }
942    }
943    measurements
944}
945
946fn collect_expr_names<'a>(expr: &'a Expr, names: &mut Vec<&'a str>) {
947    match expr {
948        Expr::Var(name) => names.push(name),
949        Expr::Not(inner) => collect_expr_names(inner, names),
950        Expr::Binary(_, lhs, rhs) => {
951            collect_expr_names(lhs, names);
952            collect_expr_names(rhs, names);
953        }
954    }
955}
956
957fn measurement_touches_region(
958    graph: &BlockGraph,
959    target: MeasureTarget,
960    region: &BranchRegion,
961) -> bool {
962    match target {
963        MeasureTarget::Node(position) => {
964            graph
965                .get_endpoint_block(position)
966                .is_some_and(|block| region.contains_any_block(block.pos()))
967                || region.contains_any_block(position)
968        }
969        MeasureTarget::Edge { src, dir } => {
970            let Some(dst) = src.checked_add(dir.to_ivec3()) else {
971                return false;
972            };
973            [src, dst].into_iter().any(|endpoint| {
974                graph
975                    .get_endpoint_block(endpoint)
976                    .is_some_and(|block| region.contains_any_block(block.pos()))
977                    || region.contains_any_endpoint(endpoint)
978            })
979        }
980    }
981}
982
983/// Validates explicit branch definitions against a candidate action list and
984/// returns them in resolve-action order.
985pub(crate) fn validate_regions_for_actions(
986    graph: &BlockGraph,
987    actions: &[Action],
988) -> Result<Vec<BranchRegion>, BlockGraphError> {
989    if !graph.branches.is_empty() {
990        let mut validation = BranchArmValidation::new(graph);
991        for region in &graph.branches {
992            validation.validate(region)?;
993            if !region.on_true.contains_block(region.target) {
994                return Err(invalid_interface(
995                    &region.name,
996                    "the true-arm anchor block was removed",
997                ));
998            }
999        }
1000    }
1001    let by_target = graph
1002        .branches
1003        .iter()
1004        .enumerate()
1005        .map(|(index, region)| (region.target, index))
1006        .collect::<HashMap<_, _>>();
1007    let mut regions = Vec::new();
1008    let mut seen = HashSet::new();
1009    let mut earliest = None;
1010    for action in actions {
1011        let Action::Branch { target, condition } = action else {
1012            continue;
1013        };
1014        let &index = by_target
1015            .get(target)
1016            .ok_or(InvalidActionError::InvalidBranchTarget(*target))?;
1017        if !seen.insert(*target) {
1018            return Err(InvalidActionError::DuplicateBranchTarget(*target).into());
1019        }
1020        let region = &graph.branches[index];
1021        let earliest =
1022            earliest.get_or_insert_with(|| earliest_region_action_targets(graph, actions));
1023        validate_region_action(graph, actions, condition, region, earliest[index])?;
1024        regions.push(region.clone());
1025    }
1026    if let Some(region) = graph
1027        .branches
1028        .iter()
1029        .find(|region| !seen.contains(&region.target))
1030    {
1031        return Err(InvalidActionError::MissingResolveForBranch(region.name.clone()).into());
1032    }
1033    Ok(regions)
1034}
1035
1036impl BlockGraph {
1037    /// Attaches branches, shared seams, and additional actions in one edit.
1038    /// Resolve actions are created from each branch's condition. Existing
1039    /// actions are retained. All geometry, names, interfaces, and action
1040    /// dependencies are checked before changing the graph.
1041    ///
1042    /// # Errors
1043    ///
1044    /// Returns an error for invalid geometry or actions, leaving `self` unchanged.
1045    ///
1046    /// # Examples
1047    ///
1048    /// ```
1049    /// use bloq_graph::{BlockGraph, Branch, BranchArm, Expr, Pipe, Direction};
1050    /// let mut graph = BlockGraph::from_text("BLOG 1.0\nmodule main {\n0: ZXZ [0,0,0]\n1: ZXZ [2,0,0]\nm = measure 1\n}")?;
1051    /// let arm = BranchArm::try_from_blocks([([0, 0, 1], "Z")])?;
1052    /// graph.try_add_branches(
1053    ///     [Branch::new("cap", Expr::Var("m".into()), arm.clone(), arm)],
1054    ///     [Pipe::new([0, 0, 0], Direction::ZPLUS)],
1055    ///     [],
1056    /// )?;
1057    /// assert_eq!(graph.branch_regions()?.len(), 1);
1058    /// # Ok::<(), bloq_graph::BlockGraphError>(())
1059    /// ```
1060    pub fn try_add_branches(
1061        &mut self,
1062        branches: impl IntoIterator<Item = Branch>,
1063        shared_pipes: impl IntoIterator<Item = Pipe>,
1064        actions: impl IntoIterator<Item = Action>,
1065    ) -> Result<(), BlockGraphError> {
1066        let mut candidate = self.clone();
1067        let mut combined = candidate.actions();
1068        combined.extend(actions);
1069        let (regions, conditions): (Vec<_>, Vec<_>) = branches
1070            .into_iter()
1071            .map(|branch| {
1072                (
1073                    (branch.name, branch.on_false, branch.on_true),
1074                    branch.condition,
1075                )
1076            })
1077            .unzip();
1078        let targets =
1079            candidate.try_add_branch_regions(regions, shared_pipes.into_iter().collect())?;
1080        combined.extend(
1081            targets
1082                .into_iter()
1083                .zip(conditions)
1084                .map(|(target, condition)| Action::Branch { target, condition }),
1085        );
1086        candidate.set_actions(combined)?;
1087        *self = candidate;
1088        Ok(())
1089    }
1090
1091    /// Adds a named explicit terminal region and shows its true arm.
1092    ///
1093    /// # Errors
1094    ///
1095    /// Returns an error if the region name, geometry, seams, or actions are invalid.
1096    pub fn try_add_branch_region(
1097        &mut self,
1098        name: impl Into<String>,
1099        on_false: BranchArm,
1100        on_true: BranchArm,
1101    ) -> Result<IVec3, BlockGraphError> {
1102        let targets =
1103            self.try_add_branch_regions(vec![(name.into(), on_false, on_true)], Vec::new())?;
1104        Ok(targets[0])
1105    }
1106
1107    /// Install disjoint region owners together, then resolve their common seams.
1108    /// Shared pipes remain in the common graph and are never copied into arms.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns an error if any region, shared pipe, or resulting action graph is invalid.
1113    pub fn try_add_branch_regions(
1114        &mut self,
1115        regions: Vec<(String, BranchArm, BranchArm)>,
1116        shared_pipes: Vec<Pipe>,
1117    ) -> Result<Vec<IVec3>, BlockGraphError> {
1118        let mut candidate = self.clone();
1119        let actions = candidate.actions();
1120        candidate.rebuild_action_graph_lenient(Vec::new());
1121        let mut occupied = candidate
1122            .branches
1123            .iter()
1124            .map(|region| {
1125                let mut positions = region.on_false.reserved_positions()?;
1126                positions.extend(region.on_true.reserved_positions()?);
1127                Ok((region.target, positions))
1128            })
1129            .collect::<Result<Vec<_>, BlockGraphError>>()?;
1130        let start = candidate.branches.len();
1131        for (name, on_false, on_true) in regions {
1132            if !crate::parser::is_valid_identifier(&name) {
1133                return Err(InvalidActionError::InvalidActionName(name).into());
1134            }
1135            if candidate.branches.iter().any(|region| region.name == name) {
1136                return Err(InvalidActionError::DuplicateBranchName(name).into());
1137            }
1138            let Some(first) = on_true.blocks().next() else {
1139                return Err(invalid_interface(&name, "an arm has no blocks"));
1140            };
1141            if on_false.is_empty() {
1142                return Err(invalid_interface(&name, "an arm has no blocks"));
1143            }
1144            let target = first.pos();
1145            let mut positions = on_false.reserved_positions()?;
1146            positions.extend(on_true.reserved_positions()?);
1147            for (other, other_positions) in &occupied {
1148                if let Some(&block) = positions.intersection(other_positions).next() {
1149                    return Err(InvalidActionError::OverlappingBranchRegions {
1150                        first: *other,
1151                        second: target,
1152                        block,
1153                    }
1154                    .into());
1155                }
1156            }
1157            occupied.push((target, positions));
1158            for block in on_true.blocks() {
1159                candidate.try_add_block(block.clone())?;
1160            }
1161            candidate.branches.push(BranchRegion {
1162                name,
1163                target,
1164                incoming: Vec::new(),
1165                false_incoming: Vec::new(),
1166                on_false,
1167                on_true,
1168                shown_true: true,
1169            });
1170        }
1171        // Keep edit hooks out of installation: each pipe's owner is already known.
1172        let branches = std::mem::take(&mut candidate.branches);
1173        for region in &branches[start..] {
1174            for pipe in region.on_true.pipes() {
1175                candidate.try_add_pipe(pipe.clone())?;
1176            }
1177        }
1178        for pipe in shared_pipes {
1179            candidate.try_add_pipe(pipe)?;
1180        }
1181        candidate.branches = branches;
1182        if start < candidate.branches.len() {
1183            let mut validation = BranchArmValidation::new(&candidate);
1184            for index in start..candidate.branches.len() {
1185                let (on_false, on_true) = validation.validate(&candidate.branches[index])?;
1186                let region = &mut candidate.branches[index];
1187                region.target = on_true.target;
1188                region.incoming = on_true.incoming;
1189                region.false_incoming = on_false.incoming;
1190            }
1191        }
1192        let targets = candidate.branches[start..]
1193            .iter()
1194            .map(|region| region.target)
1195            .collect();
1196        candidate.rebuild_action_graph_lenient(actions);
1197        *self = candidate;
1198        Ok(targets)
1199    }
1200
1201    pub(crate) fn branch_shared_pipes(&self) -> Vec<Pipe> {
1202        let owned = self
1203            .branches
1204            .iter()
1205            .flat_map(|region| region.shown_arm().pipes().map(pipe_key))
1206            .collect::<HashSet<_>>();
1207        self.pipes()
1208            .filter(|pipe| !owned.contains(&pipe_key(pipe)))
1209            .cloned()
1210            .collect()
1211    }
1212
1213    /// Whether any region reconnects to spatial neighbors or later common code.
1214    pub fn has_continuing_branches(&self) -> bool {
1215        self.branches.iter().any(|region| {
1216            region
1217                .incoming
1218                .iter()
1219                .any(|cut| cut.pipe.dir().is_spatial() || cut.past.z >= cut.inside.z)
1220        })
1221    }
1222
1223    /// Captures selected blocks and their explicitly selected incident pipes,
1224    /// removing them from the currently visible graph.
1225    ///
1226    /// # Errors
1227    ///
1228    /// Returns an error if a selected element is missing or the selection is not a closed arm.
1229    pub fn take_branch_arm(
1230        &mut self,
1231        positions: impl IntoIterator<Item = IVec3>,
1232        pipe_endpoints: impl IntoIterator<Item = (IVec3, IVec3)>,
1233    ) -> Result<BranchArm, BlockGraphError> {
1234        let positions = positions.into_iter().collect::<HashSet<_>>();
1235        let selected_pipes = pipe_endpoints
1236            .into_iter()
1237            .map(|(src, dst)| {
1238                self.get_pipe(src, dst)
1239                    .cloned()
1240                    .ok_or(BlockGraphError::PipeNotFound(src, dst))
1241            })
1242            .collect::<Result<Vec<_>, _>>()?;
1243        if positions.is_empty() {
1244            return if selected_pipes.is_empty() {
1245                Ok(BranchArm::default())
1246            } else {
1247                Err(invalid_interface(
1248                    "new branch",
1249                    "selected pipes require a selected block",
1250                ))
1251            };
1252        }
1253        if let Some((region, position)) = self.branches.iter().find_map(|region| {
1254            positions
1255                .iter()
1256                .find(|position| region.shown_arm().contains_block(**position))
1257                .map(|position| (region, *position))
1258        }) {
1259            return Err(invalid_interface(
1260                &region.name,
1261                format!("block {position} already belongs to this branch"),
1262            ));
1263        }
1264        let blocks = positions
1265            .iter()
1266            .map(|position| {
1267                self.get_block(*position)
1268                    .cloned()
1269                    .ok_or(BlockGraphError::BlockNotFound(*position))
1270            })
1271            .collect::<Result<Vec<_>, _>>()?;
1272        let incident_pipes = self
1273            .pipes()
1274            .filter(|pipe| {
1275                [pipe.src(), pipe.dst()].into_iter().any(|endpoint| {
1276                    self.get_endpoint_block(endpoint)
1277                        .is_some_and(|block| positions.contains(&block.pos()))
1278                })
1279            })
1280            .cloned()
1281            .collect::<Vec<_>>();
1282        if incident_pipes.iter().map(pipe_key).collect::<HashSet<_>>()
1283            != selected_pipes.iter().map(pipe_key).collect()
1284        {
1285            return Err(invalid_interface(
1286                "new branch",
1287                "select every pipe incident to the arm blocks",
1288            ));
1289        }
1290        let arm = BranchArm::new(blocks, selected_pipes);
1291        let actions = self.actions();
1292        self.rebuild_action_graph_lenient(Vec::new());
1293        remove_arm(self, &arm);
1294        self.rebuild_action_graph_lenient(actions);
1295        Ok(arm)
1296    }
1297
1298    /// Restores a previously captured loose arm to the visible graph.
1299    ///
1300    /// # Errors
1301    ///
1302    /// Returns an error if the arm conflicts with existing geometry or actions.
1303    pub fn restore_branch_arm(&mut self, arm: &BranchArm) -> Result<(), BlockGraphError> {
1304        let actions = self.actions();
1305        let mut candidate = self.clone();
1306        candidate.rebuild_action_graph_lenient(Vec::new());
1307        materialize_arm(&mut candidate, arm)?;
1308        candidate.rebuild_action_graph_lenient(actions);
1309        *self = candidate;
1310        Ok(())
1311    }
1312
1313    /// Returns explicit branch definitions in declaration order.
1314    pub fn branch_definitions(&self) -> &[BranchRegion] {
1315        &self.branches
1316    }
1317
1318    /// Finds an explicit branch definition by public name.
1319    pub fn branch_by_name(&self, name: &str) -> Option<&BranchRegion> {
1320        self.branches.iter().find(|region| region.name == name)
1321    }
1322
1323    /// Finds an explicit branch definition by stable internal anchor.
1324    pub fn branch_by_target(&self, target: IVec3) -> Option<&BranchRegion> {
1325        self.branches.iter().find(|region| region.target == target)
1326    }
1327
1328    /// Returns the displayed region owning `position`, for scene picking.
1329    pub fn shown_branch_at(&self, position: IVec3) -> Option<&BranchRegion> {
1330        self.branches
1331            .iter()
1332            .find(|region| region.shown_arm().contains_block(position))
1333    }
1334
1335    /// Switches which arm is materialized for display.
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns an error if the branch is unknown or the selected arm cannot be materialized.
1340    pub fn set_shown_branch_arm(
1341        &mut self,
1342        name: &str,
1343        show_true: bool,
1344    ) -> Result<(), BlockGraphError> {
1345        let Some(index) = self.branches.iter().position(|region| region.name == name) else {
1346            return Err(InvalidActionError::UnknownBranchName(name.to_owned()).into());
1347        };
1348        if self.branches[index].shown_true == show_true {
1349            return Ok(());
1350        }
1351        let actions = self.actions();
1352        let mut candidate = self.clone();
1353        candidate.rebuild_action_graph_lenient(Vec::new());
1354        // Detach the region while changing its display materialization: these
1355        // removals must not delete the authored arm.
1356        let shared = candidate.branch_shared_pipes();
1357        let mut branches = std::mem::take(&mut candidate.branches);
1358        let region = &mut branches[index];
1359        remove_arm(&mut candidate, region.shown_arm());
1360        materialize_arm(&mut candidate, region.arm(show_true))?;
1361        for pipe in shared {
1362            if candidate.get_pipe(pipe.src(), pipe.dst()).is_none() {
1363                candidate.try_add_pipe(pipe)?;
1364            }
1365        }
1366        region.shown_true = show_true;
1367        candidate.branches = branches;
1368        candidate.rebuild_action_graph_lenient(actions);
1369        *self = candidate;
1370        Ok(())
1371    }
1372
1373    #[doc(hidden)]
1374    pub fn canonical_true_branch_view(&self) -> Result<Self, BlockGraphError> {
1375        let mut graph = self.clone();
1376        let hidden_true = graph
1377            .branches
1378            .iter()
1379            .filter(|region| !region.shown_true)
1380            .map(|region| region.name.clone())
1381            .collect::<Vec<_>>();
1382        for name in hidden_true {
1383            graph.set_shown_branch_arm(&name, true)?;
1384        }
1385        Ok(graph)
1386    }
1387
1388    /// Returns explicit regions in resolve-action order.
1389    ///
1390    /// # Errors
1391    ///
1392    /// Returns an error if branch actions and stored regions disagree.
1393    pub fn branch_regions(&self) -> Result<Vec<BranchRegion>, BlockGraphError> {
1394        validate_regions_for_actions(self, &self.actions())
1395    }
1396
1397    /// Projects every reachable explicit branch assignment.
1398    ///
1399    /// # Errors
1400    ///
1401    /// Returns an error if branch validation, enumeration, or materialization fails.
1402    pub fn branch_projections(&self) -> Result<Vec<BranchProjection>, BlockGraphError> {
1403        self.require_flat_source("branch projection")?;
1404        self.branch_projections_up_to(usize::MAX)
1405    }
1406
1407    /// Enumerate only the reachable selector tuples, without retaining projected graphs.
1408    #[doc(hidden)]
1409    pub fn branch_assignments_up_to(
1410        &self,
1411        limit: usize,
1412    ) -> Result<Vec<Vec<(IVec3, bool)>>, BlockGraphError> {
1413        let regions = self.branch_regions()?;
1414        let actions = self.actions();
1415        let dag = self.build_action_graph(&actions)?;
1416        let targets = regions
1417            .iter()
1418            .map(|region| region.target)
1419            .collect::<Vec<_>>();
1420        Ok(dag
1421            .branch_values_up_to(&targets, limit)?
1422            .into_iter()
1423            .map(|values| targets.iter().copied().zip(values).collect())
1424            .collect())
1425    }
1426
1427    pub(crate) fn branch_projections_up_to(
1428        &self,
1429        limit: usize,
1430    ) -> Result<Vec<BranchProjection>, BlockGraphError> {
1431        self.branch_projections_impl(limit, false, BooleanLimits::DEFAULT)
1432    }
1433
1434    /// Returns reachable static projections without deriving stabilizers.
1435    #[cfg(test)]
1436    pub(crate) fn branch_projections_for_analysis(
1437        &self,
1438    ) -> Result<Vec<BranchProjection>, BlockGraphError> {
1439        self.branch_projections_for_analysis_up_to_with_limits(usize::MAX, BooleanLimits::DEFAULT)
1440    }
1441
1442    pub(crate) fn branch_projections_for_analysis_up_to_with_limits(
1443        &self,
1444        limit: usize,
1445        boolean_limits: BooleanLimits,
1446    ) -> Result<Vec<BranchProjection>, BlockGraphError> {
1447        self.branch_projections_impl(limit, true, boolean_limits)
1448    }
1449
1450    fn branch_projections_impl(
1451        &self,
1452        limit: usize,
1453        defer_action_analysis: bool,
1454        boolean_limits: BooleanLimits,
1455    ) -> Result<Vec<BranchProjection>, BlockGraphError> {
1456        if limit == 0 {
1457            return Ok(Vec::new());
1458        }
1459        let regions = self.branch_regions()?;
1460        if regions.is_empty() {
1461            return Ok(vec![BranchProjection {
1462                assignments: Vec::new(),
1463                graph: self.clone(),
1464            }]);
1465        }
1466        let actions = self.actions();
1467        let dag = self.build_action_graph(&actions)?;
1468        let remaining_actions = actions
1469            .iter()
1470            .filter(|action| !matches!(action, Action::Branch { .. }))
1471            .cloned()
1472            .collect::<Vec<_>>();
1473        let base = branchless_projection_base(self);
1474        let targets = regions
1475            .iter()
1476            .map(|region| region.target)
1477            .collect::<Vec<_>>();
1478        let shared_pipes = self.branch_shared_pipes();
1479        dag.branch_values_up_to_with_limits(&targets, limit, boolean_limits)?
1480            .iter()
1481            .map(|values| {
1482                let assignments = targets
1483                    .iter()
1484                    .copied()
1485                    .zip(values.iter().copied())
1486                    .collect::<Vec<_>>();
1487                let graph = materialize_projection(
1488                    base.clone(),
1489                    &shared_pipes,
1490                    regions
1491                        .iter()
1492                        .zip(values)
1493                        .map(|(region, &value)| (region, value)),
1494                    &remaining_actions,
1495                    defer_action_analysis,
1496                    ProjectionContext::Graph,
1497                )?;
1498                Ok(BranchProjection { assignments, graph })
1499            })
1500            .collect()
1501    }
1502
1503    /// Projects every explicit branch to an ordinary static graph.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns an error for missing assignments or an invalid projected graph.
1508    pub fn project_branches(
1509        &self,
1510        assignments: impl IntoIterator<Item = (IVec3, bool)>,
1511    ) -> Result<Self, BlockGraphError> {
1512        self.require_flat_source("branch projection")?;
1513        self.project_branches_impl(assignments, false, ProjectionContext::Graph)
1514    }
1515
1516    #[doc(hidden)]
1517    pub fn project_branches_deferred(
1518        &self,
1519        assignments: impl IntoIterator<Item = (IVec3, bool)>,
1520    ) -> Result<Self, BlockGraphError> {
1521        self.require_flat_source("branch projection")?;
1522        self.project_branches_impl(assignments, true, ProjectionContext::Graph)
1523    }
1524
1525    /// Project a definition's owned graph before module bindings install its
1526    /// external pipes. The complete linked projection still needs validation.
1527    #[doc(hidden)]
1528    pub fn project_branches_in_definition(
1529        &self,
1530        assignments: impl IntoIterator<Item = (IVec3, bool)>,
1531    ) -> Result<Self, BlockGraphError> {
1532        self.project_branches_impl(assignments, true, ProjectionContext::Definition)
1533    }
1534
1535    fn project_branches_impl(
1536        &self,
1537        assignments: impl IntoIterator<Item = (IVec3, bool)>,
1538        defer_action_analysis: bool,
1539        context: ProjectionContext,
1540    ) -> Result<Self, BlockGraphError> {
1541        let regions = self.branch_regions()?;
1542        let known_targets = regions
1543            .iter()
1544            .map(|region| region.target)
1545            .collect::<HashSet<_>>();
1546        let mut values = HashMap::new();
1547        for (target, value) in assignments {
1548            if !known_targets.contains(&target) {
1549                return Err(InvalidActionError::UnknownBranchValue(target).into());
1550            }
1551            if values.insert(target, value).is_some() {
1552                return Err(InvalidActionError::DuplicateBranchValue(target).into());
1553            }
1554        }
1555        for region in &regions {
1556            if !values.contains_key(&region.target) {
1557                return Err(InvalidActionError::MissingBranchValue(region.target).into());
1558            }
1559        }
1560
1561        let remaining_actions = self
1562            .actions()
1563            .into_iter()
1564            .filter(|action| !matches!(action, Action::Branch { .. }))
1565            .collect::<Vec<_>>();
1566        materialize_projection(
1567            branchless_projection_base(self),
1568            &self.branch_shared_pipes(),
1569            regions
1570                .iter()
1571                .map(|region| (region, values[&region.target])),
1572            &remaining_actions,
1573            defer_action_analysis,
1574            context,
1575        )
1576    }
1577
1578    /// Materialize an already validated branch description. The caller owns the
1579    /// region order and supplies one value per region; public projection APIs
1580    /// continue to validate untrusted assignments before reaching materialization.
1581    pub(crate) fn project_validated_branches_deferred(
1582        &self,
1583        regions: &[BranchRegion],
1584        shared_pipes: &[Pipe],
1585        values: &[bool],
1586    ) -> Result<Self, BlockGraphError> {
1587        assert_eq!(regions.len(), values.len());
1588        let actions = self
1589            .actions()
1590            .into_iter()
1591            .filter(|action| !matches!(action, Action::Branch { .. }))
1592            .collect::<Vec<_>>();
1593        materialize_projection(
1594            branchless_projection_base(self),
1595            shared_pipes,
1596            regions
1597                .iter()
1598                .zip(values)
1599                .map(|(region, &value)| (region, value)),
1600            &actions,
1601            true,
1602            ProjectionContext::Graph,
1603        )
1604    }
1605
1606    pub(crate) fn transformed_branches(
1607        &self,
1608        map: impl Fn(IVec3) -> Result<IVec3, BlockGraphError> + Copy,
1609    ) -> Result<Vec<BranchRegion>, BlockGraphError> {
1610        self.branches
1611            .iter()
1612            .map(|region| region.try_map_positions(map))
1613            .collect()
1614    }
1615
1616    /// A pipe touching a shown arm belongs to that arm, including a repaired cut.
1617    pub(crate) fn record_added_branch_pipe(&mut self, pipe: &Pipe) {
1618        self.record_added_branch_pipe_for(pipe, 0..self.branches.len());
1619    }
1620
1621    /// Linkers may preselect the regions touched by this pipe. Indices remain
1622    /// in stored region order, preserving cut updates and error precedence.
1623    pub(crate) fn record_added_branch_pipe_for(
1624        &mut self,
1625        pipe: &Pipe,
1626        regions: impl IntoIterator<Item = usize>,
1627    ) {
1628        if self.branches.is_empty() {
1629            return;
1630        }
1631        let (src, dst) = pipe.endpoints();
1632        let src_owner = self
1633            .get_endpoint_block(src)
1634            .expect("added pipe source exists")
1635            .pos();
1636        let dst_owner = self
1637            .get_endpoint_block(dst)
1638            .expect("added pipe destination exists")
1639            .pos();
1640        let key = pipe_key(pipe);
1641        for index in regions {
1642            let region = &mut self.branches[index];
1643            let owned = region
1644                .on_false
1645                .pipes()
1646                .chain(region.on_true.pipes())
1647                .any(|stored| pipe_key(stored) == key);
1648            region.set_cut_hadamard(key, pipe.is_hadamard());
1649            let (shown, incoming) = if region.shown_true {
1650                (&mut region.on_true, &mut region.incoming)
1651            } else {
1652                (&mut region.on_false, &mut region.false_incoming)
1653            };
1654            let src_inside = shown.contains_block(src_owner);
1655            let dst_inside = shown.contains_block(dst_owner);
1656            if !src_inside && !dst_inside {
1657                continue;
1658            }
1659            if src_inside != dst_inside && !owned {
1660                // External seams live once in the common graph, including
1661                // seams connecting two independently selected region owners.
1662                let (past, inside) = if src_inside { (dst, src) } else { (src, dst) };
1663                for cuts in [&mut region.incoming, &mut region.false_incoming] {
1664                    cuts.retain(|cut| pipe_key(&cut.pipe) != key);
1665                    cuts.push(BranchCut {
1666                        past,
1667                        inside,
1668                        pipe: pipe.clone(),
1669                    });
1670                    cuts.sort_by_key(|cut| {
1671                        (
1672                            cut.past.to_array(),
1673                            cut.inside.to_array(),
1674                            pipe_sort_key(&cut.pipe),
1675                        )
1676                    });
1677                }
1678                continue;
1679            }
1680            if let Some(stored) = shown
1681                .pipes
1682                .iter_mut()
1683                .find(|stored| pipe_key(stored) == key)
1684            {
1685                *stored = pipe.clone();
1686            } else {
1687                shown.push_pipe(pipe.clone());
1688            }
1689            if src_inside != dst_inside {
1690                let (past, inside) = if src_inside { (dst, src) } else { (src, dst) };
1691                incoming.retain(|cut| pipe_key(&cut.pipe) != key);
1692                incoming.push(BranchCut {
1693                    past,
1694                    inside,
1695                    pipe: pipe.clone(),
1696                });
1697                incoming.sort_by_key(|cut| {
1698                    (
1699                        cut.past.to_array(),
1700                        cut.inside.to_array(),
1701                        pipe_sort_key(&cut.pipe),
1702                    )
1703                });
1704            }
1705        }
1706    }
1707
1708    pub(crate) fn sync_shown_branch_arm_data(&mut self) {
1709        if self.branches.is_empty() {
1710            return;
1711        }
1712        let blocks = self
1713            .blocks()
1714            .map(|block| (block.pos(), block.clone()))
1715            .collect::<HashMap<_, _>>();
1716        let pipes = self
1717            .pipes()
1718            .map(|pipe| (pipe_key(pipe), pipe.clone()))
1719            .collect::<HashMap<_, _>>();
1720        for region in &mut self.branches {
1721            let (shown, incoming) = if region.shown_true {
1722                (&mut region.on_true, &mut region.incoming)
1723            } else {
1724                (&mut region.on_false, &mut region.false_incoming)
1725            };
1726            shown.blocks.retain_mut(|block| {
1727                if let Some(updated) = blocks.get(&block.pos()) {
1728                    *block = updated.clone();
1729                    true
1730                } else {
1731                    false
1732                }
1733            });
1734            shown.pipes.retain_mut(|pipe| {
1735                if let Some(updated) = pipes.get(&pipe_key(pipe)) {
1736                    *pipe = updated.clone();
1737                    true
1738                } else {
1739                    false
1740                }
1741            });
1742            incoming.retain_mut(|cut| {
1743                if let Some(updated) = pipes.get(&pipe_key(&cut.pipe)) {
1744                    cut.pipe = updated.clone();
1745                    true
1746                } else {
1747                    false
1748                }
1749            });
1750        }
1751    }
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756    use glam::ivec3;
1757
1758    use super::*;
1759    use crate::{Basis, BlockKind, CubeKind, Direction, GalleryItem, SelectiveKind};
1760
1761    #[test]
1762    fn reused_arm_validation_matches_independent_full_graph_copies() {
1763        // Previous implementation, retained as an independent scratch-lifetime
1764        // oracle: every region and each arm start from their own complete copy.
1765        fn cloned(
1766            source: &BlockGraph,
1767            region: &BranchRegion,
1768        ) -> Result<(ValidatedArm, ValidatedArm), BlockGraphError> {
1769            let shared = source.branch_shared_pipes();
1770            let mut common = source.clone();
1771            common.branches.clear();
1772            common.clear_actions();
1773            remove_arm(&mut common, region.shown_arm());
1774            let check = |arm: &BranchArm| {
1775                let positions = arm.reserved_positions()?;
1776                let mut arm = arm.clone();
1777                for pipe in &shared {
1778                    if positions.contains(&pipe.src()) || positions.contains(&pipe.dst()) {
1779                        arm.push_pipe(pipe.clone());
1780                    }
1781                }
1782                validate_arm(&mut common.clone(), &region.name, &arm)
1783            };
1784            let on_false = check(region.on_false())?;
1785            let on_true = check(region.on_true())?;
1786            if !on_false
1787                .incoming
1788                .iter()
1789                .map(cut_interface)
1790                .eq(on_true.incoming.iter().map(cut_interface))
1791            {
1792                return Err(invalid_interface(
1793                    &region.name,
1794                    "false and true arms have different external interfaces",
1795                ));
1796            }
1797            Ok((on_false, on_true))
1798        }
1799        let ast = crate::parse_blog_program_to_ast(include_str!(
1800            "../../docs/fixtures/conditional_cz_strip.blog"
1801        ))
1802        .unwrap();
1803        let base = crate::lower_blog_graph_ast_deferred(&ast)
1804            .unwrap()
1805            .root()
1806            .local_body()
1807            .clone();
1808        let mut cases = Vec::new();
1809        for mask in 0..8 {
1810            let mut graph = base.clone();
1811            for bit in 0..3 {
1812                graph
1813                    .set_shown_branch_arm(&format!("cz{bit}"), mask & (1 << bit) != 0)
1814                    .unwrap();
1815            }
1816            cases.push(graph);
1817        }
1818        for mutation in 0..4 {
1819            let mut graph = base.clone();
1820            let arm = &mut graph.branches[1].on_false;
1821            match mutation {
1822                0 => arm.blocks.push(arm.blocks[0].clone()),
1823                1 => arm.blocks.push(cube(ivec3(100, 100, 0))),
1824                2 => arm.blocks.push(cube(ivec3(1, -1, 0))), // Occupied by a common Port.
1825                _ => arm
1826                    .pipes
1827                    .push(Pipe::new(ivec3(100, 100, 0), Direction::ZPLUS)),
1828            }
1829            cases.push(graph);
1830        }
1831        let (mut missing_cut, _) = explicit_branch();
1832        missing_cut.branches[0].on_false.pipes.clear();
1833        cases.push(missing_cut);
1834        let summarize = |result: Result<(ValidatedArm, ValidatedArm), BlockGraphError>| {
1835            result
1836                .map(|(a, b)| ((a.incoming, a.target), (b.incoming, b.target)))
1837                .map_err(|error| error.to_string())
1838        };
1839        for graph in cases {
1840            let mut validation = BranchArmValidation::new(&graph);
1841            let mut valid = true;
1842            for region in &graph.branches {
1843                let expected = summarize(cloned(&graph, region));
1844                let actual = summarize(validation.validate(region));
1845                assert_eq!(actual, expected);
1846                if actual.is_err() {
1847                    valid = false;
1848                    break;
1849                }
1850            }
1851            if valid {
1852                let geometry = |graph: &BlockGraph| {
1853                    let mut blocks = graph.blocks().cloned().collect::<Vec<_>>();
1854                    let mut pipes = graph.pipes().cloned().collect::<Vec<_>>();
1855                    blocks.sort_by_key(|block| block.pos().to_array());
1856                    pipes.sort_by_key(pipe_sort_key);
1857                    (blocks, pipes)
1858                };
1859                assert_eq!(geometry(&validation.candidate), geometry(&graph));
1860            }
1861        }
1862    }
1863
1864    #[test]
1865    fn continuing_regions_keep_shared_seams_across_every_projection_and_preview() {
1866        let source = include_str!("../../docs/fixtures/conditional_cz_strip.blog");
1867        let ast = crate::parse_blog_program_to_ast(source).unwrap();
1868        let program = crate::lower_blog_graph_ast_deferred(&ast).unwrap();
1869        let mut graph = program.flatten().unwrap();
1870        assert!(graph.has_continuing_branches());
1871        assert_eq!(graph.branch_shared_pipes().len(), 4);
1872        let projections = graph.branch_projections_for_analysis().unwrap();
1873        assert_eq!(projections.len(), 8);
1874        for projection in projections {
1875            let enabled = projection
1876                .assignments()
1877                .iter()
1878                .filter(|(_, value)| *value)
1879                .count();
1880            assert_eq!(
1881                projection
1882                    .graph()
1883                    .pipes()
1884                    .filter(|pipe| pipe.is_hadamard())
1885                    .count(),
1886                2 * enabled
1887            );
1888            projection.graph().validate_structure().unwrap();
1889        }
1890        for mask in 0..8 {
1891            for bit in 0..3 {
1892                graph
1893                    .set_shown_branch_arm(&format!("cz{bit}"), mask & (1 << bit) != 0)
1894                    .unwrap();
1895            }
1896            assert_eq!(graph.branch_shared_pipes().len(), 4);
1897            assert_eq!(graph.branch_regions().unwrap().len(), 3);
1898        }
1899        for transformed in [
1900            graph.shift_positions(ivec3(3, -2, 4)).unwrap(),
1901            graph
1902                .rotate_about_origin_lenient(crate::UDirection::Z, 1)
1903                .unwrap(),
1904            graph.flip_xz_basis().unwrap(),
1905        ] {
1906            assert_eq!(transformed.branch_shared_pipes().len(), 4);
1907            let projections = transformed.branch_projections_for_analysis().unwrap();
1908            assert_eq!(projections.len(), 8);
1909            for projection in projections {
1910                projection.graph().validate_structure().unwrap();
1911            }
1912        }
1913        let text = program.to_blog_text();
1914        let ast = crate::parse_blog_program_to_ast(&text).unwrap();
1915        let reparsed = crate::lower_blog_graph_ast_deferred(&ast).unwrap();
1916        assert_eq!(
1917            reparsed
1918                .root()
1919                .local_body()
1920                .branch_projections_for_analysis()
1921                .unwrap()
1922                .len(),
1923            8
1924        );
1925    }
1926
1927    #[test]
1928    fn module_bindings_preserve_private_boundary_hadamards() {
1929        let source = r#"BLOG 1.0
1930module Gate {
1931  in input: data = 0
1932  out output: data = 1
1933  0: Port [0,0,0] role=input
1934  1: Port [0,0,2] role=output
1935  9: ZXZ [2,0,0]
1936  branch b {
1937    false {
1938      2: XZX [0,0,1]
1939      0 -H> +Z
1940      [0,0,1] -H> +Z
1941    }
1942    true {
1943      3: ZXZ [0,0,1]
1944      0 -> +Z
1945      [0,0,1] -> +Z
1946    }
1947  }
1948  m = measure 9
1949  resolve b if m
1950}
1951module main {
1952  gate: Gate @ [0,0,0]
1953  10: ZXZ [0,0,0]
1954  11: ZXZ [0,0,2]
1955  10 -> gate.input
1956  gate.output -> 11
1957}
1958"#;
1959        let program = crate::parse_inline_graph(source).unwrap();
1960        let graph = program.materialize_flat_graph().unwrap();
1961        let region = graph.branch_by_name("gate__b").unwrap();
1962        assert!(region.on_false().pipes().all(Pipe::is_hadamard));
1963        assert!(region.on_true().pipes().all(|pipe| !pipe.is_hadamard()));
1964        for selected in [false, true] {
1965            graph
1966                .project_branches([(region.target, selected)])
1967                .unwrap()
1968                .validate_structure()
1969                .unwrap();
1970        }
1971    }
1972
1973    fn cube(position: IVec3) -> Block {
1974        Block::new(position, BlockKind::Cube(CubeKind::ZXZ))
1975    }
1976
1977    fn explicit_branch() -> (BlockGraph, IVec3) {
1978        let mut graph = BlockGraph::new();
1979        graph.add_block(cube(ivec3(0, 0, 0)));
1980        graph.add_block(cube(ivec3(3, 0, 0)));
1981        let false_arm = BranchArm::new(
1982            vec![Block::new(ivec3(0, 0, 1), BlockKind::Measurement(Basis::X))],
1983            vec![Pipe::new(ivec3(0, 0, 0), Direction::ZPLUS)],
1984        );
1985        let true_arm = BranchArm::new(
1986            vec![cube(ivec3(0, 0, 1)), cube(ivec3(0, 0, 2))],
1987            vec![
1988                Pipe::new(ivec3(0, 0, 0), Direction::ZPLUS),
1989                Pipe::new(ivec3(0, 0, 1), Direction::ZPLUS),
1990            ],
1991        );
1992        let target = graph
1993            .try_add_branch_region("b0", false_arm, true_arm)
1994            .expect("matching explicit arms");
1995        graph
1996            .set_actions(vec![
1997                Action::Measure {
1998                    target: MeasureTarget::Node(ivec3(3, 0, 0)),
1999                    name: "m".into(),
2000                },
2001                Action::Branch {
2002                    target,
2003                    condition: Expr::Var("m".into()),
2004                },
2005            ])
2006            .expect("branch resolve validates");
2007        (graph, target)
2008    }
2009
2010    #[test]
2011    fn authored_branch_installs_resolve_and_preserves_both_projections() {
2012        let (expected, target) = explicit_branch();
2013        let mut graph = BlockGraph::new();
2014        graph.add_block(cube(ivec3(0, 0, 0)));
2015        graph.add_block(cube(ivec3(3, 0, 0)));
2016        graph
2017            .set_actions(vec![Action::Measure {
2018                target: MeasureTarget::Node(ivec3(3, 0, 0)),
2019                name: "m".into(),
2020            }])
2021            .unwrap();
2022        let branch = Branch::new(
2023            "b0",
2024            Expr::Var("m".into()),
2025            BranchArm::try_from_blocks([([0, 0, 1], "X")]).unwrap(),
2026            BranchArm::try_from_blocks([([0, 0, 1], "ZXZ"), ([0, 0, 2], "ZXZ")])
2027                .unwrap()
2028                .with_pipes([Pipe::new([0, 0, 1], Direction::ZPLUS)]),
2029        );
2030        graph
2031            .try_add_branches([branch], [Pipe::new([0, 0, 0], Direction::ZPLUS)], [])
2032            .unwrap();
2033        assert_eq!(graph.actions(), expected.actions());
2034        for value in [false, true] {
2035            assert_eq!(
2036                graph
2037                    .project_branches([(target, value)])
2038                    .unwrap()
2039                    .to_blog_text(),
2040                expected
2041                    .project_branches([(target, value)])
2042                    .unwrap()
2043                    .to_blog_text(),
2044            );
2045        }
2046        let reparsed = BlockGraph::from_text(&graph.to_blog_text()).unwrap();
2047        assert_eq!(reparsed.to_blog_text(), graph.to_blog_text());
2048    }
2049
2050    #[test]
2051    fn authored_branches_install_shared_seams_between_owners() {
2052        let mut graph = BlockGraph::new();
2053        for x in [0, 1, 3] {
2054            graph.add_block(cube(ivec3(x, 0, 0)));
2055        }
2056        let branches = [0, 1].map(|x| {
2057            let arm = BranchArm::try_from_blocks([([x, 0, 1], "ZXZ")]).unwrap();
2058            Branch::new(format!("b{x}"), Expr::Var("m".into()), arm.clone(), arm)
2059        });
2060        graph
2061            .try_add_branches(
2062                branches,
2063                [
2064                    Pipe::new([0, 0, 0], Direction::ZPLUS),
2065                    Pipe::new([1, 0, 0], Direction::ZPLUS),
2066                    Pipe::new([0, 0, 1], Direction::XPLUS),
2067                ],
2068                [Action::Measure {
2069                    target: MeasureTarget::Node(ivec3(3, 0, 0)),
2070                    name: "m".into(),
2071                }],
2072            )
2073            .unwrap();
2074        let regions = graph.branch_regions().unwrap();
2075        assert_eq!(regions.len(), 2);
2076        for region in &regions {
2077            assert!(region.on_true().pipes().next().is_none());
2078            assert!(region.on_false().pipes().next().is_none());
2079        }
2080        for first in [false, true] {
2081            for second in [false, true] {
2082                graph
2083                    .project_branches([(regions[0].target, first), (regions[1].target, second)])
2084                    .unwrap()
2085                    .validate_structure()
2086                    .unwrap();
2087            }
2088        }
2089    }
2090
2091    #[test]
2092    fn authored_branch_errors_leave_geometry_and_actions_unchanged() {
2093        let mut graph = BlockGraph::new();
2094        graph.add_block(cube(ivec3(0, 0, 0)));
2095        graph.add_block(cube(ivec3(3, 0, 0)));
2096        let before = graph.to_blog_text();
2097        let arm = BranchArm::try_from_blocks([([0, 0, 1], "Z")]).unwrap();
2098        for branch in [
2099            Branch::new("bad name", Expr::Var("m".into()), arm.clone(), arm.clone()),
2100            Branch::new("b", Expr::Var("missing".into()), arm.clone(), arm.clone()),
2101            Branch::new(
2102                "b",
2103                Expr::Var("m".into()),
2104                BranchArm::default(),
2105                arm.clone(),
2106            ),
2107            Branch::new(
2108                "b",
2109                Expr::Var("m".into()),
2110                arm.clone(),
2111                BranchArm::try_from_blocks([([1, 0, 1], "Z")]).unwrap(),
2112            ),
2113        ] {
2114            assert!(
2115                graph
2116                    .try_add_branches(
2117                        [branch],
2118                        [Pipe::new([0, 0, 0], Direction::ZPLUS)],
2119                        [Action::Measure {
2120                            target: MeasureTarget::Node(ivec3(3, 0, 0)),
2121                            name: "m".into()
2122                        }]
2123                    )
2124                    .is_err()
2125            );
2126            assert_eq!(graph.to_blog_text(), before);
2127            assert!(!graph.has_actions());
2128        }
2129        assert!(matches!(
2130            BranchArm::try_from_blocks([([0, 0, 1], "invalid")]),
2131            Err(BlockGraphError::Block(crate::BlockError::InvalidBlockKind(
2132                _
2133            )))
2134        ));
2135    }
2136
2137    // The previous per-region scan is an independent oracle for the transient
2138    // inverse index, including malformed action lists accepted by this helper.
2139    fn scalar_first_target(
2140        graph: &BlockGraph,
2141        actions: &[Action],
2142        region: &BranchRegion,
2143    ) -> Option<usize> {
2144        actions.iter().position(|action| match action {
2145            Action::Measure { target, .. } => measurement_touches_region(graph, *target, region),
2146            Action::Resolve { target, .. } => region.contains_any_block(*target),
2147            Action::Feedback { targets, .. } => targets.iter().any(|target| {
2148                let measurement =
2149                    target
2150                        .direction
2151                        .map_or(MeasureTarget::Node(target.target), |dir| {
2152                            MeasureTarget::Edge {
2153                                src: target.target,
2154                                dir,
2155                            }
2156                        });
2157                measurement_touches_region(graph, measurement, region)
2158            }),
2159            Action::Let { .. } | Action::DiscardIf(_) | Action::Branch { .. } => false,
2160        })
2161    }
2162
2163    #[test]
2164    fn region_target_index_matches_scalar_for_hidden_and_extended_endpoints() {
2165        let visible = cube(ivec3(0, 0, 0))
2166            .with_height("3d".parse().unwrap())
2167            .unwrap();
2168        let hidden = cube(ivec3(10, 0, 0))
2169            .with_height("3d".parse().unwrap())
2170            .unwrap();
2171        let virtual_offset = visible
2172            .connectable_offsets()
2173            .into_iter()
2174            .find(|&offset| offset != IVec3::ZERO)
2175            .unwrap();
2176        let mut graph = BlockGraph::new();
2177        graph.add_block(visible.clone());
2178        let mut first = BranchRegion::test_region(visible.pos(), vec![visible.pos()], Vec::new());
2179        first.on_true = BranchArm::new(vec![visible.clone()], Vec::new());
2180        first.on_false = BranchArm::new(vec![hidden.clone()], Vec::new());
2181        let mut second =
2182            BranchRegion::test_region(ivec3(20, 0, 0), vec![ivec3(20, 0, 0)], Vec::new());
2183        second.on_false = BranchArm::new(vec![hidden.clone()], Vec::new());
2184        graph.branches.extend([first, second]);
2185        let actions = [
2186            Action::Measure {
2187                target: MeasureTarget::Node(visible.pos() + virtual_offset),
2188                name: "visible".into(),
2189            },
2190            Action::Measure {
2191                target: MeasureTarget::Node(hidden.pos() + virtual_offset),
2192                name: "hidden_virtual_node".into(),
2193            },
2194            Action::Measure {
2195                target: MeasureTarget::Edge {
2196                    src: hidden.pos() + virtual_offset,
2197                    dir: Direction::XPLUS,
2198                },
2199                name: "hidden_edge".into(),
2200            },
2201            Action::Feedback {
2202                targets: vec![crate::FeedbackTarget {
2203                    target: hidden.pos(),
2204                    direction: Some(Direction::ZPLUS),
2205                    pauli: bloq_utils::PauliBasis::X,
2206                }],
2207                condition: None,
2208            },
2209            Action::Resolve {
2210                target: hidden.pos(),
2211                condition: Expr::Var("visible".into()),
2212            },
2213            Action::Measure {
2214                target: MeasureTarget::Node(ivec3(100, 0, 0)),
2215                name: "outside".into(),
2216            },
2217        ];
2218        for action in &actions {
2219            let indexed = earliest_region_action_targets(&graph, std::slice::from_ref(action));
2220            for (region, definition) in graph.branches.iter().enumerate() {
2221                assert_eq!(
2222                    indexed[region],
2223                    scalar_first_target(&graph, std::slice::from_ref(action), definition),
2224                    "{action:?}, region {region}"
2225                );
2226            }
2227        }
2228        let indexed = earliest_region_action_targets(&graph, &actions);
2229        for (region, definition) in graph.branches.iter().enumerate() {
2230            assert_eq!(
2231                indexed[region],
2232                scalar_first_target(&graph, &actions, definition)
2233            );
2234        }
2235        assert_eq!(indexed, [Some(0), Some(2)]);
2236    }
2237
2238    #[test]
2239    fn region_action_index_preserves_controller_and_action_error_order() {
2240        let (graph, target) = explicit_branch();
2241        let inside = graph.branches[0].on_true.blocks().next().unwrap().pos();
2242        let outside = ivec3(3, 0, 0);
2243        let measure = |target, name: &str| Action::Measure {
2244            target: MeasureTarget::Node(target),
2245            name: name.into(),
2246        };
2247        let branch = |name: &str| Action::Branch {
2248            target,
2249            condition: Expr::Var(name.into()),
2250        };
2251        let actions = [
2252            measure(inside, "m"),
2253            Action::Let {
2254                name: "alias".into(),
2255                expr: Expr::Var("m".into()),
2256            },
2257            branch("alias"),
2258        ];
2259        assert!(matches!(
2260            validate_regions_for_actions(&graph, &actions),
2261            Err(BlockGraphError::InvalidAction(InvalidActionError::BranchControllerInside {
2262                target: actual,
2263                controller: MeasureTarget::Node(site),
2264            })) if actual == target && site == inside
2265        ));
2266        let duplicate_alias = [
2267            measure(inside, "m"),
2268            measure(outside, "m"),
2269            Action::Let {
2270                name: "alias".into(),
2271                expr: Expr::Var("m".into()),
2272            },
2273            branch("alias"),
2274        ];
2275        assert!(matches!(
2276            validate_regions_for_actions(&graph, &duplicate_alias),
2277            Err(BlockGraphError::InvalidAction(InvalidActionError::BranchContainsActionTarget {
2278                target: actual,
2279                ordinal: 0,
2280            })) if actual == target
2281        ));
2282        let cyclic_alias = [
2283            measure(inside, "m"),
2284            Action::Let {
2285                name: "a".into(),
2286                expr: Expr::Var("b".into()),
2287            },
2288            Action::Let {
2289                name: "b".into(),
2290                expr: Expr::Var("a".into()),
2291            },
2292            branch("a"),
2293        ];
2294        assert!(matches!(
2295            validate_regions_for_actions(&graph, &cyclic_alias),
2296            Err(BlockGraphError::InvalidAction(InvalidActionError::BranchContainsActionTarget {
2297                target: actual,
2298                ordinal: 0,
2299            })) if actual == target
2300        ));
2301        assert!(matches!(
2302            validate_regions_for_actions(&graph, &[branch("m"), branch("m")]),
2303            Err(BlockGraphError::InvalidAction(InvalidActionError::DuplicateBranchTarget(actual)))
2304                if actual == target
2305        ));
2306        assert!(matches!(
2307            validate_regions_for_actions(&graph, &[]),
2308            Err(BlockGraphError::InvalidAction(
2309                InvalidActionError::MissingResolveForBranch(_)
2310            ))
2311        ));
2312        assert!(matches!(
2313            validate_regions_for_actions(
2314                &graph,
2315                &[Action::Branch {
2316                    target: ivec3(100, 0, 0),
2317                    condition: Expr::Var("m".into()),
2318                }],
2319            ),
2320            Err(BlockGraphError::InvalidAction(InvalidActionError::InvalidBranchTarget(site)))
2321                if site == ivec3(100, 0, 0)
2322        ));
2323    }
2324
2325    #[test]
2326    fn region_action_index_keeps_branch_order_before_action_order() {
2327        let ast = crate::parse_blog_program_to_ast(include_str!(
2328            "../../docs/fixtures/conditional_cz_strip.blog"
2329        ))
2330        .unwrap();
2331        let graph = crate::lower_blog_graph_ast_deferred(&ast)
2332            .unwrap()
2333            .root()
2334            .local_body()
2335            .clone();
2336        let first = &graph.branches[0];
2337        let second = &graph.branches[1];
2338        let actions = [
2339            Action::Measure {
2340                target: MeasureTarget::Node(second.on_true.blocks().next().unwrap().pos()),
2341                name: "second".into(),
2342            },
2343            Action::Measure {
2344                target: MeasureTarget::Node(first.on_true.blocks().next().unwrap().pos()),
2345                name: "first".into(),
2346            },
2347            Action::Branch {
2348                target: first.target,
2349                condition: Expr::Var("external".into()),
2350            },
2351            Action::Branch {
2352                target: second.target,
2353                condition: Expr::Var("external".into()),
2354            },
2355        ];
2356        assert_eq!(
2357            earliest_region_action_targets(&graph, &actions)[..2],
2358            [Some(1), Some(0)]
2359        );
2360        assert!(matches!(
2361            validate_regions_for_actions(&graph, &actions),
2362            Err(BlockGraphError::InvalidAction(InvalidActionError::BranchContainsActionTarget {
2363                target,
2364                ordinal: 1,
2365            })) if target == first.target
2366        ));
2367    }
2368
2369    #[test]
2370    fn analysis_branch_projections_honor_boolean_limits() {
2371        let (graph, target) = explicit_branch();
2372        for (limits, resource) in [
2373            (
2374                BooleanLimits {
2375                    max_nodes: 0,
2376                    ..BooleanLimits::UNLIMITED
2377                },
2378                "Boolean nodes",
2379            ),
2380            (
2381                BooleanLimits {
2382                    max_steps: 0,
2383                    ..BooleanLimits::UNLIMITED
2384                },
2385                "Boolean work steps",
2386            ),
2387        ] {
2388            assert!(matches!(
2389                graph.branch_projections_for_analysis_up_to_with_limits(2, limits),
2390                Err(BlockGraphError::Stabilizer(crate::StabilizerError::ResourceLimited {
2391                    phase, limit: 0, ..
2392                })) if phase == resource
2393            ));
2394        }
2395        let projections = graph
2396            .branch_projections_for_analysis_up_to_with_limits(2, BooleanLimits::UNLIMITED)
2397            .unwrap();
2398        assert_eq!(
2399            projections
2400                .iter()
2401                .map(|projection| projection.assignments().to_vec())
2402                .collect::<Vec<_>>(),
2403            [vec![(target, false)], vec![(target, true)]]
2404        );
2405    }
2406
2407    #[test]
2408    fn terminal_branch_action_analysis_checks_source_limits() {
2409        use crate::{ModuleCertificationLimits, StabilizerError};
2410
2411        let (mut graph, _) = explicit_branch();
2412        assert!(!graph.has_continuing_branches());
2413        for shown_true in [true, false] {
2414            graph.set_shown_branch_arm("b0", shown_true).unwrap();
2415            for (limits, phase, observed) in [
2416                (
2417                    ModuleCertificationLimits {
2418                        max_expanded_blocks: 0,
2419                        ..ModuleCertificationLimits::DEFAULT
2420                    },
2421                    "expanded blocks",
2422                    1,
2423                ),
2424                (
2425                    ModuleCertificationLimits {
2426                        max_occupied_cells: 0,
2427                        ..ModuleCertificationLimits::DEFAULT
2428                    },
2429                    "occupied footprint cells",
2430                    1,
2431                ),
2432                (
2433                    ModuleCertificationLimits {
2434                        max_local_columns: 0,
2435                        ..ModuleCertificationLimits::DEFAULT
2436                    },
2437                    "local ZX columns",
2438                    8,
2439                ),
2440            ] {
2441                assert!(matches!(
2442                    graph.clone().analyze_actions_with_limits(limits),
2443                    Err(BlockGraphError::Stabilizer(StabilizerError::ResourceLimited {
2444                        phase: actual_phase,
2445                        observed: actual_observed,
2446                        limit: 0,
2447                    })) if actual_phase == phase && actual_observed == observed
2448                ));
2449            }
2450            graph
2451                .clone()
2452                .analyze_actions_with_limits(ModuleCertificationLimits::DEFAULT)
2453                .unwrap();
2454        }
2455    }
2456
2457    #[test]
2458    fn arms_may_overlap_and_project_to_authored_contents() {
2459        let (graph, target) = explicit_branch();
2460        let false_graph = graph.project_branches([(target, false)]).unwrap();
2461        let true_graph = graph.project_branches([(target, true)]).unwrap();
2462        assert_eq!(
2463            false_graph.get_block(ivec3(0, 0, 1)).map(Block::kind),
2464            Some(BlockKind::Measurement(Basis::X))
2465        );
2466        assert!(false_graph.get_block(ivec3(0, 0, 2)).is_none());
2467        assert!(true_graph.get_block(ivec3(0, 0, 2)).is_some());
2468    }
2469
2470    #[test]
2471    fn deferred_projection_does_not_run_flat_action_analysis() {
2472        let (graph, target) = explicit_branch();
2473        let projected = graph.project_branches_deferred([(target, false)]).unwrap();
2474
2475        assert!(!projected.action_graph().is_analyzed());
2476    }
2477
2478    #[test]
2479    fn batched_projections_match_individual_projection() {
2480        let (graph, _) = explicit_branch();
2481        for projection in graph.branch_projections().unwrap() {
2482            let individual = graph
2483                .project_branches(projection.assignments().iter().copied())
2484                .unwrap();
2485            assert_eq!(projection.graph().to_blog_text(), individual.to_blog_text());
2486        }
2487    }
2488
2489    #[test]
2490    fn analysis_projections_include_joint_branch_choices() {
2491        let (mut graph, first) = explicit_branch();
2492        let prefix = ivec3(6, 0, 0);
2493        let target = ivec3(6, 0, 1);
2494        let reader = ivec3(9, 0, 0);
2495        graph.add_block(cube(prefix));
2496        graph.add_block(cube(reader));
2497        let second = graph
2498            .try_add_branch_region(
2499                "b1",
2500                BranchArm::new(
2501                    vec![Block::new(target, BlockKind::Measurement(Basis::X))],
2502                    vec![Pipe::new(prefix, Direction::ZPLUS)],
2503                ),
2504                BranchArm::new(
2505                    vec![cube(target)],
2506                    vec![Pipe::new(prefix, Direction::ZPLUS)],
2507                ),
2508            )
2509            .unwrap();
2510        graph
2511            .set_actions(vec![
2512                Action::Measure {
2513                    target: MeasureTarget::Node(ivec3(3, 0, 0)),
2514                    name: "m".into(),
2515                },
2516                Action::Branch {
2517                    target: first,
2518                    condition: Expr::Var("m".into()),
2519                },
2520                Action::Measure {
2521                    target: MeasureTarget::Node(reader),
2522                    name: "n".into(),
2523                },
2524                Action::Branch {
2525                    target: second,
2526                    condition: Expr::Var("n".into()),
2527                },
2528            ])
2529            .unwrap();
2530
2531        assert_eq!(graph.branch_projections_for_analysis().unwrap().len(), 4);
2532    }
2533
2534    #[test]
2535    fn projections_keep_external_action_inputs() {
2536        let (mut graph, target) = explicit_branch();
2537        graph
2538            .set_actions_with_inputs(
2539                vec![Action::Branch {
2540                    target,
2541                    condition: Expr::Var("enabled".into()),
2542                }],
2543                ["enabled".to_string()],
2544            )
2545            .unwrap();
2546
2547        for projection in graph.branch_projections().unwrap() {
2548            assert_eq!(
2549                projection
2550                    .graph()
2551                    .action_graph()
2552                    .inputs()
2553                    .collect::<Vec<_>>(),
2554                ["enabled"]
2555            );
2556        }
2557    }
2558
2559    #[test]
2560    fn reused_measurement_prefix_matches_independent_projection_tables() {
2561        let graph = GalleryItem::CCZGateTeleport
2562            .build()
2563            .materialize_root_graph()
2564            .expect("gallery flat projection");
2565        let (_, _, projections) = graph.analyze_actions_with_projections().unwrap();
2566        for (projection, reused) in projections {
2567            assert!(projection.graph().action_graph().is_analyzed());
2568            let independent = projection.graph().stabilizers().unwrap();
2569            assert_eq!(reused.generators, independent.generators);
2570        }
2571    }
2572
2573    #[test]
2574    fn scene_can_switch_the_visible_arm() {
2575        let (mut graph, _) = explicit_branch();
2576        graph.set_shown_branch_arm("b0", false).unwrap();
2577        assert_eq!(
2578            graph.get_block(ivec3(0, 0, 1)).map(Block::kind),
2579            Some(BlockKind::Measurement(Basis::X))
2580        );
2581        assert!(!graph.branch_by_name("b0").unwrap().shown_true());
2582        assert!(matches!(
2583            graph.try_add_block(cube(ivec3(0, 0, 2))),
2584            Err(BlockGraphError::BlockPositionOccupied(_))
2585        ));
2586    }
2587
2588    #[test]
2589    fn visible_arm_edits_survive_toggle() {
2590        let (mut graph, _) = explicit_branch();
2591        let pos = ivec3(0, 0, 2);
2592        graph
2593            .set_block_kind(pos, BlockKind::Cube(CubeKind::XZX))
2594            .unwrap();
2595        graph.set_block_tag(pos, "edited").unwrap();
2596        graph.set_cube_height(pos, "2d".parse().unwrap()).unwrap();
2597        graph.set_pipe_hadamard(ivec3(0, 0, 1), pos, true).unwrap();
2598        graph
2599            .set_pipe_hadamard(ivec3(0, 0, 0), ivec3(0, 0, 1), true)
2600            .unwrap();
2601        graph.set_shown_branch_arm("b0", false).unwrap();
2602        assert!(
2603            graph
2604                .get_pipe(ivec3(0, 0, 0), ivec3(0, 0, 1))
2605                .unwrap()
2606                .is_hadamard()
2607        );
2608        graph.set_shown_branch_arm("b0", true).unwrap();
2609
2610        let block = graph.get_block(pos).unwrap();
2611        assert_eq!(block.kind(), BlockKind::Cube(CubeKind::XZX));
2612        assert_eq!(block.tag(), Some("edited"));
2613        assert_eq!(block.height(), "2d".parse().unwrap());
2614        assert!(graph.get_pipe(ivec3(0, 0, 1), pos).unwrap().is_hadamard());
2615    }
2616
2617    #[test]
2618    fn captured_arm_transforms_keep_cuts_and_check_hidden_footprints() {
2619        let (graph, _) = explicit_branch();
2620        let arm = graph.branch_by_name("b0").unwrap().on_true();
2621        let offset = ivec3(3, -2, 4);
2622        let shifted = arm.try_with_shift(offset).unwrap();
2623        assert_eq!(shifted.try_with_shift(-offset).unwrap(), *arm);
2624        for (original, shifted) in arm.pipes().zip(shifted.pipes()) {
2625            assert_eq!(shifted.src(), original.src() + offset);
2626            assert_eq!(shifted.dst(), original.dst() + offset);
2627        }
2628        let rotation = crate::ModuleRotation::new(crate::UDirection::Z, 1).orientation();
2629        let rotated = arm.try_with_orientation(rotation).unwrap();
2630        let rotated_graph = graph.with_orientation_lenient(rotation).unwrap();
2631        assert_eq!(
2632            rotated,
2633            *rotated_graph.branch_by_name("b0").unwrap().on_true()
2634        );
2635        assert_eq!(arm.flip_xz_basis(&graph).flip_xz_basis(&graph), *arm);
2636
2637        let tall = BranchArm::new(
2638            vec![
2639                cube(ivec3(0, 0, i32::MAX - 1))
2640                    .with_height("2d".parse().unwrap())
2641                    .unwrap(),
2642            ],
2643            Vec::new(),
2644        );
2645        assert!(matches!(
2646            tall.try_with_shift(IVec3::Z),
2647            Err(BlockGraphError::CoordinateOverflow { .. })
2648        ));
2649        assert!(matches!(
2650            tall.try_with_orientation(
2651                crate::ModuleRotation::new(crate::UDirection::X, 1).orientation(),
2652            ),
2653            Err(BlockGraphError::RotationCubeHeightRequiresTimeAxis)
2654        ));
2655        let edge = BranchArm::new(
2656            vec![cube(ivec3(0, 0, i32::MAX - 1))],
2657            vec![Pipe::new(ivec3(0, 0, i32::MAX - 1), Direction::ZPLUS)],
2658        );
2659        assert!(matches!(
2660            edge.try_with_shift(IVec3::Z),
2661            Err(BlockGraphError::CoordinateOverflow { .. })
2662        ));
2663    }
2664
2665    #[test]
2666    fn visible_arm_deletions_survive_toggle_and_serialization() {
2667        let (mut graph, _) = explicit_branch();
2668        let terminal = ivec3(0, 0, 2);
2669        graph.remove_block(terminal).unwrap();
2670        graph.set_shown_branch_arm("b0", false).unwrap();
2671        graph.set_shown_branch_arm("b0", true).unwrap();
2672        assert!(!graph.has_block_at(terminal));
2673        assert!(!graph.has_pipe_between(IVec3::Z, terminal));
2674        let reparsed = BlockGraph::from_blog_text(&graph.to_blog_text()).unwrap();
2675        assert!(!reparsed.has_block_at(terminal));
2676        reparsed.validate().unwrap();
2677
2678        graph.remove_pipe(IVec3::ZERO, IVec3::Z).unwrap();
2679        assert!(graph.branch_regions().is_err(), "a removed cut is invalid");
2680        graph.set_shown_branch_arm("b0", false).unwrap();
2681        graph.set_shown_branch_arm("b0", true).unwrap();
2682        assert!(!graph.has_pipe_between(IVec3::ZERO, IVec3::Z));
2683        assert!(graph.branch_by_name("b0").unwrap().incoming.is_empty());
2684        BlockGraph::from_blog_text(&graph.to_blog_text()).unwrap_err();
2685    }
2686
2687    #[test]
2688    fn readding_branch_pipes_repairs_internal_edges_and_incoming_cuts() {
2689        for (shown_true, src) in [(true, IVec3::Z), (true, IVec3::ZERO), (false, IVec3::ZERO)] {
2690            let (mut graph, _) = explicit_branch();
2691            graph.set_shown_branch_arm("b0", shown_true).unwrap();
2692            let dst = src + IVec3::Z;
2693            let pipe = graph.remove_pipe(src, dst).unwrap();
2694            graph.branch_regions().unwrap_err();
2695
2696            graph.try_add_pipe(pipe).unwrap();
2697            graph.validate().unwrap();
2698            graph.set_shown_branch_arm("b0", !shown_true).unwrap();
2699            graph.set_shown_branch_arm("b0", shown_true).unwrap();
2700            assert!(graph.has_pipe_between(src, dst));
2701            let reparsed = BlockGraph::from_blog_text(&graph.to_blog_text()).unwrap();
2702            reparsed.validate().unwrap();
2703        }
2704    }
2705
2706    #[test]
2707    fn editor_can_capture_one_arm_then_build_the_other_in_place() {
2708        let past = ivec3(0, 0, 0);
2709        let inside = ivec3(0, 0, 1);
2710        let mut graph = BlockGraph::new();
2711        graph.add_block(cube(past));
2712        graph.add_block(cube(inside));
2713        graph.add_pipe(Pipe::new(past, Direction::ZPLUS));
2714        graph
2715            .set_actions_with_inputs(Vec::new(), ["enabled".into()])
2716            .unwrap();
2717
2718        graph.take_branch_arm([inside], []).unwrap_err();
2719        assert!(
2720            graph.get_block(inside).is_some(),
2721            "failed capture is atomic"
2722        );
2723        assert!(graph.get_pipe(past, inside).is_some());
2724
2725        let on_true = graph.take_branch_arm([inside], [(past, inside)]).unwrap();
2726        assert!(graph.get_block(inside).is_none(), "captured arm is hidden");
2727        graph.restore_branch_arm(&on_true).unwrap();
2728        let on_true = graph.take_branch_arm([inside], [(past, inside)]).unwrap();
2729        graph.add_block(Block::new(inside, BlockKind::Measurement(Basis::Z)));
2730        graph.add_pipe(Pipe::new(past, Direction::ZPLUS).with_tag("false").unwrap());
2731        let on_false = graph.take_branch_arm([inside], [(past, inside)]).unwrap();
2732
2733        let target = graph
2734            .try_add_branch_region("b0", on_false, on_true)
2735            .unwrap();
2736        assert_eq!(
2737            graph.action_graph().inputs().collect::<Vec<_>>(),
2738            ["enabled"]
2739        );
2740        graph
2741            .set_actions(vec![Action::Branch {
2742                target,
2743                condition: Expr::Var("enabled".into()),
2744            }])
2745            .unwrap();
2746        assert_eq!(
2747            graph.get_block(inside).map(Block::kind),
2748            Some(BlockKind::Cube(CubeKind::ZXZ)),
2749            "completed region initially shows its true arm"
2750        );
2751    }
2752
2753    #[test]
2754    fn branch_arms_reject_ports_t_and_selectives() {
2755        let past = ivec3(0, 0, 0);
2756        let inside = ivec3(0, 0, 1);
2757        let valid = BranchArm::new(vec![cube(inside)], vec![Pipe::new(past, Direction::ZPLUS)]);
2758        let mut graph = BlockGraph::new();
2759        graph.add_block(cube(past));
2760
2761        for kind in [
2762            BlockKind::Port,
2763            BlockKind::T,
2764            BlockKind::Selective(SelectiveKind::XY),
2765        ] {
2766            let invalid = BranchArm::new(
2767                vec![Block::new(inside, kind)],
2768                vec![Pipe::new(past, Direction::ZPLUS)],
2769            );
2770            assert!(matches!(
2771                graph.try_add_branch_region("b0", invalid, valid.clone()),
2772                Err(BlockGraphError::InvalidAction(
2773                    InvalidActionError::InvalidBranchInterface { .. }
2774                ))
2775            ));
2776        }
2777    }
2778
2779    #[test]
2780    fn basis_flip_updates_native_resource_cut_pipes_in_both_arms() {
2781        let (mut graph, target) = explicit_branch();
2782        graph.set_block_kind(IVec3::ZERO, BlockKind::T).unwrap();
2783        for hadamard in [false, true] {
2784            graph
2785                .set_pipe_hadamard(IVec3::ZERO, IVec3::Z, hadamard)
2786                .unwrap();
2787            let flipped = graph.flip_xz_basis().unwrap();
2788            for selected in [false, true] {
2789                let branch = flipped.branch_by_name("b0").unwrap();
2790                assert_eq!(
2791                    branch.arm_incoming(selected)[0].pipe.is_hadamard(),
2792                    !hadamard
2793                );
2794                assert_eq!(
2795                    graph
2796                        .branch_by_name("b0")
2797                        .unwrap()
2798                        .arm(selected)
2799                        .flip_xz_basis(&graph),
2800                    *branch.arm(selected),
2801                    "captured-arm and whole-graph flips share the external T frame"
2802                );
2803                let projected = flipped.project_branches([(target, selected)]).unwrap();
2804                assert_eq!(
2805                    projected
2806                        .get_pipe(IVec3::ZERO, IVec3::Z)
2807                        .unwrap()
2808                        .is_hadamard(),
2809                    !hadamard
2810                );
2811            }
2812            assert_eq!(
2813                flipped.flip_xz_basis().unwrap().to_blog_text(),
2814                graph.to_blog_text()
2815            );
2816        }
2817    }
2818
2819    #[test]
2820    fn basis_flip_keeps_a_hidden_y_arm_and_its_own_hadamard_cut() {
2821        let mut graph = BlockGraph::new();
2822        graph.add_block(cube(IVec3::ZERO));
2823        graph.add_block(cube(ivec3(3, 0, 0)));
2824        let pipe = Pipe::new(IVec3::ZERO, Direction::ZPLUS);
2825        let target = graph
2826            .try_add_branch_region(
2827                "b0",
2828                BranchArm::new(vec![Block::new(IVec3::Z, BlockKind::Y)], vec![pipe.clone()]),
2829                BranchArm::new(vec![cube(IVec3::Z)], vec![pipe]),
2830            )
2831            .unwrap();
2832        graph
2833            .set_actions(vec![
2834                Action::Measure {
2835                    name: "m".into(),
2836                    target: MeasureTarget::Node(ivec3(3, 0, 0)),
2837                },
2838                Action::Branch {
2839                    target,
2840                    condition: Expr::Var("m".into()),
2841                },
2842            ])
2843            .unwrap();
2844        let flipped = graph.flip_xz_basis().unwrap();
2845        let region = flipped.branch_by_name("b0").unwrap();
2846        assert!(region.arm_incoming(false)[0].pipe.is_hadamard());
2847        assert!(!region.arm_incoming(true)[0].pipe.is_hadamard());
2848        assert_eq!(
2849            graph
2850                .branch_by_name("b0")
2851                .unwrap()
2852                .on_false()
2853                .flip_xz_basis(&graph),
2854            *region.on_false()
2855        );
2856        for selected in [false, true] {
2857            let expected = graph
2858                .project_branches([(target, selected)])
2859                .unwrap()
2860                .flip_xz_basis()
2861                .unwrap();
2862            let actual = flipped.project_branches([(target, selected)]).unwrap();
2863            assert_eq!(actual.to_blog_text(), expected.to_blog_text());
2864        }
2865        assert_eq!(
2866            flipped.flip_xz_basis().unwrap().to_blog_text(),
2867            graph.to_blog_text()
2868        );
2869    }
2870
2871    #[test]
2872    fn different_regions_cannot_overlap_in_either_arm() {
2873        let (mut graph, _) = explicit_branch();
2874        let arm = BranchArm::new(
2875            vec![Block::new(ivec3(0, 0, 1), BlockKind::Measurement(Basis::X))],
2876            vec![Pipe::new(ivec3(0, 0, 0), Direction::ZPLUS)],
2877        );
2878        assert!(matches!(
2879            graph.try_add_branch_region("b1", arm.clone(), arm),
2880            Err(BlockGraphError::InvalidAction(
2881                InvalidActionError::OverlappingBranchRegions { .. }
2882            ))
2883        ));
2884    }
2885
2886    #[test]
2887    fn xz_flip_updates_both_stored_arms() {
2888        let (graph, _) = explicit_branch();
2889        let flipped = graph.flip_xz_basis().unwrap();
2890        let branch = flipped.branch_by_name("b0").unwrap();
2891        assert_eq!(
2892            branch.on_false().blocks().next().map(Block::kind),
2893            Some(BlockKind::Measurement(Basis::Z))
2894        );
2895        assert_eq!(
2896            branch.on_true().blocks().next().map(Block::kind),
2897            Some(BlockKind::Cube(CubeKind::XZX))
2898        );
2899    }
2900
2901    #[test]
2902    fn rotation_updates_both_stored_arms_and_cut() {
2903        let (graph, _) = explicit_branch();
2904        let rotated = graph
2905            .rotate_about_origin_lenient(crate::UDirection::X, 1)
2906            .unwrap();
2907        let branch = rotated.branch_by_name("b0").unwrap();
2908
2909        assert!(branch.on_false().contains_block(ivec3(0, -1, 0)));
2910        assert!(branch.on_true().contains_block(ivec3(0, -2, 0)));
2911        assert_eq!(branch.incoming[0].inside, ivec3(0, -1, 0));
2912        assert_eq!(branch.incoming[0].pipe.dir(), Direction::YMINUS);
2913    }
2914
2915    #[test]
2916    fn blog_parses_and_round_trips_named_region_and_resolve() {
2917        let source = r#"BLOG 1.0
2918
2919  0: ZXZ [0, 0, 0]
2920  1: ZXZ [3, 0, 0]
2921  5: ZXZ [2, 0, 0]
2922  [0, 0, 0] -> +Z
2923  [2, 0, 0] -> +Z
2924  branch b0 {
2925    false {
2926      2: ZXZ [0, 0, 1] height=2d
2927      6: ZXZ [1, 0, 1]
2928      7: X [2, 0, 1]
2929      [0, 0, 1] -> +X
2930    }
2931    true {
2932      3: ZXZ [0, 0, 1]
2933      4: ZXZ [0, 0, 2]
2934      8: Z [2, 0, 1]
2935      [0, 0, 1] -> +Z
2936    }
2937  }
2938
2939  m = measure 1
2940  resolve b0 if m
2941"#;
2942        let graph = BlockGraph::from_blog_text(source).expect("generic branch BLOG parses");
2943        assert_eq!(graph.branch_definitions().len(), 1);
2944        let mut false_cubes = graph.branch_definitions()[0]
2945            .on_false()
2946            .blocks()
2947            .filter(|block| block.kind().is_cube());
2948        assert!(false_cubes.all(|block| block.height() == "2d".parse().unwrap()));
2949        let written = graph.to_blog_text();
2950        assert!(written.contains("branch b0 {"));
2951        assert!(written.contains("resolve b0 if m"));
2952        assert_eq!(written.matches("0 -> +Z").count(), 1);
2953        assert_eq!(written.matches("2 -> +Z").count(), 1);
2954        assert!(written.contains(": X [2, 0, 1]"));
2955        assert!(written.contains(": Z [2, 0, 1]"));
2956        assert_eq!(
2957            BlockGraph::from_blog_text(&written)
2958                .expect("canonical generic BLOG reparses")
2959                .to_blog_text(),
2960            written
2961        );
2962    }
2963}