liquidhaskell
Liquid Types for Haskell
https://github.com/ucsd-progsys/liquidhaskell
Stackage Nightly 2025-08-17: | 0.9.12.2 |
Latest on Hackage: | 0.9.12.2 |
liquidhaskell-0.9.12.2@sha256:f38b2f3aa25edc85e3f06f66b2c237a510471fcffd588319927d94b2b02c01a7,3457
Module documentation for 0.9.12.2
- Data
- Foreign
- GHC
- GHC.Base_LHAssumptions
- GHC.CString_LHAssumptions
- GHC.Classes_LHAssumptions
- GHC.Exts_LHAssumptions
- GHC.Float_LHAssumptions
- GHC.ForeignPtr_LHAssumptions
- GHC.IO
- GHC.Int_LHAssumptions
- GHC.List_LHAssumptions
- GHC.Maybe_LHAssumptions
- GHC.Num
- GHC.Num_LHAssumptions
- GHC.Ptr_LHAssumptions
- GHC.Real_LHAssumptions
- GHC.Types_LHAssumptions
- GHC.Word_LHAssumptions
- Liquid
- LiquidHaskell
- Prelude_LHAssumptions
This is the development site of the LiquidHaskell formal verification tool.
If you’re a LiquidHaskell user (or just curious), you probably want to go to the documentation website instead.
Contributing
This is an open-source project, and we love getting feedback (and patches)!
Reporting a Bug
If something doesn’t work as it should, please consider opening a github issue to let us know. If possible, try to:
- Try to use a descriptive title;
- State as clearly as possible what is the problem you are facing;
- Provide a small Haskell file producing the issue;
- Write down the expected behaviour vs the actual behaviour;
- Please, let us know which liquidhaskell version you are using.
Your first Pull Request
We are thrilled to get PRs! Please follow these guidelines, as doing so will increase the chances of having your PR accepted:
- The main LH repo lives here
- Please create pull requests against the
develop
branch. - Please be sure to include test cases that illustrate the effect of the PR
- e.g. show new features that that are supported or how it fixes some previous issue
- If you’re making user-visible changes, please also add documentation
- e.g. options.md, specifications.md, the main tutorial (as relevant)
Pull requests don’t just have to be about code: documentation can often be improved too!
Ask for Help
If you have further questions or you just need help, you can always reach out on our slack channel, google groups mailing list, GitHub issue tracker, or by emailing Ranjit Jhala, Niki Vazou.
General Development Guide
For those diving into the implementation of LiquidHaskell, here are a few tips:
Running the pluging on individual files
cabal build liquidhaskell
cabal exec ghc -- -fplugin=LiquidHaskell FILE.hs
Building
Cabal
cabal build
Faster recompilation
When changing the liquidhaskell-boot
library, sometimes we don’t want
to rebuild liquidhaskell
or liquid-vector
when testing the changes.
In these cases we can set the environment variable LIQUID_DEV_MODE=true
when running cabal
to skip rebuilding those packages.
DANGER: Note that this can give an invalid result if the changes to
liquidhaskell-boot
do require rebuilding other liquid*
packages.
How To Run Regression Tests
For documentation on the test-driver
executable itself, please refer to the
README.md
in tests/
or run cabal run tests:test-driver -- --help
You can run all the tests by
$ ./scripts/test/test_plugin.sh
You can run a bunch of particular test-groups instead by
$ ./scripts/test/test_plugin.sh <test-group-name1> <test-group-name2> ...
and you can list all the possible test options with
$ ./scripts/test/test_plugin.sh --help
or get a list of just the test groups, one per line, with
$ ./scripts/test/test_plugin.sh --show-all
To pass in specific parameters, you can invoke cabal directly with
$ cabal build tests:<test-group-name> --ghc-options=-fplugin-opt=LiquidHaskell:--no-termination
For example:
$ cabal build tests:unit-neg --ghc-options=-fplugin-opt=LiquidHaskell:--no-termination
Another useful option is to change the underlying solver:
$ cabal build tests:unit-pos --ghc-options=-fplugin-opt=LiquidHaskell:--smtsolver=cvc5
You can also modify the number of used threads, depending on cores etc.
You can directly extend and run the tests by modifying the files in
tests/harness/
Parallelism in Tests
Tests run in parallel, unless the flag --measure-timings
is specified to test_plugin.sh
.
How to create performance comparison charts
When liquidhaskell
tests run, we can collect timing information with
$ ./scripts/test/test_plugin.sh --measure-timings
Measures will be collected in .dump-timings
files under dist-newstyle
directory. These can be
converted to json data with
cabal v2-build ghc-timings
cabal v2-exec ghc-timings dist-newstyle
which will produce tmp/*.json
files.
Then a csv report can be generated from this json files with
cabal v2-run benchmark-timings -- tmp/*.json --phase LiquidHaskell -o summary.csv
On each line, the report will contain the time taken by each test.
Comparison charts in svg
format can be generated by invoking
cabal v2-run plot-performance -- -b path_to_before_summary.csv -a path_to_after_summary.csv -s 50 -f "benchmark"
This will generate three files filtered.svg
(a subset of tests with a benchmark
prefix, enabled by the -f
option),
top.svg
and bot.svg
(top 50 speedups and slowdowns over the entire test set, both enabled by the -s
option) in the
current directory. The -f
and -s
options can be used/omitted independently. If both are omitted, a single perf.svg
will be produced covering the full input test set. Additionally, their effects can be combined by providing a third -c
option (this will produce 2 files filtered-top.svg
and filtered-bot.svg
instead of 3). An optional key -o
can be
supplied to specify an output directory for the generated files.
There is also a legacy script scripts/plot-performance/chart_perf.sh
that can be used to generate comparison charts
in both svg
and png
formats. It requires gnuplot to run and assumes both files contain
the same test set. The following command will produce two files perf.svg
and perf.png
in the current directory.
$ scripts/plot-performance/chart_perf.sh path_to_before_summary.csv path_to_after_summary.csv
The current formatting is optimized for comparing some subsets of the full test run, typically just the benchmarks alone. If one wishes to save time or is not interested in top speedups/slowdowns, the benchmark subset can be obtained by running
$ scripts/test/test_plugin.sh \
benchmark-stitch-lh \
benchmark-bytestring \
benchmark-vector-algorithms \
benchmark-cse230 \
benchmark-esop2013 \
benchmark-icfp15-pos \
benchmark-icfp15-neg
Miscelaneous tasks
- Profiling See the instructions in scripts/profiling-driver/ProfilingDriver.hs.
- Getting stack traces on exceptions See
-xc
flag in the GHC user’s guide. - Working with submodules See
man gitsubmodules
or the git documentation site.
GHC support policy
LH supports only one version of GHC at any given time. This is because LH depends heavily on the ghc
library
and there is currently no distinction between public API’s and API’s internal to GHC. There are currently no
release notes for the ghc
library and breaking changes happen without notice and without deprecation
periods. Supporting only one GHC version saves developer time because it obviates the need for #ifdef
’s
throughout the codebase, or for an compatibility layer that becomes increasingly difficult to write as we
attempt to support more GHC versions. Porting to newer GHC versions takes less time, the code is easier to
read and there is less code duplication.
Users of older versions of GHC can still use older versions of LH.
The GHC.API module
In order to minimize the effort in porting LH to new releases of GHC, we need a way to abstract over breaking
changes in the ghc
library, which might change substantially with every major GHC release. This is
accomplished by the GHC.API module. The idea is that rather than importing multiple ghc
modules,
LH developers must import this single module in order to write future-proof code. This is especially
important for versions of the compiler greater than 9, where the module hierarchy changed substantially,
and using the GHC.API makes it easier to support new versions of GHC when they are released.
Fragile import strategy
import Predicate
import TyCoRep
...
-- This will break if 'isEqPrimPred' is (re)moved or the import hierarchy changes.
foo :: Type -> Bool
foo = isEqPrimPred
Recommended import strategy
import qualified Language.Haskell.Liquid.GHC.API as GHC
...
foo :: GHC.Type -> Bool
foo = GHC.isEqPrimPred -- OK.
GHC Plugin Development Guide
This code commentary describes the current architecture for the GHC Plugin that enables LiquidHaskell
to check files as part of the normal compilation process. For the sake of this commentary, we refer to
the code provided as part of the release/0.8.10.2
branch, commit 9a2f8284c5fe5b18ed0410e842acd3329a629a6b
.
GHC.Interface vs GHC.Plugin
The module GHC.Plugin is the main entrypoint for all the plugin functionalities. Whenever possible, this module is reusing common functionalities from the GHC.Interface, which is the original module used to interface LH with the old executable. Generally speaking, the GHC.Interface module is considered “legacy” and it’s rarely what one wants to modify. It will probably be removed at some point.
Plugin architecture
Broadly speaking, the Plugin is organised this way: In the typechecking phase, we typecheck and desugar
each module via the GHC API in order to extract the unoptimised core binds that are needed by
LH to work correctly. This is due to a tension in the design space; from one side LH needs access to the
“raw” core binds (binds where primitives types are not unboxed in the presence of a PRAGMA annotation,
for example) but yet the user can specify any arbitrary optimisation settings during compilation and we do
not want to betray the principle of least expectation by silently compiling the code with -O0
. Practically
speaking, this introduces some overhead and is far from ideal, but for now it allows us to iterate quickly.
This phase is also responsible for:
- Extracting the [BareSpec][]s associated to any of the dependent modules;
- Producing the LiftedSpec for the currently-compiled module;
- Storing the LiftedSpec into an interface annotation for later retrieval;
- Checking and verifying the module using LH’s existing API.
The reason why we do everything in the typechecking phase is also to allow integrations with tools like ghcide. There are a number of differences between the plugin and the operations performed as part of the GHC.Interface, which we are going to outline in the next section.
Differences with the GHC.Interface
-
The GHC.Interface pre-processes the input files and calls into configureGhcTargets trying to build a dependency graph by discovering dependencies the target files might require. Then, from this list any file in the include directory is filtered out, as well as any module which has a “fresh”
.bspec
file on disk, to save time during checking. In the GHC.Plugin module though we don’t do this and for us, essentially, each input file is considered a target, where we exploit the fact GHC will skip recompilation if unnecessary. This also implies that while the GHC.Interface calls into processTargetModule only for target files, the GHC.Plugin has a single, flat function simply called processModule that essentially does the same asGHC.Interface.processModule
andGHC.Interface.processTargetModule
fused together. -
While the GHC.Interface sometimes “assembles” a [BareSpec][] by
mappend
ing thecommSpec
(i.e. comment spec) with the LiftedSpec fetched from disk, if any, the Plugin doesn’t do this but rather piggybacks on the SpecFinder (described later) to fetch dependencies’ specs. -
There is a difference in how we process LIQUID pragmas. In particular, for the executable they seems to be accumulated “in bulk” i.e. if we are refining a target module
A
that depends onB
,B
seems to inherit whichever flags we were using in the target moduleA
. Conversely, the source plugin is “stateless” when it comes to LIQUID options, i.e. it doesn’t have memory of past options, what it counts when compiling a moduleB
is the global options and any option this module defines. The analogy is exactly the same as with GHC language extensions, they have either global scope (i.e.default-extensions
in the cabal manifest) or local scope (i.e.{-# LANGUAGE ... #-}
).
Finding specs for existing modules
This is all done by a specialised module called the SpecFinder. The main exported function is
findRelevantSpecs which, given a list of Module
s, tries to retrieve the LiftedSpec
s associated with
them. Typically this is done by looking into the interface files of the input modules, trying to deserialise
any LiftedSpec
from the interface file’s annotations.
General Development FAQs
A new version of GHC is out. How do I support it?
Typically the first thing you might want to do is to run a “clean” cabal build
using
the latest compiler and “check the damage”. If you are lucky, everything works out of the box, otherwise
compilation might fail with an error, typically because some ghc
API function has been removed/moved/renamed.
The way to fix it is to modify the GHC.API shim module and perform any required change, likely by
conditionally compiling some code in a CPP
block. For minor changes, it’s usually enough to perform small
changes, but for more tricky migrations it might be necessary to backport some GHC code, or create some
patter synonym to deal with changes in type constructors.
Is there a way to run the testsuite for different versions of GHC?
Currently, no. Only one version of GHC is supported and that is the one
that can be tested with ./scripts/test/test_plugin.sh
.
Changes
Changes
Next
0.9.12.2 (2025-03-22)
- Simplify kvar solutions in fqout files liquid-fixpoint#741.
- Upgrade ghc to 9.12.2 #2474.
0.9.10.1.2 (2025-03-06)
- Implement opaque reflection, a feature to allow reflecting functions which call to non-reflected functions #2323.
- Implement reflection from interface files, which can reflect functions from their unfoldings #2326. The feature is limited at the moment by the constraints that affect reflecting functions in general. But we hope it becomes more interesting as reflection is made more flexible.
- Operators in the logic cannot be shadowed locally anymore since #2327.
- Added a flag
--dump-pre-normalized-core
to show core before A normalization and constraint generation #2336. - Augmented the context of error messages #2350.
- Add a new flag
--etabeta
to reason with lambdas in PLE #2356 - Add a new flag
--dependentcase
to expand support for higher-order reasoning #2384 - Add support for reflecting lambda expressions #2465.
- Enabling the LiquidHaskell plugin now enables
-fno-ignore-interface-pragmas
(#2326) and-dkeep-comments
(#2367). - LiquidHaskell earned a new
--minimal
verbosity level as default that prints the banner with the amount of constraints checked (#2395). This banner is now suppressed when the verbosity is set to--quiet
(#2391). - Avoid reparsing and retypechecking when verifying modules #2389.
- Name resolution is done only when verifying a module. It is no longer done when importing it #2169. One side effect of this change is that LH can now pick up names in scope using import aliases in most places (but see #2481).
- Allow to link Haskell definitions with logical primitives via
define
declarations #2463. - CVC5 solver is now supported for all logical theories, including Sets/Bags #2483
0.9.10.1 (2024-08-21)
- Add support for GHC 9.10.1.
0.9.8.2 (2024-08-21)
- Support for GHC 9.8.2.
- Implement assume-reflect, a feature to assume the reflection of functions in dependencies 2313.
- Fixed the polymorphism-related crash in liquid-fixpoint caused by a restrictive theory encoding #2272.
0.9.8.1 (2024-02-05)
- Set support for GHC 9.8.1 #2248
- Embedded files
include/CoreToLogic.lg
andsyntax/liquid.css
in the source code #2265
0.9.6.3.1 (2024-03-07)
- Avoid enabling plugins in ghc-options (workaround for #9375)
0.9.6.3 (2024-01-29)
- Set support for GHC 9.6.3
0.9.4.7.0
- Set support for GHC 9.4.7
0.9.2.8.0
- Support for GHC 9.2.8
- Fix A normalization when type binder and lets are mixed in the input (#2236)
- Move KMeansHelper from liquid-prelude to tests
0.9.2.5
- Introduce package liquidhaskell-boot and eliminate wrapper packages for boot libraries
- List all definitions used from the GHC API
- Allow LH to verify modules in parallel (remove withArgs call)
- Remove some calls to HashMap.toList which caused some non-determinisms in different machines
- Implement a Haskell script to plot performance without gnuplot
0.9.0.2
- breaking change Remove the implicit types mechanism and corresponding tests
- breaking change Remove the
decrease
keyword and mechanism in favor of the terminating expressions syntax (/ [a,b]
)
0.8.10.1
- Support for GHC 8.10.1
- LiquidHaskell is now available as a GHC Plugin
0.8.6.0
- Automatically check (transitive) dependencies
- Built with GHC 8.6.4
- Structural termination checker (on by default)
- Support for specifying class-laws and that they hold on instances
- Bug fixes for PLE
- Need to run LH on imported libs (with source) first; can use
--compile-spec
to avoid checking.
0.8.4.0
- Support for GHC 8.4.3
- Significant restructuring of
Bare
front-end to shrink dependency on GHC-API
0.8.2.2
-
Support for GHC 8.2.2
-
Support for GADTs and TypeFamilies, see
tests/{pos,neg}/ExactGADT*.hs
-
Add support for Bags/Multisets, see
tests/pos/bag.hs
tests/neg/bag.hs
tests/pos/ListISort-bag.hs
-
Add support for inductive predicates see
tests/pos/IndEven.hs
tests/pos/IndPerm.hs
tests/pos/IndStar.hs
0.8.0.1
- Support for GHC 8.0.2
0.7.0.1
-
DELETED the gsDcons and generally carrying DataConP beyond Bare; this may cause problems with
target
as I removed thedconEnv
field inTargetState
. Is it live? To restore: have to apply the substitution syms/su in Bare.hs ALSO to gsDconsP (after restoring the gsDconsP field to [(DataCon, DataConP)]) -
breaking change Remove the
Bool
vs.Prop
distinction. This means that:-
signatures that use(d)
Prop
as a type, e.g.foo :: Int -> Prop
should just befoo :: Int -> Bool
. -
refinements that use(d)
Prop v
e.g.isNull :: xs:[a] -> {v:Bool | Prop v <=> len xs > 0}
should just beisNull :: xs:[a] -> {v:Bool | v <=> len xs > 0}
.
-
-
Add
--eliminate={none, some, all}
. Herenone
means don’t use eliminate at all, use qualifiers everywhere (old-style)some
which is the DEFAULT – means eliminate all the non-cut variablesall
means eliminate where you can, and solve cut variables toTrue
.
-
Change
--higherorder
so that it uses only the qualifiers obtained from type aliases (e.g.type Nat = {v:Int | ... }
) and nothing else. This requireseliminate=some
. -
Add a
--json
flag that runs in quiet mode where all output is suppressed and only the list of errors is returned as a JSON object to be consumed by an editor. -
Add
--checks
flag (formerly--binders
), which checks a given binder’s definition, assuming specified types for all callees (but inferring types for callees without signatures.) -
Add
--time-binds
which is like the above, but checks all binders in a module and prints out time taken for each.
0.5.0.1
- Fixed a bug in the specification for
Data.Traversable.sequence
- Make interpreted mul and div the default, when
solver = z3
- Use
--higherorder
to allow higher order binders into the fixpoint environment
0.5.0.0
-
Added support for building with
stack
-
Added support for GHC 7.10 (in addition to 7.8)
-
Added ‘–cabaldir’ option that will automatically find a .cabal file in the ancestor path from which the target file belongs, and then add the relevant source and dependencies to the paths searched for by LiquidHaskell.
This means we don’t have to manually do
-i src
etc. when checking large projects, which can be tedious e.g. within emacs.
0.4.0.0
- Bounds as an alternative for logical constraints see
benchmarks/icfp15/pos/Overview.lhs
0.3.0.0
-
Logical constraints: add extra subtyping constraints to signatures, e.g.
{-@ (.) :: forall <p :: b -> c -> Prop, q :: a -> b -> Prop, r :: a -> c -> Prop>. {x::a, w::b |- c <: c} (y:b -> c) -> (z:a -> b) -> x:a -> c @-} (.) f g x = f (g x)
-
Inlining haskell functions as predicates and expressions, e.g.
{-@ inline max @-} max x y = if x >= y then x else y
-
Refining class instances. For example
{-@ instance Compare Int where cmax :: Odd -> Odd -> Odd @-}
-
Major restructuring of internal APIs
0.2.1.0
- Experimental support for lifting haskell functions to measures
If you annotate a Haskell function
foo
with {-@ measure foo @-}, LiquidHaskell will attempt to derive an equivalent measure fromfoo
’s definition. This should help eliminate some boilerplate measures that used to be required.
0.2.0.0
-
Move to GHC-7.8.3 LiquidHaskell now requires ghc-7.8.3.
-
Termination LiquidHaskell will now attempt to prove all recursive functions terminating. It tries to prove that some parameter (or combination thereof) decreases at each recursive callsite. By default, this will be the first parameter with an associated size measure (see Size Measures), but can be overridden with the
Decreases
annotation or a termination expression (see Termination Expressions).
If proving termination is too big of burden, it can be disabled on a per-module basis with the --no-termination
flag, or on a per-function basis with the Lazy
annotation.
-
Size Measures Data declarations now optionally take a size measure, which LiquidHaskell will use to prove termination of recursive functions. The syntax is:
{-@ data List a [len] = Nil | Cons a (List a) @-}
-
Termination Expressions Termination Expressions can be used to specify the decreasing metric of a recursive function. They can be any valid LiquidHaskell expression and must be placed after the function’s LiquidHaskell type, e.g.
{-@ map :: (a -> b) -> xs:[a] -> [a] / [len xs] @-}
-
Type Holes To reduce the annotation burden, LiquidHaskell now accepts
_
as a placeholder for types and refinements. It can take the place of any base Haskell type and LiquidHaskell will query GHC to fill in the blanks, or it can take the place of a refinement predicate, in which case LiquidHaskell will infer an appropriate refinement. For example,{-@ add :: x:_ -> y:_ -> {v:_ | v = x + y} @-} add x y = x + y
becomes
{-@ add :: Num a => x:a -> y:a -> {v:a | v = x + y} @-}
add x y = x + y
-
Assumed Specifications The
assume
annotation now works as you might expect it to, i.e. LiquidHaskell will not verify that the implementation is correct. Furthermore,assume
can be used to locally override the type of an imported function. -
Derived Measure Selectors Given a data definition
{-@ data Foo = Foo { bar :: Int, baz :: Bool } @-}
LiquidHaskell will automatically derive measures
{-@ measure bar :: Foo -> Int @-}
{-@ measure baz :: Foo -> Bool @-}
-
Type-Class Specifications LiquidHaskell can now verify prove that type-class instances satisfy a specification. Simply use the new
class
annotation{-@ class Num a where (+) :: x:a -> y:a -> {v:a | v = x + y} (-) :: x:a -> y:a -> {v:a | v = x - y} … @-}
and LiquidHaskell will attempt to prove at each instance declaration that the implementations satisfy the class specification.
When defining type-class specifications you may find the need to use overloaded measures, to allow for type-specific definitions (see Type-Indexed Measures).
-
Type-Indexed Measures LiquidHaskell now accepts measures with type-specific definitions, e.g. a measure to describe the size of a value. Such measures are defined using the
class measure
syntax{-@ class measure size :: forall a. a -> Int @-}
and instances can be defined using the instance measure
syntax, which mirrors the regular measure syntax
{-@ instance measure size :: [a] -> Int
size ([]) = 0
size (x:xs) = 1 + size xs
@-}
{-@ instance measure size :: Tree a -> Int
size (Leaf) = 0
size (Node l x r) = 1 + size l + size r
@-}
-
Parsing We have greatly improved our parser to require fewer parentheses! Yay!
-
Emacs/Vim Support LiquidHaskell now comes with syntax checkers for flycheck in Emacs and syntastic in Vim.
-
Incremental Checking LiquidHaskell has a new
--diffcheck
flag that will only check binders that have changed since the last run, which can drastically improve verification times. -
Experimental Support for Z3’s theory of real numbers with the
--real
flag.