Using a PPX Preprocessor ======================== Our calculator is pretty much opaque: we feed it a string, and it displays a result (on an error message), but we have now way to know what the internal expression looks like. In this chapter, we're going to use a `ppx` deriver to generate a `pp_expr` function that can display expressions. ## Prerequisites Install [ppx_deriving](https://github.com/ocaml-ppx/ppx_deriving) by running: ```sh opam install ppx_deriving.5.2.1 ``` ## Create a test Add a new test in `test/calc.t`: ```console $ calc --debug-ast -e '2 * sin (pi / 2)' ``` Run `dune runtest` and see the failure. Run `dune promote` to add the failure to the test file. Our goal in the rest of the chapter is to change the output of this test. ## Use `ppx_deriving.show` Add an `[@@deriving show]` attribute on types in `lib/ast.ml`: ```{code-block} ocaml :emphasize-lines: 5,13 type op = | Add | Mul | Div [@@deriving show] type expr = | Int of int | Float of float | Ident of string | Op of op * expr * expr | Call of string * expr [@@deriving show] ``` This will generate `pp_op`, `show_op`, `pp_expr`, and `show_expr` functions. To do do we need to instruct Dune to use `ppx_deriving.show` as a preprocessor by updating `lib/dune`: ```{code-block} dune :emphasize-lines: 4-5 (library (name calc) (libraries cmdliner) (preprocess (pps ppx_deriving.show)) (foreign_stubs (language c) (names calc_stubs))) ``` ## Add a `--debug-ast` Flag We have a few edits to make to `lib/cli.ml`. First, add a new `cmdliner` flag to parse the command-line and pass it to `repl` and `eval_lb`: ```{code-block} ocaml :emphasize-lines: 6-8,11-12 let term = let open Cmdliner.Term.Syntax in let+ expr_opt = let open Cmdliner.Arg in value & opt (some string) None & info [ "e" ] and+ debug_ast = let open Cmdliner.Arg in value & flag & info [ "debug-ast" ] in match expr_opt with | Some s -> eval_lb ~debug_ast (Lexing.from_string s) | None -> repl ~debug_ast ``` Then, forward it from `repl` to `eval_lb`: ```{code-block} ocaml :emphasize-lines: 1,5 let repl ~debug_ast = while true do Printf.printf ">> %!"; let lb = Lexing.from_channel Stdlib.stdin in eval_lb ~debug_ast lb done ``` Finally, update `eval_lb` to use it: ```{code-block} ocaml :emphasize-lines: 1,4 let eval_lb ~debug_ast lb = try let expr = Parser.main Lexer.token lb in if debug_ast then Format.eprintf "[debug] %a\n" Ast.pp_exp expr; let v = eval expr in Printf.printf "%s\n" (value_to_string v) with Parser.Error -> Printf.printf "parse error near character %d" lb.lex_curr_pos ``` Run the tests again with `dune runtest`. At that point, the output should be correct. Call `dune promote` to update the expected output. ::::{dropdown} Checkpoint :icon: location This is how the project looks like at the end of this chapter. :::{literalinclude} introduction/dune-project :caption: dune-project (unchanged) :language: dune ::: :::{literalinclude} structure/bin/dune :caption: bin/dune (unchanged) :language: dune ::: :::{literalinclude} structure/bin/calc.ml :caption: bin/calc.ml (unchanged) :language: ocaml ::: :::{literalinclude} using-ppx/lib/dune :caption: lib/dune :language: dune ::: :::{literalinclude} using-ppx/lib/ast.ml :caption: lib/ast.ml :language: ocaml ::: :::{literalinclude} interfacing-with-c/lib/calc_stubs.c :caption: lib/calc_stubs.c (unchanged) :language: c ::: :::{literalinclude} using-ppx/lib/cli.ml :caption: lib/cli.ml :language: ocaml ::: :::{literalinclude} interfacing-with-c/lib/lexer.mll :caption: lib/lexer.mll :language: ocaml ::: :::{literalinclude} development-cycle/lib/parser.mly :caption: lib/parser.mly (unchanged) :language: ocaml ::: :::{literalinclude} structure/test/dune :caption: test/dune (unchanged) :language: dune ::: :::{literalinclude} using-ppx/test/calc.t :caption: test/calc.t :language: cram ::: ::::