Here is the presentation of F*. F* is a proof-oriented, general-purpose programming language that supports both pure functional programming and effect programming, and combines the power of expressing dependent types with the automation of proofs.
F* is a type-dependent programming language that has several roles:
- A general-purpose programming language that encourages higher-order functional programming with effects, in the tradition of the ML family of languages.
- A compiler that translates F* programs to OCaml or F#, and even C or Wasm, for execution.
- A proof assistant, which allows you to state and prove properties of programs.
- A program verification engine, which uses SMT solvers to partially automate program proofs.
- A metaprogramming system that supports the programmatic construction of F* programs and proof automation procedures.
To achieve these goals, the F* design is structured around several key elements, described below. Maybe all of this doesn’t make sense to you – that’s okay, you’ll learn along the way.
- A central complete functional language with comprehensive dependent types, including an extensional form of type conversion, indexed inductive types and pattern matching, recursive functions with semantic completion checking, dependent type refinements and subtyping, as well as polymorphism on predicative hierarchy universes.
- A system of user-defined index effects, for static modeling, encapsulation, and reasoning about various forms of computational effects, including the primitive notion of general recursion and divergence, as well as an open system of user-defined effects examples including condition, exceptions, concurrency, algebraic effects, and several others.
- Integrated coding of the classical fragment of F* logic into the first-order logic of the SMT solver, which allows automatic retrieval of many proofs.
- Reasoning within F* about the syntax and proof state of F*, allowing Meta-F* programs to manipulate the syntax and goals of F* proofs and users to construct proofs interactively with tactics.
DSLs integrated into F*
In practice, instead of a single language, the F* ecosystem is also a collection of domain-specific languages (DSLs). A common use of F* is to integrate programming languages with different levels of abstraction or for specific programming tasks, and to design an integrated language with domain-specific reasoning, proof automation, and compilation backends. Here are some examples:
- Low*, a shallow DSL for sequential programming with a Type-C memory model including explicit stack and heap memory management; Hoare logic for partial correction based on implicit dynamic frames; and a custom backend (Karamel) for translating Low* programs into C for later translation by commercially available C compilers.
- EverParse, a shallow DSL integration (layered to DSL Low*) combiner of parsers and serializers, for low-level binary formats.
- Vale, a deeply integrated DSL for structured programming in user-defined assembly language, with Hoare logic for full correction and a printer for putting verified programs into assembly syntax compatible with a variety of standard assemblers.
- Steel, a shallow integration of concurrency as an effect in F*, with an extensible concurrency logic for partial correction as the underlying program logic and proof automation built by a combination of Meta-F* tactics, higher-order unification, and SMT.
To get a taste of F*, let’s dive into a few examples. At this point we don’t expect you to understand these examples in detail, but they should give you an overview of what is possible with F*.
F* is language dependent typing
Type-dependent programming allows you to more precisely capture program properties and invariants using types. Here’s a classic example: a guy i-th year represents a vector not dimensions of typical elements have ; or, more simply, a list not values of each type have. Like other dependent type languages, F* supports inductive type definitions.
1 | type vec (a:Type) : nat -> Type = | Nil : vec a 0 | Cons : #n:nat -> hd:a -> tl:vec a n -> vec a (n + 1) |
Vector operations can be assigned types that describe their behavior in terms of the length of the vector.
For example, here is a recursive function to add join two vectors. Its type indicates that the resulting vector has a length that is the sum of the lengths of the input vectors.
1 | let rec append #a #n #m (v1:vec a n) (v2:vec a m) : vec a (n + m) = match v1 with | Nil -> v2 | Cons hd tl -> Cons hd (append tl v2) |
Of course, once a function like to add is defined, can be used to define other operations, and its type allows proving other properties. For example, it is easy to show that flipping a vector does not change its length.
1 | let rec reverse #a #n (v:vec a n) : vec a n = match v with | Nil -> Nil | Cons hd tl -> append (reverse tl) (Cons hd Nil) |
Finally, to get an element of a vector, we can program a selector whose type also includes a type of finishing to specify that the index I is less than the length of the vector.
1 | let rec get #a #n (i:nat{i < n}) (v:vec a n) : a = let Cons hd tl = v in if i = 0 then hd else get (i - 1) tl |
Although such examples can be programmed in other dependently typed languages, they can often be tedious due to various technical limitations. F* provides basic logic with a more flexible notion of equality to facilitate programming and proof. For now, the bottom line is that type-dependent programming models that are quite technical in other languages are often quite natural in F*.
F* supports user-defined effect programming
Although functional programming is at the heart of the language, F* is not limited to pure functions. In fact, F* is a Turing complete language. That this is worth mentioning might surprise readers experienced with general-purpose programming languages like C# or Scala, but not all languages are type-dependent Turing complete, as non-interruption can break robustness. However, F* supports general recursive functions and non-termination in a safe manner, without compromising robustness.
In addition to non-interrupts, F* supports a system of user-defined computing effects that can be used to model a variety of programming idioms, including things like mutable state, exceptions, concurrency, input-output, and so on.
Below is code in the F* dialect called Low* that provides a model of C-style sequential and imperative programming with variable memory. Function malloc_copy_free assign an array destinationcopies the contents of a byte array heart in dest, not to distribute heart and returns destination.
1 | let malloc_copy_free (len:uint32 { 0ul < len }) (src:lbuffer len uint8) : ST (lbuffer len uint8) (requires fun h -> live h src /\ freeable src) (ensures fun h0 dest h1 -> live h1 dest /\ (forall (j:uint32). j < len ==> get h0 src j == get h1 dest j)) = let dest = malloc 0uy len in memcpy len 0ul src dest; free src; dest |
We'll have to wait a long time to explain this code in detail, but here are two main takeaways:
- A procedure type signature verifies that, under certain constraints imposed on the caller, execution malloc_copy_free safe (for example, it does not read beyond the limits of the allocated memory) and that it is correct (that is, it copies successfully heart IN destination without modifying any other memory).
- Given the implementation of the procedure, F* actually constructs a mathematical proof that it is secure and correct given its signature.
Although other program verification tools offer similar functionality to what we've used here, F* differs in that program semantics with side effects (such as reading and writing memory) are fully encoded in F* logic using system user-defined effects.
While malloc_copy_free is programmed in Low* and specified using a special kind of Floyd-Hoare logic, there is nothing special about F*.
Here, for example, is a competing program in another user-defined F* dialect called Steel. It increments two heap-allocated references in parallel and is specified for safety and correctness in concurrent allocation logic, a different type of Floyd-Hoare logic than the one we used for malloc_copy_free.
1 | let par_incr (#v0 #v1:erased int) (r0 r1:ref int) : SteelT _ (pts_to r0 v0 `star` pts_to r1 v1) (fun _ -> pts_to r0 (v0 + 1) `star` pts_to r1 (v1 + 1)) = par (incr r0) (incr r1) |
As an F* user, you can choose a programming model and set of program proof abstractions to suit your needs.
Source : Introducing the F* language
and you
What is your opinion on the subject?
See also: