bloqade_lanes_bytecode_core/arch/
query.rs

1//! Arch spec queries: JSON loading, position lookup, lane resolution,
2//! and group-level address validation.
3
4use std::collections::HashSet;
5use std::fmt;
6
7use thiserror::Error;
8
9use super::addr::{LaneAddr, LocationAddr, MoveType};
10use super::types::{ArchSpec, Bus, Word};
11use super::validate::ArchSpecError;
12
13/// Error returned when loading an arch spec from JSON fails.
14#[derive(Debug, Error)]
15pub enum ArchSpecLoadError {
16    #[error("JSON parse error: {0}")]
17    Json(#[from] serde_json::Error),
18
19    #[error("validation errors: {0:?}")]
20    Validation(Vec<ArchSpecError>),
21}
22
23impl From<Vec<ArchSpecError>> for ArchSpecLoadError {
24    fn from(errors: Vec<ArchSpecError>) -> Self {
25        ArchSpecLoadError::Validation(errors)
26    }
27}
28
29// --- Group-level error types ---
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum LocationGroupError {
33    /// A location address appears more than once in the group.
34    DuplicateAddress { address: u32 },
35    /// A location address is invalid per the arch spec.
36    InvalidAddress { word_id: u32, site_id: u32 },
37}
38
39impl fmt::Display for LocationGroupError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            LocationGroupError::DuplicateAddress { address } => {
43                let addr = LocationAddr::decode(*address);
44                write!(
45                    f,
46                    "duplicate location address word_id={}, site_id={}",
47                    addr.word_id, addr.site_id
48                )
49            }
50            LocationGroupError::InvalidAddress { word_id, site_id } => {
51                write!(
52                    f,
53                    "invalid location word_id={}, site_id={}",
54                    word_id, site_id
55                )
56            }
57        }
58    }
59}
60
61impl std::error::Error for LocationGroupError {}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum LaneGroupError {
65    /// A lane address appears more than once in the group.
66    DuplicateAddress { address: (u32, u32) },
67    /// A lane address is invalid per the arch spec.
68    InvalidLane { message: String },
69    /// Lanes have inconsistent bus_id, move_type, or direction.
70    Inconsistent { message: String },
71    /// Lane word_id not in words_with_site_buses.
72    WordNotInSiteBusList { word_id: u32 },
73    /// Lane site_id not in sites_with_word_buses.
74    SiteNotInWordBusList { site_id: u32 },
75    /// Lane group violates AOD grid constraint (e.g. not a complete grid).
76    AODConstraintViolation { message: String },
77}
78
79impl fmt::Display for LaneGroupError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            LaneGroupError::DuplicateAddress { address } => {
83                let combined = (address.0 as u64) | ((address.1 as u64) << 32);
84                write!(f, "duplicate lane address 0x{:016x}", combined)
85            }
86            LaneGroupError::InvalidLane { message } => {
87                write!(f, "invalid lane: {}", message)
88            }
89            LaneGroupError::Inconsistent { message } => {
90                write!(f, "lane group inconsistent: {}", message)
91            }
92            LaneGroupError::WordNotInSiteBusList { word_id } => {
93                write!(f, "word_id {} not in words_with_site_buses", word_id)
94            }
95            LaneGroupError::SiteNotInWordBusList { site_id } => {
96                write!(f, "site_id {} not in sites_with_word_buses", site_id)
97            }
98            LaneGroupError::AODConstraintViolation { message } => {
99                write!(f, "AOD constraint violation: {}", message)
100            }
101        }
102    }
103}
104
105impl std::error::Error for LaneGroupError {}
106
107// --- Bus methods ---
108
109impl Bus {
110    /// Given a source value, return the destination value (forward move).
111    /// For site buses, this maps source site → destination site.
112    /// For word buses, this maps source word → destination word.
113    pub fn resolve_forward(&self, src: u32) -> Option<u32> {
114        self.src
115            .iter()
116            .position(|&s| s == src)
117            .and_then(|i| self.dst.get(i).copied())
118    }
119
120    /// Given a destination value, return the source value (backward move).
121    /// For site buses, this maps destination site → source site.
122    /// For word buses, this maps destination word → source word.
123    pub fn resolve_backward(&self, dst: u32) -> Option<u32> {
124        self.dst
125            .iter()
126            .position(|&d| d == dst)
127            .and_then(|i| self.src.get(i).copied())
128    }
129}
130
131// --- Word methods ---
132
133impl Word {
134    /// Resolve a site's grid indices to physical (x, y) coordinates.
135    pub fn site_position(&self, site_idx: usize) -> Option<(f64, f64)> {
136        let pair = self.site_indices.get(site_idx)?;
137        let x = self.positions.x_position(pair[0] as usize)?;
138        let y = self.positions.y_position(pair[1] as usize)?;
139        Some((x, y))
140    }
141}
142
143// --- ArchSpec methods ---
144
145impl ArchSpec {
146    // -- Deserialization --
147
148    /// Deserialize from a JSON string.
149    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
150        serde_json::from_str(json)
151    }
152
153    /// Deserialize from JSON and validate.
154    pub fn from_json_validated(json: &str) -> Result<Self, ArchSpecLoadError> {
155        let spec = Self::from_json(json)?;
156        spec.validate()?;
157        Ok(spec)
158    }
159
160    // -- Lookup helpers --
161
162    pub fn word_by_id(&self, id: u32) -> Option<&Word> {
163        self.geometry.words.get(id as usize)
164    }
165
166    pub fn zone_by_id(&self, id: u32) -> Option<&super::types::Zone> {
167        self.zones.get(id as usize)
168    }
169
170    pub fn site_bus_by_id(&self, id: u32) -> Option<&Bus> {
171        self.buses.site_buses.get(id as usize)
172    }
173
174    pub fn word_bus_by_id(&self, id: u32) -> Option<&Bus> {
175        self.buses.word_buses.get(id as usize)
176    }
177
178    // -- Position resolution --
179
180    /// Resolve a LocationAddr to physical (x, y) coordinates.
181    pub fn location_position(&self, loc: &crate::arch::addr::LocationAddr) -> Option<(f64, f64)> {
182        let word = self.word_by_id(loc.word_id)?;
183        word.site_position(loc.site_id as usize)
184    }
185
186    /// Resolve a `LaneAddr` to its source and destination `LocationAddr` pair.
187    ///
188    /// Returns `Some((src, dst))` if the lane can be resolved through the bus,
189    /// or `None` if the lane references invalid words, sites, or buses.
190    pub fn lane_endpoints(
191        &self,
192        lane: &crate::arch::addr::LaneAddr,
193    ) -> Option<(LocationAddr, LocationAddr)> {
194        use crate::arch::addr::{Direction, MoveType};
195
196        // Validate the lane address up front so callers always get None
197        // for invalid lanes (e.g. out-of-range word_id or site_id).
198        if !self.check_lane(lane).is_empty() {
199            return None;
200        }
201
202        // In the lane address convention, site_id and word_id always encode
203        // the forward-direction source. The direction field only controls
204        // which endpoint is returned as src vs dst.
205        let fwd_src = LocationAddr {
206            word_id: lane.word_id,
207            site_id: lane.site_id,
208        };
209
210        let fwd_dst = match lane.move_type {
211            MoveType::SiteBus => {
212                let bus = self.site_bus_by_id(lane.bus_id)?;
213                let dst_site = bus.resolve_forward(lane.site_id)?;
214                LocationAddr {
215                    word_id: lane.word_id,
216                    site_id: dst_site,
217                }
218            }
219            MoveType::WordBus => {
220                let bus = self.word_bus_by_id(lane.bus_id)?;
221                let dst_word = bus.resolve_forward(lane.word_id)?;
222                LocationAddr {
223                    word_id: dst_word,
224                    site_id: lane.site_id,
225                }
226            }
227        };
228
229        match lane.direction {
230            Direction::Forward => Some((fwd_src, fwd_dst)),
231            Direction::Backward => Some((fwd_dst, fwd_src)),
232        }
233    }
234
235    /// Get the CZ pair (blockaded location) for a given location.
236    ///
237    /// Returns `Some(LocationAddr)` if the word at `loc.word_id` has CZ data,
238    /// site `loc.site_id` has a partner, and the partner is a valid location.
239    /// Returns `None` otherwise.
240    pub fn get_blockaded_location(&self, loc: &LocationAddr) -> Option<LocationAddr> {
241        let word = self.word_by_id(loc.word_id)?;
242        let cz_pairs = word.has_cz.as_ref()?;
243        let pair = cz_pairs.get(loc.site_id as usize)?;
244        let result = LocationAddr {
245            word_id: pair[0],
246            site_id: pair[1],
247        };
248        // Validate the CZ pair target is in range
249        if self.check_location(&result).is_some() {
250            return None;
251        }
252        Some(result)
253    }
254
255    // -- Address validation --
256
257    /// Check whether a location address (word_id, site_id) is valid.
258    pub fn check_location(&self, loc: &crate::arch::addr::LocationAddr) -> Option<String> {
259        let num_words = self.geometry.words.len() as u32;
260        let sites_per_word = self.geometry.sites_per_word;
261        if loc.word_id >= num_words || loc.site_id >= sites_per_word {
262            Some(format!(
263                "invalid location word_id={}, site_id={}",
264                loc.word_id, loc.site_id
265            ))
266        } else {
267            None
268        }
269    }
270
271    /// Check whether a lane address is valid.
272    ///
273    /// Validates that the bus exists, word/site are in range, and the
274    /// site/word is a valid forward source for the bus. In the lane address
275    /// convention, `site_id` and `word_id` always encode the forward-direction
276    /// source — the `direction` field only controls which endpoint is src vs
277    /// dst, not which site/word is encoded.
278    pub fn check_lane(&self, addr: &LaneAddr) -> Vec<String> {
279        let num_words = self.geometry.words.len() as u32;
280        let sites_per_word = self.geometry.sites_per_word;
281        let mut errors = Vec::new();
282
283        match addr.move_type {
284            MoveType::SiteBus => {
285                if addr.word_id >= num_words {
286                    errors.push(format!("word_id {} out of range", addr.word_id));
287                } else if !self.words_with_site_buses.contains(&addr.word_id) {
288                    errors.push(format!(
289                        "word_id {} not in words_with_site_buses",
290                        addr.word_id
291                    ));
292                }
293                if addr.site_id >= sites_per_word {
294                    errors.push(format!("site_id {} out of range", addr.site_id));
295                }
296                if let Some(bus) = self.site_bus_by_id(addr.bus_id) {
297                    if errors.is_empty() && bus.resolve_forward(addr.site_id).is_none() {
298                        errors.push(format!(
299                            "site_id {} is not a valid source for site_bus {}",
300                            addr.site_id, addr.bus_id
301                        ));
302                    }
303                } else {
304                    errors.push(format!("unknown site_bus id {}", addr.bus_id));
305                }
306            }
307            MoveType::WordBus => {
308                if addr.word_id >= num_words {
309                    errors.push(format!("word_id {} out of range", addr.word_id));
310                }
311                if addr.site_id >= sites_per_word {
312                    errors.push(format!("site_id {} out of range", addr.site_id));
313                } else if !self.sites_with_word_buses.contains(&addr.site_id) {
314                    errors.push(format!(
315                        "site_id {} not in sites_with_word_buses",
316                        addr.site_id
317                    ));
318                }
319                if let Some(bus) = self.word_bus_by_id(addr.bus_id) {
320                    if errors.is_empty() && bus.resolve_forward(addr.word_id).is_none() {
321                        errors.push(format!(
322                            "word_id {} is not a valid source for word_bus {}",
323                            addr.word_id, addr.bus_id
324                        ));
325                    }
326                } else {
327                    errors.push(format!("unknown word_bus id {}", addr.bus_id));
328                }
329            }
330        }
331        errors
332    }
333
334    /// Check whether a zone address is valid.
335    pub fn check_zone(&self, zone: &crate::arch::addr::ZoneAddr) -> Option<String> {
336        if self.zone_by_id(zone.zone_id).is_none() {
337            Some(format!("invalid zone_id={}", zone.zone_id))
338        } else {
339            None
340        }
341    }
342
343    // -- Group validation --
344
345    /// Check that a group of lanes share consistent bus_id, move_type, and direction.
346    pub fn check_lane_group_consistency(&self, lanes: &[LaneAddr]) -> Vec<String> {
347        if lanes.is_empty() {
348            return vec![];
349        }
350        let first = &lanes[0];
351        let mut errors = Vec::new();
352
353        for lane in &lanes[1..] {
354            if lane.bus_id != first.bus_id {
355                errors.push(format!(
356                    "bus_id mismatch: expected {}, got {}",
357                    first.bus_id, lane.bus_id
358                ));
359            }
360            if lane.move_type != first.move_type {
361                errors.push(format!(
362                    "move_type mismatch: expected {:?}, got {:?}",
363                    first.move_type, lane.move_type
364                ));
365            }
366            if lane.direction != first.direction {
367                errors.push(format!(
368                    "direction mismatch: expected {:?}, got {:?}",
369                    first.direction, lane.direction
370                ));
371            }
372        }
373
374        errors
375    }
376
377    /// Check that each lane's word/site belongs to the correct bus membership list.
378    /// Returns unique `(word_ids_not_in_site_bus_list, site_ids_not_in_word_bus_list)`.
379    pub fn check_lane_group_membership(&self, lanes: &[LaneAddr]) -> (Vec<u32>, Vec<u32>) {
380        use std::collections::BTreeSet;
381
382        let mut bad_words = BTreeSet::new();
383        let mut bad_sites = BTreeSet::new();
384
385        for lane in lanes {
386            match lane.move_type {
387                MoveType::SiteBus => {
388                    if !self.words_with_site_buses.contains(&lane.word_id) {
389                        bad_words.insert(lane.word_id);
390                    }
391                }
392                MoveType::WordBus => {
393                    if !self.sites_with_word_buses.contains(&lane.site_id) {
394                        bad_sites.insert(lane.site_id);
395                    }
396                }
397            }
398        }
399
400        (
401            bad_words.into_iter().collect(),
402            bad_sites.into_iter().collect(),
403        )
404    }
405
406    /// Validate a group of location addresses: checks each address against the
407    /// arch spec and checks for duplicates within the group.
408    pub fn check_locations(&self, locations: &[LocationAddr]) -> Vec<LocationGroupError> {
409        let mut errors = Vec::new();
410
411        // Check each unique address is valid (report once per unique address)
412        let mut checked = HashSet::new();
413        for loc in locations {
414            let bits = loc.encode();
415            if checked.insert(bits) && self.check_location(loc).is_some() {
416                errors.push(LocationGroupError::InvalidAddress {
417                    word_id: loc.word_id,
418                    site_id: loc.site_id,
419                });
420            }
421        }
422
423        // Check for duplicates (report once per unique duplicated address)
424        let mut seen = HashSet::new();
425        let mut reported = HashSet::new();
426        for loc in locations {
427            let bits = loc.encode();
428            if !seen.insert(bits) && reported.insert(bits) {
429                errors.push(LocationGroupError::DuplicateAddress { address: bits });
430            }
431        }
432
433        errors
434    }
435
436    /// Validate a group of lane addresses: checks each address against the
437    /// arch spec, checks for duplicates, and (when more than one lane)
438    /// validates consistency, bus membership, and AOD constraints.
439    pub fn check_lanes(&self, lanes: &[LaneAddr]) -> Vec<LaneGroupError> {
440        let mut errors = Vec::new();
441
442        // Check each unique address is valid (report once per unique address)
443        let mut checked = HashSet::new();
444        for lane in lanes {
445            let bits = lane.encode();
446            if checked.insert(bits) {
447                for msg in self.check_lane(lane) {
448                    errors.push(LaneGroupError::InvalidLane { message: msg });
449                }
450            }
451        }
452
453        // Check for duplicates (report once per unique duplicated address)
454        let mut seen = HashSet::new();
455        let mut reported = HashSet::new();
456        for lane in lanes {
457            let pair = lane.encode();
458            if !seen.insert(pair) && reported.insert(pair) {
459                errors.push(LaneGroupError::DuplicateAddress { address: pair });
460            }
461        }
462
463        // Group-level checks (only meaningful with >1 lane)
464        if lanes.len() > 1 {
465            for msg in self.check_lane_group_consistency(lanes) {
466                errors.push(LaneGroupError::Inconsistent { message: msg });
467            }
468            let (bad_words, bad_sites) = self.check_lane_group_membership(lanes);
469            for word_id in bad_words {
470                errors.push(LaneGroupError::WordNotInSiteBusList { word_id });
471            }
472            for site_id in bad_sites {
473                errors.push(LaneGroupError::SiteNotInWordBusList { site_id });
474            }
475            for msg in self.check_lane_group_geometry(lanes) {
476                errors.push(LaneGroupError::AODConstraintViolation { message: msg });
477            }
478        }
479
480        errors
481    }
482
483    /// Check AOD grid constraint: lane positions must form a complete grid
484    /// (Cartesian product of unique X and Y values).
485    pub fn check_lane_group_geometry(&self, lanes: &[LaneAddr]) -> Vec<String> {
486        use std::collections::BTreeSet;
487
488        let positions: Vec<(f64, f64)> = lanes
489            .iter()
490            .filter_map(|lane| {
491                let loc = crate::arch::addr::LocationAddr {
492                    word_id: lane.word_id,
493                    site_id: lane.site_id,
494                };
495                self.location_position(&loc)
496            })
497            .collect();
498
499        if positions.len() != lanes.len() {
500            return vec!["some lane positions could not be resolved".to_string()];
501        }
502
503        let unique_x: BTreeSet<u64> = positions.iter().map(|(x, _)| x.to_bits()).collect();
504        let unique_y: BTreeSet<u64> = positions.iter().map(|(_, y)| y.to_bits()).collect();
505
506        let expected: BTreeSet<(u64, u64)> = unique_x
507            .iter()
508            .flat_map(|x| unique_y.iter().map(move |y| (*x, *y)))
509            .collect();
510
511        let actual: BTreeSet<(u64, u64)> = positions
512            .iter()
513            .map(|(x, y)| (x.to_bits(), y.to_bits()))
514            .collect();
515
516        if actual != expected {
517            vec![format!(
518                "lanes do not form a complete grid: expected {} positions ({}x × {}y), got {} unique positions",
519                expected.len(),
520                unique_x.len(),
521                unique_y.len(),
522                actual.len()
523            )]
524        } else {
525            vec![]
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use crate::arch::example_arch_spec;
533
534    #[test]
535    fn word_by_id_found() {
536        let spec = example_arch_spec();
537        let word = spec.word_by_id(0).unwrap();
538        assert_eq!(word.site_indices.len(), 10);
539    }
540
541    #[test]
542    fn word_by_id_not_found() {
543        let spec = example_arch_spec();
544        assert!(spec.word_by_id(99).is_none());
545    }
546
547    #[test]
548    fn zone_by_id_found() {
549        let spec = example_arch_spec();
550        let zone = spec.zone_by_id(0).unwrap();
551        assert_eq!(zone.words, vec![0, 1]);
552    }
553
554    #[test]
555    fn zone_by_id_not_found() {
556        let spec = example_arch_spec();
557        assert!(spec.zone_by_id(99).is_none());
558    }
559
560    #[test]
561    fn site_bus_by_id_found() {
562        let spec = example_arch_spec();
563        let bus = spec.site_bus_by_id(0).unwrap();
564        assert_eq!(bus.src, vec![0, 1, 2, 3, 4]);
565    }
566
567    #[test]
568    fn site_bus_by_id_not_found() {
569        let spec = example_arch_spec();
570        assert!(spec.site_bus_by_id(99).is_none());
571    }
572
573    #[test]
574    fn word_bus_by_id_found() {
575        let spec = example_arch_spec();
576        let bus = spec.word_bus_by_id(0).unwrap();
577        assert_eq!(bus.src, vec![0]);
578        assert_eq!(bus.dst, vec![1]);
579    }
580
581    #[test]
582    fn word_bus_by_id_not_found() {
583        let spec = example_arch_spec();
584        assert!(spec.word_bus_by_id(99).is_none());
585    }
586
587    #[test]
588    fn site_bus_resolve_forward() {
589        let spec = example_arch_spec();
590        let bus = spec.site_bus_by_id(0).unwrap();
591        assert_eq!(bus.resolve_forward(0), Some(5));
592        assert_eq!(bus.resolve_forward(4), Some(9));
593        assert_eq!(bus.resolve_forward(99), None);
594    }
595
596    #[test]
597    fn site_bus_resolve_backward() {
598        let spec = example_arch_spec();
599        let bus = spec.site_bus_by_id(0).unwrap();
600        assert_eq!(bus.resolve_backward(5), Some(0));
601        assert_eq!(bus.resolve_backward(9), Some(4));
602        assert_eq!(bus.resolve_backward(99), None);
603    }
604
605    #[test]
606    fn word_bus_resolve_forward() {
607        let spec = example_arch_spec();
608        let bus = spec.word_bus_by_id(0).unwrap();
609        assert_eq!(bus.resolve_forward(0), Some(1));
610        assert_eq!(bus.resolve_forward(99), None);
611    }
612
613    #[test]
614    fn word_bus_resolve_backward() {
615        let spec = example_arch_spec();
616        let bus = spec.word_bus_by_id(0).unwrap();
617        assert_eq!(bus.resolve_backward(1), Some(0));
618        assert_eq!(bus.resolve_backward(99), None);
619    }
620
621    #[test]
622    fn site_position_valid() {
623        let spec = example_arch_spec();
624        let word = spec.word_by_id(0).unwrap();
625        // Site 0 is [0, 0] -> x_positions[0]=1.0, y_positions[0]=2.5
626        assert_eq!(word.site_position(0), Some((1.0, 2.5)));
627        // Site 5 is [0, 1] -> x_positions[0]=1.0, y_positions[1]=5.0
628        assert_eq!(word.site_position(5), Some((1.0, 5.0)));
629        // Site 4 is [4, 0] -> x_positions[4]=9.0, y_positions[0]=2.5
630        assert_eq!(word.site_position(4), Some((9.0, 2.5)));
631    }
632
633    #[test]
634    fn site_position_out_of_range() {
635        let spec = example_arch_spec();
636        let word = spec.word_by_id(0).unwrap();
637        assert!(word.site_position(99).is_none());
638    }
639
640    #[test]
641    fn location_position_valid() {
642        let spec = example_arch_spec();
643        let loc = crate::arch::addr::LocationAddr {
644            word_id: 0,
645            site_id: 0,
646        };
647        assert_eq!(spec.location_position(&loc), Some((1.0, 2.5)));
648    }
649
650    #[test]
651    fn location_position_invalid_word() {
652        let spec = example_arch_spec();
653        let loc = crate::arch::addr::LocationAddr {
654            word_id: 99,
655            site_id: 0,
656        };
657        assert!(spec.location_position(&loc).is_none());
658    }
659
660    #[test]
661    fn location_position_invalid_site() {
662        let spec = example_arch_spec();
663        let loc = crate::arch::addr::LocationAddr {
664            word_id: 0,
665            site_id: 99,
666        };
667        assert!(spec.location_position(&loc).is_none());
668    }
669
670    #[test]
671    fn from_json_valid() {
672        let json = serde_json::to_string(&example_arch_spec()).unwrap();
673        let spec = super::super::ArchSpec::from_json(&json).unwrap();
674        assert_eq!(spec.version, crate::version::Version::new(1, 0));
675    }
676
677    #[test]
678    fn from_json_validated_valid() {
679        let json = serde_json::to_string(&example_arch_spec()).unwrap();
680        let spec = super::super::ArchSpec::from_json_validated(&json).unwrap();
681        assert_eq!(spec.version, crate::version::Version::new(1, 0));
682    }
683
684    #[test]
685    fn check_lane_group_consistency_empty() {
686        let spec = example_arch_spec();
687        assert!(spec.check_lane_group_consistency(&[]).is_empty());
688    }
689
690    #[test]
691    fn from_json_validated_invalid() {
692        // Missing required fields
693        let json = r#"{"version": "1.0"}"#;
694        let result = super::super::ArchSpec::from_json_validated(json);
695        assert!(result.is_err());
696    }
697
698    #[test]
699    fn get_blockaded_location_valid() {
700        let spec = example_arch_spec();
701        // Site 0 in word 0 pairs with site 5 in word 0
702        let loc = crate::arch::addr::LocationAddr {
703            word_id: 0,
704            site_id: 0,
705        };
706        let pair = spec.get_blockaded_location(&loc).unwrap();
707        assert_eq!(pair.word_id, 0);
708        assert_eq!(pair.site_id, 5);
709    }
710
711    #[test]
712    fn get_blockaded_location_reverse() {
713        let spec = example_arch_spec();
714        // Site 5 in word 0 pairs back with site 0 in word 0
715        let loc = crate::arch::addr::LocationAddr {
716            word_id: 0,
717            site_id: 5,
718        };
719        let pair = spec.get_blockaded_location(&loc).unwrap();
720        assert_eq!(pair.word_id, 0);
721        assert_eq!(pair.site_id, 0);
722    }
723
724    #[test]
725    fn get_blockaded_location_invalid_word() {
726        let spec = example_arch_spec();
727        let loc = crate::arch::addr::LocationAddr {
728            word_id: 99,
729            site_id: 0,
730        };
731        assert!(spec.get_blockaded_location(&loc).is_none());
732    }
733
734    #[test]
735    fn get_blockaded_location_invalid_site() {
736        let spec = example_arch_spec();
737        let loc = crate::arch::addr::LocationAddr {
738            word_id: 0,
739            site_id: 99,
740        };
741        assert!(spec.get_blockaded_location(&loc).is_none());
742    }
743
744    // ── lane_endpoints tests ──
745
746    #[test]
747    fn lane_endpoints_site_bus_forward() {
748        let spec = example_arch_spec();
749        // Site bus 0: src=[0,1,2,3,4] dst=[5,6,7,8,9]
750        let lane = crate::arch::addr::LaneAddr {
751            direction: crate::arch::addr::Direction::Forward,
752            move_type: crate::arch::addr::MoveType::SiteBus,
753            word_id: 0,
754            site_id: 0,
755            bus_id: 0,
756        };
757        let (src, dst) = spec.lane_endpoints(&lane).unwrap();
758        assert_eq!(src.word_id, 0);
759        assert_eq!(src.site_id, 0);
760        assert_eq!(dst.word_id, 0);
761        assert_eq!(dst.site_id, 5);
762    }
763
764    #[test]
765    fn lane_endpoints_site_bus_backward() {
766        let spec = example_arch_spec();
767        // Backward: same site_id (forward source), but endpoints are swapped
768        let lane = crate::arch::addr::LaneAddr {
769            direction: crate::arch::addr::Direction::Backward,
770            move_type: crate::arch::addr::MoveType::SiteBus,
771            word_id: 0,
772            site_id: 0,
773            bus_id: 0,
774        };
775        let (src, dst) = spec.lane_endpoints(&lane).unwrap();
776        // Backward swaps: src is the forward dst, dst is the forward src
777        assert_eq!(src.word_id, 0);
778        assert_eq!(src.site_id, 5);
779        assert_eq!(dst.word_id, 0);
780        assert_eq!(dst.site_id, 0);
781    }
782
783    #[test]
784    fn lane_endpoints_word_bus_forward() {
785        let spec = example_arch_spec();
786        // Word bus 0: src=[0] dst=[1]; site_id must be in sites_with_word_buses
787        let lane = crate::arch::addr::LaneAddr {
788            direction: crate::arch::addr::Direction::Forward,
789            move_type: crate::arch::addr::MoveType::WordBus,
790            word_id: 0,
791            site_id: 5,
792            bus_id: 0,
793        };
794        let (src, dst) = spec.lane_endpoints(&lane).unwrap();
795        assert_eq!(src.word_id, 0);
796        assert_eq!(src.site_id, 5);
797        assert_eq!(dst.word_id, 1);
798        assert_eq!(dst.site_id, 5);
799    }
800
801    #[test]
802    fn lane_endpoints_word_bus_backward() {
803        let spec = example_arch_spec();
804        // site_id must be in sites_with_word_buses
805        let lane = crate::arch::addr::LaneAddr {
806            direction: crate::arch::addr::Direction::Backward,
807            move_type: crate::arch::addr::MoveType::WordBus,
808            word_id: 0,
809            site_id: 5,
810            bus_id: 0,
811        };
812        let (src, dst) = spec.lane_endpoints(&lane).unwrap();
813        // Backward swaps endpoints
814        assert_eq!(src.word_id, 1);
815        assert_eq!(src.site_id, 5);
816        assert_eq!(dst.word_id, 0);
817        assert_eq!(dst.site_id, 5);
818    }
819
820    #[test]
821    fn lane_endpoints_invalid_bus_returns_none() {
822        let spec = example_arch_spec();
823        let lane = crate::arch::addr::LaneAddr {
824            direction: crate::arch::addr::Direction::Forward,
825            move_type: crate::arch::addr::MoveType::SiteBus,
826            word_id: 0,
827            site_id: 0,
828            bus_id: 99,
829        };
830        assert!(spec.lane_endpoints(&lane).is_none());
831    }
832
833    #[test]
834    fn lane_endpoints_invalid_site_not_in_bus_returns_none() {
835        let spec = example_arch_spec();
836        // Site 5 is a destination, not a source for forward resolution
837        let lane = crate::arch::addr::LaneAddr {
838            direction: crate::arch::addr::Direction::Forward,
839            move_type: crate::arch::addr::MoveType::SiteBus,
840            word_id: 0,
841            site_id: 5,
842            bus_id: 0,
843        };
844        assert!(spec.lane_endpoints(&lane).is_none());
845    }
846
847    // ── check_lane tests ──
848
849    #[test]
850    fn check_lane_valid_forward() {
851        let spec = example_arch_spec();
852        let lane = crate::arch::addr::LaneAddr {
853            direction: crate::arch::addr::Direction::Forward,
854            move_type: crate::arch::addr::MoveType::SiteBus,
855            word_id: 0,
856            site_id: 0,
857            bus_id: 0,
858        };
859        assert!(spec.check_lane(&lane).is_empty());
860    }
861
862    #[test]
863    fn check_lane_valid_backward() {
864        let spec = example_arch_spec();
865        // Backward with forward source site_id=0 should be valid
866        let lane = crate::arch::addr::LaneAddr {
867            direction: crate::arch::addr::Direction::Backward,
868            move_type: crate::arch::addr::MoveType::SiteBus,
869            word_id: 0,
870            site_id: 0,
871            bus_id: 0,
872        };
873        assert!(spec.check_lane(&lane).is_empty());
874    }
875
876    #[test]
877    fn check_lane_destination_site_rejected() {
878        let spec = example_arch_spec();
879        // Site 5 is a destination, not a forward source
880        let lane = crate::arch::addr::LaneAddr {
881            direction: crate::arch::addr::Direction::Forward,
882            move_type: crate::arch::addr::MoveType::SiteBus,
883            word_id: 0,
884            site_id: 5,
885            bus_id: 0,
886        };
887        let errors = spec.check_lane(&lane);
888        assert!(!errors.is_empty());
889        assert!(errors[0].contains("not a valid source"));
890    }
891
892    #[test]
893    fn check_lane_destination_site_backward_also_rejected() {
894        let spec = example_arch_spec();
895        // Site 5 with backward direction — still not a forward source
896        let lane = crate::arch::addr::LaneAddr {
897            direction: crate::arch::addr::Direction::Backward,
898            move_type: crate::arch::addr::MoveType::SiteBus,
899            word_id: 0,
900            site_id: 5,
901            bus_id: 0,
902        };
903        let errors = spec.check_lane(&lane);
904        assert!(!errors.is_empty());
905        assert!(errors[0].contains("not a valid source"));
906    }
907
908    #[test]
909    fn check_lane_invalid_bus() {
910        let spec = example_arch_spec();
911        let lane = crate::arch::addr::LaneAddr {
912            direction: crate::arch::addr::Direction::Forward,
913            move_type: crate::arch::addr::MoveType::SiteBus,
914            word_id: 0,
915            site_id: 0,
916            bus_id: 99,
917        };
918        let errors = spec.check_lane(&lane);
919        assert!(!errors.is_empty());
920    }
921
922    #[test]
923    fn check_lane_word_not_in_site_bus_list() {
924        let mut spec = example_arch_spec();
925        // Remove word 0 from words_with_site_buses
926        spec.words_with_site_buses.retain(|&w| w != 0);
927        let lane = crate::arch::addr::LaneAddr {
928            direction: crate::arch::addr::Direction::Forward,
929            move_type: crate::arch::addr::MoveType::SiteBus,
930            word_id: 0,
931            site_id: 0,
932            bus_id: 0,
933        };
934        let errors = spec.check_lane(&lane);
935        assert!(!errors.is_empty());
936        assert!(errors[0].contains("words_with_site_buses"));
937    }
938
939    #[test]
940    fn check_lane_site_not_in_word_bus_list() {
941        let spec = example_arch_spec();
942        // sites_with_word_buses is [5,6,7,8,9]; site 0 is not in the list
943        let lane = crate::arch::addr::LaneAddr {
944            direction: crate::arch::addr::Direction::Forward,
945            move_type: crate::arch::addr::MoveType::WordBus,
946            word_id: 0,
947            site_id: 0,
948            bus_id: 0,
949        };
950        let errors = spec.check_lane(&lane);
951        assert!(!errors.is_empty());
952        assert!(errors[0].contains("sites_with_word_buses"));
953    }
954}