I’m using the Logs library. I’d like log entries to contain the filename and the line number where it was created. This is a bit tricky though - I could use __FILE__ and __LINE__ but they’re hard to use in abstractions.
In Rust I’d either create a macro, or better I’d use #[track_caller] which makes available std::panic::Location::caller().line() to get the line number of the caller. Are either of these approaches viable in OCaml?
Oh and I forgot to mention. I tend to play with these things with various degrees of success in my ad-hoc testing frameworks, so that when a test fails I can directly jump to the failing test in compilation mode. See for example here and the printing stuff here. Maybe you can try to play with that too.
You could use something like ppx_here, which gives a Lexing.position wherever you write [%here]. But you would have to write [%here] at each call site of a logging function in order to get that callsite’s position.
Note that the __POS__ value/macro also returns a source-code position 4-tuple that is roughly equivalent to Lexing.position, although it has the same problem w.r.t. abstraction as ppx_here and the other macros that have been mentioned.
@dbuenzli and @bcc32’s answers get as far as I think it’s possible to go in stock OCaml without PPX. With PPX – and some familiarity with Ppxlib – this feature is relatively straightforward to implement in ~20 LoC. (And while we’re at it, we can also generate the (fun f -> f ...) CPS wrapper that Logs requires.)
let () =
Logs.(set_reporter (format_reporter ()));
[%log err "Important numbers: %d, %f" 42 3.14];
[%log warn "Lorem ipsum dolor sit amet"]
(* prints: *)
; dune exec ./main.exe
main.exe: [ERROR] [Line 3] Important numbers: 42, 3.140000
main.exe: [WARNING] [Line 4] Lorem ipsum dolor sit amet
This works as follows:
open Ppxlib
(* Input: [%log <log_fn> <fmt_string> <args...>]
Output: Logs.(<log_fn>) (fun f ->
f ("[Line %d] " ^^ fmt_string) __LINE__ <args...>)
*)
let expansion_function ~loc ~path:_ payload =
let open Ast_builder.Default in
let log_fn, fmt_string, args =
match payload.pexp_desc with
| Pexp_apply (fn, (Nolabel, fmt) :: args) -> (fn, fmt, List.map snd args)
| _ ->
Location.raise_errorf ~loc "ppx_logs: invalid payload %a"
Pprintast.expression payload
in
let args =
[%expr "[Line %d] " ^^ [%e fmt_string]] :: [%expr __LINE__] :: args
|> List.map (fun x -> (Nolabel, x))
in
[%expr Logs.([%e log_fn]) (fun f -> [%e pexp_apply ~loc [%expr f] args])]
let extension =
Extension.declare "log" Extension.Context.expression
Ast_pattern.(single_expr_payload __)
expansion_function
let rule = Context_free.Rule.extension extension
let () = Driver.register_transformation ~rules:[ rule ] "ppx_logs"
(Just a proof-of-concept. I expect an implementation that uses tags & handles shadowing and error reporting appropriately would be around twice this much code, so not too bad.)
After sending this, I remembered that Logs recently gained a PPX thanks to @ulrikstrid. This generates the (fun f -> f ...) wrapper but doesn’t attach any other information. Perhaps “add source positions via Logs tags” would be an appropriate feature request over there