Verification of
object-oriented programs
with invariants
by M. Barnett, R. DeLine, M. Fähndrich, K.R.M. Leino, W. Schulte
Bordignon Claudio
Zampieron Elisa
Introduzione
Scopo: fornire una metodologia di
programmazione che sfrutta gli invarianti
La metodologia
utilizza il design-by-contract
determina in quale stato un oggetto può essere
modificato;
permette l’object-composition e il subclassing;
estesa per permette il multithreading;
Introduzione: Design-By-Contract
Il Design By Contract (DBC) è una metodologia inventato da Betrand
Meyer e integrata in Eiffel
Consiste nel considerare un'interfaccia (insieme dei metodi esposti da una
classe) come un contratto fra chi la invoca (client) e chi la implementa
(classe)
Vengono quindi definiti i termini del contratto utilizzando 4 elementi:
Pre-condizioni: i requisiti minimi che devono essere soddisfatti dal
cliente affinché il metodo possa andare a buon fine
Post-condizioni: i requisiti per cui l’esecuzione del metodo si può
ritenere corretta e che il metodo si impegna quindi a rispettare
Clausule di modifica: una lista dei campi della classe che vengono
modificati dall’implementazione del metodo
Invarianti: condizioni che devono essere sempre valide.
I termini del contratto
Le pre e post-condizioni riguardano quindi un metodo:
le pre-condizioni vincolano il chiamante
le post-condizioni vincolano il metodo
Gli invarianti sono invece un vincolo a livello dell’intera classe:
devono valere
prima che una qualunque invocazione di metodo avvenga
e alla fine dell’esecuzione del metodo stesso
possono non essere validi temporaneamente durante
l’esecuzione di un metodo
Esempio
class SquareRootable feature
value: Real;
sqrt is
require
value
do
value
ensure
value
value
end;
>= 0;
:= ....;
>= 0;
<= old value;
invariant
value >= 0;
end ;
Pre-condizione: un'espressione boolean
valutata prima del codice di un metodo;
è introdotta a mezzo della parola chiave
require e può comporsi di una o più
espressioni boolean separate da ‘;’
Post-condizione: un'espressione boolean
valutata al termine di un metodo;
è introdotta a mezzo della parola chiave
ensure
Invariante di classe: è una condizione che,
ogni volta che viene valutata, deve valere
true;
è introdotta a mezzo della parola chiave
invariant
Invarianti…
Spesso vengono usati come sostitutivi di pre e post-condizioni:
class C{
private int x, y;
invariant 0 x < y;
public C(){
x = 0; y = 1;
}
public void M()
modifies x, y {
assert y – x 0;
L’invariante è soddisfatto prima dell’incremento di x
Stato dell’oggetto CONSISTENTE
L’invariante potrebbe non valere
Stato dell’oggetto INCONSISTENTE
x = x + 3;
y = 4 * y;
}
L’invariante è ristabilito dopo l’assegnamento di y
Torniamo ad uno stato CONSISTENTE
}
Quindi non sono necessarie pre e post-condizioni ?
E se il metodo invoca P()?
class C{
private int x, y;
invariant 0 x < y;
public C(){
x = 0; y = 1;
}
public void M()
modifies x, y {
assert y – x 0;
x = x + 3;
P();
L’invariante deve essere soddisfatta
all’uscita del costrutture
Si assume che l’invariante sia soddisfatta
all’entrata del metodo
Qui l’invariante potrebbe non valere
temporaneamente
Cosa succede se P() esegue
una chiamata ricorsiva?
y = 4 * y;
}
public P() {
/*…*/;
M();
}
}
L’invariante è qui ripristinata
L’invariante deve essere valida
all’uscita dal metodo
Specifichiamo i dettagli del
contratto
class C{
private int x, y;
invariant 0 x < y;
Il metodo P() invoca M(), quindi
deve garantire le precondizioni.
public C()
ensures 0 x < y; {
x = 0; y = 1;
}
public M()
requires 0 x < y;
modifies x, y
ensures 0 x < y {
assert y – x 0;
x = x + 3;
P();
Y = 4 * y;
}
public P() {
/*…*/
M();
}
}
Esprimere l’invariante
come pre e post condizione
viola l’information-hiding
La metodologia quindi dovrà:
specificare SE L’INVARIANTE E’
SODDISFATTO
RISPETTARE L’INFORMATION
HIDING
Validità dell’invariante 1/2
Introduciamo un campo speciale st:{Invalid, Valid}
compare solo nelle pre e post-condizioni;
non può essere modificato nel corpo dei metodi ma
solo dai comandi pack e unpack;
Definiamo il predicato InvT(o) l’oggetto o di tipo
T soddisfa gli invarianti della classe T
Vogliamo che, in ogni punto del programma, valga:
( o : T | o.st = Invalid InvT(o))
Validità dell’invariante 2/2
o.st = Invalid l’oggetto o è invalido
l’invariante potrebbe non essere soddisfatto
i campi dell’oggetto possono essere modificati
un oggetto viene allocato inizialmente nello stato invalid
o.st = Valid l’oggetto o è valido
l’invariante è soddisfatto
l’oggetto non è modificabile
pack o
assert o null
assert InvT(o);
o.st := Valid;
assert o.st = Invalid;
unpack o
assert o null assert o.st = Valid;
o.st := Invalid;
Esempio
class C{
private int x, y;
invariant 0 x < y;
public C()
ensures st = Valid; {
x = 0; y = 1;
pack this;
}
public M()
requires st = Valid;
modifies x, y;
{
assert y – x 0;
unpack this;
x = x + 3;
Y = 4 * y;
pack this;
}
}
All’uscita dal costruttore l’oggetto deve
essere valido, questo è garantito dal
comando pack
La pre-condizione indica che
l’oggetto deve essere valido a
tempo di invocazione.
Il comando pack mi garantisce la
validità in uscita
Estensione object-composition
Problema
l’invariante potrebbe dipendere dai componenti
quindi l’aggiornamento dei campi può portare a
invalidità
information hiding
Introduciamo un modificatore rep
Nuovo range {Valid, Invalid, Committed} per st
Ridefiniamo le operazioni pack e unpack
Istanziamo nuovi oggetti con stato invalid
Object-composition: Pack / Unpack
pack
controlla l’invariante e imposta il campo st dei componenti allo
stato committed e varia o.st allo stato di valid
pack o assert o null assert o.st = Invalid;
assert InvT(o);
foreach p є CompT(o) { assert(p = null p.st = Valid)};
foreach p є CompT(o) { if(p
o.st := Valid;
null) p.st := Committed};
unpack
modifica il campo o.st dell’oggetto e delle sue componenti
rispettivamente allo stato invalid e valid
unpack o
assert o null assert o.st = Valid;
o.st := Invalid;
foreach p є CompT(o) { if(p
null) {p.st := Valid}};
Object-composition
Se T ha due campi rep, per esempio, x e y e o è di tipo T, allora:
pack o
assert o null
assert InvT(o);
assert (o.x = null
if(o.x
o.st = Invalid;
o.x.st = Valid)
(o.y = null
o.y.st = Valid);
null) { o.x.st := Committed };
if(o.y
null) { o.y.st := Committed };
o.st := Valid;
Vogliamo che, in ogni punto del programma, valga:
( o : T |
o.st = Invalid InvT(o)
( p є CompT(o) p = null p.st = Committed))
Esempio 1/3
class E {
method m2(s)
requires s.st=Valid …
class C {
Committed
method m0(k)
requires k.st=Valid …
{
Committed
D
unpack k ;
Committed
Committed
k.d.m1() ;
k.s.m2() ;
pack k;
E
s
d
Committed
}
k
C
Valid
Esempio 2/3
class E {
method m2(s)
requires s.st=Valid …
class C {
method m0(k)
requires k.st=Valid …
{
unpack k ;
k.d.m1() ;
k.s.m2() ;
pack k;
s
d
}
k
C
Invalid
Committed
Committed
Committed
D
Valid
E
Valid
Esempio 3/3
class E {
method m2(s)
requires s.st=Valid …
class C {
Committed
method m0(k)
requires k.st=Valid …
{
Committed
D
unpack k ;
Committed
Committed
k.d.m1() ;
k.s.m2() ;
pack k;
E
s
d
Committed
}
k
C
Valid
Estensione per Subclasses 1/2
I campi sono ereditati dai vari class frames
Ciascun frame contiene nuovi campi
L’oggetto può essere valid/invalid per ciascun class frame
Dato un oggetto o:W
W
o puo’ essere invalido per K e W
K
T
L’oggetto o puo’ essere valido per T
Y
U
Object
Se o è valido per T non puo’ essere
invalido per Y!!!
Estensione per Subclasses 2/2
Ridefiniamo st con due campi speciali
inv (l’ultima classe per la quale sono soddisfatti gli invarianti)
committed (boolean: indica se l’oggetto è committed;
true se inv = tipo dinamico dell’oggetto)
Ridefiniamo i comandi pack e unpack:
pack o as T
assert o null o.inv = Superclasse(T);
assert InvT(o);
foreach p є CompT(o){assert(p = null p.inv = type(p) p.committed)}
foreach p є CompT(o) { if(p != null) p.committed := true};
o.inv := T;
unpack o from T
assert o null o.inv = T o.committed;
o.inv := Superclasse(T);
foreach p є CompT(o) { if(p != null) p.committed := false};
Esempio Class Frames
Il tipo dinamico C ha 4 class frames
Aggiorniamo il campo c.y:
pack o as C
controlla l’invariante
per ciascun componente p,
setta p.committed = true
setta o.inv = C
unpack o from C
setta o.inv = B
per ciascun componente p,
setta p.commited = false
class C extends B
y:Y
invariant u, v, x, y
class B extends A
x:X
invariant u, v, x
class A extends object
u:U, v:V
invariant u, v
object
/* inv == C */
unpack c from B
/* inv == B */
c.y = 5;
pack this as C
/* inv == C */
Correttezza
La correttezza della metodologia è dimostrata dalle seguenti
condizioni che valgono in tutto il programma
1.
2.
3.
4.
( o,T • o.committed o.inv = type(o) )
( o,T • o.inv <: T InvT (o) )
( o,T • o.inv <: T ) ( p CompT (o) • p = null p.committed ))
( o,T, o’,T’, q • ( p CompT (o), p’ CompT’(o’) • o.inv <: T o’.inv <:
T’ q.committed p = p’ = q o = o’ T = T’ ))
Dimostriamo che questi 4 condizioni sono mantenute da ogni
comandi che possono modificare i valori dei campi
Costruttore
Comando pack
Comando unpack
Aggiornamento dei campi
Correttezza del costruttore
(3) ( o,T • o.inv <: T ) ( p CompT (o) • p = null p.committed ))
(4) ( o,T, o’,T’, q(1)
• ((
p o,T
Comp
Comp
• o.inv) <: T o’.inv <: T’
(2)
(
o,T
••o.committed
o.inv
<: p’
T
Inv
o.inv
=) type(o)
T (o),
T’(o’)
T (o)
q.committed p = p’ = q o = o’ T = T’ ))
La (1) è soddisfatta perché per definizione il costruttore setta il campo
committed a false per ogni nuovo oggetto allocato
Il costruttore setta o.inv a object e questo dimostra la (2)
La (3) e la (4) sono verificate in quanto il tipo object non ha rep-fields
Correttezza del comando pack
pack o as T
assert o null o.inv = Superclasse(T);
assert InvT(o);
foreach p є CompT(o){assert(p = null p.inv = type(p) p.committed)}
foreach p є CompT(o) { if(p != null) p.committed := true};
o.inv := T;
(4) ( o,T, o’,T’, q • ( p CompT (o), p’ CompT’(o’) • o.inv <: T o’.inv <: T’ q.committed
(3) ( o,T • o.inv <:
T(
(
po.inv
Comp
(o)
• p (o)
==
null
p.committed
))
(2)
()
o,T
••o.committed
<: S T
Inv
o.inv
) type(o)
)
S
p = p’ = q o = o’ T(1)
= T’
))o,T
Ogni componente (in cui modifichiamo lo stato) è valida quindi viene
soddisfatta la (1)
Sia T<:S. l’asserzione precedente alla verifica degli invarianti garantisce
che portando o.inv da T a S venga soddisfatta la (2)
Porre allo stato committed solo gli oggetti non nulli soddisfa la (3)
Possiamo dimostrare che pack rispetta la (4) analizzando i due casi
q.committed è false la premessa è falsa => implicazione vera
se la premessa è verificata, per come è definito il comando pack o e
o’ sono lo stesso oggetto
Correttezza del comando unpack
unpack o from T
assert o null o.inv = T o.committed;
o.inv := Superclasse(T);
foreach p є CompT(o) { if(p != null) p.committed := false};
(4) (3)
((o,T,
o’,T’,
q(1)
• <:
((
p o,T
p’
T
Comp
(o’)
• o.inv
o,T
• o.inv
(2)
(T
)
o,T
Comp
•(•o.committed
o.inv
pT(o),
<:
Comp
T
(o)
Inv
o.inv
•T p(o)
=)null
type(o)
p.committed
) <: T o’.inv
)) <: T’
T’=
q.committed p = p’ = q o = o’ T = T’ ))
La (1) è soddisfatta perché il comando cambia solo il valore di oggetti
non committed
La (2) è soddisfatta perché il comando indebolisce il predicato
La (3) è soddisfatta perché il foreach modifica solo le componenti di p e
l’assegnamento a o.inv antecedente il ciclo falsifica le premesse del
predicato => l’implicazione è vera
La (4) è soddisfatta perché il comando indebolisce il predicato
Correttezza nell’aggiornamento
dei campi
(2) ( o,T • o.inv <: T InvT (o) )
(1) ( o,T • o.committed o.inv = type(o) )
(3) ( o,T • o.inv <: T ) ( p CompT (o) • p = null p.committed ))
(4) ( o,T, o’,T’, q • ( p CompT (o), p’ CompT’(o’) • o.inv <: T o’.inv <: T’
q.committed p = p’ = q o = o’ T = T’ ))
I comandi di aggiornamento non possono modificare i valori di inv e
committed, questo mantiene la (1)
Le precondizioni per l’aggiornamento dei campi (dato x:T implica che
(x.inv <: T) ) fanno fallire le premesse di (2) (3) e (4), per la semantica
dell’implicazione quindi queste sono verificate
Estensione per il Multithreaded
Lo scopo è sviluppare una tecnica per la
verifica della consistenza degli oggetti su un
programma OO multithreaded.
Formalizziamo lo stato consistente di un
oggetto attraverso l’uso degli invarianti.
In un linguaggio Multithreaded l’interleaving
può causare la violazione degli invarianti e
quindi inconsistenza.
Esempio
class Account {
int balance;
}
L’interleavings tra due tread
concorrenti possono portare
violazioni dell’invariante
Account act = …;
int b0 = act.balance;
act.balance += 50;
int b1 = act.balance;
Non è garantito che
b1 == b0 + 50
Introduce
Mutua esclusione
Aggregate objects
Object invariants
Mutua esclusione
In ciascun oggetto, si introduce un campo owner che può essere
Null se l’oggetto o non è componente di nessun altro oggetto
Object se l’oggetto o è una componente
Thread se l’oggetto o è posseduto da un thread
Prima che il thread t acceda al campo o.f, si controlla che o.owner == t
Quando il thread t crea un oggetto o, setta o.owner = t;
Il campo owner può essere modificato solo dai comandi:
acquire o;
attende finchè o.owner == null e poi setta o.owner = t;
release o;
controlla che o.owner == t e poi setta o.owner = null;
Esempio
class Account { int balance; }
Account act = new Account();
act.balance = 5;
release act;
…
acquire act;
act.balance++;
release act;
Aggregate objects
Gli aggregate objects vengono trattati come se fossero una unica
entità
Manteniamo modificatore rep
Un thread può modificare un aggregate object tramite comando
unpack
Il comando pack trasferisce le proprietà ritornandole
all’aggregate object
Introduciamo in ciascun oggetto un campo boolean inv che indica
quando l’oggetto è stato modificato
il campo inv è settato dal comando pack e ripristinato dal
comando unpack
finchè !inv, i modificatori rep sono vuoti
Aggregate objects
[[ o = new
o = new
o.owner
o.inv =
C; ]]
C;
= t;
false;
[[ x = o.f; ]]
assert o.owner == t;
x = o.f;
[[ o.f = x; ]]
assert o.owner == t;
assert !o.inv;
o.f = x;
[[ acquire o; ]]
lock (o) {
while (o.owner != null)
Monitor.Wait(o);
o.owner = t;
}
[[ release o; ]]
assert o.owner == t;
assert o.inv;
lock (o) {
o.owner = null;
Monitor.Pulse(o); //(*)
}
Aggregate objects
[[ pack o; ]]
assert o.owner == t;
assert !o.inv;
for each rep field o.f:
if (o.f != null) {
assert o.f.owner == t;
assert o.f.inv;
}
for each rep field o.f:
if (o.f != null)
o.f.owner = o;
o.inv = true;
[[ unpack o; ]]
assert o.owner == t;
assert o.inv;
for each rep field o.f:
if (o.f != null)
o.f.owner = t;
o.inv = false;
Ciclo di vita di un oggetto
il thread t crea
l’oggetto o
modificato dal
thread t
o.Owned = “null”
o è free e
o.Owned = “thread”
o è mutable
consistente
thread t esegue
pack o
thread t esegue
unpack o
o.Owned = “object”
o è consistente e
thread t esegue pack
object p e p.f = o e f è
un campo rep
“sigillato”
thread t esegue
unpack owner p
acquire dal
tread t
o.Owned = “thread”
o è consistente
release dal
thread t
Esempio 1/7
Thread 1
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 2/7
Thread 1
a
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 3/7
Thread 1
a
r
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 4/7
Thread 1
a
r
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 5/7
Thread 1
a
r
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 6/7
Thread 1
a
r
class Agg { rep Rep f; }
class Rep {}
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Esempio 7/7
Thread 1
class Agg { rep Rep f; }
class Rep {}
a
r
Agg a = new Agg;
Rep r = new Rep;
pack r;
a.f = r;
pack a;
release a;
Object Invariants
Ciascuna classe può dichiarare un invariante
L’invariante può menzionare solo campi this.f1.f2.….fn.g
dove f1, f2, …, fn sono campi rep
Il comando pack controlla l’invariante:
di conseguenza, l’invariante vale quando inv è true
Il comando release controlla inv
di conseguenza, aggregates liberi soddisfano i loro
invarianti
Related Work
idiomi valid in ESC/Modula-3
dichiarazione di invariant in ESC/Java e JML
Spec# assicura che l’accesso dei campi avviene
tramite operazioni thread-local
supporta aggregate objects
gli invarianti sono rispettati
applicazione completamente dinamica
utilizza operazioni acquire e release
Conclusioni
Invarianti differenti dalle pre/post-conditions
Program invariants mantenuti in ogni punto di
programma
Uso di comandi pack / unpack
I campi possono essere liberamente letti
(vincoli solo per aggiornamento di campi)
Estensione per il multhitreaded
Supporto per aggregate objects
Correttezza
Riferimenti
Wolfram Schulte
Mike Barnett
Robert DeLine
Manuel Fähndrich
K. Rustan M. Leino
Verification of object-oriented programs with invariants
Mike Barnett, Robert DeLine, Manuel Fähndrich, K. Rustan M. Leino,
Wolfram Schulte
Microsoft Research, Redmond, WA, USA
Journal of Object Technology (http://www.jot.fm)
Verification of multithreaded object-oriented programs with
invariants
Bart Jacobs (Dept. of Computer Science Katholieke Universiteit Leuven),
K. Rustan M. Leino, Wolfram Schulte (Microsoft Research, Redmond, WA,
USA)
Modular verification of global module invariants in OO programs
K. Rustan M. Leino and Peter Müller
Technical Report 459, ETH Zürich, 2004