A partir da análise detalhada de comandos e seus efeitos sobre o estado do programa, o cálculo relacional oferece uma ferramenta poderosa para descrever formalmente o comportamento de fragmentos de código. Ele permite representar a transição entre o estado inicial (pré-estado) e o estado final (pós-estado) por meio de fórmulas que envolvem variáveis antigas (old_V) e novas (new_V), capturando assim, com precisão, a relação entre o antes e o depois da execução de um comando.

Por exemplo, considerando um comando que altera apenas variáveis específicas, como 𝑠 e 𝑖, sem modificar 𝑛, o cálculo relacional pode sintetizar em uma única fórmula o efeito desse comando. Caso o valor inicial de 𝑖 seja menor ou igual a 𝑛, o comando atualiza 𝑖 para 𝑛 e incrementa 𝑠 pelo somatório dos valores desde 𝑖 até 𝑛 − 1. Essa descrição, embora concisa, é capaz de expressar um comportamento complexo que, de outra forma, demandaria múltiplas condições e raciocínios.

Ao utilizar esse formalismo, podemos ir além do que o cálculo de Hoare oferece. No cálculo de Hoare, a correção parcial é expressa por tripletas {𝑃} 𝐶 {𝑄}, que conectam precondições 𝑃 e pós-condições 𝑄, cada uma referindo-se a um estado específico (pré ou pós-execução). Isso exige variáveis livres compartilhadas para relacionar estados distintos, enquanto o cálculo relacional unifica a descrição em uma única fórmula 𝐹 que relaciona explicitamente pré-estado e pós-estado.

Além disso, o cálculo relacional integra os conceitos dos transformadores de predicados, permitindo derivar a pré-condição mais fraca (wp) e a pós-condição mais forte (sp) para comandos arbitrários. Assim, para um comando simples de atribuição como x := x + y, a pré-condição mais fraca para garantir uma pós-condição 𝑄 é expressa substituindo 𝑥 por 𝑥 + 𝑦 dentro de 𝑄, enquanto a pós-condição mais forte para uma pré-condição 𝑃 exige a existência de um estado inicial 𝑥0 que satisfaz 𝑃, relacionando-o adequadamente ao valor final.

Contudo, essa abordagem inicialmente trata apenas da correção parcial, que assegura que, caso o programa termine, o resultado é correto. Ainda resta garantir que o programa não abortará com erro nem entrará em loops infinitos — a totalidade da correção. Para isso, a semântica formal precisa incorporar condições que assegurem não apenas a relação entre estados, mas também a não-abortividade e a terminação do programa.

O formalismo apresentado incorpora, portanto, condições adicionais para garantir que a execução do programa atinja um estado final válido, incluindo regras específicas para loops e comandos condicionais. Isso é fundamental para o desenvolvimento de sistemas confiáveis, onde a simples ausência de erros lógicos não é suficiente; é indispensável que o programa produza resultados em tempo finito e evite falhas inesperadas.

Além do entendimento da relação entre pré-estado e pós-estado, é importante compreender que o cálculo relacional permite a derivação mecânica de propriedades do programa, facilitando a verificação formal por meio de sistemas automatizados. Essa expressividade torna possível a análise de programas complexos, desde que se tenha um domínio claro das variáveis envolvidas e das condições iniciais.

Também é crucial reconhecer que, embora o cálculo relacional ofereça maior expressividade, sua aplicação exige um rigor matemático substancial e um entendimento profundo da semântica dos programas. Assim, para além da formulação das relações, o leitor deve internalizar a importância das condições de domínio das variáveis, da correta identificação dos estados antigos e novos, e do papel das invariantes de laço e das medidas de terminação na garantia da totalidade do programa.

Em síntese, o cálculo relacional não é apenas um meio de descrever o efeito dos comandos, mas um instrumento para garantir a confiabilidade e a robustez dos programas através de uma fundamentação lógica sólida, que relaciona estados antes e depois da execução, contempla os aspectos de correção parcial e total, e abre caminho para a verificação formal sistemática.

