Skip to content

Tracking issue for future-incompatibility lint late_bound_lifetime_arguments #42868

Open
@petrochenkov

Description

@petrochenkov

What is this lint about

In functions not all lifetime parameters are created equal.
For example, if you have a function like

fn f<'a, 'b>(arg: &'a u8) -> &'b u8 { .... }

both 'a and 'b are listed in the same parameter list, but when stripped from the surface syntax the function looks more like

for<'a> fn f<'b>(arg: &'a u8) -> &'b u8 { .... }

where 'b is a "true" ("early-bound") parameter of the function, and 'a is an "existential" ("late-bound") parameter. This means the function is not parameterized by 'a.
To give some more intuition, let's write a type for function pointer to f:

// PtrF is not parameterized by 'a,
type PtrF<'b> = for<'a> fn(&'a u8) -> &'b u8;
// but it has to be parameterized by 'b
type PtrF = for<'a, 'b> fn(&'a u8) -> &'b u8; // ERROR

See more about this distinction in http://smallcultfollowing.com/babysteps/blog/2013/10/29/intermingled-parameter-lists/#early--vs-late-bound-lifetimes

When lifetime arguments are provided to a function explicitly, e.g.

f::<'my_a, 'my_b>

the first argument doesn't make much sense because the function is not parameterized by 'a.
Providing arguments for "late-bound" lifetime parameters in general doesn't make sense, while arguments for "early-bound" lifetime parameters can be provided.

It's not clear how to provide arguments for early-bound lifetime parameters if they are intermixed with late-bound parameters in the same list. For now providing any explicit arguments is prohibited if late-bound parameters are present, so in the future we can adopt any solution without hitting backward compatibility issues.

Note that late-bound lifetime parameters can be introduced implicitly through lifetime elision:

fn f(&u8) {}
// Desugars into
fn f<'a>(&'a u8) {} // 'a is late-bound

The precise rules discerning between early- and late-bound lifetimes can be found here:

/// Detects late-bound lifetimes and inserts them into
/// `map.late_bound`.
///
/// A region declared on a fn is **late-bound** if:
/// - it is constrained by an argument type;
/// - it does not appear in a where-clause.
///
/// "Constrained" basically means that it appears in any type but
/// not amongst the inputs to a projection. In other words, `<&'a
/// T as Trait<''b>>::Foo` does not constrain `'a` or `'b`.
fn insert_late_bound_lifetimes(map: &mut NamedRegionMap,
fn_def_id: DefId,
decl: &hir::FnDecl,
generics: &hir::Generics) {
debug!("insert_late_bound_lifetimes(decl={:?}, generics={:?})", decl, generics);
let mut constrained_by_input = ConstrainedCollector { regions: FxHashSet() };
for arg_ty in &decl.inputs {
constrained_by_input.visit_ty(arg_ty);
}
let mut appears_in_output = AllCollector {
regions: FxHashSet(),
impl_trait: false
};
intravisit::walk_fn_ret_ty(&mut appears_in_output, &decl.output);
debug!("insert_late_bound_lifetimes: constrained_by_input={:?}",
constrained_by_input.regions);
// Walk the lifetimes that appear in where clauses.
//
// Subtle point: because we disallow nested bindings, we can just
// ignore binders here and scrape up all names we see.
let mut appears_in_where_clause = AllCollector {
regions: FxHashSet(),
impl_trait: false
};
for ty_param in generics.ty_params.iter() {
walk_list!(&mut appears_in_where_clause,
visit_ty_param_bound,
&ty_param.bounds);
}
walk_list!(&mut appears_in_where_clause,
visit_where_predicate,
&generics.where_clause.predicates);
for lifetime_def in &generics.lifetimes {
if !lifetime_def.bounds.is_empty() {
// `'a: 'b` means both `'a` and `'b` are referenced
appears_in_where_clause.visit_lifetime_def(lifetime_def);
}
}
debug!("insert_late_bound_lifetimes: appears_in_where_clause={:?}",
appears_in_where_clause.regions);
// Late bound regions are those that:
// - appear in the inputs
// - do not appear in the where-clauses
// - are not implicitly captured by `impl Trait`
for lifetime in &generics.lifetimes {
let name = lifetime.lifetime.name;
// appears in the where clauses? early-bound.
if appears_in_where_clause.regions.contains(&name) { continue; }
// any `impl Trait` in the return type? early-bound.
if appears_in_output.impl_trait { continue; }
// does not appear in the inputs, but appears in the return
// type? eventually this will be early-bound, but for now we
// just mark it so we can issue warnings.
let constrained_by_input = constrained_by_input.regions.contains(&name);
let appears_in_output = appears_in_output.regions.contains(&name);
if !constrained_by_input && appears_in_output {
debug!("inserting issue_32330 entry for {:?}, {:?} on {:?}",
lifetime.lifetime.id,
name,
fn_def_id);
map.issue_32330.insert(
lifetime.lifetime.id,
ty::Issue32330 {
fn_def_id,
region_name: name,
});
continue;
}
debug!("insert_late_bound_lifetimes: \
lifetime {:?} with id {:?} is late-bound",
lifetime.lifetime.name, lifetime.lifetime.id);
let inserted = map.late_bound.insert(lifetime.lifetime.id);
assert!(inserted, "visited lifetime {:?} twice", lifetime.lifetime.id);
}
return;
struct ConstrainedCollector {
regions: FxHashSet<ast::Name>,
}
impl<'v> Visitor<'v> for ConstrainedCollector {
fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'v> {
NestedVisitorMap::None
}
fn visit_ty(&mut self, ty: &'v hir::Ty) {
match ty.node {
hir::TyPath(hir::QPath::Resolved(Some(_), _)) |
hir::TyPath(hir::QPath::TypeRelative(..)) => {
// ignore lifetimes appearing in associated type
// projections, as they are not *constrained*
// (defined above)
}
hir::TyPath(hir::QPath::Resolved(None, ref path)) => {
// consider only the lifetimes on the final
// segment; I am not sure it's even currently
// valid to have them elsewhere, but even if it
// is, those would be potentially inputs to
// projections
if let Some(last_segment) = path.segments.last() {
self.visit_path_segment(path.span, last_segment);
}
}
_ => {
intravisit::walk_ty(self, ty);
}
}
}
fn visit_lifetime(&mut self, lifetime_ref: &'v hir::Lifetime) {
self.regions.insert(lifetime_ref.name);
}
}
struct AllCollector {
regions: FxHashSet<ast::Name>,
impl_trait: bool
}
impl<'v> Visitor<'v> for AllCollector {
fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'v> {
NestedVisitorMap::None
}
fn visit_lifetime(&mut self, lifetime_ref: &'v hir::Lifetime) {
self.regions.insert(lifetime_ref.name);
}
fn visit_ty(&mut self, ty: &hir::Ty) {
if let hir::TyImplTrait(_) = ty.node {
self.impl_trait = true;
}
intravisit::walk_ty(self, ty);
}
}
}

How to fix this warning/error

Just removing the lifetime arguments pointed to by the lint should be enough in most cases.

Current status

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-lifetimesArea: Lifetimes / regionsA-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.C-future-incompatibilityCategory: Future-incompatibility lintsC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCL-late_bound_lifetime_argumentsLint: late_bound_lifetime_argumentsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    Status

    Idea

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions