ivy/doc/language.md

44 KiB

layout title
page The IVy language

IVy's language is designed to let you to specify and implement systems in a way that is both convenient and conducive to automated verification. Technically, this means that important verification problems like invariant checking and bounded model checking fall within a decidable fragment of first-order logic.

For this reason, the IVy language has certain unusual features. IVy divides the world into three categories of things:

  • Data values,
  • Function values and
  • Procedures.

Data values are the kinds of things that can be printed out, stored in files, transmitted over the internet and so on. These are sometimes refered to as POD types for "plain old data". Function values are pure mathematical functions. They can be evaluated on arguments to produce deterministic results, and they have no side effects. In other words, evaluating a function does not modify the value of any variable. On the other hand, a procedure, when called, has an effect. In addition to returning values, it may also modify the values of variables.

Data values and function values can be stored in variables and passed as arguments. That is, data and functions are "first class" values. On the other hand, procedures cannot be stored in variables or passed as arguments.

A particularly unusual aspect of the IVy langauge is that there are no references. Two distinct variables are never "aliases" for the same underlying "object". Modifying variable a never has an effect on the value of variable b. IVy's philosphy is that references are a low-level optimization that should be used by compilers to avoid copying, but should never appear in high-level programs. The absense of aliases enormously simplifies the analysis, testing and verification of programs.

Another unusual aspect of the IVy language is that it is synchronous. This means that:

  • All actions occur in reaction to input from the environment, and
  • all actions are isolated, that is, they appear to occur instantantaneously, with no interruption.

This aspect of IVy's semantics greatly simplifies reasoning about concurrent and distributed systems.

We will now consider the basic elements of an IVy program.

The language specifier

Every IVy file begins with a line like the following:

#lang ivy1.5

This tells IVy what version of the language we are using.

State and actions

An IVy program describes objects that have state variables and provide actions that operate on state. State variables may hold either plain old data or mathematical relations and functions (much as in the Alloy language, but with important differences that we will discuss later).

Types and declarations

Suppose we have a network consisting of nodes, with pair-wise links between the nodes. We can model the structure of the network with a relation link like this:

type node

relation link(X:node,Y:node)

This says that node is a POD type, but tells us nothing yet about how values of type node are represented. At this point, we say that node is an uninterpreted type. Further, we declared that link is a set of pairs (X,Y) where X and Y are nodes.

In IVy, as in Prolog, identifiers beginning with capital letters are logical variables, or place-holders. These are not to be confused with program variables, which hold the program state. The colons introduce type annotations. In the above declaration, the variables X and Y are just taking up space, telling us what sort of relation link is (that is, for every X and Y of type node, link(X,Y) is a Boolean value.

We could equivalently have written the relation declaration as:

function link(X:node,Y:node) : bool

Either way, we create a variable link that holds a function value, in particular a function from pairs of node to bool.

As another example, here is a declaration of a function that gives an ID to each node:

type id

function node_id(X:node) : id

A variable that holds just a node value can be declared like this:

individual root:node

Enumerated types

The type node declared above is an uninterpreted type. This means it can be any set with at least one element. Often it is useful to define a type in extension, that is, by enumerating its elements explicitly. An example of an enumerated type in IVy would be:

type color = {red,green,blue}

This declares a type with exactly three distinct values, and also declares individuals red, green and blue as its elements.

Actions

An action in IVy mutates the values of state variables. For example, here is a declaration of an action that adds a link between two nodes:

action connect(x:node, y:node) = {
    link(x,y) := true
}

The action connect is defined in terms of one of IVy's primitive actions: an assignment. An assignment modifies the value of a variable. In this case, the single pair (x,y) is added to the relation link (or put another way, the value of the expression link(x,y) is set to true, without otherwise modifying link). Because there is no aliasing in IVy. the values of all other variables remain unchanged by the assignment.

We can use place-holders to make larger modifications to a relation. For example, here is an action that removes all links from a given node x:

action clear(x:node) = {
    link(x,Y) := false
}

The effect of the assignment with variable Y is to simultaneously assign link(x,Y) for all nodes Y. We don't have to give a type annotation for Y because it can be inferred from context.

We can be a little more selective by giving a Boolean expression in the assignment. For example, this action removes links from x to all nodes in the set failed:

action clear_failed(x:node) = {
    link(x,Y) := link(x,Y) & ~failed(Y)
}

If there are no parameters, we don't use parentheses. For example:

action flip = {
    bit := ~bit
}

In fact, you will never see a pair of empty parentheses in IVY. An action can also have return parameters. For example:

action swap(a:t,b:y) returns (c:t,d:t) = {
    c := b;
    d := t
}

Control

Sequential execution

We can execute two actions sequentially by separating them with semicolon. For example, this action removes all links from x, then links x to y:

action connect_unique(x:node, y:node) = {
    link(x,Y) := false;
    link(x,y) := true
}

The semicolon is a sequential composition operator in IVy, not a statement terminator. However, an extra semicolon is allowed before an ending brace } to make editing sequences of statements easier. In this case, we could have written the sequence of two assignments equivalently as:

