1use crate::MeasureTarget;
4use crate::{
5 ActionDag, Basis, Block, BlockGraph, BlockGraphError, BlockKind, Direction, PatchRotationKind,
6 Pipe, UDirection,
7};
8use glam::IVec3;
9use std::collections::{HashMap, HashSet};
10use thiserror::Error;
11
12#[derive(Error, Debug, Clone)]
16#[non_exhaustive]
17pub enum InvalidBlockGraphError {
18 #[error("expected exactly one pipe connected to the block at {0}")]
20 NotExactlySinglePipe(IVec3),
21 #[error("spatial pipe connected to the block at {0}")]
23 SpatialPipeNotAllowed(IVec3),
24 #[error("spatial Port at {0} must declare role=input, role=output, or role=multiplex")]
26 SpatialPortRoleRequired(IVec3),
27 #[error("temporal Port at {pos} declares role {declared}, but its pipe makes it {inferred}")]
29 TemporalPortRoleMismatch {
30 pos: IVec3,
32 declared: crate::PortRole,
34 inferred: crate::PortRole,
36 },
37 #[error("only time-like pipe connected to past is allowed for block at {0}")]
39 FuturePipeNotAllowed(IVec3),
40 #[error("only time-like pipe connected to future is allowed for block at {0}")]
42 PastPipeNotAllowed(IVec3),
43 #[error(
45 "cannot resolve the lattice surgery orientation for pipe between blocks at {0} and {1}"
46 )]
47 OrientationContradiction(IVec3, IVec3),
48 #[error("block at {0} is not a planar junction")]
50 JunctionNotPlanar(IVec3),
51 #[error(
53 "degenerate pipe face: block at {0} has the same basis on both axes perpendicular to pipe toward {1}"
54 )]
55 DegeneratePipeFace(IVec3, IVec3),
56 #[error(
58 "spatially connected cubes at {pos} and {neighbor} have different heights (height={height} vs height={neighbor_height})"
59 )]
60 CubeHeightSpatialMismatch {
61 pos: IVec3,
63 neighbor: IVec3,
65 height: crate::CubeHeight,
67 neighbor_height: crate::CubeHeight,
69 },
70 #[error(
72 "multi-cell cube at {pos} has an invalid pipe endpoint {endpoint}; spatial pipes attach at the anchor, temporal pipes attach at bottom or top"
73 )]
74 CubeInvalidPipeEndpoint {
75 pos: IVec3,
77 endpoint: IVec3,
79 },
80 #[error("block footprints overlap at occupied position {pos}")]
82 BlockFootprintOverlap {
83 pos: IVec3,
85 },
86 #[error("Walking block at {pos} has a non-temporal pipe attached at endpoint {endpoint}")]
88 WalkingPipeNotTemporal {
89 pos: IVec3,
91 endpoint: IVec3,
93 },
94 #[error("Walking block at {pos} start endpoint only allows past temporal pipes")]
96 WalkingStartPipeNotPast {
97 pos: IVec3,
99 },
100 #[error("Walking block at {pos} end endpoint only allows future temporal pipes")]
102 WalkingEndPipeNotFuture {
103 pos: IVec3,
105 },
106 #[error(
108 "Patch rotation block at {pos} endpoint {endpoint} requires exactly one temporal pipe, got {count}"
109 )]
110 PatchRotationEndpointPipeCount {
111 pos: IVec3,
113 endpoint: IVec3,
115 count: usize,
117 },
118 #[error(
120 "Patch rotation block at {pos} has a non-temporal pipe attached at endpoint {endpoint}"
121 )]
122 PatchRotationPipeNotTemporal {
123 pos: IVec3,
125 endpoint: IVec3,
127 },
128 #[error("Patch rotation block at {pos} does not allow a Hadamard pipe at its start endpoint")]
130 PatchRotationStartPipeHadamard {
131 pos: IVec3,
133 },
134 #[error(
136 "Patch rotation block at {pos} endpoint {endpoint} has boundary orientation {actual:?}, expected {expected:?}"
137 )]
138 PatchRotationBoundaryMismatch {
139 pos: IVec3,
141 endpoint: IVec3,
143 expected: Basis,
145 actual: Option<Basis>,
147 },
148}
149
150#[derive(Error, Debug, Clone)]
153#[non_exhaustive]
154pub enum InvalidActionError {
155 #[error("measurement target edge at {0} along {1} is time-like")]
157 TimeLikeMeasurementEdge(IVec3, Direction),
158 #[error("measurement target node at {0} is invalid")]
160 InvalidMeasurementNode(IVec3),
161 #[error("measurement target {0:?} is duplicated")]
163 DuplicateMeasurementTarget(MeasureTarget),
164 #[error("action name '{0}' is not a valid BLOG identifier")]
166 InvalidActionName(String),
167 #[error("variable '{0}' defined multiple times")]
169 VariableRedefinition(String),
170 #[error("undefined variable '{0}' used")]
172 UndefinedVariable(String),
173 #[error("resolve target {0} is not a selective block")]
175 InvalidResolveTarget(IVec3),
176 #[error("resolve target {0} is duplicated")]
178 DuplicateResolveTarget(IVec3),
179 #[error("branch target {0} is not a branch region")]
181 InvalidBranchTarget(IVec3),
182 #[error("branch target {0} is duplicated")]
184 DuplicateBranchTarget(IVec3),
185 #[error("branch region '{0}' is duplicated")]
187 DuplicateBranchName(String),
188 #[error("branch region '{0}' has no resolve action")]
190 MissingResolveForBranch(String),
191 #[error("resolve names unknown branch region '{0}'")]
193 UnknownBranchName(String),
194 #[error("branch region '{name}' has invalid arm interface: {reason}")]
196 InvalidBranchInterface {
197 name: String,
199 reason: String,
201 },
202 #[error(
204 "continuing branches require GuardedSurfaceSpace; select a projection for a static stabilizer table"
205 )]
206 GuardedAnalysisRequired,
207 #[error("branch target {target} is controlled by measurement {controller:?} inside its region")]
209 BranchControllerInside {
210 target: IVec3,
212 controller: MeasureTarget,
214 },
215 #[error("branch target {target} contains physical target of action ordinal {ordinal}")]
217 BranchContainsActionTarget {
218 target: IVec3,
220 ordinal: usize,
222 },
223 #[error("branch regions at {first} and {second} overlap at block {block}")]
225 OverlappingBranchRegions {
226 first: IVec3,
228 second: IVec3,
230 block: IVec3,
232 },
233 #[error("branch target {0} has no projection value")]
235 MissingBranchValue(IVec3),
236 #[error("projection value names unknown branch target {0}")]
238 UnknownBranchValue(IVec3),
239 #[error("branch target {0} has more than one projection value")]
241 DuplicateBranchValue(IVec3),
242 #[error("measurement {name:?} has branch-dependent stabilizer support")]
244 BranchDependentMeasurementSurface {
245 name: String,
247 },
248 #[error("Selective block at {target} is missing its resolve action")]
250 MissingResolveForSelective {
251 target: IVec3,
253 },
254 #[error("action dependency cycle detected at ordinal {ordinal}")]
256 DependencyCycle {
257 ordinal: usize,
259 },
260 #[error("feedback requires at least one target")]
262 EmptyFeedbackTargets,
263 #[error("feedback target {0} is not a block")]
265 InvalidFeedbackTarget(IVec3),
266}
267
268pub(crate) fn validate(graph: &BlockGraph) -> Result<(), BlockGraphError> {
271 validate_with_limits(graph, crate::ModuleCertificationLimits::DEFAULT)
272}
273
274pub(crate) fn validate_with_limits(
275 graph: &BlockGraph,
276 limits: crate::ModuleCertificationLimits,
277) -> Result<(), BlockGraphError> {
278 let mut dag = validate_source(graph)?;
279 let regions = graph.branch_regions()?;
280 if graph.has_continuing_branches() {
281 return graph.validate_guarded_actions_with_limits(limits);
282 }
283 if (!regions.is_empty() || graph.has_actions()) && !graph.action_graph().is_analyzed() {
284 let stabilizers = if regions.is_empty() {
285 graph.stabilizers_with_limits(limits)?
286 } else {
287 graph.stabilizers_for_action_analysis_with_limits(limits)?
288 };
289 stabilizers.validate_selective_decoupling()?;
290 dag.attach_readout_dependencies(&stabilizers.generators, Some(&stabilizers.zx_graph))?;
291 }
292 Ok(())
293}
294
295pub(crate) fn validate_source(graph: &BlockGraph) -> Result<ActionDag, BlockGraphError> {
301 validate_structure(graph)?;
302 let mut dag = graph.build_action_graph(&graph.actions())?;
303 if !graph.has_actions() {
304 dag.validate(Some(graph))?;
308 }
309 Ok(dag)
310}
311
312pub(crate) fn validate_structure(graph: &BlockGraph) -> Result<(), BlockGraphError> {
313 structural_errors(graph)
314 .into_iter()
315 .next()
316 .map_or(Ok(()), Err)
317}
318
319pub(crate) fn structural_errors(graph: &BlockGraph) -> Vec<BlockGraphError> {
330 let mut errors = Vec::new();
331
332 collect_block_footprint_errors(graph, &mut errors);
333 if !errors.is_empty() {
334 return errors;
335 }
336 errors.extend(graph.pipes().filter_map(|pipe| pipe.try_endpoints().err()));
337 if !errors.is_empty() {
338 return errors;
339 }
340 for pipe in graph.pipes() {
341 for endpoint in [pipe.src(), pipe.dst()] {
342 if graph.get_endpoint_block(endpoint).is_none() {
343 errors.push(BlockGraphError::BlockNotFound(endpoint));
344 }
345 }
346 }
347 if !errors.is_empty() {
348 return errors;
349 }
350 collect_lattice_surgery_errors(graph, &mut errors);
351 if !errors.is_empty() {
352 return errors;
353 }
354 collect_block_constraint_errors(graph, &mut errors);
355 errors
356}
357
358fn collect_block_footprint_errors(graph: &BlockGraph, errors: &mut Vec<BlockGraphError>) {
359 let mut occupied = HashMap::<IVec3, Vec<(IVec3, BlockKind)>>::new();
360 for block in graph.blocks() {
361 let reserved = match block.checked_reserved_positions() {
362 Ok(reserved) => reserved,
363 Err(err) => {
364 errors.push(err);
365 continue;
366 }
367 };
368 for pos in reserved {
369 for (other_pos, other_kind) in occupied.get(&pos).into_iter().flatten() {
370 if !block
371 .kind
372 .allows_reserved_overlap(block.pos(), *other_kind, *other_pos, pos)
373 {
374 errors.push(InvalidBlockGraphError::BlockFootprintOverlap { pos }.into());
375 }
376 }
377 occupied
378 .entry(pos)
379 .or_default()
380 .push((block.pos(), block.kind));
381 }
382 }
383}
384
385fn collect_lattice_surgery_errors(graph: &BlockGraph, errors: &mut Vec<BlockGraphError>) {
386 for (u_pos, v_pos, _, _, pipe) in graph.pipe_endpoints_with_blocks() {
387 debug_assert_eq!(pipe.src(), u_pos);
388 let u_bases = graph.infer_pipe_endpoint_face_bases(pipe, u_pos);
389 let v_bases = graph.infer_pipe_endpoint_face_bases(pipe, v_pos);
390 let delta = u_pos - v_pos;
391 for ((d, u_basis), v_basis) in delta.to_array().into_iter().zip(u_bases).zip(v_bases) {
392 if d == 0
393 && let (Some(u_basis), Some(v_basis)) = (u_basis, v_basis)
394 && (u_basis != v_basis) ^ pipe.hadamard
395 {
396 errors.push(InvalidBlockGraphError::OrientationContradiction(u_pos, v_pos).into());
397 break;
398 }
399 }
400 }
401}
402
403type PipeIncidence<'a> = (IVec3, IVec3, &'a Block, &'a Block, &'a Pipe);
406
407fn collect_block_constraint_errors(graph: &BlockGraph, errors: &mut Vec<BlockGraphError>) {
408 let mut incidence: HashMap<IVec3, Vec<PipeIncidence>> = HashMap::new();
411 for tuple in graph.pipe_endpoints_with_blocks() {
412 let (_, _, u, v, _) = tuple;
413 incidence.entry(u.pos()).or_default().push(tuple);
414 if v.pos() != u.pos() {
415 incidence.entry(v.pos()).or_default().push(tuple);
416 }
417 }
418
419 for block in graph.blocks() {
422 let neighbors = graph.neighbor_positions(block.pos());
423 let incident: &[PipeIncidence] = match incidence.get(&block.pos()) {
424 Some(pipes) => pipes,
425 None => &[],
426 };
427 match block.kind {
428 BlockKind::Port
429 | BlockKind::Y
430 | BlockKind::Measurement(_)
431 | BlockKind::T
432 | BlockKind::Selective(_) => {
433 if neighbors.len() != 1 {
434 errors.push(InvalidBlockGraphError::NotExactlySinglePipe(block.pos()).into());
435 continue;
436 }
437 let has_spatial_pipe = neighbors
438 .iter()
439 .any(|&pos| (pos - block.pos()).abs() != IVec3::Z);
440 if block.kind == BlockKind::Port {
441 let role = block.port_role().expect("Port blocks expose a role");
442 if has_spatial_pipe && role == crate::PortRole::Auto {
443 errors.push(
444 InvalidBlockGraphError::SpatialPortRoleRequired(block.pos()).into(),
445 );
446 }
447 if !has_spatial_pipe && role != crate::PortRole::Auto {
448 let inferred = if neighbors[0].z > block.pos().z {
449 crate::PortRole::Input
450 } else {
451 crate::PortRole::Output
452 };
453 if role != inferred {
454 errors.push(
455 InvalidBlockGraphError::TemporalPortRoleMismatch {
456 pos: block.pos(),
457 declared: role,
458 inferred,
459 }
460 .into(),
461 );
462 continue;
463 }
464 }
465 }
466 let spatial_allowed = block.kind == BlockKind::Port;
467 if has_spatial_pipe && !spatial_allowed {
468 errors.push(InvalidBlockGraphError::SpatialPipeNotAllowed(block.pos()).into());
469 continue;
470 }
471 if matches!(block.kind, BlockKind::T)
472 && neighbors.iter().any(|&pos| pos - block.pos() != IVec3::Z)
473 {
474 errors.push(InvalidBlockGraphError::PastPipeNotAllowed(block.pos()).into());
477 continue;
478 }
479 if matches!(
480 block.kind,
481 BlockKind::Measurement(_) | BlockKind::Selective(_)
482 ) && neighbors
483 .iter()
484 .any(|&pos| pos - block.pos() != IVec3::NEG_Z)
485 {
486 errors.push(InvalidBlockGraphError::FuturePipeNotAllowed(block.pos()).into());
487 continue;
488 }
489 }
490 BlockKind::Cube(ck) => {
491 let height_errors = errors.len();
492 collect_cube_height_errors(block, incident, errors);
493 if errors.len() > height_errors {
494 continue;
495 }
496 let cube_neighbors = cube_endpoint_neighbors(block, incident);
497 for (endpoint, delta) in &cube_neighbors {
502 let diff = (*delta).abs();
503 let pipe_axis = diff
504 .to_array()
505 .iter()
506 .position(|&d| d != 0)
507 .expect("neighbor differs on at least one axis");
508 let perp: [usize; 2] = match pipe_axis {
509 0 => [1, 2],
510 1 => [0, 2],
511 _ => [0, 1],
512 };
513 if ck.bases()[perp[0]] == ck.bases()[perp[1]] {
517 errors.push(
518 InvalidBlockGraphError::DegeneratePipeFace(
519 block.pos(),
520 *endpoint + *delta,
521 )
522 .into(),
523 );
524 }
525 }
526
527 if cube_neighbors
528 .into_iter()
529 .map(|(_, delta)| delta.abs())
530 .collect::<HashSet<_>>()
531 .len()
532 == 3
533 {
534 errors.push(InvalidBlockGraphError::JunctionNotPlanar(block.pos()).into());
535 }
536 }
537 BlockKind::Walking(kind) => {
538 let start = block.pos();
539 let end = kind.end_position(start);
540 for &(u_pos, v_pos, u, v, _) in incident {
541 let (endpoint, other_pos) = if u.pos() == block.pos() {
542 (u_pos, v_pos)
543 } else if v.pos() == block.pos() {
544 (v_pos, u_pos)
545 } else {
546 continue;
547 };
548 let delta = other_pos - endpoint;
551 if delta.abs() != IVec3::Z {
552 errors.push(
553 InvalidBlockGraphError::WalkingPipeNotTemporal {
554 pos: start,
555 endpoint,
556 }
557 .into(),
558 );
559 continue;
560 }
561 if endpoint == start && delta != IVec3::NEG_Z {
562 errors.push(
563 InvalidBlockGraphError::WalkingStartPipeNotPast { pos: start }.into(),
564 );
565 continue;
566 }
567 if endpoint == end && delta != IVec3::Z {
568 errors.push(
569 InvalidBlockGraphError::WalkingEndPipeNotFuture { pos: start }.into(),
570 );
571 continue;
572 }
573 if endpoint != start && endpoint != end {
574 errors.push(
575 InvalidBlockGraphError::WalkingPipeNotTemporal {
576 pos: start,
577 endpoint,
578 }
579 .into(),
580 );
581 continue;
582 }
583 }
584 }
585 BlockKind::PatchRotation(kind) => {
586 collect_patch_rotation_errors(graph, block.pos(), kind, incident, errors);
587 }
588 }
589 }
590}
591
592fn cube_endpoint_neighbors(block: &Block, incident: &[PipeIncidence]) -> Vec<(IVec3, IVec3)> {
593 incident
594 .iter()
595 .filter_map(|&(u_pos, v_pos, u, v, _)| {
596 if u.pos() == block.pos() {
597 Some((u_pos, v_pos - u_pos))
598 } else if v.pos() == block.pos() {
599 Some((v_pos, u_pos - v_pos))
600 } else {
601 None
602 }
603 })
604 .collect()
605}
606
607fn collect_patch_rotation_errors(
612 graph: &BlockGraph,
613 start: IVec3,
614 kind: PatchRotationKind,
615 incident: &[PipeIncidence],
616 errors: &mut Vec<BlockGraphError>,
617) {
618 let end = kind.end_position(start);
619 for endpoint in [start, end] {
620 let pipes = pipes_at_endpoint(incident, start, endpoint);
621 if pipes.len() != 1 {
622 errors.push(
623 InvalidBlockGraphError::PatchRotationEndpointPipeCount {
624 pos: start,
625 endpoint,
626 count: pipes.len(),
627 }
628 .into(),
629 );
630 continue;
631 }
632 let pipe = pipes[0];
633 let neighbor = pipe_neighbor_at(pipe, endpoint)
634 .expect("pipe selected for endpoint must include that endpoint");
635 if (neighbor - endpoint).abs() != IVec3::Z {
636 errors.push(
637 InvalidBlockGraphError::PatchRotationPipeNotTemporal {
638 pos: start,
639 endpoint,
640 }
641 .into(),
642 );
643 continue;
644 }
645 if endpoint == start && pipe.is_hadamard() {
646 errors
647 .push(InvalidBlockGraphError::PatchRotationStartPipeHadamard { pos: start }.into());
648 continue;
649 }
650 let expected_basis = kind
651 .pipe_face_bases_at_endpoint(start, endpoint)
652 .expect("patch rotation endpoint was checked above")[UDirection::X.index()];
653 let actual_basis = pipe_neighbor_basis_at_endpoint(graph, pipe, endpoint);
654 if actual_basis.is_some_and(|actual| (actual != expected_basis) ^ pipe.is_hadamard()) {
655 errors.push(
656 InvalidBlockGraphError::PatchRotationBoundaryMismatch {
657 pos: start,
658 endpoint,
659 expected: expected_basis,
660 actual: actual_basis,
661 }
662 .into(),
663 );
664 }
665 }
666}
667
668fn pipe_neighbor_basis_at_endpoint(
669 graph: &BlockGraph,
670 pipe: &Pipe,
671 endpoint: IVec3,
672) -> Option<Basis> {
673 let neighbor_pos = pipe_neighbor_at(pipe, endpoint)?;
674 graph.infer_pipe_endpoint_face_bases(pipe, neighbor_pos)[UDirection::X.index()]
675}
676
677fn collect_cube_height_errors(
685 block: &Block,
686 incident: &[PipeIncidence],
687 errors: &mut Vec<BlockGraphError>,
688) {
689 let height = block.height();
690 let cells = block.height_cells();
691 let top = block.pos() + IVec3::new(0, 0, cells as i32 - 1);
692
693 for &(u_pos, v_pos, u, v, _) in incident {
694 let (endpoint, other_endpoint, other_block) = if u.pos() == block.pos() {
695 (u_pos, v_pos, v)
696 } else if v.pos() == block.pos() {
697 (v_pos, u_pos, u)
698 } else {
699 continue;
700 };
701 let delta = other_endpoint - endpoint;
702
703 if delta.z == 0 {
704 if cells > 1 && endpoint != block.pos() {
705 errors.push(
706 InvalidBlockGraphError::CubeInvalidPipeEndpoint {
707 pos: block.pos(),
708 endpoint,
709 }
710 .into(),
711 );
712 continue;
713 }
714 if other_block.kind.is_cube() && other_block.height() != height {
715 errors.push(
716 InvalidBlockGraphError::CubeHeightSpatialMismatch {
717 pos: block.pos(),
718 neighbor: other_block.pos(),
719 height,
720 neighbor_height: other_block.height(),
721 }
722 .into(),
723 );
724 continue;
725 }
726 } else if cells > 1 {
727 let valid_temporal_endpoint = (endpoint == block.pos() && delta == IVec3::NEG_Z)
728 || (endpoint == top && delta == IVec3::Z);
729 if !valid_temporal_endpoint {
730 errors.push(
731 InvalidBlockGraphError::CubeInvalidPipeEndpoint {
732 pos: block.pos(),
733 endpoint,
734 }
735 .into(),
736 );
737 }
738 }
739 }
740}
741
742fn pipes_at_endpoint<'a>(
743 incident: &[PipeIncidence<'a>],
744 owner_pos: IVec3,
745 endpoint: IVec3,
746) -> Vec<&'a Pipe> {
747 incident
748 .iter()
749 .filter_map(|&(u_pos, v_pos, u, v, pipe)| {
750 let touches_endpoint = (u.pos() == owner_pos && u_pos == endpoint)
751 || (v.pos() == owner_pos && v_pos == endpoint);
752 touches_endpoint.then_some(pipe)
753 })
754 .collect()
755}
756
757fn pipe_neighbor_at(pipe: &Pipe, endpoint: IVec3) -> Option<IVec3> {
758 if pipe.src() == endpoint {
759 Some(pipe.dst())
760 } else if pipe.dst() == endpoint {
761 Some(pipe.src())
762 } else {
763 None
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770 use crate::{Action, FeedbackTarget, PauliBasis};
771 use crate::{CubeKind, Expr, SelectiveKind, WalkingBoundaryKind, WalkingKind};
772 use glam::IVec2;
773
774 #[test]
775 fn structural_errors_lists_every_offending_block() {
776 let mut graph = BlockGraph::new();
777 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
779 graph.add_block(Block::new(IVec3::new(4, 0, 0), BlockKind::Port));
780
781 let errors = structural_errors(&graph);
782
783 assert_eq!(errors.len(), 2, "{errors:?}");
784 assert_eq!(
785 validate_structure(&graph)
786 .expect_err("graph is invalid")
787 .to_string(),
788 errors[0].to_string(),
789 "fail-fast validation reports the first collected error"
790 );
791 }
792
793 fn measurement(name: &str) -> Action {
795 Action::Measure {
796 target: MeasureTarget::Node(IVec3::ZERO),
797 name: name.to_string(),
798 }
799 }
800
801 #[test]
802 fn resolve_must_target_selective_block() {
803 let mut g = BlockGraph::new();
805 g.add_block(Block::new(
806 IVec3::new(0, 0, 0),
807 BlockKind::Cube(CubeKind::ZXZ),
808 ));
809 g.add_block(Block::new(
810 IVec3::new(1, 0, 0),
811 BlockKind::Cube(CubeKind::ZXZ),
812 ));
813 g.set_actions_unchecked(vec![
814 Action::Measure {
815 target: MeasureTarget::Node(IVec3::new(1, 0, 0)),
816 name: "m".to_string(),
817 },
818 Action::Resolve {
819 target: IVec3::new(0, 0, 0),
820 condition: Expr::Var("m".to_string()),
821 },
822 ]);
823 let err = validate(&g).unwrap_err();
824 assert!(
825 matches!(
826 err,
827 BlockGraphError::InvalidAction(InvalidActionError::InvalidResolveTarget(..))
828 ),
829 "expected InvalidResolveTarget, got: {err:?}",
830 );
831 }
832
833 #[test]
834 fn validate_rechecks_dag_actions_after_graph_mutation() {
835 let mut graph = BlockGraph::new();
836 graph.add_block(Block::new(
837 glam::ivec3(0, 0, 0),
838 BlockKind::Cube(CubeKind::ZXZ),
839 ));
840 graph.add_block(Block::new(
841 glam::ivec3(0, 0, 1),
842 BlockKind::Cube(CubeKind::ZXZ),
843 ));
844 graph.add_block(Block::new(
845 glam::ivec3(2, 0, 0),
846 BlockKind::Selective(SelectiveKind::XY),
847 ));
848 graph.add_block(Block::new(
849 glam::ivec3(2, 0, -1),
850 BlockKind::Cube(CubeKind::ZXZ),
851 ));
852 graph.add_pipe(Pipe::new(glam::ivec3(0, 0, 0), Direction::ZPLUS));
853 graph.add_pipe(Pipe::new(glam::ivec3(2, 0, -1), Direction::ZPLUS));
854 graph.set_actions_unchecked(vec![
855 Action::Measure {
856 target: MeasureTarget::Node(glam::ivec3(0, 0, 1)),
857 name: "m".to_string(),
858 },
859 Action::Resolve {
860 target: glam::ivec3(2, 0, 0),
861 condition: Expr::Var("m".to_string()),
862 },
863 ]);
864 let err = validate(&graph);
865 assert!(err.is_ok(), "{err:?}");
866
867 graph.add_block(Block::new(
868 glam::ivec3(4, 0, -1),
869 BlockKind::Cube(CubeKind::ZXZ),
870 ));
871 graph.add_block(Block::new(
872 glam::ivec3(4, 0, 0),
873 BlockKind::Selective(SelectiveKind::XY),
874 ));
875 graph.add_pipe(Pipe::new(glam::ivec3(4, 0, -1), Direction::ZPLUS));
876
877 let err = graph.validate().unwrap_err();
878 assert!(matches!(
879 err,
880 BlockGraphError::InvalidAction(InvalidActionError::MissingResolveForSelective {
881 target
882 }) if target == glam::ivec3(4, 0, 0)
883 ));
884 }
885
886 #[test]
887 fn validate_rejects_dependency_cycles_from_action_dag() {
888 let mut graph = BlockGraph::new();
889 graph.add_block(Block::new(
890 glam::ivec3(0, 0, -1),
891 BlockKind::Cube(CubeKind::ZXZ),
892 ));
893 graph.add_block(Block::new(
894 glam::ivec3(0, 0, 0),
895 BlockKind::Selective(SelectiveKind::XY),
896 ));
897 graph.add_pipe(Pipe::new(glam::ivec3(0, 0, -1), Direction::ZPLUS));
898 graph.set_actions_unchecked(vec![
899 Action::Measure {
900 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
901 name: "m".into(),
902 },
903 Action::Resolve {
904 target: glam::ivec3(0, 0, 0),
905 condition: Expr::Var("m".into()),
906 },
907 ]);
908 let err = validate(&graph).unwrap_err();
909 assert!(
910 matches!(
911 err,
912 BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { .. })
913 ),
914 "expected DependencyCycle, got: {err:?}",
915 );
916 }
917
918 #[test]
919 fn boundary_nodes_require_exactly_one_pipe() {
920 for kind in [
921 BlockKind::Port,
922 BlockKind::Y,
923 BlockKind::Measurement(Basis::X),
924 BlockKind::T,
925 BlockKind::Selective(SelectiveKind::XY),
926 ] {
927 let mut g = BlockGraph::new();
928 g.add_block(Block::new(IVec3::new(0, 0, 0), kind));
929
930 let err = validate(&g).unwrap_err();
931 assert!(
932 matches!(
933 err,
934 BlockGraphError::Invalid(InvalidBlockGraphError::NotExactlySinglePipe(_))
935 ),
936 "expected NotExactlySinglePipe for {kind:?}, got: {err:?}",
937 );
938 }
939 }
940
941 #[test]
942 fn spatial_ports_require_roles_and_temporal_roles_must_match_time() {
943 let mut spatial = BlockGraph::new();
944 spatial.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
945 spatial.add_block(Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ)));
946 spatial.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
947 assert!(matches!(
948 validate(&spatial),
949 Err(BlockGraphError::Invalid(
950 InvalidBlockGraphError::SpatialPortRoleRequired(IVec3::ZERO)
951 ))
952 ));
953 spatial
954 .set_port_role(IVec3::ZERO, crate::PortRole::Input)
955 .unwrap();
956 validate(&spatial).expect("directed spatial Port validates");
957 spatial
958 .set_port_role(IVec3::ZERO, crate::PortRole::Multiplex)
959 .unwrap();
960 validate(&spatial).expect("Multiplex spatial Port validates");
961
962 let mut temporal = BlockGraph::new();
963 temporal.add_block(
964 Block::new(IVec3::ZERO, BlockKind::Port)
965 .with_port_role(crate::PortRole::Output)
966 .unwrap(),
967 );
968 temporal.add_block(Block::new(IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
969 temporal.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
970 assert!(matches!(
971 validate(&temporal),
972 Err(BlockGraphError::Invalid(
973 InvalidBlockGraphError::TemporalPortRoleMismatch { .. }
974 ))
975 ));
976 temporal
977 .set_port_role(IVec3::ZERO, crate::PortRole::Multiplex)
978 .unwrap();
979 assert!(matches!(
980 validate(&temporal),
981 Err(BlockGraphError::Invalid(
982 InvalidBlockGraphError::TemporalPortRoleMismatch { .. }
983 ))
984 ));
985 }
986
987 #[test]
988 fn walking_blocks_may_be_unconnected() {
989 let mut graph = BlockGraph::new();
990 graph.add_block(Block::new(
991 IVec3::ZERO,
992 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::X).unwrap()),
993 ));
994
995 validate(&graph).unwrap();
996 }
997
998 #[test]
999 fn walking_soft_corridor_allows_same_start_layer_ports() {
1000 let mut graph = BlockGraph::new();
1001 graph.add_block(Block::new(
1002 IVec3::ZERO,
1003 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1004 ));
1005
1006 graph
1007 .try_add_block(
1008 Block::new(IVec3::new(1, 0, 0), BlockKind::Port)
1009 .with_port_role(crate::PortRole::Input)
1010 .unwrap(),
1011 )
1012 .expect("same-start-layer port may share walking soft corridor");
1013 graph.add_block(Block::new(
1014 IVec3::new(2, 0, 0),
1015 BlockKind::Cube(CubeKind::ZXZ),
1016 ));
1017 graph.add_pipe(Pipe::new(IVec3::new(1, 0, 0), Direction::XPLUS));
1018
1019 validate(&graph).unwrap();
1020 }
1021
1022 #[test]
1023 fn walking_soft_corridor_rejects_upper_layer_ports() {
1024 let mut graph = BlockGraph::new();
1025 graph.add_block(Block::new(
1026 IVec3::ZERO,
1027 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1028 ));
1029
1030 let err = graph
1031 .try_add_block(Block::new(IVec3::new(1, 0, 1), BlockKind::Port))
1032 .expect_err("upper-layer port cannot share walking soft corridor");
1033
1034 assert!(matches!(
1035 err,
1036 BlockGraphError::BlockPositionOccupied(pos) if pos == IVec3::new(1, 0, 1)
1037 ));
1038 }
1039
1040 #[test]
1041 fn walking_soft_corridor_allows_parallel_same_layer_walking() {
1042 let mut graph = BlockGraph::new();
1043 graph.add_block(Block::new(
1044 IVec3::ZERO,
1045 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1046 ));
1047
1048 graph
1049 .try_add_block(Block::new(
1050 IVec3::Y,
1051 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::XZX, IVec2::ONE).unwrap()),
1052 ))
1053 .expect("parallel same-layer walking blocks may share soft corridors");
1054
1055 validate(&graph).unwrap();
1056 }
1057
1058 #[test]
1059 fn walking_soft_corridor_rejects_nonparallel_walking() {
1060 let mut graph = BlockGraph::new();
1061 graph.add_block(Block::new(
1062 IVec3::ZERO,
1063 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1064 ));
1065
1066 let err = graph
1067 .try_add_block(Block::new(
1068 IVec3::X,
1069 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::Y).unwrap()),
1070 ))
1071 .expect_err("nonparallel walking blocks cannot share soft corridors");
1072
1073 assert!(matches!(
1074 err,
1075 BlockGraphError::BlockPositionOccupied(pos) if pos == IVec3::new(1, 0, 0)
1076 ));
1077 }
1078
1079 #[test]
1080 fn walking_soft_corridor_rejects_different_start_layer_walking() {
1081 let mut graph = BlockGraph::new();
1082 graph.add_block(Block::new(
1083 IVec3::ZERO,
1084 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1085 ));
1086
1087 let err = graph
1088 .try_add_block(Block::new(
1089 IVec3::new(1, 0, 1),
1090 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap()),
1091 ))
1092 .expect_err("different-start-layer walking blocks cannot share soft corridors");
1093
1094 assert!(matches!(
1095 err,
1096 BlockGraphError::BlockPositionOccupied(pos) if pos == IVec3::new(1, 0, 1)
1097 ));
1098 }
1099
1100 #[test]
1101 fn walking_start_accepts_past_temporal_pipe() {
1102 let mut graph = BlockGraph::new();
1103 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Port));
1104 graph.add_block(Block::new(
1105 IVec3::ZERO,
1106 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::X).unwrap()),
1107 ));
1108 graph.add_pipe(Pipe::new(IVec3::NEG_Z, Direction::ZPLUS));
1109
1110 validate(&graph).unwrap();
1111 }
1112
1113 #[test]
1114 fn walking_end_accepts_future_temporal_pipe() {
1115 let mut graph = BlockGraph::new();
1116 let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::X).expect("valid movement");
1117 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Walking(walking)));
1118 graph.add_block(Block::new(IVec3::new(1, 0, 2), BlockKind::Port));
1119 graph.add_pipe(Pipe::new(
1120 walking.end_position(IVec3::ZERO),
1121 Direction::ZPLUS,
1122 ));
1123
1124 validate(&graph).unwrap();
1125 }
1126
1127 #[test]
1128 fn walking_rejects_spatial_pipe_at_endpoint() {
1129 let mut graph = BlockGraph::new();
1130 graph.add_block(Block::new(
1131 IVec3::ZERO,
1132 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::X).unwrap()),
1133 ));
1134 graph.add_block(Block::new(IVec3::NEG_X, BlockKind::Port));
1135 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XMINUS));
1136
1137 let err = validate(&graph).unwrap_err();
1138
1139 assert!(matches!(
1140 err,
1141 BlockGraphError::Invalid(InvalidBlockGraphError::WalkingPipeNotTemporal {
1142 pos,
1143 endpoint
1144 }) if pos == IVec3::ZERO && endpoint == IVec3::ZERO
1145 ));
1146 }
1147
1148 #[test]
1149 fn walking_participates_in_lattice_surgery_orientation_checks() {
1150 let mut graph = BlockGraph::new();
1151 graph.add_block(Block::new(
1152 IVec3::ZERO,
1153 BlockKind::Walking(WalkingKind::new(WalkingBoundaryKind::XZZ, IVec2::X).unwrap()),
1154 ));
1155 graph.add_block(Block::new(
1156 IVec3::new(1, 0, 2),
1157 BlockKind::Cube(CubeKind::ZXX),
1158 ));
1159 graph.add_pipe(Pipe::new(IVec3::new(1, 0, 1), Direction::ZPLUS));
1160
1161 let err = validate(&graph).unwrap_err();
1162
1163 assert!(matches!(
1164 err,
1165 BlockGraphError::Invalid(InvalidBlockGraphError::OrientationContradiction(a, b))
1166 if a == IVec3::new(1, 0, 1) && b == IVec3::new(1, 0, 2)
1167 ));
1168 }
1169
1170 #[test]
1171 fn patch_rotation_requires_temporal_pipes_at_start_and_end() {
1172 let mut graph = BlockGraph::new();
1173 let kind = PatchRotationKind::new(Basis::X, IVec2::X).unwrap();
1174 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
1175
1176 let err = validate(&graph).unwrap_err();
1177
1178 assert!(matches!(
1179 err,
1180 BlockGraphError::Invalid(InvalidBlockGraphError::PatchRotationEndpointPipeCount {
1181 pos,
1182 endpoint,
1183 count: 0
1184 }) if pos == IVec3::ZERO && endpoint == IVec3::ZERO
1185 ));
1186 }
1187
1188 #[test]
1189 fn patch_rotation_allows_hadamard_only_at_end() {
1190 let mut graph = BlockGraph::new();
1191 let kind = PatchRotationKind::new(Basis::X, IVec2::X).unwrap();
1192 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
1193 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Port));
1194 graph.add_block(Block::new(IVec3::new(1, 0, 2), BlockKind::Port));
1195 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZMINUS));
1196 graph.add_pipe(Pipe::new(kind.end_position(IVec3::ZERO), Direction::ZPLUS).with_hadamard());
1197
1198 validate(&graph).unwrap();
1199
1200 graph
1201 .set_pipe_hadamard(IVec3::ZERO, IVec3::NEG_Z, true)
1202 .unwrap();
1203 assert!(matches!(
1204 validate(&graph),
1205 Err(BlockGraphError::Invalid(
1206 InvalidBlockGraphError::PatchRotationStartPipeHadamard { pos }
1207 )) if pos == IVec3::ZERO
1208 ));
1209 }
1210
1211 #[test]
1212 fn patch_rotation_rejects_spatial_pipe_at_endpoint() {
1213 let mut graph = BlockGraph::new();
1214 let kind = PatchRotationKind::new(Basis::X, IVec2::X).unwrap();
1215 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
1216 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Port));
1217 graph.add_block(Block::new(IVec3::new(1, 0, 2), BlockKind::Port));
1218 graph.add_block(Block::new(IVec3::NEG_X, BlockKind::Port));
1219 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZMINUS));
1220 graph.add_pipe(Pipe::new(kind.end_position(IVec3::ZERO), Direction::ZPLUS));
1221 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XMINUS));
1222
1223 let err = validate(&graph).unwrap_err();
1224
1225 assert!(matches!(
1226 err,
1227 BlockGraphError::Invalid(InvalidBlockGraphError::PatchRotationEndpointPipeCount {
1228 pos,
1229 endpoint,
1230 count: 2
1231 }) if pos == IVec3::ZERO && endpoint == IVec3::ZERO
1232 ));
1233 }
1234
1235 #[test]
1236 fn patch_rotation_rejects_mismatched_start_boundary_orientation() {
1237 let mut graph = BlockGraph::new();
1238 let kind = PatchRotationKind::new(Basis::X, IVec2::X).unwrap();
1239 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
1240 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Cube(CubeKind::XXZ)));
1241 graph.add_block(Block::new(IVec3::new(1, 0, 2), BlockKind::Port));
1242 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZMINUS));
1243 graph.add_pipe(Pipe::new(kind.end_position(IVec3::ZERO), Direction::ZPLUS));
1244
1245 let err = validate(&graph).unwrap_err();
1246
1247 assert!(matches!(
1248 err,
1249 BlockGraphError::Invalid(InvalidBlockGraphError::OrientationContradiction(a, b))
1250 if a == IVec3::ZERO && b == IVec3::NEG_Z
1251 ));
1252 }
1253
1254 #[test]
1255 fn spatially_connected_cubes_must_have_matching_height() {
1256 let mut graph = BlockGraph::new();
1257 graph.add_block(
1258 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
1259 .with_height("2d".parse().expect("valid height"))
1260 .unwrap(),
1261 );
1262 graph.add_block(
1263 Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ))
1264 .with_height("3d".parse().expect("valid height"))
1265 .unwrap(),
1266 );
1267 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
1268
1269 assert!(matches!(
1270 graph.validate_structure(),
1271 Err(BlockGraphError::Invalid(
1272 InvalidBlockGraphError::CubeHeightSpatialMismatch { .. }
1273 ))
1274 ));
1275 }
1276
1277 #[test]
1278 fn multi_cell_cube_spatial_pipe_must_attach_at_anchor() {
1279 let mut graph = BlockGraph::new();
1280 graph.add_block(
1281 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
1282 .with_height("2d".parse().expect("valid height"))
1283 .unwrap(),
1284 );
1285 graph.add_block(
1286 Block::new(IVec3::new(1, 0, 1), BlockKind::Cube(CubeKind::ZXZ))
1287 .with_height("2d".parse().expect("valid height"))
1288 .unwrap(),
1289 );
1290 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 1), Direction::XPLUS));
1291
1292 assert!(matches!(
1293 graph.validate_structure(),
1294 Err(BlockGraphError::Invalid(
1295 InvalidBlockGraphError::CubeInvalidPipeEndpoint { .. }
1296 ))
1297 ));
1298 }
1299
1300 #[test]
1301 fn multi_cell_cube_allows_bottom_past_and_top_future_temporal_pipes() {
1302 let mut graph = BlockGraph::new();
1303 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Cube(CubeKind::ZXZ)));
1304 graph.add_block(
1305 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
1306 .with_height("3d".parse().expect("valid height"))
1307 .unwrap(),
1308 );
1309 graph.add_block(Block::new(
1310 IVec3::new(0, 0, 3),
1311 BlockKind::Cube(CubeKind::ZXZ),
1312 ));
1313 graph.add_pipe(Pipe::new(IVec3::NEG_Z, Direction::ZPLUS));
1314 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 2), Direction::ZPLUS));
1315
1316 graph
1317 .validate_structure()
1318 .expect("scaled cube temporal endpoints should validate");
1319 }
1320
1321 #[test]
1322 fn resolve_alias_defined_later_than_raw_measurement_is_accepted() {
1323 let mut g = BlockGraph::new();
1324 g.add_block(Block::new(
1325 IVec3::new(0, 0, 2),
1326 BlockKind::Selective(SelectiveKind::XY),
1327 ));
1328 g.add_block(Block::new(
1329 IVec3::new(0, 0, 1),
1330 BlockKind::Cube(CubeKind::ZXZ),
1331 ));
1332 g.add_block(Block::new(
1333 IVec3::new(0, 0, 0),
1334 BlockKind::Cube(CubeKind::ZXZ),
1335 ));
1336 g.add_pipe(Pipe::new(IVec3::new(0, 0, 2), Direction::ZMINUS));
1337 g.set_actions_unchecked(vec![
1338 measurement("m"),
1339 Action::Let {
1340 name: "alias".to_string(),
1341 expr: Expr::Var("m".to_string()),
1342 },
1343 Action::Resolve {
1344 target: IVec3::new(0, 0, 2),
1345 condition: Expr::Var("alias".to_string()),
1346 },
1347 ]);
1348
1349 validate(&g).unwrap();
1350 }
1351
1352 #[test]
1353 fn empty_feedback_is_rejected_atomically_by_every_source_install() {
1354 for install in [
1355 BlockGraph::set_actions,
1356 BlockGraph::set_actions_deferred,
1357 BlockGraph::set_actions_lenient,
1358 ] {
1359 let mut graph =
1360 BlockGraph::from_blog_text("BLOG 1.0\n0: ZXZ [0,0,0]\nm = measure 0\n").unwrap();
1361 let previous = graph.actions();
1362 let error = install(
1363 &mut graph,
1364 vec![Action::Feedback {
1365 targets: vec![],
1366 condition: None,
1367 }],
1368 )
1369 .unwrap_err();
1370 assert!(matches!(
1371 error,
1372 BlockGraphError::InvalidAction(InvalidActionError::EmptyFeedbackTargets)
1373 ));
1374 assert_eq!(graph.actions(), previous);
1375 }
1376 }
1377
1378 #[test]
1379 fn feedback_accepts_a_future_measurement_binding() {
1380 let mut g = BlockGraph::new();
1381 g.add_block(Block::new(
1382 IVec3::new(0, 0, 0),
1383 BlockKind::Cube(CubeKind::ZXZ),
1384 ));
1385 g.add_block(Block::new(IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
1386 g.add_pipe(crate::Pipe::new(IVec3::ZERO, crate::Direction::ZPLUS));
1387 g.set_actions_unchecked(vec![
1388 Action::Feedback {
1389 targets: vec![FeedbackTarget {
1390 pauli: PauliBasis::Z,
1391 target: IVec3::ZERO,
1392 direction: None,
1393 }],
1394 condition: Some(Expr::Var("m".to_string())),
1395 },
1396 measurement("m"),
1397 ]);
1398
1399 validate(&g).unwrap();
1400 assert_eq!(
1401 BlockGraph::from_blog_text(&g.to_blog_text())
1402 .unwrap()
1403 .actions(),
1404 g.actions()
1405 );
1406 g.set_actions(vec![Action::Feedback {
1407 targets: vec![FeedbackTarget {
1408 pauli: PauliBasis::Z,
1409 target: IVec3::ZERO,
1410 direction: None,
1411 }],
1412 condition: None,
1413 }])
1414 .unwrap();
1415 assert_eq!(
1416 BlockGraph::from_blog_text(&g.to_blog_text())
1417 .unwrap()
1418 .actions(),
1419 g.actions()
1420 );
1421 }
1422
1423 #[test]
1424 fn discard_if_accepts_a_measurement_binding() {
1425 let mut g = BlockGraph::new();
1426 g.add_block(Block::new(
1427 IVec3::new(0, 0, 0),
1428 BlockKind::Cube(CubeKind::ZXZ),
1429 ));
1430 g.add_action(measurement("m")).unwrap();
1431 g.add_action(Action::DiscardIf(Expr::Var("m".to_string())))
1432 .unwrap();
1433
1434 validate(&g).unwrap();
1435 }
1436
1437 #[test]
1438 fn feedback_rejects_undefined_variables() {
1439 let mut g = BlockGraph::new();
1440 g.add_block(Block::new(
1441 IVec3::new(0, 0, 0),
1442 BlockKind::Cube(CubeKind::ZXZ),
1443 ));
1444 g.set_actions_unchecked(vec![Action::Feedback {
1445 targets: vec![FeedbackTarget {
1446 pauli: PauliBasis::Z,
1447 target: IVec3::ZERO,
1448 direction: None,
1449 }],
1450 condition: Some(Expr::Var("missing".to_string())),
1451 }]);
1452 let err = validate(&g).unwrap_err();
1453 assert!(
1454 matches!(
1455 err,
1456 BlockGraphError::InvalidAction(InvalidActionError::UndefinedVariable(_))
1457 ),
1458 "expected UndefinedVariable, got: {err:?}",
1459 );
1460 }
1461
1462 #[test]
1463 fn duplicate_measure_pipe_from_both_directions_is_rejected() {
1464 let mut g = BlockGraph::new();
1465 g.add_block(Block::new(
1466 IVec3::new(0, 0, 0),
1467 BlockKind::Cube(CubeKind::ZXZ),
1468 ));
1469 g.add_block(Block::new(
1470 IVec3::new(1, 0, 0),
1471 BlockKind::Cube(CubeKind::ZXZ),
1472 ));
1473 g.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::XPLUS));
1474 g.set_actions_unchecked(vec![
1475 Action::Measure {
1476 target: MeasureTarget::Edge {
1477 src: IVec3::new(0, 0, 0),
1478 dir: Direction::XPLUS,
1479 },
1480 name: "m0".into(),
1481 },
1482 Action::Measure {
1483 target: MeasureTarget::Edge {
1484 src: IVec3::new(1, 0, 0),
1485 dir: Direction::XMINUS,
1486 },
1487 name: "m1".into(),
1488 },
1489 ]);
1490
1491 let err = validate(&g).unwrap_err();
1492 assert!(
1493 matches!(
1494 err,
1495 BlockGraphError::InvalidAction(InvalidActionError::DuplicateMeasurementTarget(..))
1496 ),
1497 "expected DuplicateMeasurementTarget, got: {err:?}",
1498 );
1499 }
1500
1501 #[test]
1502 fn terminal_measurements_with_future_pipes_are_rejected() {
1503 for kind in [
1504 BlockKind::Measurement(Basis::X),
1505 BlockKind::Selective(SelectiveKind::XY),
1506 ] {
1507 let mut graph = BlockGraph::new();
1508 graph.add_block(Block::new(IVec3::ZERO, kind));
1509 graph.add_block(Block::new(IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
1510 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
1511
1512 let err = validate(&graph).unwrap_err();
1513 assert!(matches!(
1514 err,
1515 BlockGraphError::Invalid(InvalidBlockGraphError::FuturePipeNotAllowed(pos))
1516 if pos == IVec3::ZERO
1517 ));
1518 }
1519 }
1520
1521 #[test]
1522 fn fixed_measurement_accepts_one_past_pipe() {
1523 let mut graph = BlockGraph::new();
1524 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Cube(CubeKind::ZXZ)));
1525 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Measurement(Basis::X)));
1526 graph.add_pipe(Pipe::new(IVec3::NEG_Z, Direction::ZPLUS));
1527
1528 validate(&graph).unwrap();
1529 }
1530
1531 #[test]
1532 fn feedback_rejects_missing_targets() {
1533 let mut g = BlockGraph::new();
1534 g.add_block(Block::new(
1535 IVec3::new(0, 0, 0),
1536 BlockKind::Cube(CubeKind::ZXZ),
1537 ));
1538 g.set_actions_unchecked(vec![Action::Feedback {
1539 targets: vec![crate::FeedbackTarget {
1540 pauli: crate::PauliBasis::Z,
1541 target: IVec3::new(1, 0, 0),
1542 direction: None,
1543 }],
1544 condition: None,
1545 }]);
1546
1547 let err = validate(&g).unwrap_err();
1548 assert!(
1549 matches!(
1550 err,
1551 BlockGraphError::InvalidAction(InvalidActionError::InvalidFeedbackTarget(..))
1552 ),
1553 "expected InvalidFeedbackTarget, got: {err:?}",
1554 );
1555 }
1556
1557 #[test]
1558 fn feedback_accepts_output_port_targets() {
1559 let mut g = BlockGraph::new();
1560 g.add_block(Block::new(
1561 IVec3::new(0, 0, 0),
1562 BlockKind::Cube(CubeKind::ZXZ),
1563 ));
1564 g.add_block(Block::new(IVec3::new(0, 0, 1), BlockKind::Port));
1565 g.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS));
1566 g.add_action(Action::Feedback {
1567 targets: vec![crate::FeedbackTarget {
1568 pauli: crate::PauliBasis::Z,
1569 target: IVec3::new(0, 0, 1),
1570 direction: None,
1571 }],
1572 condition: None,
1573 })
1574 .unwrap();
1575
1576 validate(&g).unwrap();
1577 }
1578
1579 #[test]
1580 fn feedback_accepts_existing_non_output_targets() {
1581 let mut g = BlockGraph::new();
1582 g.add_block(Block::new(
1583 IVec3::new(0, 0, 0),
1584 BlockKind::Cube(CubeKind::ZXZ),
1585 ));
1586 g.add_block(Block::new(IVec3::new(0, 0, 1), BlockKind::Port));
1587 g.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS));
1588 g.set_actions_unchecked(vec![Action::Feedback {
1589 targets: vec![crate::FeedbackTarget {
1590 pauli: crate::PauliBasis::Z,
1591 target: IVec3::new(0, 0, 0),
1592 direction: None,
1593 }],
1594 condition: None,
1595 }]);
1596
1597 validate(&g).unwrap();
1598 }
1599
1600 #[test]
1601 fn set_actions_rejects_measure_name_outside_identifier_grammar() {
1602 let mut g = BlockGraph::new();
1605 g.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
1606 let err = g
1607 .set_actions(vec![Action::Measure {
1608 target: MeasureTarget::Node(IVec3::ZERO),
1609 name: "bad name".to_string(),
1610 }])
1611 .expect_err("a name with a space cannot round-trip through to_blog_text");
1612 assert!(
1613 matches!(
1614 err,
1615 BlockGraphError::InvalidAction(InvalidActionError::InvalidActionName(_))
1616 ),
1617 "expected InvalidActionName, got: {err:?}",
1618 );
1619 }
1620
1621 #[test]
1622 fn set_actions_rejects_reserved_keyword_name() {
1623 let mut g = BlockGraph::new();
1626 g.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
1627 let err = g
1628 .set_actions(vec![Action::Let {
1629 name: "measure".to_string(),
1630 expr: Expr::Var("m".to_string()),
1631 }])
1632 .expect_err("a reserved keyword name would not reparse");
1633 assert!(
1634 matches!(
1635 err,
1636 BlockGraphError::InvalidAction(InvalidActionError::InvalidActionName(_))
1637 ),
1638 "expected InvalidActionName, got: {err:?}",
1639 );
1640 }
1641
1642 #[test]
1643 fn set_actions_accepts_valid_name_and_round_trips() {
1644 let graph =
1645 crate::parse_blog_to_graph("BLOG 1.0\n\n 0: ZXZ [0, 0, 0]\n\n m0 = measure 0\n")
1646 .expect("valid name parses");
1647 let text = graph.to_blog_text();
1648 let reparsed = crate::parse_blog_to_graph(&text).expect("round-trip reparses");
1649 assert_eq!(reparsed.to_blog_text(), text);
1650 }
1651}