1use core::fmt;
17use std::{convert::Infallible, error};
18
19use log::{info, warn};
20use taco_interval_ta::IntervalThresholdAutomaton;
21use taco_model_checker::{
22 ModelChecker, ModelCheckerResult, TASpecOf,
23 internal_spec::{ErrorSpec, ErrorTarget},
24};
25use taco_smt_encoder::SMTSolverBuilder;
26
27use crate::{acs_threshold_automaton::ACSThresholdAutomaton, error_graph::ErrorGraph};
28
29pub mod partial_ord;
30
31pub mod error_graph;
32
33pub mod acs_threshold_automaton;
34
35#[derive(Debug, Clone)]
50pub struct ACSModelChecker {
51 ta_spec: TASpecOf<Self>,
53 unknown: Vec<String>,
55 ctx: SMTSolverBuilder,
57}
58
59impl ModelChecker for ACSModelChecker {
60 type ModelCheckerContext = SMTSolverBuilder;
61
62 type ModelCheckerOptions = Option<()>;
63
64 type SpecType = ErrorSpec;
65
66 type ThresholdAutomatonType = IntervalThresholdAutomaton;
67
68 type InitializationError = ACSModelCheckerInitializationError;
69
70 type ModelCheckingError = Infallible;
71
72 fn initialize(
73 _mode: Self::ModelCheckerOptions,
74 ta_spec: TASpecOf<Self>,
75 unknown: Vec<String>,
76 ctx: Self::ModelCheckerContext,
77 ) -> Result<Self, Self::InitializationError> {
78 if let Some((_, reach_spec, _)) = ta_spec
79 .iter()
80 .find(|(_, s, _)| s.contains_reachability_constraint())
81 {
82 return Err(ACSModelCheckerInitializationError::ReachabilitySpec(
83 Box::new(reach_spec.clone()),
84 ));
85 }
86
87 Ok(Self {
88 ta_spec,
89 unknown,
90 ctx,
91 })
92 }
93
94 fn verify(
95 self,
96 abort_on_violation: bool,
97 ) -> Result<ModelCheckerResult, Self::ModelCheckingError> {
98 let mut unsafe_properties = Vec::new();
99 let mut unknown_properties = self.unknown;
100
101 for (name, target, tas) in self.ta_spec {
102 let ErrorTarget::Reach(spec) = target else {
103 warn!("Property '{name}' ({target}) is not supported by the ACS model checker");
104 unknown_properties.push(name);
105 continue;
106 };
107
108 for ta in tas.into_iter().map(ACSThresholdAutomaton::new) {
109 let error_graph = ErrorGraph::compute_error_graph(spec.clone(), ta);
110
111 if error_graph.is_empty() {
112 info!(
113 "Error graph of property {} is empty, property is SAFE in this interval order.",
114 spec
115 );
116 continue;
117 }
118
119 let solver = self.ctx.new_solver();
120
121 info!(
122 "Error graph of property {} is not empty, checking paths for spuriousness",
123 spec
124 );
125 let res = error_graph.check_for_non_spurious_counter_example(solver);
126
127 match res {
128 ModelCheckerResult::SAFE => info!(
129 "Error graph of property {} is spurious, property is SAFE in this interval order.",
130 spec
131 ),
132 ModelCheckerResult::UNSAFE { violations, .. } => {
133 info!("Found counterexample to property {}", name);
134 unsafe_properties
135 .extend(violations.into_iter().map(|(_, p)| (name.clone(), p)));
136
137 if abort_on_violation {
138 return Ok(ModelCheckerResult::UNSAFE {
139 violations: unsafe_properties,
140 unknown: unknown_properties,
141 });
142 }
143 }
144 ModelCheckerResult::UNKNOWN(_) => {
145 info!(
146 "The model checker could not determine whether property {} is safe or unsafe",
147 spec
148 );
149 unknown_properties.push(name.clone());
150 }
151 }
152 }
153 }
154
155 if !unsafe_properties.is_empty() {
156 return Ok(ModelCheckerResult::UNSAFE {
157 violations: unsafe_properties,
158 unknown: unknown_properties,
159 });
160 }
161 if !unknown_properties.is_empty() {
162 return Ok(ModelCheckerResult::UNKNOWN(unknown_properties));
163 }
164
165 Ok(ModelCheckerResult::SAFE)
166 }
167}
168
169#[derive(Clone, Debug, PartialEq)]
170pub enum ACSModelCheckerInitializationError {
171 ReachabilitySpec(Box<ErrorTarget>),
172}
173
174impl fmt::Display for ACSModelCheckerInitializationError {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 ACSModelCheckerInitializationError::ReachabilitySpec(spec) => {
178 write!(
179 f,
180 "The counter system model checker does not support reachability specifications, but property {} contains a reachability constraint",
181 spec
182 )
183 }
184 }
185 }
186}
187
188impl error::Error for ACSModelCheckerInitializationError {}
189
190#[cfg(test)]
191mod tests {
192 use std::collections::HashMap;
193
194 use taco_model_checker::{ModelChecker, ModelCheckerResult, ModelCheckerSetupError};
195 use taco_parser::{ParseTAWithLTL, bymc::ByMCParser};
196 use taco_smt_encoder::SMTSolverBuilderCfg;
197 use taco_threshold_automaton::{
198 expressions::{
199 BooleanExpression, ComparisonOp, IntegerExpression, Location, Parameter, Variable,
200 },
201 general_threshold_automaton::{Action, UpdateExpression, builder::RuleBuilder},
202 path::{Configuration, PathBuilder, Transition},
203 };
204
205 use crate::{ACSModelChecker, ACSModelCheckerInitializationError};
206
207 #[test]
208 fn test_full_model_checker_reach_location_three_rules_one_guard_self_loop() {
209 let test_spec = "
210 skel test_ta1 {
211 shared var1, var2, var3;
212 parameters n, f;
213
214 assumptions (1) {
215 n > 3 * f;
216 n == 1;
217 f == 0;
218 }
219
220 locations (2) {
221 loc1 : [0];
222 loc2 : [1];
223 loc3 : [2];
224 }
225
226 inits (1) {
227 loc1 == n - f;
228 loc2 == 0;
229 loc3 == 0;
230 var1 == 0;
231 var2 == 0;
232 var3 == 0;
233 }
234
235 rules (4) {
236 0: loc1 -> loc1
237 when(true)
238 do {var1' == var1 + 1};
239 1: loc1 -> loc2
240 when(true)
241 do {};
242 2: loc2 -> loc3
243 when(var1 > 3 && var2 <= 0 && var1 <= 4)
244 do {};
245
246 }
247
248 specifications (1) {
249 test1:
250 [](loc3 == 0)
251 }
252 }
253 ";
254
255 let (ta, spec) = ByMCParser::new().parse_ta_and_spec(test_spec).unwrap();
256
257 let mc = ACSModelChecker::new(
258 Some(SMTSolverBuilderCfg::new_z3()),
259 None,
260 Vec::new(),
261 ta.clone(),
262 spec.into_iter(),
263 );
264 let mc = mc.unwrap();
265 let res = mc.verify(true).unwrap();
266
267 let path = PathBuilder::new(ta.clone())
268 .add_parameter_assignment(HashMap::from([
269 (Parameter::new("n"), 1),
270 (Parameter::new("f"), 0),
271 ]))
272 .unwrap()
273 .add_configuration(Configuration::new(
274 HashMap::from([
275 (Variable::new("var1"), 0),
276 (Variable::new("var2"), 0),
277 (Variable::new("var3"), 0),
278 ]),
279 HashMap::from([
280 (Location::new("loc1"), 1),
281 (Location::new("loc2"), 0),
282 (Location::new("loc3"), 0),
283 ]),
284 ))
285 .unwrap()
286 .add_transition(Transition::new(
287 RuleBuilder::new(0, Location::new("loc1"), Location::new("loc1"))
288 .with_action(Action::new_with_update(
289 Variable::new("var1"),
290 UpdateExpression::Inc(1),
291 ))
292 .build(),
293 1,
294 ))
295 .unwrap()
296 .add_configuration(Configuration::new(
297 HashMap::from([
298 (Variable::new("var1"), 1),
299 (Variable::new("var2"), 0),
300 (Variable::new("var3"), 0),
301 ]),
302 HashMap::from([
303 (Location::new("loc1"), 1),
304 (Location::new("loc2"), 0),
305 (Location::new("loc3"), 0),
306 ]),
307 ))
308 .unwrap()
309 .add_transition(Transition::new(
310 RuleBuilder::new(0, Location::new("loc1"), Location::new("loc1"))
311 .with_action(Action::new_with_update(
312 Variable::new("var1"),
313 UpdateExpression::Inc(1),
314 ))
315 .build(),
316 2,
317 ))
318 .unwrap()
319 .add_configuration(Configuration::new(
320 HashMap::from([
321 (Variable::new("var1"), 3),
322 (Variable::new("var2"), 0),
323 (Variable::new("var3"), 0),
324 ]),
325 HashMap::from([
326 (Location::new("loc1"), 1),
327 (Location::new("loc2"), 0),
328 (Location::new("loc3"), 0),
329 ]),
330 ))
331 .unwrap()
332 .add_transition(Transition::new(
333 RuleBuilder::new(0, Location::new("loc1"), Location::new("loc1"))
334 .with_action(Action::new_with_update(
335 Variable::new("var1"),
336 UpdateExpression::Inc(1),
337 ))
338 .build(),
339 1,
340 ))
341 .unwrap()
342 .add_configuration(Configuration::new(
343 HashMap::from([
344 (Variable::new("var1"), 4),
345 (Variable::new("var2"), 0),
346 (Variable::new("var3"), 0),
347 ]),
348 HashMap::from([
349 (Location::new("loc1"), 1),
350 (Location::new("loc2"), 0),
351 (Location::new("loc3"), 0),
352 ]),
353 ))
354 .unwrap()
355 .add_transition(Transition::new(
356 RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2")).build(),
357 1,
358 ))
359 .unwrap()
360 .add_configuration(Configuration::new(
361 HashMap::from([
362 (Variable::new("var1"), 4),
363 (Variable::new("var2"), 0),
364 (Variable::new("var3"), 0),
365 ]),
366 HashMap::from([
367 (Location::new("loc1"), 0),
368 (Location::new("loc2"), 1),
369 (Location::new("loc3"), 0),
370 ]),
371 ))
372 .unwrap()
373 .add_transition(Transition::new(
374 RuleBuilder::new(2, Location::new("loc2"), Location::new("loc3"))
375 .with_guard(
376 BooleanExpression::ComparisonExpression(
377 Box::new(IntegerExpression::Atom(Variable::new("var1"))),
378 ComparisonOp::Gt,
379 Box::new(IntegerExpression::Const(3)),
380 ) & BooleanExpression::ComparisonExpression(
381 Box::new(IntegerExpression::Atom(Variable::new("var2"))),
382 ComparisonOp::Leq,
383 Box::new(IntegerExpression::Const(0)),
384 ) & BooleanExpression::ComparisonExpression(
385 Box::new(IntegerExpression::Atom(Variable::new("var1"))),
386 ComparisonOp::Leq,
387 Box::new(IntegerExpression::Const(4)),
388 ),
389 )
390 .build(),
391 1,
392 ))
393 .unwrap()
394 .add_configuration(Configuration::new(
395 HashMap::from([
396 (Variable::new("var1"), 4),
397 (Variable::new("var2"), 0),
398 (Variable::new("var3"), 0),
399 ]),
400 HashMap::from([
401 (Location::new("loc1"), 0),
402 (Location::new("loc2"), 0),
403 (Location::new("loc3"), 1),
404 ]),
405 ))
406 .unwrap()
407 .build()
408 .unwrap();
409
410 let res = match res {
411 ModelCheckerResult::SAFE => {
412 unreachable!("Expected UNSAFE result based on test specification")
413 }
414 ModelCheckerResult::UNSAFE { violations: v, .. } => {
415 assert_eq!(v.len(), 1);
416 *v[0].1.clone()
417 }
418 ModelCheckerResult::UNKNOWN(_) => todo!(),
419 };
420
421 assert_eq!(
422 res,
423 path.clone(),
424 "Got:\n{}\n\nExpected:\n{}\n\n",
425 res,
426 path
427 );
428 }
429
430 #[test]
431 fn test_full_model_checker_reach_location_three_rules_one_guard_self_loop_with_var_constr() {
432 let test_spec = "
433 skel test_ta1 {
434 shared var1, var2, var3;
435 parameters n, f;
436
437 assumptions (1) {
438 n > 3 * f;
439 n == 1;
440 f == 0;
441 }
442
443 locations (2) {
444 loc1 : [0];
445 loc2 : [1];
446 loc3 : [2];
447 }
448
449 inits (1) {
450 loc1 == n - f;
451 loc2 == 0;
452 loc3 == 0;
453 var1 == 0;
454 var2 == 0;
455 var3 == 0;
456 }
457
458 rules (4) {
459 0: loc1 -> loc1
460 when(true)
461 do {var1' == var1 + 1};
462 1: loc1 -> loc2
463 when(true)
464 do {};
465 2: loc2 -> loc3
466 when(var1 > 3 && var2 <= 0 && var1 < 6 )
467 do {};
468
469 }
470
471 specifications (1) {
472 test1:
473 []((loc3 == 0) || (var1 < 5))
474 }
475 }
476 ";
477
478 let (ta, spec) = ByMCParser::new().parse_ta_and_spec(test_spec).unwrap();
479
480 let mc = ACSModelChecker::new(
481 Some(SMTSolverBuilderCfg::new_z3()),
482 None,
483 Vec::new(),
484 ta.clone(),
485 spec.into_iter(),
486 );
487 let mc = mc.unwrap();
488 let res = mc.verify(true).unwrap();
489
490 let path = PathBuilder::new(ta.clone())
493 .add_parameter_assignment(HashMap::from([
494 (Parameter::new("n"), 1),
495 (Parameter::new("f"), 0),
496 ]))
497 .unwrap()
498 .add_configuration(Configuration::new(
499 HashMap::from([
500 (Variable::new("var1"), 0),
501 (Variable::new("var2"), 0),
502 (Variable::new("var3"), 0),
503 ]),
504 HashMap::from([
505 (Location::new("loc1"), 1),
506 (Location::new("loc2"), 0),
507 (Location::new("loc3"), 0),
508 ]),
509 ))
510 .unwrap()
511 .add_transition(Transition::new(
512 RuleBuilder::new(0, Location::new("loc1"), Location::new("loc1"))
513 .with_action(Action::new_with_update(
514 Variable::new("var1"),
515 UpdateExpression::Inc(1),
516 ))
517 .build(),
518 5,
519 ))
520 .unwrap()
521 .add_configuration(Configuration::new(
522 HashMap::from([
523 (Variable::new("var1"), 5),
524 (Variable::new("var2"), 0),
525 (Variable::new("var3"), 0),
526 ]),
527 HashMap::from([
528 (Location::new("loc1"), 1),
529 (Location::new("loc2"), 0),
530 (Location::new("loc3"), 0),
531 ]),
532 ))
533 .unwrap()
534 .add_transition(Transition::new(
535 RuleBuilder::new(1, Location::new("loc1"), Location::new("loc2")).build(),
536 1,
537 ))
538 .unwrap()
539 .add_configuration(Configuration::new(
540 HashMap::from([
541 (Variable::new("var1"), 5),
542 (Variable::new("var2"), 0),
543 (Variable::new("var3"), 0),
544 ]),
545 HashMap::from([
546 (Location::new("loc1"), 0),
547 (Location::new("loc2"), 1),
548 (Location::new("loc3"), 0),
549 ]),
550 ))
551 .unwrap()
552 .add_transition(Transition::new(
553 RuleBuilder::new(2, Location::new("loc2"), Location::new("loc3"))
554 .with_guard(
555 BooleanExpression::ComparisonExpression(
556 Box::new(IntegerExpression::Atom(Variable::new("var1"))),
557 ComparisonOp::Gt,
558 Box::new(IntegerExpression::Const(3)),
559 ) & BooleanExpression::ComparisonExpression(
560 Box::new(IntegerExpression::Atom(Variable::new("var2"))),
561 ComparisonOp::Leq,
562 Box::new(IntegerExpression::Const(0)),
563 ) & BooleanExpression::ComparisonExpression(
564 Box::new(IntegerExpression::Atom(Variable::new("var1"))),
565 ComparisonOp::Lt,
566 Box::new(IntegerExpression::Const(6)),
567 ),
568 )
569 .build(),
570 1,
571 ))
572 .unwrap()
573 .add_configuration(Configuration::new(
574 HashMap::from([
575 (Variable::new("var1"), 5),
576 (Variable::new("var2"), 0),
577 (Variable::new("var3"), 0),
578 ]),
579 HashMap::from([
580 (Location::new("loc1"), 0),
581 (Location::new("loc2"), 0),
582 (Location::new("loc3"), 1),
583 ]),
584 ))
585 .unwrap()
586 .build()
587 .unwrap();
588
589 let res = match res {
590 ModelCheckerResult::SAFE => {
591 unreachable!("Expected UNSAFE result based on test specification")
592 }
593 ModelCheckerResult::UNSAFE { violations: v, .. } => {
594 assert_eq!(v.len(), 1);
595 *v[0].1.clone()
596 }
597 ModelCheckerResult::UNKNOWN(_) => todo!(),
598 };
599
600 assert_eq!(
601 res,
602 path.clone(),
603 "Got:\n{}\n\nExpected:\n{}\n\n",
604 res,
605 path
606 );
607 }
608
609 #[test]
610 fn test_err_on_reach_spec() {
611 let test_spec = "
612 skel test_ta1 {
613 shared var1, var2, var3;
614 parameters n, f;
615
616 assumptions (1) {
617 n > 3 * f;
618 n == 1;
619 f == 0;
620 }
621
622 locations (2) {
623 loc1 : [0];
624 loc2 : [1];
625 loc3 : [2];
626 }
627
628 inits (1) {
629 loc1 == n - f;
630 loc2 == 0;
631 loc3 == 0;
632 var1 == 0;
633 var2 == 0;
634 var3 == 0;
635 }
636
637 rules (4) {
638 0: loc1 -> loc1
639 when(true)
640 do {var1' == var1 + 1};
641 1: loc1 -> loc2
642 when(true)
643 do {};
644 2: loc2 -> loc3
645 when(var1 > 3 && var2 <= 0 && var1 <= 4)
646 do {};
647
648 }
649
650 specifications (1) {
651 test1:
652 [](loc3 == 0 && loc2 != 0)
653 }
654 }
655 ";
656
657 let (ta, spec) = ByMCParser::new().parse_ta_and_spec(test_spec).unwrap();
658
659 let mc = ACSModelChecker::new(
660 Some(SMTSolverBuilderCfg::new_z3()),
661 None,
662 Vec::new(),
663 ta.clone(),
664 spec.into_iter(),
665 );
666
667 assert!(mc.is_err());
668
669 assert!(matches!(
670 mc.unwrap_err(),
671 ModelCheckerSetupError::ErrorInitializingModelChecker(
672 ACSModelCheckerInitializationError::ReachabilitySpec(_)
673 )
674 ));
675 }
676
677 #[test]
680 fn test_result_reporting_per_property() {
681 let run = |specs: &str| {
682 let (ta, spec) = ByMCParser::new()
683 .parse_ta_and_spec(&format!(
684 "
685 skel test_ta1 {{
686 shared var1;
687 parameters n, f;
688
689 assumptions (1) {{
690 n > 3 * f;
691 n == 1;
692 }}
693
694 locations (2) {{
695 loc1 : [0];
696 loc2 : [1];
697 loc3 : [2];
698 }}
699
700 inits (1) {{
701 loc1 == n - f;
702 loc2 == 0;
703 loc3 == 0;
704 var1 == 0;
705 }}
706
707 rules (4) {{
708 0: loc1 -> loc2
709 when(true)
710 do {{}};
711 }}
712
713 specifications (1) {{
714 {specs}
715 }}
716 }}
717 "
718 ))
719 .unwrap();
720
721 let mc = ACSModelChecker::new(
722 Some(SMTSolverBuilderCfg::new_z3()),
723 None,
724 Vec::new(),
725 ta,
726 spec.expressions().iter().cloned(),
727 );
728 mc.expect("Failed to create model checker")
729 .verify(false)
730 .expect("Failed to model check")
731 };
732
733 let res = run("
734 holds: [](loc3 == 0);
735 reach_violated: [](loc2 == 0);
736 init_violated: loc1 == 0;
737 untranslatable: [](<>(loc1 == 0 || [](loc2 == 0)));
738 ");
739 let ModelCheckerResult::UNSAFE {
740 violations,
741 unknown,
742 } = res
743 else {
744 panic!("Expected UNSAFE, got {res:?}");
745 };
746 assert_eq!(unknown, vec!["untranslatable"]);
747 let mut names = violations
748 .iter()
749 .map(|(n, _)| n.as_str())
750 .collect::<Vec<_>>();
751 names.sort();
752 assert_eq!(names, vec!["init_violated", "reach_violated"]);
753
754 let res = run("
755 holds: [](loc3 == 0);
756 liveness: <>(loc2 != 0);
757 untranslatable: [](<>(loc1 == 0 || [](loc2 == 0)));
758 ");
759 let ModelCheckerResult::UNKNOWN(mut unknown) = res else {
760 panic!("Expected UNKNOWN, got {res:?}");
761 };
762 unknown.sort();
763 assert_eq!(unknown, vec!["liveness", "untranslatable"]);
764 }
765}