A Gentle Introduction to F*

fstar-logo

Cătălin Hriţcu, Inria Paris

MPRI 2-30, Paris, Friday, 6 January 2017

What is F*?

  • Functional programming language
  • Advanced type and effect system
  • Semi-automated program verification tool
  • Proof assistant for writing/checking math proofs

Yesterday's poll

fp-poll

Functional programming in F*

Functional programming paradigm

Computing by evaluating expressions and declarations

let foo x y = (let z = y + 1 in z - y)

Recursive functions

let rec factorial n = (if n = 0 then 1 else n * (factorial (n - 1)))

Immutable data structures

let rec map f xs = match xs with
                  | Nil        -> Nil
                  | Cons hd tl -> Cons (f hd) (map f tl)

Unnamed functions (lambdas expressions, first-class values)

map (fun x -> x + 42) (Cons 1 (Cons 2 (Cons 3 Nil)))

Functional programming paradigm (2)

avoid mutable state and other “side-effects” when possible

fold_right (+) (map incr xs) 0

this pure point-free style considered better than:

let sum = alloc 0 in
foreach xs (fun x ->
  sum := !sum + x + 1
)

Functional programming paradigm (3)

but of course, can't always avoid side-effects

module Hello

open FStar.IO

let main = print_string "Hello F*\n"
$ fstar.exe --no_extract ... --odir out --codegen OCaml Hello.fst
Extracting module Hello
...
ocamlfind opt ... out/Hello.ml -o Hello.exe
./Hello.exe
Hello F*!

Functional programming

  • General-purpose functional languages:
    • OCaml, F#, Standard ML, Haskell, Scheme,
  • Getting increasingly popular in industry
    • Was always a big deal in research
  • Ideas picked up by mainstream languages:
    • generics in Java and C#
    • lambdas in Python, JavaScript, Java, C#, C++11, Rust
    • type inference, garbage collection, etc
  • F* is an intersection of OCaml and F#
    • F* uses OCaml and F# for executing programs
    • C backend for restricted subset (Low*/KreMLin)

Types in F*

ML types and type inference

$ocaml
...
# 42;;
- : int = 42
# let x = 2 in x + 1;;
- : int = 3
# "Hello";;
- : string = "Hello"
# let rec factorial n = (if n = 0 then 1 else n * (factorial (n - 1)));;
val factorial : int -> int = <fun>
# List.map (fun x -> x + 42) [1;2;3];;
- : int list = [43; 44; 45]
# List.map (fun x -> x + 42) ["one";"two";"three"];;
                               ^^^
Error: This expression has type string but an expression was expected of type int

Abstract types: an interface

Stack.fsti

module Stack

  val stack : Type0  (* type stack *)

  val empty : stack
  val push : int -> stack -> stack
  val is_empty : stack -> bool
  val pop : stack -> option stack
  val top : stack -> option int

A client of this interface

StackClient.fst

module StackClient

  let main =
    let s0 = Stack.empty in
    let s1 = Stack.push 3 s0 in
    let s2 = Stack.push 4 s1 in
    Stack.top s2

An implementation using lists of ints

Stack.fst

module Stack

  let stack = list int
  let empty = []
  let push x xs = x :: xs
  let is_empty xs = match xs with
                    | [] -> true
                    | x::xs' -> false
  let pop xs = match xs with
               | [] -> None
               | x::xs' -> Some xs'
  let top xs = match xs with
               | [] -> None
               | x::xs' -> Some x

Clients cannot break abstraction

StackClientBad.fst

module StackClientBad

  let main =
    let s0 = Stack.empty in
    let s1 = Stack.push 3 s0 in
    2 :: s1
[hritcu@detained code]$ fstar.exe StackClientBad.fst
./StackClientBad.fst(6,9-6,11) : Error
Expected expression of type "list (?32566 s0 s1)";
got expression "s1" of type "Stack.stack"

More precise types in F*

Refinement types

type nat = x:int{x>=0}

Function types (together with refinements)

val factorial : nat -> nat
let rec factorial n = (if n = 0 then 1 else n * (factorial (n - 1)))

Dependent function types

val incr : x:int -> y:int{x < y}
let incr x = x + 1

Quiz: can you find other types for incr?

Purity in F*

The F* functions we saw so far were all pure/total functions

Tot marker = no side-effects, terminates on all inputs

val incr : x:int -> Tot (y:int{x < y})
let incr x = x + 1
val factorial : nat -> Tot nat
let rec factorial n = (if n = 0 then 1 else n * (factorial (n - 1)))

Quiz: how about giving this weak type to factorial?

val factorial : int -> Tot int
let rec factorial n = (if n = 0 then 1 else n * (factorial (n - 1)))
                                                            ^^^^^
Subtyping check failed; expected type (x:int{(x << n)}); got type int

factorial (-1) loops! (int type in F* is unbounded)

