A Gentle Introduction to F*

What is F*?

  • Functional programming language
  • Advanced type and effect system
  • Semi-automated program verification tool
  • Checker for manual math proofs

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

  • Was always a big deal in research
  • General-purpose functional languages:
    • OCaml, F#, Standard ML, Haskell, Scheme,
  • Getting increasingly popular in industry:
    • Microsoft F#, financial industry,
    • ideas picked up by mainstream languages:
      • generics in Java and C#
      • lambdas in Python, JavaScript, Java, C#, C++11, Rust
      • type inference
  • F* language is an intersection of OCaml and F#
    • F* uses OCaml and F# for executing programs
    • work-in-progress C backend for restricted subset

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

  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

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: what 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 infinite)

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 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

module FStar.Heap

  val heap : Type0

  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)))

Exercise: verify imperative factorial

val factorial_tot : nat -> Tot nat
let rec factorial_tot x = if x = 0 then 1 else x * factorial_tot (x - 1)

(* TODO: write a stronger ensures clause for factorial that proves
         it does the same thing as factorial_tot *)
val factorial : r1:ref nat -> r2:ref nat -> ST unit
      (requires (fun h' -> True))
      (ensures (fun h' a h -> True))
let rec factorial r1 r2 =
  let x1 = !r1 in
  if x1 = 0
  then r2 := 1
  else 
    (r1 := x1 - 1;
     factorial r1 r2;
     r2 := !r2 * x1)

Monotonic log

(* global log of integers *)
val log : ref (list int)
let log = alloc []

val add_to_log : e:int -> ST unit
  (requires (fun h0 -> True))
  (ensures (fun h0 _ h1 ->
        mem e (sel h1 log) (* p1: new element added *)
    /\ (forall x. mem x (sel h0 log) ==> mem x (sel h1 log))
                            (* p2: all old elements preserved *)
  ))
let add_to_log e =
  log := (e :: !log)

let main =
  add_to_log 1;
  let log1 = !log in
  assert (mem 1 log1); (* proved by p1 *)
  add_to_log 2;
  let log2 = !log in
  assert (mem 2 log2  (* proved by p1 *)
       /\ mem 1 log2) (* proved by p2! *)

Uses of F*

Ongoing research on F*

Next steps

Thank you

F* team: Nikhil Swamy, Danel Ahman, Catalin Hritcu, Guido Martinez, Gordon Plotkin, Jonathan Protzenko, Aseem Rastogi, Chantal Keller, Antoine Delignat-Lavaud, Simon Forest, Karthikeyan Bhargavan, Cédric Fournet, Pierre-Yves Strub, Markulf Kohlweiss, Jean-Karim Zinzindohoue, Santiago Zanella-Béguelin, Daan Leijen, Nataliya Guts, Michael Hicks, Gavin Bierman, Clement Pit-Claudel, Perry Wang, Alfredo Pironti, Matthew Green, Chris Brzuska, Ben Dowling,