Small ocaml/js output rehaul (#582)

This commit is contained in:
Louis Gesbert 2024-02-22 12:15:31 +01:00 committed by GitHub
commit c78a6b62c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 259 additions and 175 deletions

View File

@ -137,19 +137,17 @@ To uninstall, use
#### Updating [`@catala-lang/catala-web-assets`](https://www.npmjs.com/package/@catala-lang/catala-web-assets)
The Catala website features assets generated by the Catala compiler. They are
needed to build the website. To produce them, simply run
needed to build the website. To produce them, run
make website-assets DUNE_PROFILE=release
make website-assets.tar
Then, use a helper script to copy them over to the `assets` directory of the
[`catala-web-assets`](https://github.com/CatalaLang/catala-web-assets)
repository.
Then, get the base [`catala-web-assets`](https://github.com/CatalaLang/catala-web-assets)
repository, and untar the generated file within the `assets/` subdirectory:
./generate_website_assets.sh <path-to-catala-web-assets>/assets
tar xf website-assets.tar -C <path-to-catala-web-assets>/assets
> You will need the `groff` executable to generate the HTML versions of the man
pages, as well as the `rsync` executable to transfer files (preferred to `cp`)
because it also works with a remote server.
pages.
## Syntax highlighting

View File

@ -217,15 +217,27 @@ tests/%: .FORCE
# (and therefore the doc target here)
WEBSITE_ASSETS = grammar.html catala.html clerk.html
$(addprefix _build/default/,$(WEBSITE_ASSETS)):
dune build $@
WEBSITE_ASSETS_EXAMPLES = \
tutorial_en/tutorial_en.html \
tutoriel_fr/tutoriel_fr.html \
us_tax_code/us_tax_code.html \
allocations_familiales/allocations_familiales.html \
allocations_familiales/allocations_familiales_schema.json \
aides_logement/aides_logement.html \
aides_logement/aides_logement_schema.json
WEBSITE_ASSETS_ALL = $(WEBSITE_ASSETS) $(addprefix catala-examples.tmp/,$(WEBSITE_ASSETS_EXAMPLES))
website-assets-base: build
dune build $(WEBSITE_ASSETS)
$(call local_tmp_clone,catala-examples) && \
dune build $(addprefix _build/default/,$(WEBSITE_ASSETS_ALL))
website-assets.tar:
# $(MAKE) DUNE_PROFILE=release website-assets-base
tar cf $@ $(foreach file,$(WEBSITE_ASSETS_ALL),-C $(CURDIR)/$(dir _build/default/$(file)) $(notdir $(file)))
#> website-assets : Builds all the assets necessary for the Catala website
website-assets:
$(MAKE) DUNE_PROFILE=release website-assets-base
website-assets: website-assets.tar
##########################################
# Miscellaneous
@ -238,32 +250,35 @@ all: \
runtimes \
plugins
BRANCH=$(shell git branch --show-current 2>/dev/null || echo master)
BRANCH = $(shell git branch --show-current 2>/dev/null || echo master)
# Attempt a clone of the named CatalaLang repo into <name>.tmp, using local git
# objects in ../<name> if available, the branch with the same name as the
# current branch if it exists (master otherwise), and falling back to a local
# clone of ../<name> if the network is not available. The temp dir is removed
# when the shell terminates, so this must be run in the same "Makefile line" as
# its usage.
local_tmp_clone = { \
rm -rf $1.tmp && \
trap "rm -rf $1.tmp" EXIT && \
git clone https://github.com/CatalaLang/$1 \
--depth 1 --reference-if-able ../$1 \
$1.tmp -b $(BRANCH) || \
git clone https://github.com/CatalaLang/$1 \
--depth 1 --reference-if-able ../$1 \
$1.tmp || \
git clone -s ../$1 $1.tmp $(BRANCH) || \
git clone -s ../$1 $1.tmp master; \
}
#> alltest : Runs more extensive tests, including the examples and french-law. Use before push!
alltest: dependencies-python
@export DUNE_PROFILE=check && \
dune build @update-parser-messages @install @runtest && \
$(CLERK_BIN) test tests && \
rm -rf catala-examples.tmp french-law.tmp && \
trap "rm -rf catala-examples.tmp french-law.tmp $$TMP" EXIT && \
{ git clone https://github.com/CatalaLang/catala-examples \
--depth 1 --reference-if-able ../catala-examples \
catala-examples.tmp -b $(BRANCH) || \
git clone https://github.com/CatalaLang/catala-examples \
--depth 1 --reference-if-able ../catala-examples \
catala-examples.tmp || \
git clone -s ../catala-examples catala-examples.tmp $(BRANCH) || \
git clone -s ../catala-examples catala-examples.tmp master; } && \
$(call local_tmp_clone,catala-examples) && \
$(CLERK_BIN) test catala-examples.tmp && \
{ git clone https://github.com/CatalaLang/french-law \
--depth 1 --reference-if-able ../french-law \
french-law.tmp -b $(BRANCH) || \
git clone https://github.com/CatalaLang/french-law \
--depth 1 --reference-if-able ../french-law \
french-law.tmp || \
git clone -s ../french-law french-law.tmp $(BRANCH) || \
git clone -s ../french-law french-law.tmp master; } && \
$(call local_tmp_clone,french-law) && \
make -C french-law.tmp all PY_VENV_DIR=$(ROOT_DIR)/_python_venv
#> clean : Clean build artifacts
@ -288,4 +303,4 @@ help_catala:
.PHONY: inspect clean all english allocations_familiales \
pygments install build_dev build doc format dependencies \
dependencies-ocaml catala.html help parser-messages plugins \
website-assets website-assets-base
website-assets.tar website-assets-base

View File

@ -347,9 +347,7 @@ module Poll = struct
let ocaml_link_flags : string list Lazy.t =
lazy
(let link_libs =
["yojson"; "ppx_yojson_conv_lib"; "zarith"; "dates_calc"]
in
(let link_libs = ["zarith"; "dates_calc"] in
let link_libs_flags =
List.concat_map
(fun lib ->
@ -493,6 +491,10 @@ let[@ocamlformat "disable"] static_base_rules =
]
~description:["<ocaml>"; ""; !output];
Nj.rule "python"
~command:[!catala_exe; "python"; !catala_flags; !input; "-o"; !output]
~description:["<catala>"; "python"; ""; !output];
Nj.rule "out-test"
~command: [
!catala_exe; !test_command; "--plugin-dir="; "-o -"; !catala_flags; !input;
@ -591,21 +593,30 @@ let gen_build_statements
~outputs:[modd m])
item.module_def
in
let ml_file =
let target_file ext =
match item.module_def with
| Some m -> (!Var.builddir / src /../ m) ^ ".ml"
| None -> (!Var.builddir / !Var.src) ^ ".ml"
| Some m -> (!Var.builddir / src /../ m) ^ "." ^ ext
| None -> (!Var.builddir / !Var.src) ^ "." ^ ext
in
let ocaml =
let ml_file = target_file "ml" in
let py_file = target_file "py" in
let ocaml, python =
if item.extrnal then
Nj.build "copy"
~implicit_in:[inc srcv]
~inputs:[src -.- "ml"]
~outputs:[ml_file]
( Nj.build "copy"
~implicit_in:[inc srcv]
~inputs:[src -.- "ml"]
~outputs:[ml_file],
Nj.build "copy"
~implicit_in:[inc srcv]
~inputs:[src -.- "py"]
~outputs:[py_file] )
else
Nj.build "catala-ocaml"
~inputs:[inc srcv]
~implicit_in:[!Var.catala_exe] ~outputs:[ml_file]
( Nj.build "catala-ocaml"
~inputs:[inc srcv]
~implicit_in:[!Var.catala_exe] ~outputs:[ml_file],
Nj.build "python"
~inputs:[inc srcv]
~implicit_in:[!Var.catala_exe] ~outputs:[py_file] )
in
let ocamlopt =
let implicit_out_exts = ["cmi"; "cmx"; "cmt"; "o"] in
@ -774,6 +785,7 @@ let gen_build_statements
Option.to_seq expose_module;
Seq.return ocaml;
Seq.return ocamlopt;
Seq.return python;
List.to_seq tests;
Seq.return interpret;
]

View File

@ -31,8 +31,6 @@ depends: [
"ocaml" {>= "4.14.0"}
"ocamlfind" {!= "1.9.5"}
"ocamlgraph" {>= "1.8.8"}
"yojson" {>= "2.0" }
"ppx_yojson_conv" {>= "0.14.0"}
"re" {>= "1.9.0"}
"sedlex" {>= "2.4"}
"uutf" {>= "1.0.3"}

View File

@ -462,7 +462,7 @@ let format_struct_embedding
format_struct_name struct_name format_to_module_name (`Sname struct_name)
else
Format.fprintf fmt
"@[<hov 2>let embed_%a (x: %a.t) : runtime_value =@ Struct([\"%a\"],@ \
"@[<hov 2>let embed_%a (x: %a.t) : runtime_value =@ Struct(\"%a\",@ \
@[<hov 2>[%a]@])@]@\n\
@\n"
format_struct_name struct_name format_to_module_name (`Sname struct_name)
@ -485,7 +485,7 @@ let format_enum_embedding
else
Format.fprintf fmt
"@[<hv 2>@[<hov 2>let embed_%a@ @[<hov 2>(x:@ %a.t)@]@ : runtime_value \
=@]@ Enum([\"%a\"],@ @[<hov 2>match x with@ %a@])@]@\n\
=@]@ Enum(\"%a\",@ @[<hov 2>match x with@ %a@])@]@\n\
@\n"
format_enum_name enum_name format_to_module_name (`Ename enum_name)
EnumName.format enum_name

View File

@ -994,7 +994,7 @@ let interpret_program_dcalc p s : (Uid.MarkedString.info * ('a, 'm) gexpr) list
built-in interpreter does not have a way to retrieve input \
values from the command line, so it cannot execute this scope. \
Please create another scope that provides the input arguments \
to this one and execute it instead. ")
to this one and execute it instead.")
taus
in
let to_interpret =

View File

@ -1,25 +0,0 @@
#! /usr/bin/env sh
set -eux
cd "$(dirname "$0")"
if [ $# -ne 1 ]; then
echo "USAGE: \$1 DST where DST is the directory in which files have to be copied"
exit 1
fi
BUILD=_build/default
rsync $BUILD/examples/allocations_familiales/allocations_familiales.html $1/
rsync $BUILD/examples/aides_logement/aides_logement.html $1/
rsync $BUILD/examples/us_tax_code/us_tax_code.html $1/
rsync $BUILD/examples/tutorial_en/tutorial_en.html $1/
rsync $BUILD/examples/tutoriel_fr/tutoriel_fr.html $1/
rsync $BUILD/grammar.html $1/
rsync $BUILD/catala.html $1/
rsync $BUILD/clerk.html $1/
rsync $BUILD/examples/allocations_familiales/allocations_familiales_schema.json $1/
rsync $BUILD/examples/aides_logement/aides_logement_schema.json $1/

View File

@ -84,83 +84,75 @@ let event_manager : event_manager Js.t =
method resetLog = Js.wrap_meth_callback R_ocaml.reset_log
method retrieveEvents =
Js.wrap_meth_callback (fun () ->
Js.array
(Array.of_list
(R_ocaml.retrieve_log ()
|> R_ocaml.EventParser.parse_raw_events
|> List.map (fun event ->
object%js
val mutable data =
event
|> R_ocaml.yojson_of_event
|> Yojson.Safe.to_string
|> Js.string
end))))
Js.wrap_meth_callback
@@ fun () ->
R_ocaml.retrieve_log ()
|> R_ocaml.EventParser.parse_raw_events
|> List.map (fun event ->
object%js
val mutable data = event |> R_ocaml.Json.event |> Js.string
end)
|> Array.of_list
|> Js.array
method retrieveRawEvents =
Js.wrap_meth_callback (fun () ->
Js.array
(Array.of_list
(List.map
(fun evt ->
object%js
val mutable eventType =
Js.string
(match evt with
| R_ocaml.BeginCall _ -> "Begin call"
| EndCall _ -> "End call"
| VariableDefinition _ -> "Variable definition"
| DecisionTaken _ -> "Decision taken")
let evt_to_js evt =
(* FIXME: ideally this could be just a Json.parse (R_ocaml.Json.event
foo) ? *)
object%js
val mutable eventType =
(match evt with
| R_ocaml.BeginCall _ -> "Begin call"
| EndCall _ -> "End call"
| VariableDefinition _ -> "Variable definition"
| DecisionTaken _ -> "Decision taken")
|> Js.string
val mutable information =
Js.array
(Array.of_list
(match evt with
| BeginCall info
| EndCall info
| VariableDefinition (info, _, _) ->
List.map Js.string info
| DecisionTaken _ -> []))
val mutable information =
(match evt with
| BeginCall info | EndCall info | VariableDefinition (info, _, _) ->
List.map Js.string info
| DecisionTaken _ -> [])
|> Array.of_list
|> Js.array
val mutable loggedIOJson =
match evt with
| VariableDefinition (_, io, _) ->
io
|> R_ocaml.yojson_of_io_log
|> Yojson.Safe.to_string
|> Js.string
| EndCall _ | BeginCall _ | DecisionTaken _ ->
"unavailable" |> Js.string
val mutable loggedIOJson =
match evt with
| VariableDefinition (_, io, _) ->
io |> R_ocaml.Json.io_log |> Js.string
| EndCall _ | BeginCall _ | DecisionTaken _ ->
"unavailable" |> Js.string
val mutable loggedValueJson =
(match evt with
| VariableDefinition (_, _, v) -> v
| EndCall _ | BeginCall _ | DecisionTaken _ ->
R_ocaml.unembeddable ())
|> R_ocaml.yojson_of_runtime_value
|> Yojson.Safe.to_string
|> Js.string
val mutable loggedValueJson =
(match evt with
| VariableDefinition (_, _, v) -> v
| EndCall _ | BeginCall _ | DecisionTaken _ ->
R_ocaml.unembeddable ())
|> R_ocaml.Json.runtime_value
|> Js.string
val mutable sourcePosition =
match evt with
| DecisionTaken pos ->
Js.def
(object%js
val mutable fileName = Js.string pos.filename
val mutable startLine = pos.start_line
val mutable endLine = pos.end_line
val mutable startColumn = pos.start_column
val mutable endColumn = pos.end_column
val mutable sourcePosition =
match evt with
| DecisionTaken pos ->
Js.def
(object%js
val mutable fileName = Js.string pos.filename
val mutable startLine = pos.start_line
val mutable endLine = pos.end_line
val mutable startColumn = pos.start_column
val mutable endColumn = pos.end_column
val mutable lawHeadings =
Js.array
(Array.of_list
(List.map Js.string pos.law_headings))
end)
| _ -> Js.undefined
end)
(R_ocaml.retrieve_log ()))))
val mutable lawHeadings =
List.map Js.string pos.law_headings
|> Array.of_list
|> Js.array
end)
| _ -> Js.undefined
end
in
Js.wrap_meth_callback
@@ fun () ->
R_ocaml.retrieve_log () |> List.map evt_to_js |> Array.of_list |> Js.array
end
let execute_or_throw_error f =

View File

@ -1,7 +1,5 @@
(library
(name runtime_ocaml)
(public_name catala.runtime_ocaml)
(preprocess
(pps ppx_yojson_conv))
(libraries dates_calc zarith zarith_stubs_js)
(modules runtime))

View File

@ -13,8 +13,6 @@
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License. *)
open Ppx_yojson_conv_lib.Yojson_conv.Primitives
type nonrec unit = unit
type nonrec bool = bool
@ -30,8 +28,8 @@ module Eoption = struct
type 'a t = ENone of unit | ESome of 'a
end
type io_input = NoInput | OnlyInput | Reentrant [@@deriving yojson_of]
type io_log = { io_input : io_input; io_output : bool } [@@deriving yojson_of]
type io_input = NoInput | OnlyInput | Reentrant
type io_log = { io_input : io_input; io_output : bool }
type source_position = {
filename : string;
@ -41,7 +39,6 @@ type source_position = {
end_column : int;
law_headings : string list;
}
[@@deriving yojson_of]
exception EmptyError
exception AssertionFailed of source_position
@ -190,12 +187,6 @@ let duration_to_string (d : duration) : string =
let duration_to_years_months_days (d : duration) : int * int * int =
Dates_calc.Dates.period_to_ymds d
let yojson_of_money (m : money) = `Float (money_to_float m)
let yojson_of_integer (i : integer) = `Int (integer_to_int i)
let yojson_of_decimal (d : decimal) = `Float (decimal_to_float d)
let yojson_of_date (d : date) = `String (date_to_string d)
let yojson_of_duration (d : duration) = `String (duration_to_string d)
type runtime_value =
| Unit
| Bool of bool
@ -204,11 +195,10 @@ type runtime_value =
| Decimal of decimal
| Date of date
| Duration of duration
| Enum of string list * (string * runtime_value)
| Struct of string list * (string * runtime_value) list
| Enum of string * (string * runtime_value)
| Struct of string * (string * runtime_value) list
| Array of runtime_value array
| Unembeddable
[@@deriving yojson_of]
let unembeddable _ = Unembeddable
let embed_unit () = Unit
@ -220,7 +210,7 @@ let embed_date x = Date x
let embed_duration x = Duration x
let embed_array f x = Array (Array.map f x)
type information = string list [@@deriving yojson_of]
type information = string list
type raw_event =
| BeginCall of information
@ -236,7 +226,6 @@ type event =
inputs : var_def list;
body : event list;
}
[@@deriving yojson_of]
and var_def = {
pos : source_position option;
@ -253,6 +242,108 @@ and fun_call = {
output : var_def;
}
module BufferedJson = struct
let rec list f buf = function
| [] -> ()
| [x] -> f buf x
| x :: r ->
f buf x;
Buffer.add_char buf ',';
list f buf r
let quote buf str =
Buffer.add_char buf '"';
String.iter
(function
| ('"' | '\\') as c ->
Buffer.add_char buf '\\';
Buffer.add_char buf c
| '\n' -> Buffer.add_string buf "\\n"
| '\t' -> Buffer.add_string buf "\\t"
| '\r' -> Buffer.add_string buf "\\r"
| '\x00' .. '\x1F' as c -> Printf.bprintf buf "\\u%04x" (int_of_char c)
| c -> Buffer.add_char buf c)
str;
Buffer.add_char buf '"'
(* Note: the output format is made for transition with what Yojson gave us,
but we could change it to something nicer (e.g. objects for structures) *)
let rec runtime_value buf = function
| Unit -> Buffer.add_string buf {|"Unit"|}
| Bool b -> Buffer.add_string buf (string_of_bool b)
| Money m -> Buffer.add_string buf (money_to_string m)
| Integer i -> Buffer.add_string buf (integer_to_string i)
| Decimal d ->
Buffer.add_string buf (decimal_to_string ~max_prec_digits:10 d)
| Date d -> quote buf (date_to_string d)
| Duration d -> quote buf (duration_to_string d)
| Enum (name, (constr, v)) ->
Printf.bprintf buf {|[["%s"],["%s",%a]]|} name constr runtime_value v
| Struct (name, elts) ->
Printf.bprintf buf {|["%s",[%a]]|} name
(list (fun buf (cstr, v) ->
Printf.bprintf buf {|"%s":%a|} cstr runtime_value v))
elts
| Array elts ->
Printf.bprintf buf "[%a]" (list runtime_value) (Array.to_list elts)
| Unembeddable -> Buffer.add_string buf {|"unembeddable"|}
let information buf info = Printf.bprintf buf "[%a]" (list quote) info
let source_position buf pos =
Printf.bprintf buf {|{"filename":%a|} quote pos.filename;
Printf.bprintf buf {|,"start_line":%d|} pos.start_line;
Printf.bprintf buf {|,"start_column":%d|} pos.start_column;
Printf.bprintf buf {|,"end_line":%d|} pos.end_line;
Printf.bprintf buf {|,"end_column":%d|} pos.end_column;
Printf.bprintf buf {|,"law_headings":[%a]}|} (list quote) pos.law_headings
let io_input buf = function
| NoInput -> quote buf "NoInput"
| OnlyInput -> quote buf "OnlyInput"
| Reentrant -> quote buf "Reentrant"
let io_log buf iol =
Printf.bprintf buf {|{"io_input":%a|} io_input iol.io_input;
Printf.bprintf buf {|,"io_output":%b}|} iol.io_output
let rec event buf = function
| VarComputation vd ->
Printf.bprintf buf {|"VarComputation",%a]|} var_def vd
| FunCall fc -> Printf.bprintf buf {|"FunCall",%a]|} fun_call fc
| SubScopeCall { name; inputs; body } ->
Printf.bprintf buf {|{"name":%a,"inputs":[%a],"body":[%a]}|} information
name (list var_def) inputs (list event) body
and var_def buf def =
Option.iter (Printf.bprintf buf {|{"pos":%a|} source_position) def.pos;
Printf.bprintf buf {|,"name":%a|} information def.name;
Printf.bprintf buf {|,"io":%a|} io_log def.io;
Printf.bprintf buf {|,"value":%a|} runtime_value def.value;
Option.iter
(Printf.bprintf buf {|,"fun_calls":[%a]}|} (list fun_call))
def.fun_calls
and fun_call buf fc =
Printf.bprintf buf {|{"fun_name":%a|} information fc.fun_name;
Printf.bprintf buf {|,"fun_inputs":[%a]|} (list var_def) fc.fun_inputs;
Printf.bprintf buf {|,"body":[%a]|} (list event) fc.body;
Printf.bprintf buf {|,"output":%a}|} var_def fc.output
end
module Json = struct
let str f x =
let buf = Buffer.create 800 in
f buf x;
Buffer.contents buf
open BufferedJson
let runtime_value = str runtime_value
let io_log = str io_log
let event = str event
end
let log_ref : raw_event list ref = ref []
let reset_log () = log_ref := []
let retrieve_log () = List.rev !log_ref
@ -313,8 +404,7 @@ let rec pp_events ?(is_first_call = true) ppf events =
| Duration x -> Format.fprintf ppf "%s" (duration_to_string x)
| Enum (_, (name, _)) -> Format.fprintf ppf "%s" name
| Struct (name, attrs) ->
Format.fprintf ppf "@[<hv 2>%s = {@ %a@;<1 -2>}@]"
(String.concat "." name)
Format.fprintf ppf "@[<hv 2>%s = {@ %a@;<1 -2>}@]" name
(Format.pp_print_list
~pp_sep:(fun ppf () -> Format.fprintf ppf ",@,")
(fun fmt (name, value) ->
@ -414,7 +504,7 @@ module EventParser = struct
| VariableDefinition (name, io, value) ->
Printf.sprintf "VariableDefinition([ %s ], %s, %s)"
(String.concat ", " name) (io_log_to_string io)
(yojson_of_runtime_value value |> Yojson.Safe.to_string)
(Json.runtime_value value)
| DecisionTaken pos ->
Printf.sprintf "DecisionTaken(%s:%d.%d-%d.%d)" pos.filename pos.start_line
pos.start_column pos.end_line pos.end_column

View File

@ -57,13 +57,11 @@ type io_input =
| Reentrant
(** For variables defined in the scope that can also be redefined by the
caller as they appear in the input. *)
[@@deriving yojson_of]
type io_log = {
io_input : io_input;
io_output : bool; (** [true] if the variable is an output *)
}
[@@deriving yojson_of]
(** {1 Exceptions} *)
@ -85,11 +83,10 @@ type runtime_value =
| Decimal of decimal
| Date of date
| Duration of duration
| Enum of string list * (string * runtime_value)
| Struct of string list * (string * runtime_value) list
| Enum of string * (string * runtime_value)
| Struct of string * (string * runtime_value) list
| Array of runtime_value Array.t
| Unembeddable
[@@deriving yojson_of]
val unembeddable : 'a -> runtime_value
val embed_unit : unit -> runtime_value
@ -115,7 +112,7 @@ val embed_array : ('a -> runtime_value) -> 'a Array.t -> runtime_value
(** {2 Data structures} *)
type information = string list [@@deriving yojson_of]
type information = string list
(** Represents information about a name in the code -- i.e. variable name,
subscope name, etc...
@ -188,7 +185,6 @@ type event =
inputs : var_def list;
body : event list;
}
[@@deriving yojson_of]
and var_def = {
pos : source_position option;
@ -230,6 +226,16 @@ val log_decision_taken : source_position -> bool -> bool
(** {3 Pretty printers} *)
(** {4 Conversions to JSON} *)
module Json : sig
(* val io_input: io_input -> string *)
val io_log : io_log -> string
val runtime_value : runtime_value -> string
(* val information: information -> string *)
val event : event -> string
end
val pp_events : ?is_first_call:bool -> Format.formatter -> event list -> unit
(** [pp_events ~is_first_call ppf events] pretty prints in [ppf] the string
representation of [events].