Unfinished functor evaluation on an exception, in the toplevel

The code in the file at ocaml_repository/exception_in_module_from_functor.ml at main · jonathandoyle58/ocaml_repository · GitHub, produces the following surprising output in utop version 2.7.0 (using OCaml version 4.11.0) :

g_add_compatible 3 [1; 2; 3; 5; 6; 7] ;;
Exception: Next_level(Preceding_level).Impatient_measure_exn _.

Two things surprise (and bother) me here :

  1. The underline character instead of an explicitly description of the exception argument and
  2. The module is written is an “unfinished evaluation” form Next_level(Preceding_level) ; I happen to know that in this particular instruction, Preceding_level is Level_two and Next_level(Preceding_level) is Level_three, and it seems clear that the compiler needs to determine that also before making any further evaluations.

I don’t remember anything like this being explained in the documentation on functors, but maybe I missed something … What is going on here ?

When the toplevel is stopped by an uncaught exception, if the exception itself is in scope then it will print it as you would expect (including its payload), but when the exception is not in scope it has no type information to print the payload so it relies on runtime information. The name of the constructor is computed at compile time based on the surrounding code structure (it’s in the functor Next_level, which has an argument named Preceding_level, and those are used to compose the name of the exception that is stored in its runtime value), and the payload is printed only if it is a string at runtime.

Here is a toplevel session that shows some examples in action:

# module Not_string : sig
  type t
  val s : string -> t
end = struct
  type t = string
  let s x = x
end;;
            module Not_string : sig type t val s : string -> t end
# exception E1 of string;;
exception E1 of string
# exception E2 of int list;;
exception E2 of int list
# exception E3 of Not_string.t;;
exception E3 of Not_string.t
# raise (E1 "foo");;
Exception: E1 "foo".
# raise (E2 [1]);;
Exception: E2 [1].
# raise (E3 (Not_string.s "foo"));;
Exception: E3 <abstr>.
# module M1 : sig
  val f : unit -> unit
end = struct
  exception M1 of string
  let f () = raise (M1 "foo")
end;;
          module M1 : sig val f : unit -> unit end
# let () = M1.f ();;
Exception: M1.M1 "foo".
# module M2 : sig
  val f : unit -> unit
end = struct
  exception M2 of int list
  let f () = raise (M2 [1])
end;;
          module M2 : sig val f : unit -> unit end
# let () = M2.f ();;
Exception: M2.M2 _.
# module M3 : sig
  val f : unit -> unit
end = struct
  exception M3 of Not_string.t
  let f () = raise (M3 (Not_string.s "foo"))
end;;
          module M3 : sig val f : unit -> unit end
# let () = M3.f ();;
Exception: M3.M3 "foo".

Thanks for your feedback.
What does it mean exactly for an exception to be “in scope” in the toplevel ?
Initially I thought that the reason that the M2.M2 exception is displayed in an incomplete manner in your example, is because it is not declared in the signature ; but that can’t be right because the M3.M3 exception is displayed and yet it is not declared in the signature you wrote. I’m still confused.

The easiest way to look at it is whether you can refer to the exception directly.
In my example, I can refer to E1, E2 and E3 directly in the toplevel, but M1.M1, M2.M2 and M3.M3 are not in scope (you can try raise (M1.M1 "foo"), and you’ll get an error).

Both M1.M1 and M3.M3 only display their payloads because they’re strings at runtime. If M3.M3 was in scope, it would instead display an <abstr> payload, like E3.

Note that in your example, the use of a functor complicates matters. I’m not completely sure of exactly what’s going on, but I suspect that when an exception is defined in a functor body, the toplevel can’t accurately track which concrete exceptions correspond to which applications of the functor and will use the same printing as for out of scope exceptions, even if it turns out that the exception is actually in scope under another name.

Adding to the previous examples:

# module M4 : sig
  exception M4 of int list
  val f : unit -> unit
end = struct
  exception M4 of int list
  let f () = raise (M4 [1])
end;;
            module M4 : sig exception M4 of int list val f : unit -> unit end
# let () = M4.f ();;
Exception: M4.M4 [1].
# module F5 (Arg : sig end) : sig
  exception M5 of int list
  val f : unit -> unit
end = struct
  exception M5 of int list
  let f () = raise (M5 [1])
end;;
            module F5 :
  functor (Arg : sig end) ->
    sig exception M5 of int list val f : unit -> unit end
# module Arg = struct end;;
module Arg : sig end
# module M5 = F5 (Arg);;
module M5 : sig exception M5 of int list val f : unit -> unit end
# let () = M5.f ();;
Exception: F5(Arg).M5 _.

There is an exception M5.M5 in scope, but the toplevel can’t recognize it as the same as the one thrown by M5.f ()

1 Like

Not quite. If the payload is a int * int * string, not just a string, it also gets printed fully at runtime :

module M2 : sig
  val f : unit -> unit
end = struct
  exception M2 of int * int * string
  let f () = raise (M2 (7,8, "9"))
end;;
module M2 : sig val f : unit -> unit end
utop # M2.f () ;;
Exception: M2.M2 (7, 8, "9").

In any case, moving all the exception definitions out of the functor body (and if necessary adding an extra parameter to some exceptions to express which functor application they “belong” to) worked for me. Thanks for your help.