Skip to content

Commit 29c7f8c

Browse files
committed
* lisp/emacs-lisp/cl-macs.el: Optimize self-calls in tail position
Implement a limited form of tail-call optimization for the special case of recursive functions defined with `cl-labels`. Only self-recursion is optimized, no attempt is made to handle more complex cases such a mutual recursion. The main benefit is to reduce the use of the stack, tho in my limited tests, this can also improve performance (about half of the way to a hand-written `while` loop). (cl--self-tco): New function. (cl-labels): Use it. * lisp/subr.el (letrec): Optimize single-binding corner case. * test/lisp/emacs-lisp/cl-macs-tests.el (cl-macs--labels): Add tests to check that TCO is working.
1 parent 6e73e07 commit 29c7f8c

3 files changed

Lines changed: 135 additions & 11 deletions

File tree

lisp/emacs-lisp/cl-macs.el

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,10 +2060,98 @@ Like `cl-flet' but the definitions can refer to previous ones.
20602060
((null (cdr bindings)) `(cl-flet ,bindings ,@body))
20612061
(t `(cl-flet (,(pop bindings)) (cl-flet* ,bindings ,@body)))))
20622062

2063+
(defun cl--self-tco (var fargs body)
2064+
;; This tries to "optimize" tail calls for the specific case
2065+
;; of recursive self-calls by replacing them with a `while' loop.
2066+
;; It is quite far from a general tail-call optimization, since it doesn't
2067+
;; even handle mutually recursive functions.
2068+
(letrec
2069+
((done nil) ;; Non-nil if some TCO happened.
2070+
(retvar (make-symbol "retval"))
2071+
(ofargs (mapcar (lambda (s) (if (memq s cl--lambda-list-keywords) s
2072+
(make-symbol (symbol-name s))))
2073+
fargs))
2074+
(opt-exps (lambda (exps) ;; `exps' is in tail position!
2075+
(append (butlast exps)
2076+
(list (funcall opt (car (last exps)))))))
2077+
(opt
2078+
(lambda (exp) ;; `exp' is in tail position!
2079+
(pcase exp
2080+
;; FIXME: Optimize `apply'?
2081+
(`(funcall ,(pred (eq var)) . ,aargs)
2082+
;; This is a self-recursive call in tail position.
2083+
(let ((sets nil)
2084+
(fargs ofargs))
2085+
(while fargs
2086+
(pcase (pop fargs)
2087+
('&rest
2088+
(push (pop fargs) sets)
2089+
(push `(list . ,aargs) sets)
2090+
;; (cl-assert (null fargs))
2091+
)
2092+
('&optional nil)
2093+
(farg
2094+
(push farg sets)
2095+
(push (pop aargs) sets))))
2096+
(setq done t)
2097+
`(progn (setq . ,(nreverse sets))
2098+
:recurse)))
2099+
(`(progn . ,exps) `(progn . ,(funcall opt-exps exps)))
2100+
(`(if ,cond ,then . ,else)
2101+
`(if ,cond ,(funcall opt then) . ,(funcall opt-exps else)))
2102+
(`(cond . ,conds)
2103+
(let ((cs '()))
2104+
(while conds
2105+
(pcase (pop conds)
2106+
(`(,exp)
2107+
(push (if conds
2108+
;; This returns the value of `exp' but it's
2109+
;; only in tail position if it's the
2110+
;; last condition.
2111+
`((setq ,retvar ,exp) nil)
2112+
`(,(funcall opt exp)))
2113+
cs))
2114+
(exps
2115+
(push (funcall opt-exps exps) cs))))
2116+
(if (eq t (caar cs))
2117+
`(cond . ,(nreverse cs))
2118+
`(cond ,@(nreverse cs) (t (setq ,retvar nil))))))
2119+
((and `(,(or 'let 'let*) ,bindings . ,exps)
2120+
(guard
2121+
;; Note: it's OK for this `let' to shadow any
2122+
;; of the formal arguments since we will only
2123+
;; setq the fresh new `ofargs' vars instead ;-)
2124+
(let ((shadowings (mapcar #'car bindings)))
2125+
;; If `var' is shadowed, then it clearly can't be
2126+
;; tail-called any more.
2127+
(not (memq var shadowings)))))
2128+
`(,(car exp) ,bindings . ,(funcall opt-exps exps)))
2129+
(_
2130+
`(progn (setq ,retvar ,exp) nil))))))
2131+
2132+
(let ((optimized-body (funcall opt-exps body)))
2133+
(if (not done)
2134+
(cons fargs body)
2135+
;; We use two sets of vars: `ofargs' and `fargs' because we need
2136+
;; to be careful that if a closure captures a formal argument
2137+
;; in one iteration, it needs to capture a different binding
2138+
;; then that of other iterations, e.g.
2139+
(cons
2140+
ofargs
2141+
`((let (,retvar)
2142+
(while (let ,(delq nil
2143+
(cl-mapcar
2144+
(lambda (a oa)
2145+
(unless (memq a cl--lambda-list-keywords)
2146+
(list a oa)))
2147+
fargs ofargs))
2148+
. ,optimized-body))
2149+
,retvar)))))))
2150+
20632151
;;;###autoload
20642152
(defmacro cl-labels (bindings &rest body)
2065-
"Make local (recursive) function definitions.
2066-
Each definition can take the form (FUNC ARGLIST BODY...) where
2153+
"Make local (recursive) function definitions.
2154+
+BINDINGS is a list of definitions of the form (FUNC ARGLIST BODY...) where
20672155
FUNC is the function name, ARGLIST its arguments, and BODY the
20682156
forms of the function body. FUNC is defined in any BODY, as well
20692157
as FORM, so you can write recursive and mutually recursive
@@ -2075,17 +2163,33 @@ details.
20752163
(let ((binds ()) (newenv macroexpand-all-environment))
20762164
(dolist (binding bindings)
20772165
(let ((var (make-symbol (format "--cl-%s--" (car binding)))))
2078-
(push (list var `(cl-function (lambda . ,(cdr binding)))) binds)
2166+
(push (cons var (cdr binding)) binds)
20792167
(push (cons (car binding)
20802168
(lambda (&rest args)
20812169
(if (eq (car args) cl--labels-magic)
20822170
(list cl--labels-magic var)
20832171
(cl-list* 'funcall var args))))
20842172
newenv)))
2085-
(macroexpand-all `(letrec ,(nreverse binds) ,@body)
2086-
;; Don't override lexical-let's macro-expander.
2087-
(if (assq 'function newenv) newenv
2088-
(cons (cons 'function #'cl--labels-convert) newenv)))))
2173+
;; Don't override lexical-let's macro-expander.
2174+
(unless (assq 'function newenv)
2175+
(push (cons 'function #'cl--labels-convert) newenv))
2176+
;; Perform self-tail call elimination.
2177+
(setq binds (mapcar
2178+
(lambda (bind)
2179+
(pcase-let*
2180+
((`(,var ,sargs . ,sbody) bind)
2181+
(`(function (lambda ,fargs . ,ebody))
2182+
(macroexpand-all `(cl-function (lambda ,sargs . ,sbody))
2183+
newenv))
2184+
(`(,ofargs . ,obody)
2185+
(cl--self-tco var fargs ebody)))
2186+
`(,var (function (lambda ,ofargs . ,obody)))))
2187+
(nreverse binds)))
2188+
`(letrec ,binds
2189+
. ,(macroexp-unprogn
2190+
(macroexpand-all
2191+
(macroexp-progn body)
2192+
newenv)))))
20892193

20902194
;; The following ought to have a better definition for use with newer
20912195
;; byte compilers.

lisp/subr.el

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,9 +1893,14 @@ all symbols are bound before any of the VALUEFORMs are evalled."
18931893
`(let ,(mapcar #'car binders)
18941894
,@(mapcar (lambda (binder) `(setq ,@binder)) binders)
18951895
,@body))))
1896-
(if seqbinds
1897-
`(let* ,(nreverse seqbinds) ,nbody)
1898-
nbody))))
1896+
(cond
1897+
;; All bindings are recursive.
1898+
((null seqbinds) nbody)
1899+
;; Special case for trivial uses.
1900+
((and (symbolp nbody) (null (cdr seqbinds)) (eq nbody (caar seqbinds)))
1901+
(nth 1 (car seqbinds)))
1902+
;; General case.
1903+
(t `(let* ,(nreverse seqbinds) ,nbody))))))
18991904

19001905
(defmacro dlet (binders &rest body)
19011906
"Like `let*' but using dynamic scoping."

test/lisp/emacs-lisp/cl-macs-tests.el

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,21 @@ collection clause."
616616
;; Simple recursive function.
617617
(cl-labels ((len (xs) (if xs (1+ (len (cdr xs))) 0)))
618618
(should (equal (len (make-list 42 t)) 42)))
619-
)
619+
620+
;; Simple tail-recursive function.
621+
(cl-labels ((len (xs n) (if xs (len (cdr xs) (1+ n)) n)))
622+
(should (equal (len (make-list 42 t) 0) 42))
623+
;; Should not bump into stack depth limits.
624+
(should (equal (len (make-list 42000 t) 0) 42000)))
625+
626+
;; Check that non-recursive functions are handled more efficiently.
627+
(should (pcase (macroexpand '(cl-labels ((f (x) (+ x 1))) (f 5)))
628+
(`(let* ,_ (funcall ,_ 5)) t)))
629+
630+
;; Case of "tail-recursive lambdas".
631+
(should (pcase (macroexpand
632+
'(cl-labels ((len (xs n) (if xs (len (cdr xs) (1+ n)) n)))
633+
#'len))
634+
(`(function (lambda (,_ ,_) . ,_)) t))))
620635

621636
;;; cl-macs-tests.el ends here

0 commit comments

Comments
 (0)