Alloy Dynamic Modeling - TUHH · Alloy Dynamic Modeling ... abstract sig Name extends Target {} sig...

34
Alloy Dynamic Modeling base on slides from Greg Dennis and Rob Seater Software Design Group, MIT

Transcript of Alloy Dynamic Modeling - TUHH · Alloy Dynamic Modeling ... abstract sig Name extends Target {} sig...

Alloy Dynamic Modeling

base on slides from

Greg Dennis and Rob SeaterSoftware Design Group, MIT

static vs. dynamic models

● static models

– describes states, not behaviors

– properties are invariants

– e.g. that a list is sorted

● dynamic models

– describe transitions between states

– properties are operations

– e.g. how a sorting algorithm works

model of an address bookmodule addressBook

abstract sig Target {}

sig Addr extends Target {}

abstract sig Name extends Target {}

sig Alias, Group extends Name {}

sig Book {

names: set Name,

addr: names -> some Target }

{

no n: Name | n in n.^(addr)

all a: Alias | lone a.addr

}

fun lookup [b: Book, n: Name]: set Addr {

n.^(b.addr) & Addr

}

assert lookupYields {

all b: Book, n: b.names | some lookup[b,n]

}

check lookupYields for 4 but 1 Book

what about operations?

● how is a name & address added to a book?

● no built-in model of execution

– no notion of time or mutable state

● need to model time/state explicitly

● Solution: can use a new “book” after each mutation:

pred add (b, b': Book, n: Name, t: Target) {

b'.addr = b.addr + n->t

}

address book: delete operation

● The delete should remove a name mapping

● Check whether delete undoes add

assert delUndoesAdd {

all b,b’,b“: Book, n: Name, t: Target |

add (b,b’,n,t) and del (b’,b”,n,t)

implies b.addr = b“.addr

}

check delUndoesAdd for 3 ???????

pred del (b, b’: Book, n: Name, t: Target) {

b’.addr = b.addr - n -> t

}

Counter Example

The book after the add is again Book_0

address book: delete operation - 2

● Check whether delete undoes add

assert delUndoesAdd {

all b,b’,b“: Book, n: Name, t: Target |

no n.(b.addr) and

add (b,b’,n,t) and del (b’,b”,n,t)

implies b.addr = b“.addr

}

check delUndoesAdd for 3 ????

pattern: abstract machine

● treat actions as operations on global state

● in addressBook, State is Book

– each Book represents a new system state

sig State {…}

pred init [s: State] {…}

pred inv [s: State] {…}

pred op1 [s, s’: State] {…}

pred opN [s, s’: State] {…}

pred inv [b: Book] {

let addr = b.addr | all n: Name {

n not in n.^addr

some addr.n => some n.addr

}

}

Possible invariant for Book

pattern: invariant preservation

● check that an operation preserves an invariant

➢ apply this pattern to the addressBook model

➢ do the add and delete ops preserve the invariant?

assert initEstablishes {

all s: State | init[s] => inv[s]

}

check initEstablishes

// for each operation

assert opPreserves {

all s, s': State |

inv[s] && op[s, s'] => inv[s']

}

check opPreserves

pattern: operation preconditions

● include precondition constraints in an operation

– operations no longer total

● the add operation with a precondition:

➢ check that add now preserves the invariant

➢ add a sensible precondition to the delete operation

– check that it now preserves the invariant

