Type Checking

When we work on a new lint or improve an existing lint, we might want to retrieve the type Ty of an expression Expr for a variety of reasons. This can be achieved by utilizing the LateContext that is available for LateLintPass.

LateContext and TypeckResults

The lint context LateContext and TypeckResults (returned by LateContext::typeck_results) are the two most useful data structures in LateLintPass. They allow us to jump to type definitions and other compilation stages such as HIR.

Note: LateContext.typeck_results's return value is TypeckResults and is created in the type checking step, it includes useful information such as types of expressions, ways to resolve methods and so on.

TypeckResults contains useful methods such as expr_ty, which gives us access to the underlying structure Ty of a given expression.

#![allow(unused)]
fn main() {
pub fn expr_ty(&self, expr: &Expr<'_>) -> Ty<'tcx>
}

As a side note, besides expr_ty, TypeckResults contains a pat_ty() method that is useful for retrieving a type from a pattern.

Ty

Ty struct contains the type information of an expression. Let's take a look at rustc_middle's Ty struct to examine this struct:

#![allow(unused)]
fn main() {
pub struct Ty<'tcx>(Interned<'tcx, WithStableHash<TyS<'tcx>>>);
}

At a first glance, this struct looks quite esoteric. But at a closer look, we will see that this struct contains many useful methods for type checking.

For instance, is_char checks if the given Ty struct corresponds to the primitive character type.

is_* Usage

In some scenarios, all we need to do is check if the Ty of an expression is a specific type, such as char type, so we could write the following:

#![allow(unused)]
fn main() {
impl LateLintPass<'_> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        // Get type of `expr`
        let ty = cx.typeck_results().expr_ty(expr);

        // Check if the `Ty` of this expression is of character type
        if ty.is_char() {
            println!("Our expression is a char!");
        }
    }
}
}

Furthermore, if we examine the source code for is_char, we find something very interesting:

#![allow(unused)]
fn main() {
#[inline]
pub fn is_char(self) -> bool {
    matches!(self.kind(), Char)
}
}

Indeed, we just discovered Ty's kind() method, which provides us with TyKind of a Ty.

TyKind

TyKind defines the kinds of types in Rust's type system. Peeking into TyKind documentation, we will see that it is an enum of over 25 variants, including items such as Bool, Int, Ref, etc.

kind Usage

The TyKind of Ty can be returned by calling Ty.kind() method. We often use this method to perform pattern matching in Clippy.

For instance, if we want to check for a struct, we could examine if the ty.kind corresponds to an Adt (algebraic data type) and if its AdtDef is a struct:

#![allow(unused)]
fn main() {
impl LateLintPass<'_> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        // Get type of `expr`
        let ty = cx.typeck_results().expr_ty(expr);
        // Match its kind to enter the type
        match ty.kind {
            ty::Adt(adt_def, _) if adt_def.is_struct() => println!("Our `expr` is a struct!"),
            _ => ()
        }
    }
}
}

hir::Ty and ty::Ty

We've been talking about ty::Ty this whole time without addressing hir::Ty, but the latter is also important to understand.

hir::Ty would represent what the user wrote, while ty::Ty is how the compiler sees the type and has more information. Example:

#![allow(unused)]
fn main() {
fn foo(x: u32) -> u32 { x }
}

Here the HIR sees the types without "thinking" about them, it knows that the function takes an u32 and returns an u32. As far as hir::Ty is concerned those might be different types. But at the ty::Ty level the compiler understands that they're the same type, in-depth lifetimes, etc...

To get from a hir::Ty to a ty::Ty, you can use the lower_ty function outside of bodies or the TypeckResults::node_type() method inside of bodies.

Warning: Don't use lower_ty inside of bodies, because this can cause ICEs.

Creating Types programmatically

A common usecase for creating types programmatically is when we want to check if a type implements a trait (see Trait Checking).

Here's an example of how to create a Ty for a slice of u8, i.e. [u8]

#![allow(unused)]
fn main() {
use rustc_middle::ty::Ty;
// assume we have access to a LateContext
let ty = Ty::new_slice(cx.tcx, Ty::new_u8());
}

In general, we rely on Ty::new_* methods. These methods define the basic building-blocks that the type-system and trait-system use to define and understand the written code.

Below are some useful links to further explore the concepts covered in this chapter: