Internals
The encoding of Rust in Lean has three main components:
- the syntax (items, functions,
if
-else
,match
, etc), defined by the backend (/rust-engine/src/backends/lean.rs
) - the primitives/intrinsics (
u32
,isize
, slices, etc) defined by in the Prelude (hax-lib/proof-libs/lean
) - the models of core and std libraries.
While mostly separated, the Backend make some assumption on the Prelude,
typically when it inserts notations for some symbol (i.e. +?
for addition).
[!Disclaimer] : The lean backend is still experimental. See the list of open issues for known problems and workaround. See also the Hax Zulip for technical support.
Backend
Monadic encoding
All rust computations can panic or diverge, while Lean ones cannot (by default). To account for this, Rust types are wrapped inside a monad that represents the possible results:
inductive Error where
| assertionFailure: Error
| integerOverflow: Error
| divisionByZero: Error
| arrayOutOfBounds: Error
| maximumSizeExceeded: Error
| panic: Error
| undef: Error
inductive Result.{u} (α : Type u) where
| ok (v: α): Result α
| fail (e: Error): Result α
| div
This monadic encoding shows for simple expressions: the result of the lean
extracted function is not u32
but Result u32
.
The backend relies on the
do-notation:
all functions start with the do
keyword, indicating that the sequence of bindings should
actually be understood as bindings in the monad, propagating potential
errors to the top.
The do
keywords enables the lifting ←
and the pure
operators. Intuitively,
lifting turns a value of type Result T
into a value of type T
by turning the
rest of the program into a use of bind
. Conversely, pure
turns a value of
type T
into a value of type Result T
. This shows also for let-bindings :
Currently, the backend does not try to be parsimonious with the introduction of pure
and ←
.
Structs
Type definitions
Rust structs are encoded as Lean
structures. The
special case of tuple
structs
are also encoded as Lean structures, where the fields are numbered : _0
, _1
,
etc. See for instance :
Expressions, accessors and pattern-matching
Building, accessing and destructing structs :
-- Building
let s1 : S1 ← (pure
(S1.mk
(f1 := (0 : usize))
(f2 := (1 : usize))));
let t3 :
(T3 T0 (T1 i32) (T2 i32 i32))
← (pure
(T3.mk
T0.mk
(T1.mk (1 : i32))
(T2.mk
(1 : i32)
(2 : i32))));
-- Matching
let ({f1 := (f1 : usize),
f2 := (f2 : usize)} : S1) ←
(pure s1);
let (⟨(⟨⟩ : T0),
(⟨(_ : i32)⟩ : (T1 i32)),
(⟨(_ : i32), (_ : i32)⟩
: (T2 i32 i32))⟩ :
(T3 T0 (T1 i32) (T2 i32 i32))) ←
(pure t3);
-- Accessing
let (_ : (Tuple2 usize usize)) ←
(pure
(Tuple2.mk
(S1.f1 s1)
(S1.f2 s1)));
let (_ : i32) ← (pure
(T2._1 t2));
let (_ : T0) ← (pure
(T3._0 t3));
let (_ : (T1 i32)) ← (pure
(T3._1 t3));
let (_ : (T2 i32 i32)) ← (pure
(T3._2 t3));
let (_ : i32) ← (pure
(T2._1 (T3._2 t3)));
Enums
Type definitions
Rust enums are encoded as Lean inductive types. Variants with record fields use named arguments, whereas variants with tuple fields use normal positional arguments.
Expressions and pattern-matching
// Building
let e_v1 = E::V1;
let e_v2 = E::V2;
let e_v3 = E::V3(23);
let e_v4 = E::V4(23, 12, 1);
let e_v5 = E::V5 { f1: 23, f2: 43 };
let e_v6 = E::V6 { f1: 12, f2: 13 };
// Matching
match e_v1 {
E::V1 => (),
E::V2 => (),
E::V3(_) => (),
E::V4(x1, x2, x3) => {
let y1 = x1 + x2;
let y2 = y1 - x2;
let y3 = y2 + x3;
()
}
E::V5 { f1, f2 } => (),
E::V6 {
f1,
f2: other_name_for_f2,
} => (),
}
def enums (_ : Tuple0)
: Result Tuple0
:= do
let e_v1 : E ← (pure E.V1);
let e_v2 : E ← (pure E.V2);
let e_v3 : E ← (pure
(E.V3 (23 : usize)));
let e_v4 : E ← (pure
(E.V4
(23 : usize)
(12 : usize)
(1 : usize)));
let e_v5 : E ← (pure
(E.V5
(f1 := (23 : usize))
(f2 := (43 : usize))));
let e_v6 : E ← (pure
(E.V6
(f1 := (12 : usize))
(f2 := (13 : usize))));
(match e_v1 with
| (E.V1 ) => do Tuple0.mk
| (E.V2 ) => do Tuple0.mk
| (E.V3 (_ : usize))
=> do Tuple0.mk
| (E.V4
(x1 : usize)
(x2 : usize)
(x3 : usize))
=> do
let y1 : usize ← (pure
(← x1 +? x2));
let y2 : usize ← (pure
(← y1 -? x2));
let y3 : usize ← (pure
(← y2 +? x3));
Tuple0.mk
| (E.V5
(f1 := (f1 : usize))
(f2 := (f2 : usize)))
=> do Tuple0.mk
| (E.V6
(f1 := (f1 : usize))
(f2 :=
(other_name_for_f2 : usize)))
=> do Tuple0.mk)
Traits
Rust traits are represented as Lean classes, while Rust impl are Lean
instances. The Lean code relies on the typeclass inference of Lean. Hax exposes
identifiers for rust impls (that are otherwise implicit), like
8040238289193487104
. Lean uses them for naming fields or parameters.
Supertraits
Super trait bounds are represented as extra fields
Associated types
The support for associated types is currently restricted to types defined within the current trait
class Foo (Self : Type) (T : Type) where
class Bar (Self : Type) where
class T1 (Self : Type) where
T : Type
f : Self -> T -> Result T
class T3 (Self : Type) where
T : Type
[_constr_13086648656846024831 :
(Foo T Tuple0)]
Tp : Type
[_constr_15450263461214744089 : (Foo Tp T)]
f (A : Type) [(Bar A)] :
Self -> T -> Tp -> Result usize
class T2 (Self : Type) where
T : Type
[_constr_18277713886489441014 : (T1 T)]
f : Self -> T -> Result usize
Prelude
See the Hax Lean library