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("{message}")]
20 Zone { message: String },
21
22 #[error("{message}")]
24 Geometry { message: String },
25
26 #[error("{message}")]
28 Bus { message: String },
29
30 #[error("{message}")]
32 Path { message: String },
33}
34
35impl ArchSpec {
36 pub fn validate(&self) -> Result<(), Vec<ArchSpecError>> {
39 let mut errors = Vec::new();
40
41 let num_words = self.geometry.words.len() as u32;
42 let num_zones = self.zones.len() as u32;
43 let sites_per_word = self.geometry.sites_per_word;
44
45 check_zone0_includes_all_words(self, num_words, &mut errors);
47
48 check_measurement_mode_first_is_zone0(self, &mut errors);
50
51 check_entangling_zones_valid(self, num_zones, &mut errors);
53
54 check_measurement_mode_zones_valid(self, num_zones, &mut errors);
56
57 check_word_sites(self, &mut errors);
59
60 check_site_buses(self, sites_per_word, &mut errors);
62
63 check_word_buses(self, num_words, &mut errors);
65
66 check_consistent_grid_shape(self, &mut errors);
68
69 check_words_with_site_buses(self, num_words, &mut errors);
71
72 check_sites_with_word_buses(self, sites_per_word, &mut errors);
74
75 check_path_lanes(self, &mut errors);
77
78 if errors.is_empty() {
79 Ok(())
80 } else {
81 Err(errors)
82 }
83 }
84}
85
86fn check_zone0_includes_all_words(
87 spec: &ArchSpec,
88 num_words: u32,
89 errors: &mut Vec<ArchSpecError>,
90) {
91 if let Some(zone0) = spec.zones.first() {
92 let zone0_words: HashSet<u32> = zone0.words.iter().copied().collect();
93 let all_word_ids: HashSet<u32> = (0..num_words).collect();
94 let mut missing: Vec<u32> = all_word_ids.difference(&zone0_words).copied().collect();
95 missing.sort_unstable();
96 if !missing.is_empty() {
97 errors.push(ArchSpecError::Zone {
98 message: format!(
99 "zone 0 must include all words: missing word IDs {:?}",
100 missing
101 ),
102 });
103 }
104 }
105}
106
107fn check_measurement_mode_first_is_zone0(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
108 if spec.measurement_mode_zones.is_empty() {
109 errors.push(ArchSpecError::Zone {
110 message: "measurement_mode_zones must not be empty".into(),
111 });
112 return;
113 }
114 if spec.measurement_mode_zones[0] != 0 {
115 errors.push(ArchSpecError::Zone {
116 message: format!(
117 "measurement_mode_zones[0] must be zone 0, got {}",
118 spec.measurement_mode_zones[0]
119 ),
120 });
121 }
122}
123
124fn check_entangling_zones_valid(spec: &ArchSpec, num_zones: u32, errors: &mut Vec<ArchSpecError>) {
125 for &id in &spec.entangling_zones {
126 if id >= num_zones {
127 errors.push(ArchSpecError::Zone {
128 message: format!("entangling_zones contains invalid zone ID {}", id),
129 });
130 }
131 }
132}
133
134fn check_measurement_mode_zones_valid(
135 spec: &ArchSpec,
136 num_zones: u32,
137 errors: &mut Vec<ArchSpecError>,
138) {
139 for &id in &spec.measurement_mode_zones {
140 if id >= num_zones {
141 errors.push(ArchSpecError::Zone {
142 message: format!("measurement_mode_zones contains invalid zone ID {}", id),
143 });
144 }
145 }
146}
147
148fn check_word_sites(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
149 let sites_per_word = spec.geometry.sites_per_word;
150 for (word_id, word) in spec.geometry.words.iter().enumerate() {
151 let word_id = word_id as u32;
152 if let Err(field) = word.positions.check_finite() {
153 errors.push(ArchSpecError::Geometry {
154 message: format!(
155 "word {} grid contains non-finite value in {}",
156 word_id, field
157 ),
158 });
159 }
160 if word.site_indices.len() != sites_per_word as usize {
161 errors.push(ArchSpecError::Geometry {
162 message: format!(
163 "word {} has {} sites, expected {} (sites_per_word)",
164 word_id,
165 word.site_indices.len(),
166 sites_per_word
167 ),
168 });
169 }
170 if let Some(cz) = &word.has_cz
171 && cz.len() != sites_per_word as usize
172 {
173 errors.push(ArchSpecError::Geometry {
174 message: format!(
175 "word {} has {} cz_pairs, expected {} (sites_per_word)",
176 word_id,
177 cz.len(),
178 sites_per_word
179 ),
180 });
181 }
182 let x_len = word.positions.num_x();
183 let y_len = word.positions.num_y();
184 for (site_idx, site) in word.site_indices.iter().enumerate() {
185 let x_idx = site[0];
186 let y_idx = site[1];
187 if x_idx as usize >= x_len {
188 errors.push(ArchSpecError::Geometry {
189 message: format!(
190 "word {}, site {}: x_idx {} out of range (grid has num_x={})",
191 word_id, site_idx, x_idx, x_len
192 ),
193 });
194 }
195 if y_idx as usize >= y_len {
196 errors.push(ArchSpecError::Geometry {
197 message: format!(
198 "word {}, site {}: y_idx {} out of range (grid has num_y={})",
199 word_id, site_idx, y_idx, y_len
200 ),
201 });
202 }
203 }
204 }
205}
206
207fn check_site_buses(spec: &ArchSpec, sites_per_word: u32, errors: &mut Vec<ArchSpecError>) {
208 for (bus_id, bus) in spec.buses.site_buses.iter().enumerate() {
209 let bus_id = bus_id as u32;
210 if bus.src.len() != bus.dst.len() {
211 errors.push(ArchSpecError::Bus {
212 message: format!(
213 "site_bus {}: src length ({}) != dst length ({})",
214 bus_id,
215 bus.src.len(),
216 bus.dst.len()
217 ),
218 });
219 }
220 for &idx in bus.src.iter().chain(bus.dst.iter()) {
221 if idx >= sites_per_word {
222 errors.push(ArchSpecError::Bus {
223 message: format!(
224 "site_bus {}: site index {} >= sites_per_word ({})",
225 bus_id, idx, sites_per_word
226 ),
227 });
228 }
229 }
230 let src_set: HashSet<u32> = bus.src.iter().copied().collect();
231 for &idx in &bus.dst {
232 if src_set.contains(&idx) {
233 errors.push(ArchSpecError::Bus {
234 message: format!(
235 "site_bus {}: src and dst overlap at site index {}",
236 bus_id, idx
237 ),
238 });
239 }
240 }
241 }
242}
243
244fn check_word_buses(spec: &ArchSpec, num_words: u32, errors: &mut Vec<ArchSpecError>) {
245 for (bus_id, bus) in spec.buses.word_buses.iter().enumerate() {
246 let bus_id = bus_id as u32;
247 if bus.src.len() != bus.dst.len() {
248 errors.push(ArchSpecError::Bus {
249 message: format!(
250 "word_bus {}: src length ({}) != dst length ({})",
251 bus_id,
252 bus.src.len(),
253 bus.dst.len()
254 ),
255 });
256 }
257 for &wid in bus.src.iter().chain(bus.dst.iter()) {
258 if wid >= num_words {
259 errors.push(ArchSpecError::Bus {
260 message: format!("word_bus {}: invalid word ID {}", bus_id, wid),
261 });
262 }
263 }
264 }
265}
266
267fn check_words_with_site_buses(spec: &ArchSpec, num_words: u32, errors: &mut Vec<ArchSpecError>) {
268 for &wid in &spec.words_with_site_buses {
269 if wid >= num_words {
270 errors.push(ArchSpecError::Bus {
271 message: format!("words_with_site_buses: invalid word ID {}", wid),
272 });
273 }
274 }
275}
276
277fn check_consistent_grid_shape(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
278 if let Some(first) = spec.geometry.words.first() {
279 let ref_x_len = first.positions.num_x();
280 let ref_y_len = first.positions.num_y();
281 for (idx, word) in spec.geometry.words.iter().enumerate().skip(1) {
282 let x_len = word.positions.num_x();
283 let y_len = word.positions.num_y();
284 if x_len != ref_x_len || y_len != ref_y_len {
285 errors.push(ArchSpecError::Geometry {
286 message: format!(
287 "word {} grid shape ({}x{}) differs from word 0 ({}x{})",
288 idx, x_len, y_len, ref_x_len, ref_y_len
289 ),
290 });
291 }
292 }
293 }
294}
295
296fn check_path_lanes(spec: &ArchSpec, errors: &mut Vec<ArchSpecError>) {
297 if let Some(paths) = &spec.paths {
298 for (index, path) in paths.iter().enumerate() {
299 if !path.check_finite() {
300 errors.push(ArchSpecError::Path {
301 message: format!("paths[{}]: waypoint contains non-finite coordinate", index),
302 });
303 }
304 let lane = crate::arch::addr::LaneAddr::decode_u64(path.lane);
305 let lane_errors = spec.check_lane(&lane);
306 for message in lane_errors {
307 errors.push(ArchSpecError::Path {
308 message: format!(
309 "paths[{}]: lane 0x{:016X} is invalid: {}",
310 index, path.lane, message
311 ),
312 });
313 }
314
315 if path.waypoints.len() < 2 {
317 errors.push(ArchSpecError::Path {
318 message: format!(
319 "paths[{}]: lane 0x{:016X} has {} waypoint(s), minimum is 2",
320 index,
321 path.lane,
322 path.waypoints.len()
323 ),
324 });
325 continue; }
327
328 if let Some((src_loc, dst_loc)) = spec.lane_endpoints(&lane) {
330 if let Some(src_pos) = spec.location_position(&src_loc) {
331 let first = path.waypoints.first().unwrap();
332 if first[0] != src_pos.0 || first[1] != src_pos.1 {
333 errors.push(ArchSpecError::Path {
334 message: format!(
335 "paths[{}]: lane 0x{:016X} first waypoint ({}, {}) does not match expected position ({}, {})",
336 index, path.lane, first[0], first[1], src_pos.0, src_pos.1
337 ),
338 });
339 }
340 }
341 if let Some(dst_pos) = spec.location_position(&dst_loc) {
342 let last = path.waypoints.last().unwrap();
343 if last[0] != dst_pos.0 || last[1] != dst_pos.1 {
344 errors.push(ArchSpecError::Path {
345 message: format!(
346 "paths[{}]: lane 0x{:016X} last waypoint ({}, {}) does not match expected position ({}, {})",
347 index, path.lane, last[0], last[1], dst_pos.0, dst_pos.1
348 ),
349 });
350 }
351 }
352 }
353 }
354 }
355}
356
357fn check_sites_with_word_buses(
358 spec: &ArchSpec,
359 sites_per_word: u32,
360 errors: &mut Vec<ArchSpecError>,
361) {
362 for &idx in &spec.sites_with_word_buses {
363 if idx >= sites_per_word {
364 errors.push(ArchSpecError::Bus {
365 message: format!(
366 "sites_with_word_buses: site index {} >= sites_per_word ({})",
367 idx, sites_per_word
368 ),
369 });
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use crate::arch::example_arch_spec;
377
378 use super::*;
379
380 fn has_error<F: Fn(&ArchSpecError) -> bool>(errors: &[ArchSpecError], predicate: F) -> bool {
381 errors.iter().any(predicate)
382 }
383
384 #[test]
385 fn valid_spec_passes() {
386 let spec = example_arch_spec();
387 assert!(spec.validate().is_ok());
388 }
389
390 #[test]
391 fn test_zone0_missing_words() {
392 let mut spec = example_arch_spec();
393 spec.zones[0].words = vec![0];
394 let errors = spec.validate().unwrap_err();
395 assert!(has_error(
396 &errors,
397 |e| matches!(e, ArchSpecError::Zone { message } if message.contains("missing word IDs"))
398 ));
399 }
400
401 #[test]
402 fn test_measurement_mode_first_not_zone0() {
403 let mut spec = example_arch_spec();
404 spec.measurement_mode_zones = vec![1];
405 let errors = spec.validate().unwrap_err();
406 assert!(has_error(
407 &errors,
408 |e| matches!(e, ArchSpecError::Zone { message } if message.contains("must be zone 0"))
409 ));
410 }
411
412 #[test]
413 fn test_invalid_entangling_zone() {
414 let mut spec = example_arch_spec();
415 spec.entangling_zones.push(99);
416 let errors = spec.validate().unwrap_err();
417 assert!(has_error(
418 &errors,
419 |e| matches!(e, ArchSpecError::Zone { message } if message.contains("invalid zone ID 99"))
420 ));
421 }
422
423 #[test]
424 fn test_invalid_measurement_mode_zone() {
425 let mut spec = example_arch_spec();
426 spec.measurement_mode_zones.push(99);
427 let errors = spec.validate().unwrap_err();
428 assert!(has_error(
429 &errors,
430 |e| matches!(e, ArchSpecError::Zone { message } if message.contains("invalid zone ID 99"))
431 ));
432 }
433
434 #[test]
435 fn test_wrong_site_count() {
436 let mut spec = example_arch_spec();
437 spec.geometry.words[0].site_indices.pop();
438 let errors = spec.validate().unwrap_err();
439 assert!(has_error(
440 &errors,
441 |e| matches!(e, ArchSpecError::Geometry { message } if message.contains("9 sites, expected 10"))
442 ));
443 }
444
445 #[test]
446 fn test_wrong_cz_pairs_count() {
447 let mut spec = example_arch_spec();
448 spec.geometry.words[0].has_cz.as_mut().unwrap().pop();
449 let errors = spec.validate().unwrap_err();
450 assert!(has_error(
451 &errors,
452 |e| matches!(e, ArchSpecError::Geometry { message } if message.contains("9 cz_pairs, expected 10"))
453 ));
454 }
455
456 #[test]
457 fn test_site_x_index_out_of_range() {
458 let mut spec = example_arch_spec();
459 spec.geometry.words[0].site_indices[0] = [99, 0];
460 let errors = spec.validate().unwrap_err();
461 assert!(has_error(
462 &errors,
463 |e| matches!(e, ArchSpecError::Geometry { message } if message.contains("x_idx 99 out of range"))
464 ));
465 }
466
467 #[test]
468 fn test_site_y_index_out_of_range() {
469 let mut spec = example_arch_spec();
470 spec.geometry.words[0].site_indices[0] = [0, 99];
471 let errors = spec.validate().unwrap_err();
472 assert!(has_error(
473 &errors,
474 |e| matches!(e, ArchSpecError::Geometry { message } if message.contains("y_idx 99 out of range"))
475 ));
476 }
477
478 #[test]
479 fn test_site_bus_length_mismatch() {
480 let mut spec = example_arch_spec();
481 spec.buses.site_buses[0].dst.pop();
482 let errors = spec.validate().unwrap_err();
483 assert!(has_error(
484 &errors,
485 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("site_bus 0: src length"))
486 ));
487 }
488
489 #[test]
490 fn test_site_bus_overlap() {
491 let mut spec = example_arch_spec();
492 spec.buses.site_buses[0].src = vec![0, 1];
493 spec.buses.site_buses[0].dst = vec![0, 2];
494 let errors = spec.validate().unwrap_err();
495 assert!(has_error(
496 &errors,
497 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("overlap at site index 0"))
498 ));
499 }
500
501 #[test]
502 fn test_site_bus_index_out_of_range() {
503 let mut spec = example_arch_spec();
504 spec.buses.site_buses[0].src[0] = 99;
505 let errors = spec.validate().unwrap_err();
506 assert!(has_error(
507 &errors,
508 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("site index 99"))
509 ));
510 }
511
512 #[test]
513 fn test_word_bus_length_mismatch() {
514 let mut spec = example_arch_spec();
515 spec.buses.word_buses[0].dst.pop();
516 let errors = spec.validate().unwrap_err();
517 assert!(has_error(
518 &errors,
519 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("word_bus 0: src length"))
520 ));
521 }
522
523 #[test]
524 fn test_word_bus_invalid_word_id() {
525 let mut spec = example_arch_spec();
526 spec.buses.word_buses[0].src = vec![99];
527 let errors = spec.validate().unwrap_err();
528 assert!(has_error(
529 &errors,
530 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("invalid word ID 99"))
531 ));
532 }
533
534 #[test]
535 fn test_invalid_word_with_site_bus() {
536 let mut spec = example_arch_spec();
537 spec.words_with_site_buses.push(99);
538 let errors = spec.validate().unwrap_err();
539 assert!(has_error(
540 &errors,
541 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("words_with_site_buses: invalid word ID 99"))
542 ));
543 }
544
545 #[test]
546 fn test_invalid_site_with_word_bus() {
547 let mut spec = example_arch_spec();
548 spec.sites_with_word_buses.push(99);
549 let errors = spec.validate().unwrap_err();
550 assert!(has_error(
551 &errors,
552 |e| matches!(e, ArchSpecError::Bus { message } if message.contains("site index 99 >= sites_per_word"))
553 ));
554 }
555
556 #[test]
557 fn test_invalid_path_lane() {
558 let mut spec = example_arch_spec();
559 let bad_lane = crate::arch::addr::LaneAddr {
560 direction: crate::arch::addr::Direction::Forward,
561 move_type: crate::arch::addr::MoveType::SiteBus,
562 word_id: 0,
563 site_id: 0,
564 bus_id: 99,
565 };
566 spec.paths = Some(vec![crate::arch::types::TransportPath {
567 lane: {
568 let (d0, d1) = bad_lane.encode();
569 (d0 as u64) | ((d1 as u64) << 32)
570 },
571 waypoints: vec![[1.0, 2.0]],
572 }]);
573 let errors = spec.validate().unwrap_err();
574 assert!(has_error(
575 &errors,
576 |e| matches!(e, ArchSpecError::Path { message } if message.contains("is invalid"))
577 ));
578 }
579
580 #[test]
581 fn test_valid_path_lane() {
582 let mut spec = example_arch_spec();
583 let good_lane = crate::arch::addr::LaneAddr {
584 direction: crate::arch::addr::Direction::Forward,
585 move_type: crate::arch::addr::MoveType::SiteBus,
586 word_id: 0,
587 site_id: 0,
588 bus_id: 0,
589 };
590 spec.paths = Some(vec![crate::arch::types::TransportPath {
591 lane: {
592 let (d0, d1) = good_lane.encode();
593 (d0 as u64) | ((d1 as u64) << 32)
594 },
595 waypoints: vec![[1.0, 2.5], [1.0, 5.0]],
596 }]);
597 assert!(spec.validate().is_ok());
598 }
599
600 #[test]
601 fn test_path_too_few_waypoints() {
602 let mut spec = example_arch_spec();
603 let lane = crate::arch::addr::LaneAddr {
604 direction: crate::arch::addr::Direction::Forward,
605 move_type: crate::arch::addr::MoveType::SiteBus,
606 word_id: 0,
607 site_id: 0,
608 bus_id: 0,
609 };
610 spec.paths = Some(vec![crate::arch::types::TransportPath {
611 lane: {
612 let (d0, d1) = lane.encode();
613 (d0 as u64) | ((d1 as u64) << 32)
614 },
615 waypoints: vec![[1.0, 2.5]],
616 }]);
617 let errors = spec.validate().unwrap_err();
618 assert!(has_error(
619 &errors,
620 |e| matches!(e, ArchSpecError::Path { message } if message.contains("1 waypoint(s), minimum is 2"))
621 ));
622 }
623
624 #[test]
625 fn test_path_endpoint_mismatch_first() {
626 let mut spec = example_arch_spec();
627 let lane = crate::arch::addr::LaneAddr {
628 direction: crate::arch::addr::Direction::Forward,
629 move_type: crate::arch::addr::MoveType::SiteBus,
630 word_id: 0,
631 site_id: 0,
632 bus_id: 0,
633 };
634 spec.paths = Some(vec![crate::arch::types::TransportPath {
635 lane: {
636 let (d0, d1) = lane.encode();
637 (d0 as u64) | ((d1 as u64) << 32)
638 },
639 waypoints: vec![[99.0, 99.0], [1.0, 5.0]],
640 }]);
641 let errors = spec.validate().unwrap_err();
642 assert!(has_error(
643 &errors,
644 |e| matches!(e, ArchSpecError::Path { message } if message.contains("first waypoint"))
645 ));
646 }
647
648 #[test]
649 fn test_path_endpoint_mismatch_last() {
650 let mut spec = example_arch_spec();
651 let lane = crate::arch::addr::LaneAddr {
652 direction: crate::arch::addr::Direction::Forward,
653 move_type: crate::arch::addr::MoveType::SiteBus,
654 word_id: 0,
655 site_id: 0,
656 bus_id: 0,
657 };
658 spec.paths = Some(vec![crate::arch::types::TransportPath {
659 lane: {
660 let (d0, d1) = lane.encode();
661 (d0 as u64) | ((d1 as u64) << 32)
662 },
663 waypoints: vec![[1.0, 2.5], [99.0, 99.0]],
664 }]);
665 let errors = spec.validate().unwrap_err();
666 assert!(has_error(
667 &errors,
668 |e| matches!(e, ArchSpecError::Path { message } if message.contains("last waypoint"))
669 ));
670 }
671
672 #[test]
673 fn test_inconsistent_grid_shape() {
674 let mut spec = example_arch_spec();
675 spec.geometry.words[1].positions.x_spacing = vec![2.0, 2.0];
676 let errors = spec.validate().unwrap_err();
677 assert!(has_error(
678 &errors,
679 |e| matches!(e, ArchSpecError::Geometry { message } if message.contains("grid shape"))
680 ));
681 }
682
683 #[test]
684 fn multiple_errors_collected() {
685 let mut spec = example_arch_spec();
686 spec.zones[0].words = vec![0];
687 spec.measurement_mode_zones = vec![1];
688 spec.sites_with_word_buses.push(99);
689 let errors = spec.validate().unwrap_err();
690 assert!(
691 errors.len() >= 3,
692 "expected at least 3 errors, got {}",
693 errors.len()
694 );
695 }
696}