1use std::collections::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}")]
32 GridInvariant(String),
33
34 #[error("{0}")]
36 EntanglingPair(String),
37
38 #[error("{0}")]
40 Mode(String),
41
42 #[error("{0}")]
44 Path(String),
45}
46
47impl ArchSpec {
48 pub fn validate(&self) -> Result<(), Vec<ArchSpecError>> {
51 let mut errors = Vec::new();
52
53 let num_words = self.words.len();
54 let num_zones = self.zones.len();
55 let sites_per_word = self.sites_per_word();
56
57 check_minimum_counts(self, &mut errors);
59 check_non_negative_grid_spacings(self, &mut errors);
60 check_uniform_grid_dimensions(self, &mut errors);
61 check_uniform_word_site_counts(self, &mut errors);
62 check_word_site_indices(self, &mut errors);
63
64 for (zone_idx, zone) in self.zones.iter().enumerate() {
66 check_zone_words_with_site_buses(zone_idx, zone, num_words, &mut errors);
67 check_zone_sites_with_word_buses(zone_idx, zone, sites_per_word, &mut errors);
68 check_zone_site_buses(zone_idx, zone, sites_per_word, &mut errors);
69 check_zone_word_buses(zone_idx, zone, num_words, &mut errors);
70 check_zone_entangling_pairs(zone_idx, zone, num_words, &mut errors);
71 }
72
73 check_zone_buses(self, num_zones, num_words, &mut errors);
75
76 check_modes(self, num_zones, num_words, sites_per_word, &mut errors);
78
79 check_paths(self, num_zones, &mut errors);
81
82 check_zone_overlap(self, &mut errors);
84
85 if errors.is_empty() {
86 Ok(())
87 } else {
88 Err(errors)
89 }
90 }
91}
92
93fn check_minimum_counts(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
95 if spec.zones.is_empty() {
96 errors.push(ArchSpecError::Structure(
97 "at least one zone must exist".into(),
98 ));
99 }
100 if spec.words.is_empty() {
101 errors.push(ArchSpecError::Structure(
102 "at least one word must exist".into(),
103 ));
104 }
105}
106
107fn check_non_negative_grid_spacings(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
111 for (idx, zone) in spec.zones.iter().enumerate() {
112 if let Some(pos) = zone.grid.x_spacing.iter().position(|&v| v < 0.0) {
113 errors.push(ArchSpecError::Structure(format!(
114 "zone {} grid x_spacing[{}] is negative ({:.6})",
115 idx, pos, zone.grid.x_spacing[pos]
116 )));
117 }
118 if let Some(pos) = zone.grid.y_spacing.iter().position(|&v| v < 0.0) {
119 errors.push(ArchSpecError::Structure(format!(
120 "zone {} grid y_spacing[{}] is negative ({:.6})",
121 idx, pos, zone.grid.y_spacing[pos]
122 )));
123 }
124 }
125}
126
127fn check_uniform_grid_dimensions(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
129 if let Some(first_zone) = spec.zones.first() {
130 let ref_x = first_zone.grid.num_x();
131 let ref_y = first_zone.grid.num_y();
132 for (idx, zone) in spec.zones.iter().enumerate().skip(1) {
133 let zx = zone.grid.num_x();
134 let zy = zone.grid.num_y();
135 if zx != ref_x || zy != ref_y {
136 errors.push(ArchSpecError::Structure(format!(
137 "zone {} grid dimensions ({}x{}) differ from zone 0 ({}x{})",
138 idx, zx, zy, ref_x, ref_y
139 )));
140 }
141 }
142 }
143}
144
145fn check_uniform_word_site_counts(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
147 if let Some(first_word) = spec.words.first() {
148 let ref_count = first_word.sites.len();
149 for (idx, word) in spec.words.iter().enumerate().skip(1) {
150 if word.sites.len() != ref_count {
151 errors.push(ArchSpecError::Structure(format!(
152 "word {} has {} sites, expected {} (same as word 0)",
153 idx,
154 word.sites.len(),
155 ref_count
156 )));
157 }
158 }
159 }
160}
161
162fn check_word_site_indices(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
165 let (grid_x, grid_y) = match spec.zones.first() {
167 Some(z) => (z.grid.num_x(), z.grid.num_y()),
168 None => return, };
170
171 for (word_idx, word) in spec.words.iter().enumerate() {
172 for (site_idx, site) in word.sites.iter().enumerate() {
173 let x = site[0] as usize;
174 let y = site[1] as usize;
175 if x >= grid_x {
176 errors.push(ArchSpecError::Structure(format!(
177 "word {}, site {}: x index {} out of range (grid has {} x-positions)",
178 word_idx, site_idx, site[0], grid_x
179 )));
180 }
181 if y >= grid_y {
182 errors.push(ArchSpecError::Structure(format!(
183 "word {}, site {}: y index {} out of range (grid has {} y-positions)",
184 word_idx, site_idx, site[1], grid_y
185 )));
186 }
187 }
188 }
189}
190
191use super::types::Zone;
194
195fn check_zone_words_with_site_buses(
197 zone_idx: usize,
198 zone: &Zone,
199 num_words: usize,
200 errors: &mut Vec<ArchSpecError>,
201) {
202 for &wid in &zone.words_with_site_buses {
203 if wid as usize >= num_words {
204 errors.push(ArchSpecError::ZoneBus(format!(
205 "zone {}: words_with_site_buses contains invalid word ID {}",
206 zone_idx, wid
207 )));
208 }
209 }
210}
211
212fn check_zone_sites_with_word_buses(
214 zone_idx: usize,
215 zone: &Zone,
216 sites_per_word: usize,
217 errors: &mut Vec<ArchSpecError>,
218) {
219 for &sid in &zone.sites_with_word_buses {
220 if sid as usize >= sites_per_word {
221 errors.push(ArchSpecError::ZoneBus(format!(
222 "zone {}: sites_with_word_buses contains invalid site index {} (sites_per_word={})",
223 zone_idx, sid, sites_per_word
224 )));
225 }
226 }
227}
228
229fn check_zone_site_buses(
231 zone_idx: usize,
232 zone: &Zone,
233 sites_per_word: usize,
234 errors: &mut Vec<ArchSpecError>,
235) {
236 for (bus_idx, bus) in zone.site_buses.iter().enumerate() {
237 if bus.src.len() != bus.dst.len() {
238 errors.push(ArchSpecError::ZoneBus(format!(
239 "zone {}, site_bus {}: src length ({}) != dst length ({})",
240 zone_idx,
241 bus_idx,
242 bus.src.len(),
243 bus.dst.len()
244 )));
245 }
246 for (i, sref) in bus.src.iter().enumerate() {
247 if sref.0 as usize >= sites_per_word {
248 errors.push(ArchSpecError::ZoneBus(format!(
249 "zone {}, site_bus {}: src[{}] SiteRef({}) >= sites_per_word ({})",
250 zone_idx, bus_idx, i, sref.0, sites_per_word
251 )));
252 }
253 }
254 for (i, sref) in bus.dst.iter().enumerate() {
255 if sref.0 as usize >= sites_per_word {
256 errors.push(ArchSpecError::ZoneBus(format!(
257 "zone {}, site_bus {}: dst[{}] SiteRef({}) >= sites_per_word ({})",
258 zone_idx, bus_idx, i, sref.0, sites_per_word
259 )));
260 }
261 }
262 }
263}
264
265fn check_zone_word_buses(
267 zone_idx: usize,
268 zone: &Zone,
269 num_words: usize,
270 errors: &mut Vec<ArchSpecError>,
271) {
272 for (bus_idx, bus) in zone.word_buses.iter().enumerate() {
273 if bus.src.len() != bus.dst.len() {
274 errors.push(ArchSpecError::ZoneBus(format!(
275 "zone {}, word_bus {}: src length ({}) != dst length ({})",
276 zone_idx,
277 bus_idx,
278 bus.src.len(),
279 bus.dst.len()
280 )));
281 }
282 for (i, wref) in bus.src.iter().enumerate() {
283 if wref.0 as usize >= num_words {
284 errors.push(ArchSpecError::ZoneBus(format!(
285 "zone {}, word_bus {}: src[{}] WordRef({}) >= num_words ({})",
286 zone_idx, bus_idx, i, wref.0, num_words
287 )));
288 }
289 }
290 for (i, wref) in bus.dst.iter().enumerate() {
291 if wref.0 as usize >= num_words {
292 errors.push(ArchSpecError::ZoneBus(format!(
293 "zone {}, word_bus {}: dst[{}] WordRef({}) >= num_words ({})",
294 zone_idx, bus_idx, i, wref.0, num_words
295 )));
296 }
297 }
298 }
299}
300
301fn check_zone_buses(
306 spec: &ArchSpec,
307 num_zones: usize,
308 num_words: usize,
309 errors: &mut Vec<ArchSpecError>,
310) {
311 for (bus_idx, bus) in spec.zone_buses.iter().enumerate() {
312 if bus.src.len() != bus.dst.len() {
313 errors.push(ArchSpecError::InterZoneBus(format!(
314 "zone_bus {}: src length ({}) != dst length ({})",
315 bus_idx,
316 bus.src.len(),
317 bus.dst.len()
318 )));
319 }
320
321 for (i, zwr) in bus.src.iter().enumerate() {
323 if zwr.zone_id as usize >= num_zones {
324 errors.push(ArchSpecError::InterZoneBus(format!(
325 "zone_bus {}: src[{}] zone_id {} >= num_zones ({})",
326 bus_idx, i, zwr.zone_id, num_zones
327 )));
328 }
329 if zwr.word_id as usize >= num_words {
330 errors.push(ArchSpecError::InterZoneBus(format!(
331 "zone_bus {}: src[{}] word_id {} >= num_words ({})",
332 bus_idx, i, zwr.word_id, num_words
333 )));
334 }
335 }
336 for (i, zwr) in bus.dst.iter().enumerate() {
337 if zwr.zone_id as usize >= num_zones {
338 errors.push(ArchSpecError::InterZoneBus(format!(
339 "zone_bus {}: dst[{}] zone_id {} >= num_zones ({})",
340 bus_idx, i, zwr.zone_id, num_zones
341 )));
342 }
343 if zwr.word_id as usize >= num_words {
344 errors.push(ArchSpecError::InterZoneBus(format!(
345 "zone_bus {}: dst[{}] word_id {} >= num_words ({})",
346 bus_idx, i, zwr.word_id, num_words
347 )));
348 }
349 }
350
351 let pair_count = bus.src.len().min(bus.dst.len());
353 for i in 0..pair_count {
354 if bus.src[i].zone_id == bus.dst[i].zone_id {
355 errors.push(ArchSpecError::InterZoneBus(format!(
356 "zone_bus {}: pair {} does not cross a zone boundary \
357 (src zone_id={}, dst zone_id={})",
358 bus_idx, i, bus.src[i].zone_id, bus.dst[i].zone_id
359 )));
360 }
361 }
362 }
363}
364
365fn check_zone_entangling_pairs(
369 zone_idx: usize,
370 zone: &super::types::Zone,
371 num_words: usize,
372 errors: &mut Vec<ArchSpecError>,
373) {
374 let mut seen: HashSet<[u32; 2]> = HashSet::new();
375 for (idx, pair) in zone.entangling_pairs.iter().enumerate() {
376 let [a, b] = *pair;
377 if a as usize >= num_words {
378 errors.push(ArchSpecError::EntanglingPair(format!(
379 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
380 zone_idx, idx, a, num_words
381 )));
382 }
383 if b as usize >= num_words {
384 errors.push(ArchSpecError::EntanglingPair(format!(
385 "zone[{}].entangling_pairs[{}]: word ID {} >= num_words ({})",
386 zone_idx, idx, b, num_words
387 )));
388 }
389 if a == b {
390 errors.push(ArchSpecError::EntanglingPair(format!(
391 "zone[{}].entangling_pairs[{}]: word paired with itself ({})",
392 zone_idx, idx, a
393 )));
394 }
395 let normalized = if a <= b { [a, b] } else { [b, a] };
396 if !seen.insert(normalized) {
397 errors.push(ArchSpecError::EntanglingPair(format!(
398 "zone[{}].entangling_pairs[{}]: duplicate pair [{}, {}]",
399 zone_idx, idx, a, b
400 )));
401 }
402 }
403}
404
405fn check_modes(
409 spec: &ArchSpec,
410 num_zones: usize,
411 num_words: usize,
412 sites_per_word: usize,
413 errors: &mut Vec<ArchSpecError>,
414) {
415 for (mode_idx, mode) in spec.modes.iter().enumerate() {
416 for &zone_id in &mode.zones {
417 if zone_id as usize >= num_zones {
418 errors.push(ArchSpecError::Mode(format!(
419 "mode '{}' (index {}): zone ID {} >= num_zones ({})",
420 mode.name, mode_idx, zone_id, num_zones
421 )));
422 }
423 }
424 for (loc_idx, loc) in mode.bitstring_order.iter().enumerate() {
425 if loc.zone_id as usize >= num_zones {
426 errors.push(ArchSpecError::Mode(format!(
427 "mode '{}' (index {}): bitstring_order[{}] zone_id {} >= num_zones ({})",
428 mode.name, mode_idx, loc_idx, loc.zone_id, num_zones
429 )));
430 }
431 if loc.word_id as usize >= num_words {
432 errors.push(ArchSpecError::Mode(format!(
433 "mode '{}' (index {}): bitstring_order[{}] word_id {} >= num_words ({})",
434 mode.name, mode_idx, loc_idx, loc.word_id, num_words
435 )));
436 }
437 if loc.site_id as usize >= sites_per_word {
438 errors.push(ArchSpecError::Mode(format!(
439 "mode '{}' (index {}): bitstring_order[{}] site_id {} >= sites_per_word ({})",
440 mode.name, mode_idx, loc_idx, loc.site_id, sites_per_word
441 )));
442 }
443 }
444 }
445}
446
447fn check_paths(spec: &ArchSpec, num_zones: usize, errors: &mut Vec<ArchSpecError>) {
451 if let Some(paths) = &spec.paths {
452 for (idx, path) in paths.iter().enumerate() {
453 if !path.check_finite() {
454 errors.push(ArchSpecError::Path(format!(
455 "paths[{}]: waypoint contains non-finite coordinate",
456 idx
457 )));
458 }
459
460 let lane = super::addr::LaneAddr::decode_u64(path.lane);
462 if lane.zone_id as usize >= num_zones {
463 errors.push(ArchSpecError::Path(format!(
464 "paths[{}]: lane 0x{:016X} has invalid zone_id {} (num_zones={})",
465 idx, path.lane, lane.zone_id, num_zones
466 )));
467 }
468
469 if path.waypoints.len() < 2 {
470 errors.push(ArchSpecError::Path(format!(
471 "paths[{}]: lane 0x{:016X} has {} waypoint(s), minimum is 2",
472 idx,
473 path.lane,
474 path.waypoints.len()
475 )));
476 }
477 }
478 }
479}
480
481fn check_zone_overlap(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
485 let boxes: Vec<(usize, (f64, f64, f64, f64))> = spec
486 .zones
487 .iter()
488 .enumerate()
489 .map(|(i, z)| (i, z.grid.bounding_box()))
490 .collect();
491
492 for i in 0..boxes.len() {
493 for j in (i + 1)..boxes.len() {
494 let (zi, (ax_min, ax_max, ay_min, ay_max)) = boxes[i];
495 let (zj, (bx_min, bx_max, by_min, by_max)) = boxes[j];
496
497 let x_overlap = ax_min < bx_max && bx_min < ax_max;
500 let y_overlap = ay_min < by_max && by_min < ay_max;
501
502 if x_overlap && y_overlap {
503 errors.push(ArchSpecError::Structure(format!(
504 "zone {} and zone {} have overlapping bounding boxes: \
505 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}], \
506 zone {} spans x=[{:.6}, {:.6}] y=[{:.6}, {:.6}]",
507 zi, zj, zi, ax_min, ax_max, ay_min, ay_max, zj, bx_min, bx_max, by_min, by_max,
508 )));
509 }
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::arch::addr::{SiteRef, WordRef, ZonedWordRef};
518 use crate::arch::types::{Bus, Grid, Mode, Word, Zone};
519 use crate::version::Version;
520
521 fn make_valid_two_zone_spec() -> ArchSpec {
523 let grid0 = Grid::from_positions(&[0.0, 5.0, 10.0], &[0.0, 3.0]);
524 let grid1 = Grid::from_positions(&[20.0, 27.5, 35.0], &[0.0, 4.0]);
526
527 ArchSpec {
528 version: Version::new(2, 0),
529 words: vec![
530 Word {
531 sites: vec![[0, 0], [0, 1]],
532 },
533 Word {
534 sites: vec![[1, 0], [1, 1]],
535 },
536 ],
537 zones: vec![
538 Zone {
539 name: String::new(),
540 grid: grid0,
541 site_buses: vec![Bus {
542 src: vec![SiteRef(0)],
543 dst: vec![SiteRef(1)],
544 }],
545 word_buses: vec![Bus {
546 src: vec![WordRef(0)],
547 dst: vec![WordRef(1)],
548 }],
549 words_with_site_buses: vec![0, 1],
550 sites_with_word_buses: vec![0],
551 entangling_pairs: vec![[0, 1]],
552 },
553 Zone {
554 name: String::new(),
555 grid: grid1,
556 site_buses: vec![],
557 word_buses: vec![],
558 words_with_site_buses: vec![],
559 sites_with_word_buses: vec![],
560 entangling_pairs: vec![],
561 },
562 ],
563 zone_buses: vec![Bus {
564 src: vec![ZonedWordRef {
565 zone_id: 0,
566 word_id: 0,
567 }],
568 dst: vec![ZonedWordRef {
569 zone_id: 1,
570 word_id: 0,
571 }],
572 }],
573 modes: vec![Mode {
574 name: "full".to_string(),
575 zones: vec![0, 1],
576 bitstring_order: vec![],
577 }],
578 paths: None,
579 feed_forward: false,
580 atom_reloading: false,
581 blockade_radius: None,
582 }
583 }
584
585 #[test]
586 fn test_valid_two_zone_spec() {
587 let spec = make_valid_two_zone_spec();
588 assert!(spec.validate().is_ok());
589 }
590
591 #[test]
592 fn test_validate_zones_must_have_same_grid_dimensions() {
593 let mut spec = make_valid_two_zone_spec();
594 spec.zones[1].grid = Grid::from_positions(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0]);
596 assert!(matches!(
597 spec.validate(),
598 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(_)))
599 ));
600 }
601
602 #[test]
603 fn test_validate_site_bus_ref_out_of_range() {
604 let mut spec = make_valid_two_zone_spec();
605 spec.zones[0].site_buses = vec![Bus {
606 src: vec![SiteRef(0)],
607 dst: vec![SiteRef(999)],
608 }];
609 assert!(matches!(
610 spec.validate(),
611 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(_)))
612 ));
613 }
614
615 #[test]
616 fn test_validate_zone_bus_must_cross_zones() {
617 let mut spec = make_valid_two_zone_spec();
618 spec.zone_buses = vec![Bus {
619 src: vec![ZonedWordRef {
620 zone_id: 0,
621 word_id: 0,
622 }],
623 dst: vec![ZonedWordRef {
624 zone_id: 0,
625 word_id: 1,
626 }], }];
628 assert!(matches!(
629 spec.validate(),
630 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(_)))
631 ));
632 }
633
634 #[test]
635 fn test_validate_entangling_pair_invalid_word() {
636 let mut spec = make_valid_two_zone_spec();
637 spec.zones[0].entangling_pairs = vec![[0, 99]];
638 assert!(matches!(
639 spec.validate(),
640 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(_)))
641 ));
642 }
643
644 #[test]
645 fn test_validate_mode_invalid_zone() {
646 let mut spec = make_valid_two_zone_spec();
647 spec.modes = vec![Mode {
648 name: "bad".to_string(),
649 zones: vec![99],
650 bitstring_order: vec![],
651 }];
652 assert!(matches!(
653 spec.validate(),
654 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(_)))
655 ));
656 }
657
658 #[test]
659 fn test_validate_no_zones() {
660 let mut spec = make_valid_two_zone_spec();
661 spec.zones = vec![];
662 assert!(matches!(
663 spec.validate(),
664 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("zone")))
665 ));
666 }
667
668 #[test]
669 fn test_validate_no_words() {
670 let mut spec = make_valid_two_zone_spec();
671 spec.words = vec![];
672 assert!(matches!(
673 spec.validate(),
674 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("word")))
675 ));
676 }
677
678 #[test]
679 fn test_validate_word_site_count_mismatch() {
680 let mut spec = make_valid_two_zone_spec();
681 spec.words[1].sites = vec![[0, 0]]; assert!(matches!(
683 spec.validate(),
684 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("sites")))
685 ));
686 }
687
688 #[test]
689 fn test_validate_word_site_x_out_of_range() {
690 let mut spec = make_valid_two_zone_spec();
691 spec.words[0].sites[0] = [99, 0]; assert!(matches!(
693 spec.validate(),
694 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("x index")))
695 ));
696 }
697
698 #[test]
699 fn test_validate_word_site_y_out_of_range() {
700 let mut spec = make_valid_two_zone_spec();
701 spec.words[0].sites[0] = [0, 99]; assert!(matches!(
703 spec.validate(),
704 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("y index")))
705 ));
706 }
707
708 #[test]
709 fn test_validate_zone_words_with_site_buses_invalid() {
710 let mut spec = make_valid_two_zone_spec();
711 spec.zones[0].words_with_site_buses = vec![0, 99];
712 assert!(matches!(
713 spec.validate(),
714 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("words_with_site_buses")))
715 ));
716 }
717
718 #[test]
719 fn test_validate_zone_sites_with_word_buses_invalid() {
720 let mut spec = make_valid_two_zone_spec();
721 spec.zones[0].sites_with_word_buses = vec![99];
722 assert!(matches!(
723 spec.validate(),
724 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("sites_with_word_buses")))
725 ));
726 }
727
728 #[test]
729 fn test_validate_site_bus_length_mismatch() {
730 let mut spec = make_valid_two_zone_spec();
731 spec.zones[0].site_buses = vec![Bus {
732 src: vec![SiteRef(0), SiteRef(1)],
733 dst: vec![SiteRef(0)],
734 }];
735 assert!(matches!(
736 spec.validate(),
737 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("src length")))
738 ));
739 }
740
741 #[test]
742 fn test_validate_word_bus_invalid_word_ref() {
743 let mut spec = make_valid_two_zone_spec();
744 spec.zones[0].word_buses = vec![Bus {
745 src: vec![WordRef(0)],
746 dst: vec![WordRef(99)],
747 }];
748 assert!(matches!(
749 spec.validate(),
750 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::ZoneBus(msg) if msg.contains("WordRef(99)")))
751 ));
752 }
753
754 #[test]
755 fn test_validate_zone_bus_invalid_zone_id() {
756 let mut spec = make_valid_two_zone_spec();
757 spec.zone_buses = vec![Bus {
758 src: vec![ZonedWordRef {
759 zone_id: 99,
760 word_id: 0,
761 }],
762 dst: vec![ZonedWordRef {
763 zone_id: 1,
764 word_id: 0,
765 }],
766 }];
767 assert!(matches!(
768 spec.validate(),
769 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("zone_id 99")))
770 ));
771 }
772
773 #[test]
774 fn test_validate_zone_bus_invalid_word_id() {
775 let mut spec = make_valid_two_zone_spec();
776 spec.zone_buses = vec![Bus {
777 src: vec![ZonedWordRef {
778 zone_id: 0,
779 word_id: 99,
780 }],
781 dst: vec![ZonedWordRef {
782 zone_id: 1,
783 word_id: 0,
784 }],
785 }];
786 assert!(matches!(
787 spec.validate(),
788 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("word_id 99")))
789 ));
790 }
791
792 #[test]
793 fn test_validate_zone_bus_length_mismatch() {
794 let mut spec = make_valid_two_zone_spec();
795 spec.zone_buses = vec![Bus {
796 src: vec![
797 ZonedWordRef {
798 zone_id: 0,
799 word_id: 0,
800 },
801 ZonedWordRef {
802 zone_id: 0,
803 word_id: 1,
804 },
805 ],
806 dst: vec![ZonedWordRef {
807 zone_id: 1,
808 word_id: 0,
809 }],
810 }];
811 assert!(matches!(
812 spec.validate(),
813 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::InterZoneBus(msg) if msg.contains("src length")))
814 ));
815 }
816
817 #[test]
818 fn test_validate_entangling_pair_duplicate() {
819 let mut spec = make_valid_two_zone_spec();
820 spec.zones[0].entangling_pairs = vec![[0, 1], [1, 0]]; assert!(matches!(
822 spec.validate(),
823 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::EntanglingPair(msg) if msg.contains("duplicate")))
824 ));
825 }
826
827 #[test]
828 fn test_validate_mode_bitstring_order_invalid() {
829 use crate::arch::addr::LocationAddr;
830 let mut spec = make_valid_two_zone_spec();
831 spec.modes = vec![Mode {
832 name: "bad_loc".to_string(),
833 zones: vec![0],
834 bitstring_order: vec![LocationAddr {
835 zone_id: 99,
836 word_id: 0,
837 site_id: 0,
838 }],
839 }];
840 assert!(matches!(
841 spec.validate(),
842 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Mode(msg) if msg.contains("zone_id 99")))
843 ));
844 }
845
846 #[test]
847 fn test_validate_multiple_errors_collected() {
848 let mut spec = make_valid_two_zone_spec();
849 spec.zones[0].entangling_pairs = vec![[0, 99]]; spec.zones[0].words_with_site_buses = vec![99]; let errors = spec.validate().unwrap_err();
853 assert!(
854 errors.len() >= 2,
855 "expected at least 2 errors, got {}",
856 errors.len()
857 );
858 }
859
860 #[test]
861 fn test_validate_path_non_finite_waypoint() {
862 let mut spec = make_valid_two_zone_spec();
863 let lane = crate::arch::addr::LaneAddr {
864 direction: crate::arch::addr::Direction::Forward,
865 move_type: crate::arch::addr::MoveType::SiteBus,
866 zone_id: 0,
867 word_id: 0,
868 site_id: 0,
869 bus_id: 0,
870 };
871 spec.paths = Some(vec![crate::arch::types::TransportPath {
872 lane: lane.encode_u64(),
873 waypoints: vec![[f64::NAN, 0.0], [1.0, 2.0]],
874 }]);
875 assert!(matches!(
876 spec.validate(),
877 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("non-finite")))
878 ));
879 }
880
881 #[test]
882 fn test_validate_path_too_few_waypoints() {
883 let mut spec = make_valid_two_zone_spec();
884 let lane = crate::arch::addr::LaneAddr {
885 direction: crate::arch::addr::Direction::Forward,
886 move_type: crate::arch::addr::MoveType::SiteBus,
887 zone_id: 0,
888 word_id: 0,
889 site_id: 0,
890 bus_id: 0,
891 };
892 spec.paths = Some(vec![crate::arch::types::TransportPath {
893 lane: lane.encode_u64(),
894 waypoints: vec![[1.0, 2.0]],
895 }]);
896 assert!(matches!(
897 spec.validate(),
898 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("minimum is 2")))
899 ));
900 }
901
902 #[test]
903 fn test_validate_path_invalid_zone_id() {
904 let mut spec = make_valid_two_zone_spec();
905 let lane = crate::arch::addr::LaneAddr {
906 direction: crate::arch::addr::Direction::Forward,
907 move_type: crate::arch::addr::MoveType::SiteBus,
908 zone_id: 99,
909 word_id: 0,
910 site_id: 0,
911 bus_id: 0,
912 };
913 spec.paths = Some(vec![crate::arch::types::TransportPath {
914 lane: lane.encode_u64(),
915 waypoints: vec![[0.0, 0.0], [1.0, 2.0]],
916 }]);
917 assert!(matches!(
918 spec.validate(),
919 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Path(msg) if msg.contains("zone_id")))
920 ));
921 }
922
923 #[test]
926 fn test_non_overlapping_zones_pass() {
927 let spec = make_valid_two_zone_spec();
930 assert!(spec.validate().is_ok());
931 }
932
933 #[test]
934 fn test_overlapping_zones_rejected() {
935 let mut spec = make_valid_two_zone_spec();
936 spec.zones[1].grid = Grid::from_positions(&[5.0, 12.5, 20.0], &[0.0, 4.0]);
938 assert!(matches!(
939 spec.validate(),
940 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping bounding boxes")))
941 ));
942 }
943
944 #[test]
945 fn test_adjacent_zones_not_overlapping() {
946 let mut spec = make_valid_two_zone_spec();
947 spec.zones[1].grid = Grid::from_positions(&[10.0, 17.5, 25.0], &[0.0, 4.0]);
949 assert!(spec.validate().is_ok());
950 }
951
952 #[test]
953 fn test_shared_x_range_with_y_overlap_rejected() {
954 let mut spec = make_valid_two_zone_spec();
955 spec.zones[1].grid = Grid::from_positions(&[0.0, 7.5, 15.0], &[1.0, 5.0]);
957 assert!(matches!(
959 spec.validate(),
960 Err(ref errs) if errs.iter().any(|e| matches!(e, ArchSpecError::Structure(msg) if msg.contains("overlapping")))
961 ));
962 }
963
964 #[test]
965 fn test_single_zone_no_overlap_check() {
966 let mut spec = make_valid_two_zone_spec();
968 spec.zones.pop();
969 spec.zone_buses.clear();
970 spec.modes[0].zones = vec![0];
971 assert!(spec.validate().is_ok());
972 }
973}