Базовые примеры формальной верификации в Engee
В этой статье приведены базовые примеры формальной верификации моделей, демонстрирующие практическое использование LTL-спецификаций в Spin и интерпретацию получаемых результатов.
Пример 1

Блок Chart модели promela_a.engee содержит единственное состояние A.
Узнать, активно состояние A или нет, можно по значению глобальной переменной promela_a_S.Chart.state.A.is_active в сгенерированном файле promela_a.pml.
Об активности Chart также можно судить по значению глобальной переменной promela_a_S.Chart.state.is_active.
С помощью препроцессора назначим этим переменным сокращенные псевдонимы для использования в формулах спецификации:
#define a promela_a_S.Chart.state.A.is_active
#define chart promela_a_S.Chart.state.is_active
и добавим их вместе со спецификациями:
// состояние "A" не активно в начальный момент времени:
ltl spec1 { !a }
// состояние "A" обязано стать активным в начальный момент времени или когда-нибудь в будущем:
ltl spec2 { <>a }
// блок Chart не активен в начальный момент времени:
ltl spec3 { !chart }
// активность состояния "A" и блока Chart изменяется синхронно:
ltl spec4 { [] (chart == a) }
в конец файла promela_a.pml.
Произведем верификацию спецификаций:
$ spin -run -ltl spec1 promela_a.pml 2>&1 | grep errors:
State-vector 40 byte, depth reached 0, errors: 0
$ spin -run -ltl spec2 promela_a.pml 2>&1 | grep errors:
State-vector 40 byte, depth reached 6, errors: 0
$ spin -run -ltl spec3 promela_a.pml 2>&1 | grep errors:
State-vector 40 byte, depth reached 0, errors: 0
$ spin -run -ltl spec4 promela_a.pml 2>&1 | grep errors:
State-vector 40 byte, depth reached 12, errors: 0
Консольный вывод программы Spin показывает, что каждая из спецификаций выполняется — верификация прошла успешно.
Пример 2

Модель abcpq.engee на рисунке имеет три особенности:
-
Бесконечный цикл
P→Q→P→ …; -
Недостижимое состояние
C; -
Тупиковое состояние
B.
Попробуем проверить эти утверждения.
Как обычно, объявим сокращенные псевдонимы для переменных, определяющих активность наших состояний. Для этого добавим в файл abcpq.pml секцию макросов препроцессора:
#define a abcpq_S.Chart.state.A.is_active
#define b abcpq_S.Chart.state.B.is_active
#define c abcpq_S.Chart.state.C.is_active
#define p abcpq_S.Chart.state.B.P.is_active
#define q abcpq_S.Chart.state.B.Q.is_active
Специфицировать недостижимость состояния C можно формулой «всегда не C», проверка которой, как и ожидается, проходит успешно:
ltl spec1 { []!c } // состояние C недостижимо
Эту спецификацию можно считать свойством безопасности (safety) □p для формулы p, запрещающей состоянию C активизироваться (то есть p = ¬C).
Однако если заменить в формуле выше имя c на q:
ltl absurd2 { []!q } // утверждается (неверно), что Q никогда не станет активным
то верификация измененной формулы ожидаемо завершится ошибкой, так как изначально было очевидно, что состояние Q достижимо:
$ spin -run -ltl absurd2 abcpq.pml
pan: ltl formula absurd2
pan:1: assertion violated !( !( !(abcpq_S.Chart.state.B.Q.is_active))) (at depth 49)
pan: wrote abcpq.pml.trail
...
State-vector 64 byte, depth reached 49, errors: 1
...
Контрпример, полученный из файла трассы:
$ spin -t -p -k abcpq.pml.trail abcpq.pml
где следует обратить внимание на строку, указывающую на присваивание, которого, согласно спецификации, не должно быть:
42: proc 0 (fsm:1) abcpq.pml:114 (state 34) [abcpq_S.Chart.state.B.Q.is_active = 1]
и строку с противоречащим спецификации значением переменной в конце трассы:
abcpq_S.Chart.state.B.Q.is_active = 1
Теперь проверим свойство живости (liveness) — необходимость когда-либо стать активным:
ltl absurd3 { <>c } // утверждается, что C рано или поздно станет активным
Результат верификации:
$ spin -run -ltl absurd3 abcpq.pml
...
pan: ltl formula absurd3
pan:1: acceptance cycle (at depth 31)
pan: wrote abcpq.pml.trail
...
State-vector 56 byte, depth reached 65, errors: 1
...
Здесь исследование трассы неэффективно: контрпример, очевидно, бесконечен.
А вот свойства живости ◊A ∧ ◊B ∧ ◊P ∧ ◊Q остальных состояний верифицируются успешно:
ltl spec4 { (<>a) && (<>b) && (<>p) && (<>q) } // свойство живости состояний A, B, P и Q
Также с помощью LTL можно верифицировать свойство справедливости (fairness) □◊P или □◊Q, то есть требование, что событие (активизация P или Q) будет происходить бесконечно часто:
ltl spec5 { []<>p } // свойство справедливости P
ltl spec6 { []<>q } // свойство справедливости Q
и свойство стабилизации (stabilization) ◊□B — требование, что некоторое событие (активность тупикового B) рано или поздно наступит навсегда:
ltl spec7 { <>[]b } // свойство стабилизации B
С помощью импликации и исключающего «ИЛИ» можно специфицировать корреляцию активности родительского и дочерних состояний:
ltl spec8 { [](b -> (p ^ q)) } // если активно родительское состояние, то активно и одно из дочерних
Вместо импликации можно использовать отношение эквивалентности:
ltl spec9 { [](b <-> (p ^ q)) } // родительское состояние активно тогда и только тогда, когда активно одно из дочерних
Комбинируя импликацию и оператор neXt, можно убедиться, что активность состояний P и Q осциллирует с периодичностью 1 шаг:
ltl spec10 { []((p -> X q) && (q -> X p)) } // за активацией состояния P всегда следует активация Q и наоборот
Пример 3