Como a Refinamento de Comandos e a Verificação de Programas Podem Garantir a Correção Total

A validação de condições de verificação é um aspecto crucial da programação formal. Quando lidamos com loops aninhados, como no exemplo que propomos, o processo de raciocínio começa pela elaboração do invariável do laço externo. Esse invariável precisa ser parcialmente verificado para garantir que, ao final do laço, as condições de saída desejadas sejam atendidas. A partir daí, nos aprofundamos no corpo do laço para garantir que o invariável seja preservado ao longo da execução do laço interno. Esse processo de verificação de invariantes envolve garantir a correta definição dos acessos ao array, a preservação da invariabilidade e a diminuição da medida de término, aspectos que são essenciais para assegurar o comportamento correto do programa.

Em um cenário mais complexo, como o que envolve loops duplamente aninhados, o raciocínio pode ser descrito de uma maneira “top-down”, ou seja, primeiramente é necessário elaborar e verificar o invariável do laço externo. Este invariável assegura que a pré-condição fornecida garantirá a invariância ao início do laço e que a saída do laço manterá a condição de pós-condição desejada. Só depois, o foco pode ser deslocado para o corpo do laço externo, verificando se a execução do laço interno preserva o invariável. Para o desenvolvimento do invariável do laço interno, todas as informações presentes no invariável do laço externo devem ser levadas em consideração.

O processo de simplificação da verificação pode ser aprimorado ao optar por encapsular o corpo do laço em um procedimento, em vez de incorporar outro laço diretamente dentro do corpo de um laço. Esse procedimento, ao ocultar os detalhes da computação correspondente, facilita a verificação, uma vez que podemos nos concentrar na validação de laços simples. Assim, ao invocar um procedimento (ainda não definido) no corpo de um laço, pode-se considerá-lo uma abstração da computação que se espera que satisfaça uma certa condição. A implementação do procedimento será uma refinação dessa abstração, garantindo que as expectativas sejam atendidas.

É importante destacar que a refinação de comandos é um conceito essencial nesse contexto. Em termos simples, um comando 𝐶1 refina outro comando 𝐶2 se for possível substituir qualquer ocorrência de 𝐶2 por 𝐶1 em um programa sem afetar seu resultado. O conceito de refinação pode ser formalizado através da relação 𝐶1 ⊑ 𝐶2, que implica que a execução de 𝐶1 leva ao mesmo resultado que a execução de 𝐶2, sem causar efeitos adversos no estado do programa. A refinação é reflexiva (qualquer comando refina a si mesmo) e transitiva (se 𝐶1 refina 𝐶2 e 𝐶2 refina 𝐶3, então 𝐶1 refina 𝐶3), mas não necessariamente simétrica.

A relação de refinação também garante a preservação da correção total. Isso significa que, se 𝐶1 refina 𝐶2 e 𝐶2 é totalmente correto em relação a uma especificação dada, então 𝐶1 também será totalmente correto em relação à mesma especificação. Este é um princípio fundamental em programação formal, pois permite substituir comandos mais complexos ou menos eficientes por alternativas mais simples, sem comprometer a correção do programa.

A ideia de comandos de especificação, que podem ser vistos como abstrações de comandos, oferece uma maneira prática de trabalhar com as especificações. Um comando de especificação é caracterizado por uma condição de entrada (pré-condição), uma condição de saída (pós-condição) e um conjunto de variáveis de entrada/saída (quadro). A especificação é, portanto, um comando que satisfaz a condição de pré e pós-condições associadas, sem revelar a implementação detalhada.

Para ilustrar, considere um comando que opera sobre variáveis inteiras 𝑥 e 𝑦, com a seguinte especificação: [ 0 ≤ 𝑦 ∧ 𝑦 ≤ old_𝑥 〜 new_𝑥 = old_𝑥 − 𝑦 ]. Esse comando decremente 𝑥 por 𝑦, garantindo que os outros estados do programa não sejam alterados, desde que 𝑦 esteja dentro do intervalo válido. Aqui, a entrada 𝑦 é validada de acordo com a condição prévia (0 ≤ 𝑦 ≤ 𝑥), e a saída 𝑥 é ajustada conforme a especificação da operação.