link(x,Y) := Y = y

Calling actions

We could also have written connect_unique by calling clear and connect:

action connect_unique(a:node, b:node) = {
    call clear(a);
    call connect(a,b)
}

Since there are no references in IVy, so in effect, IVy uses the call-by-value convention. That is, when we call clear(a) a local variable x is created during the execution of clear and assigned the value a. This means that, as in the C programming language, modifying the value of x in clear would not result in modifying the value of a in connect_unique.

The return values of an action can be obtained like this:

call x,y := swap(x,y)

An action with a single return value can be called within an expression. For example, if sqrt is an action, then:

x := y + sqrt(z)

is equivalent to

call temp := sqrt(z)
x := y + temp

If there is more than call within an expression, the calls are executed in left-to-right order.

Parentheses are not used when calling an action with no parameters. For example, if we have:

action next returns (val:t) = {
    current := current + 1;
    val := current
}

then we would write:

x := y + next

The lack of paretheses introduces no ambiguity, since the action next is not a value and cannot itself be passed as an argument to the function +. An advantage of this convetion is that we don't have to remember whether next is an action or a variable, and we can easily replace a variable by an action without modifying all references to the variable.

Conditionals

Conditionals in IVy are much as in any procedural programming language. For example, the following code clears the incoming links to node y if y is in the failed set:

if failed(y) {
    link(X,y) := false
}

The curly brackets around the assignment are required. No parentheses are need around the condition. A conditional can have an associated else clause, for example:

if failed(y) {
    link(X,y) := false
} else {
    link(y,z) := true
}

As in the C programming language, the "else" is associated to the nearest "if".

The following syntax can be used to find a element of a type that satisfies some condition:

if some x:t. f(x) = y {
    z := x + y
}
else {
    z := y
}        

Here, if there is any value x of type t such that f(x) = y, then such a value is assigned to x and the assignment z := x + y is executed. If there is more than one such value, the choice is non-deterministic. If there is no such value, the else clause is executed. The symbol x is only in scope in the if clause. It acts like a local variable and is distinct from any x declared in an outer scope.

It is also possible to choose a value of x minimizing some function of x. For example, we can find an element of a set s with the least key like this:

if some x:t. s(x) minimizing key(x) {
   ...
}

This is logically equivalent to the following:

if some x:t. s(x) & ~exists Y. s(Y) & key(Y) < key(x) {
   ...
}

Besides being more concise, the minimizing syntax can be more efficiently compiled and is easier for Ivy to reason about (see the decidability discussion). The keyword maximizing produces the same result with the direction of < reversed.

Loops

Loops are discouraged IVy. Often, the effect of a loop can be described using an assignment or an if some conditional.

For example, instead of something like this:

for y in type {
    link(x,y) := false
}

we can instead write this:

link(x,Y) := false

When a loop is needed, it can be written like this:

sum := 0;
i := x;
while i > 0
{
    sum := sum + f(i);
i := i - 1
}

This loop computes the sum of f(i) for i in the range (0,x]. A loop can be decorated with a invariants, like this:

while i > 0
invariant sum >= 0
{
    sum := sum + f(i);
i := i - 1
}

The invariant sum >= 0 is a special assertion that is applied on each loop iteration, before the evaluation of the condition. Invariants are helpful in proving properties of programs with loops.

Non-deterministic choice

The asterisk "*" can be used to represent non-deterministic choice in IVy in two situations. First, on the right-hand side of an assignment:

x := *

This cause x to be assigned non-deterministically to any value of its type. We can use variables in non-deterministic assignments, so for example:

