An tutorial-style implementation of liquid/refinement types for a subset of Ocaml/Reason.
- [*] Lang1 : STLC + Annot (refinements 101)
- [*] Lang2 : "" + Branches (path-sensitivity)
- [*] Lang3 : "" + *-refinements (inference + qual-fixpoint)
- [*] Lang4 : "" + T-Poly (type-polymorphism)
- [*] Lang5 : "" + Data (datatypes & measures)
- [*] Lang6 : "" + R-Poly (refinement-polymorphism)
- [*] Lang7 : "" + Termination (metrics + invariants)
- [-] Lang8 : "" + Reflection (proofs)
- well-formedness
- L7 Invariant & Validity
- intro
- outro
- [*] Save Horn QUERY
- ANF
I, G, f_k |- t
---------------------- [Inv-RAbs]
I, G |- rall r. t
I, G, a |- t
---------------------- [Inv-TAbs]
I, G |- all a. t
I, G, x:s |- x:s -> t
---------------------- [Inv-Fun]
I, G |- x:s -> t
G |- t <: I(t)
---------------------- [Inv-Base]
I, G |- t
L1 --> L2 --> L3 --> L4 --> L6
`--> L5 --> L7 --> L8
- Syntax
- Semantics
- Solution
- Houdini
- Fusion
- [McMillan, Bjorner, Rybalchenko]
p :== x -- PREDICATES
| c
| (op p1 ... pn) -- interpreted ops
| (f p1 ... pn) -- uninterpreted ops
| (bop p1 ... pn) -- boolean ops
c :== p -- FORMULAS
| c /\ c
| all x. (p => c)
r ::= [v|p] -- known refinements
t ::= Int[r] -- refined int
| x:t -> t -- dependent arrow
e ::= x -- variables [synth]
| c -- constants [synth]
| \x. e -- functions [check]
| e x -- anf-application [synth]
| let x = e in e -- let-binding [check]
| e : t -- annotation [check]
Environments
G ::= 0 | G, x:t -- environment
Judgments
G |- e <== t -- checking judgment
G |- e ==> t -- synthesis judgment
Rules
- Check-_ deals with
let
,\x.e
- Synth-_ deals with
x
,c
,e x
,e:t
Valid(G,p)
----------[Sub-Valid]
G |- p
G,v:int{p} |- q[w:=v]
-------------------------[Sub-Base]
G |- int{v:p} <: int{w:q}
G |- s2 <: s1 G,x2:s2 |- t1[x1:=x2] <: t2
------------------------------------------------[Sub-Fun]
G |- x1:s1 -> t1 <: x2:s2 -> t2
Alternate "Hybrid" Presentation
Valid(c)
--------
0 => c
G => (all x. (p => c))
----------------------
G, x:{p} => c
G => sub(s, t)
---------------
G |- s <: t
G, x:t1 |- e <== t2
--------------------------[Chk-Lam]
G |- \x.e <== x:t1 -> t2
G |- e1 ==> t1 G, x:t1 |- e2 <== t2
--------------------------------------------[Chk-Let]
G |- let x = e1 in e2 <== t2
G |- e ==> s G |- s <: t
----------------------------------[Chk-Syn]
G |- e <== t
----------------- [Syn-Var]
G |- x ==> G(x)
----------------- [Syn-Con]
G |- x ==> ty(c)
G |- e <== t
----------------- [Syn-Ann]
G |- e:t ==> t
G |- e ==> x:s -> t
G |- y <== s
-----------------------[Syn-App]
G |- e y ==> t[x := y]
G |- e ==> s -> t G |- (s -> t) * x ==> t
-------------------------------------------
G |- e x ==> t
G |- e ==> forall as. x1:s1 -> ... xn:sn -> t
G |- (forall as.S * y1 ... yn) <== T
G |- (e y1 ... yn) ==> t
--------------------------------------------------------------------
sub :: (Type, Type) -> Cstr
--------------------------------------------------------------------
sub(B{v:p}, B{w:q}) = (v::p) => q[w:=v]
sub(x1:s1 -> t1, x2:s2 -> t2) = sub (s2, s1) /\ (x2 :: t2) => sub(t1[x1:=x2], t2)
--------------------------------------------------------------------
imp :: (Env, Cstr) -> Cstr
--------------------------------------------------------------------
imp (0, c) = c
imp (G;x:t, c) = imp(G, all x t c)
-------------------------------------------------------
check :: (Env, Expr, Type) -> C
-------------------------------------------------------
check (g, \x.e, x:s -> t) = (x :: s) => c
where
c = check(g;x:s, e, t)
check (g, let x e e', t') = c /\ ((x::s) => c')
where
(c, s) = synth(g, e)
c' = check(g;x:s, e', t')
check (g, e, t) = sub(s, t)
where
(c, s) = synth(g, e)
-----------------------------------------------------
synth :: (Env, Expr) -> (C, Type)
-----------------------------------------------------
synth (g, x) = (TT, lookupEnv g x )
synth (g, c) = (TT, ty c )
synth (g, e y) = (ce /\ cy, t[x := y])
where
(ce, x:s->t) = synth (g, e)
cy = check (g, y)
synth (g, e:t) = (c, t )
where
c = check g e t
Booleans
/*@ val cmp : (x:int, y:int) => bool{b| b <=> (x < y)} */
let cmp = (x, y) => {
if (x < y) {
true
} else {
false
}
}
Path Sensitivity
/*@ val abs : x:int => int[v| 0<=v && x <= v] */
let abs = (x) => {
if x >= 0 {
x
} else {
0 - x
};
};
/*@ val test : (a:int, b:int) => int[v|0<=v && a + b <= v] */
let test = (a, b) => {
let t1 = abs(a);
let t2 = abs(b);
t1 + t2
}
Recursion
/*@ val sum : n:int => int[v|0 <= v && n <= v] */
let rec sum = (n) => {
if (n <= 0) {
0
} else {
let n1 = n-1;
let t1 = sum(n1);
n + t1
}
}
t ::=
| Bool{r} -- refined bool
e ::= ...
| if v e1 e2 -- branches
| letrec f = (e:t) -- recursive binder with annot
Environments
G ::= ... | G, _:t -- extend with "fresh"/"distinct" binder
Judgments, Rules (as before)
Subtyping (as before)
Rec binders must have type-sig
G |- v <== bool
G, _:int{v} |- e1 <== t
G, _:int{not v} |- e2 <== t
--------------------------------[Chk-If]
G |- if v e1 e2 <== t
G; f:t1 |- e1 <== t1 G; f:t1 |- e2 <== t2
------------------------------------------------[Chk-Rec]
G |- letrec f = (e1:t1) in e2 <== t2
Note Only when you add branches do you need
the singleton rule for variable lookup: without it
abs00.re
doesn't check!
--------------------------- [Syn-Var]
G |- x ==> singleton(G, x)
where
singleton(G, x) = v:b{p /\ v = x} if G(x) = b[v|p]
G(x) otherwise
check g (EIf v e1 e2) t = ((x :: v) => c1) /\ ((x :: ~v) => c2)
where
c1 = check g e1 t
c2 = check g e2 t
x = fresh
check g (ERec f e1 t1 e2) t2 = c1 /\ c2
where
c1 = check g' e1 t1
c2 = check g' e2 t2
g' = g; f:t1
as before
/*@ val assert : bool[b|b] => int */
let assert = (b) => { 0 };
/*@ val abs : x:int => int[?] */
let abs = (x) => {
if x >= 0 {
x
} else {
0 - x
};
};
/*@ val main : int -> int */
let main = (y) => {
let ya = abs(y); // neg: omit 'abs'
let ok = 0 <= ya;
assert(ok) // never fails
}
/*@ val assert : bool[b|b] => int */
let assert = (b) => { 0 };
/*@ val abs : x:int => int[?] */
let abs = (x) => {
if x >= 0 {
x
} else {
0 - x
};
};
/*@ val incf: int => int */
let incf = (x:nat) : nat => {
/*@ val wrap : (int -> int[?]) -> int[?] */
let wrap = (f) => {
let r = f(x);
r + 1
};
let res = wrap(abs);
let ok = 0 < res;
assert (ok)
};
p ::= ...
| K(x1,...,xn) -- horn-variable
r ::= ...
| [?] -- unknown
"fresh" each annotation
(as before)
t1 := fresh(s1)
G; f:t1 |- e1 <== t1
G; f:t1 |- e2 <== t2
------------------------------------- [Chk-Rec]
G |- letrec f = (e1:s1) in e2 <== t2
G |- e <== t t := fresh(s)
--------------------------- [Syn-Ann]
G |- e:s => t
"fresh" each annotation
fresh :: Env -> Type -> Type
fresh g (B[r]) = B[freshR g B r]
where
r' = freshR g r
fresh g (x:s -> t) = x:s' -> t'
where
s' = fresh g s
t' = fresh (g; x:s') t
freshR :: Env -> Base -> Reft -> Reft
freshR _ _ [v|p] = [v|p]
freshR g B [?] = [v|k(v, x1...)]
where
v = freshV
k = freshK [B, s1...]
(x1:s1...) = g
Rest is swept under the horn solving rug?
check g (ERec f e1 s1 e2) t2 = c1 /\ c2
where
c1 = check g' e1 t1
c2 = check g' e2 t2
g' = g; f:t1
t1 = fresh s1
synth (g, e:s) = (c, t)
where
c = check g e t
t = fresh s
see tests/L4/{pos, neg}
G ::= ...
| G, a -- ^ type variables
t :== ...
| a[r] -- ^ refined base type
| all a. t -- ^ forall types (quantifiers at the top)
e :== ...
| Λ a. e -- ^ type abstraction
| e [t] -- ^ type application
The real hassle here is the elaboration step that adds the explicit type abstraction and application.
G, v:int{p} |- q[w:=v]
-------------------------[Sub-Base]
G |- b[v|p] <: b[w|q]
G |- t1 <: t2 [a2 := a1]
------------------------------[Sub-All]
G |- all a1. t1 <: all a2. t2
G, a |- e <== s
------------------------ [Chk-TLam]
G |- Λ a. e <== all a. s
G |- e ==> all a. s
------------------------- [Syn-TApp]
G |- e[t] ==> s [ a := t]
check g (ETLam a e _) (TAll b t) | a == b = do
check g e t
synth g (ETApp e t l) = do
(ce, te) <- synth g e
case te of
TAll a s -> do tt <- Misc.traceShow "REFRESH" <$> refresh l g t
return (ce, tsubst a tt s)
_ -> failWith "Type Application to non-forall" l
containers
- head00
data-refinement
- tuple00
- olist00
measures
- head01
- tail01
- append
d ::= [C1:t1,...] -- ^ data-definition
G ::= ...
| G, d -- ^ data-definitions
a := C(x1...) => e -- ^ switch-alternative
e :== ...
| C(x1...) -- ^ constructor
| switch(x){ a1...} -- ^ destructor
G,v:int{p} |- q[w:=v] G |- si <: ti
-----------------------------------------[Sub-TCon]
G |- (C s1...)[v|p] <: (C t1...)[w|q]
Environment Extension
G(y) = s
-----------------------------[G-Scr]
G | y + 0 * t ~~> G, y:s/\t
G, z:s | y + zs * t[z/x] ~~> G'
---------------------------------[G-Ext]
G | y + z;zs * (x:s -> t) ~~> G'
G | y |- a_i <== t
----------------------------[Chk-Switch]
G |- switch y {a_1...} <== t
unfold(G, c, y) === s G | y + z... * s ~~> G' G' |- e <== t
----------------------------------------------------------------- [Chk-Alt]
G | y |- C z... -> e <== t
G(y) === tc[ts] G(c) === all as. t
------------------------------------- [Unfold]
unfold(g, c, y) === t [as := ts]
singleton(G, x) = v:b [p /\ v = x] if G(x) = b[v|p]
v:(c ts)[p /\ v = x] if G(x) = c ts
G(x) otherwise