1use std::collections::{HashMap, HashSet};
7
8use thiserror::Error;
9
10use super::types::ArchSpec;
11
12#[derive(Debug, Clone, PartialEq, Error)]
17pub enum ArchSpecError {
18 #[error("{0}")]
20 Structure(String),
21
22 #[error("{0}")]
24 ZoneBus(String),
25
26 #[error("{0}")]
28 InterZoneBus(String),
29
30 #[error("{0}")]
42 CyclicBus(String),
43
44 #[error("{0}")]
46 EntanglingPair(String),
47
48 #[error("{0}")]
50 Mode(String),
51
52 #[error("{0}")]
54 Path(String),
55}
56
57impl ArchSpec {
58 pub fn validate(&self) -> Result<(), Vec<ArchSpecError>> {
61 let mut errors = Vec::new();
62
63 let num_words = self.words.len();
64 let num_zones = self.zones.len();
65 let sites_per_word = self.sites_per_word();
66
67 check_minimum_counts(self, &mut errors);
69 check_non_negative_grid_spacings(self, &mut errors);
70 check_uniform_grid_dimensions(self, &mut errors);
71 check_uniform_word_site_counts(self, &mut errors);
72 check_word_site_indices(self, &mut errors);
73
74 for (zone_idx, zone) in self.zones.iter().enumerate() {
76 check_zone_words_with_site_buses(zone_idx, zone, num_words, &mut errors);
77 check_zone_sites_with_word_buses(zone_idx, zone, sites_per_word, &mut errors);
78 check_zone_site_buses(zone_idx, zone, sites_per_word, &mut errors);
79 check_zone_word_buses(zone_idx, zone, num_words, &mut errors);
80 check_zone_entangling_pairs(zone_idx, zone, num_words, &mut errors);
81 }
82
83 check_zone_buses(self, num_zones, num_words, &mut errors);
85
86 check_modes(self, num_zones, num_words, sites_per_word, &mut errors);
88
89 check_paths(self, num_zones, &mut errors);
91
92 check_zone_overlap(self, &mut errors);
94
95 if errors.is_empty() {
96 Ok(())
97 } else {
98 Err(errors)
99 }
100 }
101}
102
103fn check_minimum_counts(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
105 if spec.zones.is_empty() {
106 errors.push(ArchSpecError::Structure(
107 "at least one zone must exist".into(),
108 ));
109 }
110 if spec.words.is_empty() {
111 errors.push(ArchSpecError::Structure(
112 "at least one word must exist".into(),
113 ));
114 }
115}
116
117fn check_non_negative_grid_spacings(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
121 for (idx, zone) in spec.zones.iter().enumerate() {
122 if let Some(pos) = zone.grid.x_spacing.iter().position(|&v| v < 0.0) {
123 errors.push(ArchSpecError::Structure(format!(
124 "zone {} grid x_spacing[{}] is negative ({:.6})",
125 idx, pos, zone.grid.x_spacing[pos]
126 )));
127 }
128 if let Some(pos) = zone.grid.y_spacing.iter().position(|&v| v < 0.0) {
129 errors.push(ArchSpecError::Structure(format!(
130 "zone {} grid y_spacing[{}] is negative ({:.6})",
131 idx, pos, zone.grid.y_spacing[pos]
132 )));
133 }
134 }
135}
136
137fn check_uniform_grid_dimensions(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
139 if let Some(first_zone) = spec.zones.first() {
140 let ref_x = first_zone.grid.num_x();
141 let ref_y = first_zone.grid.num_y();
142 for (idx, zone) in spec.zones.iter().enumerate().skip(1) {
143 let zx = zone.grid.num_x();
144 let zy = zone.grid.num_y();
145 if zx != ref_x || zy != ref_y {
146 errors.push(ArchSpecError::Structure(format!(
147 "zone {} grid dimensions ({}x{}) differ from zone 0 ({}x{})",
148 idx, zx, zy, ref_x, ref_y
149 )));
150 }
151 }
152 }
153}
154
155fn check_uniform_word_site_counts(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
157 if let Some(first_word) = spec.words.first() {
158 let ref_count = first_word.sites.len();
159 for (idx, word) in spec.words.iter().enumerate().skip(1) {
160 if word.sites.len() != ref_count {
161 errors.push(ArchSpecError::Structure(format!(
162 "word {} has {} sites, expected {} (same as word 0)",
163 idx,
164 word.sites.len(),
165 ref_count
166 )));
167 }
168 }
169 }
170}
171
172fn check_word_site_indices(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
175 let (grid_x, grid_y) = match spec.zones.first() {
177 Some(z) => (z.grid.num_x(), z.grid.num_y()),
178 None => return, };
180
181 for (word_idx, word) in spec.words.iter().enumerate() {
182 for (site_idx, site) in word.sites.iter().enumerate() {
183 let x = site[0] as usize;
184 let y = site[1] as usize;
185 if x >= grid_x {
186 errors.push(ArchSpecError::Structure(format!(
187 "word {}, site {}: x index {} out of range (grid has {} x-positions)",
188 word_idx, site_idx, site[0], grid_x
189 )));
190 }
191 if y >= grid_y {
192 errors.push(ArchSpecError::Structure(format!(
193 "word {}, site {}: y index {} out of range (grid has {} y-positions)",
194 word_idx, site_idx, site[1], grid_y
195 )));
196 }
197 }
198 }
199}
200
201use super::types::Zone;
204
205fn check_zone_words_with_site_buses(
207 zone_idx: usize,
208 zone: &Zone,
209 num_words: usize,
210 errors: &mut Vec<ArchSpecError>,
211) {
212 for &wid in &zone.words_with_site_buses {
213 if wid as usize >= num_words {
214 errors.push(ArchSpecError::ZoneBus(format!(
215 "zone {}: words_with_site_buses contains invalid word ID {}",
216 zone_idx, wid
217 )));
218 }
219 }
220}
221
222fn check_zone_sites_with_word_buses(
224 zone_idx: usize,
225 zone: &Zone,
226 sites_per_word: usize,
227 errors: &mut Vec<ArchSpecError>,
228) {
229 for &sid in &zone.sites_with_word_buses {
230 if sid as usize >= sites_per_word {
231 errors.push(ArchSpecError::ZoneBus(format!(
232 "zone {}: sites_with_word_buses contains invalid site index {} (sites_per_word={})",
233 zone_idx, sid, sites_per_word
234 )));
235 }
236 }
237}
238
239fn check_zone_site_buses(
241 zone_idx: usize,
242 zone: &Zone,
243 sites_per_word: usize,
244 errors: &mut Vec<ArchSpecError>,
245) {
246 for (bus_idx, bus) in zone.site_buses.iter().enumerate() {
247 if bus.src.len() != bus.dst.len() {
248 errors.push(ArchSpecError::ZoneBus(format!(
249 "zone {}, site_bus {}: src length ({}) != dst length ({})",
250 zone_idx,
251 bus_idx,
252 bus.src.len(),
253 bus.dst.len()
254 )));
255 }
256 for (i, sref) in bus.src.iter().enumerate() {
257 if sref.0 as usize >= sites_per_word {
258 errors.push(ArchSpecError::ZoneBus(format!(
259 "zone {}, site_bus {}: src[{}] SiteRef({}) >= sites_per_word ({})",
260 zone_idx, bus_idx, i, sref.0, sites_per_word
261 )));
262 }
263 }
264 for (i, sref) in bus.dst.iter().enumerate() {
265 if sref.0 as usize >= sites_per_word {
266 errors.push(ArchSpecError::ZoneBus(format!(
267 "zone {}, site_bus {}: dst[{}] SiteRef({}) >= sites_per_word ({})",
268 zone_idx, bus_idx, i, sref.0, sites_per_word
269 )));
270 }
271 }
272 check_bus_relation(
273 &bus.src,
274 &bus.dst,
275 &format!("zone {zone_idx}, site_bus {bus_idx}"),
276 |s| s.0.to_string(),
277 ArchSpecError::ZoneBus,
278 errors,
279 );
280 }
281}
282
283fn check_zone_word_buses(
285 zone_idx: usize,
286 zone: &Zone,
287 num_words: usize,
288 errors: &mut Vec<ArchSpecError>,
289) {
290 for (bus_idx, bus) in zone.word_buses.iter().enumerate() {
291 if bus.src.len() != bus.dst.len() {
292 errors.push(ArchSpecError::ZoneBus(format!(
293 "zone {}, word_bus {}: src length ({}) != dst length ({})",
294 zone_idx,
295 bus_idx,
296 bus.src.len(),
297 bus.dst.len()
298 )));
299 }
300 for (i, wref) in bus.src.iter().enumerate() {
301 if wref.0 as usize >= num_words {
302 errors.push(ArchSpecError::ZoneBus(format!(
303 "zone {}, word_bus {}: src[{}] WordRef({}) >= num_words ({})",
304 zone_idx, bus_idx, i, wref.0, num_words
305 )));
306 }
307 }
308 for (i, wref) in bus.dst.iter().enumerate() {
309 if wref.0 as usize >= num_words {
310 errors.push(ArchSpecError::ZoneBus(format!(
311 "zone {}, word_bus {}: dst[{}] WordRef({}) >= num_words ({})",
312 zone_idx, bus_idx, i, wref.0, num_words
313 )));
314 }
315 }
316 check_bus_relation(
317 &bus.src,
318 &bus.dst,
319 &format!("zone {zone_idx}, word_bus {bus_idx}"),
320 |w| w.0.to_string(),
321 ArchSpecError::ZoneBus,
322 errors,
323 );
324 }
325}
326
327fn check_zone_buses(
332 spec: &ArchSpec,
333 num_zones: usize,
334 num_words: usize,
335 errors: &mut Vec<ArchSpecError>,
336) {
337 for (bus_idx, bus) in spec.zone_buses.iter().enumerate() {
338 if bus.src.len() != bus.dst.len() {
339 errors.push(ArchSpecError::InterZoneBus(format!(
340 "zone_bus {}: src length ({}) != dst length ({})",
341 bus_idx,
342 bus.src.len(),
343 bus.dst.len()
344 )));
345 }
346
347 for (i, zwr) in bus.src.iter().enumerate() {
349 if zwr.zone_id as usize >= num_zones {
350 errors.push(ArchSpecError::InterZoneBus(format!(
351 "zone_bus {}: src[{}] zone_id {} >= num_zones ({})",
352 bus_idx, i, zwr.zone_id, num_zones
353 )));
354 }
355 if zwr.word_id as usize >= num_words {
356 errors.push(ArchSpecError::InterZoneBus(format!(
357 "zone_bus {}: src[{}] word_id {} >= num_words ({})",
358 bus_idx, i, zwr.word_id, num_words
359 )));
360 }
361 }
362 for (i, zwr) in bus.dst.iter().enumerate() {
363 if zwr.zone_id as usize >= num_zones {
364 errors.push(ArchSpecError::InterZoneBus(format!(
365 "zone_bus {}: dst[{}] zone_id {} >= num_zones ({})",
366 bus_idx, i, zwr.zone_id, num_zones
367 )));
368 }
369 if zwr.word_id as usize >= num_words {
370 errors.push(ArchSpecError::InterZoneBus(format!(
371 "zone_bus {}: dst[{}] word_id {} >= num_words ({})",
372 bus_idx, i, zwr.word_id, num_words
373 )));
374 }
375 }
376
377 let pair_count = bus.src.len().min(bus.dst.len());
379 for i in 0..pair_count {
380 if bus.src[i].zone_id == bus.dst[i].zone_id {
381 errors.push(ArchSpecError::InterZoneBus(format!(
382 "zone_bus {}: pair {} does not cross a zone boundary \
383 (src zone_id={}, dst zone_id={})",
384 bus_idx, i, bus.src[i].zone_id, bus.dst[i].zone_id
385 )));
386 }
387 }
388
389 check_bus_relation(
390 &bus.src,
391 &bus.dst,
392 &format!("zone_bus {bus_idx}"),
393 |z| format!("z{}w{}", z.zone_id, z.word_id),
394 ArchSpecError::InterZoneBus,
395 errors,
396 );
397 }
398}
399
400fn check_bus_relation<T>(
420 src: &[T],
421 dst: &[T],
422 label: &str,
423 fmt: impl Fn(&T) -> String,
424 dup_err: impl Fn(String) -> ArchSpecError,
425 errors: &mut Vec<ArchSpecError>,
426) where
427 T: Copy + Eq + std::hash::Hash,
428{
429 let mut first_src: HashMap<T, usize> = HashMap::new();
430 for (i, s) in src.iter().enumerate() {
431 if let Some(&j) = first_src.get(s) {
432 errors.push(dup_err(format!(
433 "{label}: duplicate src {} at indices {j} and {i} \
434 (first-match resolution silently shadows the later pair)",
435 fmt(s)
436 )));
437 } else {
438 first_src.insert(*s, i);
439 }
440 }
441
442 let mut first_dst: HashMap<T, usize> = HashMap::new();
443 for (i, d) in dst.iter().enumerate() {
444 if let Some(&j) = first_dst.get(d) {
445 errors.push(dup_err(format!(
446 "{label}: duplicate dst {} at indices {j} and {i} \
447 (two simultaneous transports into one site cannot both complete)",
448 fmt(d)
449 )));
450 } else {
451 first_dst.insert(*d, i);
452 }
453 }
454
455 let mut next: HashMap<T, T> = HashMap::new();
459 for (s, d) in src.iter().zip(dst.iter()) {
460 next.entry(*s).or_insert(*d);
461 }
462
463 let mut state: HashMap<T, u8> = HashMap::new();
465 for start in src {
466 if state.get(start).copied() == Some(2) {
467 continue;
468 }
469 let mut walk: Vec<T> = Vec::new();
470 let mut cur = *start;
471 loop {
472 match state.get(&cur).copied() {
473 Some(2) => break,
474 Some(1) => {
475 let pos = walk
476 .iter()
477 .position(|n| *n == cur)
478 .expect("a node in state 1 is always on the current walk stack");
479 let cycle: Vec<String> = walk[pos..]
480 .iter()
481 .chain(std::iter::once(&cur))
482 .map(&fmt)
483 .collect();
484 errors.push(ArchSpecError::CyclicBus(format!(
485 "{label}: src→dst relation contains a cycle: {} \
486 (a bus is a set of explicit transports, never a rotation)",
487 cycle.join(" -> ")
488 )));
489 break;
490 }
491 _ => {
492 state.insert(cur, 1);
493 walk.push(cur);
494 match next.get(&cur) {
495 Some(n) => cur = *n,
496 None => break,
497 }
498 }
499 }
500 }
501 for n in walk {
502 state.insert(n, 2);
503 }
504 }
505}
506
507fn check_zone_entangling_pairs(
511 zone_idx: usize,
512 zone: &super::types::Zone,
513 num_words: usize,
514 errors: &mut Vec<ArchSpecError>,
515) {
516 let mut seen: HashSet<[u32; 2]> = HashSet::new();
517 let mut pair_of_word: HashMap<u32, usize> = HashMap::new();
520 for (idx, pair) in zone.entangling_pairs.iter().enumerate() {
521 let [a, b] = *pair;
522 if a as usize >= num_words {
523 errors.push(ArchSpecError::EntanglingPair(format!(
524 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
525 zone_idx, idx, a, num_words
526 )));
527 }
528 if b as usize >= num_words {
529 errors.push(ArchSpecError::EntanglingPair(format!(
530 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
531 zone_idx, idx, b, num_words
532 )));
533 }
534 if a == b {
535 errors.push(ArchSpecError::EntanglingPair(format!(
536 "zone[{}].entangling_pairs[{}]: word paired with itself ({})",
537 zone_idx, idx, a
538 )));
539 }
540 let normalized = if a <= b { [a, b] } else { [b, a] };
541 if !seen.insert(normalized) {
542 errors.push(ArchSpecError::EntanglingPair(format!(
543 "zone[{}].entangling_pairs[{}]: duplicate pair [{}, {}]",
544 zone_idx, idx, a, b
545 )));
546 continue;
548 }
549 if a == b {
550 continue;
551 }
552 for word in [a, b] {
553 if let Some(&first) = pair_of_word.get(&word) {
554 errors.push(ArchSpecError::EntanglingPair(format!(
555 "zone[{}].entangling_pairs[{}]: word {} is already paired in \
556 entangling_pairs[{}] (a word has at most one partner per zone)",
557 zone_idx, idx, word, first
558 )));
559 } else {
560 pair_of_word.insert(word, idx);
561 }
562 }
563 }
564}
565
566fn check_modes(
570 spec: &ArchSpec,
571 num_zones: usize,
572 num_words: usize,
573 sites_per_word: usize,
574 errors: &mut Vec<ArchSpecError>,
575) {
576 for (mode_idx, mode) in spec.modes.iter().enumerate() {
577 for &zone_id in &mode.zones {
578 if zone_id as usize >= num_zones {
579 errors.push(ArchSpecError::Mode(format!(
580 "mode '{}' (index {}): zone ID {} >= num_zones ({})",
581 mode.name, mode_idx, zone_id, num_zones
582 )));
583 }
584 }
585 for (loc_idx, loc) in mode.bitstring_order.iter().enumerate() {
586 if loc.zone_id as usize >= num_zones {
587 errors.push(ArchSpecError::Mode(format!(
588 "mode '{}' (index {}): bitstring_order[{}] zone_id {} >= num_zones ({})",
589 mode.name, mode_idx, loc_idx, loc.zone_id, num_zones
590 )));
591 }
592 if loc.word_id as usize >= num_words {
593 errors.push(ArchSpecError::Mode(format!(
594 "mode '{}' (index {}): bitstring_order[{}] word_id {} >= num_words ({})",
595 mode.name, mode_idx, loc_idx, loc.word_id, num_words
596 )));
597 }
598 if loc.site_id as usize >= sites_per_word {
599 errors.push(ArchSpecError::Mode(format!(
600 "mode '{}' (index {}): bitstring_order[{}] site_id {} >= sites_per_word ({})",
601 mode.name, mode_idx, loc_idx, loc.site_id, sites_per_word
602 )));
603 }
604 }
605 }
606}
607
608fn check_paths(spec: &ArchSpec, num_zones: usize, errors: &mut Vec<ArchSpecError>) {
612 if let Some(paths) = &spec.paths {
613 for (idx, path) in paths.iter().enumerate() {
614 if !path.check_finite() {
615 errors.push(ArchSpecError::Path(format!(
616 "paths[{}]: waypoint contains non-finite coordinate",
617 idx
618 )));
619 }
620
621 let lane = super::addr::LaneAddr::decode_u64(path.lane);
623 if lane.zone_id as usize >= num_zones {
624 errors.push(ArchSpecError::Path(format!(
625 "paths[{}]: lane 0x{:016X} has invalid zone_id {} (num_zones={})",
626 idx, path.lane, lane.zone_id, num_zones
627 )));
628 }
629
630 if path.waypoints.len() < 2 {
631 errors.push(ArchSpecError::Path(format!(
632 "paths[{}]: lane 0x{:016X} has {} waypoint(s), minimum is 2",
633 idx,
634 path.lane,
635 path.waypoints.len()
636 )));
637 }
638 }
639 }
640}
641
642fn check_zone_overlap(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
646 let boxes: Vec<(usize, (f64, f64, f64, f64))> = spec
647 .zones
648 .iter()
649 .enumerate()
650 .map(|(i, z)| (i, z.grid.bounding_box()))
651 .collect();
652
653 for i in 0..boxes.len() {
654 for j in (i + 1)..boxes.len() {
655 let (zi, (ax_min, ax_max, ay_min, ay_max)) = boxes[i];
656 let (zj, (bx_min, bx_max, by_min, by_max)) = boxes[j];
657
658 let x_overlap = ax_min < bx_max && bx_min < ax_max;
661 let y_overlap = ay_min < by_max && by_min < ay_max;
662
663 if x_overlap && y_overlap {
664 errors.push(ArchSpecError::Structure(format!(
665 "zone {} and zone {} have overlapping bounding boxes: \
666 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}], \
667 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}]",
668 zi, zj, zi, ax_min, ax_max, ay_min, ay_max, zj, bx_min, bx_max, by_min, by_max,
669 )));
670 }
671 }
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678 use crate::arch::addr::{SiteRef, WordRef, ZonedWordRef};
679 use crate::arch::types::{Bus, Grid, Mode, Word, Zone};
680 use crate::version::Version;
681
682 fn make_valid_two_zone_spec() -> ArchSpec {
684 let grid0 = Grid::from_positions(&[0.0, 5.0, 10.0], &[0.0, 3.0]);
685 let grid1 = Grid::from_positions(&[20.0, 27.5, 35.0], &[0.0, 4.0]);
687
688 ArchSpec {
689 version: Version::new(2, 0),
690 words: vec![
691 Word {
692 sites: vec![[0, 0], [0, 1]],
693 },
694 Word {
695 sites: vec![[1, 0], [1, 1]],
696 },
697 ],
698 zones: vec![
699 Zone {
700 name: String::new(),
701 grid: grid0,
702 site_buses: vec![Bus {
703 src: vec![SiteRef(0)],
704 dst: vec![SiteRef(1)],
705 }],
706 word_buses: vec![Bus {
707 src: vec![WordRef(0)],
708 dst: vec![WordRef(1)],
709 }],
710 words_with_site_buses: vec![0, 1],
711 sites_with_word_buses: vec![0],
712 entangling_pairs: vec![[0, 1]],
713 },
714 Zone {
715 name: String::new(),
716 grid: grid1,
717 site_buses: vec![],
718 word_buses: vec![],
719 words_with_site_buses: vec![],
720 sites_with_word_buses: vec![],
721 entangling_pairs: vec![],
722 },
723 ],
724 zone_buses: vec![Bus {
725 src: vec![ZonedWordRef {
726 zone_id: 0,
727 word_id: 0,
728 }],
729 dst: vec![ZonedWordRef {
730 zone_id: 1,
731 word_id: 0,
732 }],
733 }],
734 modes: vec![Mode {
735 name: "full".to_string(),
736 zones: vec![0, 1],
737 bitstring_order: vec![],
738 }],
739 paths: None,
740 feed_forward: false,
741 atom_reloading: false,
742 blockade_radius: None,
743 }
744 }
745
746 #[test]
747 fn test_valid_two_zone_spec() {
748 let spec = make_valid_two_zone_spec();
749 assert!(spec.validate().is_ok());
750 }
751
752 #[test]
753 fn test_validate_zones_must_have_same_grid_dimensions() {
754 let mut spec = make_valid_two_zone_spec();
755 spec.zones[1].grid = Grid::from_positions(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0]);
757 assert!(matches!(
758 spec.validate(),
759 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(_)))
760 ));
761 }
762
763 #[test]
764 fn test_validate_site_bus_ref_out_of_range() {
765 let mut spec = make_valid_two_zone_spec();
766 spec.zones[0].site_buses = vec![Bus {
767 src: vec![SiteRef(0)],
768 dst: vec![SiteRef(999)],
769 }];
770 assert!(matches!(
771 spec.validate(),
772 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(_)))
773 ));
774 }
775
776 #[test]
777 fn test_validate_zone_bus_must_cross_zones() {
778 let mut spec = make_valid_two_zone_spec();
779 spec.zone_buses = vec![Bus {
780 src: vec![ZonedWordRef {
781 zone_id: 0,
782 word_id: 0,
783 }],
784 dst: vec![ZonedWordRef {
785 zone_id: 0,
786 word_id: 1,
787 }], }];
789 assert!(matches!(
790 spec.validate(),
791 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(_)))
792 ));
793 }
794
795 fn add_third_word(spec: &mut ArchSpec) {
798 spec.words.push(Word {
799 sites: vec![[2, 0], [2, 1]],
800 });
801 spec.zones[0].words_with_site_buses.push(2);
802 }
803
804 #[test]
805 fn test_validate_overlapping_acyclic_bus_accepted() {
806 let mut spec = make_valid_two_zone_spec();
807 add_third_word(&mut spec);
808 spec.zones[0].word_buses = vec![Bus {
810 src: vec![WordRef(0), WordRef(1)],
811 dst: vec![WordRef(1), WordRef(2)],
812 }];
813 assert!(spec.validate().is_ok());
814 }
815
816 #[test]
817 fn test_validate_cyclic_site_bus_rejected() {
818 let mut spec = make_valid_two_zone_spec();
819 spec.zones[0].site_buses = vec![Bus {
821 src: vec![SiteRef(0), SiteRef(1)],
822 dst: vec![SiteRef(1), SiteRef(0)],
823 }];
824 assert!(matches!(
825 spec.validate(),
826 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
827 ));
828 }
829
830 #[test]
831 fn test_validate_self_loop_site_bus_rejected() {
832 let mut spec = make_valid_two_zone_spec();
833 spec.zones[0].site_buses = vec![Bus {
834 src: vec![SiteRef(0)],
835 dst: vec![SiteRef(0)],
836 }];
837 assert!(matches!(
838 spec.validate(),
839 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
840 ));
841 }
842
843 #[test]
844 fn test_validate_duplicate_bus_src_rejected() {
845 let mut spec = make_valid_two_zone_spec();
846 add_third_word(&mut spec);
847 spec.zones[0].word_buses = vec![Bus {
849 src: vec![WordRef(0), WordRef(0)],
850 dst: vec![WordRef(1), WordRef(2)],
851 }];
852 assert!(matches!(
853 spec.validate(),
854 Err(ref errs) if errs.iter().any(
855 |e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("duplicate src"))
856 )
857 ));
858 }
859
860 #[test]
861 fn test_validate_duplicate_bus_dst_rejected() {
862 let mut spec = make_valid_two_zone_spec();
863 add_third_word(&mut spec);
864 spec.zones[0].word_buses = vec![Bus {
866 src: vec![WordRef(0), WordRef(2)],
867 dst: vec![WordRef(1), WordRef(1)],
868 }];
869 assert!(matches!(
870 spec.validate(),
871 Err(ref errs) if errs.iter().any(
872 |e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("duplicate dst"))
873 )
874 ));
875 }
876
877 #[test]
878 fn test_validate_cyclic_zone_bus_rejected() {
879 let mut spec = make_valid_two_zone_spec();
880 let z0 = ZonedWordRef {
883 zone_id: 0,
884 word_id: 0,
885 };
886 let z1 = ZonedWordRef {
887 zone_id: 1,
888 word_id: 0,
889 };
890 spec.zone_buses = vec![Bus {
891 src: vec![z0, z1],
892 dst: vec![z1, z0],
893 }];
894 assert!(matches!(
895 spec.validate(),
896 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
897 ));
898 }
899
900 #[test]
901 fn test_validate_entangling_pair_invalid_word() {
902 let mut spec = make_valid_two_zone_spec();
903 spec.zones[0].entangling_pairs = vec![[0, 99]];
904 assert!(matches!(
905 spec.validate(),
906 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(_)))
907 ));
908 }
909
910 #[test]
911 fn test_validate_mode_invalid_zone() {
912 let mut spec = make_valid_two_zone_spec();
913 spec.modes = vec![Mode {
914 name: "bad".to_string(),
915 zones: vec![99],
916 bitstring_order: vec![],
917 }];
918 assert!(matches!(
919 spec.validate(),
920 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(_)))
921 ));
922 }
923
924 #[test]
925 fn test_validate_no_zones() {
926 let mut spec = make_valid_two_zone_spec();
927 spec.zones = vec![];
928 assert!(matches!(
929 spec.validate(),
930 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("zone")))
931 ));
932 }
933
934 #[test]
935 fn test_validate_no_words() {
936 let mut spec = make_valid_two_zone_spec();
937 spec.words = vec![];
938 assert!(matches!(
939 spec.validate(),
940 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("word")))
941 ));
942 }
943
944 #[test]
945 fn test_validate_word_site_count_mismatch() {
946 let mut spec = make_valid_two_zone_spec();
947 spec.words[1].sites = vec![[0, 0]]; assert!(matches!(
949 spec.validate(),
950 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("sites")))
951 ));
952 }
953
954 #[test]
955 fn test_validate_word_site_x_out_of_range() {
956 let mut spec = make_valid_two_zone_spec();
957 spec.words[0].sites[0] = [99, 0]; assert!(matches!(
959 spec.validate(),
960 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("x index")))
961 ));
962 }
963
964 #[test]
965 fn test_validate_word_site_y_out_of_range() {
966 let mut spec = make_valid_two_zone_spec();
967 spec.words[0].sites[0] = [0, 99]; assert!(matches!(
969 spec.validate(),
970 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("y index")))
971 ));
972 }
973
974 #[test]
975 fn test_validate_zone_words_with_site_buses_invalid() {
976 let mut spec = make_valid_two_zone_spec();
977 spec.zones[0].words_with_site_buses = vec![0, 99];
978 assert!(matches!(
979 spec.validate(),
980 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("words_with_site_buses")))
981 ));
982 }
983
984 #[test]
985 fn test_validate_zone_sites_with_word_buses_invalid() {
986 let mut spec = make_valid_two_zone_spec();
987 spec.zones[0].sites_with_word_buses = vec![99];
988 assert!(matches!(
989 spec.validate(),
990 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("sites_with_word_buses")))
991 ));
992 }
993
994 #[test]
995 fn test_validate_site_bus_length_mismatch() {
996 let mut spec = make_valid_two_zone_spec();
997 spec.zones[0].site_buses = vec![Bus {
998 src: vec![SiteRef(0), SiteRef(1)],
999 dst: vec![SiteRef(0)],
1000 }];
1001 assert!(matches!(
1002 spec.validate(),
1003 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("src length")))
1004 ));
1005 }
1006
1007 #[test]
1008 fn test_validate_word_bus_invalid_word_ref() {
1009 let mut spec = make_valid_two_zone_spec();
1010 spec.zones[0].word_buses = vec![Bus {
1011 src: vec![WordRef(0)],
1012 dst: vec![WordRef(99)],
1013 }];
1014 assert!(matches!(
1015 spec.validate(),
1016 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("WordRef(99)")))
1017 ));
1018 }
1019
1020 #[test]
1021 fn test_validate_zone_bus_invalid_zone_id() {
1022 let mut spec = make_valid_two_zone_spec();
1023 spec.zone_buses = vec![Bus {
1024 src: vec![ZonedWordRef {
1025 zone_id: 99,
1026 word_id: 0,
1027 }],
1028 dst: vec![ZonedWordRef {
1029 zone_id: 1,
1030 word_id: 0,
1031 }],
1032 }];
1033 assert!(matches!(
1034 spec.validate(),
1035 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("zone_id 99")))
1036 ));
1037 }
1038
1039 #[test]
1040 fn test_validate_zone_bus_invalid_word_id() {
1041 let mut spec = make_valid_two_zone_spec();
1042 spec.zone_buses = vec![Bus {
1043 src: vec![ZonedWordRef {
1044 zone_id: 0,
1045 word_id: 99,
1046 }],
1047 dst: vec![ZonedWordRef {
1048 zone_id: 1,
1049 word_id: 0,
1050 }],
1051 }];
1052 assert!(matches!(
1053 spec.validate(),
1054 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("word_id 99")))
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_validate_zone_bus_length_mismatch() {
1060 let mut spec = make_valid_two_zone_spec();
1061 spec.zone_buses = vec![Bus {
1062 src: vec![
1063 ZonedWordRef {
1064 zone_id: 0,
1065 word_id: 0,
1066 },
1067 ZonedWordRef {
1068 zone_id: 0,
1069 word_id: 1,
1070 },
1071 ],
1072 dst: vec![ZonedWordRef {
1073 zone_id: 1,
1074 word_id: 0,
1075 }],
1076 }];
1077 assert!(matches!(
1078 spec.validate(),
1079 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("src length")))
1080 ));
1081 }
1082
1083 #[test]
1084 fn test_validate_entangling_pair_duplicate() {
1085 let mut spec = make_valid_two_zone_spec();
1086 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 0]]; assert!(matches!(
1088 spec.validate(),
1089 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(msg) if msg.contains("duplicate")))
1090 ));
1091 }
1092
1093 fn make_valid_three_word_spec() -> ArchSpec {
1096 let mut spec = make_valid_two_zone_spec();
1097 spec.words.push(Word {
1098 sites: vec![[2, 0], [2, 1]],
1099 });
1100 spec
1101 }
1102
1103 #[test]
1104 fn test_validate_entangling_pair_word_in_two_pairs() {
1105 let mut spec = make_valid_three_word_spec();
1106 assert!(spec.validate().is_ok());
1107 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 2]];
1108 let errs = spec.validate().unwrap_err();
1109 let overlap: Vec<_> = errs
1110 .iter()
1111 .filter(|e| matches!(e, ArchSpecError::EntanglingPair(msg) if msg.contains("already paired")))
1112 .collect();
1113 assert_eq!(overlap.len(), 1, "{errs:?}");
1114 assert_eq!(
1115 overlap[0].to_string(),
1116 "zone[0].entangling_pairs[1]: word 1 is already paired in \
1117 entangling_pairs[0] (a word has at most one partner per zone)"
1118 );
1119 }
1120
1121 #[test]
1122 fn test_validate_entangling_pair_reports_every_overlapping_word() {
1123 let mut spec = make_valid_three_word_spec();
1124 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 2], [2, 0]];
1125 let errs = spec.validate().unwrap_err();
1126 let overlap = errs
1127 .iter()
1128 .filter(|e| matches!(e, ArchSpecError::EntanglingPair(msg) if msg.contains("already paired")))
1129 .count();
1130 assert_eq!(overlap, 3, "{errs:?}");
1132 }
1133
1134 #[test]
1135 fn test_validate_entangling_pair_same_word_in_different_zones() {
1136 let mut spec = make_valid_three_word_spec();
1137 spec.zones[0].entangling_pairs = vec![[0, 1]];
1138 spec.zones[1].entangling_pairs = vec![[1, 2]];
1139 assert!(spec.validate().is_ok());
1140 }
1141
1142 #[test]
1143 fn test_validate_entangling_pair_duplicate_not_reported_as_overlap() {
1144 let mut spec = make_valid_two_zone_spec();
1145 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 0]];
1146 let errs = spec.validate().unwrap_err();
1147 assert_eq!(errs.len(), 1, "{errs:?}");
1148 }
1149
1150 #[test]
1151 fn test_validate_mode_bitstring_order_invalid() {
1152 use crate::arch::addr::LocationAddr;
1153 let mut spec = make_valid_two_zone_spec();
1154 spec.modes = vec![Mode {
1155 name: "bad_loc".to_string(),
1156 zones: vec![0],
1157 bitstring_order: vec![LocationAddr {
1158 zone_id: 99,
1159 word_id: 0,
1160 site_id: 0,
1161 }],
1162 }];
1163 assert!(matches!(
1164 spec.validate(),
1165 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(msg) if msg.contains("zone_id 99")))
1166 ));
1167 }
1168
1169 #[test]
1170 fn test_validate_multiple_errors_collected() {
1171 let mut spec = make_valid_two_zone_spec();
1172 spec.zones[0].entangling_pairs = vec![[0, 99]]; spec.zones[0].words_with_site_buses = vec![99]; let errors = spec.validate().unwrap_err();
1176 assert!(
1177 errors.len() >= 2,
1178 "expected at least 2 errors, got {}",
1179 errors.len()
1180 );
1181 }
1182
1183 #[test]
1184 fn test_validate_path_non_finite_waypoint() {
1185 let mut spec = make_valid_two_zone_spec();
1186 let lane = crate::arch::addr::LaneAddr {
1187 direction: crate::arch::addr::Direction::Forward,
1188 move_type: crate::arch::addr::MoveType::SiteBus,
1189 zone_id: 0,
1190 word_id: 0,
1191 site_id: 0,
1192 bus_id: 0,
1193 };
1194 spec.paths = Some(vec![crate::arch::types::TransportPath {
1195 lane: lane.encode_u64(),
1196 waypoints: vec![[f64::NAN, 0.0], [1.0, 2.0]],
1197 }]);
1198 assert!(matches!(
1199 spec.validate(),
1200 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("non-finite")))
1201 ));
1202 }
1203
1204 #[test]
1205 fn test_validate_path_too_few_waypoints() {
1206 let mut spec = make_valid_two_zone_spec();
1207 let lane = crate::arch::addr::LaneAddr {
1208 direction: crate::arch::addr::Direction::Forward,
1209 move_type: crate::arch::addr::MoveType::SiteBus,
1210 zone_id: 0,
1211 word_id: 0,
1212 site_id: 0,
1213 bus_id: 0,
1214 };
1215 spec.paths = Some(vec![crate::arch::types::TransportPath {
1216 lane: lane.encode_u64(),
1217 waypoints: vec![[1.0, 2.0]],
1218 }]);
1219 assert!(matches!(
1220 spec.validate(),
1221 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("minimum is 2")))
1222 ));
1223 }
1224
1225 #[test]
1226 fn test_validate_path_invalid_zone_id() {
1227 let mut spec = make_valid_two_zone_spec();
1228 let lane = crate::arch::addr::LaneAddr {
1229 direction: crate::arch::addr::Direction::Forward,
1230 move_type: crate::arch::addr::MoveType::SiteBus,
1231 zone_id: 99,
1232 word_id: 0,
1233 site_id: 0,
1234 bus_id: 0,
1235 };
1236 spec.paths = Some(vec![crate::arch::types::TransportPath {
1237 lane: lane.encode_u64(),
1238 waypoints: vec![[0.0, 0.0], [1.0, 2.0]],
1239 }]);
1240 assert!(matches!(
1241 spec.validate(),
1242 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("zone_id")))
1243 ));
1244 }
1245
1246 #[test]
1249 fn test_non_overlapping_zones_pass() {
1250 let spec = make_valid_two_zone_spec();
1253 assert!(spec.validate().is_ok());
1254 }
1255
1256 #[test]
1257 fn test_overlapping_zones_rejected() {
1258 let mut spec = make_valid_two_zone_spec();
1259 spec.zones[1].grid = Grid::from_positions(&[5.0, 12.5, 20.0], &[0.0, 4.0]);
1261 assert!(matches!(
1262 spec.validate(),
1263 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping bounding boxes")))
1264 ));
1265 }
1266
1267 #[test]
1268 fn test_adjacent_zones_not_overlapping() {
1269 let mut spec = make_valid_two_zone_spec();
1270 spec.zones[1].grid = Grid::from_positions(&[10.0, 17.5, 25.0], &[0.0, 4.0]);
1272 assert!(spec.validate().is_ok());
1273 }
1274
1275 #[test]
1276 fn test_shared_x_range_with_y_overlap_rejected() {
1277 let mut spec = make_valid_two_zone_spec();
1278 spec.zones[1].grid = Grid::from_positions(&[0.0, 7.5, 15.0], &[1.0, 5.0]);
1280 assert!(matches!(
1282 spec.validate(),
1283 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping")))
1284 ));
1285 }
1286
1287 #[test]
1288 fn test_single_zone_no_overlap_check() {
1289 let mut spec = make_valid_two_zone_spec();
1291 spec.zones.pop();
1292 spec.zone_buses.clear();
1293 spec.modes[0].zones = vec![0];
1294 assert!(spec.validate().is_ok());
1295 }
1296}