link(x,Y) := *

causes x to be linked to an arbitrary set of nodes.

Further, we can use an asterisk in a conditional to create a non-deterministic branch:

if * {
    link(x,y) := true
} else {
    link(x,z) := true
}

Non-deterministic choice also occurs when we create a local variable (see below). On entry to an action the values of return parameters are non-deterministically chosen.

Expressions

Expressions in IVy are terms or formulas in first-order logic with equality. IVy provides the following built-in operators: ~ (not), & (and), | (or), -> (implies), <-> (iff) and = (equality). The equality operator binds most strongly, followed by not, and, iff, implies.

Expressions may also use logical quantifiers. For example, this formula says that there exists a node X such that for every node Y, X is linked to Y:

exists X. forall Y. link(X,Y)

In this case, the variables do not need type annotations, since we can infer that both X and Y are nodes. However, in some cases, annotations are needed. For example, this is a statement of the transitivity of equality:

forall X,Y,Z. X=Y & Y=Z -> X=Y

We can determine from this expression that X, Y and Z must all be of the same type, but not what that type is. This means we have to annotate at least one variable, like this:

forall X:node,Y,Z. X=Y & Y=Z -> X=Y

Assume, assert and init

The primitive actions assume and assert allow us to write specifications. The assert action fails if the associated condition is false. For example, suppose we wish the connect action to handle only the case where the node y is not in the failed set. We could write

action connect(x:node, y:node) = {
    assert ~failed(y);
    link(x,y) := true
}

If the condition ~failed(y) is true, control passes through the assert and this action behaves in the same way as the original. If the condition ~failed(y) is false, however, the semantics of assert is undefined. This means that whenever we use connect we must prove that the y argument is not in the failed set.

On the other hand, the assume action does not allow control to pass through if the associated condition is false. A typical application of assume in modeling a protocol is to implement a guarded command. For example, this action non-deterministically chooses a non-failed node and connects x to it:

action connect_non_failed(x:node) = {
    y := *;
    assume ~failed(y);
    link(x,y) := true
}

Of course, if all the nodes are failed, this action cannot terminate. There is some degree of risk in using assumptions when modeling, since assumptions can eliminate behaviors in unexpected ways.

In assert and assume actions, any free variables are treated as universally quantified. For example, if we want to connect x to a node that is not currently linked to any node, we could change the assumption above to

 assume ~link(y,Z)

Normally, we expect a system to start in some well-defined state, or at least for some specified conditions to hold initially. In IVy, we use an init declaration for this purpose. For example:

init ~link(X,Y)

This says that initially, no two nodes are linked. As in assume and assert, unbound variables are universal.

Local variables

The above example of a guarded command action assumes that y is a declared component of type node. We can also declare y locally, however, like this:

action connect_non_failed(x:node) = {
    local y:node {
        y := *;
        assume ~failed(y);
        link(x,y) := true
    }
}

This creates a fresh y that is in scope only within the 'local' declaration. In fact, we don't need the non-deterministic assignment to y since the value of y is already non-deterministic at the beginning of the local action.

Modelling interleaving concurrency in IVy

Actions in an IVy program execute only in response to calls from the program's environment. IVy makes the synchronous hypothesis: when the environment calls an action, it waits for the action to complete before issuing another call. Put another way, IVy actions appear to execute in zero time. At first blush, it might seem that this eliminates the possiblity of concurrency. In fact, the synchronous hypothesis is intended to make the implementation of concurrent and distributed systems simpler. The key idea is that only the appearance of synchronicity is required. In practice actions can execute concurrently, provided that to an outside observer they appear to have executed sequentially.

For now, we will leave aside the question of how to enforce the synchronous hypothesis in practice. Instead, we will consider how to use the synchronous IVY language to model a distributed protocol at an abstract level using interleaving concurrency. In an interleaving model, processes take turns executing actions in isolation (that is, in apparently zero time) in a non-deterministic order.

An IVy program exports a set of actions to its environment. Each of these actions can be used to model a single isolated step of a process. Since the environment is allowed to call these actions in an arbitrary order, the IVy program can be used to model arbitrary interleaving of process actions.

An abstract interface model

The following is a very abstract model of an interface that establishes connections between clients and servers. Each server has a semaphore that is used to guarantee that at any time at most one client can be connected to the server.

