1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct BranchArm {
16 blocks: Vec<Block>,
17 pipes: Vec<Pipe>,
18}
19
20impl BranchArm {
21 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 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 #[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 pub fn blocks(&self) -> impl Iterator<Item = &Block> {
60 self.blocks.iter()
61 }
62
63 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 pub fn is_empty(&self) -> bool {
75 self.blocks.is_empty()
76 }
77
78 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 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 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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Branch {
194 pub name: String,
196 pub condition: Expr,
198 pub on_false: BranchArm,
200 pub on_true: BranchArm,
202}
203
204impl Branch {
205 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#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct BranchCut {
238 pub past: IVec3,
240 pub inside: IVec3,
242 pub pipe: Pipe,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct BranchRegion {
254 pub name: String,
256 pub target: IVec3,
259 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 pub fn on_false(&self) -> &BranchArm {
291 &self.on_false
292 }
293
294 pub fn on_true(&self) -> &BranchArm {
296 &self.on_true
297 }
298
299 pub fn shown_true(&self) -> bool {
301 self.shown_true
302 }
303
304 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 pub fn arm(&self, value: bool) -> &BranchArm {
323 if value { &self.on_true } else { &self.on_false }
324 }
325
326 pub fn arm_incoming(&self, value: bool) -> &[BranchCut] {
328 if value {
329 &self.incoming
330 } else {
331 &self.false_incoming
332 }
333 }
334
335 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 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#[derive(Debug, Clone)]
476pub struct BranchProjection {
477 assignments: Vec<(IVec3, bool)>,
478 graph: BlockGraph,
479}
480
481impl BranchProjection {
482 pub fn assignments(&self) -> &[(IVec3, bool)] {
484 &self.assignments
485 }
486
487 pub fn graph(&self) -> &BlockGraph {
489 &self.graph
490 }
491
492 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
703struct 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 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, ®ion.name, &on_false)?;
768 let on_true = self.with_seams(region.on_true())?;
769 let on_true = validate_arm(&mut self.candidate, ®ion.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 ®ion.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 (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 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 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 ®ion 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
983pub(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 ®ion.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(®ion.target))
1030 {
1031 return Err(InvalidActionError::MissingResolveForBranch(region.name.clone()).into());
1032 }
1033 Ok(regions)
1034}
1035
1036impl BlockGraph {
1037 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 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 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 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 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 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 ®ion.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 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 pub fn branch_definitions(&self) -> &[BranchRegion] {
1315 &self.branches
1316 }
1317
1318 pub fn branch_by_name(&self, name: &str) -> Option<&BranchRegion> {
1320 self.branches.iter().find(|region| region.name == name)
1321 }
1322
1323 pub fn branch_by_target(&self, target: IVec3) -> Option<&BranchRegion> {
1325 self.branches.iter().find(|region| region.target == target)
1326 }
1327
1328 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 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 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 pub fn branch_regions(&self) -> Result<Vec<BranchRegion>, BlockGraphError> {
1394 validate_regions_for_actions(self, &self.actions())
1395 }
1396
1397 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 #[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 #[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 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 #[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 ®ions {
1556 if !values.contains_key(®ion.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[®ion.target])),
1572 &remaining_actions,
1573 defer_action_analysis,
1574 context,
1575 )
1576 }
1577
1578 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 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 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 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 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(), ®ion.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 ®ion.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))), _ => 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 ®ions {
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 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}