Skip to content

Confusing interaction between @tailcall and inlining #9510

@craigfe

Description

@craigfe

In the following example,

module A : sig end = struct
  let _ = fun () ->
    let f () =
      let rec aux k = if true then k () else (aux [@tailcall]) k in
      (aux [@tailcall]) (fun () -> ())
    in
    f (); ()
end

the second [@tailcall] annotation causes the program to be rejected:

File "example.ml", line 5, characters 6-38:
5 |       (aux [@tailcall]) (fun () -> ())
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning 51: expected tailcall

This happens due to a "used-once" inlining of f:

ᐅ ocamlopt.opt -dlambda example.ml
(let
  (A/80 =
     (seq
       (function param/86
         (seq
           (let (param/88 = 0a)
             (letrec
               (aux/82
                  (function k/83
                    (if 1a (apply k/83 0a) (apply aux/82 k/83 @tailcall))))
               (apply aux/82 (function param/84 0a) @tailcall)))
           0a))
       (makeblock 0)))
  (seq (setfield_ptr(root-init) 0 (global IO!) A/80) 0a))

If f is applied twice, or defined with [@local never], the program is accepted.

This behaviour leads to inconsistencies according to the specific compiler being used. With a modified example,

module A : sig end = struct
  let f () =
    let rec aux k = if true then k () else (aux [@tailcall]) k in
    (aux [@tailcall]) (fun () -> ())

  let () = f ()
end

the code is accepted by ocamlopt (which does not inline in this case), but rejected by ocamlc and Flambda (which do inline).

CC: @mshinwell, @lthls.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions