Getting off the ground
To start writing some F* programs, we’ll need to learn some basics about the syntax of the language and some core concepts of types and functions.
Text Editors
F* can be used as a command line tool with any text editor. If you’re viewing this in the interactive online tutorial, you can use the Ace-based text editor alongside, which provides some basic conveniences like syntax highlighting. However, beyond casual use, most users of F* rely on one of the following IDE plugins.
fstar-mode.el, which provides several utilities for interactively editing and checking F* files in emacs.
fstar-vscode-assistant, which also provides interactive editing and checking support in VS Code.
The main benefit to using these IDE plugins is that they allow you to incrementally check just the changing suffix of an F* file, rather than rechecking the entire file in batch mode. They also provide standard things like jumping to definitions, type of a symbol etc.
Both these plugins rely on a generic but custom interaction protocol implemented by the F* compiler. It should be possible to implement IDE support similar to fstar-mode.el or fstar-vscode-assistant in your favorite plugin-capable editor.
Basic syntactic structure
An F* program is a collection of modules, with each
module represented by a single file with the filename extension
.fst. Later, we’ll see that a module’s interface is in a separate
.fsti file and allows hiding details of a module’s implementation
from a client module.
A module begins with the module’s name (which must match the name of
its file, i.e., module A is in A.fst) and contains a sequence
of top-level signatures and definitions. Module names always begin
with a capital letter.
Signatures ascribe a type to a definition, e.g.,
val f : t.
Definitions come in several flavors: the two main forms we’ll focus on when programming with total functions are
possibly recursive definitions (let bindings,
let [rec] f = e)and, inductive type definitions (datatypes,
type t = | D1 : t1 | ... | Dn : tn)
In later sections, we’ll see two other kinds of definition: user-defined indexed effects and sub-effects.
Primitives
Every F* program is checked in the context of some ambient primitive
definitions taken from the core F* module Prims.
False
The type False has no elements. Since there are no terms that
satisfy e : False, the type False is the type of unprovable
propositions.
Unit
The type unit has a single element denoted (), i.e., () :
unit.
Booleans
The type bool has two elements, true and false. Note, the
lowercase false is a boolean constant, distinct from the uppercase
False type.
The following primitive boolean operators are available, in decreasing order of precedence.
not: Boolean negation (unary, prefix)&&: Boolean conjunction (binary, infix)||: Boolean disjunction (binary, infix)
Conditionals
You can, of course, branch on a boolean with if/then/else
if b then 1 else 0
if b1 && b2 || b3
then 17
else 42
Integers
The type int represents unbounded, primitive mathematical
integers. Its elements are formed from the literals 0, 1, 2, ...,
and the following primitive operators, in decreasing order of
precedence.
-: Unary negation (prefix)-: Subtraction (infix)+: Addition (infix)/: Euclidean division (infix)%: Euclidean modulus (infix)op_Multiply: Unfortunately, the traditional multiplication symbol*is reserved by default for the tuple type constructor. Use the moduleFStar.Multo treat*as integer multiplication.<: Less than (infix)<=: Less than or equal (infix)>: Greater than (infix)>=: Greater than or equal (infix)
Note
F* follows the OCaml style of no negative integer literals,
instead negate a positive integer like (- 1).
Boolean refinement types
The F* core library, Prims, defines the type of
natural numbers as follows
let nat = x:int{x >= 0}
This is an instance of a boolean refinement type, whose general form
is x:t { e } where t is a type, and e is a bool-typed term
that may refer to the t-typed bound variable x. The term e
refines the type t, in the sense that the set S denoted by t
is restricted to those elements x \(\in\) S for which e evaluates to
true.
That is, the type nat describes the set of terms that evaluate to an
element of the set {0, 1, 2, 3, ...}.
But, there’s nothing particularly special about nat. You can define
arbitrary refinements of your choosing, e.g.,
let empty = x:int { false } //the empty set
let zero = x:int{ x = 0 } //the type containing one element `0`
let pos = x:int { x > 0 } //the positive numbers
let neg = x:int { x < 0 } //the negative numbers
let even = x:int { x % 2 = 0 } //the even numbers
let odd = x:int { x % 2 = 1 } //the odd numbers
If you’re coming from a language like C or Java where a type primarily describes some properties about the representation of data in memory, this view of types as describing arbitrary sets of values may feel a bit alien. But, let it sink in a bit—types that carve out precise sets of values will let you state and check invariants about your programs that may otherwise have only been implicit in your code.
Note
Refinement types in F* trace their lineage to F7, a language developed at Microsoft Research c. 2007 – 2011. Liquid Haskell is another language with refinement types. Those languages provide additional background and resources for learning about refinement types.
Boolean refinements are a special case of a more powerful form of propositional refinement type in F*. Refinement types, in conjunction with dependent function types, are, in principle, sufficient to encode many kinds of logics for program correctness. However, refinement types are just one among several tools in F* for program specification and proof.
Refinement subtyping
We have seen so far how to define a new refinement type, like nat or
even. However, to make use of refinement types we need rules that
allow us to:
check that a program term has a given refinement type, e.g., to check that
0has typenat. This is sometimes called introducing a refinement type.make use of a term that has a refinement type, e.g., given
x : evenwe would like to be able to writex + 1, treatingxas anintto add1to it. This is sometimes called eliminating a refinement type.
The technical mechanism in F* that supports both these features is called refinement subtyping.
If you’re used to a language like Java, C# or some other
object-oriented language, you’re familiar with the idea of
subtyping. A type t is a subtype of s whenever a program term
of type t can be safely treated as an s. For example, in Java,
all object types are subtypes of the type Object, the base class
of all objects.
For boolean refinement types, the subtyping rules are as follows:
The type
x:t { p }is a subtype oft. That is, givene : (x:t{p}), it is always safe to eliminate the refinement and considereto also have typet.For a term
eof typet(i.e.,e : t),tis a subtype of the boolean refinement typex:t { p }wheneverp[e / x](p[e/x]is notation for the termpwith the variablexreplaced bye), is provably equal totrue. In other words, to introducee : tat the boolean refinement typex:t{ p }, it suffices to prove that the termpwithesubstituted for bound variablex, evaluates totrue.
The elimination rule for refinement types (i.e., the first part above)
is simple—with our intuition of types as sets, the refinement type
x:t{ p } refines the set corresponding to t by the predicate
p, i.e., the x:t{ p } denotes a subset of t, so, of course
x:t{ p } is a subtype of t.
The other direction is a bit more subtle: x:t{ p } is only a
subtype of p, for those terms e that validate p. You’re
probably also wondering about how to prove that p[e/x] evaluates
to true—we will look at this in detail later. But, the short
version is that F*, by default, uses an SMT solver to prove such fact,
though you can also use tactics and other techniques to do so.
An example
Given x:even, consider proving x + 1 : odd; it takes a few
steps:
The operator
+is defined in F*’s library. It expects both its arguments to have typeintand returns anint.To prove that the first argument
x:evenis a valid argument for+, we use refinement subtyping to eliminate the refinement and obtainx:int. The second argument1:intalready has the required type. Thus,x + 1 : int.To conclude that
x + 1 : odd, we need to introduce a refinement type, by proving that the refinement predicate ofoddevaluates to true, i.e.,x + 1 % 2 = 1. This is provable by SMT, since we started with the knowledge thatxis even.
As such, F* applies subtyping repeatedly to introduce and eliminate
refinement types, applying it multiple times even to check a simple
term like x + 1 : odd.
Functions
We need a way to define functions to start writing interesting programs. In the core of F*, functions behave like functions in maths. In other words, they are defined on their entire domain (i.e., they are total functions and always return a result) and their only observable behavior is the result they return (i.e., they don’t have any side effect, like looping forever, or printing a message etc.).
Functions are first-class values in F*, e.g., they can be passed as arguments to other functions and returned as results. While F* provides several ways to define functions, the most basic form is the \(\lambda\) term, also called a function literal, an anonymous function, or a simply a lambda. The syntax is largely inherited from OCaml, and this OCaml tutorial provides more details for those unfamiliar with the language. We’ll assume a basic familiarity with OCaml-like syntax.
Lambda terms
The term fun (x:int) -> x + 1 defines a function,
a lambda term, which adds 1 to its integer-typed parameter x. You
can also let F* infer the type of the parameter and write fun x ->
x + 1 instead.
Named functions
Any term in F* can be given a name using a let binding. We’ll
want this to define a function once and to call it many times. For
example, all of the following are synonyms and bind the lambda term
fun x -> x + 1 to the name incr
let incr = fun (x:int) -> x + 1
let incr (x:int) = x + 1
let incr x = x + 1
Functions can take several arguments and the result type of a function can also be annotated, if desired
let incr (x:int) : int = x + 1
let more_than_twice (x:int) (y:int) : bool = x > y + y
It’s considered good practice to annotate all the parameters and result type of a named function definition.
Note
In addition to decorating the types of parameters and the results
of function, F* allows annotating any term e with its expected
type t by writing e <: t. This is called a type
ascription. An ascription instructs F* to check that the
term e has the type t. For example, we could have written
let incr = fun (x:int) -> (x + 1 <: int)
Recursive functions
Recursive functions in F* are always named. To define them, one uses
the let rec syntax, as shown below.
open FStar.Mul
let rec factorial (n:nat)
: nat
= if n = 0
then 1
else n * factorial (n - 1)
This syntax defines a function names factorial with a single
parameter n:nat, returning a nat. The definition of factorial
is allowed to use the factorial recursively—as we’ll see in a
later chapter, ensuring that the recursion is well-founded (i.e., all
recursive calls terminate) is key to F*’s soundness. However, in this
case, the proof of termination is automatic.
Note
Notice the use of open FStar.Mul in the example above. This
brings the module FStar.Mul into scope and resolves the symbol
* to integer multiplication.
F* also supports mutual recursion. We’ll see that later.
Arrow types
Functions are the main abstraction facility of any functional language and their types are pervasive in F*. In its most basic form, function types, or arrows, have the shape:
x:t0 -> t1
This is the type of a function that
receives an argument
eof typet0, andalways returns a value of type
t1[e / x], i.e., the type of the returned value depends on the argumente.
It’s worth emphasizing how this differs from function types in other languages.
F*’s arrows are dependent—the type of the result depends on the argument. For example, we can write a function that returns a
boolwhen applied to an even number and returns astringwhen applied to an odd number. Or, more commonly, a function whose result is one greater than its argument.In F*’s core language, all functions are total, i.e., a function call always terminates after consuming a finite but unbounded amount of resources.
Note
That said, on any given computer, it is possible for a function call to fail to return due to resource exhaustion, e.g., running out of memory. Later, as we look at effects, we will see that F* also supports writing non-terminating functions.
Some examples and common notation
Functions are curried. Functions that take multiple arguments are written as functions that take the first argument and return a function that takes the next argument and so on. For instance, the type of integer addition is:
val (+) : x:int -> y:int -> int
Not all functions are dependent and the name of the argument can be omitted when it is not needed. For example, here’s a more concise way to write the type of
(+):val (+) : int -> int -> int
Function types can be mixed with refinement types. For instance, here’s the type of integer division—the refinement on the divisor forbids division-by-zero errors:
val (/) : int -> (divisor:int { divisor <> 0 }) -> int
Dependence between the arguments and the result type can be used to state relationships among them. For instance, there are several types for the function
let incr = (fun (x:int) -> x + 1):val incr : int -> int val incr : x:int -> y:int{y > x} val incr : x:int -> y:int{y = x + 1}
The first type
int -> intis its traditional type in languages like OCaml.The second type
x:int -> y:int{y > x}states that the returned valueyis greater than the argumentx.The third type is the most precise:
x:int -> y:int{y = x + 1}states that the resultyis exactly the increment of the argumentx.It’s often convenient to add refinements on arguments in a dependent function type. For instance:
val f : x:(x:int{ x >= 1 }) -> y:(y:int{ y > x }) -> z:int{ z > x + y }
Since this style is so common, and it is inconvenient to have to bind two names for the parameters
xandy, F* allows (and encourages) you to write:val f : x:int{ x >= 1 } -> y:int{ y > x } -> z:int{ z > x + y }
To emphasize that functions in F*’s core are total functions (i.e., they always return a result), we sometimes annotate the result type with the effect label “
Tot”. This label is optional, but especially as we learn about effects, emphasizing that some functions have no effects via theTotlabel is useful. For example, one might sometimes write:val f : x:int{ x >= 1 } -> y:int{ y > x } -> Tot (z:int{ z > x + y })
adding a
Totannotation on the last arrow, to indicate that the function has no side effects. One could also write:val f : x:int{ x >= 1 } -> Tot (y:int{ y > x } -> Tot (z:int{ z > x + y }))
adding an annotation on the intermediate arrow, though this is not customary.
Exercises
This first example is just to show you how to run the tool and interpret its output.
module Part1.GettingOffTheGround
let incr (x:int) : int = x + 1
Notice that the program begins with a module declaration. It
contains a single definition named incr. Definitions that appear
at the scope of a module are called “top-level” definitions.
You have several options to try out these examples.
F* online
To get started and for trying small exercises, the easiest way is via the online tutorial. If that’s where you’re reading this, you can just use the in-browser editor alongside which communicates with an F* instance running in the cloud. Just click on this link to load the code of an exercise in the editor.
That said, the online mode can be a bit slow, depending on the load at the server, and the editor is very minimalistic.
For anything more than small exercises, you should have a working local installation of the F* toolchain, as described next.
F* in batch mode
You can download pre-built F* binaries from here.
Once you have a local installation, to check a program you can run the
fstar at the command line, like so:
$ fstar Sample.fst
In response fstar should output:
Verified module: Sample
All verification conditions discharged successfully
This means that F* attempted to verify the module named Sample. In
doing so, it generated some “verification conditions”, or proof
obligations, necessary to prove that the module is type correct, and
it discharged, or proved, all of them successfully.
F* in emacs
Rather than running fstar in batch mode from the command line, F*
programmers using the emacs
editor often use fstar-mode.el, an editor plugin that
allows interactively checking an F* program. If you plan to use F* in
any serious way, this is strongly recommended.
Many types for incr
Here are some types for incr, including some types that are valid
and some others that are not.
This type claims that incr result is
greater than its argument and F* agrees—remember, the int type is
unbounded, so there’s no danger of the addition overflowing.
let incr1 (x:int) : y:int{y > x} = x + 1
This type claims that incr always returns a natural number, but it
isn’t true, since incrementing a negative number doesn’t always
produce a non-negative number.
let incr2 (x:int) : nat = x + 1
F* produces the following error message:
Sample.fst(11,26-11,31): (Error 19) Subtyping check failed; expected type
Prims.nat; got type Prims.int; The SMT solver could not prove the query, try to
spell your proof in more detail or increase fuel/ifuel (see also prims.fst(626,
18-626,24))
Verified module: Sample
1 error was reported (see above)
Source location
The error message points to Sample.fst(11,26-11,31), a source
range mentioned the file name, a starting position (line, column), and
an ending position (line, column). In this case, it highlights the
x + 1 term.
Severity and error code
The (Error 19) mentions a severity (i.e., Error, as opposed
to, say, Warning), and an error code (19).
Error message
The first part of the message stated what you might expect:
Subtyping check failed; expected type Prims.nat; got type Prims.int
The rest of the message provides more details, which we’ll ignore for now, until we’ve had a chance to explain more about how F* interacts with the SMT solver. However, one part of the error message is worth pointing out now:
(see also prims.fst(626,18-626,24))
Error messages sometimes mention an auxiliary source location in a
“see also” parenthetical. This source location can provide some more
information about why F* rejected a program—in this case, it points to
the constraint x>=0 in the definition of nat in prims.fst,
i.e., this is the particular constraint that F* was not able to prove.
So, let’s try again. Here’s another type for incr, claiming that
if its argument is a natural number then so is its result. This time
F* is happy.
let incr3 (x:nat) : nat = x + 1
Sometimes, it is convenient to provide a type signature independently
of a definition. Below, the val incr4 provides only the signature
and the subsequent let incr4 provides the definition—F* checks
that the definition is compatible with the signature.
val incr4 (x:int) : int
let incr4 x = x + 1
Try writing some more types for incr.
(Load exercise.)
Some answers
let incr5 (x:int) : y:int{y = x + 1} = x + 1
let incr6 (x:int) : y:int{x = y - 1} = x + 1
let incr7 (x:int) : y:int{if x%2 = 0 then y%2 = 1 else y%2 = 0} = x + 1
Computing the maximum of two integers
Provide an implementation of the following signature:
val max (x:int) (y:int) : int
There are many possible implementations that satisfy this signature, including trivial ones like:
let max x y = 0
Provide an implementation of max coupled with a type that is
precise enough to rule out definitions that do not correctly return
the maximum of x and y.
Some answers
val max (x:int) (y:int) : int
let max x y = if x >= y then x else y
val max1 (x:int) (y:int)
: z:int{ z >= x && z >= y && (z = x || z = y)}
let max1 x y = if x >= y then x else y
let max2 (x:int) (y:int)
: z:int{ z = max x y }
= if x > y
then x
else y
More types for factorial
Recall the definition of factorial from earlier.
open FStar.Mul
let rec factorial (n:nat)
: nat
= if n = 0
then 1
else n * factorial (n - 1)
Can you write down some more types for factorial?
Some answers
let rec factorial1 (n:nat)
: int
= if n = 0
then 1
else n * factorial1 (n - 1)
let rec factorial2 (n:nat)
: y:int{y>=1}
= if n = 0
then 1
else n * factorial2 (n - 1)
Fibonacci
Here’s a doubly recursive function:
let rec fibonacci (n:nat)
: nat
= if n <= 1
then 1
else fibonacci (n - 1) + fibonacci (n - 2)
What other types can you give to it?
Some answers
val fibonacci_1 : x:int -> y:int{y >= 1 /\ y >= x /\ (if x>=3 then y >= 2 else true)}
let rec fibonacci_1 n =
if n <= 1 then 1 else fibonacci_1 (n - 1) + fibonacci_1 (n - 2)
(* Try these other types too *)
(* val fibonacci_1 : int -> int *)
(* val fibonacci_1 : int -> nat *)
(* val fibonacci_1 : int -> y:int{y>=1} *)
(* val fibonacci_1 : x:int -> y:int{y>=1 /\ y >= x} *)
(* val fibonacci_1 : int -> Tot (x:nat{x>0}) *)
Comments
Block comments are delimited by
(*and*). Line comments begin with//.