#lang ivy1.5

type client
type server

relation link(X:client, Y:server)
relation semaphore(X:server)

init semaphore(W) & ~link(X,Y)

action connect(x:client,y:server) = {
  assume semaphore(y);
  link(x,y) := true;
  semaphore(y) := false
}

action disconnect(x:client,y:server) = {
  assume link(x,y);
  link(x,y) := false;
  semaphore(y) := true
}

export connect
export disconnect

This program declares two types client and server. The state of the protocol model consists of two relations. The relation link tells us which clients are connected to which servers, while semaphore tells us which servers have their semaphore "up". The link and semaphore components aren't "real". They are abstractions that represent the interface user's view of the system.

The program exports two actions to the environment: connect and disconnect. The connect actions creates a link from client x to server y, putting the server's semaphore down. Notice that connect assumes the server's semaphore is initially up. The disconnect action removes a link and puts the semaphore up. The two export declarations at the end tell us that the environment may call connect and disconnect in arbitrary sequence, though it must obey the stated assumptions.

Safety and invariant conjectures

A program is safe if the environment cannot call it in any way that causes an assertion to be false. There are various way to use assertions to specify desired safety properties of a program. A simple one is to add a test action that asserts some property of the program state. In the client/server example above, we might specify that no two distinct clients can be connected to a single server using the following test action:

action test = {
  assert ~(X ~= Z & link(X,Y) & link(Z,Y))
}

export test

The assertion is implicitly universally quantified over (distinct) clients X and Z and server Y. To help IVy to prove that this assertion always holds, we can suggest facts that might be useful in constructing an inductive invariant. For example:

conjecture X = Z | ~link(X,Y) | ~link(Z,Y)
conjecture link(X,Y) -> ~semaphore(Y)

Here, we state that no two clients are connected to the same server (which is just the property we want to prove) and additionally that when a client is connected to a server, its semaphore is down. These facts are inductive in the sense that they are initially true, and each of our three actions preserves them. Moreover, they are sufficient to guarantee that our test assertion is true. Thus, IVy can use these conjectures to prove safety of the program.

While we can give IVy conjectured invariants, there is no way outside of an action to assert that a proposition is invariant. This is to avoid ambiguity as to exactly when an invariant should be established. In IVy we can only state that a formula is true at a specific point in the execution of an action.

Axioms and background theories

The built-in types and operators provided by IVy are fairly impoverished. We have only uninterpreted types, enumerated types and the basic operators of first-order logic. This is by design. By introducing richer data types, or theories, we would quickly make our verification problems undecidable, meaning we would sacrifice reliability of automated verification. In practice, before introducing, say, the integers into a model, we should make sure that the power of the integers is really needed. It may be, for example, that all we require is a totally ordered set.

IVy allows us to introduce background theories in the form of logical axioms. This in turn allows us to avoid using unnecessarily powerful theories. As an example, consider defining an ordering relation over our node ID's:

relation (I:id < J:id)

This is an example of an infix symbol. The symbol < is no different than other relational symbols, except that IVy pre-defines it as having infix syntax.

We can ensure that < is a total order by writing axioms:

axiom X:id < Y & Y < Z -> X < Z
axiom ~(X:id < X)
axiom X:id < Y | X = Y | Y < X

These axioms say, respectively, that < is [transitive][tr], [anti-symmetric][as] and [total][to]. As in other cases, the free variables are universally quantified. You may also notice that we annotated the variable X with its type. This has to do with the fact that < is polymorphic, an issue we will deal with shortly.

Of course, axioms are assumptions and assumptions are dangerous. We want to make sure that our axioms are consistent, that is, that they have at least one [model][mo]. The IVy tool can be helpful in determining this.

Polymorphic operators.

In IVy the equality operator is polymorphic in the sense that it applies to any pair of arguments so long as they are of the same type. One way to think of this is that there is really a distinct equality operator pre-declared for each type, but that we use = as a short-hand for all of them. It is useful to be able to declare other such polymorphic operators to avoid, for example, having to invent a new "less than" symbol for every ordered type, or adding type annotations to operators.

IVy provides for this in a limited way. Certain symbols, such as < and + are always polymorphic. This allows us to declare relations with the same symbol over different sorts and to disambiguate them based on type inference.

