1use std::collections::HashSet;
34use std::fmt;
35
36use vihaco::value::Value;
37use vihaco_cpu::Instruction as Cpu;
38
39use super::{Instruction, Program};
40use crate::arch::addr::{LaneAddr, LocationAddr, ZoneAddr};
41use crate::arch::query::{LaneGroupError, LocationGroupError};
42use crate::arch::types::ArchSpec;
43
44pub mod tag {
47 pub const FLOAT: u8 = 0x0;
48 pub const INT: u8 = 0x1;
49 pub const ARRAY_REF: u8 = 0x2;
50 pub const LOCATION: u8 = 0x3;
51 pub const LANE: u8 = 0x4;
52 pub const ZONE: u8 = 0x5;
53 pub const MEASURE_FUTURE: u8 = 0x6;
54 pub const DETECTOR_REF: u8 = 0x7;
55 pub const OBSERVABLE_REF: u8 = 0x8;
56 pub const MEASUREMENT_RESULT: u8 = 0x9;
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ValidationError {
63 ControlFlowRequiresFeedForward {
65 pc: usize,
66 mnemonic: &'static str,
68 },
69 MultipleMeasuresRequireFeedForward { pc: usize },
71 FillRequiresAtomReloading { pc: usize },
73 InvalidLocation { pc: usize, message: String },
75 InvalidLane { pc: usize, message: String },
77 InvalidZone { pc: usize, message: String },
79
80 NewArrayZeroDim0 { pc: usize },
83 NewArrayInvalidTypeTag { pc: usize, type_tag: u32 },
85 InitialFillNotFirst { pc: usize },
87 EmptyProgram,
89 MissingTerminator { pc: usize },
91 UnreachableInstruction { pc: usize },
93
94 StackUnderflow { pc: usize },
97 TypeMismatch { pc: usize, expected: u8, got: u8 },
99 LocationGroupValidation {
101 pc: usize,
102 error: LocationGroupError,
103 },
104 LaneGroupValidation { pc: usize, error: LaneGroupError },
106}
107
108const MAX_TYPE_TAG: u32 = 0x8;
110
111impl fmt::Display for ValidationError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 ValidationError::ControlFlowRequiresFeedForward { pc, mnemonic } => write!(
115 f,
116 "pc {pc}: control-flow instruction '{mnemonic}' requires feed_forward capability"
117 ),
118 ValidationError::MultipleMeasuresRequireFeedForward { pc } => write!(
119 f,
120 "pc {pc}: multiple measure instructions require feed_forward capability"
121 ),
122 ValidationError::FillRequiresAtomReloading { pc } => {
123 write!(
124 f,
125 "pc {pc}: fill instruction requires atom_reloading capability"
126 )
127 }
128 ValidationError::InvalidLocation { pc, message } => {
129 write!(f, "pc {pc}: invalid location: {message}")
130 }
131 ValidationError::InvalidLane { pc, message } => {
132 write!(f, "pc {pc}: invalid lane: {message}")
133 }
134 ValidationError::InvalidZone { pc, message } => {
135 write!(f, "pc {pc}: invalid zone: {message}")
136 }
137 ValidationError::NewArrayZeroDim0 { pc } => {
138 write!(f, "pc {pc}: new_array dim0 must be > 0")
139 }
140 ValidationError::NewArrayInvalidTypeTag { pc, type_tag } => {
141 write!(f, "pc {pc}: invalid new_array type tag {type_tag}")
142 }
143 ValidationError::InitialFillNotFirst { pc } => write!(
144 f,
145 "pc {pc}: initial_fill must be the first non-constant instruction"
146 ),
147 ValidationError::EmptyProgram => {
148 write!(
149 f,
150 "program has no instructions: missing return or halt terminator"
151 )
152 }
153 ValidationError::MissingTerminator { pc } => {
154 write!(f, "pc {pc}: program must end with return or halt")
155 }
156 ValidationError::UnreachableInstruction { pc } => {
157 write!(f, "pc {pc}: unreachable instruction after return or halt")
158 }
159 ValidationError::StackUnderflow { pc } => write!(f, "pc {pc}: stack underflow"),
160 ValidationError::TypeMismatch { pc, expected, got } => write!(
161 f,
162 "pc {pc}: type mismatch: expected tag 0x{expected:x}, got 0x{got:x}"
163 ),
164 ValidationError::LocationGroupValidation { pc, error } => {
165 write!(f, "pc {pc}: {error}")
166 }
167 ValidationError::LaneGroupValidation { pc, error } => write!(f, "pc {pc}: {error}"),
168 }
169 }
170}
171
172impl std::error::Error for ValidationError {}
173
174fn control_flow_mnemonic(cpu: &Cpu) -> Option<&'static str> {
180 match cpu {
181 Cpu::Branch(_) => Some("br"),
182 Cpu::ConditionalBranch(_, _) => Some("cond_br"),
183 Cpu::Call(_, _) => Some("call"),
184 Cpu::IndirectCall => Some("call_indirect"),
185 _ => None,
186 }
187}
188
189pub fn validate(program: &Program, arch: Option<&ArchSpec>) -> Vec<ValidationError> {
195 let Some(arch) = arch else {
196 return Vec::new();
197 };
198
199 let mut errors = Vec::new();
200 let mut measure_count = 0u32;
201
202 for (pc, inst) in program.code.iter().enumerate() {
203 match inst {
204 Instruction::Cpu(cpu) if !arch.feed_forward => {
206 if let Some(mnemonic) = control_flow_mnemonic(cpu) {
207 errors.push(ValidationError::ControlFlowRequiresFeedForward { pc, mnemonic });
208 }
209 }
210 Instruction::Measure(_) => {
211 measure_count += 1;
212 if !arch.feed_forward && measure_count > 1 {
213 errors.push(ValidationError::MultipleMeasuresRequireFeedForward { pc });
214 }
215 }
216 Instruction::Fill(_) if !arch.atom_reloading => {
217 errors.push(ValidationError::FillRequiresAtomReloading { pc });
218 }
219
220 Instruction::ConstLoc(bits) => {
222 if let Some(message) = arch.check_location(&LocationAddr::decode(*bits)) {
223 errors.push(ValidationError::InvalidLocation { pc, message });
224 }
225 }
226 Instruction::ConstLane(bits) => {
227 for message in arch.check_lane(&LaneAddr::decode_u64(*bits)) {
228 errors.push(ValidationError::InvalidLane { pc, message });
229 }
230 }
231 Instruction::ConstZone(bits) => {
232 if let Some(message) = arch.check_zone(&ZoneAddr::decode(*bits)) {
233 errors.push(ValidationError::InvalidZone { pc, message });
234 }
235 }
236
237 _ => {}
238 }
239 }
240
241 errors
242}
243
244fn is_terminator(inst: &Instruction) -> bool {
246 matches!(inst, Instruction::Return | Instruction::Cpu(Cpu::Halt))
247}
248
249fn is_constant_push(inst: &Instruction) -> bool {
251 matches!(
252 inst,
253 Instruction::ConstLoc(_)
254 | Instruction::ConstLane(_)
255 | Instruction::ConstZone(_)
256 | Instruction::Cpu(Cpu::Const(_))
257 )
258}
259
260pub fn validate_structure(program: &Program) -> Vec<ValidationError> {
264 let mut errors = Vec::new();
265 let mut seen_non_constant = false;
266
267 for (pc, inst) in program.code.iter().enumerate() {
268 match inst {
269 Instruction::NewArray(type_tag, dim0, _dim1) => {
270 if *dim0 == 0 {
271 errors.push(ValidationError::NewArrayZeroDim0 { pc });
272 }
273 if *type_tag > MAX_TYPE_TAG {
274 errors.push(ValidationError::NewArrayInvalidTypeTag {
275 pc,
276 type_tag: *type_tag,
277 });
278 }
279 seen_non_constant = true;
280 }
281 Instruction::InitialFill(_) => {
282 if seen_non_constant {
283 errors.push(ValidationError::InitialFillNotFirst { pc });
284 }
285 seen_non_constant = true;
286 }
287 inst if is_constant_push(inst) => {}
288 _ => seen_non_constant = true,
289 }
290 }
291
292 let mut found_terminator = false;
296 let mut unreachable = Vec::new();
297 for (pc, inst) in program.code.iter().enumerate() {
298 if found_terminator {
299 unreachable.push(ValidationError::UnreachableInstruction { pc });
300 }
301 if is_terminator(inst) {
302 found_terminator = true;
303 }
304 }
305
306 if unreachable.is_empty() {
307 match program.code.last() {
308 None => errors.push(ValidationError::EmptyProgram),
309 Some(last) if !is_terminator(last) => errors.push(ValidationError::MissingTerminator {
310 pc: program.code.len() - 1,
311 }),
312 Some(_) => {}
313 }
314 } else {
315 errors.extend(unreachable);
316 }
317
318 errors
319}
320
321#[derive(Debug, Clone)]
325struct SimEntry {
326 tag: u8,
327 value: Option<u64>,
328}
329
330struct StackSimulator<'a> {
335 stack: Vec<SimEntry>,
336 errors: Vec<ValidationError>,
337 arch: Option<&'a ArchSpec>,
338 pc: usize,
339}
340
341impl<'a> StackSimulator<'a> {
342 fn new(arch: Option<&'a ArchSpec>) -> Self {
343 Self {
344 stack: Vec::new(),
345 errors: Vec::new(),
346 arch,
347 pc: 0,
348 }
349 }
350
351 fn pop_any(&mut self) {
352 if self.stack.pop().is_none() {
353 self.errors
354 .push(ValidationError::StackUnderflow { pc: self.pc });
355 }
356 }
357
358 fn pop_typed(&mut self, expected: u8) {
359 match self.stack.pop() {
360 Some(entry) if entry.tag != expected => {
361 self.errors.push(ValidationError::TypeMismatch {
362 pc: self.pc,
363 expected,
364 got: entry.tag,
365 })
366 }
367 Some(_) => {}
368 None => self
369 .errors
370 .push(ValidationError::StackUnderflow { pc: self.pc }),
371 }
372 }
373
374 fn pop_typed_n(&mut self, expected: u8, count: u32) {
375 for _ in 0..count {
376 self.pop_typed(expected);
377 }
378 }
379
380 fn pop_addr(&mut self, expected: u8) -> Option<u64> {
383 match self.stack.pop() {
384 Some(entry) if entry.tag == expected => entry.value,
385 Some(entry) => {
386 self.errors.push(ValidationError::TypeMismatch {
387 pc: self.pc,
388 expected,
389 got: entry.tag,
390 });
391 None
392 }
393 None => {
394 self.errors
395 .push(ValidationError::StackUnderflow { pc: self.pc });
396 None
397 }
398 }
399 }
400
401 fn push(&mut self, tag: u8, value: Option<u64>) {
402 self.stack.push(SimEntry { tag, value });
403 }
404
405 fn sim_dup(&mut self) {
406 if let Some(top) = self.stack.last().cloned() {
407 self.stack.push(top);
408 } else {
409 self.errors
410 .push(ValidationError::StackUnderflow { pc: self.pc });
411 }
412 }
413
414 fn sim_swap(&mut self) {
415 let len = self.stack.len();
416 if len >= 2 {
417 self.stack.swap(len - 1, len - 2);
418 } else {
419 self.errors
420 .push(ValidationError::StackUnderflow { pc: self.pc });
421 }
422 }
423
424 fn check_duplicate_locations(&mut self, locations: &[LocationAddr]) {
426 let mut seen = HashSet::new();
427 let mut reported = HashSet::new();
428 for loc in locations {
429 let bits = loc.encode();
430 if !seen.insert(bits) && reported.insert(bits) {
431 self.errors.push(ValidationError::LocationGroupValidation {
432 pc: self.pc,
433 error: LocationGroupError::DuplicateAddress { address: bits },
434 });
435 }
436 }
437 }
438
439 fn check_duplicate_lanes(&mut self, lanes: &[LaneAddr]) {
441 let mut seen = HashSet::new();
442 let mut reported = HashSet::new();
443 for lane in lanes {
444 let (d0, d1) = lane.encode();
445 let combined = (d0 as u64) | ((d1 as u64) << 32);
446 if !seen.insert(combined) && reported.insert(combined) {
447 self.errors.push(ValidationError::LaneGroupValidation {
448 pc: self.pc,
449 error: LaneGroupError::DuplicateAddress { address: (d0, d1) },
450 });
451 }
452 }
453 }
454
455 fn pop_and_validate_locations(&mut self, arity: u32) {
457 let bits: Vec<Option<u64>> = (0..arity).map(|_| self.pop_addr(tag::LOCATION)).collect();
458 let locations: Vec<LocationAddr> = bits
459 .iter()
460 .filter_map(|v| v.map(LocationAddr::decode))
461 .collect();
462 let pc = self.pc;
463 if let Some(arch) = self.arch {
464 for error in arch.check_locations(&locations) {
465 self.errors
466 .push(ValidationError::LocationGroupValidation { pc, error });
467 }
468 } else {
469 self.check_duplicate_locations(&locations);
470 }
471 }
472
473 fn sim_move(&mut self, arity: u32) {
475 let bits: Vec<Option<u64>> = (0..arity).map(|_| self.pop_addr(tag::LANE)).collect();
476 let lanes: Vec<LaneAddr> = bits
477 .iter()
478 .filter_map(|v| v.map(LaneAddr::decode_u64))
479 .collect();
480 let pc = self.pc;
481 if let Some(arch) = self.arch {
482 for error in arch.check_lanes(&lanes) {
483 self.errors
484 .push(ValidationError::LaneGroupValidation { pc, error });
485 }
486 } else {
487 self.check_duplicate_lanes(&lanes);
488 }
489 }
490
491 fn dispatch(&mut self, inst: &Instruction) {
492 match inst {
493 Instruction::Cpu(Cpu::Const(Value::F64(v))) => self.push(tag::FLOAT, Some(v.to_bits())),
495 Instruction::Cpu(Cpu::Const(Value::I64(v))) => self.push(tag::INT, Some(*v as u64)),
496 Instruction::ConstLoc(v) => self.push(tag::LOCATION, Some(*v)),
497 Instruction::ConstLane(v) => self.push(tag::LANE, Some(*v)),
498 Instruction::ConstZone(v) => self.push(tag::ZONE, Some(*v as u64)),
499
500 Instruction::Pop => self.pop_any(),
502 Instruction::Cpu(Cpu::Dup) => self.sim_dup(),
503 Instruction::Swap => self.sim_swap(),
504
505 Instruction::InitialFill(arity) | Instruction::Fill(arity) => {
507 self.pop_and_validate_locations(*arity)
508 }
509 Instruction::Move(arity) => self.sim_move(*arity),
510
511 Instruction::LocalR(arity) => {
513 self.pop_typed_n(tag::FLOAT, 2);
514 self.pop_and_validate_locations(*arity);
515 }
516 Instruction::LocalRz(arity) => {
517 self.pop_typed_n(tag::FLOAT, 1);
518 self.pop_and_validate_locations(*arity);
519 }
520 Instruction::GlobalR => self.pop_typed_n(tag::FLOAT, 2),
521 Instruction::GlobalRz => self.pop_typed_n(tag::FLOAT, 1),
522 Instruction::Cz => self.pop_typed(tag::ZONE),
523
524 Instruction::Measure(arity) => {
526 self.pop_typed_n(tag::ZONE, *arity);
527 for _ in 0..*arity {
528 self.push(tag::MEASURE_FUTURE, None);
529 }
530 }
531 Instruction::AwaitMeasure => {
532 self.pop_typed(tag::MEASURE_FUTURE);
533 self.push(tag::MEASUREMENT_RESULT, None);
534 }
535
536 Instruction::NewArray(_type_tag, dim0, dim1) => {
538 let count = dim0 * if *dim1 == 0 { 1 } else { *dim1 };
539 for _ in 0..count {
540 self.pop_any();
541 }
542 self.push(tag::ARRAY_REF, None);
543 }
544 Instruction::GetItem(ndims) => {
545 self.pop_typed_n(tag::INT, *ndims);
546 self.pop_typed(tag::ARRAY_REF);
547 self.push(tag::FLOAT, None);
549 }
550
551 Instruction::SetDetector => {
553 self.pop_typed(tag::ARRAY_REF);
554 self.push(tag::DETECTOR_REF, None);
555 }
556 Instruction::SetObservable => {
557 self.pop_typed(tag::ARRAY_REF);
558 self.push(tag::OBSERVABLE_REF, None);
559 }
560
561 Instruction::Return => self.pop_any(),
563 Instruction::Cpu(Cpu::Halt) => {}
564
565 Instruction::Cpu(_) => {}
568 }
569 }
570
571 fn run(mut self, program: &Program) -> Vec<ValidationError> {
572 for (pc, inst) in program.code.iter().enumerate() {
573 self.pc = pc;
574 self.dispatch(inst);
575 }
576 self.errors
577 }
578}
579
580pub fn simulate_stack(program: &Program, arch: Option<&ArchSpec>) -> Vec<ValidationError> {
584 StackSimulator::new(arch).run(program)
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::version::Version;
591
592 const SIMPLE_ARCH_JSON: &str = include_str!("../../../../examples/arch/simple.json");
594
595 fn simple_arch() -> ArchSpec {
596 ArchSpec::from_json(SIMPLE_ARCH_JSON).expect("examples/arch/simple.json should parse")
597 }
598
599 fn caps_arch(feed_forward: bool, atom_reloading: bool) -> ArchSpec {
602 ArchSpec {
603 version: Version::new(2, 0),
604 words: vec![],
605 zones: vec![],
606 zone_buses: vec![],
607 modes: vec![],
608 paths: None,
609 feed_forward,
610 atom_reloading,
611 blockade_radius: None,
612 }
613 }
614
615 fn program(instructions: Vec<Instruction>) -> Program {
616 crate::isa::program::from_code(Version::new(1, 0), instructions)
617 }
618
619 fn loc(zone_id: u32, word_id: u32, site_id: u32) -> u64 {
620 LocationAddr {
621 zone_id,
622 word_id,
623 site_id,
624 }
625 .encode()
626 }
627
628 #[test]
631 fn no_arch_skips_all_checks() {
632 let p = program(vec![
633 Instruction::Cpu(Cpu::ConditionalBranch(0, 1)),
634 Instruction::Fill(1),
635 Instruction::Measure(1),
636 Instruction::Measure(1),
637 Instruction::ConstZone(99), ]);
639 assert!(validate(&p, None).is_empty());
640 }
641
642 #[test]
643 fn branching_rejected_without_feed_forward() {
644 let p = program(vec![
645 Instruction::Cpu(Cpu::Branch(0)),
646 Instruction::Cpu(Cpu::ConditionalBranch(0, 1)),
647 Instruction::Cpu(Cpu::Call(1, 0)),
648 Instruction::Cpu(Cpu::IndirectCall),
649 ]);
650 let errors = validate(&p, Some(&caps_arch(false, false)));
651 assert_eq!(
652 errors,
653 vec![
654 ValidationError::ControlFlowRequiresFeedForward {
655 pc: 0,
656 mnemonic: "br"
657 },
658 ValidationError::ControlFlowRequiresFeedForward {
659 pc: 1,
660 mnemonic: "cond_br"
661 },
662 ValidationError::ControlFlowRequiresFeedForward {
663 pc: 2,
664 mnemonic: "call"
665 },
666 ValidationError::ControlFlowRequiresFeedForward {
667 pc: 3,
668 mnemonic: "call_indirect"
669 },
670 ]
671 );
672 }
673
674 #[test]
675 fn branching_allowed_with_feed_forward() {
676 let p = program(vec![
677 Instruction::Cpu(Cpu::ConditionalBranch(0, 1)),
678 Instruction::Measure(1),
679 Instruction::Measure(1),
680 ]);
681 assert!(validate(&p, Some(&caps_arch(true, false))).is_empty());
682 }
683
684 #[test]
685 fn non_control_flow_cpu_ops_are_fine() {
686 use vihaco::value::Value;
687 let p = program(vec![
688 Instruction::Cpu(Cpu::Const(Value::I64(1))),
689 Instruction::Cpu(Cpu::Dup),
690 Instruction::Cpu(Cpu::Halt),
691 ]);
692 assert!(validate(&p, Some(&caps_arch(false, false))).is_empty());
693 }
694
695 #[test]
696 fn single_measure_ok_but_second_rejected_without_feed_forward() {
697 let p = program(vec![Instruction::Measure(1), Instruction::Measure(1)]);
698 assert_eq!(
699 validate(&p, Some(&caps_arch(false, false))),
700 vec![ValidationError::MultipleMeasuresRequireFeedForward { pc: 1 }]
701 );
702 }
703
704 #[test]
705 fn fill_requires_atom_reloading() {
706 let p = program(vec![Instruction::Fill(1)]);
707 assert_eq!(
708 validate(&p, Some(&caps_arch(false, false))),
709 vec![ValidationError::FillRequiresAtomReloading { pc: 0 }]
710 );
711 assert!(validate(&p, Some(&caps_arch(false, true))).is_empty());
712 }
713
714 #[test]
717 fn valid_addresses_pass() {
718 let arch = simple_arch();
719 let p = program(vec![
721 Instruction::ConstLoc(loc(0, 0, 0)),
722 Instruction::ConstLoc(loc(0, 0, 4)),
723 Instruction::ConstZone(ZoneAddr { zone_id: 0 }.encode()),
724 ]);
725 assert!(validate(&p, Some(&arch)).is_empty(), "expected no errors");
726 }
727
728 #[test]
729 fn invalid_location_rejected() {
730 let arch = simple_arch();
731 let p = program(vec![Instruction::ConstLoc(loc(0, 0, 99))]);
733 let errors = validate(&p, Some(&arch));
734 assert!(
735 matches!(
736 errors.as_slice(),
737 [ValidationError::InvalidLocation { pc: 0, .. }]
738 ),
739 "got {errors:?}"
740 );
741 }
742
743 #[test]
744 fn invalid_zone_rejected() {
745 let arch = simple_arch();
746 let p = program(vec![Instruction::ConstZone(
748 ZoneAddr { zone_id: 5 }.encode(),
749 )]);
750 let errors = validate(&p, Some(&arch));
751 assert!(
752 matches!(
753 errors.as_slice(),
754 [ValidationError::InvalidZone { pc: 0, .. }]
755 ),
756 "got {errors:?}"
757 );
758 }
759
760 #[test]
761 fn invalid_lane_rejected() {
762 let arch = simple_arch();
763 let bad = LaneAddr {
765 direction: crate::arch::addr::Direction::Forward,
766 move_type: crate::arch::addr::MoveType::SiteBus,
767 zone_id: 9,
768 word_id: 0,
769 site_id: 0,
770 bus_id: 0,
771 };
772 let p = program(vec![Instruction::ConstLane(bad.encode_u64())]);
773 let errors = validate(&p, Some(&arch));
774 assert!(
775 errors
776 .iter()
777 .any(|e| matches!(e, ValidationError::InvalidLane { pc: 0, .. })),
778 "got {errors:?}"
779 );
780 }
781
782 #[test]
785 fn well_formed_program_has_no_structural_errors() {
786 let p = program(vec![
787 Instruction::ConstLoc(loc(0, 0, 0)),
788 Instruction::InitialFill(1),
789 Instruction::Return,
790 ]);
791 assert!(validate_structure(&p).is_empty());
792 }
793
794 #[test]
795 fn empty_program_is_rejected() {
796 assert_eq!(
797 validate_structure(&program(vec![])),
798 vec![ValidationError::EmptyProgram]
799 );
800 }
801
802 #[test]
803 fn missing_terminator_rejected() {
804 let p = program(vec![
805 Instruction::ConstLoc(loc(0, 0, 0)),
806 Instruction::InitialFill(1),
807 ]);
808 assert_eq!(
809 validate_structure(&p),
810 vec![ValidationError::MissingTerminator { pc: 1 }]
811 );
812 }
813
814 #[test]
815 fn halt_is_a_valid_terminator() {
816 let p = program(vec![Instruction::Cpu(Cpu::Halt)]);
817 assert!(validate_structure(&p).is_empty());
818 }
819
820 #[test]
821 fn unreachable_after_terminator_rejected() {
822 let p = program(vec![Instruction::Return, Instruction::Cpu(Cpu::Halt)]);
823 assert_eq!(
824 validate_structure(&p),
825 vec![ValidationError::UnreachableInstruction { pc: 1 }]
826 );
827 }
828
829 #[test]
830 fn initial_fill_must_be_first_non_constant() {
831 let ok = program(vec![
833 Instruction::ConstLoc(loc(0, 0, 0)),
834 Instruction::InitialFill(1),
835 Instruction::Return,
836 ]);
837 assert!(validate_structure(&ok).is_empty());
838
839 let bad = program(vec![
840 Instruction::GlobalR,
841 Instruction::InitialFill(1),
842 Instruction::Return,
843 ]);
844 assert!(validate_structure(&bad).contains(&ValidationError::InitialFillNotFirst { pc: 1 }));
845 }
846
847 #[test]
848 fn new_array_bounds_checked() {
849 let p = program(vec![Instruction::NewArray(99, 0, 0), Instruction::Return]);
850 let errors = validate_structure(&p);
851 assert!(errors.contains(&ValidationError::NewArrayZeroDim0 { pc: 0 }));
852 assert!(errors.contains(&ValidationError::NewArrayInvalidTypeTag {
853 pc: 0,
854 type_tag: 99
855 }));
856 }
857
858 #[test]
859 fn structure_and_arch_checks_compose() {
860 let arch = simple_arch();
863 let p = program(vec![Instruction::ConstZone(
864 ZoneAddr { zone_id: 5 }.encode(),
865 )]);
866 let errors: Vec<_> = validate_structure(&p)
867 .into_iter()
868 .chain(validate(&p, Some(&arch)))
869 .collect();
870 assert!(errors.contains(&ValidationError::MissingTerminator { pc: 0 }));
871 assert!(
872 errors
873 .iter()
874 .any(|e| matches!(e, ValidationError::InvalidZone { .. }))
875 );
876 }
877
878 #[test]
879 fn capability_and_address_errors_collected_together() {
880 let arch = simple_arch(); let p = program(vec![
882 Instruction::Cpu(Cpu::Branch(0)), Instruction::Fill(1), Instruction::ConstZone(ZoneAddr { zone_id: 5 }.encode()), ]);
886 let errors = validate(&p, Some(&arch));
887 assert_eq!(
888 errors.len(),
889 3,
890 "one error per violation, in pc order: {errors:?}"
891 );
892 assert!(matches!(
893 errors[0],
894 ValidationError::ControlFlowRequiresFeedForward { pc: 0, .. }
895 ));
896 assert!(matches!(
897 errors[1],
898 ValidationError::FillRequiresAtomReloading { pc: 1 }
899 ));
900 assert!(matches!(
901 errors[2],
902 ValidationError::InvalidZone { pc: 2, .. }
903 ));
904 }
905
906 fn cpu_float(v: f64) -> Instruction {
909 Instruction::Cpu(Cpu::Const(Value::F64(v)))
910 }
911
912 #[test]
913 fn stack_well_typed_program_has_no_errors() {
914 let p = program(vec![
917 Instruction::ConstLoc(loc(0, 0, 0)),
918 Instruction::ConstLoc(loc(0, 0, 1)),
919 Instruction::InitialFill(2),
920 Instruction::ConstZone(0),
921 Instruction::Measure(1),
922 Instruction::AwaitMeasure,
923 Instruction::Return,
924 ]);
925 assert!(
926 simulate_stack(&p, None).is_empty(),
927 "{:?}",
928 simulate_stack(&p, None)
929 );
930 }
931
932 #[test]
933 fn stack_underflow_detected() {
934 let p = program(vec![Instruction::Pop]);
935 assert_eq!(
936 simulate_stack(&p, None),
937 vec![ValidationError::StackUnderflow { pc: 0 }]
938 );
939 }
940
941 #[test]
942 fn type_mismatch_detected() {
943 let p = program(vec![cpu_float(1.0), Instruction::InitialFill(1)]);
945 let errors = simulate_stack(&p, None);
946 assert!(
947 errors.iter().any(|e| matches!(
948 e,
949 ValidationError::TypeMismatch {
950 pc: 1,
951 expected,
952 got
953 } if *expected == tag::LOCATION && *got == tag::FLOAT
954 )),
955 "got {errors:?}"
956 );
957 }
958
959 #[test]
960 fn measure_pushes_future_consumed_by_await() {
961 let p = program(vec![cpu_float(1.0), Instruction::AwaitMeasure]);
963 let errors = simulate_stack(&p, None);
964 assert!(
965 errors.iter().any(|e| matches!(
966 e,
967 ValidationError::TypeMismatch { pc: 1, expected, .. } if *expected == tag::MEASURE_FUTURE
968 )),
969 "got {errors:?}"
970 );
971 }
972
973 #[test]
974 fn local_r_pops_two_floats_then_locations() {
975 let p = program(vec![
977 Instruction::ConstLoc(loc(0, 0, 0)),
978 cpu_float(1.5),
979 cpu_float(0.5),
980 Instruction::LocalR(1),
981 ]);
982 assert!(simulate_stack(&p, None).is_empty());
983 }
984
985 #[test]
986 fn duplicate_locations_flagged_without_arch() {
987 let p = program(vec![
989 Instruction::ConstLoc(loc(0, 0, 0)),
990 Instruction::ConstLoc(loc(0, 0, 0)),
991 Instruction::InitialFill(2),
992 ]);
993 let errors = simulate_stack(&p, None);
994 assert!(
995 errors
996 .iter()
997 .any(|e| matches!(e, ValidationError::LocationGroupValidation { .. })),
998 "got {errors:?}"
999 );
1000 }
1001
1002 #[test]
1003 fn invalid_lane_group_flagged_with_arch() {
1004 let arch = simple_arch();
1005 let bad = crate::arch::addr::LaneAddr {
1007 direction: crate::arch::addr::Direction::Forward,
1008 move_type: crate::arch::addr::MoveType::SiteBus,
1009 zone_id: 9,
1010 word_id: 0,
1011 site_id: 0,
1012 bus_id: 0,
1013 };
1014 let p = program(vec![
1015 Instruction::ConstLane(bad.encode_u64()),
1016 Instruction::Move(1),
1017 ]);
1018 let errors = simulate_stack(&p, Some(&arch));
1019 assert!(
1020 errors
1021 .iter()
1022 .any(|e| matches!(e, ValidationError::LaneGroupValidation { .. })),
1023 "got {errors:?}"
1024 );
1025 }
1026
1027 #[test]
1028 fn await_measure_pushes_measurement_result() {
1029 let p = program(vec![
1033 Instruction::ConstZone(0),
1034 Instruction::Measure(1),
1035 Instruction::AwaitMeasure,
1036 ]);
1037 assert_eq!(tag::MEASUREMENT_RESULT, 0x9);
1039 assert!(simulate_stack(&p, None).iter().all(|e| !matches!(
1041 e,
1042 ValidationError::StackUnderflow { .. } | ValidationError::TypeMismatch { .. }
1043 )));
1044 }
1045
1046 #[test]
1049 fn validation_error_display_strings() {
1050 use crate::arch::query::{LaneGroupError, LocationGroupError};
1051
1052 let cases: Vec<(ValidationError, String)> = vec![
1053 (
1054 ValidationError::ControlFlowRequiresFeedForward {
1055 pc: 3,
1056 mnemonic: "cond_br",
1057 },
1058 "pc 3: control-flow instruction 'cond_br' requires feed_forward capability".into(),
1059 ),
1060 (
1061 ValidationError::MultipleMeasuresRequireFeedForward { pc: 5 },
1062 "pc 5: multiple measure instructions require feed_forward capability".into(),
1063 ),
1064 (
1065 ValidationError::FillRequiresAtomReloading { pc: 1 },
1066 "pc 1: fill instruction requires atom_reloading capability".into(),
1067 ),
1068 (
1069 ValidationError::InvalidLocation {
1070 pc: 2,
1071 message: "bad".into(),
1072 },
1073 "pc 2: invalid location: bad".into(),
1074 ),
1075 (
1076 ValidationError::InvalidLane {
1077 pc: 2,
1078 message: "bad".into(),
1079 },
1080 "pc 2: invalid lane: bad".into(),
1081 ),
1082 (
1083 ValidationError::InvalidZone {
1084 pc: 2,
1085 message: "bad".into(),
1086 },
1087 "pc 2: invalid zone: bad".into(),
1088 ),
1089 (
1090 ValidationError::NewArrayZeroDim0 { pc: 0 },
1091 "pc 0: new_array dim0 must be > 0".into(),
1092 ),
1093 (
1094 ValidationError::NewArrayInvalidTypeTag {
1095 pc: 0,
1096 type_tag: 99,
1097 },
1098 "pc 0: invalid new_array type tag 99".into(),
1099 ),
1100 (
1101 ValidationError::InitialFillNotFirst { pc: 4 },
1102 "pc 4: initial_fill must be the first non-constant instruction".into(),
1103 ),
1104 (
1105 ValidationError::EmptyProgram,
1106 "program has no instructions: missing return or halt terminator".into(),
1107 ),
1108 (
1109 ValidationError::MissingTerminator { pc: 7 },
1110 "pc 7: program must end with return or halt".into(),
1111 ),
1112 (
1113 ValidationError::UnreachableInstruction { pc: 8 },
1114 "pc 8: unreachable instruction after return or halt".into(),
1115 ),
1116 (
1117 ValidationError::StackUnderflow { pc: 1 },
1118 "pc 1: stack underflow".into(),
1119 ),
1120 (
1121 ValidationError::TypeMismatch {
1122 pc: 1,
1123 expected: tag::LOCATION,
1124 got: tag::FLOAT,
1125 },
1126 "pc 1: type mismatch: expected tag 0x3, got 0x0".into(),
1127 ),
1128 ];
1129 for (err, expected) in cases {
1130 assert_eq!(err.to_string(), expected);
1131 }
1132
1133 let loc_err = LocationGroupError::DuplicateAddress { address: 0x10 };
1136 assert_eq!(
1137 ValidationError::LocationGroupValidation {
1138 pc: 2,
1139 error: loc_err.clone(),
1140 }
1141 .to_string(),
1142 format!("pc 2: {loc_err}")
1143 );
1144 let lane_err = LaneGroupError::DuplicateAddress { address: (1, 2) };
1145 assert_eq!(
1146 ValidationError::LaneGroupValidation {
1147 pc: 3,
1148 error: lane_err.clone(),
1149 }
1150 .to_string(),
1151 format!("pc 3: {lane_err}")
1152 );
1153 }
1154
1155 #[test]
1158 fn stack_sim_int_const_and_pop() {
1159 let p = program(vec![
1161 Instruction::Cpu(Cpu::Const(Value::I64(7))),
1162 Instruction::Pop,
1163 ]);
1164 assert!(simulate_stack(&p, None).is_empty());
1165 }
1166
1167 #[test]
1168 fn stack_sim_swap_needs_two_entries() {
1169 let ok = program(vec![
1170 Instruction::ConstLoc(loc(0, 0, 0)),
1171 Instruction::ConstLoc(loc(0, 0, 1)),
1172 Instruction::Swap,
1173 ]);
1174 assert!(simulate_stack(&ok, None).is_empty());
1175
1176 let bad = program(vec![Instruction::ConstLoc(loc(0, 0, 0)), Instruction::Swap]);
1178 assert_eq!(
1179 simulate_stack(&bad, None),
1180 vec![ValidationError::StackUnderflow { pc: 1 }]
1181 );
1182 }
1183
1184 #[test]
1185 fn stack_sim_dup_underflow_on_empty() {
1186 let p = program(vec![Instruction::Cpu(Cpu::Dup)]);
1187 assert_eq!(
1188 simulate_stack(&p, None),
1189 vec![ValidationError::StackUnderflow { pc: 0 }]
1190 );
1191 }
1192
1193 #[test]
1194 fn stack_sim_dup_copies_top_of_stack() {
1195 let p = program(vec![
1198 Instruction::ConstLoc(loc(0, 0, 0)),
1199 Instruction::Cpu(Cpu::Dup),
1200 Instruction::Pop,
1201 Instruction::Pop,
1202 ]);
1203 assert!(
1204 simulate_stack(&p, None).is_empty(),
1205 "{:?}",
1206 simulate_stack(&p, None)
1207 );
1208 }
1209
1210 #[test]
1211 fn stack_sim_gate_ops_are_well_typed() {
1212 let p = program(vec![
1214 Instruction::ConstLoc(loc(0, 0, 0)),
1215 cpu_float(0.5),
1216 Instruction::LocalRz(1),
1217 cpu_float(0.5),
1218 Instruction::GlobalRz,
1219 cpu_float(0.5),
1220 cpu_float(0.25),
1221 Instruction::GlobalR,
1222 Instruction::ConstZone(0),
1223 Instruction::Cz,
1224 ]);
1225 assert!(
1226 simulate_stack(&p, None).is_empty(),
1227 "{:?}",
1228 simulate_stack(&p, None)
1229 );
1230 }
1231
1232 #[test]
1233 fn stack_sim_gate_underflow_on_empty_stack() {
1234 assert_eq!(
1236 simulate_stack(&program(vec![Instruction::GlobalRz]), None),
1237 vec![ValidationError::StackUnderflow { pc: 0 }]
1238 );
1239 assert_eq!(
1241 simulate_stack(&program(vec![Instruction::Move(1)]), None),
1242 vec![ValidationError::StackUnderflow { pc: 0 }]
1243 );
1244 }
1245
1246 #[test]
1247 fn stack_sim_new_array_and_get_item() {
1248 let p = program(vec![
1251 Instruction::Cpu(Cpu::Const(Value::I64(1))),
1252 Instruction::Cpu(Cpu::Const(Value::I64(2))),
1253 Instruction::NewArray(tag::INT as u32, 2, 0),
1254 Instruction::Cpu(Cpu::Const(Value::I64(0))),
1255 Instruction::GetItem(1),
1256 ]);
1257 assert!(
1258 simulate_stack(&p, None).is_empty(),
1259 "{:?}",
1260 simulate_stack(&p, None)
1261 );
1262 }
1263
1264 #[test]
1265 fn stack_sim_set_detector_and_observable() {
1266 let det = program(vec![
1268 Instruction::Cpu(Cpu::Const(Value::I64(0))),
1269 Instruction::NewArray(tag::INT as u32, 1, 0),
1270 Instruction::SetDetector,
1271 ]);
1272 assert!(
1273 simulate_stack(&det, None).is_empty(),
1274 "{:?}",
1275 simulate_stack(&det, None)
1276 );
1277
1278 let obs = program(vec![
1279 Instruction::Cpu(Cpu::Const(Value::I64(0))),
1280 Instruction::NewArray(tag::INT as u32, 1, 0),
1281 Instruction::SetObservable,
1282 ]);
1283 assert!(
1284 simulate_stack(&obs, None).is_empty(),
1285 "{:?}",
1286 simulate_stack(&obs, None)
1287 );
1288 }
1289
1290 #[test]
1291 fn stack_sim_halt_and_other_cpu_ops_are_noops() {
1292 let p = program(vec![
1295 Instruction::Cpu(Cpu::Print),
1296 Instruction::Cpu(Cpu::Const(Value::Bool(true))),
1297 Instruction::Cpu(Cpu::Halt),
1298 ]);
1299 assert!(simulate_stack(&p, None).is_empty());
1300 }
1301
1302 #[test]
1303 fn location_group_validated_against_arch() {
1304 let arch = simple_arch();
1308 let p = program(vec![
1309 Instruction::ConstLoc(loc(0, 0, 0)),
1310 Instruction::ConstLoc(loc(0, 0, 1)),
1311 Instruction::InitialFill(2),
1312 ]);
1313 let errors = simulate_stack(&p, Some(&arch));
1314 assert!(
1315 !errors
1316 .iter()
1317 .any(|e| matches!(e, ValidationError::LocationGroupValidation { .. })),
1318 "got {errors:?}"
1319 );
1320 }
1321
1322 #[test]
1323 fn location_group_arch_errors_are_reported() {
1324 let arch = simple_arch();
1329 let p = program(vec![
1330 Instruction::ConstLoc(loc(0, 0, 0)),
1331 Instruction::ConstLoc(loc(0, 0, 0)),
1332 Instruction::InitialFill(2),
1333 ]);
1334 let errors = simulate_stack(&p, Some(&arch));
1335 assert!(
1336 errors
1337 .iter()
1338 .any(|e| matches!(e, ValidationError::LocationGroupValidation { .. })),
1339 "got {errors:?}"
1340 );
1341 }
1342
1343 #[test]
1344 fn duplicate_lanes_flagged_without_arch() {
1345 let lane = LaneAddr {
1347 direction: crate::arch::addr::Direction::Forward,
1348 move_type: crate::arch::addr::MoveType::SiteBus,
1349 zone_id: 0,
1350 word_id: 0,
1351 site_id: 0,
1352 bus_id: 0,
1353 };
1354 let p = program(vec![
1355 Instruction::ConstLane(lane.encode_u64()),
1356 Instruction::ConstLane(lane.encode_u64()),
1357 Instruction::Move(2),
1358 ]);
1359 let errors = simulate_stack(&p, None);
1360 assert!(
1361 errors
1362 .iter()
1363 .any(|e| matches!(e, ValidationError::LaneGroupValidation { .. })),
1364 "got {errors:?}"
1365 );
1366 }
1367}