presented by Juraj Mičko

for Research Topics in Software Engineering

on 2.11.2021

Motivation

-- type-safe, but throws exception
main = print ("Hello" !! 42)

-- total, but non-terminating
main = let z = z in z

-- total, terminating,
-- but probably not functional as expected
main = print (sum 1 2)
  where
    sum a b = a - b

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Motivation

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

can reject these programs during compile-time

Tradeoff: Some correct programs

are rejected, too

Refinement Types

-- Type specification (Haskell code)
foo :: Int

-- Type refinement (LiquidHaskell code)
{-@ foo :: { v:Int | v >= 42 } @-}
foo = 7  -- does not compile
foo = 42 -- OK

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Goals of the paper

  1. Showcase LiquidHaskell
  2. Discuss properties that can be checked
  3. Show how real-world libraries can be verified using LH

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

take :: t:Text -> i:Int -> Text
take t i =
  if i < len t
  then
    Unsafe.take t i
  else
    error "Out of bounds"

UNSAFE

SLOW

(essentially an array of Unicode characters)

Example of using LiquidHaskell

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

{-@ take :: t:Text -> { i:Int | i < len t } -> Text @-}
take t i =
  if i < len t
  then
    Unsafe.take t i
  else
    error "Out of bounds"

Example of using LiquidHaskell

LiquidHaskell syntax

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

{-@ take :: t:Text -> { i:Int | i < len t } -> Text @-}
take t i =
    Unsafe.take t i

SAFE & FAST :)

(under-approximation)

Example of using LiquidHaskell

LiquidHaskell syntax

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Workflow of LiquidHaskell

(under-approximation)

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

"refines Haskell's types with logical predicates that   let us enforce critical properties at compile time"

  • totality
  • termination
  • application-specific properties

LiquidHaskell can prove:

  • memory safety
    (buffer overflows, dangling pointers)
  • data structure correctness invariants
    (e.g. red-black trees)
  • functional properties
    (e.g. sum)

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Specifying correctness

{-@ append :: x:[a] -> y:[a] -> {z:[a] | len z = len x + len y} @-}
append [] ys = ys
append (x:xs) ys = x : append xs ys

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Specifying totality

totality verified out of the box

(no extra annotation needed)

{-@ head :: { v:[_] | 0 < len v } -> _ @-}
head (x:xs) = x

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Verifying totality

{-@ head :: { v:[_] | 0 < len v } -> _ @-}
head (x:xs) = x

-- translates to:

head l = case l of
  (x:xs) ->
    x
  [] ->
    error "..."

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Verifying totality

{-@ head :: { v:[_] | 0 < len v } -> _ @-}
head (x:xs) = x

-- translates to:

head l = case l of
  (x:xs) ->
    -- l :: { 0 < len l }
    x
  [] ->
    -- l :: { 0 < len l && len l = 0 }
    error "..."

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Specifying termination

-- automatic verification
{-@ last :: { v:[Int] | len v > 0 } -> Int @-}
last [x] = x
last (x:xs) = last xs

-- termination expression
{-@ range :: lo:Int -> hi:Int -> [Int] / [hi-lo] @-}
range lo hi
  | lo < hi = lo : range (lo+1) hi
  | otherwise = []
/ [hi-lo]

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Demo:

Verify QuickSort

  1. returns an ordered list
  2. is total
  3. terminates