A linguagem estendida de comandos que permite a introdução de comandos de especificação é uma ferramenta poderosa. Ela formaliza o entendimento intuitivo de que um comando pode ser refinado em outro que satisfaça as mesmas condições de entrada e saída, mas com uma implementação mais eficiente ou mais simples. Esse conceito é central para o desenvolvimento de programas de alta confiabilidade, pois permite construir abstrações que são refinadas ao longo do tempo.

No entanto, a implementação desses conceitos na prática não é trivial. A verificação manual de condições de invariantes e a validação de comandos refinados exigem uma abordagem sistemática e, muitas vezes, o auxílio de ferramentas automatizadas de raciocínio ou raciocínio semi-automático. É crucial que o processo de refinamento preserve a lógica do programa, mantendo sua correção total em todas as suas etapas.

Como Verificar a Resposta e a Justificação de Sistemas Concorrentes com LTS

Os sistemas concorrentes são frequentemente modelados usando o conceito de LTS (Sistema de Transições Etiquetadas, ou Labelled Transition System), que se caracteriza por um conjunto de estados, condições iniciais e relações de transição. Em um modelo LTS, o objetivo é assegurar que a execução do sistema seja analisada e validada com base em certos critérios de resposta e progressão. O comportamento do sistema é formalizado de maneira rigorosa, utilizando ferramentas matemáticas que permitem verificar a correção do sistema e garantir que ele se comporte conforme esperado em cenários dinâmicos e paralelos.

Considere um LTS definido por lts=space:S,init:I,next:R\text{lts} = \langle \text{space}: S, \text{init}: I, \text{next}: R \rangle, onde SS é o espaço de estados, II é a condição inicial e RR é a relação de transição. Nesse contexto, introduzimos um conceito fundamental para a análise de liveness (ou vivacidade) em sistemas concorrentes: a verificação da propriedade de resposta.

A definição formal da propriedade de resposta pode ser expressa de maneira a garantir que, sempre que uma determinada condição FF (representando uma situação desejada ou uma solicitação) for satisfeita, o sistema eventualmente atingirá o estado associado à condição GG, que indica a conclusão ou resposta esperada. Para isso, é necessário definir um valor de medida T:SZT: S \rightarrow \mathbb{Z}, que mede o progresso de transições no sistema em relação ao alcance de GG. Essa medida garante que, caso o sistema entre em um estado onde FF se mantém verdadeira mas GG ainda não se verifica, o sistema continuará a transitar entre estados de FF até atingir GG, ou ao menos diminuir progressivamente a "distância" até a conclusão, sem retroceder.

Matematicamente, isso pode ser formalizado da seguinte maneira:

sS,F(s)¬G(s)enabled(s)lL,sS,R(l,s,s)(G(s)(F(s)0T(s)T(s)<T(s)))\forall s \in S, F(s) \land \neg G(s) \Rightarrow \text{enabled}(s) \land \forall l \in L, s' \in S, R(l, s, s') \Rightarrow (G(s') \lor (F(s') \land 0 \leq T(s') \land T(s') < T(s)))

A validade dessa condição pode ser demonstrada por meio de uma prova, onde se assume uma execução infinita do sistema, na qual a condição FF é satisfeita por um período indefinido, enquanto GG não é atendida. O objetivo da prova é demonstrar que, sob essas condições, o sistema não pode continuar indefinidamente sem transitar para um estado em que GG seja satisfeita, devido à diminuição progressiva da medida TT.

Esse raciocínio é muito semelhante ao usado para verificar a terminação de loops em sistemas de controle. A principal diferença é que, ao invés de se tratar da execução de um único loop, o sistema é composto por transições entre múltiplos estados, cada um com seus próprios critérios de progresso. Quando um estado satisfaz FF, o sistema deve, eventualmente, evoluir para um estado que satisfaz GG, e isso é garantido pela estrutura da medida TT.

