Документация Engee

Базовые примеры формальной верификации в Engee

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

Пример 1

formal verification simple 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

formal verification simple 2

Модель abcpq.engee на рисунке имеет три особенности:

  • Бесконечный цикл PQP → …​;

  • Недостижимое состояние 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

formal verification simple 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) }