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 for (idx, pair) in zone.entangling_pairs.iter().enumerate() {
518 let [a, b] = *pair;
519 if a as usize >= num_words {
520 errors.push(ArchSpecError::EntanglingPair(format!(
521 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
522 zone_idx, idx, a, num_words
523 )));
524 }
525 if b as usize >= num_words {
526 errors.push(ArchSpecError::EntanglingPair(format!(
527 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
528 zone_idx, idx, b, num_words
529 )));
530 }
531 if a == b {
532 errors.push(ArchSpecError::EntanglingPair(format!(
533 "zone[{}].entangling_pairs[{}]: word paired with itself ({})",
534 zone_idx, idx, a
535 )));
536 }
537 let normalized = if a <= b { [a, b] } else { [b, a] };
538 if !seen.insert(normalized) {
539 errors.push(ArchSpecError::EntanglingPair(format!(
540 "zone[{}].entangling_pairs[{}]: duplicate pair [{}, {}]",
541 zone_idx, idx, a, b
542 )));
543 }
544 }
545}
546
547fn check_modes(
551 spec: &ArchSpec,
552 num_zones: usize,
553 num_words: usize,
554 sites_per_word: usize,
555 errors: &mut Vec<ArchSpecError>,
556) {
557 for (mode_idx, mode) in spec.modes.iter().enumerate() {
558 for &zone_id in &mode.zones {
559 if zone_id as usize >= num_zones {
560 errors.push(ArchSpecError::Mode(format!(
561 "mode '{}' (index {}): zone ID {} >= num_zones ({})",
562 mode.name, mode_idx, zone_id, num_zones
563 )));
564 }
565 }
566 for (loc_idx, loc) in mode.bitstring_order.iter().enumerate() {
567 if loc.zone_id as usize >= num_zones {
568 errors.push(ArchSpecError::Mode(format!(
569 "mode '{}' (index {}): bitstring_order[{}] zone_id {} >= num_zones ({})",
570 mode.name, mode_idx, loc_idx, loc.zone_id, num_zones
571 )));
572 }
573 if loc.word_id as usize >= num_words {
574 errors.push(ArchSpecError::Mode(format!(
575 "mode '{}' (index {}): bitstring_order[{}] word_id {} >= num_words ({})",
576 mode.name, mode_idx, loc_idx, loc.word_id, num_words
577 )));
578 }
579 if loc.site_id as usize >= sites_per_word {
580 errors.push(ArchSpecError::Mode(format!(
581 "mode '{}' (index {}): bitstring_order[{}] site_id {} >= sites_per_word ({})",
582 mode.name, mode_idx, loc_idx, loc.site_id, sites_per_word
583 )));
584 }
585 }
586 }
587}
588
589fn check_paths(spec: &ArchSpec, num_zones: usize, errors: &mut Vec<ArchSpecError>) {
593 if let Some(paths) = &spec.paths {
594 for (idx, path) in paths.iter().enumerate() {
595 if !path.check_finite() {
596 errors.push(ArchSpecError::Path(format!(
597 "paths[{}]: waypoint contains non-finite coordinate",
598 idx
599 )));
600 }
601
602 let lane = super::addr::LaneAddr::decode_u64(path.lane);
604 if lane.zone_id as usize >= num_zones {
605 errors.push(ArchSpecError::Path(format!(
606 "paths[{}]: lane 0x{:016X} has invalid zone_id {} (num_zones={})",
607 idx, path.lane, lane.zone_id, num_zones
608 )));
609 }
610
611 if path.waypoints.len() < 2 {
612 errors.push(ArchSpecError::Path(format!(
613 "paths[{}]: lane 0x{:016X} has {} waypoint(s), minimum is 2",
614 idx,
615 path.lane,
616 path.waypoints.len()
617 )));
618 }
619 }
620 }
621}
622
623fn check_zone_overlap(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
627 let boxes: Vec<(usize, (f64, f64, f64, f64))> = spec
628 .zones
629 .iter()
630 .enumerate()
631 .map(|(i, z)| (i, z.grid.bounding_box()))
632 .collect();
633
634 for i in 0..boxes.len() {
635 for j in (i + 1)..boxes.len() {
636 let (zi, (ax_min, ax_max, ay_min, ay_max)) = boxes[i];
637 let (zj, (bx_min, bx_max, by_min, by_max)) = boxes[j];
638
639 let x_overlap = ax_min < bx_max && bx_min < ax_max;
642 let y_overlap = ay_min < by_max && by_min < ay_max;
643
644 if x_overlap && y_overlap {
645 errors.push(ArchSpecError::Structure(format!(
646 "zone {} and zone {} have overlapping bounding boxes: \
647 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}], \
648 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}]",
649 zi, zj, zi, ax_min, ax_max, ay_min, ay_max, zj, bx_min, bx_max, by_min, by_max,
650 )));
651 }
652 }
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::arch::addr::{SiteRef, WordRef, ZonedWordRef};
660 use crate::arch::types::{Bus, Grid, Mode, Word, Zone};
661 use crate::version::Version;
662
663 fn make_valid_two_zone_spec() -> ArchSpec {
665 let grid0 = Grid::from_positions(&[0.0, 5.0, 10.0], &[0.0, 3.0]);
666 let grid1 = Grid::from_positions(&[20.0, 27.5, 35.0], &[0.0, 4.0]);
668
669 ArchSpec {
670 version: Version::new(2, 0),
671 words: vec![
672 Word {
673 sites: vec![[0, 0], [0, 1]],
674 },
675 Word {
676 sites: vec![[1, 0], [1, 1]],
677 },
678 ],
679 zones: vec![
680 Zone {
681 name: String::new(),
682 grid: grid0,
683 site_buses: vec![Bus {
684 src: vec![SiteRef(0)],
685 dst: vec![SiteRef(1)],
686 }],
687 word_buses: vec![Bus {
688 src: vec![WordRef(0)],
689 dst: vec![WordRef(1)],
690 }],
691 words_with_site_buses: vec![0, 1],
692 sites_with_word_buses: vec![0],
693 entangling_pairs: vec![[0, 1]],
694 },
695 Zone {
696 name: String::new(),
697 grid: grid1,
698 site_buses: vec![],
699 word_buses: vec![],
700 words_with_site_buses: vec![],
701 sites_with_word_buses: vec![],
702 entangling_pairs: vec![],
703 },
704 ],
705 zone_buses: vec![Bus {
706 src: vec![ZonedWordRef {
707 zone_id: 0,
708 word_id: 0,
709 }],
710 dst: vec![ZonedWordRef {
711 zone_id: 1,
712 word_id: 0,
713 }],
714 }],
715 modes: vec![Mode {
716 name: "full".to_string(),
717 zones: vec![0, 1],
718 bitstring_order: vec![],
719 }],
720 paths: None,
721 feed_forward: false,
722 atom_reloading: false,
723 blockade_radius: None,
724 }
725 }
726
727 #[test]
728 fn test_valid_two_zone_spec() {
729 let spec = make_valid_two_zone_spec();
730 assert!(spec.validate().is_ok());
731 }
732
733 #[test]
734 fn test_validate_zones_must_have_same_grid_dimensions() {
735 let mut spec = make_valid_two_zone_spec();
736 spec.zones[1].grid = Grid::from_positions(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0]);
738 assert!(matches!(
739 spec.validate(),
740 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(_)))
741 ));
742 }
743
744 #[test]
745 fn test_validate_site_bus_ref_out_of_range() {
746 let mut spec = make_valid_two_zone_spec();
747 spec.zones[0].site_buses = vec![Bus {
748 src: vec![SiteRef(0)],
749 dst: vec![SiteRef(999)],
750 }];
751 assert!(matches!(
752 spec.validate(),
753 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(_)))
754 ));
755 }
756
757 #[test]
758 fn test_validate_zone_bus_must_cross_zones() {
759 let mut spec = make_valid_two_zone_spec();
760 spec.zone_buses = vec![Bus {
761 src: vec![ZonedWordRef {
762 zone_id: 0,
763 word_id: 0,
764 }],
765 dst: vec![ZonedWordRef {
766 zone_id: 0,
767 word_id: 1,
768 }], }];
770 assert!(matches!(
771 spec.validate(),
772 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(_)))
773 ));
774 }
775
776 fn add_third_word(spec: &mut ArchSpec) {
779 spec.words.push(Word {
780 sites: vec![[2, 0], [2, 1]],
781 });
782 spec.zones[0].words_with_site_buses.push(2);
783 }
784
785 #[test]
786 fn test_validate_overlapping_acyclic_bus_accepted() {
787 let mut spec = make_valid_two_zone_spec();
788 add_third_word(&mut spec);
789 spec.zones[0].word_buses = vec![Bus {
791 src: vec![WordRef(0), WordRef(1)],
792 dst: vec![WordRef(1), WordRef(2)],
793 }];
794 assert!(spec.validate().is_ok());
795 }
796
797 #[test]
798 fn test_validate_cyclic_site_bus_rejected() {
799 let mut spec = make_valid_two_zone_spec();
800 spec.zones[0].site_buses = vec![Bus {
802 src: vec![SiteRef(0), SiteRef(1)],
803 dst: vec![SiteRef(1), SiteRef(0)],
804 }];
805 assert!(matches!(
806 spec.validate(),
807 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
808 ));
809 }
810
811 #[test]
812 fn test_validate_self_loop_site_bus_rejected() {
813 let mut spec = make_valid_two_zone_spec();
814 spec.zones[0].site_buses = vec![Bus {
815 src: vec![SiteRef(0)],
816 dst: vec![SiteRef(0)],
817 }];
818 assert!(matches!(
819 spec.validate(),
820 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
821 ));
822 }
823
824 #[test]
825 fn test_validate_duplicate_bus_src_rejected() {
826 let mut spec = make_valid_two_zone_spec();
827 add_third_word(&mut spec);
828 spec.zones[0].word_buses = vec![Bus {
830 src: vec![WordRef(0), WordRef(0)],
831 dst: vec![WordRef(1), WordRef(2)],
832 }];
833 assert!(matches!(
834 spec.validate(),
835 Err(ref errs) if errs.iter().any(
836 |e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("duplicate src"))
837 )
838 ));
839 }
840
841 #[test]
842 fn test_validate_duplicate_bus_dst_rejected() {
843 let mut spec = make_valid_two_zone_spec();
844 add_third_word(&mut spec);
845 spec.zones[0].word_buses = vec![Bus {
847 src: vec![WordRef(0), WordRef(2)],
848 dst: vec![WordRef(1), WordRef(1)],
849 }];
850 assert!(matches!(
851 spec.validate(),
852 Err(ref errs) if errs.iter().any(
853 |e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("duplicate dst"))
854 )
855 ));
856 }
857
858 #[test]
859 fn test_validate_cyclic_zone_bus_rejected() {
860 let mut spec = make_valid_two_zone_spec();
861 let z0 = ZonedWordRef {
864 zone_id: 0,
865 word_id: 0,
866 };
867 let z1 = ZonedWordRef {
868 zone_id: 1,
869 word_id: 0,
870 };
871 spec.zone_buses = vec![Bus {
872 src: vec![z0, z1],
873 dst: vec![z1, z0],
874 }];
875 assert!(matches!(
876 spec.validate(),
877 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::CyclicBus(_)))
878 ));
879 }
880
881 #[test]
882 fn test_validate_entangling_pair_invalid_word() {
883 let mut spec = make_valid_two_zone_spec();
884 spec.zones[0].entangling_pairs = vec![[0, 99]];
885 assert!(matches!(
886 spec.validate(),
887 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(_)))
888 ));
889 }
890
891 #[test]
892 fn test_validate_mode_invalid_zone() {
893 let mut spec = make_valid_two_zone_spec();
894 spec.modes = vec![Mode {
895 name: "bad".to_string(),
896 zones: vec![99],
897 bitstring_order: vec![],
898 }];
899 assert!(matches!(
900 spec.validate(),
901 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(_)))
902 ));
903 }
904
905 #[test]
906 fn test_validate_no_zones() {
907 let mut spec = make_valid_two_zone_spec();
908 spec.zones = vec![];
909 assert!(matches!(
910 spec.validate(),
911 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("zone")))
912 ));
913 }
914
915 #[test]
916 fn test_validate_no_words() {
917 let mut spec = make_valid_two_zone_spec();
918 spec.words = vec![];
919 assert!(matches!(
920 spec.validate(),
921 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("word")))
922 ));
923 }
924
925 #[test]
926 fn test_validate_word_site_count_mismatch() {
927 let mut spec = make_valid_two_zone_spec();
928 spec.words[1].sites = vec![[0, 0]]; assert!(matches!(
930 spec.validate(),
931 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("sites")))
932 ));
933 }
934
935 #[test]
936 fn test_validate_word_site_x_out_of_range() {
937 let mut spec = make_valid_two_zone_spec();
938 spec.words[0].sites[0] = [99, 0]; assert!(matches!(
940 spec.validate(),
941 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("x index")))
942 ));
943 }
944
945 #[test]
946 fn test_validate_word_site_y_out_of_range() {
947 let mut spec = make_valid_two_zone_spec();
948 spec.words[0].sites[0] = [0, 99]; assert!(matches!(
950 spec.validate(),
951 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("y index")))
952 ));
953 }
954
955 #[test]
956 fn test_validate_zone_words_with_site_buses_invalid() {
957 let mut spec = make_valid_two_zone_spec();
958 spec.zones[0].words_with_site_buses = vec![0, 99];
959 assert!(matches!(
960 spec.validate(),
961 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("words_with_site_buses")))
962 ));
963 }
964
965 #[test]
966 fn test_validate_zone_sites_with_word_buses_invalid() {
967 let mut spec = make_valid_two_zone_spec();
968 spec.zones[0].sites_with_word_buses = vec![99];
969 assert!(matches!(
970 spec.validate(),
971 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("sites_with_word_buses")))
972 ));
973 }
974
975 #[test]
976 fn test_validate_site_bus_length_mismatch() {
977 let mut spec = make_valid_two_zone_spec();
978 spec.zones[0].site_buses = vec![Bus {
979 src: vec![SiteRef(0), SiteRef(1)],
980 dst: vec![SiteRef(0)],
981 }];
982 assert!(matches!(
983 spec.validate(),
984 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("src length")))
985 ));
986 }
987
988 #[test]
989 fn test_validate_word_bus_invalid_word_ref() {
990 let mut spec = make_valid_two_zone_spec();
991 spec.zones[0].word_buses = vec![Bus {
992 src: vec![WordRef(0)],
993 dst: vec![WordRef(99)],
994 }];
995 assert!(matches!(
996 spec.validate(),
997 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("WordRef(99)")))
998 ));
999 }
1000
1001 #[test]
1002 fn test_validate_zone_bus_invalid_zone_id() {
1003 let mut spec = make_valid_two_zone_spec();
1004 spec.zone_buses = vec![Bus {
1005 src: vec![ZonedWordRef {
1006 zone_id: 99,
1007 word_id: 0,
1008 }],
1009 dst: vec![ZonedWordRef {
1010 zone_id: 1,
1011 word_id: 0,
1012 }],
1013 }];
1014 assert!(matches!(
1015 spec.validate(),
1016 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("zone_id 99")))
1017 ));
1018 }
1019
1020 #[test]
1021 fn test_validate_zone_bus_invalid_word_id() {
1022 let mut spec = make_valid_two_zone_spec();
1023 spec.zone_buses = vec![Bus {
1024 src: vec![ZonedWordRef {
1025 zone_id: 0,
1026 word_id: 99,
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("word_id 99")))
1036 ));
1037 }
1038
1039 #[test]
1040 fn test_validate_zone_bus_length_mismatch() {
1041 let mut spec = make_valid_two_zone_spec();
1042 spec.zone_buses = vec![Bus {
1043 src: vec![
1044 ZonedWordRef {
1045 zone_id: 0,
1046 word_id: 0,
1047 },
1048 ZonedWordRef {
1049 zone_id: 0,
1050 word_id: 1,
1051 },
1052 ],
1053 dst: vec![ZonedWordRef {
1054 zone_id: 1,
1055 word_id: 0,
1056 }],
1057 }];
1058 assert!(matches!(
1059 spec.validate(),
1060 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("src length")))
1061 ));
1062 }
1063
1064 #[test]
1065 fn test_validate_entangling_pair_duplicate() {
1066 let mut spec = make_valid_two_zone_spec();
1067 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 0]]; assert!(matches!(
1069 spec.validate(),
1070 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(msg) if msg.contains("duplicate")))
1071 ));
1072 }
1073
1074 #[test]
1075 fn test_validate_mode_bitstring_order_invalid() {
1076 use crate::arch::addr::LocationAddr;
1077 let mut spec = make_valid_two_zone_spec();
1078 spec.modes = vec![Mode {
1079 name: "bad_loc".to_string(),
1080 zones: vec![0],
1081 bitstring_order: vec![LocationAddr {
1082 zone_id: 99,
1083 word_id: 0,
1084 site_id: 0,
1085 }],
1086 }];
1087 assert!(matches!(
1088 spec.validate(),
1089 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(msg) if msg.contains("zone_id 99")))
1090 ));
1091 }
1092
1093 #[test]
1094 fn test_validate_multiple_errors_collected() {
1095 let mut spec = make_valid_two_zone_spec();
1096 spec.zones[0].entangling_pairs = vec![[0, 99]]; spec.zones[0].words_with_site_buses = vec![99]; let errors = spec.validate().unwrap_err();
1100 assert!(
1101 errors.len() >= 2,
1102 "expected at least 2 errors, got {}",
1103 errors.len()
1104 );
1105 }
1106
1107 #[test]
1108 fn test_validate_path_non_finite_waypoint() {
1109 let mut spec = make_valid_two_zone_spec();
1110 let lane = crate::arch::addr::LaneAddr {
1111 direction: crate::arch::addr::Direction::Forward,
1112 move_type: crate::arch::addr::MoveType::SiteBus,
1113 zone_id: 0,
1114 word_id: 0,
1115 site_id: 0,
1116 bus_id: 0,
1117 };
1118 spec.paths = Some(vec![crate::arch::types::TransportPath {
1119 lane: lane.encode_u64(),
1120 waypoints: vec![[f64::NAN, 0.0], [1.0, 2.0]],
1121 }]);
1122 assert!(matches!(
1123 spec.validate(),
1124 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("non-finite")))
1125 ));
1126 }
1127
1128 #[test]
1129 fn test_validate_path_too_few_waypoints() {
1130 let mut spec = make_valid_two_zone_spec();
1131 let lane = crate::arch::addr::LaneAddr {
1132 direction: crate::arch::addr::Direction::Forward,
1133 move_type: crate::arch::addr::MoveType::SiteBus,
1134 zone_id: 0,
1135 word_id: 0,
1136 site_id: 0,
1137 bus_id: 0,
1138 };
1139 spec.paths = Some(vec![crate::arch::types::TransportPath {
1140 lane: lane.encode_u64(),
1141 waypoints: vec![[1.0, 2.0]],
1142 }]);
1143 assert!(matches!(
1144 spec.validate(),
1145 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("minimum is 2")))
1146 ));
1147 }
1148
1149 #[test]
1150 fn test_validate_path_invalid_zone_id() {
1151 let mut spec = make_valid_two_zone_spec();
1152 let lane = crate::arch::addr::LaneAddr {
1153 direction: crate::arch::addr::Direction::Forward,
1154 move_type: crate::arch::addr::MoveType::SiteBus,
1155 zone_id: 99,
1156 word_id: 0,
1157 site_id: 0,
1158 bus_id: 0,
1159 };
1160 spec.paths = Some(vec![crate::arch::types::TransportPath {
1161 lane: lane.encode_u64(),
1162 waypoints: vec![[0.0, 0.0], [1.0, 2.0]],
1163 }]);
1164 assert!(matches!(
1165 spec.validate(),
1166 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("zone_id")))
1167 ));
1168 }
1169
1170 #[test]
1173 fn test_non_overlapping_zones_pass() {
1174 let spec = make_valid_two_zone_spec();
1177 assert!(spec.validate().is_ok());
1178 }
1179
1180 #[test]
1181 fn test_overlapping_zones_rejected() {
1182 let mut spec = make_valid_two_zone_spec();
1183 spec.zones[1].grid = Grid::from_positions(&[5.0, 12.5, 20.0], &[0.0, 4.0]);
1185 assert!(matches!(
1186 spec.validate(),
1187 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping bounding boxes")))
1188 ));
1189 }
1190
1191 #[test]
1192 fn test_adjacent_zones_not_overlapping() {
1193 let mut spec = make_valid_two_zone_spec();
1194 spec.zones[1].grid = Grid::from_positions(&[10.0, 17.5, 25.0], &[0.0, 4.0]);
1196 assert!(spec.validate().is_ok());
1197 }
1198
1199 #[test]
1200 fn test_shared_x_range_with_y_overlap_rejected() {
1201 let mut spec = make_valid_two_zone_spec();
1202 spec.zones[1].grid = Grid::from_positions(&[0.0, 7.5, 15.0], &[1.0, 5.0]);
1204 assert!(matches!(
1206 spec.validate(),
1207 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping")))
1208 ));
1209 }
1210
1211 #[test]
1212 fn test_single_zone_no_overlap_check() {
1213 let mut spec = make_valid_two_zone_spec();
1215 spec.zones.pop();
1216 spec.zone_buses.clear();
1217 spec.modes[0].zones = vec![0];
1218 assert!(spec.validate().is_ok());
1219 }
1220}