Termination checking

  • based on well-founded ordering on expressions (<<)
    • naturals related by < (negative ones unrelated)
    • inductives (e.g. lists) related by subterm ordering
  • arbitrary pure expression as metric (decreases e)
      val ackermann: m:nat -> n:nat -> Tot nat (decreases %[m;n])
      let rec ackermann n m =
        if m=0 then n + 1
        else if n = 0 then ackermann 1 (m - 1)
        else ackermann (ackermann (n - 1) m) (m - 1)
  • default metric is lex ordering of all (non-function) args
      val ackermann: m:nat -> n:nat -> Tot nat

The divergence effect (Dv)

We might not want to prove all code terminating

    val factorial : int -> Dv int

Some useful code really is not always terminating

val eval : exp -> Dv exp
let rec eval e =
  match e with
  | App (Lam x e1) e2 -> eval (subst x e2 e1)
  | App e1 e2 -> App (eval e1) (eval e2)
  | Lam x e1 -> Lam x (eval e1)
  | _ -> e
let main = eval (App (Lam 0 (App (Var 0) (Var 0)))
                     (Lam 0 (App (Var 0) (Var 0))))
./Divergence.exe

Effects in F*

  • F* encapsulates effects

    • e.g. pure code cannot call divergent code
    • only pure code can appear in specifications
      val factorial : int -> Dv int
      type tau = x:int{x = factorial (-1)}
                       ^^^^^^^^^^^^^^^^^^
      Expected a pure expression; got an expression ... with effect "DIV"
  • ML effects are implemented primitively:

    • ML effect: non-termination, state, exceptions, input-output
    • user can define new effects using monads (Haskell)
  • F* automatically lifts effects as needed (Tot<Dv<ML)

    • e.g. divergent code can call pure code directly
      (1+2) + factorial (-1) : Dv int

State in F* (mutable references)

val swap_add_sub : r1:ref int -> r2:ref int -> St unit
let swap_add_sub r1 r2 =
  r1 := !r1 + !r2;
  r2 := !r1 - !r2;
  r1 := !r1 - !r2

let main =
  let r1 = alloc 1 in
  let r2 = alloc 2 in
  swap_add_sub r1 r2;
  print_string ("r1=" ^ string_of_int !r1 ^ "; " ^
                "r2=" ^ string_of_int !r2 ^ "\n")
...
r1=2; r2=1

Exercise: write a more natural implementation for swap

Verification in F*

Verifying pure programs

  • Using refinements (saw this already)
    val factorial : nat -> Tot nat
  • Can equivalently use pre- and post- conditions for this
    val factorial : x:int -> Pure int (requires (x >= 0))
                                    (ensures (fun y -> y >= 0))
  • Tot is actually just an abbreviation
    Tot t = Pure t (requires True) (ensures (fun _ -> True))

F* program verification system

  • Each computation assigned
    • an effect (Pure, ST)
    • a result type (int)
    • a Hoare-style specification (pre- and post- conditions)
      val factorial : x:int -> Pure int (requires (x >= 0))
                                    (ensures (fun y -> y >= 0))
  • Type inference also computes weakest preconditions
    • works generically for any effect (using Dijkstra monads)
    • depend on the effect, simple for Pure, more complex for ST and ML
  • Weakest preconditions checked against user's spec
    • Logical verification conditions discharged by SMT solver (Z3)
  • Related tools: Dafny, Boogie, Frama-C, Spec#, VCC, Why3

Verifying stateful programs

  • Verifying the swap function
    val swap_add_sub : r1:ref int -> r2:ref int -> ST unit
        (requires (fun h' -> r1<>r2))
        (ensures (fun h' _ h ->
                    let x1 = sel h' r1 in
                    let x2 = sel h' r2 in
                    equal h (upd (upd h' r1 x2) r2 x1)))
    let swap_add_sub r1 r2 =
    r1 := !r1 + !r2;
    r2 := !r1 - !r2;
    r1 := !r1 - !r2
  • St is actually just an abbreviation
    St t = ST t (requires True) (ensures (fun _ _ _ -> True))

Heap operations (simplified)

module FStar.Heap

  type heap

  val sel   : #a:Type -> heap -> ref a -> Tot a
  val upd   : #a:Type -> heap -> ref a -> a -> Tot heap
  val equal : heap -> heap -> Tot bool

  assume SelUpd1 :           sel (upd h r v) r  == v
  assume SelUpd2 : r<>r' ==> sel (upd h r v) r' == sel h r'

module FStar.ST

  val (!): #a:Type -> r:ref a -> ST a
    (requires (fun h -> True))
    (ensures (fun h0 x h1 -> equal h0 h1 /\ x==sel h0 r))

  val (:=): #a:Type -> r:ref a -> v:a -> ST unit
    (requires (fun h -> True))
    (ensures (fun h0 x h1 -> equal h1 (upd h0 r v)))

Uses of F*

Ongoing research on F*

Next steps

  • in 1 weeks: Verifying Stateful Programs in F*

  • in 2 weeks: Verified Low-Level Programming Embedded in F* (Jonathan Protzenko)

  • in 3 weeks: From “Hoare Logic” to “Dijkstra Monads for Free”

  • in 4-5 weeks: Verifying Crypto Implementations in F* (Karthik)