
2. Основы аналитической верификации программ
2.1. Основные правила аналитической верификации программ
Рассмотрим утверждения о программах и свойства программ. Далее при формулировке свойств программ для краткости под программой S будет пониматься оператор, фрагмент программы или полная программа.
Правило (определение) 1. Программа S обладает свойством {P} S {Q}, где P и Q некоторые утверждения о значениях используемых в программе переменных, если каждому набору начальных значений переменных, относительно которых справедливо P, отвечают после завершения программы S такие значения переменных, относительно которых справедливо Q.
Иногда P называют предусловием (предутверждением), Q постусловием (постутверждением), а пару P и Q спецификацией программы S. Программу называют корректной относительно спецификации, если она обладает свойством {P} S {Q}. Более точно, имеется в виду частичная корректность программы, поскольку свойство завершимости программы здесь предполагается и должно доказываться отдельно. Запись {P} S {Q} называют также тройкой Хоара.
Для верификации (другими словами, для доказательства корректности) программ в описанном смысле целесообразно иметь набор таких правил верификации, применение которых к составным частям программы позволяет из их корректности сделать вывод о корректности программы в целом.
Простейший способ композиции составных частей – это последовательное соединение.
Правило 2 (последовательного соединения). Пусть S1 и S2 – программы, обладающие свойствами {P} S1 {R} и {R} S2 {Q}. Тогда программа S begin S1; S2 end обладает свойством {P} S {Q}.
Обоснование (доказательство) правила 2: если до выполнения программы S было справедливо утверждение P, то после завершения первой части S1 будет справедливо R, но тогда по свойству второй части {R} S2 {Q} после завершения S2 будет справедливо Q.
Это правило иногда записывают в форме так называемого правила вывода исчисления высказываний (предикатов)
{P} S1 {R}, {R} S2 {Q}
, (2.1)
{P} S {Q}
где над горизонтальной чертой располагают посылки, а под ней – следствие.
Об утверждениях A и B говорят, что A сильнее B, а B слабее A, если A B (из A следует B). В утверждениях о переменных программы более слабое утверждение налагает меньше ограничений на переменные программы. Отсутствие ограничений соответствует самому слабому утверждению, тождественному значению True.
Пусть программа S имеет свойство {P} S {Q}. Можно ли применить программу S в другом контексте, например: {P1} S {Q1}? Это возможно, если выполняются соотношения P1 P и Q Q1, и такой вывод следует непосредственно из правила 1. В свойстве {P1} S {Q1} по сравнению с исходным свойством {P} S {Q} усилено предусловие и ослаблено постусловие.
Правило 3 (об усилении предусловия и ослаблении постусловия). Пусть имеются свойство {P} S {Q} и утверждения P1 и Q1, такие, что P1 P и Q Q1, тогда имеет место свойство {P1} S {Q1}.
В форме правила вывода это можно записать в виде
P1 P, {P} S {Q}, Q Q1
. (2.2)
{P1} S {Q1}
С точки зрения универсальности программы S правило 3 обосновывает естественное стремление найти такое свойство, в котором P было бы как можно слабее, а Q как можно сильнее.
Основным элементарным оператором любой программы является оператор присваивания.
Правило 4 (свойство оператора присваивания). Пусть E – выражение, P – утверждение, а P(x E) означает утверждение, полученное из утверждения P заменой всех вхождений переменной x на выражение E. Тогда оператор присваивания обладает свойством
{ P(x E) } x := E {P}. (2.3)
Запись (2.3) означает, что, если до выполнения присваивания верно утверждение P(x E), то после выполнения будет верным утверждение P.
Доказательство свойства (2.3). Обозначим все переменные программы как a, b, ..., z (кроме уже выделенной переменной x). Пусть до выполнения оператора присваивания все переменные принимали значения a0, b0, ..., z0 (включая x0), а после выполнения a1, b1, ..., z1. Будем считать, что x1 = E(x0, a0, b0, ..., z0), a1 = a0, b1 = b0, ..., z1 = z0. (2.4)
Цепочка равенств в (2.4), начиная со второго, означает, что при вычислении выражения E(x, a, b, ..., z) отсутствует побочный эффект [5]. Далее при рассмотрении свойств программных конструкций (операторов) будет предполагаться отсутствие побочного эффекта при вычислении любых выражений. Пусть P = P(x, a, b, ..., z) и до выполнения оператора присваивания было справедливо P(x E), т. е. P( E(x0, a0, b0, ..., z0), a0, b0, ..., z0), тогда с учетом (2.4) будет справедливо P(x1, a1, b1, ..., z1).
Оказывается, что верно и утверждение, обратное (2.3): если после выполнения присваивания верно утверждение P, то до выполнения было верно утверждение P(x E).
Доказательство. Пусть после завершения присваивания справедливо утверждение P(x1, a1, b1, ..., z1), тогда с учетом (2.4) будет справедливо и утверждение P( E(x0, a0, b0, ..., z0), a0, b0, ..., z0), т. е. P(x E).
Итак, правило 4 можно усилить и сформулировать так: для того чтобы после завершения присваивания x := E было справедливо утверждение о переменных программы P, необходимо и достаточно, чтобы до выполнения оператора x := E было справедливо утверждение P(x E).
Необходимость, а не только достаточность, условия P(x E) можно интерпретировать следующим образом: предусловие P(x E) является слабейшим из всех предусловий R, таких, что имеется свойство {R} x: = E {P} при заданном постусловии P.
В общем случае для заданных программы S и постусловия P слабейшее предусловие для P относительно S – это предикат, обозначаемый wp(S P) (от англ.: weakest precondition) и описывающий самое слабое условие, которому достаточно подчинить начальное состояние для того, чтобы выполнение программы S завершилось и привело к заключительному состоянию, удовлетворяющему P. В этих обозначениях P(x E) wp(S P).
Итак, для проверки свойства {R} x := E {P} рекомендуется сначала найти wp(x := E P), т. е. P(x E), а затем проверить R wp(x := E P).
Примеры: 1) проверим свойство {(x = 2p + 1) & (p > 0)} x := x mod 2 {x = 1}, где p: Integer; действительно, следуя рекомендации, имеем
wp(x := x mod 2 x = 1) (x mod 2 = 1) Odd(x)
и затем {(x = 2p + 1) & (p > 0)} Odd(x), что и доказывает требуемое;
2) рассмотрим следующее свойство:
{(x = a) & (y = b)} x := x + y; y := x – y; x := x y {(x = b) & (y = a)},
которое позволяет интерпретировать данную последовательность присваиваний как операцию обмена значений переменных x и y; для проверки этого свойства применяем правила 2 и 4, раccматривая отдельные операторы присваивания в порядке, обратном их выполнению:
wp (x := x y (x = b) & (y = a)) (x y = b) & (y = a),
wp (y := x y (x – y = b) & (y = a)) (y = b) & (x y = a),
wp (x := x + y (y = b) & (x y = a)) (y = b) & (x = a).
Правило 5 (для условного оператора). Пусть рассматривается свойство условного оператора
{P} if B then S1 else S2 {Q}. (2.5)
Для того чтобы это свойство имело место, необходимо и достаточно наличие свойств {P & B} S1 {Q} и {P & not B} S2 {Q}.
Доказательство. Необходимость: пусть имеется (2.5); если условие B выполнено, то, в силу семантики оператора if-then-else, должен выполняться оператор S1, но тогда должно быть {P & B} S1 {Q}. Аналогично рассматривается случай, когда выполнено not B. Достаточность: пусть перед выполнением условного оператора справедливы P и B, тогда будет выполняться then-ветвь, а поскольку имеет место свойство {P & B} S1 {Q}}, то после выполнения будет справедливо утверждение Q. Аналогично рассматривается выполнение else-ветви при справедливости P & not B.
Рассмотренные до сих пор правила были естественны, а их доказательства просты. Перейдем к рассмотрению циклической программы – это более интересный и трудный случай. Здесь в формулировке правила будет скрыт, а при доказательстве явно использован метод математической индукции.
Правило 6 (для цикла while-do). Пусть рассматривается оператор цикла while B do S, и пусть утверждение P таково, что тело цикла S обладает свойством {P & B} S {P}, тогда оператор цикла обладает свойством {P} while B do S {P & not B}.
В форме правила вывода это записывается так:
{P&B} S {P}
. (2.6)
{P} while B do S {P & not B}
Доказательство. Пусть имеет место свойство {P & B} S {P} и перед входом в цикл справедливо P, тогда либо условие продолжения цикла B не выполнено и требуемое постусловие {P & not B} получается сразу, либо условие продолжения B выполнено и тогда работает тело цикла, после чего, в силу свойства {P & B} S {P}, рассмотренная ситуация повторяется. Далее, если цикл завершился, то, рассмотрев последовательность шагов цикла begin S; S; ...; S end, индукцией по числу шагов легко показать, что при последней итерации вновь имеется свойство {P&B} S {P} и, следовательно, при завершении цикла справедливо постусловие {P & not B}.
Утверждение P, которое справедливо перед выполнением цикла и воспроизводится на каждом его шаге (соответственно свойству {P&B} S {P}), называют инвариантом цикла. Иногда его называют инвариантом Хоара или хоаровским инвариантом (по имени К.Хоара, предложившего правило 6).
В некоторых случаях используют более слабое, чем (2.6), правило
{P} S {P}
.
{P} while B do S {P & not B}
Очевидно, что если тело цикла обладает свойством {P} S {P}, то оно обладает и свойством {P & B} S {P}. Поэтому хоаровский инвариант цикла P можно попытаться получить из инварианта тела цикла, т. е. из такого утверждения P*, что {P*} S {P*}.
Если рассматривается цикл с заданными предусловием Q и постусловием R, т. е. {Q} while B do S {R}, то полезно совместить правила 6 и 3:
Q P, {P & B} S {P}, P & not B R
. (2.7)
{Q} while B do S {R}
Не всякий инвариант цикла в смысле (2.6) представляет интерес. Обычно требуется указать либо самый сильный инвариант, либо инвариант, согласованный с постусловием R, т. е. такой, что P & not B R.
Задание. Используя представление цикла repeat S until B в виде begin S; while not B do S end и применяя правила вывода (2.6) или (2.7), (2.1), (2.2), требуется получить следующее правило вывода.
Правило 7 (для цикла repeat-until):
{P} S {Q}, {Q & not B} S {Q}
, (2.8)
{P} repeat S until B {Q & B}
или, в другой форме,
{P} S {Q}, Q & not B P
. (2.9)
{P} repeat S until B {Q & B}
Форма (2.8) явно предполагает наличие двух свойств тела цикла: для первой итерации и для всех остальных. Форму (2.9) иногда удобнее использовать, чем (2.8), так как в ней фигурирует лишь одно свойство тела цикла, хотя имеется еще дополнительное утверждение Q & not B P.
Отметим, что правила 6 и 7 позволяют доказать лишь частичную корректность цикла. Для полной корректности необходимо отдельно доказать завершимость цикла.
В простых случаях для доказательства завершимости выполнения циклической программы достаточно рассмотреть характер изменения какой-либо одной переменной. Например, в программе
{n 1} p := 1; k := n;
{инвариант: (p*k! = n!) & (0 k n)}
while k > 0 do
begin p := p*k; k := k 1 end
{p=n!}
переменная k 0 (k = n перед входом в цикл и k > 0 перед выполнением тела цикла) и на каждом шаге цикла уменьшается ровно на 1, поэтому выполнение цикла завершится ровно за n шагов. В более сложных случаях значения переменных программы могут изменяться немонотонно. Тогда следует рассмотреть значения не отдельной переменной, а некоторой функции от переменных программы. Пусть X – множество переменных программы.
Правило 8 (проверка завершимости цикла while B do S). Пусть t = t(X) – целочисленная функция переменных X, удовлетворяющая условиям:
1) t 0;
2) P & B (t > 0);
3) для любого значения t0 функции t имеет место свойство
{(P & B) & (0 < t = t0)} S {0 t < t0};
тогда выполнение цикла while B do S завершится.
Условие 1 означает, что утверждение t 0 – один из инвариантов цикла. Условие 2 означает, что перед выполнением тела цикла t > 0. Условие 3 означает, что функция t(X) убывает на каждом шаге цикла.
Справедливость правила 8 следует из того, что любая целочисленная убывающая последовательность с положительными элементами конечна.
Функция t(X) называется ограничивающей функцией цикла. В принципе, наличие ограничивающей функции не только доказывает завершимость цикла, но и позволяет оценить число его шагов: оно заведомо не превысит величины t(X0), где X0 – начальные значения переменных цикла.
Подбор ограничивающей функции, как и подбор инварианта цикла, требует изобретательности. Рассмотрим пример. Пусть дана циклическая программа (для n: Integer):
while n > 1 do if Odd(n) then n := n + 1 else n := n div 2
Функцию t(n) для n 1 определим следующим образом:
0, если n = 1,
t(n) =
n (1)n, если n > 1.
Проверим условия правила 8. Условия 1 и 2 выполнены, поскольку t(1) = 0, а если n > 1, то n (1)n n – 1 > 0. Условие 3 проверим отдельно для четного и нечетного n. Пусть n – четно. Если n > 4, то t(n div 2) n div 2 + 1 < t(n) = t0. При n = 4: t(n div 2) = t(2) = 1 < t(n) = t0 = 3. При n = 2: t(n div 2) = t(1) = 0 < t(n) = t0 = 1. Пусть теперь n – нечетно. Тогда t(n) = t0 = n + 1, а t(n + 1) = t = n, т. е. t < t0.
Отметим, что сама по себе функция t(n) не монотонна: t(1) = 0, t(2) = 1, t(3) = 4, t(4) = 3, t(5) = 6, t(6) = 5, t(7) = 8, t(8) = 7, t(9) = 10, t(10) = 9, t(11) = 12, t(12) = 11, ... . Она монотонно изменяется лишь на последовательности аргументов, порождаемой при работе цикла. Пусть, например, начальное значение n = 11, тогда при выполнении цикла порождается последовательность значений n = 11, 12, 6, 3, 4, 2, 1 и на этой последовательности функция t(n) строго убывает: t(n) = 12, 11, 5, 4, 3, 1, 0.
До сих пор не доказана и не опровергнута завершимость цикла
while n > 1 do
if Odd(n) then n := 3*n + 1 else n := n div 2
Компьютерные эксперименты показали [17], что цикл завершается для всех «разумных» значений n, однако формального доказательства этого факта нет.
Пример незавершающегося цикла (для n > 1):
while n > 1 do
if Odd(n) then n := n + 1 else n := n div 2 + 1
Действительно, тело цикла обладает легко проверяемым свойством {n > 1} S {n > 1}, поэтому условие окончания цикла not (n > 1) не может быть выполнено.