Aula 3 · Verificação Formal de Software

Programas e Teoremas

Tipos indutivos, funções recursivas, avaliação e o enunciado de teoremas em Lean

Baseada no Hitchhiker's Guide to Logical Verification (LoVe), capítulo 2.

§3.1 Das provas aos programas

  • A Aula 1 leu uma prova como um termo cujo tipo é a proposição que ela prova. A mesma teoria de tipos classifica dados.

  • Um tipo como ℕ reúne valores, e um termo de tipo ℕ → ℕ é um programa. O #check abaixo se lê como o #check de um termo de prova, com tipos no lugar de proposições.

fun n => n + 1 : #check fun n : => n + 1
fun n => n + 1 :   
  • Esta aula define tipos e funções e enuncia teoremas sobre eles. As provas esperam pela indução estrutural, nas próximas aulas.

§3.2 Tipos indutivos

  • O comando inductive define um tipo listando os seus construtores. O tipo contém exatamente os valores construídos por finitas aplicações de construtores, e nada mais.

Números naturais

namespace MyNat inductive Nat : Type where | zero : Nat | succ : Nat Nat end MyNat

Listas sobre um tipo qualquer

namespace MyList inductive List (α : Type) : Type where | nil : List α | cons : α List α List α end MyList
  • Lean já fornece Nat e List, então cada reconstrução vive em um espaço de nomes.

§3.2 Sintaxe abstrata

  • Os construtores carregam dados de outros tipos. AExp é a sintaxe abstrata das expressões aritméticas, e a linguagem imperativa das últimas aulas a estende.

inductive AExp : Type where | num : AExp | var : String AExp | add : AExp AExp AExp | sub : AExp AExp AExp | mul : AExp AExp AExp | div : AExp AExp AExp

A expressão (x + 3) * y

