Especificação de Programas

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 onde representa os números inteiros e as variáveis.

Então:

  • Expressão numérica: Uma expressão numérica resulta da variável . As expressões numéricas usam números, variáveis e as funções aritméticas ; São termos na LPO.
  • Expressão booleana: Uma expressão booleana resulta da variável . As expressões booleanas podem ser construídas com os conectivos lógicos e com as relações numéricas ; São fórmulas sem quantificadores na LPO.
  • Instrução: Uma instrução resulta da variável . Há três tipos de instruções:
    • Cópia . O lado esquerdo, , é uma variável e o lado direito, , uma expressão numérica.
    • Condição . A guarda é uma expressão booleana e são (sub-)programas, o ramo positivo e o ramo negativo respetivamente.
    • Repetição ou Ciclo . A guarda é uma expressão booleana e o corpo um (sub-)programa.
  • Programa: Um programa resulta da variável .
    • O caso é o programa vazio, sem instruções.
    • O caso é uma sequência em que a a seguir à instrução estão as instruções de . 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 um programa com instruções e variáveis .

Estado: Um estado, ou configuração, de é um par em que:

  • é o índice da instrução ativa, um número natural positivo.
  • é um vetor de números tal que o valor da variável é .

Valoração: Um estado de um programa define o valor das expressões desse programa da seguinte forma:

  • Valor numérico: Seja uma expressão numérica. Então é o valor de em e resulta de:
    1. Substituir as variáveis de pelos respetivos valores em .
    2. Usar as regras usuais da aritmética para as operações.
  • Valor booleano: Seja uma expressão booleana. Então é o valor de em e resulta de:
    1. Calcular os valores numéricos em .
    2. Usar as regras usuais das desigualdades e dos conectivos booleanos em .
  • Se for diz-se que satisfaz e escreve-se ou simplesmente .

Computação: Uma computação do programa a partir do estado é uma sequência de estados em que resulta de em função da instrução activa de .

  • Estado inicial: Em geral é dado. Caso contrário, assume-se que , a instrução ativa é a primeira instrução de e todas as variáveis têm valor .
  • Estado seguinte: Seja um estado com instrução ativa (índice) e valores . O estado seguinte, , depende do tipo da instrução ativa:
    • Se não existe instrução ativa () o programa termina e é o estado final.
    • Cópia
      • em que em todas as posições exceto ;
    • Condição
      • Se seja o estado que resulta da computação de (o ramo positivo) a partir do estado e os valores em . Então .
      • Caso contrário, se , seja o estado que resulta da computação de (o ramo negativo) a partir do estado e os valores em . Então .
    • Repetição
      • (continuação) Se , então .
      • Caso contrário, se então seja o estado que resulta da computação de (o corpo) a partir do estado e os valores em :
        • (ciclo) Se então .
        • (continuação) Caso contrário, se , então .

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:

  1. Quantas instruções tem este programa? De que tipos?
  2. Quantos sub-programas?

A computação deste programa no estado inicial (assumindo e ) está ilustrada na seguinte tabela:

PassoValoresInstrução ()Instrução (prog)Observação
11y = 1;
22if x > y { P } else { N }
sub N
32 - 1while x < y { C }
sub C
42 - 1 - 1x = x + 1;
2 - 1 - 2C termina
52 - 1while x < y { C }
2 - 2N termina
3Copy termina

Exercícios:

  1. Faça a computação para o estado inicial .
  2. Será que definir o valor de no estado inicial tem algum efeito no estado final?
  3. Será que este programa garante que o estado final seja qual for o estado inicial?
  4. Em que casos o valor de não é afetado pelo programa? E o valor de ?

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 que verifique se qualquer programa 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 e estado inicial por um número, digamos , e correr no estado inicial .

Essa computação de deve terminar e, no estado final, fica se e só se o programa com estado inicial termina.

O que Turing mostrou é que não existem programas como : nenhum programa resolve o problema da paragem.

Programas Totais e Parciais


Definição (Programa Total, Parcial)

Seja um programa e um estado.

  • Se termina (ou converge) no estado como acima escreve-se
  • Caso contrário, diverge no estado e escreve-se

Além disso,

  • é total se termina qualquer que seja o estado inicial como acima: . Nesse caso escreve-se
  • é parcial se não termina com alguns estados iniciais como acima: . Nesse caso escreve-se

Por exemplo o programa

#![allow(unused)]
fn main() {
while x != y {
    x = x + 1
}
}

termina quando mas não termina se . 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 o estado inicial e o estado final.

se, no inícioentão, quando termina

A ideia é caraterizar o "comportamento" de um programa pelos estados iniciais e finais.


Triplos de Hoare

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, )

Seja um programa, e fórmulas. Um triplo de Hoare é a expressão e significa que se o programa corre num estado inicial que satisfaz , , então, quando termina o estado final, , satisfaz , .

  • A fórmula é a pré-condição do triplo.
  • A fórmula é a pós-condição do triplo.
  • O programa é o programa do triplo.

As fórmulas e são fórmulas LPO no universo dos números inteiros, com as interpretações usuais dos símbolos .

Além disso:

  • Cada variável de é representada da mesma forma em e : é representada por , por , etc.
  • As variáveis ligadas de e não ocorrem em .

Notação. A representação usual dos triplos de Hoare é . Aqui usa-se a forma 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 para substituir as ocorrências da variável pelo termo na fórmula .

Especificação por Triplos de Hoare

Supondo que se pretende que um programa tenha o seguinte comportamento: quando é um número positivo, o programa define como um número cujo quadrado é menor do que :

Isto é, se o programa correr num estado tal que quando termina, no estado que resulta, , tem-se .

Um programa que produza "lixo" quando está de acordo com a especificação, desde que funcione como indicado quando .

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 especifica que, se então Fac1 deve calcular o fatorial de e guardar o resultado em . Neste caso a variável lógica está associada a e a .

Mais adiante vai-se provar que este programa efetivamente calcula o fatorial de .

Agora, seja Fac2 o programa

y = 1;
while x != 0 {
    y = y * x;
    x = x - 1;
}

Para Fac2 o triplo não funciona porque é transformado pelo programa.

Este problema pode ser resolvido usando uma "variável lógica auxiliar":

  • é uma variável lógica universalmente quantificada e que não ocorre em Fac2: Para qualquer , .
  • Como não ocorre em Fac2 este triplo especifica o comportamento que se pretende: no fim é o fatorial de .