I haven't studied the type inference algorithm in detail, but I believe it mostly just goes top-to-bottom through all statements in evaluation order, and tries to infer the types. For non-generic functions, it's mostly easy: check that the arguments conform to the desired parameter types, infer that the result has the type specified in the signature. Generics make it more complicated, since their type can be inferred "backwards" in a limited sense. It works introducing obligation equations, which track all unresolved generic parameters (including associated types), their trait bounds and possible equalities between them. On each step, the type checker tries to reduce the obligations, either finding the unique types satisfying them (for some parameters), or finding ambiguity or contradiction, either of which is a type error.
Method resolution is a wrench in this process: the deref coercion and method resolution algorithm in general make it very difficult to track all possible obligations without knowing the specifics of the receiver type (I think it's possible in principle, but it can easily cause an exponential blowup in type inference, since all possible deref possibilities need to be considered separately). Add in that the type inference for traits is a Turing-complete algorithm, and all of that means that inferring the method receiver type via obligations is generally infeasible, and quite brittle/slow even in the cases where we could do it.
For this reason, method resolution doesn't introduce obligations w.r.t. the receiver and possible traits defining the method. We require that the method can be uniquely resolved given the information that we already know about the receiver. Of course, the receiver may itself be generic, but that just means that we must find a properly generic unique trait impl which fits.
This means that we can't delay the resolution of to_string
until the moment where we know more about s
, even if at a later point s
becomes uniquely defined. So the type checker errors out.
The trick with generic functions works because it changes the inference order. Instead of going left-to-right through the body of the closure, we find the generic parameters of your call
function. This allows to look at the type of the second argument before the body of the closure, which breaks the typechecking block. It wouldn't help if the relation between parameter types were more complex, e.g. if call had a signature
fn call<T, R>(f: impl FnOnce(R) -> T, t: T) -> R;
In principle, your specific example with the closure is simple enough that it could be special-cased in the typechecker, but it's not clear that the win is worth the extra complexity, and changing the type inference algorithm is always a bit scary (what if we infer something differently, add support for something too brittle, or make a change we regret?).