Especificação de Programas
$$ \Huge \ploop{G}{C} \qquad \ho{a}{P}{b} \qquad e \models a $$
O processo de verificação formal de um programa começa por uma definição formal da sintaxe dos programas, continua com a respetivas semântica, isto é, as computação e a especificação dos “comportamentos”: condições sobre as entradas e os respetivos resultados.
As condições das entradas e resultados são fórmulas de uma lógica de primeira ordem adequada e a verificação é uma prova com regras adequadas à sintaxe do programa.
Programas Sequenciais Transformacionais
O primeiro passo consiste em definir uma linguagem de programação adequada que, essencialmente, é um sub-conjunto das linguagens imperativas comuns (C, Python, Rust).
A formalização de uma linguagem (de programação ou outro uso) é assunto de outra disciplina, não tratado aqui. Resumindo, uma linguagem de programação pode ser definida usando uma gramática independente do contexto.
Definição (Expressão, Instrução, Programa)
Dada a gramática
$$ \begin{aligned} E \to & \quad N \cr & | \quad V \cr & | \quad \pneg{E} \cr & | \quad \padd{E}{E} \cr & | \quad \psub{E}{E} \cr & | \quad \pmul{E}{E} \cr B \to & \quad \ptrue \cr & | \quad \pfalse \cr & | \quad \pnot{B} \cr & | \quad \por{B}{B} \cr & | \quad \pand{B}{B} \cr & | \quad \peq{E}{E} \cr & | \quad \pne{E}{E} \cr & | \quad \plt{E}{E} \cr & | \quad \ple{E}{E} \cr C \to & \quad \pcp{V}{E} \cr & | \quad \pcond{B}{P}{P} \cr & | \quad \ploop{B}{P} \cr P \to & \quad \lambda \cr & | \quad \pseq{C}{P} \cr \end{aligned} $$
onde $N$ representa os números inteiros e $V$ as variáveis.
Então:
- Expressão numérica: Uma expressão numérica resulta da variável $E$. As expressões numéricas usam números, variáveis e as funções aritméticas $-_1, +_2, -_2, \times_2$; São termos na LPO.
- Expressão booleana: Uma expressão booleana resulta da variável $B$. As expressões booleanas podem ser construídas com os conectivos lógicos $\top, \bot, \neg, \lor, \land$ e com as relações numéricas $=_2, \not=_2, <_2, \leq_2$; São fórmulas sem quantificadores na LPO.
- Instrução: Uma instrução resulta da variável $C$. Há três tipos de instruções:
- Cópia $\boxed{\pcp{x}{n}}$. O lado esquerdo, $x$, é uma variável e o lado direito, $n$, uma expressão numérica.
- Condição $\boxed{\pcond{b}{p}{n}}$. A guarda $b$ é uma expressão booleana e $p, n$ são (sub-)programas, o ramo positivo e o ramo negativo respetivamente.
- Repetição ou Ciclo $\boxed{\ploop{b}{c}}$. A guarda $b$ é uma expressão booleana e o corpo $c$ um (sub-)programa.
- Programa: Um programa resulta da variável $P$. - O caso $\boxed{\lambda}$ é o programa vazio, sem instruções. - O caso $\boxed{\pseq{c}{p}}$ é uma sequência em que a a seguir à instrução $c$ estão as instruções de $p$. Nesta sequência não estão incluídas as instruções dos corpos das repetições nem dos ramos das condições.
Isto é, um programa é uma sequência de instruções, possivelmente vazia. Há três tipos de instruções: cópia, condição e repetição. As expressões numéricas e booleanas são parte dessas instruções.
Esta definição apenas cobre a sintaxe de um programa. Para definir a computação de um programa é necessário um estado que descreva a instrução atual e os valores das variáveis e como os diferentes tipos de instruções comandam a evolução do estado.
Definição (Estado, Valoração, Computação)
Seja $\kw{P}$ um programa com instruções $c = \del{c_1; \ldots; c_K}$ e variáveis $x = \del{x_1, \ldots, x_N}$.
Estado: Um estado, ou configuração, de $\kw{P}$ é um par $e = \del{a, v}$ em que:
- $a$ é o índice da instrução ativa, um número natural positivo.
- $v = \del{v_1, \ldots, v_N}$ é um vetor de números tal que o valor da variável $x_i$ é $v_i$.
Valoração: Um estado $e = \del{a, v}$ de um programa $\kw{P}$ define o valor das expressões desse programa da seguinte forma:
- Valor numérico: Seja $\alpha$ uma expressão numérica. Então $v_e\at{\alpha}$ é o valor de $\alpha$ em $e$ e resulta de:
- Substituir as variáveis de $\alpha$ pelos respetivos valores em $e$.
- Usar as regras usuais da aritmética para as operações.
- Valor booleano: Seja $\beta$ uma expressão booleana. Então $v_e\at{\beta}$ é o valor de $\beta$ em $e$ e resulta de:
- Calcular os valores numéricos em $\beta$.
- Usar as regras usuais das desigualdades e dos conectivos booleanos em $\beta$.
- Se $v_e\at{\beta}$ for $\ndT$ diz-se que $e$ satisfaz $\beta$ e escreve-se $v_e \models \beta$ ou simplesmente $e \models \beta$.
Computação: Uma computação do programa $\kw{P}$ a partir do estado $e_0$ é uma sequência de estados $\del{e_0, e_1, \ldots}$ em que $e_{i+1}$ resulta de $e_i$ em função da instrução activa de $\kw{P}$.
- Estado inicial: Em geral $e_0$ é dado. Caso contrário, assume-se que $e_0 = \del{1, \del{0, 0, \ldots, 0}}$, a instrução ativa é a primeira instrução de $\kw{P}$ e todas as variáveis têm valor $0$.
- Estado seguinte: Seja $e = \del{a, v}$ um estado com instrução ativa (índice) $a$ e valores $v$. O estado seguinte, $e’$, depende do tipo da instrução ativa:
- Se não existe instrução ativa ($a > K$) o programa termina e $e$ é o estado final.
- Cópia $\boxed{\pcp{x_k}{y}}$
- $e’ = \del{a+ 1, u}$ em que $u_j = v_j$ em todas as posições exceto $u_k = v_e\at{y}$;
- Condição $\boxed{\pcond{b}{p}{n}}$
- Se $e \models b$ seja $f$ o estado que resulta da computação de $p$ (o ramo positivo) a partir do estado $g = \del{1, v}$ e $u$ os valores em $f$. Então $e’ = \del{a + 1, u}$.
- Caso contrário, se $e \not\models b$, seja $f$ o estado que resulta da computação de $n$ (o ramo negativo) a partir do estado $g = \del{1, v}$ e $u$ os valores em $f$. Então $e’ = \del{a + 1, u}$.
- Repetição $\boxed{\ploop{b}{c}}$
- (continuação) Se $e \not\models b$, então $e’ = \del{a + 1, v}$.
- Caso contrário, se $e \models b$ então seja $f$ o estado que resulta da computação de $c$ (o corpo) a partir do estado $g = \del{1, v}$ e $u$ os valores em $f$:
- (ciclo) Se $f \models b$ então $e’ = \del{a, u}$.
- (continuação) Caso contrário, se $f \not\models b$, então $e’ = \del{a + 1, u}$.
As definições acima formalizam objetivamente a computação que um programa executa dada uma entrada (o estado inicial) para produzir o respetivo resultado (o estado final).
Exemplo. Computação
Copy.
Considere-se o programa Copy:
#![allow(unused)]
fn main() {
y = 1;
if x > y {
while y < x {
y = y + 1;
}
} else {
while x < y {
x = x + 1;
}
}
}
Exercícios:
- Quantas instruções tem este programa? De que tipos?
- Quantos sub-programas?
A computação deste programa no estado inicial $e = \del{1, \del{0, 0}}$ (assumindo $\kw{x} = x = x_1$ e $\kw{y} = y = x_2$) está ilustrada na seguinte tabela:
| Passo | Valores | Instrução ($a$) | Instrução (prog) | Observação |
|---|---|---|---|---|
| 1 | $\begin{matrix}x = 0\cr y = 0\end{matrix}$ | 1 | y = 1; | |
| 2 | $\begin{matrix}x = 0\cr y = 1\end{matrix}$ | 2 | if x > y { P } else { N } | $\not\models x > y$ |
| $\begin{matrix}x = 0\cr y = 1\end{matrix}$ | sub N | |||
| 3 | $\begin{matrix}x = 0\cr y = 1\end{matrix}$ | 2 - 1 | while x < y { C } | $\models x < y$ |
sub C | ||||
| 4 | $\begin{matrix}x = 0\cr y = 1\end{matrix}$ | 2 - 1 - 1 | x = x + 1; | |
| $\begin{matrix}x = 1\cr y = 1\end{matrix}$ | 2 - 1 - 2 | C termina | ||
| 5 | $\begin{matrix}x = 1\cr y = 1\end{matrix}$ | 2 - 1 | while x < y { C } | $\not\models x < y$ |
| $\begin{matrix}x = 1\cr y = 1\end{matrix}$ | 2 - 2 | N termina | ||
| $\begin{matrix}x = 1\cr y = 1\end{matrix}$ | 3 | Copy termina |
Exercícios:
- Faça a computação para o estado inicial $e = \del{1, \del{2, 0}}$.
- Será que definir o valor de $y$ no estado inicial tem algum efeito no estado final?
- Será que este programa garante que o estado final $f \models x = y$ seja qual for o estado inicial?
- Em que casos o valor de $x$ não é afetado pelo programa? E o valor de $y$?
Problema da Paragem
Pode acontecer que uma computação não termine. Por exemplo o programa
#![allow(unused)]
fn main() {
while true {}
}
não termina em qualquer estado inicial. Neste caso a computação é uma sequência infinita de estados e o estado final não está definido.
O Problema da Paragem, apresentado informalmente na secção Gödel e Turing da Consequência Semântica Proposicional, consiste em determinar se existe algum programa $\kw{T}$ que verifique se qualquer programa $\kw{X}$ termina ou não.
Agora, com uma definição formal de “programa”, este problema pode ser apresentado com mais rigor.
A ideia é representar cada programa $\kw{X}$ e estado inicial $e$ por um número, digamos $x$, e correr $\kw{T}$ no estado inicial $e_x = \del{1, (x, y = 0)}$.
Essa computação de $\kw{T}$ deve terminar e, no estado final, fica $y = 1$ se e só se o programa $\kw{X}$ com estado inicial $e$ termina.
O que Turing mostrou é que não existem programas como $\kw{T}$: nenhum programa resolve o problema da paragem.
Programas Totais e Parciais
Definição (Programa Total, Parcial)
Seja $P$ um programa e $e = \del{1, v}$ um estado.
- Se $P$ termina (ou converge) no estado $e$ como acima escreve-se $$P\at{e}\downarrow.$$
- Caso contrário, $P$ diverge no estado $e$ e escreve-se $$P\at{e}\uparrow.$$
Além disso,
- $P$ é total se termina qualquer que seja o estado inicial $e$ como acima: $\forall e~P\at{e}\downarrow$. Nesse caso escreve-se $$P\downarrow.$$
- $P$ é parcial se não termina com alguns estados iniciais $e$ como acima: $\exists e~P\at{e}\uparrow$. Nesse caso escreve-se $$P\uparrow.$$
Por exemplo o programa
#![allow(unused)]
fn main() {
while x != y {
x = x + 1
}
}
termina quando $e \models x \leq y$ mas não termina se $e \models x > y$. Portanto é um programa parcial.
Com a definição de programa total e parcial, o problema da paragem pode ser enunciado da seguinte forma:
Problema da Paragem. Existe um programa que classifica corretamente cada programa como total ou parcial?
Nesta formulação estamos a assumir que a entrada dos programas é dada na forma de um número que “codifica” juntamente o programa e o estado inicial.
O problema que se está a tratar aqui é mais “simples”: É possível provar que um programa tem um determinado comportamento? Isto é, se a entrada verificar determinadas condições iniciais existe uma prova que o resultado do programa satisfaz as condições finais?
Exemplo. Satisfação de estados iniciais e finais.
Considere-se o seguinte programa e condições sobre os valores das variáveis antes do programa correr e quando termina.
#![allow(unused)]
fn main() {
y = 0;
z = 0;
while z != x {
z = z + 1;
y = y + z;
}
}
Seja $e$ o estado inicial e $f$ o estado final.
| se, no início | então, quando termina |
|---|---|
| $\models x = 3$ | $\models z = 3$ |
| $\models x = 3$ | $\models y = 6$ |
| $\models x = 3$ | $\models z = x$ |
| $\models x = 3$ | $\models y = 0 + 1 + \cdots + x$ |
| $\models x = 3$ | $\models z = x \land y = \sum_{i=0}^x i$ |
| $\models x = 3$ | $\models y = \frac{x(x + 1)}{2}$ |
A ideia é caraterizar o “comportamento” de um programa pelos estados iniciais e finais.
Triplos de Hoare
$$\Large \ho{a}{P}{b}$$
Pretende-se relacionar uma condição inicial dada e uma condição final obtida por efeito da computação do programa.
Definição (Triplo de Hoare, $\ho{a}{P}{b}$)
Seja $\kw{P}$ um programa, $a$ e $b$ fórmulas. Um triplo de Hoare é a expressão
$$ \ho{a}{P}{b} $$
e significa que se o programa $\kw{P}$ corre num estado inicial $e$ que satisfaz $a$, $e \models a$, então, quando termina o estado final, $f$, satisfaz $b$, $f \models b$.
- A fórmula $a$ é a pré-condição do triplo.
- A fórmula $b$ é a pós-condição do triplo.
- O programa $\kw{P}$ é o programa do triplo.
As fórmulas $a$ e $b$ são fórmulas LPO no universo dos números inteiros, com as interpretações usuais dos símbolos $-_1, +_2, -_2, \times_2, <_2, =_2$.
Além disso:
- Cada variável de $\kw{P}$ é representada da mesma forma em $a$ e $b$: $\kw{x}$ é representada por $x$, $\kw{cont}$ por $\text{cont}$, etc.
- As variáveis ligadas de $a$ e $b$ não ocorrem em $\kw{P}$.
Notação. A representação usual dos triplos de Hoare é $\set{a} \kw{P} \set{b}$. Aqui usa-se a forma $\ho{a}{P}{b}$ para evitar confusão com a sintaxe dos programas, que usa {} para delimitar sub-programas, e com a substituição em fórmulas FOL, que usa $a\subst{x}{y}$ para substituir as ocorrências da variável $x$ pelo termo $y$ na fórmula $a$.
Especificação por Triplos de Hoare
Supondo que se pretende que um programa tenha o seguinte comportamento: quando $x$ é um número positivo, o programa define $y$ como um número cujo quadrado é menor do que $x$:
$$ \ho{x > 0}{P}{y^2 < x} $$
Isto é, se o programa correr num estado $e$ tal que $e \models x > 0$ quando termina, no estado que resulta, $f$, tem-se $f \models y^2 < x$.
Um programa que produza “lixo” quando $x \leq 0$ está de acordo com a especificação, desde que funcione como indicado quando $x > 0$.
Variáveis lógicas e do programa
Há uma associação entre as variáveis lógicas das condições e as variáveis que ocorrem nos programas. Esta associação é necessária mas requer algum cuidado.
Seja Fac1 o seguinte programa
y = 1;
z = 0;
while z != x {
z = z + 1;
y = y * z;
}
O triplo
$$ \ho{x \geq 0}{\kw{Fac1}}{y = x!} $$
especifica que, se $x \geq 0$ então Fac1 deve calcular o fatorial de $x$ e guardar o resultado em $y$. Neste caso a variável lógica $x$ está associada a $\kw{x}$ e $y$ a $\kw{y}$.
Mais adiante vai-se provar que este programa efetivamente calcula o fatorial de $x$.
Agora, seja Fac2 o programa
y = 1;
while x != 0 {
y = y * x;
x = x - 1;
}
Para Fac2 o triplo $\ho{x > 0}{\kw{Fac2}}{y = x!}$ não funciona porque $x$ é transformado pelo programa.
Este problema pode ser resolvido usando uma “variável lógica auxiliar”:
$$ \ho{x_0 > 0 \land x = x_0}{\kw{Fac2}}{y = x_0!} $$
- $x_0$ é uma variável lógica universalmente quantificada e que não ocorre em
Fac2: Para qualquer $x_0$, $\ho{x_0 > 0 \land x = x_0}{\kw{Fac2}}{y = x_0!}$. - Como $x_0$ não ocorre em
Fac2este triplo especifica o comportamento que se pretende: no fim $y$ é o fatorial de $x_0$.