In OPAL, fixed-point analyses need to represent their results using values from an explicitly defined lattices.

This article will discuss a few possibilities when defining them.

You should have read the tutorial on Writing Fixed-Point Analyses first that introduces the basic concepts.

There are two basic kinds of lattices that are typically used in OPAL: singleton-based lattices and set-based lattices.
It is also possible to combine them.

Before discussing each of them individually, let's have a look at what they have in common.

A lattice in OPAL consists of three parts.

The first one is the `PropertyMetaInformation`

:

```
sealed trait ClassImmutabilityPropertyMetaInformation extends PropertyMetaInformation {
final type Self = ClassImmutability
}
```

We use it to provide the type of the property.

Second, the property companion object provides the PropertyKey:

```
object ClassImmutability extends ClassImmutabilityPropertyMetaInformation {
final val key: PropertyKey[ClassImmutability] = PropertyKey.create(
"ClassImmutability",
MutableClass
)
}
```

We discuss more on the `PropertyKey`

below.

A second use of the companion object would be to provide factory methods to create your lattice values conveniently.

Finally, the actual properties.

If you have more than one class, you should provide a trait through which the `PropertyKey`

can be accessed:

```
sealed trait ClassImmutability extends ClassImmutabilityPropertyMetaInformation {
final def key: PropertyKey[ClassImmutability] = ClassImmutability.key
}
```

Implement your lattice using classes that implement this trait.

If you only need a single class for your lattice, you can of course define the `key`

method there and omit defining a trait.

You can also use this trait to declare methods that you need for lattice, like `meet`

or `join`

methods or methods to extract data from your lattice values conveniently.

Singleton-based lattices are very common in OPAL.

They consist of a number of case objects each of which represents a singleton value of the lattice:

```
case object TransitivelyImmutableClass extends ClassImmutability
case object NonTransitivelyImmutableClass extends ClassImmutability
case object MutableClass extends ClassImmutability
```

Of course, you may need to implement any methods defined in your trait above.

The second common type of lattices in OPAL is set-based lattices. They typically consist of one (or sometimes several) case classes that hold the set of values:

```
case class InstantiatedTypes(classes: UIDSet[ObjectType]) extends InstantiatedTypesPropertyMetaInformation {
final def key: PropertyKey[InstantiatedTypes] = InstantiatedTypes.key
}
```

Note that here we extended the `PropertyMetaInformation`

directly without an additional trait as we need only one class.

You can combine both concepts in a single lattice if you need to.

For example, you could extend the above `ClassImmutability`

lattice with this set-based case class:

```
case class DependentlyImmutableClass(dependentTypeParameters: Set[String])
```

Or you could provide a singleton object as a convenient extension for the above `InstantiatedTypes`

lattice:

```
object NoInstantiatedTypes extends InstantiatedTypes(UIDSet.empty)
```

However, always take care that what you implement really has the semantics of a lattice (i.e., the values form a partial order and there exist a unique least upper bound and unique greatest lower bound for every two values).

There are two additional classes that you might want to consider when implementing your lattice.

The first one is `OrderedProperty`

.

If your lattice values extend this trait, you get additional checks in the PropertyStore whether your analyses refine their results monotonically with respect to your lattice's partial order.

`OrderedProperty`

requires that you implement a method `checkIsEqualOrBetterThan(e: Entity, other: Self)`

that should throw an exception if `other`

is greater than the current value with respect to the lattice's partial order.

If you defined a `meet`

method, you can simply implement it like this:

```
override def checkIsEqualOrBetterThan(e: Entity, other: ClassImmutability): Unit = {
if (meet(other) != other) {
throw new IllegalArgumentException(s"$e: impossible refinement: $other => $this")
}
}
```

However, it may be better to provide optimized implementations for your individual lattice values if the `meet`

operation is not trivial.

The second option to consider is `AggregatedProperty`

.

Some properties really represent an aggregation of another property, e.g., `ClassImmutability`

aggregates the `FieldImmutability`

of a class' instance fields.

In such cases, one often needs to convert between corresponding values of the two lattices.

Also, the partial order and thus `meet`

operator are equivalent and need to be defined only once.

In order to use `AggregatedProperty`

, the base lattice has to extend `IndividualProperty[BaseLattice, AggregateLattice]`

, implement the `meet`

method as well as an `aggregatedProperty`

method that maps each value to its corresponding value of the aggregated lattice.

The aggregate lattice then has to extend `AggregatedProperty[BaseLattice, AggregateLattice]`

.

It has to implement the `individualProperty`

method that maps each value to the corresponding value of the base lattice.

The `meet`

method on the aggregate lattice is provided for you based on the `meet`

operator of the base lattice.

The `PropertyKey`

makes your lattice known to OPAL and gives it a unique identifier.

You create it using one of two methods:

```
final val classImmutabilityKey: PropertyKey[ClassImmutability] = PropertyKey.create(
"ClassImmutability",
MutableClass
)
final val instantiatedTypesKey: PropertyKey[InstantiatedTypes] = PropertyKey.create(
"InstantiatedTypes",
(_: PropertyStore, reason: FallbackReason, _: Entity) => reason match {
case PropertyIsNotDerivedByPreviouslyExecutedAnalysis => InstantiatedTypes(UIDSet.empty)
case _ => throw new IllegalStateException(s"No analysis is scheduled for property InstantiatedTypes")
}
)
```

For both, you provide a unique name for your lattice and a fallback if a value from your lattice is queried but not computed.

The difference is in the fallback: You can either provide a single lattice value to use in all cases.

It should be your lattice's bottom element in order to be sound.

Or you can provide a function to compute the fallback value on demand.

This has two benefits: First, you can use some information about your entity to compute a sound but more precise fallback value.

Additionally, as shown above, this function gets a `FallbackReason`

.

This tells you whether no analysis was executed to compute any values of your lattice at all (`PropertyIsNotComputedByAnyAnalysis`

), in which case you should provide a sound over-approximation as before.

Or it tells you that an analysis was executed, but it did not provide a result for the respective entity (`PropertyIsNotDerivedByPreviouslyExecutedAnalysis`

), in which case you may be able to provide a precise result, because you know that, e.g., the entity belongs to dead code.