Allow pinning variables to a scope and introduce lifetime 'self

The current borrow checker in rust considers only general use cases and doesn't allow the user to specify a specific use case which may be valid but not in general. How ? for example a struct that have a vec of references that must be valid as long as the struct is valid may be dclared as this:

trait Object { ... }

struct SomeSt {
	objects: Vec<&' ??? dyn Object> // which lifetime to use ?
}

So which lifetime the objects must all satisfy ? the lifetime of self, but what is the lifetime of self ? it is 'static ! because rust allows all objects to move a struct that has no lifetime annotations is considered static since it can be moved to a static object. In order to limit the lifetime of a struct to a scope it is declared in the struct must have a reference to something in this scope. But if the struct has no references ? or has a collection of objects that must satisfy a lifetime but their lifetimes aren't the same ? The current way to do this is via PhantomData. Another solution is to allow immovable types but it won't be usable in rust without constructors. The idea I propose is to allow the user to pin a variable to the current scope so it has a hidden lifetime parameter to the current scope and introduce a lifetime 'self which is reinterpreted as follows: it is the sum of all lifetimes annotations of the struct including the the pin lifetime and if no lifetime annotations and no pin it is equivalent to 'static

example code using PhantomData:

use std::marker::PhantomData;

pub struct PinFlag;

pub const STATIC_PIN: PinFlag = PinFlag;

pub trait ExecutorOperation {
	fn invoke(&self);
}

pub struct Executor<'selff> {
	_pin: PhantomData<&'selff PinFlag>,
	ops: Vec<&'selff dyn ExecutorOperation>,
}

impl<'selff> Executor<'selff> {
	fn pinned(_: &'selff PinFlag) -> Self {
		Self {
			_pin: PhantomData,
			ops: Vec::new()
		}
	}
	
	fn run(&mut self) {
		for op in self.ops.drain(..) {
			op.invoke();
		}
	}
}

impl<'selff> Drop for Executor<'selff> {
	fn drop(&mut self) {}
}

pub struct Operation {
	id: i32
}

impl Operation {
	fn new(id: i32) -> Self {
		Self {
			id: id
		}
	}
}

impl ExecutorOperation for Operation {
	fn invoke(&self) {
		println!("operation {} is invoked", self.id);
	}
}

pub fn consume_static<T: 'static>(_t: T) {}

pub fn main() {
	let static_ex = Executor::pinned(&STATIC_PIN);
	consume_static(static_ex);
	
	let scope_pin = PinFlag;
	let op1 = Operation::new(1);
	let op2 = Operation::new(2);
	let mut ex1 = Executor::pinned(&scope_pin); // let mut pinned ex1 = Executor::new()
	// static_ex = ex1; // error static_ex is required to be static but ex1 can't escape the current scope
	ex1.ops.push(&op1);
	ex1.ops.push(&op2);
	
	{
		let scope_pin = PinFlag;
		let op3 = Operation::new(3);
		let op4 = Operation::new(4);
		let mut ex2 = Executor::pinned(&scope_pin); // let mut pinned ex2 = Executor::new()
		ex2.ops.push(&op1);
		ex2.ops.push(&op2);
		ex2.ops.push(&op3);
		ex2.ops.push(&op4);
		//ex1 = ex2; // error ex2 is can't escape the current scope since it borrows scope_pin for the current scope
		ex2.run();
	}
	ex1.run();
}

So as I understand it, the desire here is to specifically say what lifetime/scope/region a type is in (within a specific function body / without a function signature to introduce lifetime names), instead of relying inference.

Or more concretely, with your example without lifetime pinning

pub struct Executor<'a> {
    ops: Vec<&'a dyn ExecutorOperation>,
}

impl<'a> Executor<'a> {
    fn new() -> Self {
        Self {
            ops: Vec::new(),
        }
    }
}

what you point out as should be errors are errors, w.g.

error[E0597]: `op3` does not live long enough
  --> src/main.rs:66:22
   |
