%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % PVS files corresponding to FM'06 paper % Modeling and Validating Distributed Embedded % Real-Time Systems with VDM++ % % Marcel Verhoef, Peter Gorm Larsen, Jozef Hooman % % PVS files by Jozef Hooman, last change: 24 May 2006 % % see PVS dump file for all proofs % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% SyntacticDomains: THEORY % This theory defines the syntax of instructions and % the topology (nodes and links) BEGIN Time: TYPE+ = nonneg_real % real numbers >= 0 ObjectId : TYPE+ % the "+" above means that the type is not empty % so we can declare a constant of this type: someoid : ObjectId Operation : TYPE+ % operations are synchronous or asynchronous % the next predicate defines whether it is synchronous syn? : [Operation -> bool] Thread : TYPE+ % instructions are defined as a data type % the recorgnizers such as "call?" define subtypes % of instructions and can be used as predicates on % instructions to test whether they are of that subtype % the auxiliary instruction "return" is already included Instruction : DATATYPE BEGIN skip: skip? call(id: ObjectId, op: Operation): call? duration(d: Time): duration? period(d: Time, iseq: finite_sequence[Instruction]): period? return(thr: Thread): return? END Instruction % note that "finite_sequence" is predefined in the % PVS prelude and has a length and a sequence "seq" % of elements with index 0 ... length-1. % we define a few useful variables and abbreviations op : VAR Operation oid : VAR ObjectId instr, instr1 : VAR Instruction iseq : VAR finite_sequence[Instruction] % define when an instruction is a member of a sequence member(instr, iseq) : bool = EXISTS (n:below[length(iseq)]) : instr = seq(iseq)(n) % predicate NoReturn defines (recursively) whether % and instruction does not contain the auxiliary return % (recursion is needed because period contains a sequence) NoReturn(instr) : RECURSIVE bool = CASES instr OF skip: true, call(oid, op): true, duration(d): true, period(d, iseq): (FORALL instr1 : member(instr1, iseq) IMPLIES NoReturn(instr1)), return(thr): false ENDCASES MEASURE instr BY << % define proper instruction sequences (without return) ProperInstrSeq?(iseq) : bool = FORALL instr : member(instr, iseq) IMPLIES NoReturn(instr) ProperInstructionSeq : TYPE = { iseq | ProperInstrSeq?(iseq) } % the next constant is used to show that we can % assign at least one sequence to each operation and % object (namely the empty one) empy_def(op,oid) : ProperInstructionSeq = empty_seq % assume given an explicit definition for each operation % of each object as a proper sequence of instructions def(op,oid) : ProperInstructionSeq % to express deployment, define nodes and links between them Node : TYPE+ node(oid) : Node node1, node2 : VAR Node % for the moment, assume all nodes are directly connected connected(node1,node2) : bool = true Link : TYPE = { (node1,node2) | connected(node1,node2) } END SyntacticDomains %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Queue[T: TYPE]: THEORY % In this theory defines FIFO queues of type T % using finite sequences; the first element, the one which % has been inserted first, is at position 0 BEGIN Queue: TYPE = finite_sequence[T] % a few variables elt, elt1, elt2 : VAR T q : VAR Queue % an empty queue is just the empty sequence empty_queue : Queue = empty_seq % insert elements at the end of the sequence insert(elt, q): Queue = LET newlen = length(q)+1 IN (# length := newlen, seq := (LAMBDA (n:below[newlen]) : IF n < length(q) THEN seq(q)(n) ELSE elt ENDIF) #) empty?(q) : bool = length(q) = 0 nonempty?(q): bool = length(q) > 0 neq: VAR (nonempty?) % the first element is at position 0 first(neq): T = seq(neq)(0) % after removing the first element, the rest of the queue % is obtained by shifting all elements one position rest(neq): Queue = LET newlen = length(neq)-1 IN (# length := newlen, seq := (LAMBDA (n:below[newlen]) : seq(neq)(n+1)) #) % define when an element is a member of a queue member(elt, q) : bool = EXISTS (n:below[length(q)]) : elt = seq(q)(n) % a few useful properties (stated as lemmas) % the label before the LEMMA keyword is the % name of the lemma (can be used in proofs) InsertFirst: LEMMA first(insert(elt, q)) = IF empty?(q) THEN elt ELSE first(q) ENDIF MemberInsert : LEMMA member(elt1, insert(elt2, q)) IMPLIES (elt1 = elt2 OR member(elt1, q)) MemberRest : LEMMA member(elt, rest(neq)) IMPLIES member(elt,neq) END Queue %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% SemanticDomains: THEORY % This theory defines the semantics primitives % and a number of related definitions BEGIN IMPORTING SyntacticDomains % status of a thread is defined by an enumeration type: % dormant (not used), alive (used and willing to execute) % or waiting (used, but waiting for the return of a call) Status : TYPE = {dormant, alive, waiting} % to transfer call requests and returns we use messages % the thread refers to the calling thread Message : DATATYPE BEGIN callm(thr: Thread, id: ObjectId, op: Operation): callm? returnm(thr: Thread): returnm? END Message % use the recognizer to define the subtype of call messages % becayse these are also used in the object queues % (in fact, then the object is not needed, but in this % way semantic definitions become a bit easier) CallMsg : TYPE = (callm?) IMPORTING Queue[CallMsg] % link elements are records consisting of a message (msg) % a lower bound (lb) and an upper bound (ub) LinkElt : TYPE = [# msg : Message, lb : Time, ub : Time #] % a configuration defines a snapshot of the state of affairs % at a certain moment during the execution Configuration : TYPE = [# instr : [Thread -> finite_sequence[Instruction]], curthr : [Node -> Thread], status : [Thread -> Status], q : [ObjectId -> Queue[CallMsg]], linkset : [Link -> setof[LinkElt]], now : Time #] % a few variables C : VAR Configuration oid : VAR ObjectId i : VAR Thread iseq, iseq1, iseq2 : VAR finite_sequence[Instruction] instr, instr1, instr2 : VAR Instruction % assume a fixed assignment "obj" of threads to objects obj(i) : ObjectId % derive deployment of threads from their object node(i) : Node = node(obj(i)) % define when a thread is executing exec(C,i) : bool = (curthr(C)(node(i)) = i) % use the pre-defined "epsilon" function to select a fresh % thread for an object oid in a configuration C fresh(C,oid) : Thread = epsilon({i | status(C)(i) = dormant AND obj(i) = oid}) % next a number of useful definitions for finite sequences of % instructions (similar to those for queues) empty?(iseq) : bool = length(iseq) = 0 nonempty?(iseq): bool = length(iseq) > 0 neiseq: VAR (nonempty?) NonEmptyInstr(C) : setof[Thread] = { i | nonempty?(instr(C)(i))} head(neiseq) : Instruction = seq(neiseq)(0) tail(neiseq) : finite_sequence[Instruction] = LET newlen = length(neiseq)-1 IN (# length := newlen, seq := (LAMBDA (n:below[newlen]) : seq(neiseq)(n+1)) #) % a useful propery MemberTail : LEMMA member(instr, tail(neiseq)) IMPLIES member(instr, neiseq) % define concatenation on instructions and sequences concat(instr,iseq) : finite_sequence[Instruction] = LET newlen = length(iseq)+1 IN (# length := newlen, seq := (LAMBDA (n:below[newlen]) : IF n=0 THEN instr ELSE seq(iseq)(n-1) ENDIF) #) concat(instr1,instr2) : finite_sequence[Instruction] = LET len = 2 IN (# length := len, seq := (LAMBDA (n:below[len]) : IF n=0 THEN instr1 ELSE instr2 ENDIF) #) concat(iseq,instr) : finite_sequence[Instruction] = LET newlen = length(iseq)+1 IN (# length := newlen, seq := (LAMBDA (n:below[newlen]) : IF n < length(iseq) THEN seq(iseq)(n) ELSE instr ENDIF) #) % for concatenation of finite sequences we use the predefined % "o" operator from the PVS prelude concat(iseq1,iseq2) : finite_sequence[Instruction] = iseq1 o iseq2 END SemanticDomains %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% OperationalSemantics : THEORY % This theory contains the operational semantics % We define a step if an execution by means of four cases: % - execute the first instruction of a currently executing % thread % - make a context switch % - deliver a message by link % - process a message from a queue BEGIN IMPORTING SemanticDomains % declare a number of variables C, C1, C2 : VAR Configuration t : VAR Time oid, oid1, oid2 : VAR ObjectId op, op1 : VAR Operation i, j, thr : VAR Thread m : VAR Message lelt : VAR LinkElt node : VAR Node l : VAR Link d : VAR Time k : VAR nat iseq : VAR finite_sequence[Instruction] %%%%%%%%%% message delivery %%%%%%%%%%%%%%%%%%%%%%%%%% % define the transmission of a message over a link % assume given min and max transmission delays TransmitDelayMin(l) : Time TransmitDelayMax(l) : Time % general definition to transmit message m over link l Transmit(C1,C2,l,m) : bool = LET lelt = (# msg := m, lb := now(C1) + TransmitDelayMin(l), ub := now(C1) + TransmitDelayMax(l) #) IN C2 = C1 WITH [(linkset)(l) := add(lelt, linkset(C1)(l))] % definitions for first executing an instruction which % is a the head of the sequence of an executing thread i %%%%%%%%%%%%%% skip %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % for a skip just remove the head of queue RemoveHead(C1,C2,(i:(NonEmptyInstr(C1)))) : bool = C2 = C1 WITH [(instr)(i) := tail(instr(C1)(i))] %%%%%%%%%%%%% call %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % for a call there are four possibilities: % (1) to synchronous operation within a node (inter node) % then execute instructions of operation at caller InterNodeSynchrCall(C1,C2,i,oid,op) : bool = C2 = C1 WITH [(instr)(i) := concat(def(op,oid),instr(C1)(i))] % (2) to asynchronous operation within a node (inter node) % then insert call request in called object InterNodeAsynchrCall(C1,C2,i,oid,op) : bool = LET m = callm(i,oid,op) IN C2 = C1 WITH [(q)(oid) := insert(m, q(C1)(oid))] % (3) to asynchronous operation of objecton another node % (intra node); then transmit call request via link IntraNodeAsynchrCall(C1,C2,i,oid,op) : bool = LET l = (node(oid),node(i)), m = callm(i,oid,op) IN Transmit(C1,C2,l,m) % (4) to synchronous operation of objecton another node % (intra node); similar to (3), but in addition % caller becomes waiting IntraNodeSynchrCall(C1,C2,i,oid,op) : bool = EXISTS C : IntraNodeAsynchrCall(C1,C,i,oid,op) AND C2 = C WITH [(status)(i) := waiting] % these four possibilities lead to definition of call Call(C1,C2,i,oid,op) : bool = IF node(oid) = node(i) THEN IF syn?(op) THEN InterNodeSynchrCall(C1,C2,i,oid,op) ELSE InterNodeAsynchrCall(C1,C2,i,oid,op) ENDIF ELSE IF syn?(op) THEN IntraNodeSynchrCall(C1,C2,i,oid,op) ELSE IntraNodeAsynchrCall(C1,C2,i,oid,op) ENDIF ENDIF %%%%%%%%%%%%% duration %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % a duration statement allows global progress of time % with "d" time units under a number of conditions % first require that all alive threads with a nonempty % instruction sequence start with duration statement OnlyDurations(C) : bool = FORALL i : status(C)(i) = alive AND nonempty?(instr(C)(i)) IMPLIES duration?(head(instr(C)(i))) % time should not progress above any upper bound % in any of the link sets LeqUpperBound(C,d) : bool = FORALL l, lelt : member(lelt, linkset(C)(l)) IMPLIES now(C) + d <= ub(lelt) % the time increase "d" should be smaller than all durations % of any alive thread LeqDuration(C,d) : bool = FORALL i : status(C)(i) = alive AND nonempty?(instr(C)(i)) AND duration?(head(instr(C)(i))) IMPLIES d <= d(head(instr(C)(i))) % to obtain non-Zenoness, we choose the time step "d" to be % equal to an upper bound or a delay EqualUbOrDuration(C,d) : bool = (EXISTS l, lelt : member(lelt, linkset(C)(l)) AND now(C) + d = ub(lelt)) OR (EXISTS i : status(C)(i) = alive AND nonempty?(instr(C)(i)) AND duration?(head(instr(C)(i))) AND d = d(head(instr(C)(i)))) % UpdateDuration : [Thread -> finite_sequence[Instruction]] % updates"now" and all durations with a delay of "d" % a durations which decrease to 0 is removed % we require that "d" satisfies LeqDuration(C,d) % to be able to show that durations remain nonnegative UpdateDuration(C,(d| LeqDuration(C,d)))(i) : finite_sequence[Instruction] = IF status(C)(i) = alive AND nonempty?(instr(C)(i)) AND duration?(head(instr(C)(i))) THEN IF d(head(instr(C)(i))) - d > 0 THEN concat(duration(d(head(instr(C)(i))) - d), tail(instr(C)(i))) ELSE tail(instr(C)(i)) ENDIF ELSE instr(C)(i) ENDIF % this leads to the following modification of a configuration TimeIncrease(C1,C2,(d| LeqDuration(C1,d))) : bool = C2 = C1 WITH [(instr) := UpdateDuration(C1,d), (now) := now(C1) + d ] % and finally the definition of a global time step GlobalTimeStep(C1,C2) : bool = EXISTS d : OnlyDurations(C1) AND LeqUpperBound(C1,d) AND LeqDuration(C1,d) AND EqualUbOrDuration(C1,d) AND TimeIncrease(C1,C2,d) %%%%%%%%%%%%% period %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % for a period statement the specified instruction sequence % can be executed and a new thread is started for the % next period Period(C1,C2,i,d,iseq) : bool = LET j = fresh(C1,obj(i)) IN C2 = C1 WITH [(instr)(i) := iseq, (instr)(j) := concat(duration(d),period(d,iseq)), (status)(j) := alive] %%%%%%%%%%%%% return %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % the auxiliary return statement only occurs for % calls between nodes (inter node calls), hence % a return message is transmitted to the calling thread Return(C1,C2,i,thr) : bool = LET l = (node(i),node(thr)), m = returnm(thr) IN Transmit(C1,C2,l,m) %%%%%%%%%%%%% execute instruction %%%%%%%%%%%%%%%%%%%%%% % the above definitions are used to define the execution % of the first instruction of an executing thread ExecuteInstruction(C1,C2) : bool = EXISTS i : exec(C1,i) AND nonempty?(instr(C1)(i)) AND CASES head(instr(C1)(i)) OF skip: RemoveHead(C1,C2,i), call(oid, op): (EXISTS C : RemoveHead(C1,C,i) AND Call(C,C2,i,oid,op)), duration(d): GlobalTimeStep(C1,C2), period(d, iseq): Period(C1,C2,i,d,iseq), return(thr): (EXISTS C : RemoveHead(C1,C,i) AND Return(C,C2,i,thr)) ENDCASES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % a context switch changes the currently execution thread % to another, not yet executing, alive thread % we add a duration to model the context switch delay ContextSwitchDelay : Time ContextSwitch(C1,C2) : bool = EXISTS i : status(C1)(i) = alive AND NOT exec(C1,i) AND C2 = C1 WITH [(instr)(i) := concat(duration(ContextSwitchDelay), instr(C1)(i)), (curthr)(node(i)) := i ] %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % a link may deliver a message if the current time ("now") % is between its lower and upper bound % if the message corresponds to a call, % the call request is inserted in the queue of the callee, % otherwise it must be a return and the calling thread % should change status (from waiting) to alive Deliver(C1,C2) : bool = EXISTS l, lelt : member(lelt, linkset(C1)(l)) AND lb(lelt) <= now(C1) AND now(C1) <= ub(lelt) AND IF callm?(msg(lelt)) THEN % put in queue of callee LET oid = id(msg(lelt)) IN C2 = C1 WITH [(q)(oid) := insert(msg(lelt), q(C1)(oid)), (linkset)(l) := remove(lelt, linkset(C1)(l))] ELSE % return, wake-up caller LET thr = thr(msg(lelt)) IN C2 = C1 WITH [(status)(thr) := alive, (linkset)(l) := remove(lelt, linkset(C1)(l))] ENDIF %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % an object may process the first element of its queue % which contains call requests; then a new thread is created % with the instructions of the operation % in case of a synchronous call, a return to the calling % thread is added at the end of the sequence AcceptQueueMessage(C1,C2) : bool = EXISTS oid : nonempty?(q(C1)(oid)) AND LET m = first(q(C1)(oid)), thr = thr(m), op = op(m), j = fresh(C1,oid) IN IF syn?(op) THEN C2 = C1 WITH [(instr)(j) := concat(def(op,oid),return(thr)), (status)(j) := alive, (q)(oid) := rest(q(C1)(oid))] ELSE C2 = C1 WITH [(instr)(j) := def(op,oid), (status)(j) := alive, (q)(oid) := rest(q(C1)(oid))] ENDIF %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % the definitions above are combined in a step Step(C1,C2): bool = ExecuteInstruction(C1,C2) OR ContextSwitch(C1,C2) OR Deliver(C1,C2) OR AcceptQueueMessage(C1,C2) % we define the set of initial configurations by % a number of requirements: % no thread is waiting NoWaiting(C) : bool = FORALL i : status(C)(i) /= waiting % the currently executing thread is alive CurThrAlive(C) : bool = FORALL node : (status(C)(curthr(C)(node)) = alive) % a dormant thread has an empty instruction sequence DormantEmpty(C) : bool = FORALL i : status(C)(i) = dormant IFF instr(C)(i) = empty_seq % all queues are empty QueuesEmpty(C) : bool = FORALL oid : q(C)(oid) = empty_queue % all linksets are empty LinksetsEmpty(C) : bool = FORALL l : linkset(C)(l) = emptyset % all instruction sequences are proper, i.e. % do not contain the auxiliary return statement NoReturns(C) : bool = FORALL i : ProperInstrSeq?(instr(C)(i)) InitialConfigurations : setof[Configuration] = { C | NoWaiting(C) AND CurThrAlive(C) AND DormantEmpty(C) AND QueuesEmpty(C) AND LinksetsEmpty(C) AND NoReturns(C) } r : VAR sequence[Configuration] % non-Zenoness need not be added explicitly because we % selected time increase to be equal to a duration on % upperbound on transmission, but it could be formulated % as follows: % % NonZeno(r) : bool = FORALL t : EXISTS k : t < now(r(k)) % the set of execution runs is defined as a sequence of % configuraitons, where the first configuration satisfies % the requirements on the initial state and two successive % configurations are related by the "Step" relation Run: TYPE = { r | member(r(0), InitialConfigurations) AND FORALL k: Step(r(k), r(k+1)) } END OperationalSemantics %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Properties : THEORY % we formulate and prove a number of properties % about the operational semantics BEGIN IMPORTING OperationalSemantics % a number of variables i, j : VAR Thread oid : VAR ObjectId op : VAR Operation node : VAR Node l : VAR Link r : VAR Run k : VAR nat m : VAR Message C, C1, C2 : VAR Configuration lelt : VAR LinkElt instr : VAR Instruction % define when a predicate on configurations % holds "always", i.e. at any point in any run P : VAR [Configuration -> bool] Always(P) : bool = FORALL r : FORALL k : P(r(k)) % the status of current threads is not dormant CurThrNotDormant(C) : bool = FORALL node : (status(C)(curthr(C)(node)) /= dormant) CurThrAliveProp : LEMMA Always(CurThrNotDormant) % all dormant threads have an empty instruction list DormantImpliesEmpty(C) : bool = FORALL i : status(C)(i) = dormant IMPLIES instr(C)(i) = empty_seq DormantImpliesEmptyProp : LEMMA Always(DormantImpliesEmpty) % all elements in link sets have an upper bound which is % not larger than the current time NowLeqUb(C) : bool = FORALL l, lelt : member(lelt, linkset(C)(l)) IMPLIES now(C) <= ub(lelt) NowLeqUbProp : LEMMA Always(NowLeqUb) % a call request in the queue of an object is always % a call to this object CallObject(C) : bool = FORALL oid, m : callm?(m) AND member(m, q(C)(oid)) IMPLIES id(m) = oid CallObjectProp : LEMMA Always(CallObject) % a call request in a link set always corresponds to % a call between different nodes (inter node call) CallLinksetNode(C) : bool = FORALL l, lelt: LET m = msg(lelt) IN member(lelt, linkset(C)(l)) AND callm?(msg(lelt)) IMPLIES node(thr(m)) /= node(id(m)) CallLinksetNodeProp : LEMMA Always(CallLinksetNode) % a call request for a synchronous operation occurs only in a % queue of an object if it is a call between different nodes % (inter node call) CallQueueNode(C) : bool = FORALL m, oid: callm?(m) AND syn?(op(m)) AND member(m, q(C)(oid)) IMPLIES node(thr(m)) /= node(oid) CallQueueNodeProp : LEMMA Always(CallQueueNode) % time does not decrease - proof is trivial NoTimeDecreaseProp : LEMMA now(r(k)) <= now(r(k+1)) END Properties %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%