Skip to main content

bloqade_lanes_bytecode_core/bytecode/
validate.rs

1//! Bytecode program validation: structural checks, address validation,
2//! and stack-type simulation.
3//!
4//! Validation runs in layers:
5//! - **Structural** (always): operand bounds, arity limits, instruction ordering
6//! - **Architecture** (when arch spec provided): address validity
7//! - **Stack simulation** (when enabled): type checking via abstract interpretation
8
9use std::collections::HashSet;
10use std::fmt;
11
12use super::instruction::{
13    ArrayInstruction, AtomArrangementInstruction, CpuInstruction, DetectorObservableInstruction,
14    Instruction, LaneConstInstruction, MeasurementInstruction, QuantumGateInstruction,
15};
16use super::program::Program;
17use super::value::{
18    TAG_ARRAY_REF, TAG_DETECTOR_REF, TAG_FLOAT, TAG_INT, TAG_LANE, TAG_LOCATION,
19    TAG_MEASURE_FUTURE, TAG_OBSERVABLE_REF, TAG_ZONE,
20};
21use crate::arch::addr::{LaneAddr, LocationAddr, ZoneAddr};
22use crate::arch::query::{LaneGroupError, LocationGroupError};
23use crate::arch::types::ArchSpec;
24
25/// Maximum valid type tag (TAG_OBSERVABLE_REF = 0x8).
26const MAX_TYPE_TAG: u8 = 0x8;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ValidationError {
30    /// NewArray dim0 must be > 0.
31    NewArrayZeroDim0 { pc: usize },
32    /// NewArray type_tag is invalid.
33    NewArrayInvalidTypeTag { pc: usize, type_tag: u8 },
34    /// InitialFill is not the first non-constant instruction.
35    InitialFillNotFirst { pc: usize },
36    /// Stack underflow at the given program counter.
37    StackUnderflow { pc: usize },
38    /// Type mismatch on stack.
39    TypeMismatch { pc: usize, expected: u8, got: u8 },
40    /// Invalid zone address per arch spec.
41    InvalidZone { pc: usize, zone_id: u32 },
42    /// Location group validation error (invalid address, duplicate, etc.).
43    LocationGroupValidation {
44        pc: usize,
45        error: LocationGroupError,
46    },
47    /// Lane group validation error (invalid address, duplicate, inconsistent, AOD constraint, etc.).
48    LaneGroupValidation { pc: usize, error: LaneGroupError },
49    /// Multiple measure instructions when feed_forward is disabled.
50    FeedForwardNotSupported { pc: usize },
51    /// Fill instruction when atom_reloading is disabled.
52    AtomReloadingNotSupported { pc: usize },
53    /// Program has no instructions (no terminator present).
54    EmptyProgram,
55    /// Last instruction is not `return` or `halt`.
56    MissingTerminator { pc: usize },
57    /// Instruction follows a `return` or `halt` and is unreachable.
58    UnreachableInstruction { pc: usize },
59}
60
61impl fmt::Display for ValidationError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            ValidationError::NewArrayZeroDim0 { pc } => {
65                write!(f, "pc {}: new_array dim0 must be > 0", pc)
66            }
67            ValidationError::NewArrayInvalidTypeTag { pc, type_tag } => {
68                write!(f, "pc {}: invalid type tag 0x{:x}", pc, type_tag)
69            }
70            ValidationError::InitialFillNotFirst { pc } => {
71                write!(
72                    f,
73                    "pc {}: initial_fill must be the first non-constant instruction",
74                    pc
75                )
76            }
77            ValidationError::StackUnderflow { pc } => {
78                write!(f, "pc {}: stack underflow", pc)
79            }
80            ValidationError::TypeMismatch { pc, expected, got } => {
81                write!(
82                    f,
83                    "pc {}: type mismatch: expected tag 0x{:x}, got 0x{:x}",
84                    pc, expected, got
85                )
86            }
87            ValidationError::InvalidZone { pc, zone_id } => {
88                write!(f, "pc {}: invalid zone_id={}", pc, zone_id)
89            }
90            ValidationError::LocationGroupValidation { pc, error } => {
91                write!(f, "pc {}: {}", pc, error)
92            }
93            ValidationError::LaneGroupValidation { pc, error } => {
94                write!(f, "pc {}: {}", pc, error)
95            }
96            ValidationError::FeedForwardNotSupported { pc } => {
97                write!(
98                    f,
99                    "pc {}: multiple measure instructions require feed_forward capability",
100                    pc
101                )
102            }
103            ValidationError::AtomReloadingNotSupported { pc } => {
104                write!(
105                    f,
106                    "pc {}: fill instruction requires atom_reloading capability",
107                    pc
108                )
109            }
110            ValidationError::EmptyProgram => {
111                write!(
112                    f,
113                    "program has no instructions: missing return or halt terminator"
114                )
115            }
116            ValidationError::MissingTerminator { pc } => {
117                write!(f, "pc {}: program must end with return or halt", pc)
118            }
119            ValidationError::UnreachableInstruction { pc } => {
120                write!(f, "pc {}: unreachable instruction after return or halt", pc)
121            }
122        }
123    }
124}
125
126impl std::error::Error for ValidationError {}
127
128/// Run structural validation on a program. Returns collected errors.
129pub fn validate_structure(program: &Program) -> Vec<ValidationError> {
130    let mut errors = Vec::new();
131    let mut seen_non_constant = false;
132
133    for (pc, instr) in program.instructions.iter().enumerate() {
134        match instr {
135            Instruction::Array(ArrayInstruction::NewArray {
136                type_tag,
137                dim0,
138                dim1: _,
139            }) => {
140                if *dim0 == 0 {
141                    errors.push(ValidationError::NewArrayZeroDim0 { pc });
142                }
143                if *type_tag > MAX_TYPE_TAG {
144                    errors.push(ValidationError::NewArrayInvalidTypeTag {
145                        pc,
146                        type_tag: *type_tag,
147                    });
148                }
149            }
150            Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { .. }) => {
151                if seen_non_constant {
152                    errors.push(ValidationError::InitialFillNotFirst { pc });
153                }
154                seen_non_constant = true;
155            }
156            Instruction::AtomArrangement(AtomArrangementInstruction::Fill { .. })
157            | Instruction::AtomArrangement(AtomArrangementInstruction::Move { .. })
158            | Instruction::QuantumGate(QuantumGateInstruction::LocalR { .. })
159            | Instruction::QuantumGate(QuantumGateInstruction::LocalRz { .. })
160            | Instruction::Measurement(MeasurementInstruction::Measure { .. }) => {
161                seen_non_constant = true;
162            }
163            // Constants don't set seen_non_constant
164            Instruction::Cpu(CpuInstruction::ConstFloat(_))
165            | Instruction::Cpu(CpuInstruction::ConstInt(_))
166            | Instruction::LaneConst(LaneConstInstruction::ConstLoc(_))
167            | Instruction::LaneConst(LaneConstInstruction::ConstLane(_, _))
168            | Instruction::LaneConst(LaneConstInstruction::ConstZone(_)) => {}
169            // All other instructions are non-constant
170            _ => {
171                seen_non_constant = true;
172            }
173        }
174    }
175
176    // Scan forward for unreachable instructions (any instruction after a return/halt).
177    // If unreachable instructions exist, they explain why the last instruction isn't a
178    // terminator, so MissingTerminator would be a redundant and misleading second error.
179    let mut found_terminator = false;
180    let mut unreachable_errors: Vec<ValidationError> = Vec::new();
181    for (pc, instr) in program.instructions.iter().enumerate() {
182        if found_terminator {
183            unreachable_errors.push(ValidationError::UnreachableInstruction { pc });
184        }
185        if matches!(
186            instr,
187            Instruction::Cpu(CpuInstruction::Return) | Instruction::Cpu(CpuInstruction::Halt)
188        ) {
189            found_terminator = true;
190        }
191    }
192
193    if unreachable_errors.is_empty() {
194        // No internal terminator found: check that the program ends with one.
195        match program.instructions.last() {
196            None => {
197                errors.push(ValidationError::EmptyProgram);
198            }
199            Some(last) => {
200                if !matches!(
201                    last,
202                    Instruction::Cpu(CpuInstruction::Return)
203                        | Instruction::Cpu(CpuInstruction::Halt)
204                ) {
205                    errors.push(ValidationError::MissingTerminator {
206                        pc: program.instructions.len() - 1,
207                    });
208                }
209            }
210        }
211    } else {
212        errors.extend(unreachable_errors);
213    }
214
215    errors
216}
217
218/// Validate addresses in the program against an architecture specification.
219/// Checks each constant location, lane, and zone instruction to ensure the
220/// encoded address refers to valid entries in the arch spec.
221pub fn validate_addresses(program: &Program, arch: &ArchSpec) -> Vec<ValidationError> {
222    let mut errors = Vec::new();
223
224    for (pc, instr) in program.instructions.iter().enumerate() {
225        match instr {
226            Instruction::LaneConst(LaneConstInstruction::ConstLoc(bits)) => {
227                let addr = LocationAddr::decode(*bits);
228                if arch.check_location(&addr).is_some() {
229                    errors.push(ValidationError::LocationGroupValidation {
230                        pc,
231                        error: LocationGroupError::InvalidAddress {
232                            zone_id: addr.zone_id,
233                            word_id: addr.word_id,
234                            site_id: addr.site_id,
235                        },
236                    });
237                }
238            }
239            Instruction::LaneConst(LaneConstInstruction::ConstLane(d0, d1)) => {
240                let addr = LaneAddr::decode(*d0, *d1);
241                for msg in arch.check_lane(&addr) {
242                    errors.push(ValidationError::LaneGroupValidation {
243                        pc,
244                        error: LaneGroupError::InvalidLane { message: msg },
245                    });
246                }
247            }
248            Instruction::LaneConst(LaneConstInstruction::ConstZone(bits)) => {
249                let addr = ZoneAddr::decode(*bits);
250                if arch.check_zone(&addr).is_some() {
251                    errors.push(ValidationError::InvalidZone {
252                        pc,
253                        zone_id: addr.zone_id,
254                    });
255                }
256            }
257            _ => {}
258        }
259    }
260
261    errors
262}
263
264/// Validate device capability constraints against the program.
265///
266/// Checks:
267/// - If `feed_forward` is false, at most one `measure` instruction is allowed.
268/// - If `atom_reloading` is false, no `fill` instruction is allowed.
269pub fn validate_capabilities(program: &Program, arch: &ArchSpec) -> Vec<ValidationError> {
270    let mut errors = Vec::new();
271    let mut measure_count = 0u32;
272
273    for (pc, instr) in program.instructions.iter().enumerate() {
274        match instr {
275            Instruction::Measurement(MeasurementInstruction::Measure { .. }) => {
276                measure_count += 1;
277                if !arch.feed_forward && measure_count > 1 {
278                    errors.push(ValidationError::FeedForwardNotSupported { pc });
279                }
280            }
281            Instruction::AtomArrangement(AtomArrangementInstruction::Fill { .. })
282                if !arch.atom_reloading =>
283            {
284                errors.push(ValidationError::AtomReloadingNotSupported { pc });
285            }
286            _ => {}
287        }
288    }
289
290    errors
291}
292
293/// Validate all architecture-dependent constraints (addresses + capabilities).
294///
295/// Convenience helper that runs both [`validate_addresses`] and
296/// [`validate_capabilities`] so callers get consistent architecture
297/// validation in a single call.
298pub fn validate_arch_constraints(program: &Program, arch: &ArchSpec) -> Vec<ValidationError> {
299    let mut errors = validate_addresses(program, arch);
300    errors.extend(validate_capabilities(program, arch));
301    errors
302}
303
304/// Stack simulation entry — tracks type tag and optionally the concrete value.
305#[derive(Debug, Clone)]
306struct SimEntry {
307    tag: u8,
308    #[allow(dead_code)]
309    value: Option<u64>,
310}
311
312/// Type-level stack simulator that walks an instruction stream, tracking types
313/// and concrete values. Validates stack discipline (underflow, type mismatches)
314/// and, when given an `ArchSpec`, validates lane groups at `Move` instructions.
315struct StackSimulator<'a> {
316    stack: Vec<SimEntry>,
317    errors: Vec<ValidationError>,
318    arch: Option<&'a ArchSpec>,
319    pc: usize,
320}
321
322impl<'a> StackSimulator<'a> {
323    fn new(arch: Option<&'a ArchSpec>) -> Self {
324        Self {
325            stack: Vec::new(),
326            errors: Vec::new(),
327            arch,
328            pc: 0,
329        }
330    }
331
332    // --- Primitives ---
333
334    /// Pop one value of any type. Records `StackUnderflow` if the stack is empty.
335    fn pop_any(&mut self) {
336        if self.stack.pop().is_none() {
337            self.errors
338                .push(ValidationError::StackUnderflow { pc: self.pc });
339        }
340    }
341
342    /// Pop one value and check its type tag. Records `StackUnderflow` or `TypeMismatch`.
343    fn pop_typed(&mut self, expected_tag: u8) {
344        match self.stack.pop() {
345            Some(entry) if entry.tag != expected_tag => {
346                self.errors.push(ValidationError::TypeMismatch {
347                    pc: self.pc,
348                    expected: expected_tag,
349                    got: entry.tag,
350                });
351            }
352            Some(_) => {}
353            None => {
354                self.errors
355                    .push(ValidationError::StackUnderflow { pc: self.pc });
356            }
357        }
358    }
359
360    /// Pop `count` values, each checked against `expected_tag`.
361    fn pop_typed_n(&mut self, expected_tag: u8, count: u32) {
362        for _ in 0..count {
363            self.pop_typed(expected_tag);
364        }
365    }
366
367    /// Pop one lane-typed value, returning the concrete bits if valid.
368    fn pop_lane(&mut self) -> Option<u64> {
369        match self.stack.pop() {
370            Some(entry) => {
371                if entry.tag != TAG_LANE {
372                    self.errors.push(ValidationError::TypeMismatch {
373                        pc: self.pc,
374                        expected: TAG_LANE,
375                        got: entry.tag,
376                    });
377                    None
378                } else {
379                    entry.value
380                }
381            }
382            None => {
383                self.errors
384                    .push(ValidationError::StackUnderflow { pc: self.pc });
385                None
386            }
387        }
388    }
389
390    /// Pop one location-typed value, returning the concrete bits if valid.
391    fn pop_location(&mut self) -> Option<u64> {
392        match self.stack.pop() {
393            Some(entry) => {
394                if entry.tag != TAG_LOCATION {
395                    self.errors.push(ValidationError::TypeMismatch {
396                        pc: self.pc,
397                        expected: TAG_LOCATION,
398                        got: entry.tag,
399                    });
400                    None
401                } else {
402                    entry.value
403                }
404            }
405            None => {
406                self.errors
407                    .push(ValidationError::StackUnderflow { pc: self.pc });
408                None
409            }
410        }
411    }
412
413    /// Wrap `LocationGroupError`s as `ValidationError`s with the current `pc`.
414    fn push_location_group_errors(&mut self, errors: Vec<LocationGroupError>) {
415        let pc = self.pc;
416        for error in errors {
417            self.errors
418                .push(ValidationError::LocationGroupValidation { pc, error });
419        }
420    }
421
422    /// Wrap `LaneGroupError`s as `ValidationError`s with the current `pc`.
423    fn push_lane_group_errors(&mut self, errors: Vec<LaneGroupError>) {
424        let pc = self.pc;
425        for error in errors {
426            self.errors
427                .push(ValidationError::LaneGroupValidation { pc, error });
428        }
429    }
430
431    /// Check for duplicate locations without an arch spec (standalone duplicate check).
432    /// Reports each unique duplicated address once.
433    fn check_duplicate_locations_standalone(&mut self, locations: &[LocationAddr]) {
434        let mut seen = HashSet::new();
435        let mut reported = HashSet::new();
436        for loc in locations {
437            let bits = loc.encode();
438            if !seen.insert(bits) && reported.insert(bits) {
439                self.errors.push(ValidationError::LocationGroupValidation {
440                    pc: self.pc,
441                    error: LocationGroupError::DuplicateAddress { address: bits },
442                });
443            }
444        }
445    }
446
447    /// Check for duplicate lanes without an arch spec (standalone duplicate check).
448    /// Reports each unique duplicated address once.
449    fn check_duplicate_lanes_standalone(&mut self, lanes: &[LaneAddr]) {
450        let mut seen = HashSet::new();
451        let mut reported = HashSet::new();
452        for lane in lanes {
453            let (d0, d1) = lane.encode();
454            let combined = (d0 as u64) | ((d1 as u64) << 32);
455            if !seen.insert(combined) && reported.insert(combined) {
456                self.errors.push(ValidationError::LaneGroupValidation {
457                    pc: self.pc,
458                    error: LaneGroupError::DuplicateAddress { address: (d0, d1) },
459                });
460            }
461        }
462    }
463
464    // --- Instruction handlers ---
465
466    /// Push a typed constant value onto the simulation stack.
467    fn push_const(&mut self, tag: u8, bits: u64) {
468        self.stack.push(SimEntry {
469            tag,
470            value: Some(bits),
471        });
472    }
473
474    /// Duplicate the top stack entry.
475    fn sim_dup(&mut self) {
476        if let Some(top) = self.stack.last().cloned() {
477            self.stack.push(top);
478        } else {
479            self.errors
480                .push(ValidationError::StackUnderflow { pc: self.pc });
481        }
482    }
483
484    /// Swap the top two stack entries.
485    fn sim_swap(&mut self) {
486        let len = self.stack.len();
487        if len >= 2 {
488            self.stack.swap(len - 1, len - 2);
489        } else {
490            self.errors
491                .push(ValidationError::StackUnderflow { pc: self.pc });
492        }
493    }
494
495    /// Pop `arity` location-typed values and validate the group
496    /// (duplicates + arch spec checks if available).
497    fn pop_and_validate_locations(&mut self, arity: u32) {
498        let loc_values: Vec<Option<u64>> = (0..arity).map(|_| self.pop_location()).collect();
499        let locations: Vec<LocationAddr> = loc_values
500            .iter()
501            .filter_map(|v| v.map(LocationAddr::decode))
502            .collect();
503        if let Some(arch) = self.arch {
504            self.push_location_group_errors(arch.check_locations(&locations));
505        } else {
506            self.check_duplicate_locations_standalone(&locations);
507        }
508    }
509
510    /// Simulate `initial_fill` or `fill`: pop `arity` location-typed values and validate.
511    fn sim_fill(&mut self, arity: u32) {
512        self.pop_and_validate_locations(arity);
513    }
514
515    /// Simulate `move`: pop `arity` lane-typed values. When an `ArchSpec` is
516    /// available, validates duplicates, consistency, membership, and AOD constraints
517    /// via the unified `check_lanes`. Without arch, only checks duplicates.
518    fn sim_move(&mut self, arity: u32) {
519        let lane_values: Vec<Option<u64>> = (0..arity).map(|_| self.pop_lane()).collect();
520        let lanes: Vec<LaneAddr> = lane_values
521            .iter()
522            .filter_map(|v| {
523                v.map(|bits| {
524                    let d0 = bits as u32;
525                    let d1 = (bits >> 32) as u32;
526                    LaneAddr::decode(d0, d1)
527                })
528            })
529            .collect();
530        if let Some(arch) = self.arch {
531            self.push_lane_group_errors(arch.check_lanes(&lanes));
532        } else {
533            self.check_duplicate_lanes_standalone(&lanes);
534        }
535    }
536
537    /// Simulate `local_r`: pop 2 float parameters (axis_angle, then rotation_angle) then validate locations.
538    fn sim_local_r(&mut self, arity: u32) {
539        self.pop_typed_n(TAG_FLOAT, 2);
540        self.pop_and_validate_locations(arity);
541    }
542
543    /// Simulate `local_rz`: pop 1 float parameter (rotation_angle) then validate locations.
544    fn sim_local_rz(&mut self, arity: u32) {
545        self.pop_typed_n(TAG_FLOAT, 1);
546        self.pop_and_validate_locations(arity);
547    }
548
549    /// Simulate `global_r`: pop 2 float parameters (axis_angle, then rotation_angle) for a global rotation.
550    fn sim_global_r(&mut self) {
551        self.pop_typed_n(TAG_FLOAT, 2);
552    }
553
554    /// Simulate `global_rz`: pop 1 float parameter (rotation_angle) for a global Z-rotation.
555    fn sim_global_rz(&mut self) {
556        self.pop_typed_n(TAG_FLOAT, 1);
557    }
558
559    /// Simulate `cz`: pop 1 zone value for a controlled-Z gate.
560    fn sim_cz(&mut self) {
561        self.pop_typed(TAG_ZONE);
562    }
563
564    /// Simulate `measure`: pop `arity` zone values and push `arity` measure futures.
565    fn sim_measure(&mut self, arity: u32) {
566        self.pop_typed_n(TAG_ZONE, arity);
567        for _ in 0..arity {
568            self.stack.push(SimEntry {
569                tag: TAG_MEASURE_FUTURE,
570                value: None,
571            });
572        }
573    }
574
575    /// Simulate `await_measure`: pop 1 measure future and push an array reference.
576    fn sim_await_measure(&mut self) {
577        self.pop_typed(TAG_MEASURE_FUTURE);
578        self.stack.push(SimEntry {
579            tag: TAG_ARRAY_REF,
580            value: None,
581        });
582    }
583
584    /// Simulate `new_array`: pop `dim0 * max(dim1, 1)` values of any type and push an array reference.
585    fn sim_new_array(&mut self, dim0: u16, dim1: u16) {
586        let count = dim0 * if dim1 == 0 { 1 } else { dim1 };
587        for _ in 0..count {
588            self.pop_any();
589        }
590        self.stack.push(SimEntry {
591            tag: TAG_ARRAY_REF,
592            value: None,
593        });
594    }
595
596    /// Simulate `get_item`: pop `ndims` int indices and 1 array reference, then push the
597    /// element value (assumed float since element type is not tracked).
598    fn sim_get_item(&mut self, ndims: u16) {
599        self.pop_typed_n(TAG_INT, ndims.into());
600        self.pop_typed(TAG_ARRAY_REF);
601        self.stack.push(SimEntry {
602            tag: TAG_FLOAT,
603            value: None,
604        });
605    }
606
607    /// Simulate `set_detector` or `set_observable`: pop 1 array reference and push a
608    /// typed reference (detector or observable) identified by `out_tag`.
609    fn sim_set_ref(&mut self, out_tag: u8) {
610        self.pop_typed(TAG_ARRAY_REF);
611        self.stack.push(SimEntry {
612            tag: out_tag,
613            value: None,
614        });
615    }
616
617    /// Simulate `return`: pop 1 value of any type as the return value.
618    fn sim_return(&mut self) {
619        self.pop_any();
620    }
621
622    // --- Main simulation loop ---
623
624    /// Dispatch a single instruction to the appropriate handler.
625    fn dispatch(&mut self, inst: &Instruction) {
626        match inst {
627            Instruction::Cpu(CpuInstruction::ConstFloat(v)) => {
628                self.push_const(TAG_FLOAT, v.to_bits());
629            }
630            Instruction::Cpu(CpuInstruction::ConstInt(v)) => {
631                self.push_const(TAG_INT, *v as u64);
632            }
633            Instruction::LaneConst(LaneConstInstruction::ConstLoc(v)) => {
634                self.push_const(TAG_LOCATION, *v);
635            }
636            Instruction::LaneConst(LaneConstInstruction::ConstLane(d0, d1)) => {
637                let combined = (*d0 as u64) | ((*d1 as u64) << 32);
638                self.push_const(TAG_LANE, combined);
639            }
640            Instruction::LaneConst(LaneConstInstruction::ConstZone(v)) => {
641                self.push_const(TAG_ZONE, *v as u64);
642            }
643
644            Instruction::Cpu(CpuInstruction::Pop) => self.pop_any(),
645            Instruction::Cpu(CpuInstruction::Dup) => self.sim_dup(),
646            Instruction::Cpu(CpuInstruction::Swap) => self.sim_swap(),
647
648            Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity })
649            | Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity }) => {
650                self.sim_fill(*arity);
651            }
652            Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity }) => {
653                self.sim_move(*arity);
654            }
655
656            Instruction::QuantumGate(QuantumGateInstruction::LocalR { arity }) => {
657                self.sim_local_r(*arity);
658            }
659            Instruction::QuantumGate(QuantumGateInstruction::LocalRz { arity }) => {
660                self.sim_local_rz(*arity);
661            }
662            Instruction::QuantumGate(QuantumGateInstruction::GlobalR) => self.sim_global_r(),
663            Instruction::QuantumGate(QuantumGateInstruction::GlobalRz) => self.sim_global_rz(),
664            Instruction::QuantumGate(QuantumGateInstruction::CZ) => self.sim_cz(),
665
666            Instruction::Measurement(MeasurementInstruction::Measure { arity }) => {
667                self.sim_measure(*arity);
668            }
669            Instruction::Measurement(MeasurementInstruction::AwaitMeasure) => {
670                self.sim_await_measure()
671            }
672
673            Instruction::Array(ArrayInstruction::NewArray {
674                type_tag: _,
675                dim0,
676                dim1,
677            }) => {
678                self.sim_new_array(*dim0, *dim1);
679            }
680            Instruction::Array(ArrayInstruction::GetItem { ndims }) => {
681                self.sim_get_item(*ndims);
682            }
683            Instruction::DetectorObservable(DetectorObservableInstruction::SetDetector) => {
684                self.sim_set_ref(TAG_DETECTOR_REF);
685            }
686            Instruction::DetectorObservable(DetectorObservableInstruction::SetObservable) => {
687                self.sim_set_ref(TAG_OBSERVABLE_REF);
688            }
689
690            Instruction::Cpu(CpuInstruction::Return) => self.sim_return(),
691            Instruction::Cpu(CpuInstruction::Halt) => {}
692        }
693    }
694
695    /// Run the stack simulation over the entire instruction stream.
696    /// Collects type errors, stack underflow errors, and (when an `ArchSpec` is
697    /// available) lane group validation errors.
698    fn run(mut self, program: &Program) -> Vec<ValidationError> {
699        for (pc, inst) in program.instructions.iter().enumerate() {
700            self.pc = pc;
701            self.dispatch(inst);
702        }
703        self.errors
704    }
705}
706
707/// Simulate the type-level stack through the instruction stream.
708/// Collects type errors and stack underflow errors.
709/// If an `ArchSpec` is provided, also validates lane groups at `Move` instructions.
710pub fn simulate_stack(program: &Program, arch: Option<&ArchSpec>) -> Vec<ValidationError> {
711    let sim = StackSimulator::new(arch);
712    sim.run(program)
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use crate::arch::addr::{Direction, LaneAddr, MoveType, SiteRef, WordRef};
719    use crate::arch::types::{Bus, Grid, Mode, Word, Zone};
720    use crate::version::Version;
721
722    // --- Structural validation tests ---
723
724    #[test]
725    fn test_valid_program_no_errors() {
726        let program = Program {
727            version: Version::new(1, 0),
728            instructions: vec![
729                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
730                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
731                Instruction::Cpu(CpuInstruction::Halt),
732            ],
733        };
734        assert!(validate_structure(&program).is_empty());
735    }
736
737    #[test]
738    fn test_initial_fill_not_first() {
739        let program = Program {
740            version: Version::new(1, 0),
741            instructions: vec![
742                // Move is a non-constant, non-terminator instruction
743                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 0 }),
744                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
745                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
746                Instruction::Cpu(CpuInstruction::Halt),
747            ],
748        };
749        let errors = validate_structure(&program);
750        assert!(
751            errors
752                .iter()
753                .any(|e| matches!(e, ValidationError::InitialFillNotFirst { pc: 2 }))
754        );
755    }
756
757    #[test]
758    fn test_initial_fill_after_constants_ok() {
759        let program = Program {
760            version: Version::new(1, 0),
761            instructions: vec![
762                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
763                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
764                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
765                Instruction::Cpu(CpuInstruction::Halt),
766            ],
767        };
768        assert!(validate_structure(&program).is_empty());
769    }
770
771    #[test]
772    fn test_new_array_zero_dim0() {
773        let program = Program {
774            version: Version::new(1, 0),
775            instructions: vec![
776                Instruction::Array(ArrayInstruction::NewArray {
777                    type_tag: 0,
778                    dim0: 0,
779                    dim1: 0,
780                }),
781                Instruction::Cpu(CpuInstruction::Halt),
782            ],
783        };
784        let errors = validate_structure(&program);
785        assert!(
786            errors
787                .iter()
788                .any(|e| matches!(e, ValidationError::NewArrayZeroDim0 { pc: 0 }))
789        );
790    }
791
792    #[test]
793    fn test_new_array_invalid_type_tag() {
794        let program = Program {
795            version: Version::new(1, 0),
796            instructions: vec![
797                Instruction::Array(ArrayInstruction::NewArray {
798                    type_tag: 0xF,
799                    dim0: 1,
800                    dim1: 0,
801                }),
802                Instruction::Cpu(CpuInstruction::Halt),
803            ],
804        };
805        let errors = validate_structure(&program);
806        assert!(errors.iter().any(|e| matches!(
807            e,
808            ValidationError::NewArrayInvalidTypeTag {
809                pc: 0,
810                type_tag: 0xF
811            }
812        )));
813    }
814
815    // --- Terminator validation tests ---
816
817    #[test]
818    fn test_empty_program_rejected() {
819        let program = Program {
820            version: Version::new(1, 0),
821            instructions: vec![],
822        };
823        let errors = validate_structure(&program);
824        assert!(
825            errors
826                .iter()
827                .any(|e| matches!(e, ValidationError::EmptyProgram)),
828            "expected EmptyProgram error, got: {:?}",
829            errors
830        );
831    }
832
833    #[test]
834    fn test_missing_terminator_rejected() {
835        let program = Program {
836            version: Version::new(1, 0),
837            instructions: vec![Instruction::Cpu(CpuInstruction::ConstInt(0))],
838        };
839        let errors = validate_structure(&program);
840        assert!(
841            errors
842                .iter()
843                .any(|e| matches!(e, ValidationError::MissingTerminator { pc: 0 })),
844            "expected MissingTerminator at pc 0, got: {:?}",
845            errors
846        );
847    }
848
849    #[test]
850    fn test_terminator_return_ok() {
851        let program = Program {
852            version: Version::new(1, 0),
853            instructions: vec![
854                Instruction::Cpu(CpuInstruction::ConstInt(0)),
855                Instruction::Cpu(CpuInstruction::Return),
856            ],
857        };
858        assert!(validate_structure(&program).is_empty());
859    }
860
861    #[test]
862    fn test_terminator_halt_ok() {
863        let program = Program {
864            version: Version::new(1, 0),
865            instructions: vec![Instruction::Cpu(CpuInstruction::Halt)],
866        };
867        assert!(validate_structure(&program).is_empty());
868    }
869
870    #[test]
871    fn test_unreachable_instruction_after_halt() {
872        let program = Program {
873            version: Version::new(1, 0),
874            instructions: vec![
875                Instruction::Cpu(CpuInstruction::Halt),
876                Instruction::Cpu(CpuInstruction::ConstInt(0)),
877            ],
878        };
879        let errors = validate_structure(&program);
880        assert!(
881            errors
882                .iter()
883                .any(|e| matches!(e, ValidationError::UnreachableInstruction { pc: 1 })),
884            "expected UnreachableInstruction at pc 1, got: {:?}",
885            errors
886        );
887    }
888
889    #[test]
890    fn test_unreachable_instruction_after_return() {
891        let program = Program {
892            version: Version::new(1, 0),
893            instructions: vec![
894                Instruction::Cpu(CpuInstruction::ConstInt(0)),
895                Instruction::Cpu(CpuInstruction::Return),
896                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
897            ],
898        };
899        let errors = validate_structure(&program);
900        assert!(
901            errors
902                .iter()
903                .any(|e| matches!(e, ValidationError::UnreachableInstruction { pc: 2 })),
904            "expected UnreachableInstruction at pc 2, got: {:?}",
905            errors
906        );
907    }
908
909    // --- Stack simulation tests ---
910
911    #[test]
912    fn test_stack_sim_valid_program() {
913        let program = Program {
914            version: Version::new(1, 0),
915            instructions: vec![
916                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
917                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
918                Instruction::Cpu(CpuInstruction::Halt),
919            ],
920        };
921        assert!(simulate_stack(&program, None).is_empty());
922    }
923
924    #[test]
925    fn test_stack_sim_underflow() {
926        let program = Program {
927            version: Version::new(1, 0),
928            instructions: vec![Instruction::Cpu(CpuInstruction::Pop)],
929        };
930        let errors = simulate_stack(&program, None);
931        assert_eq!(errors.len(), 1);
932        assert!(matches!(
933            errors[0],
934            ValidationError::StackUnderflow { pc: 0 }
935        ));
936    }
937
938    #[test]
939    fn test_stack_sim_type_mismatch() {
940        let program = Program {
941            version: Version::new(1, 0),
942            instructions: vec![
943                // Push a float, try to use it as a location for InitialFill
944                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
945                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
946            ],
947        };
948        let errors = simulate_stack(&program, None);
949        assert_eq!(errors.len(), 1);
950        assert!(matches!(
951            errors[0],
952            ValidationError::TypeMismatch {
953                pc: 1,
954                expected,
955                got
956            } if expected == TAG_LOCATION && got == TAG_FLOAT
957        ));
958    }
959
960    #[test]
961    fn test_stack_sim_move() {
962        let program = Program {
963            version: Version::new(1, 0),
964            instructions: vec![
965                Instruction::LaneConst(LaneConstInstruction::ConstLane(0x100, 0)),
966                Instruction::LaneConst(LaneConstInstruction::ConstLane(0x200, 0)),
967                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
968            ],
969        };
970        assert!(simulate_stack(&program, None).is_empty());
971    }
972
973    #[test]
974    fn test_stack_sim_local_r() {
975        let program = Program {
976            version: Version::new(1, 0),
977            instructions: vec![
978                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
979                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)), // rotation_angle (pushed first)
980                Instruction::Cpu(CpuInstruction::ConstFloat(0.5)), // axis_angle (pushed last, popped first)
981                Instruction::QuantumGate(QuantumGateInstruction::LocalR { arity: 1 }),
982            ],
983        };
984        assert!(simulate_stack(&program, None).is_empty());
985    }
986
987    #[test]
988    fn test_stack_sim_measure_and_await() {
989        let program = Program {
990            version: Version::new(1, 0),
991            instructions: vec![
992                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
993                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
994                Instruction::Measurement(MeasurementInstruction::AwaitMeasure),
995                Instruction::Cpu(CpuInstruction::Return),
996            ],
997        };
998        assert!(simulate_stack(&program, None).is_empty());
999    }
1000
1001    #[test]
1002    fn test_stack_sim_dup_and_swap() {
1003        let program = Program {
1004            version: Version::new(1, 0),
1005            instructions: vec![
1006                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
1007                Instruction::Cpu(CpuInstruction::Dup),
1008                Instruction::QuantumGate(QuantumGateInstruction::GlobalR),
1009            ],
1010        };
1011        assert!(simulate_stack(&program, None).is_empty());
1012    }
1013
1014    #[test]
1015    fn test_stack_sim_cz_type_mismatch() {
1016        let program = Program {
1017            version: Version::new(1, 0),
1018            instructions: vec![
1019                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
1020                Instruction::QuantumGate(QuantumGateInstruction::CZ),
1021            ],
1022        };
1023        let errors = simulate_stack(&program, None);
1024        assert_eq!(errors.len(), 1);
1025        assert!(matches!(
1026            errors[0],
1027            ValidationError::TypeMismatch {
1028                pc: 1,
1029                expected,
1030                got
1031            } if expected == TAG_ZONE && got == TAG_FLOAT
1032        ));
1033    }
1034
1035    #[test]
1036    fn test_stack_sim_set_detector() {
1037        let program = Program {
1038            version: Version::new(1, 0),
1039            instructions: vec![
1040                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1041                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1042                Instruction::Measurement(MeasurementInstruction::AwaitMeasure),
1043                Instruction::DetectorObservable(DetectorObservableInstruction::SetDetector),
1044                Instruction::Cpu(CpuInstruction::Return),
1045            ],
1046        };
1047        assert!(simulate_stack(&program, None).is_empty());
1048    }
1049
1050    // --- Address validation tests ---
1051
1052    fn test_arch_spec() -> ArchSpec {
1053        // 1 word with 2 sites, 1 zone with site bus 0->1
1054        let grid = Grid::from_positions(&[0.0, 5.0], &[0.0]);
1055        ArchSpec {
1056            version: Version::new(2, 0),
1057            words: vec![Word {
1058                sites: vec![[0, 0], [1, 0]],
1059            }],
1060            zones: vec![Zone {
1061                name: String::new(),
1062                grid,
1063                site_buses: vec![Bus {
1064                    src: vec![SiteRef(0)],
1065                    dst: vec![SiteRef(1)],
1066                }],
1067                word_buses: vec![],
1068                words_with_site_buses: vec![0],
1069                sites_with_word_buses: vec![],
1070                entangling_pairs: vec![],
1071            }],
1072            zone_buses: vec![],
1073            modes: vec![Mode {
1074                name: "full".to_string(),
1075                zones: vec![0],
1076                bitstring_order: vec![],
1077            }],
1078            paths: None,
1079            feed_forward: false,
1080            atom_reloading: false,
1081            blockade_radius: None,
1082        }
1083    }
1084
1085    #[test]
1086    fn test_addr_valid_location() {
1087        let arch = test_arch_spec();
1088        // Encode (zone_id=0, word_id=0, site_id=1) — valid since word 0 has 2 sites
1089        let valid_loc = LocationAddr {
1090            zone_id: 0,
1091            word_id: 0,
1092            site_id: 1,
1093        }
1094        .encode();
1095        let program = Program {
1096            version: Version::new(1, 0),
1097            instructions: vec![Instruction::LaneConst(LaneConstInstruction::ConstLoc(
1098                valid_loc,
1099            ))],
1100        };
1101        assert!(validate_addresses(&program, &arch).is_empty());
1102    }
1103
1104    #[test]
1105    fn test_addr_invalid_location() {
1106        let arch = test_arch_spec();
1107        // Encode (zone_id=0, word_id=0, site_id=5) — invalid because word 0 has only 2 sites
1108        let bad_loc = LocationAddr {
1109            zone_id: 0,
1110            word_id: 0,
1111            site_id: 5,
1112        }
1113        .encode();
1114        let program = Program {
1115            version: Version::new(1, 0),
1116            instructions: vec![Instruction::LaneConst(LaneConstInstruction::ConstLoc(
1117                bad_loc,
1118            ))],
1119        };
1120        let errors = validate_addresses(&program, &arch);
1121        assert_eq!(errors.len(), 1);
1122        assert!(matches!(
1123            errors[0],
1124            ValidationError::LocationGroupValidation {
1125                pc: 0,
1126                error: LocationGroupError::InvalidAddress {
1127                    zone_id: 0,
1128                    word_id: 0,
1129                    site_id: 5
1130                }
1131            }
1132        ));
1133    }
1134
1135    #[test]
1136    fn test_addr_invalid_zone() {
1137        let arch = test_arch_spec();
1138        let program = Program {
1139            version: Version::new(1, 0),
1140            instructions: vec![Instruction::LaneConst(LaneConstInstruction::ConstZone(99))],
1141        };
1142        let errors = validate_addresses(&program, &arch);
1143        assert_eq!(errors.len(), 1);
1144        assert!(matches!(
1145            errors[0],
1146            ValidationError::InvalidZone { pc: 0, zone_id: 99 }
1147        ));
1148    }
1149
1150    #[test]
1151    fn test_addr_valid_lane() {
1152        let arch = test_arch_spec();
1153        let program = Program {
1154            version: Version::new(1, 0),
1155            instructions: vec![Instruction::LaneConst(LaneConstInstruction::ConstLane(
1156                0x00000000, 0x00000000,
1157            ))],
1158        };
1159        assert!(validate_addresses(&program, &arch).is_empty());
1160    }
1161
1162    #[test]
1163    fn test_addr_invalid_lane_bus() {
1164        let arch = test_arch_spec();
1165        // data0=0x00000000 (word_id=0, site_id=0), data1=0x00000005 (bus_id=5)
1166        let program = Program {
1167            version: Version::new(1, 0),
1168            instructions: vec![Instruction::LaneConst(LaneConstInstruction::ConstLane(
1169                0x00000000, 0x00000005,
1170            ))],
1171        };
1172        let errors = validate_addresses(&program, &arch);
1173        assert!(!errors.is_empty());
1174    }
1175
1176    // --- Lane group validation tests ---
1177
1178    fn lane_group_arch_spec() -> ArchSpec {
1179        // 2 words with 4 sites each. Zone 0 has site bus mapping [0,1]->[2,3].
1180        // Grid x=[0.0, 2.0, 4.0, 6.0], y=[0.0, 10.0] so sites land on a grid:
1181        //   Word 0: site0=(0.0, 0.0), site1=(2.0, 0.0), site2=(4.0, 0.0), site3=(6.0, 0.0)
1182        //   Word 1: site0=(0.0, 10.0), site1=(2.0, 10.0), site2=(4.0, 10.0), site3=(6.0, 10.0)
1183        let grid0 = Grid::from_positions(&[0.0, 2.0, 4.0, 6.0], &[0.0, 10.0]);
1184
1185        ArchSpec {
1186            version: Version::new(2, 0),
1187            words: vec![
1188                Word {
1189                    sites: vec![[0, 0], [1, 0], [2, 0], [3, 0]],
1190                },
1191                Word {
1192                    sites: vec![[0, 1], [1, 1], [2, 1], [3, 1]],
1193                },
1194            ],
1195            zones: vec![Zone {
1196                name: String::new(),
1197                grid: grid0,
1198                site_buses: vec![Bus {
1199                    src: vec![SiteRef(0), SiteRef(1)],
1200                    dst: vec![SiteRef(2), SiteRef(3)],
1201                }],
1202                word_buses: vec![Bus {
1203                    src: vec![WordRef(0)],
1204                    dst: vec![WordRef(1)],
1205                }],
1206                words_with_site_buses: vec![0, 1],
1207                sites_with_word_buses: vec![0],
1208                entangling_pairs: vec![],
1209            }],
1210            zone_buses: vec![],
1211            modes: vec![Mode {
1212                name: "full".to_string(),
1213                zones: vec![0],
1214                bitstring_order: vec![],
1215            }],
1216            paths: None,
1217            feed_forward: false,
1218            atom_reloading: false,
1219            blockade_radius: None,
1220        }
1221    }
1222
1223    fn make_lane(
1224        dir: Direction,
1225        mt: MoveType,
1226        word_id: u32,
1227        site_id: u32,
1228        bus_id: u32,
1229    ) -> (u32, u32) {
1230        LaneAddr {
1231            direction: dir,
1232            move_type: mt,
1233            zone_id: 0,
1234            word_id,
1235            site_id,
1236            bus_id,
1237        }
1238        .encode()
1239    }
1240
1241    #[test]
1242    fn test_lane_group_consistent_passes() {
1243        let arch = lane_group_arch_spec();
1244        // Both lanes: Forward, SiteBus, bus 0, site 0 (valid source), different words
1245        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0);
1246        let lane1 = make_lane(Direction::Forward, MoveType::SiteBus, 1, 0, 0);
1247        let program = Program {
1248            version: Version::new(1, 0),
1249            instructions: vec![
1250                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1251                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1252                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
1253            ],
1254        };
1255        let errors = simulate_stack(&program, Some(&arch));
1256        assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
1257    }
1258
1259    #[test]
1260    fn test_lane_group_inconsistent_bus_id() {
1261        let arch = lane_group_arch_spec();
1262        // Same direction and move type, but different bus_id (0 vs 1)
1263        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0);
1264        let lane1 = make_lane(Direction::Forward, MoveType::SiteBus, 1, 0, 1);
1265        let program = Program {
1266            version: Version::new(1, 0),
1267            instructions: vec![
1268                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1269                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1270                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
1271            ],
1272        };
1273        let errors = simulate_stack(&program, Some(&arch));
1274        assert!(errors.iter().any(|e| matches!(
1275            e,
1276            ValidationError::LaneGroupValidation {
1277                pc: 2,
1278                error: LaneGroupError::Inconsistent { .. }
1279            }
1280        )));
1281    }
1282
1283    #[test]
1284    fn test_lane_group_inconsistent_direction() {
1285        let arch = lane_group_arch_spec();
1286        // Same bus but different directions
1287        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0);
1288        let lane1 = make_lane(Direction::Backward, MoveType::SiteBus, 1, 0, 0);
1289        let program = Program {
1290            version: Version::new(1, 0),
1291            instructions: vec![
1292                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1293                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1294                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
1295            ],
1296        };
1297        let errors = simulate_stack(&program, Some(&arch));
1298        assert!(errors.iter().any(|e| matches!(
1299            e,
1300            ValidationError::LaneGroupValidation {
1301                pc: 2,
1302                error: LaneGroupError::Inconsistent { .. }
1303            }
1304        )));
1305    }
1306
1307    #[test]
1308    fn test_lane_group_word_not_in_site_bus_list() {
1309        // Build a spec where zone 0 has words_with_site_buses = [0] only
1310        // (word 1 is NOT in the site bus list).
1311        let grid = Grid::from_positions(&[0.0, 5.0, 10.0], &[0.0, 3.0]);
1312        let arch = ArchSpec {
1313            version: Version::new(2, 0),
1314            words: vec![
1315                Word {
1316                    sites: vec![[0, 0], [1, 0]],
1317                },
1318                Word {
1319                    sites: vec![[0, 0], [1, 0]],
1320                },
1321            ],
1322            zones: vec![Zone {
1323                name: String::new(),
1324                grid,
1325                site_buses: vec![Bus {
1326                    src: vec![SiteRef(0)],
1327                    dst: vec![SiteRef(1)],
1328                }],
1329                word_buses: vec![],
1330                words_with_site_buses: vec![0],
1331                sites_with_word_buses: vec![],
1332                entangling_pairs: vec![],
1333            }],
1334            zone_buses: vec![],
1335            modes: vec![Mode {
1336                name: "full".to_string(),
1337                zones: vec![0],
1338                bitstring_order: vec![],
1339            }],
1340            paths: None,
1341            feed_forward: false,
1342            atom_reloading: false,
1343            blockade_radius: None,
1344        };
1345
1346        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 1, 0, 0);
1347        let lane1 = make_lane(Direction::Forward, MoveType::SiteBus, 1, 1, 0);
1348        let program = Program {
1349            version: Version::new(1, 0),
1350            instructions: vec![
1351                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1352                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1353                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
1354            ],
1355        };
1356        let errors = simulate_stack(&program, Some(&arch));
1357        assert!(errors.iter().any(|e| matches!(
1358            e,
1359            ValidationError::LaneGroupValidation {
1360                pc: 2,
1361                error: LaneGroupError::WordNotInSiteBusList {
1362                    zone_id: 0,
1363                    word_id: 1
1364                }
1365            }
1366        )));
1367    }
1368
1369    #[test]
1370    fn test_lane_group_aod_constraint_rectangle_passes() {
1371        let arch = lane_group_arch_spec();
1372        // Use valid forward sources on two words to form a 2x2 grid:
1373        // Word 0, Site 0: (0.0, 0.0)
1374        // Word 0, Site 1: (2.0, 0.0)
1375        // Word 1, Site 0: (0.0, 10.0)
1376        // Word 1, Site 1: (2.0, 10.0)
1377        let lanes: Vec<(u32, u32)> = vec![
1378            make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0),
1379            make_lane(Direction::Forward, MoveType::SiteBus, 0, 1, 0),
1380            make_lane(Direction::Forward, MoveType::SiteBus, 1, 0, 0),
1381            make_lane(Direction::Forward, MoveType::SiteBus, 1, 1, 0),
1382        ];
1383        let program = Program {
1384            version: Version::new(1, 0),
1385            instructions: vec![
1386                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[0].0, lanes[0].1)),
1387                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[1].0, lanes[1].1)),
1388                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[2].0, lanes[2].1)),
1389                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[3].0, lanes[3].1)),
1390                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 4 }),
1391            ],
1392        };
1393        let errors = simulate_stack(&program, Some(&arch));
1394        assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
1395    }
1396
1397    #[test]
1398    fn test_lane_group_aod_constraint_not_rectangle() {
1399        let arch = lane_group_arch_spec();
1400        // 3 corners of a grid (missing word 1, site 1) — not a complete 2x2 grid
1401        // Word 0, Site 0: (0.0, 0.0)
1402        // Word 0, Site 1: (2.0, 0.0)
1403        // Word 1, Site 0: (0.0, 10.0)
1404        let lanes: Vec<(u32, u32)> = vec![
1405            make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0),
1406            make_lane(Direction::Forward, MoveType::SiteBus, 0, 1, 0),
1407            make_lane(Direction::Forward, MoveType::SiteBus, 1, 0, 0),
1408        ];
1409        let program = Program {
1410            version: Version::new(1, 0),
1411            instructions: vec![
1412                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[0].0, lanes[0].1)),
1413                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[1].0, lanes[1].1)),
1414                Instruction::LaneConst(LaneConstInstruction::ConstLane(lanes[2].0, lanes[2].1)),
1415                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 3 }),
1416            ],
1417        };
1418        let errors = simulate_stack(&program, Some(&arch));
1419        assert!(
1420            errors.iter().any(|e| matches!(
1421                e,
1422                ValidationError::LaneGroupValidation {
1423                    pc: 3,
1424                    error: LaneGroupError::AODConstraintViolation { .. }
1425                }
1426            )),
1427            "expected AOD constraint violation error, got: {:?}",
1428            errors
1429        );
1430    }
1431
1432    #[test]
1433    fn test_lane_group_no_arch_skips_validation() {
1434        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0);
1435        let lane1 = make_lane(Direction::Backward, MoveType::WordBus, 1, 1, 5);
1436        let program = Program {
1437            version: Version::new(1, 0),
1438            instructions: vec![
1439                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1440                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1441                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 2 }),
1442            ],
1443        };
1444        let errors = simulate_stack(&program, None);
1445        assert!(errors.is_empty());
1446    }
1447
1448    // --- Duplicate address validation tests ---
1449
1450    #[test]
1451    fn test_duplicate_location_in_initial_fill() {
1452        let addr = LocationAddr {
1453            zone_id: 0,
1454            word_id: 0,
1455            site_id: 9,
1456        }
1457        .encode();
1458        let program = Program {
1459            version: Version::new(1, 0),
1460            instructions: vec![
1461                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1462                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0x0007)),
1463                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1464                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 3 }),
1465                Instruction::Cpu(CpuInstruction::Halt),
1466            ],
1467        };
1468        let errors = simulate_stack(&program, None);
1469        assert!(
1470            errors
1471                .iter()
1472                .any(|e| matches!(e, ValidationError::LocationGroupValidation { pc: 3, error: LocationGroupError::DuplicateAddress { address } } if *address == addr)),
1473            "expected DuplicateLocationAddress, got: {:?}",
1474            errors
1475        );
1476    }
1477
1478    #[test]
1479    fn test_duplicate_location_in_fill() {
1480        let addr = LocationAddr {
1481            zone_id: 0,
1482            word_id: 1,
1483            site_id: 2,
1484        }
1485        .encode();
1486        let program = Program {
1487            version: Version::new(1, 0),
1488            instructions: vec![
1489                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1490                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1491                Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity: 2 }),
1492                Instruction::Cpu(CpuInstruction::Halt),
1493            ],
1494        };
1495        let errors = simulate_stack(&program, None);
1496        assert!(
1497            errors.iter().any(|e| matches!(
1498                e,
1499                ValidationError::LocationGroupValidation {
1500                    pc: 2,
1501                    error: LocationGroupError::DuplicateAddress { .. }
1502                }
1503            )),
1504            "expected DuplicateLocationAddress, got: {:?}",
1505            errors
1506        );
1507    }
1508
1509    #[test]
1510    fn test_duplicate_location_in_local_r() {
1511        let addr = LocationAddr {
1512            zone_id: 0,
1513            word_id: 0,
1514            site_id: 3,
1515        }
1516        .encode();
1517        let program = Program {
1518            version: Version::new(1, 0),
1519            instructions: vec![
1520                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1521                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1522                Instruction::Cpu(CpuInstruction::ConstFloat(0.5)),
1523                Instruction::Cpu(CpuInstruction::ConstFloat(1.0)),
1524                Instruction::QuantumGate(QuantumGateInstruction::LocalR { arity: 2 }),
1525            ],
1526        };
1527        let errors = simulate_stack(&program, None);
1528        assert!(
1529            errors.iter().any(|e| matches!(
1530                e,
1531                ValidationError::LocationGroupValidation {
1532                    pc: 4,
1533                    error: LocationGroupError::DuplicateAddress { .. }
1534                }
1535            )),
1536            "expected DuplicateLocationAddress, got: {:?}",
1537            errors
1538        );
1539    }
1540
1541    #[test]
1542    fn test_duplicate_location_in_local_rz() {
1543        let addr = LocationAddr {
1544            zone_id: 0,
1545            word_id: 0,
1546            site_id: 5,
1547        }
1548        .encode();
1549        let program = Program {
1550            version: Version::new(1, 0),
1551            instructions: vec![
1552                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1553                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1554                Instruction::Cpu(CpuInstruction::ConstFloat(0.5)),
1555                Instruction::QuantumGate(QuantumGateInstruction::LocalRz { arity: 2 }),
1556            ],
1557        };
1558        let errors = simulate_stack(&program, None);
1559        assert!(
1560            errors.iter().any(|e| matches!(
1561                e,
1562                ValidationError::LocationGroupValidation {
1563                    pc: 3,
1564                    error: LocationGroupError::DuplicateAddress { .. }
1565                }
1566            )),
1567            "expected DuplicateLocationAddress, got: {:?}",
1568            errors
1569        );
1570    }
1571
1572    #[test]
1573    fn test_duplicate_lane_in_move() {
1574        let lane = make_lane(Direction::Forward, MoveType::SiteBus, 0, 9, 0);
1575        let lane_other = make_lane(Direction::Forward, MoveType::SiteBus, 0, 7, 0);
1576        let program = Program {
1577            version: Version::new(1, 0),
1578            instructions: vec![
1579                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane.0, lane.1)),
1580                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane_other.0, lane_other.1)),
1581                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane.0, lane.1)),
1582                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 3 }),
1583            ],
1584        };
1585        let errors = simulate_stack(&program, None);
1586        assert!(
1587            errors
1588                .iter()
1589                .any(|e| matches!(e, ValidationError::LaneGroupValidation { pc: 3, error: LaneGroupError::DuplicateAddress { address } } if *address == lane)),
1590
1591            "expected DuplicateLaneAddress, got: {:?}",
1592            errors
1593        );
1594    }
1595
1596    #[test]
1597    fn test_duplicate_location_reported_once() {
1598        let addr = LocationAddr {
1599            zone_id: 0,
1600            word_id: 1,
1601            site_id: 2,
1602        }
1603        .encode();
1604        let program = Program {
1605            version: Version::new(1, 0),
1606            instructions: vec![
1607                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1608                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1609                Instruction::LaneConst(LaneConstInstruction::ConstLoc(addr)),
1610                Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity: 3 }),
1611                Instruction::Cpu(CpuInstruction::Halt),
1612            ],
1613        };
1614        let errors = simulate_stack(&program, None);
1615        let dup_count = errors
1616            .iter()
1617            .filter(|e| {
1618                matches!(
1619                    e,
1620                    ValidationError::LocationGroupValidation {
1621                        error: LocationGroupError::DuplicateAddress { .. },
1622                        ..
1623                    }
1624                )
1625            })
1626            .count();
1627        assert_eq!(
1628            dup_count, 1,
1629            "expected exactly 1 DuplicateAddress, got: {:?}",
1630            errors
1631        );
1632    }
1633
1634    #[test]
1635    fn test_duplicate_lane_reported_once() {
1636        let lane = make_lane(Direction::Forward, MoveType::SiteBus, 0, 9, 0);
1637        let program = Program {
1638            version: Version::new(1, 0),
1639            instructions: vec![
1640                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane.0, lane.1)),
1641                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane.0, lane.1)),
1642                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane.0, lane.1)),
1643                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 3 }),
1644            ],
1645        };
1646        let errors = simulate_stack(&program, None);
1647        let dup_count = errors
1648            .iter()
1649            .filter(|e| {
1650                matches!(
1651                    e,
1652                    ValidationError::LaneGroupValidation {
1653                        error: LaneGroupError::DuplicateAddress { .. },
1654                        ..
1655                    }
1656                )
1657            })
1658            .count();
1659        assert_eq!(
1660            dup_count, 1,
1661            "expected exactly 1 DuplicateAddress, got: {:?}",
1662            errors
1663        );
1664    }
1665
1666    #[test]
1667    fn test_no_duplicate_location_passes() {
1668        // Three distinct locations
1669        let loc0 = LocationAddr {
1670            zone_id: 0,
1671            word_id: 0,
1672            site_id: 0,
1673        }
1674        .encode();
1675        let loc1 = LocationAddr {
1676            zone_id: 0,
1677            word_id: 0,
1678            site_id: 1,
1679        }
1680        .encode();
1681        let loc2 = LocationAddr {
1682            zone_id: 0,
1683            word_id: 1,
1684            site_id: 0,
1685        }
1686        .encode();
1687        let program = Program {
1688            version: Version::new(1, 0),
1689            instructions: vec![
1690                Instruction::LaneConst(LaneConstInstruction::ConstLoc(loc0)),
1691                Instruction::LaneConst(LaneConstInstruction::ConstLoc(loc1)),
1692                Instruction::LaneConst(LaneConstInstruction::ConstLoc(loc2)),
1693                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 3 }),
1694                Instruction::Cpu(CpuInstruction::Halt),
1695            ],
1696        };
1697        let errors = simulate_stack(&program, None);
1698        assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
1699    }
1700
1701    // --- Capability validation tests ---
1702
1703    fn test_arch_with_caps(feed_forward: bool, atom_reloading: bool) -> ArchSpec {
1704        let mut arch = test_arch_spec();
1705        arch.feed_forward = feed_forward;
1706        arch.atom_reloading = atom_reloading;
1707        arch
1708    }
1709
1710    #[test]
1711    fn test_cap_single_measure_allowed() {
1712        let arch = test_arch_with_caps(false, false);
1713        let program = Program {
1714            version: Version::new(1, 0),
1715            instructions: vec![
1716                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1717                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1718            ],
1719        };
1720        assert!(validate_capabilities(&program, &arch).is_empty());
1721    }
1722
1723    #[test]
1724    fn test_cap_multiple_measure_rejected() {
1725        let arch = test_arch_with_caps(false, false);
1726        let program = Program {
1727            version: Version::new(1, 0),
1728            instructions: vec![
1729                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1730                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1731                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1732                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1733            ],
1734        };
1735        let errors = validate_capabilities(&program, &arch);
1736        assert_eq!(errors.len(), 1);
1737        assert!(matches!(
1738            errors[0],
1739            ValidationError::FeedForwardNotSupported { pc: 3 }
1740        ));
1741    }
1742
1743    #[test]
1744    fn test_cap_multiple_measure_with_feed_forward() {
1745        let arch = test_arch_with_caps(true, false);
1746        let program = Program {
1747            version: Version::new(1, 0),
1748            instructions: vec![
1749                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1750                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1751                Instruction::LaneConst(LaneConstInstruction::ConstZone(0)),
1752                Instruction::Measurement(MeasurementInstruction::Measure { arity: 1 }),
1753            ],
1754        };
1755        assert!(validate_capabilities(&program, &arch).is_empty());
1756    }
1757
1758    #[test]
1759    fn test_cap_fill_rejected() {
1760        let arch = test_arch_with_caps(false, false);
1761        let program = Program {
1762            version: Version::new(1, 0),
1763            instructions: vec![
1764                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
1765                Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity: 1 }),
1766            ],
1767        };
1768        let errors = validate_capabilities(&program, &arch);
1769        assert_eq!(errors.len(), 1);
1770        assert!(matches!(
1771            errors[0],
1772            ValidationError::AtomReloadingNotSupported { pc: 1 }
1773        ));
1774    }
1775
1776    #[test]
1777    fn test_cap_fill_with_atom_reloading() {
1778        let arch = test_arch_with_caps(false, true);
1779        let program = Program {
1780            version: Version::new(1, 0),
1781            instructions: vec![
1782                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
1783                Instruction::AtomArrangement(AtomArrangementInstruction::Fill { arity: 1 }),
1784            ],
1785        };
1786        assert!(validate_capabilities(&program, &arch).is_empty());
1787    }
1788
1789    #[test]
1790    fn test_cap_initial_fill_always_allowed() {
1791        let arch = test_arch_with_caps(false, false);
1792        let program = Program {
1793            version: Version::new(1, 0),
1794            instructions: vec![
1795                Instruction::LaneConst(LaneConstInstruction::ConstLoc(0)),
1796                Instruction::AtomArrangement(AtomArrangementInstruction::InitialFill { arity: 1 }),
1797            ],
1798        };
1799        assert!(validate_capabilities(&program, &arch).is_empty());
1800    }
1801
1802    #[test]
1803    fn test_no_duplicate_lane_passes() {
1804        let lane0 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 0, 0);
1805        let lane1 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 1, 0);
1806        let lane2 = make_lane(Direction::Forward, MoveType::SiteBus, 0, 2, 0);
1807        let program = Program {
1808            version: Version::new(1, 0),
1809            instructions: vec![
1810                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane0.0, lane0.1)),
1811                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane1.0, lane1.1)),
1812                Instruction::LaneConst(LaneConstInstruction::ConstLane(lane2.0, lane2.1)),
1813                Instruction::AtomArrangement(AtomArrangementInstruction::Move { arity: 3 }),
1814            ],
1815        };
1816        let errors = simulate_stack(&program, None);
1817        assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
1818    }
1819}