diff --git a/docs/_html/index.html b/docs/_html/index.html index 7cb827c..ef1d81c 100644 --- a/docs/_html/index.html +++ b/docs/_html/index.html @@ -11,7 +11,7 @@

OCaml package documentation

    -
  1. ppx_minidebug 2.0.0
  2. +
  3. ppx_minidebug 2.0.1
diff --git a/docs/_html/ppx_minidebug/Minidebug_runtime/PrintBox/index.html b/docs/_html/ppx_minidebug/Minidebug_runtime/PrintBox/index.html index 898e705..6d4cf8b 100644 --- a/docs/_html/ppx_minidebug/Minidebug_runtime/PrintBox/index.html +++ b/docs/_html/ppx_minidebug/Minidebug_runtime/PrintBox/index.html @@ -33,6 +33,6 @@ log_level:int -> is_result:bool -> string -> - unit
val log_value_printbox : entry_id:int -> log_level:int -> PrintBox.t -> unit
val exceeds_max_nesting : unit -> bool
val exceeds_max_children : unit -> bool
val get_entry_id : unit -> int
val max_nesting_depth : int option Stdlib.ref
val max_num_children : int option Stdlib.ref
val global_prefix : string
val snapshot : unit -> unit

For PrintBox runtimes, outputs the current logging stack to the logging channel. If the logging channel supports that, an output following a snapshot will rewind the channel to the state prior to the snapshot. Does nothing for the Flushing runtimes.

val description : string

A description that should be sufficient to locate where the logs end up. If not configured explicitly, it will be some combination of: the global prefix, the file name or "stdout".

val no_debug_if : bool -> unit

For PrintBox runtimes, when passed true within the scope of a log subtree, disables the logging of this subtree and its subtrees. Does not do anything when passed false (no_debug_if false does not re-enable the log). Does nothing for the Flushing runtimes.

val log_level : int Stdlib.ref

The runtime log level.

The log levels are used both at compile time, and for the PrintBox runtime. Not logging at compile time means the corresponding logging code is not generated; not logging at runtime means the logging state is not updated.

type config = {
  1. mutable backend : [ `Text + unit
val log_value_printbox : entry_id:int -> log_level:int -> PrintBox.t -> unit
val exceeds_max_nesting : unit -> bool
val exceeds_max_children : unit -> bool
val get_entry_id : unit -> int
val max_nesting_depth : int option Stdlib.ref
val max_num_children : int option Stdlib.ref
val global_prefix : string
val snapshot : unit -> unit

For PrintBox runtimes, outputs the current logging stack to the logging channel. If the logging channel supports that, an output following a snapshot will rewind the channel to the state prior to the snapshot. Does nothing for the Flushing runtimes.

val description : string

A description that should be sufficient to locate where the logs end up. If not configured explicitly, it will be some combination of: the global prefix, the file name or "stdout".

val no_debug_if : bool -> unit

For PrintBox runtimes, when passed true within the scope of a log subtree, disables the logging of this subtree and its subtrees. Does not do anything when passed false (no_debug_if false does not re-enable the log). Does nothing for the Flushing runtimes.

val log_level : int Stdlib.ref

The runtime log level.

The log levels are used both at compile time, and for the PrintBox runtime. Not logging at compile time means the corresponding logging code is not generated; not logging at runtime means the logging state is not updated.

type config = {
  1. mutable backend : [ `Text | `Html of PrintBox_html.Config.t | `Markdown of PrintBox_md.Config.t ];
    (*

    If the content is `Text, logs are generated as monospaced text; for other settings as html or markdown.

    *)
  2. mutable boxify_sexp_from_size : int;
    (*

    If positive, Sexp.t-based logs with this many or more atoms are converted to print-boxes before logging. Disabled by default (i.e. negative).

    *)
  3. mutable highlight_terms : Re.re option;
    (*

    Uses a highlight style for logs on paths ending with a log matching the regular expression.

    *)
  4. mutable exclude_on_path : Re.re option;
    (*

    Does not propagate the highlight status from child logs through log headers matching the given regular expression.

    *)
  5. mutable prune_upto : int;
    (*

    At depths lower than prune_upto (or equal if counting from 1) only ouptputs highlighted boxes. This makes it simpler to trim excessive logging while still providing some context. Defaults to 0 -- no pruning.

    *)
  6. mutable truncate_children : int;
    (*

    If > 0, only the given number of the most recent children is kept at each node. Defaults to 0 -- keep all (no pruning).

    *)
  7. mutable values_first_mode : bool;
    (*

    If set to true, does not put the source code location of a computation as a header of its subtree. Rather, puts the result of the computation as the header of a computation subtree, if rendered as a single line -- or just the name, and puts the result near the top. If false, puts the result at the end of the computation subtree, i.e. preserves the order of the computation.

    *)
  8. mutable max_inline_sexp_size : int;
    (*

    Maximal size (in atoms) up to which a sexp value can be inlined during "boxification".

    *)
  9. mutable max_inline_sexp_length : int;
    (*

    Maximal length (in characters/bytes) up to which a sexp value can be inlined during "boxification".

    *)
  10. mutable snapshot_every_sec : float option;
    (*

    If given, output a snapshot of the pending logs when at least the given time (in seconds) has passed since the previous output. This is only checked at calls to log values.

    *)
  11. mutable sexp_unescape_strings : bool;
    (*

    If true, when a value is a sexp atom or is decomposed into a sexp atom by boxification, it is not printed as a sexp, but the string of the atom is printed directly. Defaults to true.

    *)
  12. mutable with_toc_listing : bool;
    (*

    If true, outputs non-collapsed trees of ToC entries in the Table of Contents files.

    *)
  13. mutable toc_flame_graph : bool;
    (*

    If true, outputs a minimalistic rendering of a flame graph in the Table of Contents files, with boxes positioned to reflect both the ToC entries hierarchy and elapsed times for the opening and closing of entries. Not supported in the `Text backend.

    *)
  14. mutable flame_graph_separation : int;
    (*

    How many pixels a single box, for a log header, is expected to take in a flame graph. Defaults to 40. Note: ideally the height of a flame tree should be calculated automatically, then this setting would disappear.

    *)
}
val config : config
diff --git a/docs/_html/ppx_minidebug/Minidebug_runtime/module-type-PrintBox_runtime/index.html b/docs/_html/ppx_minidebug/Minidebug_runtime/module-type-PrintBox_runtime/index.html index b33f08e..b52b094 100644 --- a/docs/_html/ppx_minidebug/Minidebug_runtime/module-type-PrintBox_runtime/index.html +++ b/docs/_html/ppx_minidebug/Minidebug_runtime/module-type-PrintBox_runtime/index.html @@ -33,6 +33,6 @@ log_level:int -> is_result:bool -> string -> - unit
val log_value_printbox : entry_id:int -> log_level:int -> PrintBox.t -> unit
val exceeds_max_nesting : unit -> bool
val exceeds_max_children : unit -> bool
val get_entry_id : unit -> int
val max_nesting_depth : int option Stdlib.ref
val max_num_children : int option Stdlib.ref
val global_prefix : string
val snapshot : unit -> unit

For PrintBox runtimes, outputs the current logging stack to the logging channel. If the logging channel supports that, an output following a snapshot will rewind the channel to the state prior to the snapshot. Does nothing for the Flushing runtimes.

val description : string

A description that should be sufficient to locate where the logs end up. If not configured explicitly, it will be some combination of: the global prefix, the file name or "stdout".

val no_debug_if : bool -> unit

For PrintBox runtimes, when passed true within the scope of a log subtree, disables the logging of this subtree and its subtrees. Does not do anything when passed false (no_debug_if false does not re-enable the log). Does nothing for the Flushing runtimes.

val log_level : int Stdlib.ref

The runtime log level.

The log levels are used both at compile time, and for the PrintBox runtime. Not logging at compile time means the corresponding logging code is not generated; not logging at runtime means the logging state is not updated.