To make type inference stronger, the polymorphic operators also come with type constraints. In functional language terms, < has type alpha * alpha -> bool and + has type alpha * alpha -> alpha.

Numerals

Numerals are a special case of polymorphic symbols. A numeral is any symbol beginning with a digit, for example 0, or 0xdeadbeef. The types of numerals are inferred from context. For example, if x has type foo, then in the expression x+1, the numeral 1 is inferred to have type foo.

Numerals are special symbols in the sense that they do not have to be explicitly declared. However, IVy gives them no special interpretation. IVy does not even assume that distinct numerals have distinct values. This means that 0 = 2 is not necessarily false. In fact, this equation might be true in a type representing the integers mod 2.

Section [Interpretations] describes how to give concrete interpretations to IVy types, so that symbols like + and 0 have specific meanings.

Quoted symbols

A quoted symbols a possibly-empty sequence of characters enclosed in double quote characters (and not containing a double quote character). An example would be "ab$c". Quoted symbols are similar to numerals: their type is inferred from context.

Modules

A module in IVy is a group of declarations that can be instantiated. In this way it is similar to a template class in an object-oriented programming language. Besides defining classes of objects, modules can be used to capture a re-usable theory, or structure a modular proof.

Here is a simple example of a module representing an up/down counter with a test for zero:

module counter(t) = {

    individual val : t
    init val = 0

    action up = {
        val := val + 1
    }

    action down = {
        val := val - 1
    }

    action is_zero returns(z : bool) = {
        z := (val = 0)
    }
}

This module takes a single parameter t which is the type of the counter value val. We can create an instance of the module like this:

 type foo

 instance c : counter(foo)

This creates an object c with members c.val, c.up, c.down and c.is_zero.

Any IVy declaration can be contained in a module. This includes axioms, conjectures, instances and modules. As an example, here is a module representing a theory of partial orders:

module po(t,lt) = {
    axiom lt(X:t,Y) & lt(Y,Z) -> lt(X,Z)
    axiom ~(lt(X:t,Y) & lt(Y,X))
}

This module takes a type t and a relation lt over t. It provides axioms stating that lt is transitive and antisymmetric. We might instantiate it like this:

type foo

instantiate po(foo,<)

Since we didn't give an object name, the members of po are created within the current object (which in this case is the global object). Notice that we passed the polymorphic infix symbol < as a parameter. Any symbol representing a type, function, relation, action or object can be passed as a module parameter.

Like a class in an object-oriented programming language, a module can contain references to symbols declared outside the module. However, a declaration inside the module takes precedence. For example, consider this code:

relation r,s

module m = {
    relation r
    axiom s -> r
}

instance c : m

The axiom in c is equivalent to:

axiom s -> c.r

That is, the local declaration of r shadows the global one.

Singleton objects

We can create a module with just one instance like this:

object foo = {
    relation bit
    init ~bit
    action flip = {
        bit := ~bit
    }
}

This creates a single object foo with members foo.bit and foo.flip, exactly as if we had created a module and instantiated it once.

Parameterized objects

An array of instances of the same module can be created like this:

type foo
type bar

instance c(X:bar) : counter(foo)

This creates one instance c(X) of module counter for for every element of type bar. Since we haven't said how many elements there are in type bar, we have effectively created a collection of objects of arbitrary but fixed size.

If x is of type bar, we can treat c(x) as we would any object, for example:

call c(x).down;
if c(x).is_zero {
    call c(x).up
}

The parameter X can also be passed to the module being instantiated. This is useful to create a collection of objects with unique identifiers. For example:

type id_t

module thing(id) = {
   action my_id returns (x:id_t) = {
        x := id
   }
}

instance c(X:id_t) : thing(X)

In this case, calling c(id).my_id will return id.

An alternative way to write the above would be:

type id_t

object c(id:id_t) = {
   action my_id returns (x:id_t) = {
        x := id
   }
}

Notice that the object parameter is given as a logical constant rather than a place-holder. This constant can be referred to in the body of the object.

Types in IVy are never parameterized. For example, if we write:

object foo(self:t) = {
    type t
}

this creates a single type called foo.t, not a collection of types f.t(self) for all values of self.

Monitors

While embedding assertions in code is a useful way to write specifications, there are good reasons to separate a specification from the object being specified. For example, this allows you to re-use specifications, to construct modular proofs and to refine specifications into implementations.

The IVy language supports these goals using monitors. A monitor is an ordinary object, except that its actions are synchronized with the actions of other objects. For example, suppose that in the client/server example above, we want to specify that callers to connect do not request a connection to a server whose semaphore is down. We could express this property as a monitor like this:

object mon = {
    action pre_connect(x:client,y:server) = {
        assert semaphore(y)
    }        
    execute pre_connect before connect
}

The execute declaration says that whenever connect is called, action pre_connect should be executed first, with the same parameters. If any caller tries to connect a client to a busy server, the assertion will fail.

Monitors can also check the return values of actions. For example:

action post_incr(inp:t) returns(out:t) = {
    assert inp < out
}

execute post_incr after incr

Here, we have an action incr that is supposed to increment a value, and we specify that the output must be greater than the input.

As a shorthand, we can write out monitor action like this:

after c.post(inp:t) returns(out:t) {
    assert inp < out
}

This creates a monitor action called post.after that is executed after c.post. Similarly, we can write:

before pre_connect(x:client,y:server) {
    assert semaphore(y)
}        

If we drop the input or output parameters, they are inherited from the monitored action. For example:

after c.post {
    assert inp < out
}

This is a useful shorthand when the declaration of c.post is nearby, but should probably be avoided otherwise.

Monitor state

Usually, monitors contain state components that allow them to remember some of the history of past events. For example, here is a monitor specifying a property of counter objects. It requires that immediately after a call to up, the is_zero action cannot return true:

module counter_prop(c) = {

    relation was_up
    init ~was_up 

    after c.up {
        was_up := true
    }

    after c.down {
        was_up := false
    }

    after c.is_zero returns (z:bool) {
        assert was_up -> ~z
    }
}