66 |         ex2.ops.push(&op3);
   |                      ^^^^ borrowed value does not live long enough
...
72 |     }
   |     - `op3` dropped here while still borrowed
73 | 
74 |     ex1.run();
   |     --------- borrow later used here

But you would like instead to get something along the lines of

error[E0597]: `ex2` does not live long enough
  --> src/main.rs:LL:CC
   |
LL |          ex1 = ex2;
   |               ^^^ value does not live long enough
   = note:
67 |         ex2.ops.push(&op3);
   |                      ^^^^ ex2 borrows op3 here
...
73 |     }
   |     - `op3` dropped here while still borrowed
74 | 
75 |     ex1.run();
   |     --------- borrow later used here

The renaming certainly doesn't help the clarity of the emitted error message. However, the question of blaming the assignment rather than the borrow is an impossible one for the compiler to answer; it can't know which is "more authoritative" than the other.

However, we do have a case where we generate a different error: function signatures are considered authoritative for errors within the function:

fn example<'a>(mut long: Executor<'static>, short: Executor<'a>) {
    long = short;
}
error[E0308]: mismatched types
  --> src/main.rs:77:12
   |
77 |     long = short;
   |            ^^^^^ lifetime mismatch
   |
   = note: expected struct `Executor<'static>`
              found struct `Executor<'a>`
note: the lifetime `'a` as defined here...
  --> src/main.rs:76:12
   |
76 | fn example<'a>(mut long: Executor<'static>, short: Executor<'a>) {
   |            ^^
   = note: ...does not necessarily outlive the static lifetime

Or if you name the lifetime of the binding's type:

error[E0597]: `op1` does not live long enough
  --> src/main.rs:52:18
   |
44 |     let mut static_ex: Executor<'static> = Executor::new();
   |                        ----------------- type annotation requires that `op1` is borrowed for `'static`
...
52 |     ex1.ops.push(&op1);
   |                  ^^^^ borrowed value does not live long enough
...
74 | }
   | - `op1` dropped here while still borrowed

This is, I suppose, the intent of the proposal: to be able to name a lifetime and consider it authoritative. It's not currently possible to name lifetimes shorter than function scope. There's two potential ways to give a name name to a sub-fn lifetime: labeled blocks are interesting, as their label "lifetime" could be allowed to be used as an actual lifetime in a type (currently, though, lifetime and (loop) label namespaces are distinct and can overlap without any warnings); and we could allow let 'a; to introduce a lifetime name to be constrained by using it in types. Requiring the lifetime to be live at the let declaration seems reasonable and would make it very similar to the proposal to tie an object to a scope. (With a way to mark the lifetime dead, it becomes isomorphic up to syntax sugar.)

Doing so semi implicitly with a 'self lifetime isn't a good idea, though. The use of 'self requires some new feature to tie an object to a scope[1], and the need to do so isn't visible from the public type declaration. Additionally, calling the lifetime 'self runs far too great of a risk of being assumed to refer to the lifetime of the object's storage (i.e. self references) rather than just being an inferred lifetime hidden from the signature.

Also, explicitly naming a lifetime region works fairly well for covariant lifetimes where you can freely contract them to a smaller scope (most lifetimes), but breaks down for invariant lifetimes (much match exactly; typically behind a mutable reference) or contravariant lifetimes (you can provide a longer lifetime; rare; function input lifetimes). This makes naming regions more granularly than "outlives this fn" and "outlives this other lifetime" is difficult to benefit from more generally.

This would allow for better errors, yes, but only when used completely optionally, and only for the local function. The Rust project cares about giving library authors the tools to get good errors for their APIs, but local function bodies aren't part of an API or shared, so effort for improving errors there is much better spent on improving them generally for everyone than adding language features to influence local-only errors.


  1. And it can't be inferred as the declaration scope, as then extending/inferring to 'static is impossible. ↩︎

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.