What's the magic that makes either_string(b, str1, str2) compile even though str1 and str2 have different lifetimes without a bound between them? There is no type annotation I could put on x that would keep it compiling, right? Is there some kind of heuristic that turns this into fn test_subtyping<'c, 'a: 'c, 'b: 'c> { let x : &'c str = ... }?
The borrow checker's job is, more or less, to find a combination of lifetimes that works (or indicate an error if it can't find any). You don't explicitly assign either_string a lifetime, so any lifetime that works is fine. In this case, any lifetime that lasts during the either_string and String::from calls would suffice, as &'a str is "covariant" (this means, "&'a str is a subtype of &'c str when 'a: 'c", which means "&'a str can be used in place of &'c str when 'a: 'c"). Since both 'a and 'b necessarily outlive the whole function body, a lifetime '1 whose duration is within the function body can be used for either_string, and then normal semantics of "&'a str coerces to &'1 str because 'a: '1" make everything work out.
Note that you cannot actually name the lifetime '1, you can only name 'a, 'b, and 'static in that function body. But the borrow checker can still work with unnameable lifetimes.
As for how the borrow checker actually internally functions and figures out what lifetime to use.... I don't know. But my understanding is that it doesn't actually try to assign a specific lifetime to unnamed lifetimes like '_; it just checks if a system of constraints has any solution. Producing a specific solution is unnecessary.
There is no way to explicitly write a type annotation on x that would correctly represent this situation.
An important thing about lifetimes is that, in a function like fn test_subtyping<'a, 'b>, we don’t know anything about the 'a and 'b, except that they’re valid for the duration of the function’s execution. In other words, generic lifetimes of a function refer to lifetimes that must at least outlive the entire function.
Since 'a and 'b both are valid for the duration of the function's execution, the compiler figures out that it can assign to x a lifetime corresponding to a duration within the function's execution. That is, the lifetime for x is an unnamable lifetime whose duration is within the function. It can't be a generic parameter because it's potentially not valid for the entire function.
Note also that this is not just a weird fact about two-lifetime-parameters situations. If you write:
let local_string = String::from("hello");
let x = either_string(b, &local_string, &local_string);
then there is no lifetime name you can give to x. All lifetimes of borrows from local variables are unnameable, because there is no syntax to introduce such a name referring to a smaller scope than the entire function body. And many borrows that are in function bodies, even if they are not borrows from local variables, are borrows that don't cover the entire function body.
There may be more potential coercions going on than you realize.
For this example, in practice, everything will behave as if all of 's1, 's2, 'e are the same as 'x (there is nothing else that causes those lifetimes to be active after the let completes). So even most walkthroughs of borrow checking will just elide some of these as they are more distracting than helpful.