((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExp#check AExp.mul (AExp.add (AExp.var "x") (AExp.num 3)) (AExp.var "y")
((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExp

§3.3 Funções por casamento de padrões

  • Uma função sobre um tipo indutivo se define por casamento de padrões, uma equação por forma de construtor.

  • A recursão é estrutural quando cada chamada recursiva descasca um construtor. Lean aceita essas definições, já que elas terminam.

def add : | m, Nat.zero => m | m, Nat.succ n => Nat.succ (add m n) def mul : | _, Nat.zero => 0 | m, Nat.succ n => add m (mul m n)
def fib : | 0 => 0 | 1 => 1 | n + 2 => fib (n + 1) + fib n def powerParam (m : ) : | Nat.zero => 1 | Nat.succ n => mul m (powerParam m n)
  • Os padrões são mais ricos que construtores nus: n + 2 abrevia duas aplicações de succ. Um argumento que nenhuma equação inspeciona vai para a esquerda dos dois-pontos, como o m acima.

§3.4 Polimorfismo e argumentos implícitos

  • Uma definição pode receber um tipo como argumento, dado explicitamente em cada chamada, ou implicitamente entre chaves e inferido pelo elaborador. O prefixo @ restaura a forma explícita.

Argumento de tipo explícito

def append (α : Type) : List α List α List α | List.nil, ys => ys | List.cons x xs, ys => List.cons x (append α xs ys) append : (α : Type) List α List α List α#check @append
append : (α : Type)  List α  List α  List α

Implícito, com a notação de listas

def appendPretty {α : Type} : List α List α List α | [], ys => ys | x :: xs, ys => x :: appendPretty xs ys def reverse {α : Type} : List α List α | [] => [] | x :: xs => appendPretty (reverse xs) [x]
  • A notação escreve List.nil como [] e List.cons x xs como x :: xs, então a definição se lê como a sua própria especificação.

§3.5 Avaliação

  • #eval executa um programa pelo compilador; #reduce normaliza um termo simbolicamente no kernel. Use #eval em escala.

  • Um ambiente leva nomes de variáveis a valores, e eval reduz uma expressão ao seu valor.

def eval (env : String ) : AExp | AExp.num i => i | AExp.var x => env x | AExp.add e₁ e₂ => eval env e₁ + eval env e₂ | AExp.sub e₁ e₂ => eval env e₁ - eval env e₂ | AExp.mul e₁ e₂ => eval env e₁ * eval env e₂ | AExp.div e₁ e₂ => eval env e₁ / eval env e₂
9#eval add 2 7
9
0#eval eval (fun _ => 7) (AExp.div (AExp.var "y") (AExp.num 0))
0
  • A divisão por zero não falha. A divisão inteira é total, com x / 0 = 0. O comando #eval e a nossa função eval não têm relação, apesar dos nomes.

§3.5 A computação como método de prova

  • Uma equação cujos dois lados avaliam para o mesmo valor vale por rfl, o termo que a Aula 2 usou para n * n = 9 na testemunha 3.

  • Esta é a computação definicional. Ela resolve qualquer equação fechada, sem variáveis, e nada além.

example : add 2 7 = 9 := rfl example : eval (fun _ => 7) (AExp.div (AExp.var "y") (AExp.num 0)) = 0 := rfl
  • Em add m n = add n m as variáveis bloqueiam a computação, então a lei geral precisa de indução estrutural.

§3.6 Enunciados de teoremas

  • Um theorem é uma definição cujo tipo é uma proposição. Enunciá-lo não exige prova, e sorry fica onde a prova entrará.

namespace SorryTheorems theorem declaration uses `sorry`add_comm (m n : ) : add m n = add n m := m:n:add m n = add n m All goals completed! 🐙 theorem declaration uses `sorry`reverse_reverse {α : Type} (xs : List α) : reverse (reverse xs) = xs := α:Typexs:List αreverse (reverse xs) = xs All goals completed! 🐙 end SorryTheorems

Lean assinala cada uso de sorry

'SorryTheorems.add_comm' depends on axioms: [sorryAx]#print axioms SorryTheorems.add_comm
'SorryTheorems.add_comm' depends on axioms: [sorryAx]
  • #print axioms informa em que uma prova se apoia. Uma prova por computação não depende de axioma nenhum.

§3.6 Axiomas

  • Uma constante opaque tem tipo e nenhuma definição; um axiom afirma uma proposição sem prova nenhuma.

opaque a : opaque b : axiom a_less_b : a < b
  • Nada verifica um axioma, então um axioma inconsistente quebra silenciosamente todo o desenvolvimento. A disciplina enuncia axiomas apenas para discuti-los.

§3.7 Exemplo resolvido: subtração truncada

Defina sub sobre ℕ de modo que sub 3 7 = 0, já que ℕ não tem valores negativos.

A escolha das equações

  • A recursão descasca um succ de cada argumento, então o caso recursivo é m + 1, n + 1.

  • Dois casos base a interrompem. Subtrair zero devolve o primeiro argumento, e subtrair de zero devolve zero.

  • A primeira equação que casa vence, então 0, 0 cai na primeira delas.

def sub : | m, 0 => m | 0, _ => 0 | m + 1, n + 1 => sub m n

A verificação

4#eval sub 7 3
4
0#eval sub 3 7
0
  • Cada verificação é também um teorema, já que os dois lados são fechados.

example : sub 3 7 = 0 := rfl

§3.7 Exemplo resolvido: avaliar (x + 3) * y

Desdobrando eval, uma equação por vez

eval env ((x + 3) * y)
  = eval env (x + 3) * eval env y
  = (eval env x + eval env 3) * env "y"
  = (env "x" + 3) * env "y"
  = (2 + 3) * 4
  = 20
  • Cada passo é uma equação de eval: o caso mul, depois add, depois var e num.

  • O ambiente fornece as duas variáveis, e ℤ faz o resto.

A mesma computação em Lean

def someEnv : String | "x" => 2 | "y" => 4 | _ => 0 20#eval eval someEnv (AExp.mul (AExp.add (AExp.var "x") (AExp.num 3)) (AExp.var "y"))
20
example : eval someEnv (AExp.mul (AExp.add (AExp.var "x") (AExp.num 3)) (AExp.var "y")) = 20 := rfl

§3.7 Exemplo resolvido: o que a computação resolve

Três afirmações sobre add, que recursa sobre o seu segundo argumento.

Resolvidas por rfl

example : add 2 7 = 9 := rfl example (m : ) : add m 0 = m := rfl
  • A primeira é fechada, então os dois lados computam para 9.

  • A segunda é geral, mas add m 0 casa a primeira equação de add qualquer que seja m, e reduz a m em um passo.

Não resolvida por rfl

namespace Worked theorem declaration uses `sorry`zero_add (m : ) : add 0 m = m := m:add 0 m = m All goals completed! 🐙 end Worked
  • Aqui o segundo argumento é a variável, então equação alguma de add se aplica e o termo trava.

  • A afirmação é verdadeira, e prová-la exige indução estrutural sobre m, em uma aula adiante.

  • Quem decide qual lado computa é a forma da recursão, não a forma do enunciado.

§3.7 Exemplo resolvido: da definição ao enunciado

Acrescente um elemento ao final de uma lista e relacione a operação com reverse.

A definição

def snoc {α : Type} : List α α List α | [], y => [y] | x :: xs, y => x :: snoc xs y [1, 2, 3]#eval snoc [1, 2] 3
[1, 2, 3]

O enunciado que ela sugere

namespace Worked theorem declaration uses `sorry`reverse_cons {α : Type} (x : α) (xs : List α) : reverse (x :: xs) = snoc (reverse xs) x := α:Typex:αxs:List αreverse (x :: xs) = snoc (reverse xs) x All goals completed! 🐙 end Worked
  • reverse (x :: xs) desdobra para appendPretty (reverse xs) [x], e snoc (reverse xs) x trava na lista variável, então rfl falha.

  • Enunciar uma lei é gratuito; prová-la é o trabalho das próximas aulas. Escrever o enunciado primeiro é como um desenvolvimento cresce.

Resumo

  • Dados e provas compartilham uma teoria de tipos: um programa é um termo cujo tipo é um tipo de função.

  • inductive define um tipo pelos seus construtores; os valores são exatamente as aplicações finitas de construtores.

  • As funções vêm do casamento de padrões com recursão estrutural, uma equação por forma de construtor.

  • Os argumentos de tipo são explícitos (α : Type) ou implícitos {α : Type}, inferidos pelo elaborador e restaurados por @.

  • #eval executa pelo compilador, #reduce normaliza no kernel, e rfl transforma computação em prova de qualquer equação fechada.

  • Um theorem enuncia uma proposição; sorry adia a prova e #print axioms expõe o adiamento como sorryAx.

  • As leis gerais sobre variáveis precisam de indução estrutural, o assunto de uma aula adiante.

Exercícios: veja as notas de aula.