1use std::collections::HashSet;
4
5use thiserror::Error;
6
7use crate::ast::{ComparisonOperator, Literal, PathStep};
8use crate::ir::{Block, BlockId, BlockKind, InstrId, Op, Operand, Program, SCHEMA_VERSION};
9
10fn literal_is_finite(lit: &Literal) -> bool {
12 match lit {
13 Literal::Float(f) => f.is_finite(),
14 _ => true,
15 }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Error)]
20pub enum IrError {
21 #[error("unsupported IR schema version {found} (expected {expected})")]
23 UnsupportedSchemaVersion {
24 found: u32,
26 expected: u32,
28 },
29 #[error(
31 "instruction {instr} references value {value}, which is not defined earlier in its block"
32 )]
33 UnknownValue {
34 instr: u32,
36 value: u32,
38 },
39 #[error("instruction {instr} observes block {block}, which is not a comparison block")]
41 UnknownBlock {
42 instr: u32,
44 block: u32,
46 },
47 #[error("block {block} does not end in {expected}")]
49 MissingTerminator {
50 block: u32,
52 expected: &'static str,
54 },
55 #[error("block {block} has a terminator at instruction {instr}, which is not its last")]
57 MisplacedTerminator {
58 block: u32,
60 instr: u32,
62 },
63 #[error("instruction {instr} is not valid in a {kind} block")]
65 WrongTier {
66 instr: u32,
68 kind: &'static str,
70 },
71 #[error("instruction {instr} has the wrong operand shape: {detail}")]
73 OperandShape {
74 instr: u32,
76 detail: &'static str,
78 },
79 #[error("instruction id {id} is used more than once")]
81 DuplicateInstrId {
82 id: u32,
84 },
85 #[error("block id {id} is used more than once")]
87 DuplicateBlockId {
88 id: u32,
90 },
91 #[error("instruction {instr} holds a non-finite float, which pattern syntax cannot express")]
96 NonFiniteFloat {
97 instr: u32,
99 },
100 #[error("repeats count {count} exceeds i64::MAX, which pattern syntax cannot express")]
106 RepeatsOutOfRange {
107 count: u64,
109 },
110 #[error("instruction {instr} has path index {index} above i64::MAX, which pattern syntax cannot express")]
115 PathIndexOutOfRange {
116 instr: u32,
118 index: u64,
120 },
121 #[error("instruction {instr} references value {value}, which is already used elsewhere")]
132 MultipleUses {
133 instr: u32,
135 value: u32,
137 },
138 #[error("block {block} is observed more than once")]
148 BlockObservedTwice {
149 block: u32,
151 },
152 #[error("instruction {instr} compares value {lhs}, which is not a load")]
157 ExpectedLoad {
158 instr: u32,
160 lhs: u32,
162 },
163 #[error("instruction {instr} negates EXISTS, which pattern syntax cannot express")]
168 NegatedExists {
169 instr: u32,
171 },
172 #[error("block {block} should be {expected}")]
174 WrongBlockKind {
175 block: u32,
177 expected: &'static str,
179 },
180}
181
182impl Program {
183 pub fn validate(&self) -> Result<(), IrError> {
197 if self.schema_version != SCHEMA_VERSION {
198 return Err(IrError::UnsupportedSchemaVersion {
199 found: self.schema_version,
200 expected: SCHEMA_VERSION,
201 });
202 }
203
204 if self.main.kind != BlockKind::Main {
205 return Err(IrError::WrongBlockKind {
206 block: self.main.id.0,
207 expected: "the main block",
208 });
209 }
210 for b in &self.blocks {
211 if b.kind != BlockKind::Comparison {
212 return Err(IrError::WrongBlockKind {
213 block: b.id.0,
214 expected: "a comparison block",
215 });
216 }
217 }
218
219 let mut block_ids = HashSet::new();
220 for b in self.blocks.iter().chain(std::iter::once(&self.main)) {
221 if !block_ids.insert(b.id) {
222 return Err(IrError::DuplicateBlockId { id: b.id.0 });
223 }
224 }
225
226 let comparison_blocks: HashSet<BlockId> = self
227 .blocks
228 .iter()
229 .filter(|b| b.kind == BlockKind::Comparison)
230 .map(|b| b.id)
231 .collect();
232
233 let mut seen_instrs = HashSet::new();
234 let mut used_values = HashSet::new();
235 let mut observed_blocks = HashSet::new();
236 for b in self.blocks.iter().chain(std::iter::once(&self.main)) {
237 self.check_block(
238 b,
239 &comparison_blocks,
240 &mut seen_instrs,
241 &mut used_values,
242 &mut observed_blocks,
243 )?;
244 }
245 Ok(())
246 }
247
248 fn check_block(
249 &self,
250 b: &Block,
251 comparison_blocks: &HashSet<BlockId>,
252 seen_instrs: &mut HashSet<InstrId>,
253 used_values: &mut HashSet<InstrId>,
254 observed_blocks: &mut HashSet<BlockId>,
255 ) -> Result<(), IrError> {
256 let expected_terminator = match b.kind {
257 BlockKind::Comparison => "a yield",
258 BlockKind::Main => "a ret",
259 };
260 let kind_name = match b.kind {
261 BlockKind::Comparison => "comparison",
262 BlockKind::Main => "main",
263 };
264
265 let is_terminator = |op: &Op| match b.kind {
266 BlockKind::Comparison => matches!(op, Op::Yield { .. }),
267 BlockKind::Main => matches!(op, Op::Ret { .. }),
268 };
269
270 match b.instructions.last() {
271 Some(last) if is_terminator(&last.op) => {}
272 _ => {
273 return Err(IrError::MissingTerminator {
274 block: b.id.0,
275 expected: expected_terminator,
276 })
277 }
278 }
279
280 let mut defined: HashSet<InstrId> = HashSet::new();
282 let mut loads: HashSet<InstrId> = HashSet::new();
286 let last_index = b.instructions.len() - 1;
287
288 for (index, instr) in b.instructions.iter().enumerate() {
289 if !seen_instrs.insert(instr.id) {
290 return Err(IrError::DuplicateInstrId { id: instr.id.0 });
291 }
292
293 if matches!(instr.op, Op::Yield { .. } | Op::Ret { .. }) && index != last_index {
295 return Err(IrError::MisplacedTerminator {
296 block: b.id.0,
297 instr: instr.id.0,
298 });
299 }
300
301 let tier_ok = match (&instr.op, b.kind) {
302 (
303 Op::Load { .. } | Op::Compare { .. } | Op::Yield { .. },
304 BlockKind::Comparison,
305 ) => true,
306 (
307 Op::Observe { .. }
308 | Op::FollowedBy { .. }
309 | Op::Within { .. }
310 | Op::Repeats { .. }
311 | Op::StartStop { .. }
312 | Op::Ret { .. },
313 BlockKind::Main,
314 ) => true,
315 (Op::And { .. } | Op::Or { .. }, _) => true,
317 _ => false,
318 };
319 if !tier_ok {
320 return Err(IrError::WrongTier {
321 instr: instr.id.0,
322 kind: kind_name,
323 });
324 }
325
326 let mut check_value = |value: InstrId| -> Result<(), IrError> {
329 if !defined.contains(&value) {
330 return Err(IrError::UnknownValue {
331 instr: instr.id.0,
332 value: value.0,
333 });
334 }
335 if !used_values.insert(value) {
336 return Err(IrError::MultipleUses {
337 instr: instr.id.0,
338 value: value.0,
339 });
340 }
341 Ok(())
342 };
343
344 match &instr.op {
345 Op::Load { path } => {
346 for step in &path.steps {
347 if let PathStep::Index(n) = step {
348 if *n > i64::MAX as u64 {
349 return Err(IrError::PathIndexOutOfRange {
350 instr: instr.id.0,
351 index: *n,
352 });
353 }
354 }
355 }
356 }
357 Op::Compare {
358 operator,
359 negated,
360 lhs,
361 rhs,
362 } => {
363 check_value(*lhs)?;
364 if !loads.contains(lhs) {
365 return Err(IrError::ExpectedLoad {
366 instr: instr.id.0,
367 lhs: lhs.0,
368 });
369 }
370 if *operator == ComparisonOperator::Exists && *negated {
371 return Err(IrError::NegatedExists { instr: instr.id.0 });
372 }
373 if let (ComparisonOperator::In, Operand::Set(items)) = (operator, rhs) {
374 if items.is_empty() {
375 return Err(IrError::OperandShape {
376 instr: instr.id.0,
377 detail: "IN takes a non-empty set",
378 });
379 }
380 }
381 let shape_ok = match (operator, rhs) {
382 (ComparisonOperator::Exists, Operand::Absent) => true,
383 (ComparisonOperator::Exists, _) => false,
384 (ComparisonOperator::In, Operand::Set(_)) => true,
385 (ComparisonOperator::In, _) => false,
386 (_, Operand::Literal(_)) => true,
387 (_, _) => false,
388 };
389 if !shape_ok {
390 return Err(IrError::OperandShape {
391 instr: instr.id.0,
392 detail:
393 "EXISTS takes no operand, IN takes a set, others take a literal",
394 });
395 }
396 let finite = match rhs {
397 Operand::Literal(lit) => literal_is_finite(lit),
398 Operand::Set(items) => items.iter().all(literal_is_finite),
399 Operand::Absent => true,
400 };
401 if !finite {
402 return Err(IrError::NonFiniteFloat { instr: instr.id.0 });
403 }
404 }
405 Op::And { lhs, rhs } | Op::Or { lhs, rhs } | Op::FollowedBy { lhs, rhs } => {
406 check_value(*lhs)?;
407 check_value(*rhs)?;
408 }
409 Op::Yield { value } | Op::Ret { value } => check_value(*value)?,
410 Op::Within { input, seconds } => {
411 check_value(*input)?;
412 if !seconds.is_finite() {
413 return Err(IrError::NonFiniteFloat { instr: instr.id.0 });
414 }
415 }
416 Op::Repeats { input, count } => {
417 check_value(*input)?;
418 if *count > i64::MAX as u64 {
419 return Err(IrError::RepeatsOutOfRange { count: *count });
420 }
421 }
422 Op::StartStop { input, .. } => check_value(*input)?,
423 Op::Observe { block } => {
424 if !comparison_blocks.contains(block) {
425 return Err(IrError::UnknownBlock {
426 instr: instr.id.0,
427 block: block.0,
428 });
429 }
430 if !observed_blocks.insert(*block) {
431 return Err(IrError::BlockObservedTwice { block: block.0 });
432 }
433 }
434 }
435
436 if matches!(instr.op, Op::Load { .. }) {
437 loads.insert(instr.id);
438 }
439 defined.insert(instr.id);
440 }
441 Ok(())
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use crate::ir::{lower, BlockKind, InstrId, Instruction, Op, Operand, Program};
449 use crate::parse;
450
451 fn valid() -> Program {
452 lower(&parse("[file:size > 1024]").unwrap())
453 }
454
455 #[test]
456 fn accepts_every_lowered_corpus_pattern() {
457 let raw = include_str!("../../tests/fixtures/valid_patterns.txt");
458 for line in raw.lines().map(str::trim) {
459 if line.is_empty() || line.starts_with('#') {
460 continue;
461 }
462 let prog = lower(&parse(line).expect(line));
463 assert!(prog.validate().is_ok(), "{line} -> {:?}", prog.validate());
464 }
465 }
466
467 #[test]
468 fn rejects_a_wrong_schema_version() {
469 let mut p = valid();
470 p.schema_version = 99;
471 assert!(matches!(
472 p.validate(),
473 Err(IrError::UnsupportedSchemaVersion { found: 99, .. })
474 ));
475 }
476
477 #[test]
478 fn rejects_a_dangling_reference() {
479 let mut p = valid();
480 let last = p.blocks[0].instructions.len() - 1;
482 p.blocks[0].instructions[last].op = Op::Yield {
483 value: InstrId(999),
484 };
485 assert!(matches!(p.validate(), Err(IrError::UnknownValue { .. })));
486 }
487
488 #[test]
489 fn rejects_a_forward_reference() {
490 let mut p = valid();
491 let compare_id = p.blocks[0].instructions[1].id;
493 p.blocks[0].instructions[0].op = Op::And {
494 lhs: compare_id,
495 rhs: compare_id,
496 };
497 assert!(matches!(p.validate(), Err(IrError::UnknownValue { .. })));
498 }
499
500 #[test]
501 fn rejects_a_missing_terminator() {
502 let mut p = valid();
503 p.blocks[0].instructions.pop();
504 assert!(matches!(
505 p.validate(),
506 Err(IrError::MissingTerminator { .. })
507 ));
508 }
509
510 #[test]
511 fn rejects_a_misplaced_terminator() {
512 let mut p = valid();
513 let first_id = p.blocks[0].instructions[0].id;
514 p.blocks[0].instructions.insert(
515 1,
516 Instruction {
517 id: InstrId(500),
518 op: Op::Yield { value: first_id },
519 span: None,
520 },
521 );
522 assert!(matches!(
523 p.validate(),
524 Err(IrError::MisplacedTerminator { .. })
525 ));
526 }
527
528 #[test]
529 fn rejects_a_tier_violation() {
530 let mut p = valid();
531 let first_id = p.blocks[0].instructions[0].id;
532 p.blocks[0].instructions.insert(
533 1,
534 Instruction {
535 id: InstrId(501),
536 op: Op::Within {
537 input: first_id,
538 seconds: 1.0,
539 },
540 span: None,
541 },
542 );
543 assert!(matches!(p.validate(), Err(IrError::WrongTier { .. })));
544 }
545
546 #[test]
547 fn rejects_an_operand_shape_mismatch() {
548 let mut p = valid();
549 match &mut p.blocks[0].instructions[1].op {
550 Op::Compare { rhs, .. } => *rhs = Operand::Absent,
551 other => panic!("expected a compare, got {other:?}"),
552 }
553 assert!(matches!(p.validate(), Err(IrError::OperandShape { .. })));
554 }
555
556 #[test]
557 fn rejects_a_duplicate_instruction_id() {
558 let mut p = valid();
559 let dup = p.blocks[0].instructions[0].clone();
560 p.blocks[0].instructions.insert(1, dup);
561 assert!(matches!(
562 p.validate(),
563 Err(IrError::DuplicateInstrId { .. })
564 ));
565 }
566
567 #[test]
568 fn rejects_observe_targeting_a_non_comparison_block() {
569 let mut p = valid();
570 let main_id = p.main.id;
571 p.main.instructions[0].op = Op::Observe { block: main_id };
572 assert!(matches!(p.validate(), Err(IrError::UnknownBlock { .. })));
573 }
574
575 #[test]
576 fn allows_dead_instructions() {
577 use crate::ast::{Literal, ObjectPath, PathStep};
578 use crate::error::Span;
579
580 let mut p = valid();
581 p.blocks[0].instructions.splice(
585 1..1,
586 [
587 Instruction {
588 id: InstrId(600),
589 op: Op::Load {
590 path: ObjectPath {
591 object_type: "file".to_string(),
592 steps: vec![PathStep::Key("name".to_string())],
593 span: Span::default(),
594 },
595 },
596 span: None,
597 },
598 Instruction {
599 id: InstrId(601),
600 op: Op::Compare {
601 operator: ComparisonOperator::Equal,
602 negated: false,
603 lhs: InstrId(600),
604 rhs: Operand::Literal(Literal::String("dead".to_string())),
605 },
606 span: None,
607 },
608 ],
609 );
610 assert!(p.validate().is_ok(), "{:?}", p.validate());
611 assert_eq!(
613 p.blocks[0]
614 .instructions
615 .iter()
616 .filter(|i| matches!(i.op, Op::Compare { .. }))
617 .count(),
618 2
619 );
620 }
621
622 #[test]
623 fn rejects_a_duplicate_block_id() {
624 let mut p = valid();
625 let dup = p.blocks[0].clone();
626 p.blocks.push(dup);
627 assert!(matches!(
628 p.validate(),
629 Err(IrError::DuplicateBlockId { .. })
630 ));
631 }
632
633 #[test]
634 fn rejects_a_block_observed_twice() {
635 let mut p = lower(&parse("[file:size > 1] AND [file:size > 2]").unwrap());
636 let first = p.blocks[0].id;
637 let mut seen = 0;
638 for i in &mut p.main.instructions {
639 if let Op::Observe { block } = &mut i.op {
640 *block = first;
641 seen += 1;
642 }
643 }
644 assert_eq!(seen, 2);
645 assert_eq!(
646 p.validate(),
647 Err(IrError::BlockObservedTwice { block: first.0 })
648 );
649 }
650
651 #[test]
652 fn rejects_a_value_used_twice() {
653 let mut p = valid();
654 let load_id = p.blocks[0].instructions[0].id;
656 let last = p.blocks[0].instructions.len() - 1;
657 p.blocks[0].instructions[last].op = Op::Yield { value: load_id };
658 assert!(
659 matches!(p.validate(), Err(IrError::MultipleUses { value: 0, .. })),
660 "{:?}",
661 p.validate()
662 );
663 }
664
665 fn qualifier_chain(levels: u32) -> Program {
667 let mut p = valid();
668 let observe = p.main.instructions[0].clone();
669 let mut instrs = vec![observe.clone()];
670 let mut acc = observe.id;
671 let mut next = 1000u32;
672 for _ in 0..levels {
673 let id = InstrId(next);
674 next += 1;
675 instrs.push(Instruction {
676 id,
677 op: Op::Within {
678 input: acc,
679 seconds: 1.0,
680 },
681 span: None,
682 });
683 acc = id;
684 }
685 instrs.push(Instruction {
686 id: InstrId(next),
687 op: Op::Ret { value: acc },
688 span: None,
689 });
690 p.main.instructions = instrs;
691 p
692 }
693
694 #[test]
695 fn accepts_a_program_nested_past_the_old_depth_limit() {
696 let p = qualifier_chain(255);
698 assert_eq!(p.validate(), Ok(()));
699 }
700
701 #[test]
702 fn accepts_a_very_deeply_nested_program_on_a_small_stack() {
703 std::thread::Builder::new()
705 .stack_size(256 * 1024)
706 .spawn(|| {
707 let p = qualifier_chain(10_000);
708 assert_eq!(p.validate(), Ok(()));
709 })
710 .expect("spawn test thread")
711 .join()
712 .expect("test thread panicked");
713 }
714
715 #[test]
716 fn rejects_an_empty_in_set() {
717 let mut p = lower(&parse("[ipv4-addr:value IN ('1.1.1.1')]").unwrap());
718 match &mut p.blocks[0].instructions[1].op {
719 Op::Compare { rhs, .. } => *rhs = Operand::Set(Vec::new()),
720 other => panic!("expected a compare, got {other:?}"),
721 }
722 assert!(
723 matches!(p.validate(), Err(IrError::OperandShape { .. })),
724 "{:?}",
725 p.validate()
726 );
727 }
728
729 #[test]
730 fn rejects_a_compare_whose_lhs_is_not_a_load() {
731 use crate::ast::Literal;
732
733 let mut p = valid();
734 let compare_id = p.blocks[0].instructions[1].id;
735 let chained = InstrId(700);
736 p.blocks[0].instructions.insert(
737 2,
738 Instruction {
739 id: chained,
740 op: Op::Compare {
741 operator: ComparisonOperator::GreaterThan,
742 negated: false,
743 lhs: compare_id,
744 rhs: Operand::Literal(Literal::Integer(2)),
745 },
746 span: None,
747 },
748 );
749 let last = p.blocks[0].instructions.len() - 1;
751 p.blocks[0].instructions[last].op = Op::Yield { value: chained };
752 assert!(
753 matches!(p.validate(), Err(IrError::ExpectedLoad { instr: 700, .. })),
754 "{:?}",
755 p.validate()
756 );
757 }
758
759 #[test]
760 fn rejects_a_negated_exists() {
761 let mut p = lower(&parse("[EXISTS file:name]").unwrap());
762 match &mut p.blocks[0].instructions[1].op {
763 Op::Compare { negated, .. } => *negated = true,
764 other => panic!("expected a compare, got {other:?}"),
765 }
766 assert!(
767 matches!(p.validate(), Err(IrError::NegatedExists { .. })),
768 "{:?}",
769 p.validate()
770 );
771 }
772
773 #[test]
774 fn rejects_a_main_block_of_the_wrong_kind() {
775 let mut p = valid();
776 p.main.kind = BlockKind::Comparison;
777 assert!(
778 matches!(p.validate(), Err(IrError::WrongBlockKind { .. })),
779 "{:?}",
780 p.validate()
781 );
782 }
783
784 #[test]
785 fn rejects_a_comparison_block_of_the_wrong_kind() {
786 let mut p = valid();
787 p.blocks[0].kind = BlockKind::Main;
788 assert!(
789 matches!(p.validate(), Err(IrError::WrongBlockKind { .. })),
790 "{:?}",
791 p.validate()
792 );
793 }
794
795 #[test]
796 fn rejects_non_finite_floats() {
797 use crate::ast::Literal;
798
799 let mut p = valid();
801 match &mut p.blocks[0].instructions[1].op {
802 Op::Compare { rhs, .. } => *rhs = Operand::Literal(Literal::Float(f64::NAN)),
803 other => panic!("expected a compare, got {other:?}"),
804 }
805 assert!(matches!(p.validate(), Err(IrError::NonFiniteFloat { .. })));
806
807 let mut p = lower(&parse("[file:name='a'] WITHIN 60 SECONDS").unwrap());
809 for instr in &mut p.main.instructions {
810 if let Op::Within { seconds, .. } = &mut instr.op {
811 *seconds = f64::INFINITY;
812 }
813 }
814 assert!(matches!(p.validate(), Err(IrError::NonFiniteFloat { .. })));
815 }
816
817 fn repeats_program(count: u64) -> Program {
819 let mut p = lower(&parse("[file:name='a'] REPEATS 2 TIMES").unwrap());
820 for instr in &mut p.main.instructions {
821 if let Op::Repeats { count: c, .. } = &mut instr.op {
822 *c = count;
823 }
824 }
825 p
826 }
827
828 #[test]
829 fn accepts_the_largest_readable_repeats_count_and_round_trips() {
830 let p = repeats_program(i64::MAX as u64);
831 assert_eq!(p.validate(), Ok(()));
832 let text = crate::ir::render(&p);
833 assert!(parse(&text).is_ok(), "{text}");
834 }
835
836 #[test]
837 fn accepts_zero_repeats_count_the_grammar_accepts() {
838 let p = repeats_program(0);
839 assert_eq!(p.validate(), Ok(()));
840 assert!(parse(&crate::ir::render(&p)).is_ok());
841 }
842
843 #[test]
844 fn rejects_repeats_counts_the_lexer_cannot_read() {
845 for count in [i64::MAX as u64 + 1, u64::MAX] {
846 assert_eq!(
847 repeats_program(count).validate(),
848 Err(IrError::RepeatsOutOfRange { count })
849 );
850 }
851 }
852
853 fn index_program(index: u64) -> Program {
855 let mut p = valid();
856 for instr in &mut p.blocks[0].instructions {
857 if let Op::Load { path } = &mut instr.op {
858 path.steps.push(PathStep::Index(index));
859 }
860 }
861 p
862 }
863
864 #[test]
865 fn accepts_the_largest_readable_path_index_and_round_trips() {
866 let p = index_program(i64::MAX as u64);
867 assert_eq!(p.validate(), Ok(()));
868 let text = crate::ir::render(&p);
869 assert!(parse(&text).is_ok(), "{text}");
870 }
871
872 #[test]
873 fn rejects_path_indices_the_lexer_cannot_read() {
874 for index in [i64::MAX as u64 + 1, u64::MAX] {
875 assert!(
876 matches!(
877 index_program(index).validate(),
878 Err(IrError::PathIndexOutOfRange { index: i, .. }) if i == index
879 ),
880 "{index}"
881 );
882 }
883 }
884
885 struct Rng(u64);
887 impl Rng {
888 fn next(&mut self) -> u64 {
889 self.0 ^= self.0 << 13;
890 self.0 ^= self.0 >> 7;
891 self.0 ^= self.0 << 17;
892 self.0
893 }
894 }
895
896 #[test]
897 fn validated_programs_always_render_to_parseable_text() {
898 let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
899 let edge_counts = [
900 0,
901 1,
902 i64::MAX as u64 - 1,
903 i64::MAX as u64,
904 i64::MAX as u64 + 1,
905 u64::MAX,
906 ];
907 let edge_secs = [
908 0.0,
909 -0.0,
910 1.0,
911 -1.5,
912 0.1,
913 1e300,
914 -1e300,
915 f64::MAX,
916 f64::MIN,
917 f64::MIN_POSITIVE,
918 5e-324,
919 9.3e18,
920 -9.3e18,
921 ];
922 let mut validated = 0;
923 for _ in 0..600 {
924 let mut p = lower(&parse("[file:name='a']").unwrap());
925 let n_idx = rng.next() % 3;
926 for _ in 0..n_idx {
927 let index = match rng.next() % 3 {
928 0 => edge_counts[(rng.next() % 6) as usize],
929 1 => rng.next(),
930 _ => rng.next() >> 1,
931 };
932 for instr in &mut p.blocks[0].instructions {
933 if let Op::Load { path } = &mut instr.op {
934 path.steps.push(PathStep::Index(index));
935 }
936 }
937 }
938 let mut acc = p.main.instructions[0].id;
939 p.main.instructions.pop(); let mut next = 1000u32;
941 for _ in 0..(1 + rng.next() % 3) {
942 let id = InstrId(next);
943 next += 1;
944 let op = if rng.next().is_multiple_of(2) {
945 let count = match rng.next() % 3 {
946 0 => edge_counts[(rng.next() % 6) as usize],
947 1 => rng.next(),
948 _ => rng.next() >> 1,
949 };
950 Op::Repeats { input: acc, count }
951 } else {
952 let seconds = match rng.next() % 3 {
953 0 => edge_secs[(rng.next() % edge_secs.len() as u64) as usize],
954 1 => f64::from_bits(rng.next()),
955 _ => (rng.next() % 100_000) as f64 / 7.0,
956 };
957 Op::Within {
958 input: acc,
959 seconds,
960 }
961 };
962 p.main.instructions.push(Instruction { id, op, span: None });
963 acc = id;
964 }
965 p.main.instructions.push(Instruction {
966 id: InstrId(next),
967 op: Op::Ret { value: acc },
968 span: None,
969 });
970 if p.validate().is_ok() {
971 validated += 1;
972 let text = crate::ir::render(&p);
973 assert!(
974 parse(&text).is_ok(),
975 "validated program failed to reparse: {text}"
976 );
977 }
978 }
979 assert!(
980 validated > 100,
981 "generator produced too few valid programs: {validated}"
982 );
983 }
984}