Getting started with SMV
نویسنده
چکیده
MODEL IMPLEMENTION COMPONENT REFINEMENT MAPS Figure 1: Re nement maps interfaces and structures in the low-level design. This makes it possible to verify small parts of the low-level design in the context of the abstract model. Thus, the proof obligations can be reduced to a small enough scale to be veri ed by model checking. SMV supports this methodology by allowing one to specify many abstract de nitions for the same signal. A new construct called a \layer" is introduced for this purpose. A layer is a collection of abstract signal de nitions. A layer can, for example, de ne low-level implementation signals as a function of abstract model signals, and thus provide a re nement map (i.e., a translation between abstraction levels). The low-level implementation of a signal must be simultaneously consistent with all of its abstract de nitions. Thus, each abstract de nition entails a veri cation task { to show that every implementation behavior is allowed by this de nition. For the purpose of this veri cation task, one may use whichever abstract de nition is most convenient for de ning of the other signals. Suppose, for example, that we have abstract de nitions of both the inputs and outputs of a given low-level block as a function of a high-level model, as depicted in gure 1. We can use the abstract de nitions of the inputs to drive the inputs of the block from the high-level model when verifying that the outputs are consistent with their abstract de nitions. Thus, the abstract model provides the context (or environment) for verifying the block, and we do not need to consider the remainder of the low-level model. SMV also supports design by a successive re nement. One can de ne a sequence of layers, each of which is more detailed than the previous layer. The implementation of each signal is given by the lowest-level de nition in the hierarchy. 4.1 Layers A layer is a collection of abstract signal de nitions. These are expressed as assignments in exactly the same way that the implementation is de ned, except that they are bracketed by 15 a layer statement, as follows: layer : { assignment1; assignment2; ... assignmentn; } where each assignment is of the form := ; or next() := ; or init() := ; High level control structures, such as if, switch and for can also be used inside a layer construct, since these are simply \syntactic sugar" for assignments of the above form. The layer declaration is actually a formal speci cation, which states that every implementation behavior must be consistent with all of the given assignments. If this is the case, we say the implementation re nes the speci cation. As an example, let's consider a very simple example of a speci cation and implementation of a nite state machine: module main(){ x : boolean; /* the specification */ layer spec: { init(x) := 0; if(x=0) next(x) := 1; else next(x) := {0,1}; }/* the implementation */ init(x) := 0; next(x) := ~x; } 16 Note that spec is not a keyword here { it is just an arbitrary name given to our speci cation. This speci cation is nondeterministic, in that at state 1, it may transition to either state 0 or state 1. The implementation on the other hand has only one behavior, which alternates between state 0 and state 1. Since this is one possible behavior of spec, the speci cation spec is satis ed. If you enter this example into a le, and open the le with vw, you will nd in the Properties page a single entry named x//spec. This is a notation for \the de nition of signal x in layer spec". It appears in the Properties page because it is an obligation to be veri ed, rather than a part of the implementation. You can verify it by selecting \Prop|Verify all". SMV does this by translating the assignment into an initial condition and transition invariant. The former states that x is 0 at time t = 0, while the latter states that the value of x at time t+ 1 is 1 if x is 0 at time t, and else is either 0 or 1. The implementation must satisfy these two conditions, which are veri ed by exhaustive search of the state space of the implementation. If more than one signal is assigned in a layer, then the two de nitions are veri ed separately. This is known as decomposition. The reason for using decomposition is that we may be able to use a di erent abstraction of the implementation to prove each component of the speci cation. As a very simple example, consider the following program: module main(){ x,y : boolean; /* the specification */ layer spec: { x := 1; y := 1; }/* the implementation */ init(x) := 1; next(x) := y; init(y) := 1; next(y) := x; } Both state bits in the implementation start at 1, and at each time they swap values. Thus, the speci cation is easily seen to be satis ed { both x and y are always equal to 1. If you open this example with vw, you will nd two entries in the Properties page: x//spec and y//spec. Each of these can be veri ed separately ( .e., we can verify separately that x is always equal to 1 and that y is always equal to 1). Suppose we want to verify x//spec (select it in the Properties page). We now have two choices: we can use either the speci cation de nition of y or the implementation de nition y. Note, however, that if we use the speci cation de nition of y, we eliminate one state variable from the model, since y is de ned to be identically 1. 17 Thus, by decomposing a speci cation into parts, and using one part as the \environment" for another, we have reduced the number of state variables in the model, and thus reduced the veri cation cost (though it is in any event trivial in this case). In fact, if you click on the Cone tab in vw, you will see that SMV has selected layer spec to de ne y, and that as a result, y is not a state variable. This is because SMV assumes by default that it is better to use an abstract de nition of a signal than a detailed one. Select \Prop|Verify x//spec" to verify the property using this abstraction. Note that y//spec can now be veri ed using x//spec to de ne x. This might at rst seem to be a circular argument. However, SMV avoids the potential circularity by only assuming y//spec holds up to time t 1 when verifying x//spec at time t, and vice versa. Because of this behavior, we need not be concerned about circularities when choosing an abstract de nition to drive a signal. SMV does the bookkeeping to insure that when all components of the speci cation are declared \veri ed", then in fact the implementation re nes the speci cation. 4.2 Re nement maps The most e ective way to decompose the speci cation and veri cation of a system into manageable parts is to de ne an abstract model as a speci cation, and then to specify \re nement maps" that relate abstract model behaviors to implementation behaviors. Generally, abstract models specify \what" is being done, without specifying the \how", \where" or \when". The \where" and \when" are given by the re nement maps, while the implementation determines the \how". In the simplest case the abstract model does nothing at all. For example, in the case of a link-layer protocol that simply transfers a stream of data from point A to point B without modifying it, there is no \what" and the only important information is the \where" and \when". The abstract model in this case might consist only of the stream of data itself. In the case of a microprocessor, the abstract model might determine the sequence of instructions that are executed according to the ISA (instruction set architecture). The re nement map would determine what instruction appears at each stage of the pipeline at any given time. 4.2.1 A very simple example We will consider rst a very simple example of specifying abstractions and re nement maps. Suppose that we would like to design a circuit to transmit an array of 32 bytes from its input to its output, without modifying the array. The abstract model in this case is just an unchanging array of bytes, since no actual operations are performed on the array. The re nement maps specify the protocol by which the array is transferred at the input and output. We'll assume the the input consists of three components: a bit valid indication the the input currently holds valid data, an index idx that tells which element of the array is currently being transferred, and a byte data that gives the value of this element. Assume the output uses a similar protocol. Thus far, we have the following speci cation: typedef BIT 0..7; typedef INDEX 0..31; 18 typedef BYTE array BIT of boolean; module main(){ /* the abstract model */ bytes : array INDEX of BYTE; next(bytes) := bytes; /* the input and output signals */ inp, out : struct{ valid : boolean; idx : INDEX; data : BYTE; }/* the refinement maps */ layer spec: { if(inp.valid) inp.data := bytes[inp.idx]; if(out.valid) out.data := bytes[out.idx]; }Note that the abstract model simply states that nothing happens to the array of bytes. The re nement map is partially speci ed. For example, if inp.valid is 0, then inp.data is allowed to have any value, since there is no else clause in the conditional. You can think of this as a \don't care" case in the speci cation. Now let's add a very trivial implementation: init(out.valid) := 0; next(out) := inp; }That is, the output is just the input delayed by one time unit. Note, at time t = 0 we have to signal that the output is not valid, but we don't have to specify initial values for idx and data since they are \don't cares" in this case. Save this program in a le and open it with vw. Note that there are eight properties in the le, of the form out.data[i]//spec, where i = 0..7. Select property out.data[0]//spec, for example. If you click on the Cone tab, you'll notice that only signals with bit index 0 appear. This is because SMV has detected the property you selected doesn't depend on the other bit indices. Also notice that the data input signal inp.data[0] has used layer spec for its de nition (since this is in fact the only available de nition at this point). Thus, we are driving the input of our implementation from the abstract model (through a re nement map) and verifying the output with respect to the abstract model (again through a re nement map). Now, select \Prop|Verify out.data[0]//spec". It should take less than 2 seconds 19 to verify this property. You can select \Prop|Verify All" to verify the remainder of the re nement maps. SMV will quickly recognize that the 7 remaining veri cation problems are isomorphic to the one we just solved, and report \true" for all of them. Note that although we have reduced the number of state bits by a factor of eight by using decomposition (since we only deal with one bit index at a time) we are still using 32 bits out of the data array for each veri cation. This gives us 39 state bits, which is a fairly large number and guarantees us at least 4 billion states. In this case, the large state space is easily handled by the BDD-based model checker, so we do not have to do any further decomposition. In general however, we cannot rely on this e ect. Later we'll see how to decompose the problem further, so that we only use one bit from the data array. 4.2.2 End-to-end veri cation Now we'll consider a more complex (though still trivial) implementation with multiple stages of delay. The goal is to verify the end-to-end delivery of data by considering each stage in turn, specifying a re nement map for each stage. The re nement map for each stage drives the input of the next. Suppose we replace the above implementation with the following implementation that has three time units of delay: stage1, stage2 : struct{ valid : boolean; idx : INDEX; data : BYTE; }init(stage1.valid) := 0; next(stage1) := inp; init(stage2.valid) := 0; next(stage2) := stage1; init(out.valid) := 0; next(out) := stage2; We'll include a re nement map for each intermediate delay stage, similar to the maps for the input and output: layer spec: { if(stage1.valid) stage1.data := bytes[stage1.idx]; if(stage2.valid) stage2.data := bytes[stage2.idx]; } }When verifying the output of one stage, we can drive the output of the previous stage from the abstract model, via the re nement map, thus decomposing the veri cation of each stage into a separate problem. Open this version in vw and select, for example, the property out.data[0]//spec. That is, we want to verify the nal output against the re nement map. Select the Cone page, and notice that to de ne the data outputs of the stage2, 20 SMV has chosen the layer spec, rather that the implementation de nition. The number of state bits remaining (51) is still larger than in the previous case, however, because spec doesn't give any de nition of the signals valid and idx, hence these are still driven by the implementation. If you select \Prop|Verify out.data[0]//spec", you'll observe that we can still quickly verify this property, even thought the number of state variables is larger. Nonetheless, we would like to make the veri cation of the last stage independent of the previous stages, to be sure we can still verify it if the previous stages are made more complex. We can do this by explicitly \freeing" the signals stage2.valid and stage2.idx, that is, allowing these signals to range over any possible values of their types. This is the most abstract possible de nition of a signal, and is provided by a built-in layer called free. To tell SMV explicitly to use the free layer for these signals, we add the following declaration: using stage2.valid//free, stage2.idx//free prove out.data//spec; Open this new version, and select property out.data[0]//spec. Note the the number of state bits (in the Cone page) is now 39, as in our original problem. In fact, if you select \Prop|Verify out.data[0]//spec" you will probably get a very fast answer, since SMV will notice that the veri cation problem you are trying to solve is isomorphic to that of the one-stage implementation we started with. This information was saved in a le for future use when that property was veri ed. To verify stage2, in the same way, we need to make similar using...prove declaration, as follows: using stage1.valid//free, stage1.idx//free prove stage2.data//spec; Note that we don't need a corresponding declaration for stage1, since the input signals inp.valid and inp.idx have been left unde ned, and are thus free in any event. With this addition, chose \Prop|Verify all", and observe that all the properties are veri ed very quickly, since they are all isomorphic. 4.2.3 Re nement maps as types You may have observed that it is getting a bit tedious to re nement maps for each stage of the implementation, when they are actually all the same. SMV provides a way to avoid this by specifying abstract de nitions of a signal as part of its data type. We can also give a type a parameter, so that we can specify in the type declaration which abstract object an implementation object corresponds to. A parameterized type in SMV is otherwise known as a module. Let's declare a type with a re nement map as follows: module byte_intf(bytes){ bytes : array INDEX of BYTE; 21 valid : boolean; idx : INDEX; data : BYTE; layer spec: if(valid) data := bytes[idx]; }This de nes an interface type that transfers an array bytes of bytes according to a speci c protocol. This protocol is de ned by layer spec. Now, lets rewrite our example using this type: module main(){ /* the abstract model */ bytes : array INDEX of BYTE; next(bytes) := bytes; /* the input and output signals */ inp, out : byte_intf(bytes); /* the implementation */ stage1, stage2 : byte_intf(bytes); init(stage1.valid) := 0; next(stage1) := inp; init(stage2.valid) := 0; next(stage2) := stage1; init(out.valid) := 0; next(out) := stage2; /* abstraction choices */ using stage2.valid//free, stage2.idx//free prove out.data//spec; using stage1.valid//free, stage1.idx//free prove stage2.data//spec; }Notice that there's no need to write the intermediate re nement maps. They are part of the data type. 4.2.4 The e ect of decomposition To see the e ect of using re nement maps let's make two versions of our simple example, one with and one without intermediate re nement maps. We can easily do this by changing 22 the types of the intermediate stages. To make it interesting, we'll use 32 delay stages. Here is the version with intermediate re nement maps: /* the implementation */ stages : array 1..31 of byte_intf(bytes); init(stages[1].valid) := 0; next(stages[1]) := inp; for(i = 2; i <= 31; i = i + 1){ init(stages[i].valid) := 0; next(stages[i]) := stages[i-1]; }init(out.valid) := 0; next(out) := stages[31]; /* abstraction choices */ for(i = 2; i <= 31; i = i + 1) using stages[i-1].valid//free, stages[i-1].idx//free prove stages[i].data//spec; using stages[31].valid//free, stages[31].idx//free prove out.data//spec; Here is the version without intermediate re nement maps: /* the implementation */ stages : array 1..31 of struct{ valid : boolean; idx : INDEX; data : BYTE; } init(stages[1].valid) := 0; next(stages[1]) := inp; for(i = 2; i <= 31; i = i + 1){ init(stages[i].valid) := 0; next(stages[i]) := stages[i-1]; } 23 init(out.valid) := 0; next(out) := stages[31]; Note, we don't want to free any of the intermediate signals in this version. Now, open the rst version, and select \Props|Verify all". It should verify all 256 properties in something like 15 seconds (depending on your machine). Now, open the second version (without re nement maps). There are only 8 properties to verify in this case (one for each output bit), bit SMV cannot verify these properties, as you may observe by select \Prop|Verify all". When you get bored of watching SMV do nothing, select \Prop|Kill Veri cation" (note, this may not work under Windows), and click the Cone tab. Observe that the cone contains 256 state variables, which is usually to large for SMV to handle (though occasionally SMV will solve a problem of this size, if the structure of the problem is appropriate for BDD's). Note that it is possible to construct even a fairly trivial example which cannot be veri ed directly by model checking, but can be veri ed by decomposition and model checking. Generally, when a direct model checking approach fails, it's best to look for a decomposition of the problem using re nement maps, rather than to try to determine why the BDD's exploded. 4.3 Decomposing large data structures In our trivial example, we are sending an array of 32 bytes. Because we only need to consider one bit out of each byte at a time, we were able to verify the implementation without explicitly decomposing this data structure. However, cases often arise when it is necessary to consider only one element at a time of a large structure. For example, we might increase the size of our array to 1 million bytes. As we will see later, sometimes even small arrays must be decomposed in this way. One one of decomposing a large array in the abstract model is to write an array of re nement maps (we'll see a more elegant way later, in section 4.6). Each element of this array de nes a given low-level signal only when it contains the value of the corresponding element in the abstract array. For example, let's rewrite our interface data type to use a decomposed re nement map of this kind: module byte_intf(bytes){ bytes : array INDEX of BYTE; valid : boolean; idx : INDEX; data : BYTE; forall(i in INDEX) layer spec[i]: if(valid & idx = i) data := bytes[i]; }Notice that layer spec is now an array, with one element for each element of the array bytes. The layer spec[i] speci es the value of data only when idx is equal to i, and otherwise 24 leaves data unde ned. The advantage of this re nement map is that spec[i] refers to only one element of the array bytes. Thus, the other elements will not appear in the cone when verifying it, and we have reduced the number of state variables that the model checker must handle. Let's go back to our 3-stage delay example, and use this new de nition of byte intf. Because we have changed the layer declarations, we also have to change the corresponding using...prove declarations. Replace these with the following: forall(i in INDEX){ using stage2.valid//free, stage2.idx//free prove out.data//spec[i]; using stage1.valid//free, stage1.idx//free prove stage2.data//spec[i]; } Now, when you try to open this le, you'll get an error message, something like this: The implementation layer inherits two definitions of inp.data[5] ...in layer spec[31], "map7.smv", line 15 ...in layer spec[30], "map7.smv", line 15 Perhaps there is a missing "refines" declaration? This is because we have given many abstract de nitions for inp.data without providing an implementation. By default, if there is only one abstract de nition, SMV takes this as the implementation. However, if there are many abstract de nitions, it is possible that these de nitions are contradictory, and hence there is no possible implementation. There are several possible ways to make SMV stop complaining about this. One is to provide an actual implementation. For example, we could simply implement inp.data by a nondeterministic choice among all possible data values. This would mean, of course, that we could not then prove consistency with the maps inp.data//spec[i]. On the other hand, we don't really want to prove these, since they are actually assumptions about the inputs to our design, and not properties to be proved. One way to tell SMV this is to declare inp explicitly as an input to the design. SMV does not attempt to verify re nement maps driving global inputs. It just takes them as assumptions. If our main module is later used as a submodule in a later design, we'll have to verify these maps in the context of the larger design. Meanwhile, let's change the header of our main module to look like the following: module main(bytes,inp,out){ bytes : array INDEX of BYTE; input inp : byte_intf(bytes); output out : byte_intf(bytes); Notice we've also make bytes a parameter to the module. If we later use this module in a larger design, we can then specify what abstract data array we want the module instance to transmit. Now, open this le, and select, for example, property out.data[0]//spec[0]. You'll notice that there are now only 8 state variables in the cone, since 31 of data bits have been eliminated. Also, notice that SMV chose the layer spec[0] to de ne stage2.data[0], out of the 32 possible abstract de nitions. This is a heuristic choice, which was made on the basis of the fact that we are verifying an abstraction in layer spec[0]. If you'd like to see 25 the reasoning SMV went through to arrive at this choice, select the signal stage2.data[0] and pull down \Abstraction|Explain Layer". If you now select \Prop|Verify out.data[0]//spec[0]", you can observe that the veri cation is in fact faster than in the previous case. However, you'll also notice that the number of properties to prove is now very large. In fact, it is 32 times greater than before, since every property has now been decomposed into 32 cases! Select \Prop|Verify All", and you will nd that the total veri cation time for this long list of properties is about 15 seconds, actually longer than before. Surely it is unnecessary to verify all of the 32 cases for each re nement map, since each is in e ect symmetric to all the others. In fact, if we simply tell SMV where the symmetry is, we can convince it to prove only one case out of 32. 4.4 Exploiting Symmetry Change the type declaration for INDEX from typedef INDEX 0..31; to scalarset INDEX 0..31; This is exactly the same as an ordinary type declaration, except it tells SMV that the given scalar type is symmetric, in the sense that exchanging the roles of any two values of the type has no e ect on the semantics of the program. In order to ensure that this symmetry exists, there are a number of rules placed on the use of variables of a scalarset type. For example, we can't use constants of a scalarset type, and the only operation allowed on scalarset quantities is equality comparison. In addition, we can't mix scalarset values with values of any other type. We can, however, declare an array whose index type is a scalarset. This makes it legal for us to make the type INDEX into a scalarset. Now, when SMV encounters an array of properties whose index is of scalarset type, it chooses only one case to prove, since if it can prove one case, then by symmetry it can prove all of them. Let's see the e ect of this on our example. Open the new le (with INDEX changed to a scalarset), and look in the Properties page. You'll see that there are now only properties from layer spec[0]. Pull down \Prop|Verify All", and you'll nd the total veri cation time reduced to about a half second (a savings of a factor 32!). We can go a step further than this, and make the type BIT a scalarset as well. This is because all of the bits within a byte are symmetric to each other. So change typedef BIT 0..7; to scalarset BIT 0..7; and open the new le. Now, in the Properties pane, there are only three properties, one for each stage! Thus, using symmetry, we have reduced the number of properties, by a factor of 32 8 = 256. 26 4.5 Decomposing large structures in the implementation Thus far, we've seen how we can decompose a large structure in the abstract model (such as the byte array in our example), and verify properties relating only to one small component of the structure. Now, we'll consider the case where we have a large structure in the implementation, and wish to consider only one component at a time. Let's keep the speci cation from our previous example, but design an implementation that has a large bu er that can store data bytes in transit. To make the problem more interesting, we'll put ow control in the protocol, so that our implementation can stop the ow of incoming data when its bu er is full. To implement ow control, we'll use two signals, one to indicate the sender is ready (srdy) and one to indicate the receiver is ready (rrdy). A byte is transferred when both of these signals are true. Here's the de nition of this protocol as an interface data type: module byte_intf(bytes){ bytes : array INDEX of BYTE; srdy,rrdy : boolean; idx : INDEX; data : BYTE; valid : boolean; valid := srdy & rrdy; forall(i in INDEX) layer spec[i]: if(valid & idx = i) data := bytes[i]; }Note that the re nement map only speci es the value of the data when both srdy and rrdy are true. Our system speci cation is exactly the same as before: module main(bytes,inp,out){ bytes : array INDEX of BYTE; input inp : byte_intf(bytes); output out : byte_intf(bytes); /* the abstract model */ next(bytes) := bytes; For the implementation, we'll de ne an array of 8 cells. Since all of the cells are symmetric, we'll de ne a scalarset type to index the cells: scalarset CELL 0..7; Each cell holds an index and a data byte. Each cell also needs a bit to say when the data in the cell are valid: 27 cells : array CELL of struct{ valid : boolean; idx : INDEX; data : BYTE; } We also need pointers to tell us which cell is to receive the incoming byte and which cell is to send the outgoing byte: recv_cell, send_cell : CELL; The implementation is ready to receive a byte when the cell pointed to by recv cell is empty (i.e., not valid). On the other hand, it is ready to send a byte when the cell pointed to by send cell is full (i.e., valid): inp.rrdy := ~cells[recv_cell].valid; out.srdy := cells[send_cell].valid; Here is the code that implements the reading and writing of cells: forall(i in CELL)init(cells[i].valid) := 0; default{ if(inp.valid){ next(cells[recv_cell].valid) := 1; next(cells[recv_cell].idx) := inp.idx; next(cells[recv_cell].data) := inp.data; } } in { if(out.valid){ next(cells[send_cell].valid) := 0; } }out.idx := cells[send_cell].idx; out.data := cells[send_cell].data; For the moment, we will leave the pointers recv cond); That is, each q[v] asserts that p holds at those times when x = v. Clearly, if q[v] holds for all values of v, then p holds. Thus, SMV is relieved of the obligation of proving p, and instead separately proves all the cases of q[v]. Note that if TYPE is a scalarset type, we may in fact have to prove only one case, since all the other cases are symmetric. 4.6.1 A very simple example Now, let's look at a trivial example of this. Let's return to our very simple example of transmitting a sequence of bytes. Here is the speci cation again: scalarset BIT 0..7; scalarset INDEX 0..31; typedef BYTE array BIT of boolean; module main(){ /* the abstract model */ bytes : array INDEX of BYTE; next(bytes) := bytes; /* the input and output signals */ inp, out : struct{ valid : boolean; idx : INDEX; data : BYTE; } 31 /* the refinement maps */ layer spec: { if(inp.valid) inp.data := bytes[inp.idx]; if(out.valid) out.data := bytes[out.idx]; }And let's use our original very trivial implementation: init(out.valid) := 0; next(out) := inp; }That is, the output is just the input delayed by one time unit. Note that our speci cation (layer spec) says that at all times the output value must be equal to the element of array bytes indicated by the index value out.idx. We would like to break this speci cation into cases and consider only one index value at a time. To do this, we add the following declaration: forall (i in INDEX) subcase spec_case[i] of out.data//spec for out.idx = i; In this case, the property we are splitting into cases is out.data//spec, the assignment to out.data in layer spec. The resulting cases are out.data//spec case[i]. Note, however, that in the subcase declaration, we only give a layer name for the new cases, since the signal name is redundant. This declaration is exactly as if we had written forall (i in INDEX) layer spec_case[i]: if (out.idx = i) out.data := bytes[out.idx]; except that SMV recognizes that if we prove out.data//spec case[i] for all i, we don't have to prove out.data//spec. Run this example, and look in the properties pane. You'll see that out.data//spec does not appear, but instead we have out.data//spec case[0]. Note that only the case i = 0 appears, since INDEX is a scalarset type, and SMV knows that all the other cases are symmetric to this one. Now look in the cone pane. You'll notice that all of the elements of the array bytes are in the cone. This is because the de nition of inp.data in layer spec references all of them. However, all of them except element 0 are in the undefined layer. This is a heuristic used by SMV: if a property references some speci c value or values of a given scalarset type, then only the corresponding elements of arrays over that type are used. The rest are given the unde ned value. You might try clicking on element bytes[1] and choosing Abstraction|Explain Layer to get an explanation of why this signal was left unde ned. You can, of course, override this heuristic by explicitly specifying a layer for the other elements. In this case, however, the heuristic works, since property out.data//spec case[0] veri es correctly. 32 4.6.2 Using case analysis over data paths Now we'll look at a slightly more complex example, to show how case ananlysis can be used to reduce a veri cation problem to a smaller one, by considering only one path that a given data item might take from input to output. This technique is quite useful in reasoning about data path circuitry. We'll use essentially the same speci cation as before, but in this case our implementation will be the array of cells that we used previously when discussing re nement maps (section 4.5). We have an array of cells, and each incoming byte is stored in an arbitrarily chosen cell. Recall that the speci cation in this case has to take into account the handshake signals. That is, the data are only valid when both sdry and rrdy are true: /* the abstract model */ bytes : array INDEX of BYTE; next(bytes) := bytes; /* the input and output signals */ inp, out : struct{ srdy,rrdy : boolean; idx : INDEX; data : BYTE; }/* the refinement maps */ layer spec: { if(inp.srdy & inp.rrdy) inp.data := bytes[inp.idx]; if(out.srdy & out.rrdy) out.data := bytes[out.idx]; }For reference, here is the implementation again: /* the implementation */ cells : array CELL of struct{ valid : boolean; idx : INDEX; data : BYTE; }recv_cell, send_cell : CELL; inp.rrdy := ~cells[recv_cell].valid; out.srdy := cells[send_cell].valid;33 forall(i in CELL)init(cells[i].valid) := 0; default{ if(inp.srdy & inp.rrdy){ next(cells[recv_cell].valid) := 1; next(cells[recv_cell].idx) := inp.idx; next(cells[recv_cell].data) := inp.data; } } in { if(out.srdy & out.rrdy){ next(cells[send_cell].valid) := 0; } } o out.idx := cells[send_cell].idx; out.data := cells[send_cell].data; Recall that in the previous example, we wrote re nement maps for the data in the individual cells, in order to break the veri cation problem into two pieces: one to show that cells get correct data, and the other to show that data in cells are correctly transfered to the output. Now, we will use case analysis to get a simpler decomposition, with only one property to prove. Our case analysis in this example will be a little ner. That is because we have two arrays we would like to decompose. One is the array of bytes to transfer, and the other is the array of cells. We would like to consider separately each case where byte[i] gets transfered through cell[j]. In this way, we can consider only one byte and one cell at a time. This is done with the following declaration: forall (i in INDEX) forall (j in CELL) subcase spec_case[i][j] of out.data//spec for out.idx = i & send_cell = j; Notice that our case analysis now has two parameters. Each case is of the form out.idx = i & send cell = j where i is an INDEX and k is a CELL. We can, in fact, have as many parameters in the case analysis as we like, provided we write the condition in the above form. SMV recognizes by the form of the expression that the cases are exhaustive. Now run this example, and observe that once again, we have a single property to prove: out.data//spec case[0][0]. The other cases are symmetric. If you look in the cone, you'll see that, while all elements of bytes and cells are referenced, all except element 0 of these arrays is left unde ned, according to SMV's default heuristic. This makes the veri cation problem small enough that we can handle it directly, without resorting to an intermediate re nement map. You can con rm this by verifying out.data//spec case[0][0]. This technique of breaking into cases as a function of the speci c path taken by a data item through a system is the most important reduction in using SMV to verify data path 34 circuitry. Notice that symmetry is crucial to this reduction, since without it we would have a potential explosion in the numer of di erent paths. 4.7 Data type reductions Now suppose that we would like to verify the correct transmission of a very large array of bytes, or even an array of unknown size. SMV provides a way to do this by reducing a type with a large or unknown number of values to an abstract type, with a small xed number of values. This type has one additional abstract value to represent all the remaining values in the original type. For example, when verifying the correct transmission of byte i, we might reduce the index type to just two values { i and a value representing all numbers not equal to i, (which SMV denotes NaN). This is an abstraction, since NaN, when compared for equality against itself, produces an undetermined value. In fact, here is a truth table of the equality operator for the reduced type: = i NaN i 1 0 NaN 0 f0,1g The program with the reduced index data type is an abstraction of the original program, such that any property that is true of the abstract program is true of the original (though the converse is not true). 4.7.1 A very simple example Let's return to our very simple example of transmitting a sequence of bytes (section 4.6.1). For reference, here is the speci cation again: scalarset BIT 0..7; scalarset INDEX 0..31; typedef BYTE array BIT of boolean; module main(){ /* the abstract model */ bytes : array INDEX of BYTE; next(bytes) := bytes; /* the input and output signals */ inp, out : struct{ valid : boolean; idx : INDEX; data : BYTE; 35 }/* the refinement maps */ layer spec: { if(inp.valid) inp.data := bytes[inp.idx]; if(out.valid) out.data := bytes[out.idx]; }And let's use our original very trivial implementation: init(out.valid) := 0; next(out) := inp; }That is, the output is just the input delayed by one time unit. As before, let's break the speci cation up into cases, one for each index value: forall (i in INDEX) subcase spec_case[i] of out.data//spec for out.idx = i; If you run this example, and look in the cone pane, you'll see that there are ve state variables in the cone for both inp.idx and out.idx. This is expected, since ve bits are needed to encode 32 values. However, notice that for case i, if the index value at the output is not equal to i, we don't care what the output is. Our property spec case[i] only speci es the output at those times when out.idx = i. We can therefore group all of the index values not equal to i into a class, represented by a single abstract value (NaN), and expect that the speci cation might still be true. To do this, add the following declaration: forall (i in INDEX) using INDEX->{i} prove out.data//spec_case[i]; This tells SMV to reduce the data type INDEX to an astract type consisting of just the value i and NaN (note, we don't specify NaN explicitly). Now, open the new version, and observe the cone. You'll notice the state variables inp.idx and out.idx now require ony one boolean variable each to encode them, since their type has been reduced to two values. Now try verifying the property out.data//spec case[0]. The result is true, since the values we reduced to the abstract value don't actually matter for the particular case of the speci cation we are verifying. Now, let's suppose that we don't know in advance what the size of the array of bytes will be. Using data type reductions, we can prove the correctness of our implemenation for any size array (including an in nite array). To do this, change the declaration scalarset INDEX 0..31; to the following: 36 scalarset INDEX undefined; This tells SMV that INDEX is a symmetric type, but doesn't say exactly what the values in the type are. In such a case, SMV must have a data type reduction for INDEX to prove any properties, because it can only verify properties of nite state systems. Now run the new version. You'll notice that the result is exactly the same as in the previous case. One boolean variable is used to encode values of tye INDEX, and the speci cation is found to be true. In fact, in the previous case, SMV didn't in any way use the fact that type INDEX was declared to have the speci c range 0..31. Thus it's not surprising that when we remove this information the result is the same. By using nite state veri cation techniques, we have proved a property of a system with an in nite number of states (and an in nite number of systems with nite state spaces). One might ask what would happen if, using a scalarset of unde ned range, we ommitted the data type reduction. Wouldn't that give us an in nite state veri cation problem? Try removing the declaration forall (i in INDEX) using INDEX->{i} prove out.data//spec_case[i]; from the problem and run the resulting le. You'll observe that nothing has changed from the previous case. Since SMV can't handle unde ned scalarsets without a data type reduction, it guesses a reduction. It simply includes in the reduced type all the speci c values of the given type that appear in the property. In this case, there is only one, the index i. 4.7.2 A slightly larger example Now, let's reconsider the example from the previous section of an implementation with an array of cells (section 4.6.2). For reference, here are the speci cation and implementation: /* the specification */ layer spec: { if(inp.srdy & inp.rrdy) inp.data := bytes[inp.idx]; if(out.srdy & out.rrdy) out.data := bytes[out.idx]; }/* the implementation */ cells : array CELL of struct{ valid : boolean; idx : INDEX; data : BYTE; }recv_cell, send_cell : CELL; 37 inp.rrdy := ~cells[recv_cell].valid; out.srdy := cells[send_cell].valid; forall(i in CELL)init(cells[i].valid) := 0; default{ if(inp.srdy & inp.rrdy){ next(cells[recv_cell].valid) := 1; next(cells[recv_cell].idx) := inp.idx; next(cells[recv_cell].data) := inp.data; } } in { if(out.srdy & out.rrdy){ next(cells[send_cell].valid) := 0; } }out.idx := cells[send_cell].idx; out.data := cells[send_cell].data; Let's make just one change to the source: we'll rede ne the scalarset types INDEX and CELL to have unde ned range: scalarset INDEX undefined; scalarset CELL undefined; Since these types have unde ned ranges, SMV will choose a data type reduction for use (though, of course, we could specify one if we wanted to). Now, run this modi ed version. You'll notice that in the properties pane, we have just one property to prove, as before: out.data//spec case[0][0]. In the cone pane, obverve that the variables of type INDEX and CELL have only one boolean variable encoding them (representing the value 0 and NaN). In addition, only cell[0] and byte[0] appear. This is because SMV chose to reduce the types INDEX and CELL to contain only those values appearing in the property being veri ed, which in this case are just the value 0 for both types. Con rm that in fact the speci cation can be veri ed using this reduction. Note that the proof reduction that we used for the case of a xed number of cells and a xed number of bytes, worked with no modi cation when we switched to an arbitrary number of bytes and cells! These very simple examples provide a paradigm of the veri cation of complex hardware systems using SMV. One begins by writing re nement maps. They speci y the inputs and outputs of the system in terms of a more abstract model, and possibly specify internal points as well, to break the veri cation problem into parts. The resulting properties are then broken into cases, generally as a function of the di erent paths that a data item may take from one re nement map to another. These cases are then reduced to a tractable number by symmetry considerations. Finally, for each case, a data type reduction is chosen which reduces the large 38 (or even in nite) data types to a small xed number of values. The resulting veri cation subproblems are then handled by symbolic model checking. 4.8 Proof by induction Suppose now that we want to verify some property of a long sequence. For example, we may have a counter in our design that counts up to a very large number. Such counters can lead to ine cient veri cation in SMV because the state space is very deep, and as a result, SMV's breadth rst search technique requires a large number of iterations to exhaustively search the state space. However, the usual mathematic proof technique when dealing with long sequences is proof by induction. For example, we might prove that a property holds for 0 (the base case), and further that if it holds fr some arbitrary value i, then it holds for i + 1. We then conclude by induction that the property holds for all i. Data type reductions provide a mechanism for inductive reasoning in SMV. To do this, however, we need a data symmetric data type that allows adding and subtracting constants. In SMV, such data types are called ordsets. An ordset is just like a scalarset, except the restrictions on ordsets are slightly relaxed. If we delcare a type as follows: ordset TYPE 0..1000; then, in addition to the operations allowable on scalarset types, the following are also legal: 1. x + 1 and x 1, 2. x = 0 and x = 1000 where x is of type TYPE. That is, we can increment and decrement values of ordset types, and also compare them with the extremal values of the type. Induction is done in the following way: suppose we want to prove property p[i], where i is the induction paremeter, ranging over type TYPE. We use a data type reduction that maps TYPE onto a set of four values: X,i-1,i,Y. Here the symbolic value X abstracts all the values less that i-1, and Y abstracts all the values greater than i. Incrementing a value in this reduced type is de ned as follows: X + 1 = {X,i-1} (i-1) + 1 = i i + 1 = Y Y + 1 = Y That is, adding one to a value less than i-1 will result in either i-1 or a value less that i-1. Decrementing is similary de ned. Any property provable in this abstract interpretation is provable in the original. In addition, we can show that all the cases from i = 2 up to i = 999 are isomorphic. Thus it is su cient to prove oly the cases i = 0, 1, 2, 1000. As an example, suppose we hae a counter that starts from zero and increments once per clock cycle, up to 1000. We'd like to show that for any value i from 0 to 1000, the counter eventually reaches i. Here's how we might set this up: 39 ordset TYPE 0..1000; module main() { x : TYPE; /* the counter */ init(x) := 0; next(x) := x + 1; /* the property */ forall(i in TYPE) p[i] : assert F (x = i); /* the proof */ forall(i in TYPE) using p[i-1] prove p[i]; }We prove each case p[i] using p[i-1]. That is, when proving the counter eventually reaches i, we assume that it eventually reaches i-1. (Note that technically, for the case i = 0, we are asking SMV to use p[-1], but since this doesn't exist, it is ignored). SMV can verify that this proof is noncircular. Further, using its induction rule, it automatically generates a data type reduction using the values i and i-1, and it generates the four cases we need to prove: p[0], p[1], [2], p[1000]. To con rm this, run the example, and look in the properties ane. You should see the four aforementioned properties. Now choose Verify All to verify that in fact the induction works, and that p[i] holds for all i. 4.8.1 Induction over in nite sequences Now, suppose we have a counter that ranges from zero to in nity. We can still prove by induction that any value i is eventually reached. To do this, we declare TYPE to be an ordset without an upper bound: ordset TYPE 0..; With this change, run the example, and notice that in the properties pane there are now only three cases to prove: p[0], p[1], [2]. We don't have to prove the maximum value as a special case, because there is no maximum value. Now choose Verify All to verify that in fact the induction works, and that p[i] holds for all i. We've just proved a property of an in nite-state system by model checking. 40 4.8.2 A simple example To see how we can use induction in practice, let's return to our example of transmitting an array of bytes. This time, however, we will assume that the bytes are in an in ntie sequence. They are received at the input in the order 0, 1, 2, ... and they must be transmitted to the output in that order. To begin with, let's de ne our types: scalarset BIT 0..7; typedef BYTE array BIT of boolean; ordset INDEX 0..; Note that we de ned INDEX as an ordset type, so we can prove properties by induction over indices. We begin with the original re nement speci cation. As in section 4.2.3, we encapsulate it in a module, so we can reuse it for both input and output: module byte_intf(bytes){ bytes : array INDEX of BYTE; valid : boolean; idx : INDEX; data : BYTE; layer spec: if(valid) data := bytes[idx]; } To specify ordering we simply introduce a counter cnt that counts the number of bytes received thus far. If there is valid data at the interface, we specify that the index of that data is equal to cnt. Thus, add the following declarations to module byte intf: cnt : INDEX; init(cnt) := 0; if(valid) next(cnt) := cnt + 1; ordered: assert G (valid -> idx = cnt); Note, we can include temporal properties, like the above property ordered inside modules. Thus, for each instance of the interface de nition, we'll get one instance of this property. As our rst implementation, we'll just use the trivial implementation that delays the input by one clock cycle. Here's what the main module looks like: 41 module main(bytes,inp,out){ bytes : array INDEX of BYTE; input inp : byte_intf(bytes); output out : byte_intf(bytes); /* the abstract model */ next(bytes) := bytes; /* the implementation */ init(out.valid) := 0; next(out.valid) := inp.valid; next(out.data) := inp.data; next(out.idx) := inp.idx; }To prove the correctness of the data output (with respect to the re nement speci cation), we use the same proof as before { we split into cases based on the index of the output: forall(i in INDEX) subcase spec_case[i] of out.data//spec for out.idx = i; Note that anything that can be done with a scalarset can also be done with an ordset. So much for the data correctness { the interesting part is the correct ordering. For the proof of the ordering property, we're going to use induction over the value of the counter cnt. The intuition here is that, if the output index equals the counter when the counter is i, then at the next valid output the counter and index will both be one greater, and hence they will be equal for cnt = i + 1. This assumes, of course, that the input values are ordered correctly. To verify this, we must rst break the output ordering property into cases based on the value of cnt: forall(i in INDEX) subcase ord_case[i] of out.ordered for out.cnt = i; Then, we prove case i using case i-1 and the input ordering property. We leave inp.ordering as an assumption: forall(i in INDEX){ using ord_case[i-1], inp.ordered prove ord_case[i]; assume inp.ordered; }Now, run this example, and observe the properties pane. You'll notice that we now have three cases of the property out.data//spec case[i] to prove: i = 0, 1, 2. In fact, all of 42 these cases are isomorphic, but since INDEX is de ned as an ordset rather than a scalarset, SMV's type checking rules don't guarantee this. Thus, SMV will e ectively prove the same property three times. Fortunately, each case takes only a fraction of a second. Now observe that we also have three cases of ord case[i] to prove. Select, for example, property ord case[2] from the properties pane and observe the cone. You'll notice that each value of type INDEX requires two boolean variables to encode it. This is because there are four values in the reduced type: i-1, i and two abstract values to represent the ranges 0..i-1 and i+1..infinity. Notice also that there are no data values in the cone, since the indices do not depend on the data. Thus, we have e ectively separated the problem of correct ordering from correct delivery of data. Now, try Prop|Verify all. All the cases should be veri ed in less than a second. A note: for ordsets, a data type reduction may be speci ed, in lieu of SMV's default. The general form of the data type reduction for ordset types is: TYPE -> { min..min+a, i-b..i+c, max-d..max}; where min is the minimum value of TYPE, i is the induction parameter, and max is the maximum value of TYPE. Thus, SMV allows us to use any nite number of values around the induction parameter i and the extremal values. In this case, the number of cases that need to proved will be larger, however. 4.8.3 A circular bu er Now let's consider transmission of an in nite sequence of bytes again, but this time using our array of cells as a circular bu er (an implementation of a FIFO queue). To begin with, we need to add handshaking to our interface de nition, so add the following to module byte intf: srdy, rrdy : boolean; valid := srdy & rrdy; The signal srdy indicates that the sender is ready, while rrdy indicates the the receiver is ready. The data are valid, by de nition, when both are ready. Now, as in section 4.5, we'll use an array of 32 cells, to hold our data items. So de ne the type CELL as: ordset CELL 0..31; The reason for making it an ordset type will become apparent later. Now, replace the previous \trivial" implementation with the following: cells : array CELL of struct{ valid : boolean; idx : INDEX; data : BYTE; } 43 recv_cell, send_cell : CELL; inp.rrdy := ~cells[recv_cell].valid; out.srdy := cells[send_cell].valid; forall(i in CELL)init(cells[i].valid) := 0; default{ if(inp.valid){ next(cells[recv_cell].valid) := 1; next(cells[recv_cell].idx) := inp.idx; next(cells[recv_cell].data) := inp.data; } } in { if(out.valid){ next(cells[send_cell].valid) := 0; } }out.idx := cells[send_cell].idx; out.data := cells[send_cell].data; Note, recv cell is the cell we are receiving a byte into, and send cell is the cell we are sending a byte from. We block our input (setting inp.rrdy to zero) when the cell wwe are receiving into is full, and block our output (setting out.srdy to zero) when the cell we are sending from is empty. When we receive into a cell, we set its valid bit to true, and when we send from the cell, we clear its valid bit. Up to this point, we haven't said what policy is used to choose recv cell and send cell. To make our bu er ordered, we can use a round-robin policy. This means that each time we receive a byte, we increment recv cell by one, and each time we send a byte, we increment send cell by one. When either of these reaches its maximum value, it returns to zero. To accomplish this, add to following code to the implementation: init(recv_cell) := 0; if(inp.srdy & inp.rrdy) next(recv_cell) := (recv_cell = 31) ? 0 : recv_cell +1; init(send_cell) := 0; if(out.srdy & out.rrdy) next(send_cell) := (send_cell = 31) ? 0 : send_cell +1; Note that, since CELL is an ordset type, rather than a scalarset, it's legal to compare it against the maximum value (31) and set it back to the minumum value (0). If CELL were a scalarset, it wouldn't be legal to introduce any constants of the type. Now that we have our implementation, lets prove both the correctness of the data output and correctness of the ordering. The case splitting statement for data correctness is the 44 same as when we did this example in section 4.6.2, where we weren't concerned with data ordering: forall(i in INDEX) forall(j in CELL) subcase spec_case[i][j] of out.data//spec for out.idx = i & send_cell = j; That is, we consider separately the case of each byte index i, and the cell j that it is stored in. That way, we only need to consider one cell in the aray at a time. Notice that adding ordering does not change the proof of data correctness in any way. Now for the ordering question. Again, we are going to use induction. The ordering property says that when the output data are valid, the output index must be equal to are count of the number of previous values. We'll do the proof by induction over the value of the counter. That is, we'll assume that the index was correct when the count was i-1, and then prove that the index is correct when the count is i. This means that, as before, we have to split cases based on cnt. However, in this case we also have to split cases on the cell in which the current output value stored. Thus, we use the following case splitting declaration: forall(i in INDEX) forall(j in CELL) subcase ord_case[i][j] of out.ordered for out.cnt = i & send_cell = j; Now, the question is, what data type reduction to use for type CELL. We know we need to use cell j, since that is the one holding the data item we are interested in. However, in addition, we need to use the previous cell. The intuition behind this is as follows. We are assuming that the output index is correct for byte i-1. If byte i is stored in cell j, then byte i-1 is stored in cell j-1 (which one exception). This means we need to inclde cell j-1. Then, if cell j-1 contains index i-1, and the inputs are ordered, it follows that cell j will contain index i, which is what we are trying to prove. Thus, we might use the data type reduction: CELL -> {j-1..j} However, note that the exception to the above reasoning is the case j = 0. In this case, the \previous" cell is cell 31. Since there's no way (yet) to write a special data type reduction for this case, we'll just include the value 31 in our data type reduction for all the cases. Thus, we write: forall(i in INDEX) forall(j in CELL) using CELL -> {j-1..j,31} prove ord_case[i][j]; Now comes the actual inductive step: we use the case cnt = i-1 to prove the case cnt = i: forall(i in INDEX} forall(j in CELL) using ord_case[i-1], inp.ordered prove ord_case[i][j]; assume inp.ordered; 45 Notice that we use the entire array ord case[i-1] (for all cells) in this veri cation. This isn't really necessary, since only the \previous cell" (j-1 or 31) is needed in any give case, but its harmless. Note that we aren't doing induction over the cell number. In fact, we can't do this, since the cells are used in a circular manner. This would result in a cycle in the proof. Now, run this example, and note the properties that appear in the properties pane. You'll observe that the property ord case[i][j] has to be proved for all the combinations of i = 0,1,2 and j = 0,1,2,30,31. The reason we have extra cases to prove for the cell index j, is that we included the maximum value 31 in the data type reduction. SMV reasons that the case j=30 might not be isomorphic to the case j=31, since we might compare j in some way with the value 31. However, as you can observe by selecting \Prop|Verify All", all of these cases can be veri ed quickly. This is because the number of state variables is small after data type reductions. Thus, we've proved that a circular bu er implementation correctly transmits an in nite sequence of bytes using a given handshake protocol. 4.8.4 Abstract variables Notice that the case of the circular bu er, we don't really have to send the byte indices, since they can be inferred from the ordering property of the interface. The data output doesn't depend on them. Thus, in the actual implementation, we would leave out the idx output of the bu er, considering it only an \auxiliary" variable used in the veri cation. This use of \auxiliary state" added to the implementation gives us a convenient way to specify interfaces as a function of abstract models. The auxiliary information tells us which object in the abstract model is currently appearing at the interface. This in turn allows us to specify what data should be appear at the interface as a function of the abstract model. In the next section, we'll see a slightly di erent way to do this. We can tell SMV that a given variable is part of the proof only, and not part of the actual implementation, by declaring it as abstract. For example, in the byte intf module, we would declare the idx component as: abstract idx : INDEX; SMV will verify for us that no actual implementation logic depends on this variable. The abstract variables can thus be excised from the implementation while retaining all the properties we've proved. 4.9 Instruction processors Up to now, when discussing re nement veri cation, we've considered only the transfer of data from one place to another, without actually operating on the data. Now we'll have a look at how to verify instruction set processors, that is, machines that input a sequence of operations to be performed on some data structure, such as a register le or a memory. In this case, our abstract model is usually an \instruction set architecture" (ISA). This is represented by a simple sequential machine the processes instructions on at a time, in the 46 order they are received. The implementation is usually a more complex machine that works on more than one instruction at a time. This can be done, for example, by pipelining, or out-of-order execution techniques. The key to veri cation of such designs in SMV is to break the problem up into individual instructions. Usually, we break an instruction up into two parts, which correspond to two lemmas int the proof. The rst lemma is that all the operands fed to the function unit(s) are correct, according to the abstract model. The second is that all results produced by the functional unit(s) are correct (again, with respect to the abstract model). Needless to say, we use lemma 1 to prove lemma 2, and vice versa. The reason for breaking the probelm into two lemmas is that the operand fetching operation and the functional unit operation are somewhat di erent in nature, so it's convenient to separate the two issuues, so we can apply a di erent proof approach to each (much as we separated the issues of data correctness and ordering in the circular bu er). Now, in order to specify that the operands and results are correct with respct to the abstract model, we usually have to add some auxiliary information to the implementation (see the previous section). In this case, we add to each instruction moving through the implementation a few extra elds to store the correct operands and results for that instruction, as computed by the abstract model. 4.9.1 A very simple example As a very simple example, let's de ne an instruction set architecture with just one instruction, performed on values in a register le. Each instruction has two source operands and a destination operand. Thus, an opcode consists of three elds { srca, srcb and dst. For simplicity, we'll make the operation addition. Here's what the ISA model might look like: scalarset REG undefined; typedef WORD array 0..31 of boolean; module main() { r : array REG of WORD; srca, srcb, dst : REG; opra, oprb, res : WORD; opra := r[srca]; oprb := r[srcb]; res := opra + oprb; next(r[dst]) := res; }We've declared a type REG to represent a register index, a type WORD to represent a data word (in this case a 32 bit word). Notice that REG is a unde end scalarset. That is, we don't say, for the moment, how many registers there are. 47 Notice, also, that we've given names to the operand values opra and oprb, and to the operation result res. It wasn't necessary to do this. That is, we could have written: next(res[dst]) := r[srca] + r[srcb]; This would have been more concise. However, it's convenient to give the intermediate quantities names, since we will use these later in writing re nement relations. Now let's implement this abstract model with a simple 3 stage pipeline, where the srt stage fecthes the operands, the second stage does the addition, and the third stage stores the result into the register le. The implementation has a reguster bypass path that forwards the results directly from later stages of pipe to the operand fetch stage. /* the implementation */ /* implementation register file */ ir : array REG of WORD; /* pipe registers */ stage1 : struct { valid : boolean; dst : REG; opra, oprb : WORD; }stage2 : struct{ valid : boolean; dst : REG; res : WORD; }/* read stage : fetch operands with bypass */ next(stage1.opra) := case{ stage1.valid & srca = stage1.dst : alu_output; stage2.valid & srca = stage2.dst : stage2.res; default : ir[srca]; }; next(stage1.oprb) := case{ stage1.valid & srcb = stage1.dst : alu_output; stage2.valid & srcb = stage2.dst : stage2.res; default : ir[srcb]; 48 }; next(stage1.dst) := dst; init(stage1.valid) := 0; next(stage1.valid) := 1; /* alu stage: add operands */ alu_output : WORD; alu_output := stage1.opra + stage1.oprb; next(stage2.res) := alu_output; next(stage2.dst) := stage1.dst; init(stage2.valid) := 0; next(stage2.valid) := stage1.valid; /* writeback stage: store result in r */ if(stage2.valid) next(ir[stage2.dst]) := stage2.res; Note that each stage has a valid bit, which says whether there is an instruction in it. Initially, these bits are zero. Now, we would like to write two re nement maps { one which de nes the correct operand values in stage1 and the other which de nes the correct result at the adder output. To do this, we add some auxiliary state information to each stage tat remembers the correct operand and result values for the given stage, as computed by the abstract model. Let's add the following component to stage1 : stage1.aux : struct{ opra, oprb, res : WORD; } Now, let's add some code to record the correct operand and result values for the rst stage: next(stage1.aux.opra) := opra; next(stage1.aux.oprb) := oprb; next(stage1.aux.res) := res; That is, we simply record the abstract model's values for opra, oprb and res. Note, this is why we gave them explicit names in the abstract model. This is all the auxiliary information we'll need to state our re nement relations. However, for e deeper pipeline, 49 we could just pass the auxiliary information down the pipe along with the instructions, as follows: next(stage2.aux) := stage1.aux; ... Now, we can state the two re nement maps in terms of the auxiliary state information. For the operands, we specify that, if stage 1 has a valid instruction, then its operands are equal to the correct operand values: layer lemma1: { if(stage1.valid) stage1.opra := stage1.aux.opra; if(stage1.valid) stage1.oprb := stage1.aux.oprb; } For the ALU results, we specify that, if stage1 has a valid instruction, then the ALU output is equal to the correct result value: layer lemma2: if(stage1.valid) alu_output := stage1.aux.res; We would like to show, of course, the correct operands imply correct results, and conversely, correct results imply correct operands. However, since we have an arbitrary number of registers to deal with, we'll need to break lemma1 into cases as a function of which register is being read. The only problem we have in doing this is that we don't know which registers were the source operands for the instruction in stage one, because our implementation does not store this information. This problem is easily solved, however, since we can store the information in our auxiliary state. So let's add two components to the auxiliary state: next(stage1.aux.srca) := srca; next(stage1.aux.srcb) := srcb; Of course, we have to remember to declare these components in our auxiliary structure (their type is REG). Now, we split the operand re nement maps into cases based on which are the actual source registers of the instruction in stage 1. For the srca operand, we have: forall(i in REG) subcase lemma1[i] of stage1.opra//lemma1 for stage1.aux.srca = i; Similarly, for srcb, we have: forall(i in REG) subcase lemma1[i] of stage1.oprb//lemma1 for stage1.aux.srcb = i; This way, we only have to consider one register at a time, so we can reduce an arbitrary number of registers to just one, for each case. Note, we don't need to do this for lemma2, the result re nement maps, since it doesn't depend on the register le. It depends only on the operands. Now we're ready to prove the various cases of our lemmas. For lemma1, we say: 50 forall(i in REG) using res//free, alu_output//lemma2 prove stage1//lemma1[i]; That is, we assume that the ALU ouput is correct, and show that (future) operands we obtain are correct. Notice that there are several paths that an ALU result might take to get back to the operand registers in stage 1. It might follow the bypass path, or it might get stored in register i. Either way, it should agree with what the abstract model gets. Notice also that the correct storage and forwarding of a result deosn't depend on what the result actually is. For this reason, we free the abstract model's result res. This eliminates the abstract model's ALU from the cone. To prove the result lemma (lemma2), we assume that operands entering the ALU are correct: using opra//free, oprb//free, stage1//lemma1 prove alu_output//lemma2; Note, in this case, we don't care what the correct operands actually are { we only care that the abstract model and the implementation agree on them (lemma1). Thus, we free opra and oprb, and eliminate the abstract model register le from the cone. This is important, since this register le is of unbounded size, and in this case we have no single register index to which we can reduce the type REG. Now, run this example. You'll notice that there are 32 instances to prove for each of stage1.opra[i]//lemma1[0] stage1.oprb[i]//lemma1[0] alu_output[i]//lemma2 where i is a bit index within a word. This is because SMV proves the re nement maps for each of the 32 bits of the data path separately. Later we'll see how to reduce this rather large number of properties. For the moment, however, select property stage1.opra[0]//lemma1[0] and try to verify it. You should get a counterexample. In this counterexemple, the initial value of r[0][0] (a bit in the abstract register le) is zero, while the initial value of ir[0]0] (the corresponding bit in the implementation register le) is one. The problem here is that the abstract model is underspeci ed. Because we have speci ed the initial state of the register le, it is nondeterministic. As a result of this, the abstract model and implementation have diverged. When there is a nondeterministic choice in an abstract model, we sometimes have to provide a \witness function" for this choice. That is, as a function of the implementation behavior, we plug in a suitable value in the abstract model. In this case, since the initial value in the speci cation is complete unde ned, we are free to plug in any value we like. So let's write the following: init(r) := ir; 51 That is, we just set the initial value of the abstract model register le to be the same as the initial value of the implementation register le. You might be wondering why we have to do this. That is, why can't SMV gure out what the correct initial value of the register le is. The answer is that it could, for any given property. However, it might use di erent intial values to prove di erent properties. As a result, even though we would have \veri ed" all the properties, there would be no single choice that makes all the properties true. Thus, for reasons of soundness, SMV requires you to x the choice once and for all, and then veri es all the properties for the particular choice you make. In any event, let's open the new version, with the witness function, and try again to verify stage1.opra[0]//lemma1[0]. You should nd that the property is true. Look in the Cone pane, and observe that it contains only 11 boolean state variables. This is bacause we are considering only registers r[0] and ir[0], and only bit 0 of the data path. We obtain only bit 0 of the data path since neither the abstract model ALU nor the implementation ALU is in the cone. The former was eliminated by freeing res, while the latter was eliminated by using lemma2 to drive the ALU output in the implementation. Now select property alu output[0]//lemma2. The cone is rather large in this case (66 state variables) because bit 0 depends in this case on all the other bits of the data path through the ALU. (This is because bit 0 is the most signi cant bit,and depeds on all the others through the carry chain). However, notice the register les are not in the cone in this case, because we have freed opra and oprb, and we have driven the implementation operand registers using lemma1. Go ahead and verify property alu output[0]//lemma2. You should nd that it checks fairy quickly in spite of the large number of state variables. This is because the ALU operation is addition, and SMV succeeds in nding an ordering of the BDD variables that maes the addition function compact. In fact, select Prop|Verify All to verify all the remaining properties. On my machine, this takes a little under eight seconds. On the other hand, if we had had a multiplier in the ALU the story would have been di erent. This is because there is no BDD variable ordering that makes this function compact. The veri cation of multipliers is beyond the scope of this tutorial. There is, however, a way of separating the problem of airthmentic veri cation from the processor veri cation problem. In this way, we can verify the processor design independent of the ALU function. Then we can plug in any ALU function we like. 4.9.2 Uninterpreted functions Suppose that instead of specifying the exact function of the ALU in our abstract model, we simply use a symbol f to denote this function. Suppose further that we use the same function symbol in our implementation, and we are able to prove a re nement relation between the two. It would then follow that the re nement holds for any concrete function we might want to plug in place of f. To represent such an uninterpreted function symbol in SMV, we simply introduce an array to represent its lookup table. For example, if we have a function f that takes two WORD arguments and produces a WORD result, we might write: forall (a in WORD) forall (b in WORD) 52 f[a][b] : WORD; or equivalently f : array WORD of array WORD of WORD; The only thing we need to know about function f is that it doesn't change over time. To declare this in SMV, we can simply write: next(f) := f; Now, to evaluate function f over two arguments a and b, we just look up the result in the table. For example: res := f[opra][oprb]; The trick here is that, without a data type reduction for type WORD, the lookup table for f will be of astronomical size. However, by case splitting, we can consider only the case when the arguments are some xed values, and the result of the function is some xed value. By doing this, we then have to consider only one element of the table for f at a time. This is a good thing, but it requires that WORD be a symmetric type (a scalarset or an ordset), so that we can reduced the very large numer of cases (here 232 232 232) to a tractable number (for example, 6). So now let's rewrite our example using an uninterpreted function symbol f for the ALU function. First, let's rede ne type WORD to be a scalarset: scalarset WORD undefined; We don't have to say what the range of the type is. Instead, we'll verify our design for any word size. Now, in the main module, let's de ne an uninterpreted function f: f : array WORD of array WORD of WORD; next(f) := f; Finally, we'll replace the ALU functions in both abstract model and implementation with function f. In the abstract model, change res := opra + oprab; to res := f[opra][oprb]; In the implementation, change alu_output := stage1.opra + stage1.oprb; to alu_output := f[stage1.opra][stage1.oprb]; 53 Now that we've modeled our problem with an uninterpreted function, we need to do a little further case splitting, so that we only have to think about a few values of WORD at a time. For the operand lemma, we'll split cases on the cirrect operand value. That is, we'll prove that the operands we obtain are correct when the correct value is some xe number j: forall(i in REG) forall(j in WORD) subcase lemma1[i][j] of stage1.opra//lemma1 for stage1.aux.srca = i & stage1.aux.opra = j; (and similarly for oprb). For the results lemma, we want to consider only one entry in the lookup table for f at a time. We'll split our result re nement map (lemma2) into cases based on the values of the two operands, and the value of function f for those two particular values. Thus for example, we might verify that the alu output signal is correct only in the particular case when opra = 0 and oprb = 1 and when f[0][1] = 2. Here is a suitable case splitting declaration: forall (a in WORD) forall(b in WORD) forall(c in WORD) subcase lemma2[a][b][c] of alu_output//lemma2 for stage1.aux.opra = a & stage1.aux.oprb = b & f[a][b] = c; Our using...prove declarations are exactly the same as before, except that they have added parameters for the additional case splits: forall(i in REG) forall(j in WORD) using res//free, alu_output//lemma2 prove stage1//lemma1[i][j]; forall (a in WORD) forall(b in WORD) forall(c in WORD) using opra//free, oprb//free, stage1//lemma1 prove alu_output//lemma2[a][b][c]; Now, open the new version. For alu output//lemma2[a][b][c], there are six cases to prove: alu_output//lemma2[0][0][0] alu_output//lemma2[1][0][0] alu_output//lemma2[2][0][0] alu_output//lemma2[0][1][0] alu_output//lemma2[1][1][0] alu_output//lemma2[2][1][0] That is, SMV generates enough cases so that we see all the possible equality relationships between a, b and c, of which there are 3 factorial. For lemma 1, we now have just one case each for opra and oprb, since there is only one parameter of type WORD. 54 Select property alu output//lemma2[0][0][0] and look at the cone. You'll notice that only one element of the lookup table for f appears in the cone: f[0][0]. This is because 0 is the only speci c valued in the reduced type WORD. (SMV automatically chose a reduction for us, including just those values that speci cally appear in the property we're proving). Verify this property. It's not surprising that the veri cation is rather fast, since there are only 5 state variables. Now select property alu output//lemma2[2][1][0]. Notice that in this case we have nine cases of f[a][b] in the cone (all the combinations of a,b = 0,1,2). This is because SMV isn't smart enough to gure out that the only element that actually matters is f[2][1]. We could, if we wanted to, include a declaration to make the remaining values unde ned: forall (a in WORD) forall(b in WORD) forall(c in WORD) using f//undefined, f[a][b] prove alu_output//lemma1[a][b][c]; This would reduce the number of state variables quite a bit, but it isn't really necessary. Even with the extraneous variables, the veri cation is quite fast, as you may observe. Finally, select Prop|Verify All to verify the remaining cases. We have now veri ed our trivial pipeline design for an arbitrary number of registers, an arbitrary word size, and an arbitrary ALU function. 4.9.3 What about outputs? Up to now, we've proved a certain relationship between the abstract model and the implementation, but we haven't really proved that the circuit observably implements its speci cation. This is because the pipeline has no outputs. We could easily, however, give the processor and output instruction (perhaps one that outputs the sum of two registers). In this case the output of our pipeline would likely appear with some delay, relative to the speci cation. This means we would need to write a re ement map for the pipeline output that delays the abstract model output by some xed amount. In this case, since the delay is nitely bounded, writing such a map is straightforward (we'll leave it as an \exercise for the reader"). If there isn't a known xed bound on the output delay, we might, for example, borrow a technique from a previous section. That is, we could attach in index to each instruction, so that we know which instruction's value is appearing at any given time at the output. We could then use induction, as before, to show that the output values appear in the correct order. In any event, in the next section, we'll see an example of a more interesting implementation, with an output. 4.10 An out-of-order instruction processor The above may have seemed like a great deal of e ort to verify such a simple design. However, we will nd that the proof becomes only incrementally more complex when we move to a much more complex implementation { an instruction processor using Tomasulo's algorithm. 55
منابع مشابه
Optimization of caseinate-coated simvastatin-zein nanoparticles: improved bioavailability and modified release characteristics
The current study focuses on utilization of the natural biocompatible polymer zein to formulate simvastatin (SMV) nanoparticles coated with caseinate, to improve solubility and hence bioavailability, and in addition, to modify SMV-release characteristics. This formulation can be utilized for oral or possible depot parenteral applications. Fifteen formulations were prepared by liquid-liquid phas...
متن کاملSoybean mosaic virus Helper Component-Protease Alters Leaf Morphology and Reduces Seed Production in Transgenic Soybean Plants.
ABSTRACT Transgenic soybean (Glycine max) plants expressing Soybean mosaic virus (SMV) helper component-protease (HC-Pro) showed altered vegetative and reproductive phenotypes and responses to SMV infection. When inoculated with SMV, transgenic plants expressing the lowest level of HC-Pro mRNA and those transformed with the vector alone initially showed mild SMV symptoms. Plants that accumulate...
متن کاملMOSAIC IN Senna occidentalis IN SOUTHERN BRAZIL INDUCED BY A NEW STRAIN OF Soybean mosaic virus
Plants of Senna occidentalis (sin. Cassia occidentalis) with mosaic symptoms were collected near a soybean (Glycine max) field where some plants exhibited symptoms of mosaic and blistering. A preliminary examination of leaf tissue from diseased S. occidentalis by electron microscopy revealed the presence of pinwheel inclusions as well as long flexuous particles, indicating the presence of a pot...
متن کاملAsian EUS Cup-06: Diagnosis and treatment of recurrent upper gastrointestinal bleed with endoscopic ultrasound (gastric varix with splenic artery aneurysm)
S43 ENDOSCOPIC ULTRASOUND / VOLUME 6 / SUPPLEMENT 1 / AUGUST 2017 Asian EUS Cup-04 Endoscopic ultrasound-guided continuous catheter thrombolysis of portal venous system Malay Sharma, Piyush Somani, Saurabh Jindal Jaswant Rai Speciality Hospital, Meerut, Uttar Pradesh, India Background: Acute portal vein thrombosis is an uncommon and insidious disease that is potentially lethal due to delay in d...
متن کاملAsian EUS Cup-07: Endoscopic ultrasound-guided pancreatic duct stenting with a new plastic stent for nondilated pancreatic duct in pancreatic fistula after pancreaticoduodenectomy
S43 ENDOSCOPIC ULTRASOUND / VOLUME 6 / SUPPLEMENT 1 / AUGUST 2017 Asian EUS Cup-04 Endoscopic ultrasound-guided continuous catheter thrombolysis of portal venous system Malay Sharma, Piyush Somani, Saurabh Jindal Jaswant Rai Speciality Hospital, Meerut, Uttar Pradesh, India Background: Acute portal vein thrombosis is an uncommon and insidious disease that is potentially lethal due to delay in d...
متن کاملAsian EUS Cup-05: Successful management of peripancreatic tumors by endoscopic ultrasound-guided radiofrequency ablation
S43 ENDOSCOPIC ULTRASOUND / VOLUME 6 / SUPPLEMENT 1 / AUGUST 2017 Asian EUS Cup-04 Endoscopic ultrasound-guided continuous catheter thrombolysis of portal venous system Malay Sharma, Piyush Somani, Saurabh Jindal Jaswant Rai Speciality Hospital, Meerut, Uttar Pradesh, India Background: Acute portal vein thrombosis is an uncommon and insidious disease that is potentially lethal due to delay in d...
متن کامل