CCS Notes


1 Syntax
2 Semantics
    2.1 Coinductive interpretation
3 Weak Bisimulation
4 (Recursive) Hennessy-Milner Logic


1 Syntax

We use this syntax for CCS expressions:
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


2 Semantics

The meaning of a CCS process is a tree, which can be infinite in the length of its paths. Examples:







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


2.1 Coinductive interpretation

A process is meant to "run forever", so its semantics should be an infinite tree that contains infinite paths. But the classic, inductive interpretation of a recursion equation generates trees that have only finite paths.

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.


3 Weak Bisimulation

Weak bisimulation is used to prove two processes have the same (external) communication behavior, ignoring internal behavior. The latter (with its τ steps) adds a special case to the definition. (If internal behavior did not exist, we can define the stronger, simpler, less realistic, strong bisimulation.)

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 τ),

  1. if c -->b c', then there is some d' such that d ==>^b d', and c' R d'.
  2. if d -->b d', then there is some c' such that c ==>^b c', and c' R d'.

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.


4 (Recursive) Hennessy-Milner Logic

When process R simulates process P, it means that R has all of P's visible behaviors. If P is a desirable property, e.g., P == ~work.P, then R can do something desirable --- R ``has property P.''

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.

HML Syntax and semantics

Propositions, P, are defined using actions, a (including τ):
P ::=  true  |  false  |  P1 ^ P2  |  P1 v P2  |  <a>P  |  [a]P
Let 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:

  1. Negation, -, is added like this:
    -true ==  false  
    -false ==  true
    -(P1 ^ P2) ==  -P1 v -P2
    -(P1 v P2) ==  -P1 ^ -P2
    -<a>P ==  [a]-P
    -[a]P ==  <a>-P
    
    That's why it's not listed in the core logic above.
  2. Say that S = {a1,a2,...,am} is a set of actions. Then <S>P abbreviates <a1>P v <a2>P v ... v <am>P and [S}P abbreviates [a1]P ^ [a2]P ^ ... ^ [am]P.

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:

  1. Process C strongly simulates D iff (for all P in HML, if D |= P, then C |= P also.)
  2. Processes C and D are strongly bisimular iff they make true exactly the same HML properties.
With some care with the τ moves, an analogous theorem can be stated for weakly bisimular processes.

Recursively defined propositions: Recursive HML

We want to state properties like this:
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:

Model checking

Model checking is validating or refuting C ?|= P, for process C and property P, by mechanically unfolding the definitions of C and P. The mechanical process is called state-space exploration, because it generates all possible configurations (states) that C might generate while analyzing P.

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.