Inlining is not a solution to the general tail call elimination problem for a number of reasons. The list below is not meant to be exhaustive. It is, however, sorted -- it starts with an inconvenience and ends with a complete showstopper.
A function called in a tail position may be a large one, in which case inlining it may not be advisable from a performance standpoint.
Suppose there are multiple tail calls to
g
inf
. Under the regular definition of inlining, you'd have to inlineg
at each call site, potentially makingf
huge. If instead you choose togoto
the beginning ofg
and then jump back, then you need to remember where to jump to and all of a sudden you're maintaining your own call stack fragment (which will almost certainly exhibit poor performance when compared to the "real" call stack).For mutually recursive functions
f
andg
, you'd have to inlinef
ing
andg
inf
. Clearly this is impossible under the usual definition of inlining. So, you're left with what's effectively a custom in-function calling convention (as in thegoto
-based approach to 2. above).Real TCE works with arbitrary calls in tail position, including in higher-order contexts:
(defn say-foo-then-call [f] (println "Foo!") (f))
This can be used to great effect in certain scenarios and clearly cannot be emulated with inlining.