Early-bound parameters in rustc are identified by an index, stored in the
ParamTy
struct for types or the EarlyParamRegion
struct for lifetimes.
The index counts from the outermost declaration in scope. This means that as you
add more binders inside, the index doesn't change.
For example,
trait Foo<T> {
type Bar<U> = (Self, T, U);
}
Here, the type (Self, T, U)
would be ($0, $1, $2)
, where $N
means a
ParamTy
with the index of N
.
In rustc, the Generics
structure carries this information. So the
Generics
for Bar
above would be just like for U
and would indicate the
'parent' generics of Foo
, which declares Self
and T
. You can read more
in this chapter.
Late-bound parameters in rustc
are handled differently. We indicate their
presence by a Binder
type. The Binder
doesn't know how many variables
there are at that binding level. This can only be determined by walking the
type itself and collecting them. So a type like for<'a, 'b> ('a, 'b)
would be
for (^0.a, ^0.b)
. Here, we just write for
because we don't know the names
of the things bound within.
Moreover, a reference to a late-bound lifetime is written ^0.a
:
- The
0
is the index; it identifies that this lifetime is bound in the innermost binder (thefor
). - The
a
is the "name"; late-bound lifetimes in rustc are identified by a "name" -- theBoundRegionKind
enum. This enum can contain aDefId
or it might have various "anonymous" numbered names. The latter arise from types likefn(&u32, &u32)
, which are equivalent to something likefor<'a, 'b> fn(&'a u32, &'b u32)
, but the names of those lifetimes must be generated.
This setup of not knowing the full set of variables at a binding level has some
advantages and some disadvantages. The disadvantage is that you must walk the
type to find out what is bound at the given level and so forth. The advantage
is primarily that, when constructing types from Rust syntax, if we encounter
anonymous regions like in fn(&u32)
, we just create a fresh index and don't have
to update the binder.