I was trying to write a procedure that would test whether tail recursion was enabled or not. It turns out that this is semi-decidable. You can only tell if it is not tail recursive by blowing the stack, but you can never be sure that somebody didn’t make a really, really big stack.
But for the sake of argument, let’s say assume that 228 recursive calls is enough to blow the stack. Also, let’s assume that the system will correctly signal an error and be able to recover from the blown stack once it is unwound. Then we can test for tail recursion as follows:
(defconstant +stack-test-size+ (expt 2 28)) (defun test-self-tail-recursion (x) (or (> x +stack-test-size+) (test-self-tail-recursion (1+ x)))) (defun test-mutual-tail-recursion (x) (or (> x +stack-test-size+) (test-mutual-tail-recursion-1 (1+ x)))) (defun test-mutual-tail-recursion-1 (x) (or (> x +stack-test-size+) (test-mutual-tail-recursion (1+ x)))) (defun self-tail-recursive? () (ignore-errors (test-self-tail-recursion 0))) (defun mutually-tail-recusive? () (ignore-errors (test-mutual-tail-recursion 0)))
Naturally, your optimize
settings are going to affect
whether or not you have tail recursion enabled, and even if this
code says it is enabled, it doesn’t mean that a different
compilation unit compiled at a different time with different
optimizations would be tail recursive as well. But if you run this
in your default environment and it returns NIL, then you can be
pretty sure your default is not tail recursive. If it
returns T, however, then there are at least some conditions under
which your system is tail recursive.
Use of certain features of Common Lisp will “break”
tail recursion, for example special
variable binding, multiple-value-bind
, unwind-protect
,
and catch
, and basically anything that marks the stack
or dynamically allocates on the stack. If control has to return to
the current function after a call, then it is not tail recursive,
but if control can return directly to the caller of the current
function, then the compiler should be able to make a tail recursive
call.
Most, but not all, Common Lisp implementations can be configured to
enable tail recursion, and it is usually possible to disable it if
desired. If you want tail recursion, but your implementation cannot
provide it, you are SOL. If you don’t want tail recursion, but your
implementation provides it by default, there are a number of things
you can do to disable it. Usually, you can set the debugging
options to retain every stack frame, or you can set the debug
optimization to disable tail recursion. If the recursive call is
not in tail position, then it is incorrect to tail call it, so you
can disable tail recursion by moving the recursive call out of tail
position. For example, you could write
a without-tail-call
macro:
(defmacro without-tail-call (form) (let ((value (gensym))) `((lambda (,value) ,value) ,form))) ;; Use like this: (defun factorial (x &optional (acc 1)) (if (zerop x) (progn (cerror "Return the factorial" "Computed a factorial") acc) (without-tail-call (factorial (1- x) (* acc x)))))
Running this program will drop us into the debugger because of
the cerror
and we can inspect the stack.
> (factorial 100) Computed a factorial [Condition of type SIMPLE-ERROR] Restarts: 0: [CONTINUE] Return the factorial 1: [RETRY] Retry SLY mREPL evaluation request. 2: [*ABORT] Return to SLY’s top level. 3: [ABORT] abort thread (#<THREAD tid=179 "sly-channel-1-mrepl-remote-1" RUNNING {1000BA8003}>) Backtrace: 0: (FACT 0 2432902008176640000) 1: (FACT 1 2432902008176640000) 2: (FACT 2 1216451004088320000) 3: (FACT 3 405483668029440000) 4: (FACT 4 101370917007360000) 5: (FACT 5 20274183401472000) 6: (FACT 6 3379030566912000) ...
As requested, each recursive calls pushes a stack frame.
But what if the compiler “optimizes” ((λ
(x) x) (foo))
to simply be (foo)
? That is a
questionable “optimization”. Lisp is a call-by-value
language, meaning that the arguments to a function are fully
evaluated before the function is applied to them. The call
to (foo)
should be fully evaluated before
applying (λ (x) x)
to it. The
“optimization” essentially treats (foo)
as unevaluted
and then tail calls it after applying the lambda expression. This
is call-by-name evaluation. While anything is possible, an
“optimization” that sometimes changes the function
call semantics from call-by-value to call-by-need is dubious.
That optimization would be worse than dubious; it would be incorrect, because (foo) might return more than one value (or none), but the lambda combination always returns exactly one value.
ReplyDeleteA simpler way to express that would be for the macro body to be `(values ,form).