pred add[b, b': Book, n: Name, t: Target] {

// precondition

t in Name => (n !in t.*(b.addr) && some b.addr[t])

// postcondition b’.addr = b.addr + n->t }

what about traces?

● we can check properties of individual transitions

● what about properties of sequences of transitions?

● entire system simulation

– simulate the execution of a sequence of operations

● algorithm correctness

– check that all traces end in a desired final state

● planning problems

– find a trace that ends in a desired final state

pattern: traces

● model sequences of executions of abstract machine

● create linear (total) ordering over states

● connect successive states by operations

– constrains all states to be reachable

➢ apply traces pattern to the address book model

open util/ordering[State] as ord

fact traces {

init (ord/first())

all s: State - ord/last() |

let s' = ord/next(s) |

op1(s, s') or … or opN(s, s')

}

ordering module

● establishes linear ordering over atoms of signature S

open util/ordering[S]

s0 s1 s2 s3

prevprevprev

S = s0 + s1 + s2 + s3 + s4

first() = s0

last() = s4

next(s2) = s3

prev(s2) = s1

nexts(s2) = s3 + s4

prevs(s2) = s0 + s1

s4

prev

lt(s1, s2) = true

lt(s1, s1) = false

gt(s1, s2) = false

lte(s0, s3) = true

lte(s0, s0) = true

gte(s2, s4) = false

next next next next

Initialize

run show for 4

The last book is interesting: it creates two routes to the same address. Should that be possible???

NOWcheck the empty lookup by using traces.

Empty lookups

pred add (b, b': Book, n: Name, t: Target) {

b'.addr = b.addr + n->t

}

Counter example

Solving meaningless adds

Violation again because of deletion

Fix: Restrict deletion

checking safety properties

● can check safety property with one assertion

– because now all states are reachable

➢ check addressBook invariant with one assertion

– what's the difference between this safety check and checking that each operation preserves the invariant?

pred safe[s: State] {…}

assert allReachableSafe {

all s: State | safe[s]

}

non-modularity of abstract machine

● static traffic light model

● dynamic traffic light model with abstract machine

– all dynamic components collected in one sig

sig Color {}

sig Light {

color: Color

}

sig Color {}

sig Light {}

sig State {

color: Light -> one Color

}

pattern: local state

● embed state in individual objects

– variant of abstract machine

● move state/time signature out of first column

– typically most convenient in last column

sig Time {}

sig Color {}

sig Light {

color: Color one -> Time

}

sig Color {}

sig Light {}

sig State {

color: Light -> one Color

}

global state local state

example: leader election in a ring

● many distributed protocols require “leader” process

– leader coordinates the other processes

– leader “elected” by processes, not assigned in advance

● leader is the process with the largest identifier

– each process has unique identifier

● leader election in a ring

– processes pass identifiers around ring

– if identifier less than own, drops it

– if identifier greater, passes it on

– if identifier equal, elects itself leader

22

3

71

5

4

28

26

leader election: topology

● beginning of model using local state abstract machine:

– processes are ordered instead of given ids

➢ can be downloaded from the alloy web site

➢ constrain the successor relation to form a ring

open util/ordering[Time] as to

open util/ordering[Process] as po

sig Time {}

sig Process {

succ: Process,

toSend: Process -> Time,

elected: set Time

}

leader election: notes

● topology of the ring is static

– succ field has no Time column

fact ring {

all p: Process | Process in p.^succ

}

leader election: notes

● no constraint that there be one elected process

– that's a property we'd like to check

● set of elected processes is a definition

– “elected” at one time instance then no longer

fact defineElected {

no elected.(to/first)

all t: Time – to/first |

elected.t = {p:Process |

p in (p.toSend.t – p.toSend.(t.prev))}

}

leader election: operations

➢ write initialization condition init[t: Time]

– every process has exactly itself to send

pred init [t: Time] {

all p: Process | p.toSend.t = p

}

leader election: operations

➢ write no-op operation skip[t, t': Time, p: Process]

– process p send no ids during that time step

pred skip [t, t': Time, p: Process] {

p.toSend.t = p.toSend.t'

}

leader election: operations

➢ write send operation step[t, t': Time, p: Process]

– process p sends one id to successor

– successor keeps it or drops it

pred step [t, t': Time, p: Process] {

let from = p.toSend, to = p.succ.toSend |

some id: from.t {

from.t' = from.t - id

to.t' = to.t + (id - p.succ.prevs)

}

}

leader election: traces

● use the following traces constraint

● why does traces fact need step(t, t', succ.p)?

● what's the disadvantage to writing this instead?

fact traces {

init[to/first]

all t: Time – to/last | let t' = t.next |

all p: Process | step[t, t', p] ||

step[t, t', succ.p] || skip[t, t', p]

}

some p: Process | step[t, t', p] &&

all p': Process – (p + p.succ) | skip[t, t', p]

Leader election: analysis

● At most one should be elected

● What about

assert AtMostOneElected {

lone elected.Time

}

check AtMostOneElected for 3

Process but 7 Time

all t: Time | lone elected.t

leader election: analysis

➢ check that at most one process is ever elected

assert atLeastOneElected {

progress => some elected.Time

}

pred progress {

all t: Time - to/last | let t' = t.next |

some Process.toSend.t =>

some p: Process | not skip[t, t', p]

}

assert AtLeastOneElected {

some t: Time | some elected.t

}

check AtLeastOneElected for 3 but 7 Time

machine diameter

● what trace length is long enough to catch all bugs?

– does “at most one elected” fail in a longer trace?

● machine diameter = max steps from initial state

– longest loopless path is an upper bound

● run this predicate for longer traces until no solution

➢ for three processes, what trace lengthis sufficient to explore all possible states?

13

pred looplessPath {

no disj t, t': Time | toSend.t = toSend.t'

}

run looplessPath for 3 Process, ? Time

Summary

● Just modeled the domain and the correct states (predicates, facts, assertions).

● Just described what are legal traces (with time).

● No code was written.

● Used only logic to describe valid states.

But:

● What about generating code???