A aplicação desse conceito pode ser ilustrada por um exemplo prático envolvendo dois threads concorrentes, AA e BB, que operam sobre uma variável compartilhada ss. A implementação desse sistema em pseudo-código seria algo como:

text
var s := 0
A: loop || B: loop 0: wait s = 0 0: wait s = 1 1: ... 1: ... 2: s := 1 2: s := 0

Aqui, o comando wait F impede a execução do thread enquanto a condição FF não for atendida. Esse sistema garante que ambos os threads alternem no acesso à região crítica, assegurando a exclusão mútua. O modelo LTS para esse sistema é construído com variáveis aa e bb, que representam os contadores de programa dos threads AA e BB, respectivamente. A análise desse sistema busca verificar uma propriedade de resposta, ou seja, se o thread AA solicitar acesso à região crítica, ele eventualmente obterá esse acesso.

Para verificar essa propriedade, definimos uma medida T(a,b,s)T(a, b, s), que "conta" o número de passos necessários para o thread BB alcançar um estado em que s=0s = 0. O valor de TT depende do valor do contador de programa de BB e da variável compartilhada ss. O objetivo é mostrar que, independentemente da configuração inicial, o sistema atenderá à condição de resposta, ou seja, que o acesso de AA à região crítica será eventualmente concedido.

A verificação de tais propriedades exige a definição precisa de transições e a análise do comportamento do sistema ao longo do tempo, garantindo que, a partir de qualquer configuração válida, a transição para um estado que atenda a GG seja inevitável.

Além disso, nem todas as verificações de liveness podem ser baseadas em medidas explícitas. Para casos onde as transições não podem ser diretamente contadas ou medidas de maneira direta, introduzimos o conceito de fairness (justiça). Existem dois tipos principais de fairness: weak fairness (justiça fraca) e strong fairness (justiça forte). A diferença entre os dois tipos de fairness está na exigência de repetição das transições.

A weak fairness exige que, uma vez que uma transição esteja permanentemente habilitada, ela deve ser executada infinitamente muitas vezes. Por outro lado, strong fairness exige apenas que a transição seja habilitada infinitamente muitas vezes, sem necessariamente ser permanentemente habilitada.

Ambas as formas de fairness são úteis para garantir que, mesmo em sistemas com comportamentos complexos e não determinísticos, as transições importantes ocorram eventualmente, levando o sistema a um estado de resposta. A aplicação desses conceitos permite validar a vivacidade do sistema de uma forma rigorosa, independentemente de a medida ser explícita ou baseada em um critério de fairness.

Como a Extensão de Teorias e Definições Afeta Modelos Lógicos

Em teoria, é possível estender uma teoria sem comprometer sua consistência. Embora não explicitamente mencionado nas definições formais, o conceito de adicionar novos símbolos à teoria é amplamente aceito, contanto que isso não adicione restrições adicionais. Em outras palavras, qualquer modelo da teoria original continua sendo um modelo da teoria estendida. Um modelo é uma interpretação matemática dos axiomas de uma teoria e serve como base para derivar as consequências lógicas dessa teoria. Para deduzir tais consequências, o cálculo lógico é empregado, conforme explicado no Capítulo 3.

Por exemplo, consideremos a teoria dos números naturais, NAT. Uma consequência lógica dessa teoria pode ser representada pela fórmula s(0)+s(0)=s(s(0))s(0) + s(0) = s(s(0)). Aderindo ao processo de derivação, temos a seguinte sequência de raciocínios:

  1. s(0)+s(0)=s(s(0)+0)s(0) + s(0) = s(s(0) + 0)

  2. s(s(0)+0)=s(s(0))s(s(0) + 0) = s(s(0))

A primeira equação é garantida pelo axioma x,y.x+s(y)=s(x+y)\forall x, y. \, x + s(y) = s(x + y), e a segunda pela definição de x+0=xx + 0 = x. Estas operações, embora simples, revelam um aspecto profundo da estrutura lógica e da relação entre axiomas e suas consequências.

