Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Разработка корректных программ / 2Верификация / 2ОсновыАналитВерифПрогРовно.doc
Скачиваний:
31
Добавлен:
01.05.2014
Размер:
245.25 Кб
Скачать

49

2. Основы аналитической верификации программ

2.1. Основные правила аналитической верификации программ

Рассмотрим утверждения о программах и свойства программ. Далее при формулировке свойств программ для краткости под программой S будет пониматься оператор, фрагмент программы или полная программа.

Правило (определение) 1. Программа S обладает свойством {P} S {Q}, где P и Q некоторые утверждения о значениях используемых в программе переменных, если каждому набору начальных значений переменных, относительно которых справедливо P, отвечают после завершения программы S такие значения переменных, относительно которых справедливо Q.

Иногда P называют предусловием (предутверждением), Qпостусловием (постутверждением), а пару P и Qспецификацией программы S. Программу называют корректной относительно спецификации, если она обладает свойством {P} S {Q}. Более точно, имеется в виду частичная корректность программы, поскольку свойство завершимости программы здесь предполагается и должно доказываться отдельно. Запись {PS {Q} называют также тройкой Хоара.

Для верификации (другими словами, для доказательства корректности) программ в описанном смысле целесообразно иметь набор таких правил верификации, применение которых к составным частям программы позволяет из их корректности сделать вывод о корректности программы в целом.

Простейший способ композиции составных частей – это последовательное соединение.

Правило 2 (последовательного соединения). Пусть S1 и S2программы, обладающие свойствами {PS1 {R} и {RS2 {Q}. Тогда программа Sbegin S1; S2 end обладает свойством {PS {Q}.

Обоснование (доказательство) правила 2: если до выполнения программы S было справедливо утверждение P, то после завершения первой части S1 будет справедливо R, но тогда по свойству второй части {RS2 {Q} после завершения S2 будет справедливо Q.

Это правило иногда записывают в форме так называемого правила вывода исчисления высказываний (предикатов)

{PS1 {R}, {RS2 {Q}

 , (2.1)

{PS {Q}

где над горизонтальной чертой располагают посылки, а под ней – следствие.

Об утверждениях A и B говорят, что A сильнее B, а B слабее A, если A  B (из A следует B). В утверждениях о переменных программы более слабое утверждение налагает меньше ограничений на переменные программы. Отсутствие ограничений соответствует самому слабому утверждению, тождественному значению True.

Пусть программа S имеет свойство {PS {Q}. Можно ли применить программу S в другом контексте, например: {P1S {Q1}? Это возможно, если выполняются соотношения P1  P и Q  Q1, и такой вывод следует непосредственно из правила 1. В свойстве {P1S {Q1} по сравнению с исходным свойством {PS {Q} усилено предусловие и ослаблено постусловие.

Правило 3 (об усилении предусловия и ослаблении постусловия). Пусть имеются свойство {PS {Q} и утверждения P1 и Q1, такие, что P1  P и Q  Q1, тогда имеет место свойство {P1S {Q1}.

В форме правила вывода это можно записать в виде

P1  P, {PS {Q}, Q  Q1

 . (2.2)

{P1S {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). Обозначим все переменные программы как ab, ..., z (кроме уже выделенной переменной x). Пусть до выполнения оператора присваивания все переменные принимали значения a0b0, ..., z0 (включая x0), а после выполнения  a1b1, ..., z1. Будем считать, что x1 = E(x0a0b0, ..., z0), a1 = a0b1 = b0, ..., z1 = z0. (2.4)

Цепочка равенств в (2.4), начиная со второго, означает, что при вычислении выражения E(xab, ..., z) отсутствует побочный эффект [5]. Далее при рассмотрении свойств программных конструкций (операторов) будет предполагаться отсутствие побочного эффекта при вычислении любых выражений. Пусть P = P(xab, ..., z) и до выполнения оператора присваивания было справедливо P(x  E), т. е. PE(x0a0b0, ..., z0), a0b0, ..., z0), тогда с учетом (2.4) будет справедливо P(x1a1b1, ..., z1).

Оказывается, что верно и утверждение, обратное (2.3): если после выполнения присваивания верно утверждение P, то до выполнения было верно утверждение P(x  E).

Доказательство. Пусть после завершения присваивания справедливо утверждение P(x1a1b1, ..., z1), тогда с учетом (2.4) будет справедливо и утверждение PE(x0a0b0, ..., z0), a0b0, ..., z0), т. е. P(x  E).

Итак, правило 4 можно усилить и сформулировать так: для того чтобы после завершения присваивания x := E было справедливо утверждение о переменных программы P, необходимо и достаточно, чтобы до выполнения оператора x := E было справедливо утверждение P(x  E).

Необходимость, а не только достаточность, условия P(x  E) можно интерпретировать следующим образом: предусловие P(x  E) является слабейшим из всех предусловий R, таких, что имеется свойство {Rx: = E {P} при заданном постусловии P.

В общем случае для заданных программы S и постусловия P слабейшее предусловие для P относительно S – это предикат, обозначаемый wp(S  P) (от англ.: weakest precondition) и описывающий самое слабое условие, которому достаточно подчинить начальное состояние для того, чтобы выполнение программы S завершилось и привело к заключительному состоянию, удовлетворяющему P. В этих обозначениях P(x  E)  wp(S  P).

Итак, для проверки свойства {Rx := E {P} рекомендуется сначала найти wp(x := E  P), т. е. P(x  E), а затем проверить R  wp(x := E  P).

Примеры: 1) проверим свойство {(x = 2p + 1) & (> 0)} x := x mod 2 {x = 1}, где pInteger; действительно, следуя рекомендации, имеем

wp(x := x mod 2 x = 1)  (x mod 2 = 1)  Odd(x)

и затем {(x = 2p + 1) & (> 0)}  Odd(x), что и доказывает требуемое;

2) рассмотрим следующее свойство:

{(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) & ( y = a),

wp (x := x + y  (y = b) & (x  y = a))  (y = b) & (x = a).

Правило 5 (для условного оператора). Пусть рассматривается свойство условного оператора

{Pif B then S1 else S2 {Q}. (2.5)

Для того чтобы это свойство имело место, необходимо и достаточно наличие свойств {P & BS1 {Q} и {P & not BS2 {Q}.

Доказательство. Необходимость: пусть имеется (2.5); если условие B выполнено, то, в силу семантики оператора if-then-else, должен выполняться оператор S1, но тогда должно быть {P & BS1 {Q}. Аналогично рассматривается случай, когда выполнено not B. Достаточность: пусть перед выполнением условного оператора справедливы P и B, тогда будет выполняться then-ветвь, а поскольку имеет место свойство {P & BS1 {Q}}, то после выполнения будет справедливо утверждение Q. Аналогично рассматривается выполнение else-ветви при справедливости P & not B.

Рассмотренные до сих пор правила были естественны, а их доказательства просты. Перейдем к рассмотрению циклической программы – это более интересный и трудный случай. Здесь в формулировке правила будет скрыт, а при доказательстве явно использован метод математической индукции.

Правило 6 (для цикла while-do). Пусть рассматривается оператор цикла while B do S, и пусть утверждение P таково, что тело цикла S обладает свойством {P & BS {P}, тогда оператор цикла обладает свойством {Pwhile B do S {P & not B}.

В форме правила вывода это записывается так:

{P&B} S {P}

   . (2.6)

{Pwhile B do S {P & not B}

Доказательство. Пусть имеет место свойство {P & BS {P} и перед входом в цикл справедливо P, тогда либо условие продолжения цикла B не выполнено и требуемое постусловие {P & not B} получается сразу, либо условие продолжения B выполнено и тогда работает тело цикла, после чего, в силу свойства {P & BS {P}, рассмотренная ситуация повторяется. Далее, если цикл завершился, то, рассмотрев последовательность шагов цикла begin SS; ...; end, индукцией по числу шагов легко показать, что при последней итерации вновь имеется свойство {P&BS {P} и, следовательно, при завершении цикла справедливо постусловие {P & not B}.

Утверждение P, которое справедливо перед выполнением цикла и воспроизводится на каждом его шаге (соответственно свойству {P&BS {P}), называют инвариантом цикла. Иногда его называют инвариантом Хоара или хоаровским инвариантом (по имени К.Хоара, предложившего правило 6).

В некоторых случаях используют более слабое, чем (2.6), правило

{PS {P}

  .

{Pwhile B do S {P & not B}

Очевидно, что если тело цикла обладает свойством {PS {P}, то оно обладает и свойством {P & BS {P}. Поэтому хоаровский инвариант цикла P можно попытаться получить из инварианта тела цикла, т. е. из такого утверждения P*, что {P*} S {P*}.

Если рассматривается цикл с заданными предусловием Q и постусловием R, т. е. {Qwhile B do S {R}, то полезно совместить правила 6 и 3:

Q  P, {P & B} S {P}, P & not B  R

. (2.7)

{Qwhile B do S {R}

Не всякий инвариант цикла в смысле (2.6) представляет интерес. Обычно требуется указать либо самый сильный инвариант, либо инвариант, согласованный с постусловием R, т. е. такой, что P & not B  R.

Задание. Используя представление цикла repeat S until B в виде begin Swhile not B do S end и применяя правила вывода (2.6) или (2.7), (2.1), (2.2), требуется получить следующее правило вывода.

Правило 7 (для цикла repeat-until):

{PS {Q}, {Q & not BS {Q}

 , (2.8)

{Prepeat S until B {Q & B}

или, в другой форме,

{PS {Q}, Q & not B  P

 . (2.9)

{Prepeat 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 < t0)} S {0  t < t0};

тогда выполнение цикла while B do S завершится.

Условие 1 означает, что утверждение t  0 – один из инвариантов цикла. Условие 2 означает, что перед выполнением тела цикла t > 0. Условие 3 означает, что функция t(X) убывает на каждом шаге цикла.

Справедливость правила 8 следует из того, что любая целочисленная убывающая последовательность с положительными элементами конечна.

Функция t(X) называется ограничивающей функцией цикла. В принципе, наличие ограничивающей функции не только доказывает завершимость цикла, но и позволяет оценить число его шагов: оно заведомо не превысит величины t(X0), где X0 – начальные значения переменных цикла.

Подбор ограничивающей функции, как и подбор инварианта цикла, требует изобретательности. Рассмотрим пример. Пусть дана циклическая программа (для nInteger):

while n > 1 do if Odd(nthen 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(nthen 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) не может быть выполнено.