Verifica dei programmi Consiste nella specifica e nella dimostrazione delle proprietà dei programmi. Per specificare le proprietà utilizzeremo la logica del I ordine. Per dimostrare che le proprietà specificate sono soddisfatte dal programma in questione introdurremo le principali tecniche di verifica. Una specifica è una descrizione precisa di proprietà che un programma dovrebbe avere. Faremo vedere come inserire tali specifiche nel programma (annotazioni di programma). 1
Verifica dei programmi Concentreremo la nostra attenzione su: Proprietà di correttezza parziale (o di safety), che asseriscono che certi stati, tipicamente stati di errore, non possono mai occorrere durante l esecuzione del programma. Il metodo di dimostrazione usato è quello delle asserzioni induttive. Proprietà di correttezza totale (di progresso), che asseriscono che durante l esecuzione del programma certi stati vengono raggiunti. In generale una dimostrazione di correttezza totale viene così suddivisa: Dimostrazione di correttezza parziale Terminazione (metodo di dimostrazione: funzione di ranking) 2
Metodo fondazionale delle asserzioni induttive È un metodo basato sull induzione matematica. Per dimostrare che ogni stato della computazione soddisfa una certa formula ϕ della logica del I ordine: si dimostra come caso base che ϕ vale all inizio dell esecuzione, si assume per ipotesi induttiva che ϕ vale correntemente e si dimostra come passo induttivo che ϕ vale dopo un altro passo del programma. L applicazione del metodo può essere semplice. Tuttavia è possibile incontrare qualche difficoltà nella scoperta di annotazioni aggiuntive necessarie per l induzione. 3
Metodo fondazionale della funzione di ranking Consiste nell associare ad ogni loop e funzione ricorsiva una funzione (funzione di ranking) che mappa le variabili del programma in un dominio ben fondato, Quindi si dimostra che, durante la computazione del programma, il valore della funzione di ranking si riduce. Poichè la relazione è ben fondata, i loop e le ricorsioni devono fermarsi. 4
Linear search @pre @post bool LinearSearch(int [] a, int l, int u, int e) { for @ (int i := l;i u;i := i+1){ if (a[i] = e) return true; } return false; } LinearSearch ricerca il valore e all interno del range [l, u] di un array a. Restituisce true sse l array dato contiene il valore e fra il lower bound l e l upper bound u. Si comporta correttamente solo se 0 l ed u < a. 5
Binary search @pre @post bool BinarySearch(int [] a, int l, int u, int e) { if (l > u) return false; else { int m := (l+u) div 2; if (a[m] = e) return true; else if (a[m] < e) return BinarySearch(a, m+1, u, e); else return BinarySearch(a, l, m 1, e); } } BinarySearch è una funzione ricorsiva che ricerca il valore e nel range [l,u] di un array ordinato a di interi. Restituisce true sse a 6
contiene il valore e nel range [l, u]. Funziona correttamente solo se 0 l e u a. Un livello di ricorsione opera come segue: Se l > u allora il sottoarray (vuoto) non può contenere e e si restituisce false; Altrimenti si esamina l elemento di mezzo a[m] (a div b = Def a b ); Se a[m] = e allora si restituisce true; Altrimenti si effettua la ricorsione sulla metà destra se a[m] < e, sulla metà sinistra se a[m] > e. 7
Annotazioni di programma Un annotazione è una formula della logica del I ordine le cui variabili libere includono soltanto dele variabili del programma. Un annotazione F posta accanto ad un istruzione asserisce che F è vera tutte le volte che il controllo del programma raggiunge quella istruzione. La specifica di funzione è una coppia di annotazioni: Precondizione di funzione: formula F le cui variabili libere includono solo i parametri formali. Specifica sotto quali input deve essere eseguita la funzione. Postcondizione di funzione: è una formula G le cui variabili libere includono solo i parametri formali e la variabile speciale rv che rappresenta il valore di output della funzione. 8
Linear search con specifica di funzione @pre 0 l u < a @post rv ( i.l i u a[i] = e) bool LinearSearch(int [] a, int l, int u, int e) { for @ (int i := l;i u;i := i+1){ if (a[i] = e) return true; } return false; } La precondizione asserisce che il lower bound l deve essere non più piccolo di 0 e che l upper bound u deve essere più piccolo della lunghezza dell array. La post condizione asserisce che il valore di ritorno rv è true sse a[i] = e per qualche indice i [l,u] di a. 9
Linear search con specifica di funzione 2. @pre @post rv ( i.0 l i u < a a[i] = e) bool LinearSearch(int [] a, int l, int u, int e) { if (l < 0 u a ) return false; for @ (int i := l;i u;i := i+1){ if (a[i] = e) return true; } return false; } 10
Binary search con specifica di funzione @pre 0 l u < a sorted(a,l,u) @post rv ( i.l i u a[i] = e) bool BinarySearch(int [] a, int l, int u, int e) { if (l > u) return false; else { int m := (l+u) div 2; if (a[m] = e) return true; else if (a[m] < e) return BinarySearch(a, m+1, u, e); else return BinarySearch(a, l, m 1, e); } } La postcondizione è identica a quella di LinearSearch ma la precondizione dichiede anche che l array sia ordinato. 11
Loop invariant Ad ogni ciclo for e while viene associata un annotazione detta loop invariant. Un ciclo while while } @F ( condition ) { body Dice: eseguire body finché vale condition. L asserzione F deve valere all inizio di ogni iterazione. Viene valutata prima di condition e quindi deve valere anche sull iterazione finale, quando condition è false. 12
Loop invariant for } @F ( initialize ; condition ; increment ) { body può essere tradotto nel ciclo equivalente initialize ; while @F ( condition ) { body increment 13
} F deve valere dopo che initialize è stato valutato e, su ogni iterazione, prima che condition venga valutata. 14
Linear search con loop invariant @pre 0 l u < a @post rv ( i.l i u a[i] = e) bool LinearSearch(int [] a, int l, int u, int e) { for @L : l i ( j.l j < i a[j] e) (int i := l;i u;i := i+1){ if (a[i] = e) return true; } return false; } 15
Asserzioni Le annotazioni possono essere aggiunte in qualunque parte del programma. Quando un annotazione non è una precondizione di funzione, una postcondizione, o un loop invariant, la chiamiamo asserzione. Le asserzioni permettono ai programmatori di fornire un commento formale. Ad esempio, se all istruzione i := i+k; il programmatore pensa che k debba essere positivo, si può aggiungere un asserzione che afferma la supposizione: @ k > 0; i := i+k; 16
Correttezza parziale Useremo il metodo delle asserzioni induttive per dimostrare che un programma è parzialmente corretto. Il metodo riduce ogni funzione e le sue annotazioni ad un insieme finito di condizioni di verifica (VC) che sono formule della logica del I ordine. Se tutte le VC di una funzione sono valide, la funzione soddisfa la sua specifica. La riduzione avviene in due passi: prima, ogni funzione del programma annotato viene suddivisa in un insieme finito di cammini di base; poi ogni cammino di base genera una condizione di verifica (VC). 17
I loop e le funzioni ricorsive complicano le dimostrazioni di correttezza parziale perché creano un numero non limitato di cammini (da quando si entra in una funzione a quando si esce). Un cammino è una sequenza di istruzioni di programma. Per quanto riguarda i loop, gli invarianti di loop tagliano il cammino in un numero finito di cammini di base. Per le funzioni ricorsive invece, è la specifica di funzione a tagliare il cammino. Un cammino di base (semplice, elementare) è una sequenza di istruzioni che comincia dalla precondizione di funzione, o da un loop invariant e finisce in un loop invariant, in un asserzione, o nella postcondizione della funzione. Inoltre, un loop invariant può occorrere solo all inizio o alla fine di un cammino di base. Quindi, i cammini base non attraversano i loop. 18
Cammino 1: Cammini di base di LinearSearch annotato @pre 0 l u < a i := l; @L : l i ( j.l j < i a[j] e) Cammino 2: @L : l i ( j.l j < i a[j] e) assume i u; assume a[i] = e; rv := true; @post rv ( i.l i u a[i] = e) 19
Cammino 3: Cammini di base di LinearSearch annotato @L : l i ( j.l j < i a[j] e) assume i u; assume a[i] e; i := i+1; @L : l i ( j.l j < i a[j] e) Cammino 4: @L : l i ( j.l j < i a[j] e) assume i > u; rv := false; @post rv ( i.l i u a[i] = e) 20
Chiamate di funzione Come i cicli, anche le chiamate di funzione ricorsive creano un numero non limitato di cammini. Come gli invarianti di loop tagliano (cut) i cicli per produrre un numero finito di cammini base, le specifiche di funzione tagliano le chiamate di funzione. 21
Binary search con asserzioni di chiamata di funzione @pre 0 l u < a sorted(a,l,u) @post rv ( i.l i u a[i] = e) bool BinarySearch(int [] a, int l, int u, int e) { if (l > u) return false; else { int m := (l+u) div 2; if (a[m] = e) return true; else if (a[m] < e) { @ R 1 : 0 m+1 u < a sorted(a,m+1,u); return BinarySearch(a, m+1, u, e); } else { @ R 2 : 0 l m 1 < a sorted(a,l,m 1); return BinarySearch(a, l, m 1, e); } } } 22
Cammino 1: Cammini di base @pre 0 l u < a sorted(a,l,u) assume l > u; rv := false; @post rv ( i.l i u a[i] = e); Cammino 2: @pre 0 l u < a sorted(a,l,u) assume l u; m := (l+u) div 2; assume a[m] = e; rv := true; @post rv ( i.l i u a[i] = e); 23
Cammino 3: Cammini di base @pre 0 l u < a sorted(a,l,u) assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] < e; @R 1 : 0 m+1 u < a sorted(a,m+1,u); 24
Cammino 5: Cammini di base @pre 0 l u < a sorted(a,l,u) assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] e; @ R 2 : 0 l m 1 < a sorted(a,l,m 1); 25
Cammino 4: Cammini di base @pre 0 l u < a sorted(a,l,u) assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] < e; assume v 1 i.m+1 i u a[i] = e; rv := v 1 ; @post rv ( i.l i u a[i] = e); 26
Cammino 6: Cammini di base @pre 0 l u < a sorted(a,l,u) assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] e; assume v 2 i.l i m 1 a[i] = e; rv := v 2 ; @post rv ( i.l i u a[i] = e); 27
Le linee: Spiegazioni assume v 1 i.m+1 i u a[i] = e; rv := v 1 ; sono ottenute come segue: Si traduca lo statement: return BinarySearch(a, m+1, u, e); in un assegnamento a rv: rv := BinarySearch(a, m+1, u, e); Poi, poichè la precondizione vale (dal cammino 3), supponiamo che valga anche la postcondizione. Quindi descriviamo la chiamata di funzione con una relazione basata sulla postcondizione: 28
G[a,l,u,e,rv] : rv i.l i u a[i] = e. In questo caso specifico la relazione è G[a,m+1,u,e,v 1 ], dove v 1 è una variabile nuova che cattura il valore restituito. Nel cammino base si assuma tale relazione e si usi il valore di ritorno v 1 nell assegnamento: assume G[a,m+1,u,e,v 1 ]; rv := v 1 ; 29
Stati del programma Uno stato del programma è un assegnamento di valori alle variabili del programma. Le variabili del programma includono una variabile distinta pc, il contatore di programma. Esempio: s : {pc L 1,a [2,5,3,0],l 0,u 3,i 0} è uno stato di Linear Search. 30
Condizioni di verifica Il nostro obiettivo è quello di ridurre una funzione annotata ad un insieme finito di formule della logica del I ordine chiamate condizioni di verifica. Se condizioni di verifica sono tutte valide, il comportamento della funzione concorde con le sue annotazioni. I cammini di base vengono ridotti a condizioni di verifica utilizzando un meccanismo, detto weakest precondition, che incorpora gli effetti prodotti dalle istruzioni del programma in formule della logica del I ordine. 31
Condizioni di verifica La weakest precondition wp(f, S) è una funzione che mappa una formula del I ordine e un istruzione di un programma in una formula del I ordine, gode della proprietà che se dato uno stato s, si ha che s = wp(f,s), e se l istruzione S viene eseguita su s producendo lo stato s, allora s = F. La wp(f,s) è definita per i due tipi di istruzione presenti nei cammini di base (assunzione e assegnamento) come segue: Assunzione: cosa deve valere prima che l istruzione assume c venga eseguita per assicurare che F valga dopo? Se c F 32
vale prima, allora soddisfacendo c in assume c si garantisce che F valga dopo. wp(f,assume c) c F. Assegnamento: cosa deve valere prima che l istruzione v := e venga eseguita per assicurare che F[v] valga dopo? Se F[e] vale prima, allora assegnando e a v con v := e si garantisce che F[v] valga dopo.: wp(f[v],v := e) F[e]. Per una sequenza di istruzioni S 1 ;...;S n, definiamo: wp(f,s 1 ;...;S n ) wp(wp(f,s n ),S 1 ;...;S n 1 ) Quindi: perchè F possa valere dopo aver eseguito una sequenza di istruzioni S 1 ;...;S n, wp(f,s 1 ;...;S n ) deve valere prima di 33
eseguire le istruzioni. La condizione di verifica vc su un cammino base @ F S 1 ;.; S n ; @ G è F wp(g,s 1 ;...;S n ) La sua validità implica che quando F vale prima delle istruzioni del cammino eseguito, allora G vale dopo. Tradizionalmente questa condizione di verifica viene denotata dalla tripla di Hoare: {F}S 1 ;...;S n {G} 34
Esempio: cammino 2 BinarySearch @pre F : 0 l u < a sorted(a,l,u) S 1 : assume l u; S 2 : m := (l+u) div 2; S 3 : assume a[m] = e; S 4 : rv := true; @post G: rv ( i.l i u a[i] = e); La vc è F wp(g,s 1 ;S 2 ;S 3 ;S 4 ) 35
wp(g,s 1 ;S 2 ;S 3 ;S 4 ) Esempio: cammino 2 BinarySearch wp(wp(g,rv := true),s 1 ;S 2 ;S 3 ) wp(wp(g{rv true}, assume a[m] = e),s 1 ;S 2 ) wp(a[m] = e G{rv true},s 1 ;S 2 ) wp(wp(a[m] = e G{rv true},m := (l+u) div 2),S 1 ) wp((a[m] = e G{rv true}){m (l+u) div 2},S 1 ) wp((a[m] = e G{rv true}){m (l+u) div 2}, assume l u) l u (a[m] = e G{rv true}){m (l+u) div 2} 36
Esempio: cammino 2 BinarySearch Applicare le sostituzioni produce l u (a[(l+u) div 2] = e G{rv true,m (l+u) div 2}) Semplificando la vc di conseguenza: (0 l u < a sorted(a,l,u)) (l u a[(l+u) div 2] = e i.l i u a[i] = e) che è valida nella teoria dei numeri e degli array. 37
Correttezza totale La correttezza parziale rappresenta semplicemente un passo per dimostrare la correttezza totale di una funzione di un programma. La correttezza totale di una funzione asserisce che se l input soddisfa la precondizione della funzione, la funzione si fermerà restituendo un output che soddisfa la post condizione della funzione. Quindi, oltre a dimostrare la parziale correttezza della funzione dobbiamo far vedere che la funzione termina sempre per quegli input che soddisfano la precondizione. 38
Correttezza totale La dimostrazione di terminazione di una funzione si basa sulle relazioni ben fondate: si definisca un insieme S con una relazione ben fondata, si trovi una funzione δ che mappa gli stati del programma nell insieme S e che decresce rispetto alla relazione lungo ogni cammino di base. Poiché è ben fondata, non può esistere una sequenza infinita di stati del programma; altrimenti essi sarebbero mappati in una sequenza infinita e decrescente in S. La funzione δ viene detta funzione di ranking. 39
Esempio di correttezza totale: cammino BinarySearch @pre u l+1 0 @post u l+1 bool BinarySearch(int [] a, int l, int u, int e) { if (l > u) return false; else { int m := (l+u) div 2; if (a[m] = e) return true; else if (a[m] < e) return BinarySearch(a, m+1, u, e); else return BinarySearch(a, l, m 1, e); } } 40
Esempio di correttezza totale: cammino BinarySearch u l+1 mappa i parametri formali di BinarySearch nell insieme dei numeri naturali N con la relazione ben fondata <. L intervallo [l,u] si riduce ad ogni livello di ricorsione e quindi u l potrebbe essere una buona scelta come funzione di ranking. Tuttavia potrebbe accadere che l > u così che u l non venga mappato in N. Pertanto, poiché sarà al più l = u+1, è preferibile porre u l+1. 41
Esempio di correttezza totale: cammino BinarySearch Proprietà di u l+1: 1.Poiché, u l+1 è di tipo int, dobbiamo dimostrare che u l+1 viene di fatto mappato in N: tutte le volte che u l+1 viene valutato all entrata della funzione, deve accadere che u l+1 0. 2.Dobbiamo provare che u l+1 decresce ad ogni chiamata ricorsiva. 42
Esempio di correttezza totale: cammino BinarySearch È la precondizione della funzione stessa ad asserire la prima proprietà. Per dimostrare la seconda proprietà riduciamo l argomento ai cammini di base: attraverso ogni cammino di base u l+1 deve decrementare. Prendiamo in considerazione i cammini significativi. 43
Esempio di correttezza totale: cammino BinarySearch Cammino 1: @pre u l+1 0 u l+1 assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] < e; u (m+1)+1 44
Esempio di correttezza totale: cammino BinarySearch Cammino 2: @pre u l+1 0 u l+1 assume l u; m := (l+u) div 2; assume a[m] e; assume a[m] e; u (m+1)+1 Esistono altri cammini di base dall entrata nella funzione fino all uscita (return statement). Tuttavia, poiché entrambi portano alla fine della ricorsione, sono irrilevanti per l argomento di terminazione. 45
Esempio di correttezza totale: cammino BinarySearch I cammini di base che abbiamo mostrato inducono due condizioni di verifica: 1.u l+1 0 l u... u (((l+u) div 2)+1)+1 < u l+1, 2.u l+1 0 l u... (((l+u) div 2) 1) l+1 < u l+1, dove... elide i letterali che coinvolgono a[m] e che sono irrilevanti per l argomento di terminazione. 46