Module linking in OCaml

I noticed that if a module exists solely for side effect such that it does not have a top level value defined, then it does not always get executed (native code from ocamlopt). Not even when it is forcefully opened in another module (with [@warning "-33"] ). My guess is code generation simply skips it. When I stripped down the code to the following:

file A:

let a = ref ""

file B:

open A
;;
a := "assigned or not?"

file C:

let [@warning "-33"] () = let open B in ()
;;
print_endline !A.a

It actually works as expected (B did load). But in a much larger codebase I am working on this did not work. What worked for me is to have a value in B:

file B:

let a = A.a
;;
a := "assigned or not?"

file C:

print_endline !B.a

Is this a bug or a feature? I am on OCaml 4.10.2. Both the toy model above and my project were built with dune.

Just realized more likely than not that this may be dune related. I am on version 2.9.0. Maybe it is not calculating the dependency correctly sometimes. What I don’t know is what the canonical semantics says about this (if there is a canonical expected behavior here). Or this is undefined behavior?

Another observation is for the particular module I am having trouble with my lsp-server seems to have trouble with getting anything through as well. So another oddity. This is the file if anyone is curious about it.

The order of the execution of toplevel modules depends on the linking order of modules in the final executable. Creating a fictitious dependency with an open is only an indirect way to enforce an order in the list of linked modules. The issue might be related to dependency computation. Which module is not executed in time in your linked example?

When you link with a library, then only the modules from the library that are actually used by the rest of the program get included in the final executable.
So if a module has an empty signature, by definition no other module can actually use it (open doesn’t count as an use as it only creates a dependency on the interface, not on the implementation).
The normal way to deal with it is to use the -linkall flag. You can either pass it when compiling the file that has side-effects, or when building the library (in which case it will apply to all units in the library).
I don’t know how to do the former with dune, but the latter can be done with (library_flags -linkall).

1 Like

Note that if a module is not listed as a dependency by ocamldep of the main file of the executable it won’t get linked by dune. You can add a dependency simply by doing module B = B at the top of the main file of the executable.

Cheers,
Nicolas

Since dune use -no-alias-deps by default, a module alias doesn’t count as a dependency.

Overall, I personally often find it simpler to have a register () function that either evaluates the relevant side effect, or manifests the dependencies on the effectful module.

-no-alias-deps is used when compiling but as far as what to link dune uses the dependencies returned by ocamldep and ocamldep will return a dependency for module aliases.

Cheers,
Nicolas

Ah yes, dune is not running ocamldep with -as-map thus ocamldep does approximate the module alias as a false dependency on the aliased module. Anyway, relying on this approximation seems quite brittle.

The file I linked to database.ml(holnat/database.ml at master · htzh/holnat · GitHub) is the one I had trouble with. It works now with the workaround of forcing a top level value.

However I can’t reproduce this with the toy model summarizing the same issue. That is the module B did get linked in both examples shown above, so there is some inconsistency I don’t yet understand. After some fiddling I believe it is because the toy model files are all in the same directory (library), while in my codebase module A and B are in the same library while C is from outside. So after the modification as below:

dune for lib:
(library
 (name lib)
)

dune for module C:
(executable
 (name c)
 (libraries lib)
)

c.ml:
let [@warning "-33"] () = let open Lib.B in ()
;;
print_endline !Lib.A.a

Now the executable c.exe skips module B and prints empty string.

Thanks I believe my problem was with linking with a library.