# Lowering to logic The key observation here is that the Rust trait system is basically a kind of logic, and it can be mapped onto standard logical inference rules. We can then look for solutions to those inference rules in a very similar fashion to how e.g. a [Prolog] solver works. It turns out that we can't *quite* use Prolog rules (also called Horn clauses) but rather need a somewhat more expressive variant. [Prolog]: https://en.wikipedia.org/wiki/Prolog ## Rust traits and logic One of the first observations is that the Rust trait system is basically a kind of logic. As such, we can map our struct, trait, and impl declarations into logical inference rules. For the most part, these are basically Horn clauses, though we'll see that to capture the full richness of Rust -- and in particular to support generic programming -- we have to go a bit further than standard Horn clauses. To see how this mapping works, let's start with an example. Imagine we declare a trait and a few impls, like so: ```rust trait Clone { } impl Clone for usize { } impl Clone for Vec where T: Clone { } ``` We could map these declarations to some Horn clauses, written in a Prolog-like notation, as follows: ```text Clone(usize). Clone(Vec) :- Clone(?T). // The notation `A :- B` means "A is true if B is true". // Or, put another way, B implies A. ``` In Prolog terms, we might say that `Clone(Foo)` -- where `Foo` is some Rust type -- is a *predicate* that represents the idea that the type `Foo` implements `Clone`. These rules are **program clauses**; they state the conditions under which that predicate can be proven (i.e., considered true). So the first rule just says "Clone is implemented for `usize`". The next rule says "for any type `?T`, Clone is implemented for `Vec` if clone is implemented for `?T`". So e.g. if we wanted to prove that `Clone(Vec>)`, we would do so by applying the rules recursively: - `Clone(Vec>)` is provable if: - `Clone(Vec)` is provable if: - `Clone(usize)` is provable. (Which is is, so we're all good.) But now suppose we tried to prove that `Clone(Vec)`. This would fail (after all, I didn't give an impl of `Clone` for `Bar`): - `Clone(Vec)` is provable if: - `Clone(Bar)` is provable. (But it is not, as there are no applicable rules.) We can easily extend the example above to cover generic traits with more than one input type. So imagine the `Eq` trait, which declares that `Self` is equatable with a value of type `T`: ```rust,ignore trait Eq { ... } impl Eq for usize { } impl> Eq> for Vec { } ``` That could be mapped as follows: ```text Eq(usize, usize). Eq(Vec, Vec) :- Eq(?T, ?U). ``` So far so good. ## Type-checking normal functions OK, now that we have defined some logical rules that are able to express when traits are implemented and to handle associated types, let's turn our focus a bit towards **type-checking**. Type-checking is interesting because it is what gives us the goals that we need to prove. That is, everything we've seen so far has been about how we derive the rules by which we can prove goals from the traits and impls in the program; but we are also interested in how to derive the goals that we need to prove, and those come from type-checking. Consider type-checking the function `foo()` here: ```rust,ignore fn foo() { bar::() } fn bar>() { } ``` This function is very simple, of course: all it does is to call `bar::()`. Now, looking at the definition of `bar()`, we can see that it has one where-clause `U: Eq`. So, that means that `foo()` will have to prove that `usize: Eq` in order to show that it can call `bar()` with `usize` as the type argument. If we wanted, we could write a Prolog predicate that defines the conditions under which `bar()` can be called. We'll say that those conditions are called being "well-formed": ```text barWellFormed(?U) :- Eq(?U, ?U). ``` Then we can say that `foo()` type-checks if the reference to `bar::` (that is, `bar()` applied to the type `usize`) is well-formed: ```text fooTypeChecks :- barWellFormed(usize). ``` If we try to prove the goal `fooTypeChecks`, it will succeed: - `fooTypeChecks` is provable if: - `barWellFormed(usize)`, which is provable if: - `Eq(usize, usize)`, which is provable because of an impl. Ok, so far so good. Let's move on to type-checking a more complex function. ## Type-checking generic functions: beyond Horn clauses In the last section, we used standard Prolog horn-clauses (augmented with Rust's notion of type equality) to type-check some simple Rust functions. But that only works when we are type-checking non-generic functions. If we want to type-check a generic function, it turns out we need a stronger notion of goal than Prolog can be provide. To see what I'm talking about, let's revamp our previous example to make `foo` generic: ```rust,ignore fn foo>() { bar::() } fn bar>() { } ``` To type-check the body of `foo`, we need to be able to hold the type `T` "abstract". That is, we need to check that the body of `foo` is type-safe *for all types `T`*, not just for some specific type. We might express this like so: ```text fooTypeChecks :- // for all types T... forall { // ...if we assume that Eq(T, T) is provable... if (Eq(T, T)) { // ...then we can prove that `barWellFormed(T)` holds. barWellFormed(T) } }. ``` This notation I'm using here is the notation I've been using in my prototype implementation; it's similar to standard mathematical notation but a bit Rustified. Anyway, the problem is that standard Horn clauses don't allow universal quantification (`forall`) or implication (`if`) in goals (though many Prolog engines do support them, as an extension). For this reason, we need to accept something called "first-order hereditary harrop" (FOHH) clauses -- this long name basically means "standard Horn clauses with `forall` and `if` in the body". But it's nice to know the proper name, because there is a lot of work describing how to efficiently handle FOHH clauses; see for example Gopalan Nadathur's excellent ["A Proof Procedure for the Logic of Hereditary Harrop Formulas"][pphhf] in [the bibliography]. [the bibliography]: ./traits-bibliography.html [pphhf]: ./traits-bibliography.html#pphhf It turns out that supporting FOHH is not really all that hard. And once we are able to do that, we can easily describe the type-checking rule for generic functions like `foo` in our logic. ## Source This page is a lightly adapted version of a [blog post by Nicholas Matsakis][lrtl]. [lrtl]: http://smallcultfollowing.com/babysteps/blog/2017/01/26/lowering-rust-traits-to-logic/