This module is parameterized on c, the counter being specified. This makes the specification re-usable. It has a single state component was_up that remembers whether the last counting action was up. This is accomplished by the actions up and down that synchronize with actions of counter c (in this case it doesn't make any difference whether it is before or after). The action is_zero executes after calls for the counter's is_zero action and asserts a fact about the return value: if the last action was up the result cannot be true.

Assume/guarantee reasoning

IVy doesn't require us to prove all at once that a program is safe. Instead, we can break the proof down into smaller proofs using the assume/guarantee approach.

For example, suppose we have the following program with two objects:

#lang ivy1.5

type nat

relation even(N:nat), odd(N:nat)
axiom even(0) & (even(N) -> odd(N+1))
axiom odd(1) & (odd(N) -> even(N+1))

object evens = {
    individual number : nat
    init nat = 0

    action step = {
        call odds.put(number + 1)
    }

    action put(n:nat) = {
        number := n;
    }
}

object odds = {
    individual number : nat
    init nat = 1

    action step = {
        call evens.put(number + 1)
    }

    action put(n:nat) = {
        number := n;
    }
}

export evens.step
export odds.step

Each object stores a number and sends this number plus one to the other object when its step action is called by the environment. We want to prove that even.put only receives even numbers, while odd.put only receives odd numbers. Here is a specification of this property as a monitor:

object spec = {

    before even.put {
        assert even(n)
    }

    before odd.put = {
        assert odd(n)
    }
}

We would like to break the proof that these two assertions always hold into two parts. In the first part, we assume the object evens gets correct inputs and prove that it always produces correct outputs. In the second part, we assume the object odds gets correct inputs and prove that it always produces correct outputs.

This argument seems circular on the surface. It isn't, though, because when we prove one of the assertion holds, we are only assuming that the other assertions has always held in the past. So what we're really proving is that neither of the two objects is the first to break the rules, and so the rules always hold.

IVy calls the two parts of the proof isolates. They are declared like this:

isolate iso_even = evens with spec
isolate iso_odd = odds with spec

In the first isolate, we prove the assertion that evens guarantees. We do this in context of the spec object, but we forget about the state of the odds object. What this means is that in isolate iso_even, the assert statement in even_put becomes an assume statement.

The result is as if we had actually entered the following program:

#lang ivy1.5

type nat
function (X:nat + Y:nat) := nat

relation even(N:nat), odd(N:nat)
axiom even(0) & (even(N) -> odd(N+1))
axiom odd(1) & (odd(N) -> even(N+1))

object evens = {
    individual number : nat
    init nat = 0

    action step = {
        call odds.put(number + 1)
    }

    action put(n:nat) = {
        assume even(nat)
        number := n;
    }
}

object odds = {

    action put(n:nat) = {
        assert odd(nat)
    }
}

export evens.step
export evens.put

In isolate iso_even, the odds object acts like part of the environment. The side effect of odds.put has been eliminated, and what remains is just the assertion that the input value is odd (IVy has to verify that this side effect is in fact invisible to evens). The assertion that inputs to evens are even has become as assumption. We can prove this isolate is safe by showing that even.number is invariantly even, which means that odds.put is always called with an odd number.

The other isolate, iso_odd looks like this:

#lang ivy1.5

type nat
function (X:nat + Y:nat) := nat

relation even(N:nat), odd(N:nat)
axiom even(0) & (even(N) -> odd(N+1))
axiom odd(1) & (odd(N) -> even(N+1))

object evens = {

    action put(n:nat) = {
        assert even(nat)
    }
}

object odds = {
    individual number : nat
    init nat = 1

    action step = {
        call evens.put(number + 1)
    }

    action put(n:nat) = {
        assume odd(nat)
        number := n;
    }
}

export odds.step
export odds.put

If both of these isolates are safe, then we know that neither assertion in spec is the first to fail, so the original program is safe.

IVy has default rules for determining, for each object, which assertions are assumptions and which assertions are guarantees.

The assumptions are:

  • Monitor assertions executed before the object's actions
  • Monitor assertions executed after the object's calls

The guarantees are:

  • Assertions within the object's actions
  • Monitor assertions executed after the object's actions
  • Monitor assertions executed before the object's calls

Roughly speaking, this means that assertions about an object's inputs are assumptions for that object, while assertions about its outputs are guarantees.

Action implementations

Often it is useful to separate the declaration of an action from its implementation. We can declare an action like this:

action incr(x:t) returns(y:t)

and then give its implementation like this:

implement incr {
    y := x + 1
}

This is useful for defining and specifying interfaces. For example, we could define the interface between "evens" and "odds" like this:

object intf = {

    action put_even(n:nat)
    action put_odd(n:nat)

    object spec = {

        before put_even {
            assert even(n)
        }

        before put_odd = {
            assert odd(n)
        }
    }
}

The even object would then be:

object evens = {
    individual number : nat
    init nat = 0

    action step = {
        call intf.put_odd(number + 1)
    }

    implement intf.even_put(n:nat) = {
        number := n;
    }
}

In this way, we can make the interface specification into a re-usable component.

Initializers

In some cases it is awkward to give the initial condition as a formula. An alternative is to use an initializer. This is a special action that is executed once initially, before any exported actions are called. For example:

individual bit : bool

after init {
    bit := false
}

This behaves like a monitor after a special internal action called init. As the after implies, the initial condition prescribed by the init declarations is assumed to be established before any initializers are called. Initializers are executed in the order they are declared.

Initializers may call other actions. For example, suppose we have a module collection representing a set of objects that is initially empty. If we wanted a set that initially contained the value zero, we could use an initializer like this:

type t

object foo = {
    instance myset : collection(t)

    after init {
        call myset.add(0)
    }
}

This action is called exactly once after myset is initialized.

Parameterized objects can also have initializers. For example, we may want to have a collection of objects that each contain a bit, where initially only the bit of object 0 is true:

type t

object foo(self:t) = {
    individual bit : bool

    after init {
        bit := (self = 0)
    }
}

There are some restrictions in initializers of parameterized objects, however. These are:

  • Conditions of if statements may not refer to the parameter, and

  • In assignments, the left-hand side must contain the parameter if the right-hand side does.

For example, this initializer would not be legal:

type t

individual bit : bool

object foo(self:t) = {
    after init {
        bit := (self = 0)
    }
}

This is because the component bit being assigned is not parameterized. This means it is in effect being assigned a different value for each value of self. The restrictions guarantee that the result of the initializer does not depend on the order in which it is called for different parameter values.

Definitions

We can define the value of a previously declared function like this:

function square(X:t):t

definition square(X) = X * X

Notice we don't have to give the type of X in the definition, since it can be inferred from the type of square. Logically, the definition is equivalence to writing:

axiom square(X) = X * X

However, definitions have several advantages. Primarily, they are safer, since definitions are guaranteed to be consistent. In addition they can be computed. If we use an axiom, the only way that Ivy can compile the function square is to compile a table of squares. On the other hand, IVy can compile the definition of square into a procedure.

IVy doesn't (currently) allow recursive definitions. So, for example, this is not allowed:

definition factorial(X) = X * factorial(X-1) if X > 0 else 1

Macro definitions

A macro is a definition that is only "unfolded" when it is used. For example, let's say we want to define a predicate rng that is true of all the elements in range of function f. We could write it like this:

definition rng(X) = exists Y. f(X) = Y

The corresponding axiom might be problematic, however. Writing it out with explicit quantifiers, we have:

axiom forall X. (rng(X) <-> exists Y. f(X) = Y)

This formula has an alternation of quantifiers that might result in verification conditions that IVy can't decide (see the decidability discussion). Suppose though, that we only need to know the truth value of rng for some specific arguments. We can instead write the definition like this:

definition rng(x:t) = exists Y. f(x) = Y

Notice that the argument of rng is a constant x, not a place-holder X. This definition acts like a macro (or an axiom schema) that can be instantiated for specific values of x. So, for example, if we have an assertion to prove like this:

assert r(y)

Ivy will instantiate the definition like this:

axiom rng(y) <-> exists Y. f(y) = Y

In fact, all instances if the macro will be alternation-free, since IVy guarantees to instantiate the macro using only ground terms for the constant arguments. A macro can have both variables and constants as arguments. For example, consider this definition:

definition g(x,Y) = x < Y & exists Z. Z < x

Given a term g(f(a),b), Ivy will instantiate this macro as:

axiom g(f(a),Y) = f(a) < Y & exists Z. Z < f(a)

Choice functions

Suppose we want to define a function finv that is the inverse of function f. We can write the definition like this:

definition finv(X) = some Y. f(Y) = X in Y

This special form of definition says that finv(X) is Y for some Y such that f(Y) = X. If there is no such Y, finv(X) is left undefined. The corresponding axiom is:

axiom forall X. ((exists Y. f(X) = Y) -> f(finv(Y)) = Y)

With this definition, finv is a function, but it isn't fully specified. If a given element Y has two inverses, finv will yield one of them. This isn't a non-deterministic choice, however. Since f is a function, it will always yield the same inverse of any given value Y.

If we want to specify the value of finv in case there is no inverse, we can write the definition like this:

definition finv(X) = some Y. f(Y) = X in Y else 0

The else part gives us this additional axiom:

axiom forall X. ((~exists Y. f(Y) = X) -> finv(X) = 0)

Notice that this axiom contains a quantifier alternation. If this is a problem, we could use a macro instead:

definition finv(x) = some Y. f(Y) = x in Y else 0

The axiom we get is

axiom (~exists Y. f(Y) = x) -> finv(x) = 0)

