E : Expression a : Action K : ProcessName E ::= a . E | E1 + E2 | E1 | E2 | E \ a | 0 | K a is a port name, n, or its complement, ~n, or the "silent action", τ Note that 0 is the terminal ("dead") process.A CCS process is a CCS expression named by an equation. Examples are:
CS == ~work. ~coin. coffee. CS CTM == coin. (~coffee. CTM) + (~tea. CTM) Ex1 == (CS | CTM)\coin\coffee\tea Countn == coin. ~coffee. Countn-1 Count0 == coin. 0 for some int, n >= 0. Ex2 == CS | Count9
The nodes of a tree are the "states" or "configurations". Each node can be considered a CCS process, that is, a process "evolves into another process" when it makes a computation step.
The transitions (arcs) in a tree are generated by the CCS computation rules, which are defined in Aceto and Larsen's lecture notes, Page 21.
A CCS process is a specification of a coded component.
Or, it is a description of the component's communication behavior.
Or, it is a protocol of a desired good behavior.
It can be used to describe all of these.
We might define this desired behavior of an assembled
system of processes:
Productive == ~work. Productive
and we would like to prove that the assembled system, Ex1, has behavior
that "equals" Productive. In the literature, there are many different
definitions of "equals/equivalent" behavior. Here, we use weak bisimulation
Example: the inductive interpretation of N = z.0 + s.N is a tree whose paths are all finite: { si.0 | i >= 0} --- every path ends with 0. But we want a tree that includes the nonterminating path s.s.s.s. ...(forever) --- no 0 at the end.
The point is --- the transition diagram is not the semantics of a process --- it is the coinductive interpretation of the diagram that is the semantics.
A tree is formally defined as a set of paths through a set of nodes.
For example, the tree,
n4
b/ c\
n2 n3 is a depiction of this two element set: {(n4) b (n2) a (n1); (n4) c (n3)}
a|
n1
Here is the inductive interpretation of N, which builds the tree
``from the leaves upwards to the root'', starting with the empty tree
(no paths) and building a height-1 tree, then height-2 tree, etc.,
in stages:
N0 = {}
Ni+1 = {() z (1) 0} union { () s [2]t | t in Ni}
where [2]t prefixes a 2 in front of each node name in tree t
The nodes are uniquely labelled.
The completed tree is unioni >= 0 Ni.
We have
N0 = {}
N1 = { () z (1) 0 }
N2 = { () z (1) 0; () s (2) z (21) 0 }
N3 = { () z (1) 0; () s (2) z (21) 0; () s (2) s (22) z (221) 0 }
and so on
The union of the Ni sets is this tree:
()-s->(2)-s->(22)-s->(222)-s-> ...-s-> (2i)-s-> ...
|z |z |z |z ... |z
(1) (21) (22) (2221) (2i1)
0 0 0 0 ... 0
But there is no infinite path of form s s s ... --- there is no
legal nonterminating behavior.
The coinductive interpretation computes the tree ''from the root downwards'',
starting with an initial tree of all possible paths and trimming it down
to the solution in stages: the depth-1 tree behaves correctly for one move,
the depth-2 tree behaves correctly for two moves, etc.
The solution tree behaves correctly for all countable depth of moves:
C0 = the set of ALL PATHS with ALL POSSIBLE NODES and ARCS (''the chaos tree'')
Ci+1 = {() z (1) 0} union { () s [2]p | p in Ci}
where [2]p prefixes a 2 in front of each node name in path p
Note that Ci+1 is the same definition as Ni+1.
The solution is intersectioni >= 0 Ci.
For the example:
C0 = CHAOStree
C1 = {() z (1) 0; () s [2]CHAOStree }
C2 = { () z (1) 0; () s (2) z (21) 0 ; () s [2]CHAOStree }
and so on
The intersection of these sets (''trees'') is this tree:
()-s->(2)-s->(22)-s->(222)-s-> ...-s-> (2i)-s-> ... goes forever!
|z |z |z |z ... |z
(1) (21) (22) (2221) (2i1)
0 0 0 0 ... 0
There is one additional path:
() s (2) s (22) s (222) ... s (2k) s ... which is infinite.
This means there is one legal nonterminating behavior.
When c, c' are configurations and b is an action (possibly τ), we write c -->b c' to assert that c makes a b step to become c'.
Example: CS -->~work (~coin.coffee.CS).
Another example: (CS | work. 0) -->τ (~coin.coffee.CS | 0 ).
When action a is not τ, we write c ==>^a c' to assert that c makes zero or more τ steps, then the one a step, then zero or more τ steps, to become c'.
Example: Ex1 ==>^~work Ex1.
Also (this is the "special case" mentioned above), c ==>^τ c' means that c takes zero or more τ steps to become c'.
Example: ((~coin.coffee.CS) | CTM) ==>^τ (CS | CTM)
Other examples: CS ==>^τ CS and 0 ==>^τ 0.
Say we have two CCS-coded processes, C and D, and let CC be the configurations (states) that C might generate and let DD be the configurations (states) that D might generate. To show that C and D behave equivalently, we must match-up states from CC to those from DD like one does when one builds a tracking proof of compiler correctness.
Let R ⊆ CC × DD be a relation that matches-up states. When (c,d): R, we write it as c R d.
Definition: R is a weak bisimulation if and only if, for every c: CC and d: DD, if c R d, then for each action, b (which might be τ),
If only Clause (1) above holds, but not Clause (2), we say that R is a weak (one-many) simulation. End Definition
The intuition about weak bisimulation R goes like this: c R d means that c and d have the same communication behaviors (and lead to states that have the same communication behaviors...).
If R ⊆ CC × DD is a weak bisimulation and if C R D, that is, the starting configurations are related, then we say that C and D are weakly bisimular --- each one can imitate all the (external) steps that the other makes.
If R ⊆ CC × DD is a weak simulation, and if C R D, that is, the starting configurations are related, then we say that D weakly simulates C --- D can imitate all the (external) steps that C makes.
A weak bisimulation is an equivalence relation (reflexive, commutative, transitive).
Examples:
When you construct the proof that relation R ⊆ CC × DD is a weak bisimulation, you are constructing the coinduction step of a correctness proof-by-coinduction. Proof-by-coinduction must be used to prove properties of structures defined by coinduction, just like induction is used to prove properties of structures defined by induction.
The intuition behind coinduction is that it is "proof on the depth of the coinductively defined structure". This is the dual of inductive proof, which is proof on the height of an inductively defined structure. The mathematics of coinduction is simple and beautiful, but we cannot study it here.
When R and P are weakly bisimular, it means that R has exactly all of P's behaviors --- nothing more, nothing less. P is a ``semantics'' or ``protocol'' of R. Or, R and P are components that are interchangeable in a larger system.
There are important properties that cannot be stated and proved with
mere simulation. For example, for
CS == ~work. coin. coffee. CS
we want to prove that CS has the property "coffee is consumed infinitely
often." But we cannot prove that CS simulates C == coffee. C.
We require a property language that lets us state something like this:
C == "finite sequence of some steps". coffee. C
Robin Milner and Matthew Hennessy developed a model-logic notation, called Hennessy-Milner Logic (HML) to state logical properties of CCS processes. Then Kim Larsen added two forms of recursion to HML. If you are familiar with LTL or CTL, then you will see that Recursive HML is a simple variation.
P ::= true | false | P1 ^ P2 | P1 v P2 | <a>P | [a]PLet C be a CCS process (configuration). Write C |= P when property P is true for C. Here is the Tarski-style semantics of propositions:
=================================================== C |= true always It is never the case that C |= false C |= P1 ^ P2 iff C |= P1 and also C |= P2 C |= P1 v P2 iff C |= P1 or C |= P2 C |= <a>P iff there is some step, C ->a C' and C' |= P C |= [a]P iff for all steps from C, C ->a C', we have C' |= P ===================================================Read C |= <a>P as "there exists an a step that C can make and then P holds true", and read C |= [a]P as "For all the a steps that C can make, then P holds true". (Note that C |= [a]P holds true in the case that C cannot make an a step.)
Examples appear in a moment. Here are two useful abbreviations:
-true == false -false == true -(P1 ^ P2) == -P1 v -P2 -(P1 v P2) == -P1 ^ -P2 -<a>P == [a]-P -[a]P == <a>-PThat's why it's not listed in the core logic above.
Here are examples. Let
===================================================
CS == ~work. coin. coffee. CS
CTM == coin.(~coffee.CTM + ~tea.CTM)
TM == coin.~tea.TM
C1 == coin.(~coffee.0 + ~tea.0)
C2 == (coin.~coffee.0) + (coin.~tea.0)
and Act = {work, coin, coffee, tea, ~work, ~coin, ~coffee, ~tea, τ}
===================================================
That is, Act is the set of all possible moves (communications).
Then
===================================================
CS |= <~work><coin>true
"CS can make a ~work step and then a coin step (after that, we don't care)."
CS |= <Act><Act>true "CS can make two steps (at least)."
CS not|= <coffee>true "An initial coffee step is impossible."
CS |= [coffee]false "All initial coffee steps make false == true"
CS |= [{coffee, tea}]false "All initial drink step is impossible."
coffee.CS |= <coffee>true "coffee.CS can make a next step that is coffee"
CTM |= <coin><~coffee>true "CTM can do coin and then ~coffee steps"
CTM |= <coin><~tea>true "CTM can do coin and then ~tea steps"
CTM |= <coin>(<~coffee>true ^ <~tea>true)
"CTM can do a coin step and then both a ~coffee step and a ~tea step as desired.
TM |= <coin><~tea>true) "TM can do a coin and a ~tea step"
TM |= <coin>(<~coffee>true v <~tea>true)
"TM can do a coin step followed by a step for coffee or for tea,
but there might not be a choice available."
CTM |= <coin>(<~coffee>true v <~tea>true)
Why ?
C1 |= <coin><~coffee>true "C1 can make a coin step then a ~coffee step"
C1 |= [coin]<~coffee>true "All coin steps can be followed by a ~coffee step"
C2 |= <coin><~coffee>true "C2 can make a coin step then a ~coffee step"
C2 not|= [coin]<~coffee>true "Not all coin steps can be followed by a ~coffee step"
C2 |= -[coin]<~coffee>true
C2 |= <coin>-(<~coffee>true)
C2 |= <coin>[~coffee]false Why?
C1 |= <coin>(<~coffee>true ^ <~tea>true)
But this does not hold for C2 !
===================================================
HML as defined above can state properties of finite sequences of choices.
This is the main theorem about HML:
CS takes coffee infinitely often. CS never takes tea. CTM can offer tea in the future. CTM offers tea in the future infinitely often. There is an execution where CTM never serves a tea.These examples use the notion of an undetermined number of future steps, maybe infinitely many such future steps. We state this notion by adding (two forms of) recursion to HML:
=================================================== K ==lfp PK (least-fixed-point --- inductive --- recursion It's written formally like this: K == μX. PX ) K ==gfp PK (greatest-fixed-point --- coinductive --- recursion It's written formally like this: K == νX. PX ) ===================================================That is, propositions can be defined as equations that refer to themselves. In classical logic, this is typically malformed, but the concept works here because of the <a>P and [a]P formulas, which march forwards one step in time. For this reason, Recursive HML is called a temporal logic.
Recursive HML is also known as the modal mu-calculus.
Here are the examples stated just above, coded in Recursive HML:
CoffeeSoon ==lfp (<coffee>.true) v (([Act]CoffeeSoon) ^ (<Act>true))<Act>true means "can take a step --- not deadlocked"; [Act]CoffeeSoon means "all next steps lead closer to CoffeeSoon"; <coffee>.true means "takes coffee now".
The P ==lfp ...P... requires that the nonrecursive part of ...P...
must be made true in some finite future. (The technical definition is
that
P ==lfp ...P... is defined as
ORi>=0 Pi, that is, P0 v P1 v ... v Pk v ...
where P0 == false
Pi+1 == ...Pi... for i>=0
This works for checking properties of finite-state processes.)
In CTL, CoffeeSoon is coded as AF coffee.
In CTL, ([Act]P) ^ (<Act>true) is coded AX P.
It holds that CS |= CoffeeSoon.
CoffeeForever ==gfp CoffeeSoon ^ ([Act]CoffeeForever) ^ (<Act>true)That is, the current configuration must lead to one dose of coffee finitely soon and all next configurations also provide coffee until forever.
The P ==gfp ...P... requires that the nonrecursive part of ...P...
never goes false in any finite future. (The technical definition is
that
P ==gfp ...P... is defined as
ANDi>=0 Pi, that is, P0 ^ P1 ^ ... ^ Pk ^ ...
where P0 == true
Pi+1 == ...Pi... for i>=0
This works for checking properties of finite-state processes.)
In CTL, CoffeeForever is coded as AG CoffeeSoon.
CS |= CoffeeForever holds.
NeverTea ==gfp (-<tea>true) ^ [Act]NeverTea ==gfp [tea]false ^ [Act]NeverTeaThat is, the next move cannot be a ~tea move and all future moves lead to configurations where NeverTea holds.
CS |= NeverTea holds.
OfferTeaSoon ==lfp <~tea>true v <Act>OfferTeaSoonThat is, either tea is offered now or there is a next step that takes us closer to tea.
In CTL, this is coded as EF ~tea.
CTM |= OfferTea holds.
CT == CMM + TMM CMM == coin.~coffee.CT TMM == coin.~tea.CT NoTea ==gfp [~tea]false ^ <Act>NoTeaThat is, tea is not offered now, and there is a next step to a configuration where tea is not offered in the future. In CTL, this is coded as EG ~tea
CT |= NoTea holds, because there is an (unfair) execution path that has no ~tea step.
AlwaysOfferTea ==gfp OfferTeaSoon ^ ([Act]AlwaysOfferTea) ^ <Act>trueThat is, from the current state, and for all states, for forever, CTM can always OfferTeaSoon.
C ?|= P is checked by generating subgoals (for checking ^ and v) and by following transition steps (for checking <a> and [a]). There are at least exponentially many generated C' ?|= P' subgoals based on the sizes of C and P.
Despite the computational infeasibility of model checking, it is used in practice: All of Intel's chip designs are coded as equational systems, and standard hardware correctness properties are stated in an HML-like logic and mechanically checked. All device drivers written at Microsoft are mechanically simplified by replacing infinite data sets (ints, arrays) by booleans and then checked for standard liveness and safety properties using the SLAM model checker.
Starting from an initial goal, C ?|= P, a model checker applies the Tarski-intepretation of P, generating (exponentially many) subgoals, Ci ?|= Pi. If every subgoal degenerates to trivial subgoals, Cj ?|- true, then the original goal is proved. If any subgoal of form Cj ?|= false arises, the original goal fails, and the sequence of subgoaling from C ?|= P to Cj ?|= false forms a "counterexample" from which an execution trace of C can often (but not always!) be extracted and used to find the error within C that caused P to fail.
(There are two similar, special cases for checking a recursively defined proposition: C ?|= μX.PX subgoals to C ?|= PX; if this generates a subgoal, C ?|= X, the result is treated as C |= false. ; C ?|= νX.PX subgoals to C ?|= PX; if this generates a subgoal, C ?|= X, the result is treated as C |= true.)
Theorem prover technology is used in a model checker: tableau checking, binary decision diagrams, brute-force SAT solvers, every trick known in the field. Short cuts based on symmetry or dynamic programming are typical. Still, it is difficult to check a program/model that is coded in much more than a few thousand lines of equations. For this reason, the application of abstract interpretation --- the extraction of "property skeletons" from source code for model checking, is critical to practical success.