EarlyBinder
and instantiating parameters
Given an item that introduces a generic parameter T
, whenever we refer to types inside of foo
(i.e. the return type or argument types) from outside of foo
we must take care to handle the generic parameters defined on foo
. As an example:
fn foo<T, U>(a: T, _b: U) -> T { a }
fn main() {
let c = foo::<i32, u128>(1, 2);
}
When type checking main
we cannot just naively look at the return type of foo
and assign the type T
to the variable c
, The function main
does not define any generic parameters, T
is completely meaningless in this context. More generally whenever an item introduces (binds) generic parameters, when accessing types inside the item from outside, the generic parameters must be instantiated with values from the outer item.
In rustc we track this via the EarlyBinder
type, the return type of foo
is represented as an EarlyBinder<Ty>
with the only way to access Ty
being to provide arguments for any generic parameters Ty
might be using. This is implemented via the EarlyBinder::instantiate
method which discharges the binder returning the inner value with all the generic parameters replaced by the provided arguments.
To go back to our example, when type checking main
the return type of foo
would be represented as EarlyBinder(T/#0)
. Then, because we called the function with i32, u128
for the generic arguments, we would call EarlyBinder::instantiate
on the return type with [i32, u128]
for the args. This would result in an instantiated return type of i32
that we can use as the type of the local c
.
Here are some more examples:
fn foo<T>() -> Vec<(u32, T)> { Vec::new() }
fn bar() {
// the return type of `foo` before instantiating it would be:
// `EarlyBinder(Adt(Vec, &[Tup(&[u32, T/#=0])]))`
// we then instantiate the binder with `[u64]` resulting in the type:
// `Adt(Vec, &[Tup(&[u32, u64])])`
let a = foo::<u64>();
}
struct Foo<A, B> {
x: Vec<A>,
..
}
fn bar(foo: Foo<u32, f32>) {
// the type of `foo`'s `x` field before instantiating it would be:
// `EarlyBinder(Vec<A/#0>)`
// we then instantiate the binder with `[u32, f32]` as those are the
// generic arguments to the `Foo` struct. This results in a type of:
// `Vec<u32>`
let y = foo.x;
}
In the compiler the instantiate
call for this is done in FieldDef::ty
(src), at some point during type checking bar
we will wind up calling FieldDef::ty(x, &[u32, f32])
in order to obtain the type of foo.x
.
Note on indices: It is possible for the indices in Param
to not match with what the EarlyBinder
binds. For
example, the index could be out of bounds or it could be the index of a lifetime when we were expecting a type.
These sorts of errors would be caught earlier in the compiler when translating from a rustc_hir::Ty
to a ty::Ty
.
If they occur later, that is a compiler bug.
As mentioned previously when outside of an item, it is important to instantiate the EarlyBinder
with generic arguments before accessing the value inside, but the setup for when we are conceptually inside of the binder already is a bit different.
For example:
#![allow(unused)] fn main() { impl<T> Trait for Vec<T> { fn foo(&self, b: Self) {} } }
When constructing a Ty
to represent the b
parameter's type we need to get the type of Self
on the impl that we are inside. This can be acquired by calling the type_of
query with the impl
's DefId
, however, this will return a EarlyBinder<Ty>
as the impl block binds generic parameters that may have to be discharged if we are outside of the impl.
The EarlyBinder
type provides an instantiate_identity
function for discharging the binder when you are "already inside of it". This is effectively a more performant version of writing EarlyBinder::instantiate(GenericArgs::identity_for_item(..))
. Conceptually this discharges the binder by instantiating it with placeholders in the root universe (we will talk about what this means in the next few chapters). In practice though it simply returns the inner value with no modification taking place.