Joachim Breitner wrote a cool post describing a library for representing sets of integers as sorted lists of intervals, and how they were able to formally verify the code by translating it to Coq using their nifty new tool.

First, lets just see how plain refinement types let us specify the key “goodness” invariant, and check it automatically.

Next, we’ll see how LH’s new “type-level computation” abilities let us specify and check “correctness”, and even better, understand why the code works.

(Click here to demo)

41: {-@ LIQUID "--short-names" @-} 42: {-@ LIQUID "--exact-data-con" @-} 43: {-@ LIQUID "--no-adt" @-} 44: {-@ LIQUID "--prune-unsorted" @-} 45: {-@ LIQUID "--higherorder" @-} 46: {-@ LIQUID "--no-termination" @-} 47: 48: module Intervals where 49: 50: data Interval = I 51: { from :: Int 52: , to :: Int 53: } deriving ( (Show Interval) Show ) 54:

Encoding Sets as Intervals

The key idea underlying the intervals data structure, is that we can represent sets of integers like:

by first ordering them into a list

and then partitioning the list into compact intervals

That is,

Each interval (from, to) corresponds to the set {from,from+1,...,to-1} . Ordering ensures there is a canonical representation that simplifies interval operations.

Making Illegal Intervals Unrepresentable

We require that the list of intervals be “sorted, non-empty, disjoint and non-adjacent”. Lets follow the slogan of make-illegal-values-unrepresentable to see how we can encode the legality constraints with refinements.

A Single Interval

We can ensure that each interval is non-empty by refining the data type for a single interval to specify that the to field must be strictly bigger than the from field:

104: {-@ data Interval = I 105: { from :: Int 106: , to :: { v : Int | from < v } 107: } 108: @-}

Now, LH will ensure that we can only construct legal, non-empty Interval s

Many Intervals

We can represent arbitrary sets as a list of Interval s:

124: data Intervals = Intervals { itvs :: [ Interval ] }

The plain Haskell type doesn’t have enough teeth to enforce legality, specifically, to ensure ordering and the absence of overlaps. Refinements to the rescue!

First, we specify a lower-bounded Interval as:

134: {-@ type LbItv N = { v : Interval | N <= from v } @-}

Intuitively, an LbItv n is one that starts (at or) after n .

Next, we use the above to define an ordered list of lower-bounded intervals:

143: {-@ type OrdItvs N = [ LbItv N ] < { \ vHd vTl -> to vHd <= from vTl } > @-}

The signature above uses an abstract-refinement to capture the legality requirements.

An OrdInterval N is a list of Interval that are lower-bounded by N , and In each sub-list, the head Interval vHd precedes each in the tail vTl .

Legal Intervals

We can now describe legal Intervals simply as:

161: {-@ data Intervals = Intervals { itvs :: OrdItvs 0 } @-}

LH will now ensure that illegal Intervals are not representable.

Do the types really capture the legality requirements? In the original code, Breitner described goodness as a recursively defined predicate that takes an additional lower bound lb and returns True iff the representation was legal:

We can check that our type-based representation is indeed legit by checking that goodLIs returns True whenever it is called with a valid of OrdItvs :

190: {-@ goodLIs :: lb : Nat -> is : OrdItvs lb -> {v : Bool | v } @-}

Algorithms on Intervals

We represent legality as a type, but is that good for? After all, we could, as seen above, just as well have written a predicate goodLIs ? The payoff comes when it comes to using the Intervals e.g. to implement various set operations.

For example, here’s the code for intersecting two sets, each represented as intervals. We’ve made exactly one change to the function implemented by Breitner: we added the extra lower-bound parameter lb to the recursive go to make clear that the function takes two OrdItvs lb and returns an OrdItvs lb .

Internal vs External Verification

By representing legality internally as a refinement type, as opposed to externally as predicate ( goodLIs ) we have exposed enough information about the structure of the values that LH can automatically chomp through the above code to guarantee that we haven’t messed up the invariants.

To appreciate the payoff, compare to the effort needed to verify legality using the external representation used in the hs-to-coq proof.

The same principle and simplification benefits apply to both the union

and the subtract functions too:

both of which require non-trivial proofs in the external style. (Of course, its possible those proofs can be simplified.)

Summing Up (and Looking Ahead)

I hope the above example illustrates why “making illegal states” unrepresentable is a great principle for engineering code and proofs.

That said, notice that with hs-to-coq, Breitner was able to go far beyond the above legality requirement: he was able to specify and verify the far more important (and difficult) property that the above is a correct implementation of a Set library.

Is it even possible, let alone easier to do that with LH?

Please enable JavaScript to view the comments powered by Disqus.