LiquidHaskell Documentation
repository·develop·Indexed 23 days ago
https://github.com/ucsd-progsys/liquidhaskellA formal verification tool for Haskell that uses refinement types to check program properties via an SMT solver such as Z3. This repository includes examples from the Bounded Refinement Types (ICFP'15) paper and documentation for managing the project's mkdocs-based site and reveal.js presentation slides.
What's inside LiquidHaskell
- reveal.js is a framework for creating HTML-based presentations. It supports advanced features such as nested slides, markdown content, PDF export, speaker notes, and provides a JavaScript API for programmatic control. While it is optimized for browsers supporting CSS 3D transforms, it includes fallbacks for broader compatibility.
What LiquidHaskell (LH) can do
developLiquidHaskell (LH) refines Haskell's types with logical predicates to enforce properties at compile time. Key capabilities include:
- Guaranteeing Function Totality: LH can warn when a function (like
head) is partial (e.g., missing a case for[]) and verify that it is only called with valid inputs (e.g.,NonEmptylists). - Keeping Pointers Within Bounds: Prevents off-by-one errors, crashes, or buffer overflows by using dependent contracts to specify constraints like equal-sized vectors for a
dotProductfunction. - Avoiding Infinite Loops: LH checks for termination and can warn about infinite recursion. For complex data types, you can use Metrics to prove that recursive functions terminate.
- Enforcing Correctness Properties: You can write requirements (e.g., that a list is ordered) as refinements, making illegal values unrepresentable and proving functions return correct outputs for all inputs.
- Proving Laws via Code: You can specify laws (e.g., the associativity of
++) as Haskell functions and verify them using equational proofs. In this model, induction is expressed as recursion and case-splitting is expressed as pattern-matching.
- Guaranteeing Function Totality: LH can warn when a function (like
Overview of the Module Verification Process
developThe verification process follows a specific lifecycle to transform source code into verified specifications:
- Inputs: The process takes
CoreBinds,BareSpecs, andLiftedDeps(dependencies of the module including theirLiftedSpecs). - Transformation:
- A
TargetSpecis produced, which generatesVerification Constraints. - These constraints are checked by
liquid-fixpoint.
- A
- Outputs:
- If verified, a
LiftedSpecis produced and serialized into the module's interface file (.hi).
- If verified, a
- Inputs: The process takes
What is a Zipper and how is it structured?
developA [zipper][wiki-zipper] is an aggregate data structure used to traverse and update a structure arbitrarily. It consists of three parts:
focus: The element currently being used.up: A list of elements located before the current focus (to the left).down: A list of elements located after the current focus (to the right).
In applications like the XMonad tiling window manager, a zipper can represent a set of windows where the
focusis the active window, andup/downrepresent the preceding and succeeding windows. A common invariant for such structures is uniqueness: every element in the zipper must appear exactly once.Handling Quick Sort with Pivot-Aware Append
developA standard implementation of
quickSortmay fail LiquidHaskell verification if the++(append) operator is typed as a simpleIncrList a -> IncrList a -> IncrList a, because appending two sorted lists does not necessarily result in a sorted list (e.g.,[1, 2, 4] ++ [3, 5, 6]is not increasing).To fix this, you cannot use the standard
++operator. Instead, you must implement a specialized 'pivot-append' function. This function leverages the extra information available during Quick Sort: the existence of a pivot elementxsuch that all elements in the first list are less thanx, and all elements in the second list are greater than or equal tox. This specialized function allows LiquidHaskell to verify that the resulting concatenated list remains anIncrList.Understand Reflection in Liquid Haskell
developLiquid Haskell uses a mechanism called reflection to provide logic meaning to Haskell functions used within formal specifications. When you write a specification in a comment
{-@ ... @-}, Liquid Haskell must resolve the names in the predicate to their logic equivalents.For example, in the specification
{-@ filter :: p:(a -> Bool) -> xs:[a] -> {v:[a] | all p v } @-}, Liquid Haskell identifiespandvfrom the function parameters, but it must use reflection to understand whatallmeans by looking it up in thePrelude.{-@ filter :: p:(a -> Bool) -> xs:[a] -> {v:[a] | all p v } @-}Refinement Type Inference for Safety
developLiquidHaskell can automatically infer refinement types for local helper functions (like
goin a recursive loop) to establish safety properties.In the context of vector processing, LiquidHaskell uses the relationship between the loop index
i, the termination conditionn, and the vector lengthvlen vecto prove that an index is always valid. If you modify the logic—for example, by removing a check like0 < nor changing a guard toi <= n—LiquidHaskell will flag the program asunsafebecause it can no longer guarantee that the index remains within the valid bounds of the vector.Understand the reveal.js folder structure
developThe project uses the following directory structure:
css/: Core styles required for the project to function.js/: Core JavaScript files.plugin/: Extensions and components developed for reveal.js.lib/: Third-party assets including JavaScript, CSS, and fonts.
Understand Measures and Assumes in LiquidHaskell
developMeasures
Measures define auxiliary or ghost properties of data values. They are used for specification and verification but do not exist at runtime. They appear only inside type refinements. You can use helper functions like
lengthto "materialize" these ghost values into the actual code world.Assumes
The
assumekeyword is used when you are not verifying the implementation of a library (likeData.Vector) but are instead using its properties to verify your own client code. If you were verifying the library itself, you would ascribe these types directly to the library's source code instead of usingassume.Use fragments to highlight elements
developFragments allow you to step through individual elements on a slide. Any element with the class
fragmentwill be hidden initially and revealed sequentially.Fragment Styles
Append a class to
fragmentto change its behavior:fragment grow: Grows the element.fragment shrink: Shrinks the element.fragment roll-in: Rolls the element in.fragment fade-out: Fades the element out.fragment visible: Shows the element only once.fragment highlight-red/highlight-blue/highlight-green: Highlights the element in a specific color.
Controlling Order
Use
data-fragment-index="N"to specify the exact order in which fragments appear.<section> <p class="fragment" data-fragment-index="3">Appears last</p> <p class="fragment" data-fragment-index="1">Appears first</p> <p class="fragment" data-fragment-index="2">Appears second</p> </section>Use predicate aliases to simplify specifications
developWhen writing LiquidHaskell specifications, complex logical constraints (like checking if an index is within the bounds of a vector) can become verbose and difficult to read. You can use predicate aliases to create reusable, named abstractions for these logical patterns.
To define a predicate alias, use the
predicatekeyword within a LiquidHaskell comment block ({-@ ... @-}). This allows you to compose complex logic into a single, readable name that can then be used in refinement types.For example, instead of manually conjoining lower and upper bound checks every time you access a vector, you can define an
InBoundspredicate.Express Dependent Pairs in LiquidHaskell
developDependent Pairs are expressed by binding the initial elements of the tuple. This allows the type of the second element to depend on the value of the first. Internally, these desugar to abstract refinement types.
{-@ incrPair :: Int -> (x::Int, {v:Int | x <= v}) @-} incrPair i = (i, i+1)