验证OCaml函数是否为尾递归

时间:2014-04-20 19:33:15

标签: compiler-construction ocaml tail-recursion short-circuiting

如何判断OCaml是否将特定函数识别为尾递归?特别是,我想知道OCaml编译器是否识别Short-circuited operators and tail recursion


感谢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

3 个答案:

答案 0 :(得分:9)

从OCaml 4.03开始,尽管Changes文件中存在拼写错误,但您可以在函数应用程序中使用@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

答案 1 :(得分:8)

许多其他人比我更关注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

如果您忽略了大量额外输出,则会看到f

_camlTailrec__f_1008:
        .cfi_startproc
.L101:
        cmpq    $3, %rbx
        jg      .L100
        ret
        .align  2
.L100:
        movq    %rbx, %rdi
        addq    $-2, %rdi
        sarq    $1, %rbx
        decq    %rax
        imulq   %rbx, %rax
        incq    %rax
        movq    %rdi, %rbx
        jmp     .L101
        .cfi_endproc

编译器已将递归调用更改为循环(即函数是尾递归)。

以下是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中间形式,可能有更好的方法来解决这个问题。

答案 2 :(得分:1)

我想知道,为什么没有人告诉过古老的-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将为文件编译一个create test.annot文件,该文件将包含每个表达式的注释。 grep -A1 call test.annot将提取所有调用注释,并查看其内容。如果至少有一个堆栈调用,grep stack将返回true。

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

--- 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))