Compiler construction 验证OCaml函数是否为尾部递归函数

Compiler construction 验证OCaml函数是否为尾部递归函数,compiler-construction,ocaml,tail-recursion,short-circuiting,Compiler Construction,Ocaml,Tail Recursion,Short Circuiting,如何判断OCaml是否将特定函数识别为尾部递归?特别是,我想知道OCaml编译器是否能够识别 感谢Jeffrey下面的回答,我尝试了这个简单的函数 let rec check_all l = match l with | [] -> true | hd :: tl -> hd && check_all tl 事实上,它确实优化了: camlTest__check_all_1008: .cfi_startpro

如何判断OCaml是否将特定函数识别为尾部递归?特别是,我想知道OCaml编译器是否能够识别


感谢Jeffrey下面的回答,我尝试了这个简单的函数

let rec check_all l =
    match l with
    | [] -> true
    | hd :: tl ->
        hd && check_all tl
事实上,它确实优化了:

camlTest__check_all_1008:
        .cfi_startproc
.L102:
        cmpl    $1, %eax
        je      .L100
        movl    (%eax), %ebx
        cmpl    $1, %ebx
        je      .L101
        movl    4(%eax), %eax
        jmp     .L102
        .align  16
.L101:
        movl    $1, %eax
        ret

关于OCaml内部,许多其他人比我更明智,但对于简单的函数,很容易在ocamlopt生成的汇编代码中看到尾部递归:

$ cat tailrec.ml
let rec f a x = if x <= 1 then a else f (a * x) (x - 1)

let rec g x = if x <= 1 then 1 else x * g (x - 1)
$ ocamlopt -c -S tailrec.ml
编译器已将递归调用更改为循环(即,函数是尾部递归的)

以下是您通过
g
获得的:

        .cfi_startproc
        subq    $8, %rsp
        .cfi_adjust_cfa_offset  8
.L103:
        cmpq    $3, %rax
        jg      .L102
        movq    $3, %rax
        addq    $8, %rsp
        .cfi_adjust_cfa_offset  -8
        ret
        .cfi_adjust_cfa_offset  8
        .align  2
.L102:
        movq    %rax, 0(%rsp)
        addq    $-2, %rax
        call    _camlTailrec__g_1011
.L104:
        movq    %rax, %rbx
        sarq    $1, %rbx
        movq    0(%rsp), %rax
        decq    %rax
        imulq   %rbx, %rax
        incq    %rax
        addq    $8, %rsp
        .cfi_adjust_cfa_offset  -8
        ret
        .cfi_adjust_cfa_offset  8
        .cfi_endproc
递归由实际的递归调用(不是尾部递归)处理


正如我所说,如果您比我更了解OCaml中间形式,可能有更好的方法来解决这个问题。

从OCaml 4.03开始,尽管文件中有输入错误,您可以在函数应用程序中使用
@tailcall
,如果不是这样,编译器将发出警告

(f[@tailcall])x y
如果
f x y
不是尾部调用,则发出警告

例如:

$ cat tailrec.ml
let rec f a x = if x <= 1 then a else (f [@tailcall]) (a * x) (x - 1)

let rec g x = if x <= 1 then 1 else x * (g [@tailcall]) (x - 1)

$ ocaml tailrec.ml

File "tailrec.ml", line 3, characters 40-63:
Warning 51: expected tailcall
$cat tailrec.ml

让rec f a x=if x我想知道,为什么没有人告诉我古老的
-annot
选项,它将为所有调用转储注释。尽管使用汇编是最可靠的方法,但并不是每个人都擅长阅读汇编。但有了annot,它是如此简单,以至于你甚至可以将其自动化。例如,假设您的代码位于
test.ml
文件中,我们可以使用以下一行代码自动检查所有调用是否处于尾部位置:

 ocamlc -annot test.ml && if grep -A1 call test.annot | grep stack; then echo "non tailrecursive"; exit 1; fi
ocaml-annot test.ml
将编译一个文件,创建一个
test.annot
文件,该文件将包含每个表达式的注释。grep-A1 call test.annot
将提取所有调用注释,并查看其内容。如果至少有一个堆栈调用,
grep堆栈将返回true

实际上,您甚至可以在存储库中找到一个emacs补充,它将从
annot
文件中提取此信息。例如,有一个
caml-types-show-call
函数,它将显示在该点指定的函数的一种调用。但是,此函数当前有一个bug(看起来不再受支持),要修复它,您需要对其应用以下修补程序:

--- a/emacs/caml-types.el
+++ b/emacs/caml-types.el
@@ -221,7 +221,7 @@ See `caml-types-location-re' for annotation file format."
               (right (caml-types-get-pos target-buf (elt node 1)))
               (kind (cdr (assoc "call" (elt node 2)))))
           (move-overlay caml-types-expr-ovl left right target-buf)
-          (caml-types-feedback kind)))))
+          (caml-types-feedback "call: %s" kind)))))
     (if (and (= arg 4)
              (not (window-live-p (get-buffer-window caml-types-buffer))))
         (display-buffer caml-types-buffer))

回答得好(+1)。准确地说,我们不应该谈论尾部调用和尾部调用优化吗?真正的问题是OCaml是否优化尾部调用,而不是它是否识别尾部调用(不管这意味着什么)。随着事情变得越来越复杂,信息也会在
-annot
选项的输出中提供给编译器,一个显示类型注释的工具可以用尾部调用信息装饰文件。
ocamlopt[.opt]
-dlinear
选项在修改后的源代码输出中也会有注释,带有尾部调用。顺便问一下:您是如何获得这么好的汇编输出的?请参见下面Jeffrey的回答:ocamlopt-c-SPerhaps您应该提交PR?可能吧,但我不想让OCaml开发人员负担过重,因为PR太小(有很多待处理的PRs),更好的办法是将这些代码提取到一个单独的回购协议中。
--- a/emacs/caml-types.el
+++ b/emacs/caml-types.el
@@ -221,7 +221,7 @@ See `caml-types-location-re' for annotation file format."
               (right (caml-types-get-pos target-buf (elt node 1)))
               (kind (cdr (assoc "call" (elt node 2)))))
           (move-overlay caml-types-expr-ovl left right target-buf)
-          (caml-types-feedback kind)))))
+          (caml-types-feedback "call: %s" kind)))))
     (if (and (= arg 4)
              (not (window-live-p (get-buffer-window caml-types-buffer))))
         (display-buffer caml-types-buffer))