Module system

Alloy provides a simple module system to promote the modularisation and reusability of models. In other chapters we’ve already seen how to use some of the utility modules that come packaged with the Analyzer. This chapter explores how to create new ones.

Declaring simple modules

To create a new module in Alloy you just need to create a new .als file and add it to the same directory as the main model, possibly inside other sub-directories. You can declare the module name using the module keyword, but note that only the file name is used to search for modules to be included. To include a module in another model, we must to use an open instruction with the path to the corresponding file (without the .als extension). The module and open instructions must appear at the top of the model, before any other declaration. After a module is opened all its definitions are included in the main module, and you can refer to them by their identifier directly, or use its full qualified name using a / separator. In particular, if the Analyzer is not able to disambiguate an identifier, you may use the qualified name to disambiguate. If this becomes cumbersome, you can open a module with an alias using the keyword as.

Let us get back to the file system example from the Structural modeling chapter. The constrains we have imposed on the file system mean that it is actually a directed acyclic graph (DAG) where nodes are objects. Additionally, it is a rooted graph, since there is a single node without a parent (Root). This shape is enforced by the combination of facts no_indirect_containment and no_dangling_objects in the original file system model, but since these are common constraints on graph-like structures, it makes sense to provide them in a separate module to promote reusability.

Let us create a new Alloy model file and name it graph.als. A binary relation over a set of nodes defines a DAG if it is acyclic. So we can just declare the following predicate, that checks this property for a given set of nodes and a binary adjacency relation over them.

pred dag [node : set univ, r : node -> node] {
  all n : node | n not in n.^r
}

On the other hand, a graph has a root if all other nodes are reachable from that node, a constraint that can be encoded as follows.

pred rootedAt [node : set univ, r : node -> node, root : node] {
  node in root.*r
}

Getting back to our file system model, the adjacency relation of the file system DAG is the relation between directories and the objects they contain, abstracting away the intermediate entry atoms, that is, the composed relation entries.object. So, after including the new auxiliary model with open graph, we could just enforce the following fact in the main module, replacing no_indirect_containment and no_dangling_objects.

fact rooted_dag {
  dag[Object, entries.object]
  rootedAt[Object, entries.object, Root]
}

Alternatively, we could have used the qualified names of the predicates as graph/dag and graph/rootedAt.

Parametrized modules

You may have noticed that all predicates declared above required as input the set of nodes on which the graph if formed. This can get a bit cumbersome, and can be avoided by using module parameters. Alloy modules can be declared with signature parameters, that are specified by the including modules. These are declared after its name in the module declaration between square brackets. These module parameters can be used as regular signatures in the model, including as the type of fields and predicates. The parameter declaration can also be marked with keyword exactly, which enforces an exact scope on that signature.

For our example, we would declare the graph utilities module as graph[node] so that the set of nodes comprising the graph get specified when the module is first opened. Then, the predicates within would be rewritten assuming that the set of graph nodes is now provided as a parameter of the module.

module graph[node]

pred dag [r : node -> node] {
  all n : node | n not in n.^r
}

pred rootedAt [r : node -> node, root : node] {
  node in root.*r
}

Note that you can still define commands in parametrized modules in order to validate them. In that case module parameter signatures will be treated as any arbitrary signature. For instance, we could write the following run command in our graph module.

run dag_example {
  some r : node -> node | dag[r]
} for exactly 4 node

This is an example of a higher-order quantification, possible in certain contexts due to Skolemization. A possible resulting instance is shown below.

Get the code to generate this instance.

Getting back to our file system example, we want to force a rooted DAG over file system objects. So we include the graph module as open graph[Object], and then rewrite the fact rooted_dag as follows.

fact rooted_dag {
  dag[entries.object]
  rootedAt[entries.object, Root]
}

What we just defined is actually part of the utility module util/graph, which builds on the util/relation utility module. These come packaged with the Analyzer, and contain many more common properties that can be tested over relations and graphs. They can be inspected through menu option File ‣ Open Sample Models….

Adding fields to module parameters

Sometimes it is useful for auxiliary modules to declare additional structures to be used in the main importing module. New signatures, possibly with fields, can be introduced in the auxiliary module. However, we cannot declare new fields for the parameter signatures that are passed by the main module. However, there are some workarounds to this limitation.

