Кроме того, когда я вижу цикл Common Lisp, основан ли он на хвостовой рекурсии под капотом?
Это деталь реализации, но, скорее всего, нет.Во-первых, CL уже допускает блоки TAGBODY
, что делает LOOP
выразимым в терминах конструкций CL.
Например, если я макрорасширяю простой LOOP:
(loop)
, я получаюдовольно единообразный результат для всех реализаций.
;; SBCL
(BLOCK NIL (TAGBODY #:G869 (PROGN) (GO #:G869)))
;; CCL
(BLOCK NIL (TAGBODY #:G4 (PROGN) (GO #:G4)))
;; JSCL
(BLOCK NIL (TAGBODY #:G869 (PROGN) (GO #:G869)))
;; ECL
(BLOCK NIL (TAGBODY #:G109 (PROGN) (GO #:G109)))
;; ABCL
(BLOCK NIL (TAGBODY #:G44 (GO #:G44)))
Реализация обычно пишется на языках, в которых есть переходы или циклы, или которые могут легко эмулировать их.Более того, многие реализации CL компилируются и могут быть нацелены на язык ассемблера, который имеет примитивы перехода.Поэтому обычно нет необходимости иметь промежуточный шаг, который проходит через хвостовые рекурсивные функции.
При этом реализация TAGBODY
с хвостовой рекурсией кажется выполнимой.Например, JSCL разрезает выражения внутри tagbody
на разные методы для каждой метки, и эти методы вызываются при использовании go
: https://github.com/jscl-project/jscl/blob/db07c5ebfa2e254a0154666465d6f7591ce66e37/src/compiler/compiler.lisp#L982
Более того, если я позволю loop
работать длянекоторое время переполнение стека не происходит.В этом случае, однако, это происходит не из-за устранения хвостового вызова (который, AFAIK, не реализован во всех браузерах).Похоже, что код для tagbody
всегда имеет неявный цикл while
, и что go
выдает исключения для tagbody
для отлова.