Lifetime Elision Rules

  1. Each parameter that is a reference gets its own lifetime parameter.
  2. If there is exactly one input lifetime, that lifetime is assigned to all output lifetimes.
  3. If there are multiple input lifetimes but one of them is &self or &mut self, the lifetime of self is assigned to all output lifetimes.

Question re: (3): How is the lifetime of self determined?

They mean the outer lifetime (one of those '_ things) of the reference &self or &mut self.

It lasts at least just longer than the method call; how long is inferred by the context of the call site. For example if you use std::trim, the borrow has to last as long as the returned value is used.

The same way a, b and c are determined in Pythagorean Theorem.

Pythagorean equation a² + b² = c² doesn't mean that there's any concrete a, b, or c.

You may substitute a, b, c with 3, 4, 5, or with 5, 12, 13 or any other numbers that belong to the right triangle.

Similarly with lifetimes in Rust: they are completely ignored and removed from the code after compilation, they are only there to ensure that code is correct.

And not even to make it correct, but to describe how already correct code works to the compiler.

And in some cases that relationship is so obvious that compiler “understand things” even with any additional markup.

I understand that. Lifetimes tie references together, telling the compiler they point at the same piece of memory. That's all.

I think you're right that the answer lies within that.

Just to write it out more explicitly

fn foo(&self, a: &TypeA, b: &TypeB) -> &ReturnType { 
    \\ function body

\\ is the same as 

fn foo<'s, 'a, 'b>(&'s self, a: &'a TypeA, b: &'b TypeB) -> &'s ReturnType {
    \\ function body

Note how &self and &ReturnType get the same lifetime annotation.
So the question isn't what is the lifetime of &self but how do generic lifetime parameters work in general. If you understand that for 1) and 2) it should hopefuly now also be clear for 3).