Dune and module initialization side effects

I came across an arrangement of libraries where using dune produces confusing results (well, at least to me).

The fully functioning (and somewhat minimized) example of this can be found at https://github.com/martinslota/ocuzzle-module-initialization. It’s a bit hard to present succinctly because it seems related to the way linking does or does not happen. But here’s an attempt to paste it here all in one spot:

File: q_and_a/q_and_a.ml

module type Question = sig
  val question : string
end

module Make_question () : Question = struct
  let question = "How much is 6 * 9?"
end

module Make_answer (Q : Question) = struct
  let () = Printf.printf "Making answer for question: %s\n" Q.question

  let answer = 42
end

module Question : Question = struct
  let question = "What's on your mind?"
end

File: q_and_a/dune:

(library
  (name q_and_a))

File: cases/case1.ml

module Question = Q_and_a.Make_question ()
module Answer = Q_and_a.Make_answer (Question)

File: cases/case2.ml

module Question = Q_and_a.Question
module Answer = Q_and_a.Make_answer (Question)

File: cases/dune

(library
  (name cases)
  ; (library_flags (:standard -linkall))
  (libraries q_and_a))

File: mystery.ml

let () =
  Printf.printf "Case 1 question: %s\n" Cases.Case1.Question.question;
  Printf.printf "Case 2 question: %s\n" Cases.Case2.Question.question

File: dune:

(executable 
  (name mystery)
  (libraries cases))

If you run this, you should see the following:

$ dune exec mystery/mystery.exe
Making answer for question: How much is 6 * 9?
Case 1 question: How much is 6 * 9?
Case 2 question: What's on your mind?

However, if you uncomment the ; (library_flags (:standard -linkall)) line above, then you should see this instead:

$ dune exec mystery/mystery.exe
Making answer for question: What's on your mind?
Making answer for question: How much is 6 * 9?
Case 1 question: How much is 6 * 9?
Case 2 question: What's on your mind?

Is this the expected behaviour?

It is the expected behavior when compiling with -no-alias-deps (which is the case with dune) due to module aliases.

Indeed, in mysterious.ml third line:

     Printf.printf "Case 2 question: %s\n" Cases.Case2.Question.question

the module Cases.Case2.Question is a module alias for Q_and_A.Question. Thus, the identifier Cases.Case2.Question.question is directly translated to Q_and_A.Question.question . Consequently, the Mysterious module does not really depend on the intermediary module Case2 and the initialization of Case2 is skipped without -linkall.

A somewhat shorter reproduction case would be

(* core.ml *)
let x = 0
(* relay.ml *)
module Core = Core
;; Format.printf "Relay was initialized@."
(* main.ml *)
Format.printf "Core.x=%d" Relay.Core.x

Then the following compiles:

$ ocamlc -a core.ml -o core.cma
$ ocamlc -a relay.ml -o relay.cma
$ ocamlc -no-alias-deps -o main core.cma relay.cma main.ml

However, the Relay initialization is completely skipped, because it is not necessary to link any module in the relay.cma archive, and just

$ ocamlc -no-alias-deps -o main core.cma main.ml

works fine.

1 Like

Many thanks for this insight! I think I’ll need to go educate myself a bit more.

Do you know why dune uses -no-alias-deps?

I guess part of the answer to my own question is in this comment: