Diff package adopts LiquidHaskell for static checks

Hello, dear haskellers!

I want to make a splash about LiquidHaskell being used for static checks in the Diff package, used transitively by more than 17000 packages according to hackage-revdeps [link].

Users of Diff aren’t affected, but Diff contributors can check the package with LiquidHaskell now [link]. This blog post tells the story of how it came to be [link]. And there is the now merged PR adding the initial checks [link].

Thanks much to the maintainers of Diff, David Fox and @Bodigrim. And thanks to Tweag for supporting my colleague @ninioArtillero to do this work.

Onwards and upwards, LiquidHaskell!

28 Likes

Out of curiosity, did Liquid Haskell find any bugs?

6 Likes

The static checks found no violations, but the process of adding the static checks requires a rather deep review of the code and the slim documentation. This review did surface an unlawful Ord instance [link]. The unlawful instance didn’t produce misbehavior of Diff though.

IIRC, no other bugs were uncovered, although there was plenty of insight to lower the effort of understanding the code with refactorings and documentation contributions. Together with the static checks, I find Diff a tad easier to change and review now.

8 Likes

Great stuff, thanks @facundominguez and @ninioArtillero !!!

2 Likes

Great to see a real world library adopts Liquid Haskell. So far, I had no luck to add Liquid Haskell checks to any of my libraries, even the really simple ones. I always hit a bug or a LH limitation. :frowning:

1 Like

LiquidHaskell emits no complaint, however, GHC warns about the use of List.head. Would it be possible to replace the lists in the Diffs by NonEmpty?

If you’re using Liquid Haskell you should just disable that warning. A big advantage of Liquid Haskell is that it can give better errors for functions that would be partial if you don’t refine the type.

In the particular code I see that the function ses has an @ignore@ tag, thus no check on head is performed. But there is a non-trivial use of head in it, namely a head . dropWhile.

Ah indeed, I just saw that in the blog post too. It seems to me that any use of NonEmpty can easily be replaced by a refinement type, so conversely if we can’t easily give something a suitably refined type then it will probably be even harder to change the code to use NonEmpty.

1 Like

That is well noted. Termination of ses would come in a later PR. If you are interested, @ninioArtillero has work in progress about it.

2 Likes

The `ignore ses` annotation carries heavy weight: proving that `dropWhile` produces a non-empty list, i.e. returns the algorithm’s endpoint, is not straight-forward because Liquid Haskell has important (perhaps fundamental) limitations to reason about infinite values. My current approach is refactoring it to use explicit recursion (not library functions) to translate such requirement to a simple termination proof.

1 Like

This is surely the case to some degree: both encode a non-emptiness proof in their own way. Intuitively, `NonEmpty` value carries it within, while refinement types enforce it at the boundaries.