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. 1
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 2
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. 3
Annotazioni di programma Un annotazione è una formula della logica del I ordine le cui variabili libere includono soltanto le 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 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. 4
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. 5
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) { for @ (int i := l;i u;i := i+1){ if (a[i] = e) return true; return false; 6
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. 7
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. 8
Loop invariant for @F ( initialize ; condition ; increment ) { body può essere tradotto nel ciclo equivalente initialize ; while @F ( condition ) { body increment 9
F deve valere dopo che initialize è stato valutato e, su ogni iterazione, prima che condition venga valutata. 10
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; 11
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; 12
Correttezza parziale Useremo il metodo delle asserzioni induttive. Questa volta i cammini sono sequenze di istruzioni di programma. 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 di funzione. Inoltre, un loop invariant può occorrere solo all inizio o alla fine di un cammino di base. Tratteremo anche il caso delle funzioni ricorsive. 13
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) 14
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) 15
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. 16
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); 17
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); 18
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); 19
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); 20
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); 21
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); 22
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: 23
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 ; 24
Condizioni di verifica Il nostro obiettivo è quello di ridurre una formula annotata ad un insieme finito di formule della logica del I ordine chiamate condizioni di verifica. La riduzione da cammini di base a vc viene eseguita attraverso la costruzione della wlp. La wlp(f,s) è definita così: Assunzione: cosa deve valere prima che l istruzione assume c venga eseguita per assicurare che F valga dopo? Se c F vale prima, allora soddisfacendo c in assume c si garantisce che F valga dopo. wlp(f,assume c) c F. Assegnamento: cosa deve valere prima che l istruzione v := e 25
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.: wlp(f[v],v := e) F[e]. per una sequenza di istruzioni S 1 ;...;S n, definiamo: wlp(f,s 1 ;...;S n ) wlp(wlp(f,s n ),S 1 ;...;S n 1 ) Quindi: perchè F possa valere dopo aver eseguito una sequenza di istruzioni S 1 ;...;S n, wlp(f,s 1 ;...;S n ) deve valere prima di eseguire le istruzioni La condizione di verifica vc su un cammino base @ F S 1 ;.; S n ; @ G 26
è F wlp(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: {FS 1 ;...;S n {G 27
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 wlp(g,s 1 ;S 2 ;S 3 ;S 4 ) 28
wlp(g,s 1 ;S 2 ;S 3 ;S 4 ) Esempio: cammino 2 BinarySearch wlp(wlp(g,rv := true),s 1 ;S 2 ;S 3 ) wlp(wlp(g{rv true, assume a[m] = e),s 1 ;S 2 ) wlp(a[m] = e G{rv true,s 1 ;S 2 ) wlp(wlp(a[m] = e G{rv true,m := (l+u) div 2),S 1 ) wlp((a[m] = e G{rv true){m (l+u) div 2,S 1 ) wlp((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 29
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. 30
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); 31
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 <. Questa scelta è intuitivamente corretta perché l intervallo [l, u] si riduce ad ogni livello di ricorsione. Tuttavia potrebbe accadere che l > u così che u l+1 non viene mappato in N. proprietà di u l+1: 1.Poiché, u l+1 ha tipo int, dobbiamo dimostrare che u l+1 viene di fatto mappato in N 2.Dobbiamo provare che u l+1 decresce ad ogni chiamata ricorsiva 32
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. 33
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 34
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. 35
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. 36