1use std::cmp::Ordering;
4
5use bloq_utils::{Basis, Direction, PauliBasis, PortRole, RGBA, UDirection};
6use glam::{IVec2, IVec3};
7use smallvec::{SmallVec, smallvec};
8use strum::{Display, EnumString};
9
10use crate::{BlockGraphError, CubeHeight};
11
12pub(crate) type Offsets = SmallVec<[IVec3; 2]>;
15
16#[derive(Debug, Clone, thiserror::Error)]
18#[non_exhaustive]
19pub enum BlockError {
20 #[error("invalid bases for CubeKind: {0:?}")]
22 InvalidCubeBases([Basis; 3]),
23 #[error("spatial cube kind {0} is not a valid Walking boundary kind")]
25 SpatialCubeNotWalkingBoundary(CubeKind),
26 #[error(
28 "invalid Walking movement vector {0}; expected one of (+/-1,0), (0,+/-1), or (+/-1,+/-1)"
29 )]
30 InvalidWalkingMovement(IVec2),
31 #[error("invalid PatchRotation movement vector {0}; expected one of (+/-1,0) or (0,+/-1)")]
33 InvalidPatchRotationMovement(IVec2),
34 #[error("invalid BlockKind: {0}")]
36 InvalidBlockKind(String),
37 #[error("cube height can only be set on cube blocks, got {0}")]
39 CubeHeightOnNonCube(BlockKind),
40 #[error("port color can only be set on Port blocks, got {0}")]
42 PortColorOnNonPort(BlockKind),
43 #[error("port role can only be set on Port blocks, got {0}")]
45 PortRoleOnNonPort(BlockKind),
46 #[error("cube height {numerator}d/{denominator} is not positive")]
48 NonPositiveCubeHeight {
49 numerator: u32,
51 denominator: u32,
53 },
54 #[error("cube height occupies {cells} cells, exceeding supported maximum 65535")]
56 CubeHeightExceedsRange {
57 cells: u32,
59 },
60 #[error("cube at z={z} occupying {cells} cells exceeds the graph coordinate range")]
62 CubeHeightCoordinateOverflow {
63 z: i32,
65 cells: u32,
67 },
68 #[error("invalid cube height expression `{0}`; expected `k*d + n`, e.g. `d`, `2d`, `3d/2-1`")]
70 InvalidCubeHeight(String),
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, EnumString, Display)]
76pub enum CubeKind {
77 XZZ,
79 ZXZ,
81 ZZX,
83 ZXX,
85 XZX,
87 XXZ,
89}
90
91impl CubeKind {
92 pub const fn bases(&self) -> [Basis; 3] {
94 match self {
95 CubeKind::XZZ => [Basis::X, Basis::Z, Basis::Z],
96 CubeKind::ZXZ => [Basis::Z, Basis::X, Basis::Z],
97 CubeKind::ZZX => [Basis::Z, Basis::Z, Basis::X],
98 CubeKind::ZXX => [Basis::Z, Basis::X, Basis::X],
99 CubeKind::XZX => [Basis::X, Basis::Z, Basis::X],
100 CubeKind::XXZ => [Basis::X, Basis::X, Basis::Z],
101 }
102 }
103
104 pub const fn normal_direction(&self) -> UDirection {
107 match self {
108 CubeKind::XZZ | CubeKind::ZXX => UDirection::X,
109 CubeKind::ZXZ | CubeKind::XZX => UDirection::Y,
110 CubeKind::ZZX | CubeKind::XXZ => UDirection::Z,
111 }
112 }
113
114 pub const fn normal_basis(&self) -> Basis {
116 self.bases()[self.normal_direction().index()]
117 }
118
119 pub const fn is_x_type(&self) -> bool {
121 matches!(self.normal_basis(), Basis::X)
122 }
123
124 pub const fn x(&self) -> Basis {
126 self.bases()[0]
127 }
128
129 pub const fn y(&self) -> Basis {
131 self.bases()[1]
132 }
133
134 pub const fn z(&self) -> Basis {
136 self.bases()[2]
137 }
138
139 pub const fn is_spatial(&self) -> bool {
142 matches!(self, CubeKind::ZZX | CubeKind::XXZ)
143 }
144}
145
146impl TryFrom<[Basis; 3]> for CubeKind {
147 type Error = BlockError;
148
149 fn try_from(bases: [Basis; 3]) -> Result<Self, Self::Error> {
150 match bases {
151 [Basis::X, Basis::Z, Basis::Z] => Ok(CubeKind::XZZ),
152 [Basis::Z, Basis::X, Basis::Z] => Ok(CubeKind::ZXZ),
153 [Basis::Z, Basis::Z, Basis::X] => Ok(CubeKind::ZZX),
154 [Basis::Z, Basis::X, Basis::X] => Ok(CubeKind::ZXX),
155 [Basis::X, Basis::Z, Basis::X] => Ok(CubeKind::XZX),
156 [Basis::X, Basis::X, Basis::Z] => Ok(CubeKind::XXZ),
157 _ => Err(BlockError::InvalidCubeBases(bases)),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, EnumString, Display)]
165pub enum WalkingBoundaryKind {
166 XZZ,
168 ZXZ,
170 ZXX,
172 XZX,
174}
175
176impl WalkingBoundaryKind {
177 pub const ALL: [Self; 4] = [Self::ZXZ, Self::ZXX, Self::XZZ, Self::XZX];
179
180 pub fn bases(&self) -> [Basis; 3] {
185 CubeKind::from(*self).bases()
186 }
187
188 pub fn x(&self) -> Basis {
190 self.bases()[0]
191 }
192
193 pub fn y(&self) -> Basis {
195 self.bases()[1]
196 }
197
198 pub fn z(&self) -> Basis {
200 self.bases()[2]
201 }
202
203 pub const fn flip_xz_basis(&self) -> Self {
205 match self {
206 WalkingBoundaryKind::XZZ => WalkingBoundaryKind::ZXX,
207 WalkingBoundaryKind::ZXZ => WalkingBoundaryKind::XZX,
208 WalkingBoundaryKind::ZXX => WalkingBoundaryKind::XZZ,
209 WalkingBoundaryKind::XZX => WalkingBoundaryKind::ZXZ,
210 }
211 }
212}
213
214impl TryFrom<CubeKind> for WalkingBoundaryKind {
215 type Error = BlockError;
216
217 fn try_from(kind: CubeKind) -> Result<Self, Self::Error> {
218 match kind {
219 CubeKind::XZZ => Ok(Self::XZZ),
220 CubeKind::ZXZ => Ok(Self::ZXZ),
221 CubeKind::ZXX => Ok(Self::ZXX),
222 CubeKind::XZX => Ok(Self::XZX),
223 CubeKind::ZZX | CubeKind::XXZ => Err(BlockError::SpatialCubeNotWalkingBoundary(kind)),
224 }
225 }
226}
227
228impl From<WalkingBoundaryKind> for CubeKind {
229 fn from(kind: WalkingBoundaryKind) -> Self {
230 match kind {
231 WalkingBoundaryKind::XZZ => CubeKind::XZZ,
232 WalkingBoundaryKind::ZXZ => CubeKind::ZXZ,
233 WalkingBoundaryKind::ZXX => CubeKind::ZXX,
234 WalkingBoundaryKind::XZX => CubeKind::XZX,
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242pub struct WalkingKind {
243 boundary: WalkingBoundaryKind,
244 movement: IVec2,
245}
246
247impl WalkingKind {
248 pub const DEFAULT: Self = Self {
250 boundary: WalkingBoundaryKind::ZXZ,
251 movement: IVec2::new(1, 0),
252 };
253
254 pub fn new(boundary: WalkingBoundaryKind, movement: IVec2) -> Result<Self, BlockError> {
261 if !Self::is_valid_movement(movement) {
262 return Err(BlockError::InvalidWalkingMovement(movement));
263 }
264 Ok(Self { boundary, movement })
265 }
266
267 pub const fn boundary(self) -> WalkingBoundaryKind {
269 self.boundary
270 }
271
272 pub const fn movement(self) -> IVec2 {
274 self.movement
275 }
276
277 pub const fn movement_3d(self) -> IVec3 {
279 movement_3d(self.movement)
280 }
281
282 pub const fn with_boundary(self, boundary: WalkingBoundaryKind) -> Self {
284 Self {
285 boundary,
286 movement: self.movement,
287 }
288 }
289
290 pub fn end_position(self, start: IVec3) -> IVec3 {
297 self.try_end_position(start)
298 .expect("walking endpoint must fit in the i32 coordinate range")
299 }
300
301 pub fn try_end_position(self, start: IVec3) -> Result<IVec3, BlockGraphError> {
308 crate::checked_add_position(start, self.movement_3d())
309 }
310
311 pub fn connectable_offsets(self) -> [IVec3; 2] {
313 movement_connectable_offsets(self.movement)
314 }
315
316 pub fn reserved_offsets(self) -> Vec<IVec3> {
318 movement_footprint(self.movement)
319 }
320
321 const fn is_valid_movement(movement: IVec2) -> bool {
322 let x_ok = movement.x == -1 || movement.x == 0 || movement.x == 1;
323 let y_ok = movement.y == -1 || movement.y == 0 || movement.y == 1;
324 let nonzero = movement.x != 0 || movement.y != 0;
325 x_ok && y_ok && nonzero
326 }
327}
328
329fn cmp_movement(left: IVec2, right: IVec2) -> Ordering {
332 left.x.cmp(&right.x).then_with(|| left.y.cmp(&right.y))
333}
334
335const fn movement_3d(movement: IVec2) -> IVec3 {
338 IVec3::new(movement.x, movement.y, 1)
339}
340
341const fn movement_connectable_offsets(movement: IVec2) -> [IVec3; 2] {
343 [IVec3::ZERO, movement_3d(movement)]
344}
345
346fn movement_footprint(movement: IVec2) -> Vec<IVec3> {
349 let x_min = 0.min(movement.x);
350 let x_max = 0.max(movement.x);
351 let y_min = 0.min(movement.y);
352 let y_max = 0.max(movement.y);
353 let mut offsets = Vec::new();
354 for x in x_min..=x_max {
355 for y in y_min..=y_max {
356 for z in 0..=1 {
357 offsets.push(IVec3::new(x, y, z));
358 }
359 }
360 }
361 offsets
362}
363
364impl Default for WalkingKind {
365 fn default() -> Self {
366 Self::DEFAULT
367 }
368}
369
370impl PartialOrd for WalkingKind {
371 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
372 Some(self.cmp(other))
373 }
374}
375
376impl Ord for WalkingKind {
377 fn cmp(&self, other: &Self) -> Ordering {
378 self.boundary
379 .cmp(&other.boundary)
380 .then_with(|| cmp_movement(self.movement, other.movement))
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
387pub struct PatchRotationKind {
388 basis: Basis,
389 movement: IVec2,
390}
391
392impl PatchRotationKind {
393 pub const DEFAULT: Self = Self {
395 basis: Basis::X,
396 movement: IVec2::new(1, 0),
397 };
398
399 pub fn new(basis: Basis, movement: IVec2) -> Result<Self, BlockError> {
406 if !Self::is_valid_movement(movement) {
407 return Err(BlockError::InvalidPatchRotationMovement(movement));
408 }
409 Ok(Self { basis, movement })
410 }
411
412 pub const fn basis(self) -> Basis {
414 self.basis
415 }
416
417 pub const fn x_axis_boundary_basis(self) -> Basis {
420 if self.movement.x != 0 {
421 self.basis.flip()
422 } else {
423 self.basis
424 }
425 }
426
427 pub const fn movement(self) -> IVec2 {
429 self.movement
430 }
431
432 pub const fn movement_3d(self) -> IVec3 {
434 movement_3d(self.movement)
435 }
436
437 pub const fn with_basis(self, basis: Basis) -> Self {
439 Self {
440 basis,
441 movement: self.movement,
442 }
443 }
444
445 pub fn end_position(self, start: IVec3) -> IVec3 {
452 self.try_end_position(start)
453 .expect("patch-rotation endpoint must fit in the i32 coordinate range")
454 }
455
456 pub fn try_end_position(self, start: IVec3) -> Result<IVec3, BlockGraphError> {
463 crate::checked_add_position(start, self.movement_3d())
464 }
465
466 pub fn connectable_offsets(self) -> [IVec3; 2] {
468 movement_connectable_offsets(self.movement)
469 }
470
471 pub fn reserved_offsets(self) -> Vec<IVec3> {
473 movement_footprint(self.movement)
474 }
475
476 pub fn bases_at_endpoint(self, start: IVec3, endpoint: IVec3) -> Option<[Basis; 3]> {
479 let basis = self.x_axis_boundary_basis();
480 let x_basis = if endpoint == start {
481 basis
482 } else if self
483 .try_end_position(start)
484 .is_ok_and(|end| endpoint == end)
485 {
486 basis.flip()
487 } else {
488 return None;
489 };
490 Some([x_basis, x_basis.flip(), x_basis])
491 }
492
493 pub fn pipe_face_bases_at_endpoint(self, start: IVec3, endpoint: IVec3) -> Option<[Basis; 3]> {
500 if endpoint == start {
501 Some([self.basis, self.basis.flip(), self.basis])
502 } else if self
503 .try_end_position(start)
504 .is_ok_and(|end| endpoint == end)
505 {
506 let basis = self.basis.flip();
507 Some([basis, basis.flip(), basis])
508 } else {
509 None
510 }
511 }
512
513 const fn is_valid_movement(movement: IVec2) -> bool {
514 let is_x_step = (movement.x == -1 || movement.x == 1) && movement.y == 0;
515 let is_y_step = movement.x == 0 && (movement.y == -1 || movement.y == 1);
516 is_x_step || is_y_step
517 }
518}
519
520impl Default for PatchRotationKind {
521 fn default() -> Self {
522 Self::DEFAULT
523 }
524}
525
526impl PartialOrd for PatchRotationKind {
527 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
528 Some(self.cmp(other))
529 }
530}
531
532impl Ord for PatchRotationKind {
533 fn cmp(&self, other: &Self) -> Ordering {
534 self.basis
535 .cmp(&other.basis)
536 .then_with(|| cmp_movement(self.movement, other.movement))
537 }
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, EnumString)]
543#[strum(
544 ascii_case_insensitive,
545 parse_err_ty = String,
546 parse_err_fn = invalid_selective_kind
547)]
548pub enum SelectiveKind {
549 XY,
551 XZ,
553 YZ,
555}
556
557impl SelectiveKind {
558 pub const fn pauli_if_true(&self) -> PauliBasis {
560 match self {
561 SelectiveKind::XY | SelectiveKind::XZ => PauliBasis::X,
562 SelectiveKind::YZ => PauliBasis::Y,
563 }
564 }
565
566 pub const fn pauli_if_false(&self) -> PauliBasis {
568 match self {
569 SelectiveKind::XY => PauliBasis::Y,
570 SelectiveKind::XZ | SelectiveKind::YZ => PauliBasis::Z,
571 }
572 }
573
574 pub const fn flip_xz_basis(&self) -> Self {
576 match self {
577 SelectiveKind::XY => SelectiveKind::YZ,
578 SelectiveKind::XZ => SelectiveKind::XZ,
579 SelectiveKind::YZ => SelectiveKind::XY,
580 }
581 }
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, Default)]
587pub enum BlockKind {
588 #[strum(to_string = "{0}")]
590 Cube(CubeKind),
591 #[strum(to_string = "Walking")]
593 Walking(WalkingKind),
594 #[strum(to_string = "PatchRotation")]
596 PatchRotation(PatchRotationKind),
597 Y,
599 #[strum(to_string = "{0}")]
601 Measurement(Basis),
602 #[default]
604 Port,
605 T,
607 #[strum(to_string = "{0}")]
609 Selective(SelectiveKind),
610}
611
612impl BlockKind {
613 pub const fn is_cube(&self) -> bool {
615 matches!(self, BlockKind::Cube(_))
616 }
617
618 pub const fn is_walking(&self) -> bool {
620 matches!(self, BlockKind::Walking(_))
621 }
622
623 pub const fn is_patch_rotation(&self) -> bool {
625 matches!(self, BlockKind::PatchRotation(_))
626 }
627
628 pub const fn is_port(&self) -> bool {
630 matches!(self, BlockKind::Port)
631 }
632
633 pub const fn is_y(&self) -> bool {
635 matches!(self, BlockKind::Y)
636 }
637
638 pub const fn is_measurement(&self) -> bool {
640 matches!(self, BlockKind::Measurement(_))
641 }
642
643 pub const fn is_t(&self) -> bool {
645 matches!(self, BlockKind::T)
646 }
647
648 pub const fn is_selective(&self) -> bool {
650 matches!(self, BlockKind::Selective(_))
651 }
652
653 pub const fn is_dynamic(&self) -> bool {
656 matches!(self, BlockKind::Selective(_) | BlockKind::T)
657 }
658
659 pub fn flip_xz_basis(self) -> Self {
665 match self {
666 BlockKind::Cube(kind) => {
667 let bases = kind.bases().map(Basis::flip);
668 BlockKind::Cube(
669 CubeKind::try_from(bases)
670 .expect("flipping X/Z bases preserves cube kind validity"),
671 )
672 }
673 BlockKind::Walking(kind) => {
674 BlockKind::Walking(kind.with_boundary(kind.boundary().flip_xz_basis()))
675 }
676 BlockKind::PatchRotation(kind) => {
677 BlockKind::PatchRotation(kind.with_basis(kind.basis().flip()))
678 }
679 BlockKind::Measurement(basis) => BlockKind::Measurement(basis.flip()),
680 BlockKind::Selective(kind) => BlockKind::Selective(kind.flip_xz_basis()),
681 BlockKind::Y | BlockKind::Port | BlockKind::T => self,
682 }
683 }
684
685 pub const fn is_clifford(&self) -> bool {
687 !self.is_t()
688 }
689
690 pub const fn all_kinds() -> [BlockKind; 16] {
693 [
694 BlockKind::Cube(CubeKind::XZZ),
695 BlockKind::Cube(CubeKind::ZXZ),
696 BlockKind::Cube(CubeKind::ZZX),
697 BlockKind::Cube(CubeKind::ZXX),
698 BlockKind::Cube(CubeKind::XZX),
699 BlockKind::Cube(CubeKind::XXZ),
700 BlockKind::Y,
701 BlockKind::Measurement(Basis::X),
702 BlockKind::Measurement(Basis::Z),
703 BlockKind::T,
704 BlockKind::Port,
705 BlockKind::Walking(WalkingKind::DEFAULT),
706 BlockKind::PatchRotation(PatchRotationKind::DEFAULT),
707 BlockKind::Selective(SelectiveKind::XY),
708 BlockKind::Selective(SelectiveKind::XZ),
709 BlockKind::Selective(SelectiveKind::YZ),
710 ]
711 }
712
713 pub fn reserved_offsets(self) -> Offsets {
718 match self {
719 BlockKind::Walking(kind) => kind.reserved_offsets().into(),
720 BlockKind::PatchRotation(kind) => kind.reserved_offsets().into(),
721 _ => smallvec![IVec3::ZERO],
722 }
723 }
724
725 pub fn allows_reserved_overlap(
730 self,
731 self_pos: IVec3,
732 other: BlockKind,
733 other_pos: IVec3,
734 overlap_pos: IVec3,
735 ) -> bool {
736 match (self, other) {
737 (BlockKind::Walking(kind), BlockKind::Walking(other_kind)) => {
738 walking_walking_overlap_allowed(kind, self_pos, other_kind, other_pos, overlap_pos)
739 }
740 (BlockKind::Walking(kind), other) => {
741 walking_other_overlap_allowed(kind, self_pos, other, other_pos, overlap_pos)
742 }
743 (other, BlockKind::Walking(kind)) => {
744 walking_other_overlap_allowed(kind, other_pos, other, self_pos, overlap_pos)
745 }
746 _ => false,
747 }
748 }
749
750 pub fn connectable_offsets(self) -> Offsets {
753 match self {
754 BlockKind::Walking(kind) => kind.connectable_offsets().into(),
755 BlockKind::PatchRotation(kind) => kind.connectable_offsets().into(),
756 _ => smallvec![IVec3::ZERO],
757 }
758 }
759
760 pub fn bases(self) -> Option<[Basis; 3]> {
763 match self {
764 BlockKind::Cube(kind) => Some(kind.bases()),
765 BlockKind::Walking(kind) => Some(kind.boundary().bases()),
766 BlockKind::PatchRotation(kind) => kind.bases_at_endpoint(IVec3::ZERO, IVec3::ZERO),
767 BlockKind::Y
768 | BlockKind::Measurement(_)
769 | BlockKind::Port
770 | BlockKind::T
771 | BlockKind::Selective(_) => None,
772 }
773 }
774
775 pub fn bases_at_endpoint(self, block_pos: IVec3, endpoint: IVec3) -> Option<[Basis; 3]> {
778 match self {
779 BlockKind::Cube(kind) => (endpoint == block_pos).then_some(kind.bases()),
780 BlockKind::Walking(kind) => kind
781 .connectable_offsets()
782 .into_iter()
783 .filter_map(|offset| crate::checked_add_position(block_pos, offset).ok())
784 .any(|position| position == endpoint)
785 .then_some(kind.boundary().bases()),
786 BlockKind::PatchRotation(kind) => kind.bases_at_endpoint(block_pos, endpoint),
787 BlockKind::Y
788 | BlockKind::Measurement(_)
789 | BlockKind::Port
790 | BlockKind::T
791 | BlockKind::Selective(_) => None,
792 }
793 }
794
795 pub fn pipe_face_bases_at_endpoint(
800 self,
801 block_pos: IVec3,
802 endpoint: IVec3,
803 ) -> Option<[Basis; 3]> {
804 match self {
805 BlockKind::PatchRotation(kind) => kind.pipe_face_bases_at_endpoint(block_pos, endpoint),
806 kind => kind.bases_at_endpoint(block_pos, endpoint),
807 }
808 }
809}
810
811fn walking_other_overlap_allowed(
812 kind: WalkingKind,
813 walking_pos: IVec3,
814 other: BlockKind,
815 other_pos: IVec3,
816 overlap_pos: IVec3,
817) -> bool {
818 let Ok(walking_end) = kind.try_end_position(walking_pos) else {
819 return false;
820 };
821 if overlap_pos == walking_pos || overlap_pos == walking_end {
822 return false;
823 }
824 match other {
825 BlockKind::Port => other_pos.z == walking_pos.z,
826 _ => false,
827 }
828}
829
830fn walking_walking_overlap_allowed(
831 kind: WalkingKind,
832 pos: IVec3,
833 other_kind: WalkingKind,
834 other_pos: IVec3,
835 overlap_pos: IVec3,
836) -> bool {
837 if kind.movement() != other_kind.movement() || pos.z != other_pos.z {
838 return false;
839 }
840 let (Ok(end), Ok(other_end)) = (
841 kind.try_end_position(pos),
842 other_kind.try_end_position(other_pos),
843 ) else {
844 return false;
845 };
846 let hard_endpoint = overlap_pos == pos || overlap_pos == end;
847 let other_hard_endpoint = overlap_pos == other_pos || overlap_pos == other_end;
848 !(hard_endpoint && other_hard_endpoint)
849}
850
851impl std::str::FromStr for BlockKind {
852 type Err = BlockError;
853
854 fn from_str(s: &str) -> Result<Self, Self::Err> {
855 let s = s.to_uppercase();
856 if let Ok(cube_kind) = s.parse::<CubeKind>() {
857 Ok(BlockKind::Cube(cube_kind))
858 } else if s == "Y" {
859 Ok(BlockKind::Y)
860 } else if s == "X" {
861 Ok(BlockKind::Measurement(Basis::X))
862 } else if s == "Z" {
863 Ok(BlockKind::Measurement(Basis::Z))
864 } else if s == "PORT" {
865 Ok(BlockKind::Port)
866 } else if s == "T" {
867 Ok(BlockKind::T)
868 } else if let Ok(selective_kind) = s.parse::<SelectiveKind>() {
869 Ok(BlockKind::Selective(selective_kind))
870 } else {
871 Err(BlockError::InvalidBlockKind(s))
872 }
873 }
874}
875
876fn invalid_selective_kind(value: &str) -> String {
877 format!("Invalid SelectiveKind: {}", value.to_uppercase())
878}
879
880pub fn is_valid_tag(tag: &str) -> bool {
884 !tag.is_empty() && tag.chars().all(is_tag_char)
885}
886
887pub(crate) fn is_tag_char(c: char) -> bool {
888 !c.is_whitespace() && !c.is_control() && !matches!(c, '<' | '>')
889}
890
891#[derive(Debug, Clone, PartialEq, Eq, Hash)]
906pub struct Block {
907 pub(crate) pos: IVec3,
908 pub(crate) kind: BlockKind,
909 pub(crate) height: CubeHeight,
910 pub(crate) port_color: Option<RGBA>,
911 pub(crate) port_role: PortRole,
912 pub(crate) tag: String,
913}
914
915impl std::fmt::Display for Block {
916 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917 match self.kind {
918 BlockKind::Walking(kind) => {
919 write!(f, "walk {} {} ->", kind.boundary(), self.pos)?;
920 match kind.try_end_position(self.pos) {
921 Ok(end) => write!(f, " {end}")?,
922 Err(_) => write!(f, " [endpoint overflows i32]")?,
923 }
924 }
925 BlockKind::PatchRotation(kind) => {
926 write!(f, "rotate {} {} ->", kind.basis(), self.pos)?;
927 match kind.try_end_position(self.pos) {
928 Ok(end) => write!(f, " {end}")?,
929 Err(_) => write!(f, " [endpoint overflows i32]")?,
930 }
931 }
932 kind => {
933 write!(f, "{} {}", kind, self.pos)?;
934 if !self.height.is_default() && kind.is_cube() {
935 write!(f, " height={}", self.height)?;
936 }
937 if let Some(color) = self.port_color {
938 write!(f, " color={:02x}{:02x}{:02x}", color.r, color.g, color.b)?;
939 }
940 if self.port_role != PortRole::Auto {
941 write!(f, " role={}", self.port_role.as_str())?;
942 }
943 }
944 }
945 if !self.tag.is_empty() {
946 write!(f, " <{}>", self.tag)?;
947 }
948 Ok(())
949 }
950}
951
952impl PartialOrd for Block {
953 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
954 Some(self.cmp(other))
955 }
956}
957
958impl Ord for Block {
959 fn cmp(&self, other: &Self) -> Ordering {
960 self.tag
961 .is_empty()
962 .cmp(&other.tag.is_empty())
963 .then_with(|| self.tag.cmp(&other.tag))
964 .then_with(|| self.pos.to_array().cmp(&other.pos.to_array()))
965 .then_with(|| self.kind.cmp(&other.kind))
966 .then_with(|| self.height.cmp(&other.height))
967 .then_with(|| self.port_color.cmp(&other.port_color))
968 .then_with(|| self.port_role.cmp(&other.port_role))
969 }
970}
971
972impl Default for Block {
973 fn default() -> Self {
974 Self::new(IVec3::ZERO, BlockKind::default())
975 }
976}
977
978impl Block {
979 pub fn new(pos: impl Into<IVec3>, kind: impl Into<BlockKind>) -> Self {
981 Self {
982 pos: pos.into(),
983 kind: kind.into(),
984 height: CubeHeight::DEFAULT,
985 port_color: None,
986 port_role: PortRole::Auto,
987 tag: String::new(),
988 }
989 }
990
991 pub fn pos(&self) -> IVec3 {
993 self.pos
994 }
995
996 pub fn kind(&self) -> BlockKind {
998 self.kind
999 }
1000
1001 pub fn height(&self) -> CubeHeight {
1004 if self.kind.is_cube() {
1005 self.height
1006 } else {
1007 CubeHeight::DEFAULT
1008 }
1009 }
1010
1011 pub fn height_cells(&self) -> u32 {
1017 self.height().cells()
1018 }
1019
1020 pub fn with_height(mut self, height: CubeHeight) -> Result<Self, BlockError> {
1027 self.set_height_checked(height)?;
1028 Ok(self)
1029 }
1030
1031 pub fn port_color(&self) -> Option<RGBA> {
1033 self.kind
1034 .is_port()
1035 .then_some(self.port_color.unwrap_or(RGBA::PORT_GRAY))
1036 }
1037
1038 pub fn with_port_color(mut self, rgb: [u8; 3]) -> Result<Self, BlockError> {
1045 self.set_port_color_checked(rgb)?;
1046 Ok(self)
1047 }
1048
1049 pub fn port_role(&self) -> Option<PortRole> {
1051 self.kind.is_port().then_some(self.port_role)
1052 }
1053
1054 pub fn with_port_role(mut self, role: PortRole) -> Result<Self, BlockError> {
1060 self.set_port_role_checked(role)?;
1061 Ok(self)
1062 }
1063
1064 pub fn tag(&self) -> Option<&str> {
1066 (!self.tag.is_empty()).then_some(self.tag.as_str())
1067 }
1068
1069 pub fn with_tag(mut self, tag: impl Into<String>) -> Result<Self, BlockGraphError> {
1077 self.set_tag(tag)?;
1078 Ok(self)
1079 }
1080
1081 pub fn with_shift(&self, offset: impl Into<IVec3>) -> Self {
1088 self.try_with_shift(offset)
1089 .expect("shifted block footprint must fit in the i32 coordinate range")
1090 }
1091
1092 pub fn try_with_shift(&self, offset: impl Into<IVec3>) -> Result<Self, BlockGraphError> {
1099 let pos = crate::checked_add_position(self.pos, offset.into())?;
1100 for relative in self.reserved_offsets() {
1101 crate::checked_add_position(pos, relative)?;
1102 }
1103 let mut shifted = self.clone();
1104 shifted.pos = pos;
1105 Ok(shifted)
1106 }
1107
1108 pub fn reserved_offsets(&self) -> Offsets {
1113 match self.kind {
1114 BlockKind::Cube(_) => (0..self.height_cells_i32())
1115 .map(|z| IVec3::new(0, 0, z))
1116 .collect(),
1117 kind => kind.reserved_offsets(),
1118 }
1119 }
1120
1121 pub(crate) fn checked_reserved_positions(&self) -> Result<Vec<IVec3>, BlockGraphError> {
1122 self.reserved_offsets()
1123 .into_iter()
1124 .map(|offset| crate::checked_add_position(self.pos, offset))
1125 .collect()
1126 }
1127
1128 pub fn connectable_offsets(&self) -> Offsets {
1134 match self.kind {
1135 BlockKind::Cube(_) if self.height_cells() > 1 => {
1136 smallvec![IVec3::ZERO, IVec3::new(0, 0, self.height_cells_i32() - 1)]
1137 }
1138 kind => kind.connectable_offsets(),
1139 }
1140 }
1141
1142 fn reserved_positions(&self) -> impl Iterator<Item = IVec3> + '_ {
1145 self.reserved_offsets()
1146 .into_iter()
1147 .filter_map(|offset| crate::checked_add_position(self.pos, offset).ok())
1148 }
1149
1150 fn connectable_positions(&self) -> impl Iterator<Item = IVec3> + '_ {
1153 self.connectable_offsets()
1154 .into_iter()
1155 .filter_map(|offset| crate::checked_add_position(self.pos, offset).ok())
1156 }
1157
1158 pub(crate) fn occupies_position(&self, position: IVec3) -> bool {
1160 self.reserved_positions()
1161 .any(|occupied| occupied == position)
1162 }
1163
1164 pub fn occupies_layer(&self, z: i32) -> bool {
1166 self.reserved_offsets()
1167 .into_iter()
1168 .filter_map(|offset| self.pos.z.checked_add(offset.z))
1169 .any(|occupied_z| occupied_z == z)
1170 }
1171
1172 pub fn bases_at_endpoint(&self, endpoint: IVec3) -> Option<[Basis; 3]> {
1175 match self.kind {
1176 BlockKind::Cube(kind) => self
1177 .connectable_positions()
1178 .any(|position| position == endpoint)
1179 .then_some(kind.bases()),
1180 kind => kind.bases_at_endpoint(self.pos, endpoint),
1181 }
1182 }
1183
1184 pub fn endpoint_for_direction(&self, dir: Direction) -> IVec3 {
1189 self.connectable_positions()
1190 .find(|endpoint| {
1191 crate::checked_add_position(*endpoint, dir.to_ivec3())
1192 .ok()
1193 .is_none_or(|neighbor| !self.occupies_position(neighbor))
1194 })
1195 .unwrap_or(self.pos)
1196 }
1197
1198 pub(crate) fn set_kind(&mut self, kind: BlockKind) {
1199 self.kind = kind;
1200 if !self.kind.is_cube() {
1201 self.height = CubeHeight::DEFAULT;
1202 }
1203 if !self.kind.is_port() {
1204 self.port_color = None;
1205 self.port_role = PortRole::Auto;
1206 }
1207 }
1208
1209 pub(crate) fn set_tag(&mut self, tag: impl Into<String>) -> Result<(), BlockGraphError> {
1212 let tag = tag.into();
1213 if !tag.is_empty() && !is_valid_tag(&tag) {
1214 return Err(BlockGraphError::InvalidTag { tag });
1215 }
1216 self.tag = tag;
1217 Ok(())
1218 }
1219
1220 fn set_height_checked(&mut self, height: CubeHeight) -> Result<(), BlockError> {
1221 if !self.kind.is_cube() {
1222 return Err(BlockError::CubeHeightOnNonCube(self.kind));
1223 }
1224 let cells = height.cells();
1227 if self.pos.z.checked_add(cells as i32 - 1).is_none() {
1228 return Err(BlockError::CubeHeightCoordinateOverflow {
1229 z: self.pos.z,
1230 cells,
1231 });
1232 }
1233 self.height = height;
1234 Ok(())
1235 }
1236
1237 pub(crate) fn set_port_color_checked(&mut self, rgb: [u8; 3]) -> Result<(), BlockError> {
1238 if !self.kind.is_port() {
1239 return Err(BlockError::PortColorOnNonPort(self.kind));
1240 }
1241 let color = RGBA {
1242 r: rgb[0],
1243 g: rgb[1],
1244 b: rgb[2],
1245 a: RGBA::PORT_GRAY.a,
1246 };
1247 self.port_color = (color != RGBA::PORT_GRAY).then_some(color);
1248 Ok(())
1249 }
1250
1251 pub(crate) fn set_port_role_checked(&mut self, role: PortRole) -> Result<(), BlockError> {
1252 if !self.kind.is_port() {
1253 return Err(BlockError::PortRoleOnNonPort(self.kind));
1254 }
1255 self.port_role = role;
1256 Ok(())
1257 }
1258
1259 fn height_cells_i32(&self) -> i32 {
1260 i32::try_from(self.height_cells()).expect("cube height cells are bounded by 65535")
1261 }
1262}
1263
1264#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
1282pub struct Pipe {
1283 pub(crate) src: IVec3,
1284 pub(crate) dir: Direction,
1285 pub(crate) hadamard: bool,
1286 pub(crate) tag: String,
1287}
1288
1289impl std::fmt::Display for Pipe {
1290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1291 f.write_str(&self.display_with_ids(|_| None))
1292 }
1293}
1294
1295impl Pipe {
1296 pub(crate) fn display_with_ids(&self, p2i: impl Fn(IVec3) -> Option<u32>) -> String {
1297 let source = p2i(self.src)
1298 .map(|id| id.to_string())
1299 .unwrap_or_else(|| self.src.to_string());
1300 let tag = if self.tag.is_empty() {
1301 String::new()
1302 } else {
1303 format!(" <{}>", self.tag)
1304 };
1305 format!(
1306 "{source} {} {}{tag}",
1307 if self.hadamard { "-H>" } else { "->" },
1308 self.dir
1309 )
1310 }
1311
1312 pub fn new(src: impl Into<IVec3>, dir: impl Into<Direction>) -> Self {
1314 Self {
1315 src: src.into(),
1316 dir: dir.into(),
1317 hadamard: false,
1318 tag: String::new(),
1319 }
1320 }
1321
1322 pub fn src(&self) -> IVec3 {
1324 self.src
1325 }
1326
1327 pub fn dir(&self) -> Direction {
1329 self.dir
1330 }
1331
1332 pub fn is_hadamard(&self) -> bool {
1334 self.hadamard
1335 }
1336
1337 pub fn tag(&self) -> Option<&str> {
1339 (!self.tag.is_empty()).then_some(self.tag.as_str())
1340 }
1341
1342 pub fn with_hadamard(mut self) -> Self {
1344 self.hadamard = true;
1345 self
1346 }
1347
1348 pub fn with_tag(mut self, tag: impl Into<String>) -> Result<Self, BlockGraphError> {
1356 self.set_tag(tag)?;
1357 Ok(self)
1358 }
1359
1360 pub fn endpoints(&self) -> (IVec3, IVec3) {
1367 self.try_endpoints()
1368 .expect("pipe destination must fit in the i32 coordinate range")
1369 }
1370
1371 pub fn try_endpoints(&self) -> Result<(IVec3, IVec3), BlockGraphError> {
1378 Ok((self.src, self.try_dst()?))
1379 }
1380
1381 pub fn dst(&self) -> IVec3 {
1388 self.try_dst()
1389 .expect("pipe destination must fit in the i32 coordinate range")
1390 }
1391
1392 pub fn try_dst(&self) -> Result<IVec3, BlockGraphError> {
1399 crate::checked_add_position(self.src, self.dir.to_ivec3())
1400 }
1401
1402 pub fn with_shift(&self, offset: impl Into<IVec3>) -> Self {
1409 self.try_with_shift(offset)
1410 .expect("shifted pipe endpoints must fit in the i32 coordinate range")
1411 }
1412
1413 pub fn try_with_shift(&self, offset: impl Into<IVec3>) -> Result<Self, BlockGraphError> {
1420 let offset = offset.into();
1421 let src = crate::checked_add_position(self.src, offset)?;
1422 crate::checked_add_position(src, self.dir.to_ivec3())?;
1423 Ok(Self {
1424 src,
1425 dir: self.dir,
1426 hadamard: self.hadamard,
1427 tag: self.tag.clone(),
1428 })
1429 }
1430
1431 pub(crate) fn set_tag(&mut self, tag: impl Into<String>) -> Result<(), BlockGraphError> {
1433 let tag = tag.into();
1434 if !tag.is_empty() && !is_valid_tag(&tag) {
1435 return Err(BlockGraphError::InvalidTag { tag });
1436 }
1437 self.tag = tag;
1438 Ok(())
1439 }
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444 use super::*;
1445
1446 #[test]
1447 fn test_block_order_prefers_tagged_blocks_then_tag_then_position() {
1448 let mut blocks = [
1449 Block::new(IVec3::new(2, 0, 0), BlockKind::Port),
1450 Block::new(IVec3::new(1, 0, 0), BlockKind::Port)
1451 .with_tag("b")
1452 .expect("valid tag"),
1453 Block::new(IVec3::new(0, 0, 0), BlockKind::Port)
1454 .with_tag("a")
1455 .expect("valid tag"),
1456 ];
1457
1458 blocks.sort();
1459
1460 assert_eq!(
1461 blocks.iter().map(|block| block.pos).collect::<Vec<_>>(),
1462 vec![
1463 IVec3::new(0, 0, 0),
1464 IVec3::new(1, 0, 0),
1465 IVec3::new(2, 0, 0)
1466 ]
1467 );
1468 }
1469
1470 #[test]
1471 fn test_block_order_breaks_ties_by_kind() {
1472 let mut blocks = [
1473 Block::new(IVec3::ZERO, BlockKind::Y)
1474 .with_tag("tag")
1475 .expect("valid tag"),
1476 Block::new(IVec3::ZERO, BlockKind::Port)
1477 .with_tag("tag")
1478 .expect("valid tag"),
1479 ];
1480
1481 blocks.sort();
1482
1483 assert_eq!(blocks[0].kind, BlockKind::Y);
1484 assert_eq!(blocks[1].kind, BlockKind::Port);
1485 }
1486
1487 #[test]
1488 fn patch_rotation_x_axis_boundary_basis_matches_construction_frame() {
1489 for (basis, movement, expected) in [
1490 (Basis::X, IVec2::NEG_X, Basis::Z),
1491 (Basis::X, IVec2::X, Basis::Z),
1492 (Basis::X, IVec2::NEG_Y, Basis::X),
1493 (Basis::X, IVec2::Y, Basis::X),
1494 (Basis::Z, IVec2::NEG_X, Basis::X),
1495 (Basis::Z, IVec2::X, Basis::X),
1496 (Basis::Z, IVec2::NEG_Y, Basis::Z),
1497 (Basis::Z, IVec2::Y, Basis::Z),
1498 ] {
1499 assert_eq!(
1500 PatchRotationKind::new(basis, movement)
1501 .unwrap()
1502 .x_axis_boundary_basis(),
1503 expected
1504 );
1505 }
1506 }
1507
1508 #[test]
1509 fn test_block_kind_from_str_stays_canonical_for_selectives() {
1510 assert_eq!(
1511 "YZ".parse::<BlockKind>().unwrap(),
1512 BlockKind::Selective(SelectiveKind::YZ)
1513 );
1514 "ZY".parse::<BlockKind>().unwrap_err();
1515 }
1516
1517 #[test]
1518 fn measurement_kind_round_trips_and_flips() {
1519 for (text, basis) in [("X", Basis::X), ("Z", Basis::Z)] {
1520 let kind = BlockKind::Measurement(basis);
1521 assert_eq!(text.parse::<BlockKind>().unwrap(), kind);
1522 assert_eq!(kind.to_string(), text);
1523 assert_eq!(kind.flip_xz_basis(), BlockKind::Measurement(basis.flip()));
1524 }
1525 for old in ["MX", "MZ", "TransversalX", "TransversalZ"] {
1526 old.parse::<BlockKind>().unwrap_err();
1527 }
1528 }
1529
1530 #[test]
1531 fn with_tag_rejects_tags_the_parser_cannot_read_back() {
1532 for bad in ["has space", "angle>bracket", "angle<bracket", "nul\0byte"] {
1533 let block = Block::new(IVec3::ZERO, BlockKind::Port).with_tag(bad);
1534 assert!(
1535 matches!(block, Err(BlockGraphError::InvalidTag { .. })),
1536 "tag {bad:?} should be rejected"
1537 );
1538 let pipe = Pipe::new(IVec3::ZERO, Direction::ZPLUS).with_tag(bad);
1539 assert!(
1540 matches!(pipe, Err(BlockGraphError::InvalidTag { .. })),
1541 "tag {bad:?} should be rejected"
1542 );
1543 }
1544 }
1545
1546 #[test]
1547 fn empty_tag_clears_instead_of_erroring() {
1548 let block = Block::new(IVec3::ZERO, BlockKind::Port)
1551 .with_tag("kept")
1552 .expect("valid tag")
1553 .with_tag("")
1554 .expect("empty tag clears");
1555 assert_eq!(block.tag(), None);
1556
1557 let pipe = Pipe::new(IVec3::ZERO, Direction::ZPLUS)
1558 .with_tag("kept")
1559 .expect("valid tag")
1560 .with_tag("")
1561 .expect("empty tag clears");
1562 assert_eq!(pipe.tag(), None);
1563 }
1564
1565 #[test]
1566 fn with_tag_accepts_symbols() {
1567 for tag in ["if", "0", "^", "()", "π"] {
1568 let block = Block::new(IVec3::ZERO, BlockKind::Port)
1569 .with_tag(tag)
1570 .unwrap();
1571 assert_eq!(block.tag(), Some(tag));
1572 }
1573 }
1574
1575 #[test]
1576 fn cube_height_footprint_is_bounded_before_reserved_offset_allocation() {
1577 let near_top = Block::new(IVec3::new(0, 0, i32::MAX), BlockKind::Cube(CubeKind::ZXZ));
1578 assert!(matches!(
1579 near_top
1580 .clone()
1581 .with_height("2d".parse().expect("valid height")),
1582 Err(BlockError::CubeHeightCoordinateOverflow { .. })
1583 ));
1584 near_top
1586 .with_height("d/2".parse().expect("valid height"))
1587 .unwrap();
1588 }
1589
1590 #[test]
1591 fn cube_height_is_cube_only_and_resets_with_the_kind() {
1592 let port = Block::new(IVec3::ZERO, BlockKind::Port);
1593 assert!(matches!(
1594 port.with_height("2d".parse().expect("valid height")),
1595 Err(BlockError::CubeHeightOnNonCube(BlockKind::Port))
1596 ));
1597
1598 let mut cube = Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
1599 .with_height("2d".parse().expect("valid height"))
1600 .expect("cube accepts a height");
1601 assert_eq!(cube.height_cells(), 2);
1602 cube.set_kind(BlockKind::Y);
1603 assert_eq!(cube.height(), CubeHeight::DEFAULT);
1604 assert_eq!(cube.height_cells(), 1);
1605 }
1606
1607 #[test]
1608 fn port_color_is_port_only_and_keeps_default_alpha() {
1609 let mut port = Block::new(IVec3::ZERO, BlockKind::Port)
1610 .with_port_color([0xeb, 0x40, 0x34])
1611 .expect("Port accepts a color");
1612 assert_eq!(
1613 port.port_color(),
1614 Some(RGBA::from_hex(0xeb4034, RGBA::PORT_GRAY.a))
1615 );
1616 assert_eq!(port.to_string(), "Port [0, 0, 0] color=eb4034");
1617
1618 port.set_kind(BlockKind::Y);
1619 assert_eq!(port.port_color(), None);
1620 assert!(matches!(
1621 port.with_port_color([0, 0, 0]),
1622 Err(BlockError::PortColorOnNonPort(BlockKind::Y))
1623 ));
1624 }
1625
1626 #[test]
1627 fn cube_height_drives_cells_not_rounds() {
1628 let half = Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
1629 .with_height("d/2".parse().expect("valid height"))
1630 .expect("cube accepts a height");
1631 assert_eq!(half.reserved_offsets().as_slice(), [IVec3::ZERO]);
1634 assert_eq!(half.connectable_offsets().as_slice(), [IVec3::ZERO]);
1635 assert_eq!(half.height().rounds(7), 4);
1636
1637 let tall = half
1638 .with_height("3d/2".parse().expect("valid height"))
1639 .expect("cube accepts a height");
1640 assert_eq!(
1641 tall.reserved_offsets().as_slice(),
1642 [IVec3::ZERO, IVec3::new(0, 0, 1)]
1643 );
1644 assert_eq!(tall.height().rounds(7), 11);
1645 }
1646
1647 #[test]
1648 fn display_emits_the_canonical_height_only_for_non_default_cubes() {
1649 let plain = Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ));
1650 assert_eq!(plain.to_string(), "ZXZ [0, 0, 0]");
1651
1652 let scaled = plain
1653 .with_height("2d/4".parse().expect("valid height"))
1654 .expect("cube accepts a height");
1655 assert_eq!(scaled.to_string(), "ZXZ [0, 0, 0] height=d/2");
1656 }
1657
1658 #[test]
1659 fn pipe_shift_validates_the_translated_endpoints() {
1660 let shifted = Pipe::new(IVec3::new(i32::MAX, 0, 0), Direction::XPLUS)
1661 .try_with_shift(IVec3::NEG_X)
1662 .expect("translation brings both endpoints into range");
1663
1664 assert_eq!(
1665 shifted.try_endpoints().unwrap(),
1666 (IVec3::new(i32::MAX - 1, 0, 0), IVec3::new(i32::MAX, 0, 0),)
1667 );
1668 }
1669
1670 #[test]
1671 fn moving_block_queries_handle_unrepresentable_endpoints() {
1672 let start = IVec3::splat(i32::MAX);
1673 let wrapped_endpoint = IVec3::splat(i32::MIN);
1674 let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::ONE).unwrap();
1675 let rotation = PatchRotationKind::new(Basis::X, IVec2::X).unwrap();
1676
1677 walking.try_end_position(start).unwrap_err();
1678 rotation.try_end_position(start).unwrap_err();
1679
1680 for kind in [
1681 BlockKind::Walking(walking),
1682 BlockKind::PatchRotation(rotation),
1683 ] {
1684 let block = Block::new(start, kind);
1685 assert!(!block.occupies_position(wrapped_endpoint));
1686 assert!(!block.occupies_layer(i32::MIN));
1687 assert!(block.bases_at_endpoint(wrapped_endpoint).is_none());
1688 assert!(kind.bases_at_endpoint(start, wrapped_endpoint).is_none());
1689 assert!(
1690 kind.pipe_face_bases_at_endpoint(start, wrapped_endpoint)
1691 .is_none()
1692 );
1693 assert!(block.to_string().contains("[endpoint overflows i32]"));
1694 }
1695 }
1696}