type config = {
  1. mutable backend : [ `Text + unit
val log_value_printbox : entry_id:int -> log_level:int -> PrintBox.t -> unit
val exceeds_max_nesting : unit -> bool
val exceeds_max_children : unit -> bool
val get_entry_id : unit -> int
val max_nesting_depth : int option Stdlib.ref
val max_num_children : int option Stdlib.ref
val global_prefix : string
val snapshot : unit -> unit

For PrintBox runtimes, outputs the current logging stack to the logging channel. If the logging channel supports that, an output following a snapshot will rewind the channel to the state prior to the snapshot. Does nothing for the Flushing runtimes.

val description : string

A description that should be sufficient to locate where the logs end up. If not configured explicitly, it will be some combination of: the global prefix, the file name or "stdout".

val no_debug_if : bool -> unit

For PrintBox runtimes, when passed true within the scope of a log subtree, disables the logging of this subtree and its subtrees. Does not do anything when passed false (no_debug_if false does not re-enable the log). Does nothing for the Flushing runtimes.

val log_level : int Stdlib.ref

The runtime log level.

The log levels are used both at compile time, and for the PrintBox runtime. Not logging at compile time means the corresponding logging code is not generated; not logging at runtime means the logging state is not updated.

type config = {
  1. mutable backend : [ `Text | `Html of PrintBox_html.Config.t | `Markdown of PrintBox_md.Config.t ];
    (*

    If the content is `Text, logs are generated as monospaced text; for other settings as html or markdown.

    *)
  2. mutable boxify_sexp_from_size : int;
    (*

    If positive, Sexp.t-based logs with this many or more atoms are converted to print-boxes before logging. Disabled by default (i.e. negative).

    *)
  3. mutable highlight_terms : Re.re option;
    (*

    Uses a highlight style for logs on paths ending with a log matching the regular expression.

    *)
  4. mutable exclude_on_path : Re.re option;
    (*

    Does not propagate the highlight status from child logs through log headers matching the given regular expression.

    *)
  5. mutable prune_upto : int;
    (*

    At depths lower than prune_upto (or equal if counting from 1) only ouptputs highlighted boxes. This makes it simpler to trim excessive logging while still providing some context. Defaults to 0 -- no pruning.

    *)
  6. mutable truncate_children : int;
    (*

    If > 0, only the given number of the most recent children is kept at each node. Defaults to 0 -- keep all (no pruning).

    *)
  7. mutable values_first_mode : bool;
    (*

    If set to true, does not put the source code location of a computation as a header of its subtree. Rather, puts the result of the computation as the header of a computation subtree, if rendered as a single line -- or just the name, and puts the result near the top. If false, puts the result at the end of the computation subtree, i.e. preserves the order of the computation.

    *)
  8. mutable max_inline_sexp_size : int;
    (*

    Maximal size (in atoms) up to which a sexp value can be inlined during "boxification".

    *)
  9. mutable max_inline_sexp_length : int;
    (*

    Maximal length (in characters/bytes) up to which a sexp value can be inlined during "boxification".

    *)
  10. mutable snapshot_every_sec : float option;
    (*

    If given, output a snapshot of the pending logs when at least the given time (in seconds) has passed since the previous output. This is only checked at calls to log values.

    *)
  11. mutable sexp_unescape_strings : bool;
    (*

    If true, when a value is a sexp atom or is decomposed into a sexp atom by boxification, it is not printed as a sexp, but the string of the atom is printed directly. Defaults to true.

    *)
  12. mutable with_toc_listing : bool;
    (*

    If true, outputs non-collapsed trees of ToC entries in the Table of Contents files.

    *)
  13. mutable toc_flame_graph : bool;
    (*

    If true, outputs a minimalistic rendering of a flame graph in the Table of Contents files, with boxes positioned to reflect both the ToC entries hierarchy and elapsed times for the opening and closing of entries. Not supported in the `Text backend.

    *)
  14. mutable flame_graph_separation : int;
    (*

    How many pixels a single box, for a log header, is expected to take in a flame graph. Defaults to 40. Note: ideally the height of a flame tree should be calculated automatically, then this setting would disappear.

    *)
}
val config : config
diff --git a/docs/_html/ppx_minidebug/Ppx_minidebug/index.html b/docs/_html/ppx_minidebug/Ppx_minidebug/index.html index 8593bf2..c4d64f3 100644 --- a/docs/_html/ppx_minidebug/Ppx_minidebug/index.html +++ b/docs/_html/ppx_minidebug/Ppx_minidebug/index.html @@ -1,21 +1,22 @@ -Ppx_minidebug (ppx_minidebug.Ppx_minidebug)

Module Ppx_minidebug

module A = Ppxlib.Ast_builder.Default
type log_value =
  1. | Sexp
  2. | Show
  3. | Pp
type toplevel_opt_arg =
  1. | Nested
  2. | Toplevel_no_arg
  3. | Runtime_passing
  4. | Runtime_local