Uma teoria é chamada de consistente se existir um modelo que satisfaça todos os seus axiomas. Caso contrário, a teoria é considerada inconsistente. Após criar uma teoria, é fundamental mostrar sua consistência, pois, sem um modelo, toda análise da teoria perde seu valor. No entanto, uma teoria consistente não necessariamente determina um único modelo. Existem modelos diferentes que podem ser construídos a partir de uma mesma teoria. Um exemplo é a teoria dos números naturais NAT, que possui como modelo os números naturais tradicionais, mas também pode ser estendida com elementos adicionais, como o infinito.

Por exemplo, ao estender a teoria NAT com um novo elemento \infty, pode-se obter uma nova interpretação. Nesse modelo, s()=s(\infty) = \infty, x+=x + \infty = \infty, e outras propriedades podem ser deduzidas. Embora esse modelo seja diferente do modelo "padrão" dos números naturais, ele ainda é válido dentro da teoria NAT.

Com a consistência de uma teoria estabelecida, surge a possibilidade de expandir seu vocabulário para incluir novos conceitos, como constantes, funções e predicados. Contudo, para manter a consistência, é essencial que essas novas definições sejam cuidadosamente formuladas. Se o novo vocabulário for adicionado implicitamente, isso pode resultar em inconsistências. Por isso, as novas noções devem ser definidas explicitamente com base nos conceitos existentes.

A definição de constante é um processo que introduz um novo símbolo e uma fórmula associada, sem causar ambiguidades no contexto da teoria original. Por exemplo, ao estender a teoria NAT com as definições 1:=s(0)1 := s(0) e 2:=1+12 := 1 + 1, estamos criando duas novas constantes sem comprometer a estrutura lógica da teoria. A partir dessa definição, podemos deduzir que 2=s(s(0))2 = s(s(0)), completando o modelo de forma coerente.

Além das constantes, as funções também podem ser definidas de maneira análoga. Por exemplo, a definição de uma função d(x):=x+xd(x) := x + x introduz uma nova função sem violar a consistência da teoria. Isso permite que se criem novas relações e cálculos dentro da estrutura lógica já estabelecida. Da mesma forma, predicados podem ser introduzidos, como no caso das definições de relações de ordem, como x<yx < y, xyx \geq y, e x>yx > y, que são construídas a partir de predicados previamente definidos. A introdução desses predicados facilita a formalização de conceitos matemáticos mais complexos, como as comparações entre números.

Um ponto crucial no processo de expansão de teorias é a garantia de que tal expansão seja conservadora. Isso significa que a nova teoria, resultante da adição de novos símbolos, deve ser uma extensão conservadora da teoria original, ou seja, a teoria original ainda deve ser válida dentro do novo contexto, sem perder sua consistência.

Além disso, ao lidar com extensões de teorias, deve-se lembrar que essas definições adicionadas (seja de constantes, funções ou predicados) podem introduzir novas formas de raciocínio lógico, mas sempre com a condição de que a teoria original continue sendo válida. As novas definições apenas ampliam o vocabulário lógico, sem alterar a estrutura fundamental da teoria. Essa abordagem não só garante a consistência, mas também promove uma maior flexibilidade na exploração de conceitos matemáticos.

A importância de tais expansões na teoria lógica não pode ser subestimada. Elas permitem que se criem modelos mais complexos e abrangentes, que podem ser aplicados a uma variedade de áreas da matemática e da lógica. Contudo, o que é essencial para o leitor entender é que, ao expandir uma teoria, a adição de novos elementos não deve ser feita de maneira arbitrária, mas sim com base em definições rigorosas e claras que preservem a estrutura e a consistência da teoria original.

Ao construir modelos a partir de teorias estendidas, o raciocínio lógico deve ser aplicado com cuidado. Cada definição, seja ela de uma constante, função ou predicado, deve ser testada para garantir que não introduza inconsistências ou ambiguidades no modelo global. Só assim é possível ampliar o alcance das teorias sem comprometer sua base lógica.