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