(this still doesn't prove all about quicksort)

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Evaluation

  • found a bug in           !
text
  • Various properties of popular Haskell libraries (10K LoC, 56 modules)

including

containers, hscolour, bytestring, text, vector-algorithms, xmonad
\frac{\quad 156+242 \quad}{11512} \approx \,\,\, 3.5\% \ \text{overhead \scriptstyle (excl. specification)}

~2 LoC required for specifying a function

!
\frac{1975+156+242}{11512} \approx 20.6\% \ \text{overhead \scriptstyle (incl. specification)}

# of specifications          LoC

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Limitations

  • Limited expressivity
  • Code modifications may be needed
    • Ghost parameters
       
    • Abstract specialization of functions
       
  • Error reporting




     
  • Supports only linear arithmetic
appendSorted pivot [] ys = pivot : ys
appendSorted pivot (x:xs) ys = x : appendSorted pivot xs ys
/Users/jjurm/workspace/liquid-haskell/Main.hs:28:13-33: error:
    • Illegal type specification for `Main.appendableSortedLists`
    Main.appendableSortedLists :: forall a .
                                  (Ord<[]> a) =>
                                  lq_tmp$db##2:(Main.SortedList a) -> lq_tmp$db##3:(Main.SortedList a) -> {VV : 
GHC.Types.Bool | VV == Main.appendableSortedLists lq_tmp$db##2 lq_tmp$db##3
                                                                                                                           
      && VV == (if is$GHC.Maybe.Nothing (Main.highBound lq_tmp$db##2) then true else (if is$GHC.Maybe.Nothing 
(Main.lowBound lq_tmp$db##3) then true else GHC.Maybe.Just##lqdc##$select##GHC.Maybe.Just##1 (Main.highBound lq_tmp$db##2) 
<= GHC.Maybe.Just##lqdc##$select##GHC.Maybe.Just##1 (Main.lowBound lq_tmp$db##3)))}
    Sort Error in Refinement: {VV : bool | (VV == Main.appendableSortedLists lq_tmp$db##2 lq_tmp$db##3
                                            && VV == (if is$GHC.Maybe.Nothing (Main.highBound lq_tmp$db##2) then true else 
(if is$GHC.Maybe.Nothing (Main.lowBound lq_tmp$db##3) then true else GHC.Maybe.Just##lqdc##$select##GHC.Maybe.Just##1 
(Main.highBound lq_tmp$db##2) <= GHC.Maybe.Just##lqdc##$select##GHC.Maybe.Just##1 (Main.lowBound lq_tmp$db##3))))}
    Unbound symbol Main.lowBound --- perhaps you meant: Main.highBound ?
    • 
   |
28 | {-@ reflect appendableSortedLists @-}
   |             ^^^^^^^^^^^^^^^^^^^^^
max :: forall <p :: Int -> Prop>. Int<p> -> Int<p> -> Int<p>

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Alternatives

  • Verification of Haskell code: Coq, Agda, Idris, ...
     
  • Other Static Contract Checkers exist
     

LiquidHaskell reuses specs for: functional correctness, totality, termination

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Summary

    1. Motivation          2. Paper intro         3. Example         4. Verifying programs         5. Demo         6. Evaluation     

Syntax:

Specifications for type refinements

-- contracts
{-@ div :: n:Nat -> d:Pos -> { v:Nat | v <= n } @-}
div n d = n / d

-- dependant contracts
{-@ zip :: x:[a] -> { y:[b] | len x = len y } -> { z:[(a, b)] | len x = len z } @-}

-- measures (uninterpreted function)
{-@ measure len :: [a] -> Int @-}
len []     = 0
len (x:xs) = 1 + len xs

-- []  :: { v:[a] | len v = 0 }
-- (:) :: _ -> xs:_ -> { v:[a] | len v = 1 + len xs }

{-@ append :: xs:[a] -> ys:[a] -> { v:[a] | len v = len xs + len ys } @-}

Syntax:

Specifications for type refinements (2)

-- abstract refinements
{-@ max :: forall <p :: Int -> Prop>. Int<p> -> Int<p> -> Int<p> @-}

{-@ evenNumber :: { v: Int | v mod 2 == 0 } @-}
evenNumber = max 4 (-2)

-- type aliases
{-@ predicate NonEmp X = 0 < len X @-}
{-@ type NonEmpList a = { v: [a] | NonEmp v } @-}

{-@ deduplicate :: l:_ -> { v:_ | NonEmp l => NonEmp v } @-}
-- or
{-@ head :: l:NonEmpList a -> a @-}

-- unchecked refinement
{-@ assume answerAnything :: _ -> 42 @-}
answerAnything x = ...

Specifying termination (3)

-- mutual recursion
{-@ isEven :: n:Nat -> Bool / [n, 0] @-}
isEven 0 = True
isEven n = isOdd $ n-1

{-@ isOdd :: n:Nat -> Bool / [n, 1] @-}
isOdd n = not $ isEven n
   [1, 2]
-> [1, 1]
-> [1, 0]
-> [0, 2]
-> [0, 1]
-> [0, 0]
 

Verifying correctness

-- []  :: { v:[_] | len v = 0 }
-- (:) :: _ -> xs:_ -> { v:[_] | len v = 1 + len xs }

{-@ append :: a:[_] -> b:[_] -> {c:[_] | len c = len a + len b} @-}
append [] ys =
  -- len [] = 0
  ys
  -- len ys = len ys + len []
append (x:xs) ys =
  -- len xs = len (x:xs) - 1
  x : append xs ys
  -- len (x : append xs ys) = 1 + len xs + len ys

Specifying termination (2)

-- lexicographic termination
{-@ ack :: m:Nat -> n:Nat -> Nat / [m, n] @-}
ack m n
  | m == 0 = n + 1
  | n == 0 = ack (m-1) 1
  | otherwise = ack (m-1) (ack m (n-1))
   [1, 2]
-> [1, 1]
-> [1, 0]
-> [0, 2]
-> [0, 1]
-> [0, 0]
 

Links

Liquid Haskell

By Juraj Mičko

Liquid Haskell

  • 159