Рассмотрим еще одну простую модель ab.engee, проверка свойств которой в режиме прямой симуляции может занять значительное время, тогда как формальная верификация тех же свойств выполняется практически мгновенно.
Переход между состояниями A и B выполняется после того, как переменная i увеличится миллиард раз.
Объявим сокращенные имена:
#define chart ab_S.Chart.state.is_active
#define a ab_S.Chart.state.A.is_active
#define b ab_S.Chart.state.B.is_active
#define i ab_S.Chart.locals.i
#define x ab_S.Chart.output.x
#define y ab_S.Chart.output.y
Формула, верифицирующая факт обязательного перехода между состояниями A и B когда-нибудь в будущем:
ltl spec1 { <>(a -> X b) }
Убедимся, что этот переход не может происходить на каждом шаге; более сильная спецификация приведет к ошибке:
ltl absurd2 { [](a -> X b) } // неверно!
Оператор until также успешно верифицируется «когда-нибудь в будущем»:
ltl spec3 { <>(a U b) }
Учитывая, что оператор становится истинным сразу после активизации блока Chart, можно записать более строго:
ltl spec4 { [](!chart || (a U b)) }
Или эквивалентно, но короче:
ltl spec5 { [](chart -> (a U b)) }
Более слабая форма также успешно верифицируется:
ltl spec6 { [](chart -> (a W b)) }
А оператор «освобождения» (release) приводит к ошибке, так как состояния A и B не могут быть активны одновременно:
ltl absurd7 { [](chart -> (a V b)) } // неверно!
Проверим те же свойства, используя значения выходных переменных блока Chart x и y, изначально установленных в false:
ltl spec8 { <>(y U x) }
ltl spec9 { [](chart -> (y U x)) }
ltl spec10 { [](chart -> (y W x)) }
ltl absurd11 { [](chart -> (y V x)) } // неверно!
И в заключение проверим диапазон значений переменной i.
В начальный момент времени значение переменной i равно 0:
ltl spec12 { i == 0 }
Значение i всегда лежит в диапазоне [0..1000000001]:
ltl spec13 { [](0 <= i && i <= 1000000001) }
Когда-либо i принимает свое максимальное значение 1000000001:
ltl spec14 { <>(i == 1000000001) }