val is_local_debug_runtime : toplevel_opt_arg -> bool
val global_log_count : int Stdlib.ref
type context = {
  1. log_value : log_value;
  2. track_or_explicit : [ `Diagn | `Debug | `Track ];
  3. output_type_info : bool;
  4. interrupts : bool;
  5. comptime_log_level : int;
  6. entry_log_level : int;
  7. hidden : bool;
  8. toplevel_opt_arg : toplevel_opt_arg;
}
val init_context : context Stdlib.ref
val parse_log_level : - Ppxlib.expression -> - (int, Ppxlib__.Import.expression) Stdlib.Either.t
val last_ident : Ppxlib.longident -> string
val typ2str : Ppxlib.core_type -> string
val pat2descr : +Ppx_minidebug (ppx_minidebug.Ppx_minidebug)

Module Ppx_minidebug

module A = Ppxlib.Ast_builder.Default
type log_value =
  1. | Sexp
  2. | Show
  3. | Pp
type toplevel_opt_arg =
  1. | Nested
  2. | Toplevel_no_arg
  3. | Runtime_passing
  4. | Runtime_local
val is_local_debug_runtime : toplevel_opt_arg -> bool
val global_log_count : int Stdlib.ref
type log_level =
  1. | Comptime of int
  2. | Runtime of Ppxlib.expression
type context = {
  1. log_value : log_value;
  2. track_or_explicit : [ `Diagn | `Debug | `Track ];
  3. output_type_info : bool;
  4. interrupts : bool;
  5. log_level : log_level;
  6. entry_log_level : log_level;
  7. hidden : bool;
  8. toplevel_opt_arg : toplevel_opt_arg;
}
val init_context : context Stdlib.ref
val parse_log_level : Ppxlib.expression -> log_level
val last_ident : Ppxlib.longident -> string
val typ2str : Ppxlib.core_type -> string
val pat2descr : default:Stdlib.String.t -> Ppxlib.pattern -> Stdlib.String.t Ppxlib.loc
val pat2expr : Ppxlib.pattern -> Ppxlib_ast.Ast.expression
val lift_track_or_explicit : loc:Ppxlib.location -> [< `Debug | `Diagn | `Track ] -> - Ppxlib_ast.Ast.expression
val open_log : + Ppxlib_ast.Ast.expression
val ll_to_expr : + digit_loc:Ppxlib__.Location.t -> + log_level -> + Ppxlib__.Import.expression
val open_log : ?message:string -> loc:Ppxlib__.Import.location -> - log_level:int -> + log_level:log_level -> [< `Debug | `Diagn | `Track ] -> Ppxlib__.Import.expression
val open_log_no_source : message:Ppxlib_ast.Ast.expression -> loc:Ppxlib.location -> - log_level:int -> + log_level:log_level -> [< `Debug | `Diagn | `Track ] -> Ppxlib_ast.Ast.expression
val close_log : loc:Ppxlib.location -> Ppxlib_ast.Ast.expression
val to_descr : context -> @@ -26,7 +27,7 @@ context -> is_explicit:bool -> is_result:'a -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> (unit -> Ppxlib_ast.Ast.expression) -> Ppxlib_ast.Ast.expression
val log_value_sexp : @@ -36,7 +37,7 @@ ?descr_loc:string Ppxlib.loc -> is_explicit:bool -> is_result:bool -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> Ppxlib_ast.Ast.expression
val splice_lident : id_prefix:Stdlib.String.t -> @@ -48,7 +49,7 @@ ?descr_loc:string Ppxlib.loc -> is_explicit:bool -> is_result:bool -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> Ppxlib_ast.Ast.expression
val log_value_show : context -> @@ -57,7 +58,7 @@ ?descr_loc:string Ppxlib.loc -> is_explicit:bool -> is_result:bool -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> Ppxlib_ast.Ast.expression
val log_value : context -> @@ -66,22 +67,22 @@ ?descr_loc:string Ppxlib.loc -> is_explicit:bool -> is_result:bool -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> Ppxlib_ast.Ast.expression
val log_value_printbox : context -> loc:Ppxlib.location -> - log_level:int -> + log_level:log_level -> Ppxlib.expression -> Ppxlib_ast.Ast.expression
val log_string : loc:Ppxlib__.Import.location -> descr_loc:string Ppxlib.loc -> - log_level:int -> + log_level:log_level -> string -> Ppxlib__.Import.expression
val log_string_with_descr : loc:Ppxlib.location -> message:Ppxlib_ast.Ast.expression -> - log_level:int -> + log_level:log_level -> string -> Ppxlib_ast.Ast.expression
type fun_arg =
  1. | Pexp_fun_arg of Ppxlib.arg_label * Ppxlib.expression option @@ -131,7 +132,7 @@ ?always:bool -> toplevel_opt_arg -> Ppxlib.expression -> - Ppxlib_ast.Ast.expression
val unpack_runtime : toplevel_opt_arg -> Ppxlib.expression -> Ppxlib.expression
val has_runtime_arg : context -> bool
val loc_to_name : Ppxlib.location -> string
val debug_fun : + Ppxlib_ast.Ast.expression
val unpack_runtime : toplevel_opt_arg -> Ppxlib.expression -> Ppxlib.expression
val has_runtime_arg : context -> bool
val loc_to_name : Ppxlib.location -> string
val is_comptime_nothing : context -> bool
val debug_fun : context -> (context -> Ppxlib_ast.Ast.expression -> Ppxlib_ast.Ast.expression) -> ?typ:Ppxlib.core_type -> @@ -165,7 +166,11 @@ ?default:Ppxlib_ast.Ast.core_type -> alt_typ:Ppxlib.core_type option -> Ppxlib.expression -> - Ppxlib__.Import.core_type
type rule = {
  1. ext_point : string;
  2. track_or_explicit : [ `Diagn | `Debug | `Track ];
  3. toplevel_opt_arg : toplevel_opt_arg;
  4. expander : [ `Debug | `Str ];
  5. log_value : log_value;
  6. entry_log_level : int option;
}
val entry_rules : (string, rule) Stdlib.Hashtbl.t
val with_opt_digit : prefix:string -> suffix:string -> string -> bool
val get_opt_digit : prefix:string -> suffix:string -> string -> int option
val traverse_expression : context Ppxlib.Ast_traverse.map_with_context
val debug_expander : context -> Ppxlib.expression -> Ppxlib.expression
val str_expander : + Ppxlib__.Import.core_type
type rule = {
  1. ext_point : string;
  2. track_or_explicit : [ `Diagn | `Debug | `Track ];
  3. toplevel_opt_arg : toplevel_opt_arg;
  4. expander : [ `Debug | `Str ];
  5. log_value : log_value;
  6. entry_log_level : log_level option;
}
val entry_rules : (string, rule) Stdlib.Hashtbl.t
val with_opt_digit : prefix:string -> suffix:string -> string -> bool
val get_opt_digit : + prefix:string -> + suffix:string -> + string -> + log_level option
val traverse_expression : context Ppxlib.Ast_traverse.map_with_context
val debug_expander : context -> Ppxlib.expression -> Ppxlib.expression
val str_expander : context -> loc:Ppxlib.location -> Ppxlib.structure_item list -> diff --git a/docs/_html/ppx_minidebug/index.html b/docs/_html/ppx_minidebug/index.html index 19e043b..11fd668 100644 --- a/docs/_html/ppx_minidebug/index.html +++ b/docs/_html/ppx_minidebug/index.html @@ -1,6 +1,7 @@ -index (ppx_minidebug.index)

ppx_minidebug

<!-- TOC -->

<!-- /TOC -->

ppx_minidebug: Debug logs for selected functions and let-bindings

ppx_minidebug traces let bindings and functions within a selected scope if they have type annotations. ppx_minidebug offers three ways of instrumenting the code: %debug_pp and %debug_show (also %track_pp, %diagn_pp and %track_show, %diagn_show), based on deriving.show, and %debug_sexp (also %track_sexp, %diagn_sexp) based on sexplib0 and ppx_sexp_conv. Explicit logs can be added with %log within a debug scope (%log is not a registered extension point to avoid conflicts with other logging frameworks). The syntax extension expects a module Debug_runtime in the scope. The ppx_minidebug.runtime library (part of the ppx_minidebug package) offers multiple ways of logging the traces, as helper functions generating Debug_runtime modules. See the generated documentation for Minidebug_runtime.

Take a look at ppx_debug which has complementary strengths!

Try opam install ppx_minidebug to install from the opam repository. To install the development version of ppx_minidebug, download it with e.g. gh repo clone lukstafi/ppx_minidebug; cd ppx_minidebug and then dune build; opam install ..

To use ppx_minidebug in a Dune project, add/modify these stanzas: (preprocess (pps ... ppx_minidebug)), and (libraries ... ppx_minidebug.runtime).

Here we define a Debug_runtime either using the entrypoint module Debug_runtime = (val Minidebug_runtime.debug ()), or using the PrintBox functor, e.g.:

module Debug_runtime =
-  Minidebug_runtime.PrintBox((val Minidebug_runtime.shared_config "path/to/debugger_printbox.log" end))

The logged traces will be pretty-printed as trees using the printbox package. Truncated example (using %debug_sexp):

BEGIN DEBUG SESSION
+index (ppx_minidebug.index)

ppx_minidebug

<!-- TOC -->

<!-- /TOC -->

ppx_minidebug: Debug logs for selected functions and let-bindings

ppx_minidebug traces let bindings and functions within a selected scope if they have type annotations. ppx_minidebug offers three ways of instrumenting the code: %debug_pp and %debug_show (also %track_pp, %diagn_pp and %track_show, %diagn_show), based on deriving.show, and %debug_sexp (also %track_sexp, %diagn_sexp) based on sexplib0 and ppx_sexp_conv. Explicit logs can be added with %log within a debug scope (%log is not a registered extension point to avoid conflicts with other logging frameworks). The syntax extension expects a module Debug_runtime in the scope. The ppx_minidebug.runtime library (part of the ppx_minidebug package) offers multiple ways of logging the traces, as helper functions generating Debug_runtime modules. See the generated documentation for Minidebug_runtime.

Take a look at ppx_debug which has complementary strengths!

Try opam install ppx_minidebug to install from the opam repository. To install the development version of ppx_minidebug, download it with e.g. gh repo clone lukstafi/ppx_minidebug; cd ppx_minidebug and then dune build; opam install ..

To use ppx_minidebug in a Dune project, add/modify these stanzas: (preprocess (pps ... ppx_minidebug)), and (libraries ... ppx_minidebug.runtime).

Here we define a Debug_runtime either using the entrypoint module Debug_runtime = (val Minidebug_runtime.debug ()), or using the PrintBox functor, e.g.:

module Debug_runtime =
+  Minidebug_runtime.PrintBox((val Minidebug_runtime.shared_config "path/to/debugger_printbox.log" end))
+let () = Debug_runtime.config.values_first_mode <- false

The logged traces will be pretty-printed as trees using the printbox package. Truncated example (using %debug_sexp):

BEGIN DEBUG SESSION
 "test/test_debug_sexp.ml":7:19-9:17: foo
 ├─x = 7
 ├─"test/test_debug_sexp.ml":8:6: y
@@ -45,9 +46,12 @@
 │ │ │ │ │ │ │ ├─x = ((first 23) (second 2))
 │ │ │ │ │ │ │ └─loop = 25

Traces in HTML or Markdown as collapsible trees

The PrintBox runtime can be configured to output logs using HTML or Markdown. The logs then become collapsible trees, so that you can expose only the relevant information when debugging. Example configuration:

module Debug_runtime =
   Minidebug_runtime.PrintBox ((val Minidebug_runtime.shared_config "debug.html"))
-let () = Debug_runtime.(html_config := `Html default_html_config)
-let () = Debug_runtime.boxify_sexp_from_size := 50

Here we also convert the logged sexp values (with at least 50 atoms) to trees. Example result: PrintBox runtime with collapsible/foldable trees

Highlighting search terms

The PrintBox runtime also supports highlighting paths to logs that match a highlight_terms regular expression. For example: PrintBox runtime with collapsible/foldable trees

To limit the highlight noise, some log entries can be excluded from propagating the highlight status using the exclude_on_path setting. To trim excessive logging while still providing some context, you can set prune_upto to a level greater than 0, which only outputs highlighted boxes below that level.

PrintBox creating helpers with defaults: debug and debug_file

The configuration for the above example is more concisely just:

module Debug_runtime = (val Minidebug_runtime.debug_file ~highlight_terms:(Re.str "169") "debug")

Similarly, debug returns a PrintBox module, which by default logs to stdout:

module Debug_runtime = (val Minidebug_runtime.debug ())

The HTML and Markdown outputs support emitting file locations as hyperlinks. For example:

module Debug_runtime = (val Minidebug_runtime.debug_file ~hyperlink:"" "debug")

where ~hyperlink is the prefix to let you tune the file path and select a browsing option. For illustration, the prefixes for Markdown / HTML outputs I might use at the time of writing:

  • ~hyperlink:"./" or ~hyperlink:"../" depending on the relative locations of the log file and the binary
  • ~hyperlink:"vscode://file//wsl.localhost/Ubuntu/home/lukstafi/ppx_minidebug/"

    • if left-clicking a link from within VS Code Live Preview follows the file in the HTML preview window rather than an editor window, middle-click the link
  • ~hyperlink:"https://github.com/lukstafi/ppx_minidebug/tree/main/"

Recommended: values_first_mode

This setting puts the result of the computation as the header of a computation subtree, rather than the source code location of the computation. I recommend using this setting as it reduces noise and makes the important information easier to find and visible with less unfolding. Another important benefit is that it makes hyperlinks usable, by pushing them from the summary line to under the fold. I decided to not make it the default setting, because it is not available in the Flushing runtime, and can be confusing.

For example:

module Debug_runtime =
-  (val Minidebug_runtime.debug ~highlight_terms:(Re.str "3") ~values_first_mode:true ())
+let () =
+  let c = Debug_runtime.config in
+  c.backend <- `Html Minidebug_runtime.default_html_config;
+  c.boxify_sexp_from_size <- 50;
+  c.values_first_mode <- false

Here we also convert the logged sexp values (with at least 50 atoms) to trees. Example result: PrintBox runtime with collapsible/foldable trees

Highlighting search terms

The PrintBox runtime also supports highlighting paths to logs that match a highlight_terms regular expression. For example: PrintBox runtime with collapsible/foldable trees

To limit the highlight noise, some log entries can be excluded from propagating the highlight status using the exclude_on_path setting. To trim excessive logging while still providing some context, you can set prune_upto to a level greater than 0, which only outputs highlighted boxes below that level.

PrintBox creating helpers with defaults: debug and debug_file

The configuration for the above example is more concisely just:

module Debug_runtime = (val Minidebug_runtime.debug_file ~highlight_terms:(Re.str "169") "debug")

Similarly, debug returns a PrintBox module, which by default logs to stdout:

module Debug_runtime = (val Minidebug_runtime.debug ())

The HTML and Markdown outputs support emitting file locations as hyperlinks. For example:

module Debug_runtime = (val Minidebug_runtime.debug_file ~hyperlink:"" "debug")

where ~hyperlink is the prefix to let you tune the file path and select a browsing option. For illustration, the prefixes for Markdown / HTML outputs I might use at the time of writing:

  • ~hyperlink:"./" or ~hyperlink:"../" depending on the relative locations of the log file and the binary
  • ~hyperlink:"vscode://file//wsl.localhost/Ubuntu/home/lukstafi/ppx_minidebug/"

    • if left-clicking a link from within VS Code Live Preview follows the file in the HTML preview window rather than an editor window, middle-click the link
  • ~hyperlink:"https://github.com/lukstafi/ppx_minidebug/tree/main/"

values_first_mode

This setting, by default true, puts the result of the computation as the header of a computation subtree, rather than the source code location of the computation. I recommend using this setting as it reduces noise and makes the important information easier to find and visible with less unfolding. Another important benefit is that it makes hyperlinks usable, by pushing them from the summary line to under the fold. It is the default setting, but can be disabled by passing ~values_first_mode:false to runtime builders, because it can be confusing: the logs are no longer ordered by computation time. It is not available in the Flushing runtime.

For example:

module Debug_runtime =
+  (val Minidebug_runtime.debug ~highlight_terms:(Re.str "3") ())
 let%debug_show rec loop_highlight (x : int) : int =
   let z : int = (x - 1) / 2 in
   if x <= 0 then 0 else z + loop_highlight (z + (x / 2))
@@ -71,7 +75,7 @@
   └─┬──────────────────┐
     │loop_highlight = 4│
     ├──────────────────┘
-    ├─"test/test_expect_test.ml":1042:41-1044:58

When logging uses sexps and boxification, and the result is decomposed into a subtree, only the header of the result subtree is put in the header line, and the rest of the result subtree is just underneath it with a <returns> or a <values> header. Example showcasing the printbox-html backend: PrintBox HTML backend -- follow hyperlink

Example showcasing the printbox-md (Markdown) backend: PrintBox Markdown backend -- follow hyperlink

Usage

Tracing only happens in explicitly marked lexical scopes. For extension points applied directly to bindings (let-definitions) only the let definition in scope for logging, the body of the definition(s) is considered outside the extension point. (But if the extension is over an expression with a nested let-binding, the body of the definition is in the scope of the extension.)

The entry extension points vary along three axes:

  • %debug_ vs. %track_ vs. %diagn_

    • The prefix %debug_ means logging fewer things: only let-bound values and functions are logged, and functions only when either: directly in a %debug_-annotated let binding, or their return type is annotated.
    • %track_ also logs: which if, match, function branch is taken, for and while loops, and all functions, including anonymous ones.
    • The prefix %diagn_ means only generating logs for explicitly logged values, i.e. introduced by [%log_entry], [%log ...], [%log_result ...] and [%log_printbox ...] statements.
  • Optional infixes _rt_ and _l_:

    • _rt_ adds a first-class module argument to a function, and unpacks it as module Debug_runtime for the scope of the function.
    • _l_ calls _get_local_debug_runtime, and unpacks it for the scope of the function: let module Debug_runtime = (val _get_local_debug_runtime ()) in ....
    • This functionality is "one use only": it applies only to the function the extension point is attached to.
  • Representation and printing mechanism: _pp, _show, recommended: _sexp

    • _pp is currently most restrictive as it requires the type of a value to be an identifier. The identifier is converted to a pp_ printing function, e.g. pp_int.
    • _show converts values to strings via the %show extension provided by deriving.show: e.g. [%show: int list].
    • _sexp converts values to sexp expressions first using %sexp_of, e.g. [%sexp_of: int list]. The runtime can decide how to print the sexp expressions. The PrintBox backend allows to convert the sexps to box structures first, with the boxify_sexp_from_size setting. This means large values can be unfolded gradually for inspection.

Plus, there are non-entry extension points %log, %log_printbox and %log_result for logging values. They are not registered, which as a side effect should somewhat mitigate conflicts with other ppx extensions for logging. There's also an un-registered extension point %log_entry for opening a log subtree.

See examples in the test directory, and especially the inline tests.

Only type-annotated let-bindings, function arguments, function results can be implicitly logged. However, the bindings and function arguments can be nested patterns with only parts of them type-annotated! The explicit loggers %log and %log_result take a value and reconstruct its type from partial type annotations (deconstructing the expression), sometimes assuming unknown types are strings. The %log_printbox logger takes a PrintBox.t value. The %log_entry logger takes a string value for the header of the log subtree.

To properly trace in concurrent settings, ensure that different threads use different log channels. For example, you can bind Debug_runtime locally: let module Debug_runtime = Minidebug_runtime.debug_file thread_name in ...

ppx_minidebug can be installed using opam. ppx_minidebug.runtime depends on printbox, ptime, mtime, sexplib0.

Breaking infinite recursion with max_nesting_depth and looping with max_num_children; Flushing-based traces

The PrintBox backend only produces any output when a top-level log entry gets closed. This makes it harder to debug infinite loops and especially infinite recursion. The setting max_nesting_depth terminates a computation when the given log nesting is exceeded. For example:

module Debug_runtime = (val Minidebug_runtime.debug ())
+    ├─"test/test_expect_test.ml":1042:41-1044:58

When logging uses sexps and boxification, and the result is decomposed into a subtree, only the header of the result subtree is put in the header line, and the rest of the result subtree is just underneath it with a <returns> or a <values> header. Example showcasing the printbox-html backend: PrintBox HTML backend -- follow hyperlink

Example showcasing the printbox-md (Markdown) backend: PrintBox Markdown backend -- follow hyperlink

Usage

Tracing only happens in explicitly marked lexical scopes. For extension points applied directly to bindings (let-definitions) only the let definition in scope for logging, the body of the definition(s) is considered outside the extension point. (But if the extension is over an expression with a nested let-binding, the body of the definition is in the scope of the extension.)

The entry extension points vary along three axes:

  • %debug_ vs. %track_ vs. %diagn_

    • The prefix %debug_ means logging fewer things: only let-bound values and functions are logged, and functions only when either: directly in a %debug_-annotated let binding, or their return type is annotated.
    • %track_ also logs: which if, match, function branch is taken, for and while loops, and all functions, including anonymous ones.
    • The prefix %diagn_ means only generating logs for explicitly logged values, i.e. introduced by [%log ...], [%log_result ...], [%log_printbox ...] statements.
  • Optional infixes _rt_ and _l_:

    • _rt_ adds a first-class module argument to a function, and unpacks it as module Debug_runtime for the scope of the function.
    • _l_ calls _get_local_debug_runtime, and unpacks it for the scope of the function: let module Debug_runtime = (val _get_local_debug_runtime ()) in ....
    • This functionality is "one use only": it applies only to the function the extension point is attached to.
  • Representation and printing mechanism: _pp, _show, recommended: _sexp

    • _pp is currently most restrictive as it requires the type of a value to be an identifier. The identifier is converted to a pp_ printing function, e.g. pp_int.
    • _show converts values to strings via the %show extension provided by deriving.show: e.g. [%show: int list].
    • _sexp converts values to sexp expressions first using %sexp_of, e.g. [%sexp_of: int list]. The runtime can decide how to print the sexp expressions. The PrintBox backend allows to convert the sexps to box structures first, with the boxify_sexp_from_size setting. This means large values can be unfolded gradually for inspection.

Plus, there are non-entry extension points %log, %log_printbox and %log_result for logging values. They are not registered, which as a side effect should somewhat mitigate conflicts with other ppx extensions for logging. There's also an un-registered extension points %log_entry and %log_block for opening a log subtree; %log_entry is for arbitrary computations whereas %log_block's body is for logging purposes only.

See examples in the test directory, and especially the inline tests.

Only type-annotated let-bindings, function arguments, function results can be implicitly logged. However, the bindings and function arguments can be nested patterns with only parts of them type-annotated! The explicit loggers %log and %log_result take a value and reconstruct its type from partial type annotations (deconstructing the expression), sometimes assuming unknown types are strings. The %log_printbox logger takes a PrintBox.t value. The %log_entry and %log_block loggers takes a string value for the header of the log subtree.

To properly trace in concurrent settings, ensure that different threads use different log channels. For example, you can bind Debug_runtime locally: let module Debug_runtime = Minidebug_runtime.debug_file thread_name in ... Extension points with the _l_ or _rt_ infixes are a great help for that, e.g. %debug_l_sexp; see: Dealing with concurrent execution.

ppx_minidebug can be installed using opam. ppx_minidebug.runtime depends on printbox, ptime, mtime, sexplib0.

Breaking infinite recursion with max_nesting_depth and looping with max_num_children; Flushing-based traces

The PrintBox backend only produces any output when a top-level log entry gets closed. This makes it harder to debug infinite loops and especially infinite recursion. The setting max_nesting_depth terminates a computation when the given log nesting is exceeded. For example:

module Debug_runtime = (val Minidebug_runtime.debug ())
 
 let%debug_show rec loop_exceeded (x : int) : int =
   [%debug_interrupts
@@ -89,7 +93,7 @@
     for i = 0 to 100 do
       let _baz : int = i * 2 in
       ()
-    done]

The %debug_interrupts extension point emits the interrupt checks in a lexically delimited scope. For convenience, we offer the extension point %global_debug_interrupts which triggers emitting the interrupt checks in the remainder of the source preprocessed in the same process (its scope is therefore less well defined). For example:

module Debug_runtime = (val Minidebug_runtime.debug ())
+    done]

The %debug_interrupts extension point emits the interrupt checks in a lexically delimited scope. For convenience, we offer the extension point %global_debug_interrupts which triggers emitting the interrupt checks in the remainder of the source preprocessed in the same process (its scope is therefore less well defined). For example:

module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:false ())
 
 [%%global_debug_interrupts { max_nesting_depth = 5; max_num_children = 10 }]
 
@@ -153,7 +157,7 @@
 
 let () =
   print_endline @@ Int.to_string @@ track_branches 8;
-  print_endline @@ Int.to_string @@ track_branches 3

gives:

BEGIN DEBUG SESSION
+  print_endline @@ Int.to_string @@ track_branches 3

gives (assuming ~values_first_mode:false):

BEGIN DEBUG SESSION
 "test/test_expect_test.ml":415:37-429:16: track_branches
 ├─x = 8
 ├─"test/test_expect_test.ml":424:6: <if -- else branch>
@@ -183,7 +187,7 @@
 │ └─i = 2
 └─"test/test_expect_test.ml":517:50-517:70: __fun
   └─i = 3
-6

To disable, rather than enhance, debugging for a piece of code, you can use the %diagn_ extension points.

Explicit logging statements also help with tracking the execution, since they can be placed anywhere within a debug scope. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ()) in
+6

To disable, rather than enhance, debugging for a piece of code, you can use the %diagn_ extension points.

Explicit logging statements also help with tracking the execution, since they can be placed anywhere within a debug scope. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:false ()) in
 let%track_sexp result =
   let i = ref 0 in
   let j = ref 0 in
@@ -226,7 +230,7 @@
     ├─(2 i= 6)
     └─(3 j= 21)
   21
-      |}]

Using as a logging framework

ppx_minidebug can be used as a logging framework: its annotations can be stored permamently with the source code, rather than shyly added for a brief period of debugging. To allow this, there needs to be a mechanism of logging levels -- otherwise the system is slowed down too much, or even if performance is not an issue, the user is overwhelmed with the amount of logs. ppx_minidebug addresses these issues in a flexible way, by offering restriction of log levels both at compile time and at runtime.

The %diagn_ extension points (short for "diagnostic") are tailored for the "logging framework" use-case. Within the scope of a %diagn_ extension point, only explicit logs are generated. Therefore, one can freely add type annotations without generating debug logs. As a side-effect, %diagn_ annotations can be used to disable debugging for pieces of code where we need type annotations for code reasons, but do not have serialization/printing functions for the types.

In the PrintBox backend, logs accumulate until the current toplevel log scope is closed. This is unfortunate in the logging framework context, where promptly informing the user using the logs might be important. To remedy this, PrintBox_runtime offers the setting snapshot_every_sec. When set, if sufficient time has passed since the last output, the backend will output the whole current toplevel log scope. If possible, the previous snapshot of the same log scope is erased, to not duplicate information. The underlying mechanism is available as [snapshot] in the generic interface; it does nothing in the flushing backend. [snapshot] is useful when there's a risk of a "premature" exit of the debugged program or thread.

The log levels are integers intended to be within the range 0-9, where 0 means no logging at all. They can be provided explicitly by all extension entry points and all explicit logging extensions. When omitted, the log level of the enclosing log entry is used; the default for a top-level log entry is log level 1.

The %diagn_ extension points further restrict logging to explicit logs only. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:true ()) in
+      |}]

Using as a logging framework

ppx_minidebug can be used as a logging framework: its annotations can be stored permamently with the source code, rather than shyly added for a brief period of debugging. To allow this, there needs to be a mechanism of logging levels -- otherwise the system is slowed down too much, or even if performance is not an issue, the user is overwhelmed with the amount of logs. ppx_minidebug addresses these issues in a flexible way, by offering restriction of log levels both at compile time and at runtime.

The %diagn_ extension points (short for "diagnostic") are tailored for the "logging framework" use-case. Within the scope of a %diagn_ extension point, only explicit logs are generated. Therefore, one can freely add type annotations without generating debug logs. As a side-effect, %diagn_ annotations can be used to disable debugging for pieces of code where we need type annotations for code reasons, but do not have serialization/printing functions for the types.

In the PrintBox backend, logs accumulate until the current toplevel log scope is closed. This is unfortunate in the logging framework context, where promptly informing the user using the logs might be important. To remedy this, PrintBox_runtime offers the setting snapshot_every_sec. When set, if sufficient time has passed since the last output, the backend will output the whole current toplevel log scope. If possible, the previous snapshot of the same log scope is erased, to not duplicate information. The underlying mechanism is available as [snapshot] in the generic interface; it does nothing in the flushing backend. [snapshot] is useful when there's a risk of a "premature" exit of the debugged program or thread.

The log levels are integers intended to be within the range 0-9, where 0 means no logging at all. They can be provided explicitly by all extension entry points and all explicit logging extensions. When omitted, the log level of the enclosing log entry is used; the default for a top-level log entry is log level 1. The syntax for logging at a compile-time given level is by example: %debug2_sexp (log at level 2), %log3 (log at level 3), %log1_resut (log result at level 1), %diagn3_sexp (log at level 3) etc.

The %diagn_ extension points further restrict logging to explicit logs only. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ()) in
 let%diagn_show bar { first : int; second : int } : int =
   let { first : int = a; second : int = b } = { first; second = second + 3 } in
   let y : int = a + 1 in
@@ -234,7 +238,7 @@
   (b - 3) * y
 in
 let () = print_endline @@ Int.to_string @@ bar { first = 7; second = 42 } in
-let baz { first : int; second : int } : int =
+let%diagn_show baz { first : int; second : int } : int =
   let { first : int; second : int } = { first = first + 1; second = second + 3 } in
   [%log "for baz, f squared", (first * first : int)];
   (first * first) + second
@@ -268,7 +272,7 @@
 @@ Int.to_string
      (result
         Minidebug_runtime.(
-          forget_printbox @@ debug ~values_first_mode:true ~log_level:2 ~global_prefix:"Warning" ())
+          forget_printbox @@ debug ~log_level:2 ~global_prefix:"Warning" ())
         ());
 ...

At compile time, the level can be set for a scope with %log_level, or globally with %global_debug_log_level. (%log_level is not registered to minimize incompatibility with other logging frameworks.) For example:

[%%global_log_level 2]
 
@@ -285,8 +289,8 @@
   done;
   !j
 
-let () = print_endline @@ Int.to_string @@ warning ()

This will not emit logging code that is above the stated log level. Note that the compile-time pruning of logging happens independently of the runtime log level! This gives more flexibility but can lead to confusing situations.

There's also a way to compile the code adaptively, using a shell environment variable: [%%global_debug_log_level_from_env_var "environment_variable_name"]. The variable name is case-sensitive, but the values should be integers.

The generated code will check that the compile-time adaptive pruning matches the runtime value of the environment variable. If that's an obstacle, use %%global_debug_log_level_from_env_var_unsafe which will not perform the check. Using %%global_debug_log_level_from_env_var_unsafe is very prone to workflow bugs where different parts of a codebase are compiled with different log levels, leading to confusing behavior.

Another example from the test suite:

let module Debug_runtime =
-  (val Minidebug_runtime.debug ~values_first_mode:true ~log_level:2 ())
+let () = print_endline @@ Int.to_string @@ warning ()

This will not emit logging code that is above the stated log level. Note that the compile-time pruning of logging happens independently of the runtime log level! This gives more flexibility but can lead to confusing situations.

There's also a way to compile the code adaptively, using a shell environment variable: [%%global_debug_log_level_from_env_var "environment_variable_name"]. The variable name is case-sensitive, but the values should be integers.

The generated code will check that the compile-time adaptive pruning matches the runtime value of the environment variable. If that's an obstacle, use %%global_debug_log_level_from_env_var_unsafe which will not perform the check. Using %%global_debug_log_level_from_env_var_unsafe is very prone to workflow bugs where different parts of a codebase are compiled with different log levels, leading to confusing behavior.

Another example from the test suite, notice how the log level of %log1 overrides the parent log level of %debug3_show:

let module Debug_runtime =
+  (val Minidebug_runtime.debug ~log_level:2 ())
 in
 let%debug3_show () =
   let foo { first : int; second : int } : int =
@@ -328,7 +332,7 @@
   │   └─second = 45
   └─("for baz, f squared", 64)
   109
-  |}]

The extension point %log_result lets you benefit from the values_first_mode setting even when using only explicit logs. Conveying more information in headers lets you explore logs more quickly.

The extension point %log_printbox lets you embed a PrintBox.t in the logs directly. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:true ()) in
+  |}]

The extension point %log_result lets you benefit from the values_first_mode setting even when using only explicit logs. Conveying more information in headers lets you explore logs more quickly.

The extension point %log_printbox lets you embed a PrintBox.t in the logs directly. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ()) in
 let%debug_show foo () : unit =
   [%log_printbox
     PrintBox.init_grid ~line:5 ~col:5 (fun ~line ~col ->
@@ -386,47 +390,52 @@
       │3/0│3/1│3/2│3/3│3/4│
       ├───┼───┼───┼───┼───┤
       │4/0│4/1│4/2│4/3│4/4│
-      └───┴───┴───┴───┴───┘ |}

The extension point %log_entry lets you shape arbitrary log tree structures. Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ()) in
+      └───┴───┴───┴───┴───┘ |}

The extension point %log_entry lets you shape arbitrary log tree structures. The similar extension point %log_block ensures that its body doesn't get executed (resp. generated) when the current runtime (resp. compile-time) log level is inadequate. Example:

let module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:false ()) in
 let%diagn_show _logging_logic : unit =
-  let rec loop logs =
-    match logs with
-    | "start" :: header :: tl ->
-        let more =
-          [%log_entry
-            header;
-            loop tl]
-        in
-        loop more
-    | "end" :: tl -> tl
-    | msg :: tl ->
-        [%log msg];
-        loop tl
-    | [] -> []
+  let logify _logs =
+    [%log_block
+      "logs";
+      let rec loop logs =
+        match logs with
+        | "start" :: header :: tl ->
+            let more =
+              [%log_entry
+                header;
+                loop tl]
+            in
+            loop more
+        | "end" :: tl -> tl
+        | msg :: tl ->
+            [%log msg];
+            loop tl
+        | [] -> []
+      in
+      ignore (loop _logs)]
   in
-  ignore
-  @@ loop
-       [
-         "preamble";
-         "start";
-         "header 1";
-         "log 1";
-         "start";
-         "nested header";
-         "log 2";
-         "end";
-         "log 3";
-         "end";
-         "start";
-         "header 2";
-         "log 4";
-         "end";
-         "postscript";
-       ]
+  logify
+    [
+      "preamble";
+      "start";
+      "header 1";
+      "log 1";
+      "start";
+      "nested header";
+      "log 2";
+      "end";
+      "log 3";
+      "end";
+      "start";
+      "header 2";
+      "log 4";
+      "end";
+      "postscript";
+    ]
 in
 [%expect
   {|
-    BEGIN DEBUG SESSION
-    "test/test_expect_test.ml":3507:17: _logging_logic
+  BEGIN DEBUG SESSION
+  "test/test_expect_test.ml":3605:17: _logging_logic
+  └─logs
     ├─"preamble"
     ├─header 1
     │ ├─"log 1"
@@ -436,8 +445,8 @@
     ├─header 2
     │ └─"log 4"
     └─"postscript"
-  |}]

%log_result, %log_printbox, %log_entry also allow log-level specifications (e.g. %log2_result).

Lexical scopes vs. dynamic scopes

We track lexical scoping: every log has access to the entry_id number of the lexical scope it is in. Lexical scopes are computations: bindings, functions, tracked code branches (even if not annotated with an extension point, but always within some ppx_minidebug registered extension point). There is also dynamic scoping: which entry a particular log actually ends up belonging in. We do not expose the (lexical) entry id of an individual log, except when the log "wandered" out of all dynamic scopes, or you passed ~verbose_entry_ids:true when creating a runtime. To be able to locate where such log originates from, pass ~print_entry_ids:true when creating the runtime, and look for the path line with the log's entry id. When the backend is HTML or Markdown, the entry id is a hyperlink to the anchor of the entry. Example from the test suite:

let module Debug_runtime =
-  (val Minidebug_runtime.debug ~print_entry_ids:true ~values_first_mode:true ())
+  |}]

%log_result, %log_printbox, %log_entry, %log_block also allow log-level specifications (e.g. %log2_block).

Specifying the level to log at via a runtime expression

The unregistered extension point [%at_log_level for_log_level; <body>] sets the default log level for logging expressions within <body> to for_log_level, which can be any expression with integer type.

To express the runtime-known levels to log at more concisely, we have extension points %logN, %logN_result, %logN_printbox, %logN_block (but not other extension points), by analogy to compile time levels where instead of the letter N there is a digit 1-9. With the letter N, the extension expressions take an extra argument that is the level to log at. For example, [%logN for_log_level; "message"] will log "message" when at runtime, for_log_level's value is at or below the current log level.

In particular, [%logN_block for_log_level "header"; <body>] is roughly equivalent to:

if !Debug_runtime.log_level >= for_log_level then [%at_log_level for_log_level; [%log_entry "header"; <body>]]

Lexical scopes vs. dynamic scopes

We track lexical scoping: every log has access to the entry_id number of the lexical scope it is in. Lexical scopes are computations: bindings, functions, tracked code branches (even if not annotated with an extension point, but always within some ppx_minidebug registered extension point). There is also dynamic scoping: which entry a particular log actually ends up belonging in. We do not expose the (lexical) entry id of an individual log, except when the log "wandered" out of all dynamic scopes, or you passed ~verbose_entry_ids:true when creating a runtime. To be able to locate where such log originates from, pass ~print_entry_ids:true when creating the runtime, and look for the path line with the log's entry id. When the backend is HTML or Markdown, the entry id is a hyperlink to the anchor of the entry. Example from the test suite:

let module Debug_runtime =
+  (val Minidebug_runtime.debug ~print_entry_ids:true ())
 in
 let i = 3 in
 let pi = 3.14 in
@@ -487,7 +496,7 @@
      the no-debug call [x = 4]. *)
   Debug_runtime.no_debug_if (x <> 6 && x <> 2 && (z + 1) * 2 = x);
   if x <= 0 then 0 else z + fixpoint_changes (z + x / 2) in
-print_endline @@ Int.to_string @@ fixpoint_changes 7

leads to:

"test/test_expect_test.ml":96:43-100:58: fixpoint_changes
+print_endline @@ Int.to_string @@ fixpoint_changes 7

leads to (assuming ~values_first_mode:false):

"test/test_expect_test.ml":96:43-100:58: fixpoint_changes
 ├─x = 7
 ├─"test/test_expect_test.ml":97:8: z
 │ └─z = 3
@@ -502,7 +511,8 @@
 │ │ └─fixpoint_changes = 4
 │ └─fixpoint_changes = 6
 └─fixpoint_changes = 9
-9

The no_debug_if mechanism requires modifying the logged sources, and since it's limited to cutting out subtrees of the logs, it can be tricky to select and preserve the context one wants. The highlighting mechanism with the prune_upto setting avoids these problems. You provide a search term without modifying the debugged sources. You can tune the pruning level to keep the context around the place the search term was found.

Setting the option truncate_children will only log the given number of children at each node, prioritizing the most recent ones. An example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ~truncate_children:10 ()) in
+9

The no_debug_if mechanism requires modifying the logged sources, and since it's limited to cutting out subtrees of the logs, it can be tricky to select and preserve the context one wants. The highlighting mechanism with the prune_upto setting avoids these problems. You provide a search term without modifying the debugged sources. You can tune the pruning level to keep the context around the place the search term was found.

Setting the option truncate_children will only log the given number of children at each node, prioritizing the most recent ones. An example from the test suite:

let module Debug_runtime =
+  (val Minidebug_runtime.debug ~truncate_children:10 ~values_first_mode:false ()) in
 let () =
   let%track_show _bar : unit =
     for i = 0 to 30 do
@@ -540,7 +550,7 @@
   │     └─_baz = 60
   └─_bar = () |}]

If you provide the split_files_after setting, the logging will transition to a new file after the current file exceeds the given number of characters. However, the splits only happen at the "toplevel", to not interrupt laying out the log trees. If required, you can remove logging indicators from your high-level functions, to bring the deeper logic log trees to the toplevel. This matters when you prefer Markdown output over HTML output -- in my experience, Markdown renderers (VS Code Markdown Preview, GitHub Preview) fail for files larger than 2MB, while browsers easily handle HTML files of over 200MB (including via VS Code Live Preview).

There are a few ways ppx_minidebug is helpful with large logs. You can:

  • Use the HTML backend with foldable trees. Unlike with the Markdown backend, HTML browsers can render really large files.
  • Set elapsed_times to see how much time was spent in a subtree even if it is folded.
  • Use ~time_tagged:Elapsed to keep track of when in the course of the program particular entries are computed.
  • Enable the Table of Contents feature by e.g. passing ~with_toc_listing:true; tune toc_entry so that the tables of contents are concise enough to provide an overview.

The table of contents generation is enabled via ~with_toc_listing:true or ~toc_flame_graph:true or both (for file-based runtimes -- via table_of_contents_ch for channel-based runtimes). This will create an additional file (name ending in -toc), mirroring the main logs in a summarized way. Selected log headers are output there preserving the tree structure, and for ~with_toc_listing:true they look the same as in the main file except there is no folding. The headers are hyperlinks pointing to the main log file (or files, if file splitting is enabled). For ~toc_flame_graph:true, the entries are put in boxes, like in the depicted example from the test suite. Presenting two configurations here:

Flame graph with paths, no time tags Flame graph with values first mode, elapsed times

Note: if your flame graph trees run into each other, try setting ~flame_graph_separation:50 or higher.

If you collaborate with someone or take notes, you can pass ~print_entry_ids:true. In HTML and Markdown, this will output links to the anchors of the corresponding log entries. You can share them to point to specific log file locations.

Example demonstrating foldable trees in Markdown:

module Debug_runtime =
   (val Minidebug_runtime.debug_file ~elapsed_times:Microseconds ~hyperlink:"./"
-         ~backend:(`Markdown Minidebug_runtime.default_md_config) ~values_first_mode:true
+         ~backend:(`Markdown Minidebug_runtime.default_md_config)
          ~truncate_children:4 "debugger_sexp_time_spans")
 
 let sexp_of_int i = Sexplib0.Sexp.Atom (string_of_int i)
@@ -554,7 +564,7 @@
            let z : int = i + ((x - 1) / 2) in
            if x <= 0 then i else i + loop (z + (x / 2) - i))
   in
-  print_endline @@ Int.to_string @@ loop 3

Inlined example output, using the Markdown backend for PrintBox. Note that the elapsed time is wallclock time (see mtime) and is due to fluctuate because of e.g. garbage collection or external system events.

BEGIN DEBUG SESSION <details><summary><code>loop = 58435</code> &nbsp; &lt;16850.93μs&gt;</summary>

</details>

Providing the necessary type information

We only implicitly log values of identifiers, located inside patterns, for which the type is provided in the source code, in a syntactically close / related location. PPX rewriters do not have access to the results of type inference. We extract the available type information, but we don't do it perfectly. We propagate type information top-down, merging it, but we do not unify or substitute type variables.

Here is a probably incomplete list of the restrictions:

  • When types for a (sub) pattern are specified in multiple places, they are combined by matching syntactically, the type variable alternatives are discarded. The type that is closer to the (sub) pattern is preferred, even if selecting a corresponding type in another place would be better.
  • When faced with a binding of a form: let pattern = (expression : type_), we make use of type_, but we ignore all types nested inside expression, even if we decompose pattern.

    • For example, let%track_sexp (x, y) = ((5, 3) : int * int) works -- logs both x and y. Also work: let%track_sexp ((x, y) : int * int) = (5, 3) and let%track_sexp ((x : int), (y : int)) = (5, 3). But let%track_sexp (x, y) = ((5 : int), (3 : int)) will not log anything!
  • We ignore record and variant datatypes when processing record and variant constructor cases. That's because there is no generic(*) way to extract the types of the arguments.

    • (*) Although polymorphic variant types can be provided inline, we decided it's not worth the effort supporting them.
    • We do handle tuple types and the builtin array type (they are not records or variants).

      • For example, this works: let%track_sexp { first : int; second : int } = { first = 3; second =7 } -- but compare with the tuple examples above, the alternatives provided above would not work for records.
    • Hard-coded special cases: we do decompose the option type and the list type. For example: let%track_show f : int option -> unit = function None -> () | Some _x -> () in f (Some 3) will log the value of _x.
  • Another example of only propagating types top-down:

    • let%track_show f (l : int option) : int = match l with Some y -> ... will not log y when f is applied (but it will log l).
    • Both let%track_show f : int option -> int = function Some y -> ... and let%track_show f l : int = match (l : int option) with Some y -> ... will log y.
  • We try reconstructing or guessing the types of expressions logged with %log and %log_result, see details below.

As a help in debugging whether the right type information got propagated, we offer the extension %debug_type_info (and %global_debug_type_info). (The display strips module qualifiers from types.) %debug_type_info is not an entry extension point (%global_debug_type_info is). Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ~values_first_mode:true ()) in
+  print_endline @@ Int.to_string @@ loop 3

Inlined example output, using the Markdown backend for PrintBox. Note that the elapsed time is wallclock time (see mtime) and is due to fluctuate because of e.g. garbage collection or external system events.

BEGIN DEBUG SESSION <details><summary><code>loop = 58435</code> &nbsp; &lt;16850.93μs&gt;</summary>

</details>

Providing the necessary type information

We only implicitly log values of identifiers, located inside patterns, for which the type is provided in the source code, in a syntactically close / related location. PPX rewriters do not have access to the results of type inference. We extract the available type information, but we don't do it perfectly. We propagate type information top-down, merging it, but we do not unify or substitute type variables.

Here is a probably incomplete list of the restrictions:

  • When types for a (sub) pattern are specified in multiple places, they are combined by matching syntactically, the type variable alternatives are discarded. The type that is closer to the (sub) pattern is preferred, even if selecting a corresponding type in another place would be better.
  • When faced with a binding of a form: let pattern = (expression : type_), we make use of type_, but we ignore all types nested inside expression, even if we decompose pattern.

    • For example, let%track_sexp (x, y) = ((5, 3) : int * int) works -- logs both x and y. Also work: let%track_sexp ((x, y) : int * int) = (5, 3) and let%track_sexp ((x : int), (y : int)) = (5, 3). But let%track_sexp (x, y) = ((5 : int), (3 : int)) will not log anything!
  • We ignore record and variant datatypes when processing record and variant constructor cases. That's because there is no generic(*) way to extract the types of the arguments.

    • (*) Although polymorphic variant types can be provided inline, we decided it's not worth the effort supporting them.
    • We do handle tuple types and the builtin array type (they are not records or variants).

      • For example, this works: let%track_sexp { first : int; second : int } = { first = 3; second =7 } -- but compare with the tuple examples above, the alternatives provided above would not work for records.
    • Hard-coded special cases: we do decompose the option type and the list type. For example: let%track_show f : int option -> unit = function None -> () | Some _x -> () in f (Some 3) will log the value of _x.
  • Another example of only propagating types top-down:

    • let%track_show f (l : int option) : int = match l with Some y -> ... will not log y when f is applied (but it will log l).
    • Both let%track_show f : int option -> int = function Some y -> ... and let%track_show f l : int = match (l : int option) with Some y -> ... will log y.
  • We try reconstructing or guessing the types of expressions logged with %log and %log_result, see details below.

As a help in debugging whether the right type information got propagated, we offer the extension %debug_type_info (and %global_debug_type_info). (The display strips module qualifiers from types.) %debug_type_info is not an entry extension point (%global_debug_type_info is). Example from the test suite:

let module Debug_runtime = (val Minidebug_runtime.debug ()) in
 [%debug_show
   [%debug_type_info
     let f : 'a. 'a -> int -> int = fun _a b -> b + 1 in
@@ -610,7 +620,7 @@
 let () =
   print_endline @@ Int.to_string
   @@ foo
-       (Minidebug_runtime.debug ~global_prefix:"foo-1" ~values_first_mode:true ())
+       (Minidebug_runtime.debug ~global_prefix:"foo-1" ())
        [ 7 ]
 in
 let%track_rt_show baz : int list -> int = function
@@ -622,13 +632,13 @@
 let () =
   print_endline @@ Int.to_string
   @@ baz
-       Minidebug_runtime.(forget_printbox @@ debug ~global_prefix:"baz-1" ~values_first_mode:true ())
+       Minidebug_runtime.(forget_printbox @@ debug ~global_prefix:"baz-1" ())
        [ 4 ]
 in
 let () =
   print_endline @@ Int.to_string
   @@ baz
-       Minidebug_runtime.(forget_printbox @@ debug ~global_prefix:"baz-2" ~values_first_mode:true ())
+       Minidebug_runtime.(forget_printbox @@ debug ~global_prefix:"baz-2" ())
        [ 4; 5; 6 ]
 in
 [%expect