Login

Errors

OCaml migration error when you have a supported version of OCaml

$ dune build
File "lib/point.ml", line 1, characters 15-18:
1 | type point = x:int * y:int
                   ^^^
Error: migration error: labelled tuples are not supported before OCaml 5.4
$ ocaml --version
The OCaml toplevel, version 5.4.0

The heck? It's not ocaml complaining, but a preprocessor using an earlier-versioned OCaml AST (here: ppx_sexp_conv v0.17.1).

Bad list syntax -> bad ocamlformat -> bad list

Consider this example of dependency injection via module (inspired by why ocaml):

module type IO = sig
  val print_endline : string -> unit
  val read_line : unit -> string
end

let program (module Handler : IO) =
  let () = Handler.print_endline "Hello World" in
  let () = Handler.print_endline "What is your name?" in
  let name = Handler.read_line () in
  Handler.print_endline ("Hello " ^ name)

let () =
  let output = ref [] in
  let module TestIO = struct
    let print_endline s = output := s :: !output
    let read_line () = "Test user"
  end in
  program (module TestIO);
  assert (!output = ["Hello Test user", "What is your name?", "Hello World"])

The last line exhibits an easy error in OCaml if you're very used to other languages: that's not a list of three strings, but a list of one string * string * string tuple, making it a compile-time error as that's incompatible with !output:

19 |   assert (!output = ["Hello Test user", "What is your name?", "Hello World"])
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type 'a * 'b * 'c
       but an expression was expected of type string

OK, so you open the file back up, reflexively ocamlformat it, and then change commas to semicolons. Resulting in this line:

  assert (!output = [("Hello Test user"; "What is your name?"; "Hello World")])

Which is no longer an error! It compiles! But suspiciously, it compiles with some warnings, and the assertion fails:

$ ocaml o17.ml 
File "./o17.ml", line 19, characters 22-39:
19 |   assert (!output = [("Hello Test user"; "What is your name?"; "Hello World")])
                           ^^^^^^^^^^^^^^^^^
Warning 10 [non-unit-statement]: this expression should have type unit.

File "./o17.ml", line 19, characters 41-61:
19 |   assert (!output = [("Hello Test user"; "What is your name?"; "Hello World")])
                                              ^^^^^^^^^^^^^^^^^^^^
Warning 10 [non-unit-statement]: this expression should have type unit.

Exception: Assert_failure ("./o17.ml", 19, 2).

ocamlformat added parentheses to make it clear that we had a tuple, but parentheses are not actually a syntax for tuples - it's commas that create tuples. The result is instead equivalent to begin "Hello Test User"; ... end, so what's created is a list of a single string, and the first two strings are discarded.

With those parentheses removed, the assertions pass.

Error: Multiple rules generated for _build/default/bin/somefile.pp.ml

$ dune build
Error: Multiple rules generated for _build/default/bin/somefile.pp.ml:
- bin/dune:6
- bin/dune:13
-> required by alias default

This happens when you have multiple stanzas with ppx preprocessors defined in the same dune file, for files in the same dir. This can be fixed by adding (modules ...) to each stanza. This is more likely to happen with executables, since it's more natural to put all of a library under the same stanza with its single list of preprocessors.