open Ppxlib module Kind = struct type t = Signature | Structure | Expression | Pattern | Core_type let to_utils_kind = function | Structure -> Ppxlib_private.Utils.Kind.Impl | Signature -> Ppxlib_private.Utils.Kind.Intf | _ -> assert false end module Ast = struct type t = | Str of structure | Sig of signature | Exp of expression | Pat of pattern | Typ of core_type end let rec repr_to_yojson : Pp_ast.repr -> Yojson.Basic.t = function | Unit -> `Null | Int i -> `Int i | String s -> `String s | Special s -> `String s | Bool b -> `Bool b | Char c -> `String (String.make 1 c) | Float f -> `Float f | Int32 i32 -> `Int (Int32.to_int i32) | Int64 i64 -> `Int (Int64.to_int i64) | Nativeint ni -> `Int (Nativeint.to_int ni) | Array l -> `List (List.map repr_to_yojson l) | Tuple l -> `List (List.map repr_to_yojson l) | List l -> `List (List.map repr_to_yojson l) | Record fields -> `Assoc (List.map (fun (k, v) -> (k, repr_to_yojson v)) fields) | Constr (cname, []) -> `String cname | Constr (cname, [ x ]) -> `Assoc [ (cname, repr_to_yojson x) ] | Constr (cname, l) -> `Assoc [ (cname, `List (List.map repr_to_yojson l)) ] let json_printer fmt value = Yojson.Basic.pretty_print fmt (repr_to_yojson value) module Input = struct type t = Stdin | File of string | Source of string let to_lexbuf t = let all_source = match t with | Stdin -> Stdppx.In_channel.input_all stdin | File fn -> Stdppx.In_channel.(with_file fn ~f:input_all) | Source s -> s in Lexing.from_string all_source let from_string = function | "-" -> Stdin | s when Sys.file_exists s -> File s | s -> Source s let to_driver_fn = function | Stdin -> "-" | File fn -> fn | Source _ -> assert false end let parse_node ~kind ~input_name input = let lexbuf = Input.to_lexbuf input in lexbuf.lex_curr_p <- { lexbuf.lex_curr_p with pos_fname = input_name }; Astlib.Location.set_input_lexbuf (Some lexbuf); match (kind : Kind.t) with | Expression -> Ast.Exp (Parse.expression lexbuf) | Pattern -> Ast.Pat (Parse.pattern lexbuf) | Core_type -> Ast.Typ (Parse.core_type lexbuf) | Signature -> Ast.Str (Parse.implementation lexbuf) | Structure -> Ast.Sig (Parse.interface lexbuf) let load_input ~kind ~input_name input = match ((kind : Kind.t), (input : Input.t)) with | (Structure | Signature), (Stdin | File _) -> ( let kind = Kind.to_utils_kind kind in let fn = Input.to_driver_fn input in match Driver.load_input ~kind ~input_name ~relocate:false fn with | Error (loc_err, _ver) -> Location.Error.raise loc_err | Ok (_ast_input_name, _version, ast) -> ( match (ast : Ppxlib_private.Utils.Intf_or_impl.t) with | Impl str -> Ast.Str str | Intf sig_ -> Ast.Sig sig_)) | (Expression | Pattern | Core_type), _ | _, Source _ -> parse_node ~kind ~input_name input let pp_ast ~config ast = match (ast : Ast.t) with | Str str -> Pp_ast.structure ~config Format.std_formatter str | Sig sig_ -> Pp_ast.signature ~config Format.std_formatter sig_ | Exp exp -> Pp_ast.expression ~config Format.std_formatter exp | Pat pat -> Pp_ast.pattern ~config Format.std_formatter pat | Typ typ -> Pp_ast.core_type ~config Format.std_formatter typ let named f = Cmdliner.Term.(app (const f)) let show_attrs = let doc = "Show atributes in the pretty printed output" in named (fun x -> `Show_attrs x) Cmdliner.Arg.(value & flag & info ~doc [ "show-attrs" ]) let show_locs = let doc = "Show locations in the pretty printed output" in named (fun x -> `Show_locs x) Cmdliner.Arg.(value & flag & info ~doc [ "show-locs" ]) let loc_mode = let full_locs = let doc = "Display locations in long form. Has no effect without --show-locs." in (`Full, Cmdliner.Arg.info ~doc [ "full-locs" ]) in named (fun x -> `Loc_mode x) Cmdliner.Arg.(value & vflag `Short [ full_locs ]) let json = let doc = "Show AST as json" in named (fun x -> `Json x) Cmdliner.Arg.(value & flag & info ~doc [ "json" ]) let kind = let make_vflag (flag, (kind : Kind.t), doc) = (Some kind, Cmdliner.Arg.info ~doc [ flag ]) in let kinds = List.map make_vflag [ ("str", Structure, "Treat the input as a $(b,.ml) file"); ("sig", Signature, "Treat the input as a $(b,.mli) file"); ("exp", Expression, "Treat the input as a single OCaml expression"); ("pat", Pattern, "Treat the input as a single OCaml pattern"); ("typ", Core_type, "Treat the input as a single OCaml core_type"); ] in named (fun x -> `Kind x) Cmdliner.Arg.(value & vflag None kinds) let input = let docv = "INPUT" in let doc = "The $(docv) AST. Can be a binary AST file, a source file or a valid OCaml \ source string. Pass $(b,-) to read from stdin instead." in named (fun x -> `Input x) Cmdliner.Arg.(required & pos 0 (some string) None & info ~doc ~docv []) let errorf fmt = Printf.ksprintf (fun s -> Error s) fmt let run (`Show_attrs show_attrs) (`Show_locs show_locs) (`Loc_mode loc_mode) (`Json json) (`Kind kind) (`Input input) = let open Stdppx.Result in let kind = match kind with | Some k -> Ok k | None -> ( match Ppxlib_private.Utils.Kind.of_filename input with | Some Intf -> Ok Kind.Signature | Some Impl -> Ok Kind.Structure | None -> errorf "Could not guess kind from input %S. Please use relevant CLI \ flag." input) in kind >>= fun kind -> let input = Input.from_string input in let input_name = match input with Stdin -> "" | File fn -> fn | Source _ -> "" in let ast = load_input ~kind ~input_name input in let custom_printer = if json then Some json_printer else None in let config = Pp_ast.Config.make ~show_attrs ~show_locs ~loc_mode ?printer:custom_printer () in pp_ast ~config ast; Format.printf "%!\n"; Ok () let term = Cmdliner.Term.( const run $ show_attrs $ show_locs $ loc_mode $ json $ kind $ input) let tool_name = "ppxlib-pp-ast" let info = let p fmt = Printf.ksprintf (fun s -> `P s) fmt in let open Cmdliner in Cmd.info tool_name ~version:"0.37.0" ~exits:Cmd.Exit.defaults ~doc:"Pretty prints ppxlib's ASTs from OCaml sources" ~man: [ `S "DESCRIPTION"; p "$(b,%s) can be used to pretty print the OCaml AST for a given piece \ of source code." tool_name; `P "It prints the AST version used by ppxlib internally. This means the \ code will be parsed (or the AST unmarshalled) using the installed \ compiler and eventually migrated to ppxlib's expected version."; `P "It can read the source code directly from the command line or read \ it from a file/the standard input. In the latter case you can pass \ regular $(b,.ml) or $(b,.mli) files, marshalled AST files as the \ ones produced by the ppxlib driver or a fragment of OCaml source \ corresponding to an expression, a pattern or a core_type."; `P "When the input is not an $(b,.ml) or $(b,.mli) file you will have \ to explicitly pass the expected AST node using flags such as \ $(b,--str) or $(b,--exp)."; `P "By default the output looks like this:"; p "$(b,\\$ %s --exp \"x + 2\")" tool_name; `Noblank; `Pre (String.concat "\n" [ {|Pexp_apply|}; {| ( Pexp_ident (Lident "+")|}; {| , [ ( Nolabel, Pexp_ident (Lident "x"))|}; {| ; ( Nolabel, Pexp_constant (Pconst_integer ( "2", None)))|}; {| ]|}; {| )|}; ]); `P "If you are already familiar with the OCaml AST you will note that \ it prints a somewhat lighter version to keep the output concise. \ Locations and attributes are not printed. Some specific record \ types, such as $(b,expression) or $(b,pattern), are \"skipped\" to \ avoid too much nesting. This does not hurt comprehension of the \ underlying AST as these records only wrap a variant type to attach \ metadata to it. The tool supports a set of flags you can use to \ force printing of such metadata."; `S "EXAMPLES"; p "$(b,%s test.ml)" tool_name; p "$(b,cat test.ml | %s -)" tool_name; p "$(b,%s test.pp.ml)" tool_name; p "$(b,%s --exp \"x + 2\")" tool_name; p "$(b,%s --typ \"(int, string\\) Result.t\")" tool_name; p "$(b,%s --show-locs --full-locs --pat \"_::tl\")" tool_name; ] let () = let exit_code = Cmdliner.Cmd.eval_result (Cmdliner.Cmd.v info term) in exit exit_code