which is alternation-free.

Interpreted types and theories

The normal way of using IVy is to declare uninterpreted types and to give the necessary axioms over those types to prove desired properties of a system. However, it is also possible in IVy to associate types with sorts that are interpreted in the underlying theorem prover.

For example:

type idx

interpret idx -> int

This says that IVy type idx should be interpreted using sort int of the theorem prover. This does not mean that idx is equated with the integers. If we also interpret type num with int, we still cannot compare values of type idx and type num. In effect, these two types are treated as distinct copies of the integers.

When we declare idx as int, certain polymorphic functions and relations on idx are also automatically interpreted by the corresponding operators on integers, as are numerals of that type. So, for example, + is interpreted as addition and < as 'less than' in the theory of integers. These operators still must be declared to be used, however. Numerals are given their normal interpretations in the theory, so 0:idx = 1:idx would be false.

Concrete sorts that are currently available for interpreting IVy types are:

  • int: the integers
  • {X..Y}: the subrange of integers from X to Y inclusive
  • {a,b,c}: an enumerated type
  • bv[N]: bit vectors of length N, where N > 0

An arbitrary function or relation symbol can be interpreted. This is useful for symbols of the theory that have no pre-defined polymorphic symbol in IVy. For example:

type t
type s
function extract_lo(X:t) : s

interpret t -> bv[8]
interpret s -> bv[4]
interpret extract_lo -> bfe[3][0]

Here bfe[3][0] is the bit field extraction operator the takes the low order 4 bits of a bit vector.