For the sake of example, let us try to encode a simple auxiliary module that introduces timestamps in over a given signature. We’ll call this module timestamp, and it takes as a parameter the signature A to which the timestamps will be assigned, and the signature T representing the actual timestamps.

module timestamp[A, T]

Our timestamp module will be very simple for the sake of example: we want timestamps to be comparable, to be assigned to every A atom, and to be exhaustively used (i.e., no free dangling timestamps). To allow the comparison of timestamps, we can impose a total ordering over T with the utility module util/ordering.

open util/ordering[T]

Next, we want to define a new binary relation that assigns timestamps from T to elements of A. We cannot introduce new fields in A directly, but we can introduce a new auxiliary singleton signature with a ternary field between A and T, and then just project it when needed.

one sig TimeAux {
  aux_time : A -> T
}

For instance, to impose the desired multiplicity of the timestamp field, we can enforce the following fact. Note that since TimeAux is a singleton signature, TimeAux.aux_time simply returns a binary relation between A and T.

fact time_mult {
  TimeAux.aux_time in A some -> one T
}

Now, let us get back to our file system model and include this module. We define a new signature representing the timestamps, and then open the auxiliary module timestamp over the objects of the file system.

open timestamp[Object, Timestamp]

sig Timestamp {}

Field aux_time is now accessible from the main module, and we can impose additional restrictions on timestamps. For instance, we can force the timestamps of the contents of a directory to be after the timestamp of the directory itself as follows. Relation next is provided by util/ordering over the timestamps.

fact time {
  all d : Dir | d.entries.object.(TimeAux.aux_time) in d.(TimeAux.aux_time).*next
}

The new elements will also show up in the visualizer, as follows. Notice how atoms introduced by other modules appear with the qualified name by default. This label can be changed by customizing the theme.

Get the code to generate this instance.

While this strategy of defining a singleton auxiliary signature is perfectly functional, it is not ideal in terms of maintainability, since we have to keep projecting aux_time. Moreover, it appears as a ternary relation in the visualizer.

For this latter issue, the visualizer actually provides a functionality that can improve the visualization of these instances. On the toolbar, there is a Projection menu where you can select a signature to be projected in the visualization. For selected signatures, the visualization will show its contents projected by each atom, with a new combobox at the bottom of the visualizer allowing the selection of the highlighted atom. In this particular case, TimeAux is a singleton signature, so we are only projecting over a single atom, making the ternary aux_time a binary relation in the visualization. Below is the result after projection.

Get the code to generate this instance.

Private declarations

To avoid using the projected ternary relation, we can derive a binary relation from the auxiliary aux_time by declaring an auxiliary function that projects away TimeAux, as follows.

fun time : A -> T {
  TimeAux.aux_time
}

We want to provide this derived relation to including modules, and hide the fact that it uses an auxiliary signature. Alloy allows any declaration in a module to be marked as private. This makes the element inaccessible from any module that includes the auxiliary module. In our example, we can make TimeAux (and the field within) private as follows.

private one sig TimeAux {
  aux_time : A -> T
}

Now TimeAux is no longer visible in the file system module, but only the derived relation time. So the fact shown above would be rewritten as follows.

fact time {
  all d : Dir | d.entries.object.time in d.time.*next
}

If you now generate an instance you’ll see the binary time connecting objects to timestamps (labeled $time since it is a derived relation).

Get the code to generate this instance.

Of course you can always customize the theme to hide timestamp atoms and show time as an attribute.

Get the code to generate this instance.

You may notice that, although private signatures are omitted, the corresponding atoms still belong to the universe (there is no notion of private atom). So, for instance, if you evaluate univ in the evaluator atoms from private signatures will appear. In our file system example, you’ll see in the universe an atom timestamp/TimeAux$0 representing the singleton auxiliary signature TimeAux. Perhaps surprisingly, you’ll also see an atom timestamp/ordering/Ord$0. This because the approach just presented is precisely the one followed in util/ordering to introduce a total ordering over a given parameter signature with a derived relation next, and that atom corresponds to the singleton auxiliary signature introduced.

As a last note, you can include the same module multiple times with different signature parameters. For instance, here we could additional declare open timestamp[Entry, Timestamp] and an additional time relation would assign timestamps to entries. Alloy will try to disambiguate calls to time using its type system. For instance, the fact above would still be valid since from the type of d it identifies the time relation defined for objects. Nonetheless, it may help to open those modules with an alias using as.