diff --git a/Makefile b/Makefile index c99e248..31d6097 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,6 @@ create-switch: ## Create opam switch install: $(DUNE) build @install opam install . --deps-only --with-test - cd demo && yarn install .PHONY: init init: create-switch install diff --git a/README.md b/README.md index 58c804b..b30649f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,13 @@ On my ppx learning journey, I'm having some difficulties to find examples and ex So as I said, I'm learning, and I'm not an expert on the subject. If you find any mistake or have any suggestion, please open an issue or a pull request. I'll be glad to receive your feedback. +## Requirements + +This repository assumes that you have the following tools installed on your machine: + +- OPAM: To manage OCaml dependencies and versions. +- Make: To use the Makefile commands for building, cleaning, and running the project. + ## Content - [AST](./examples/1%20-%20AST/README.md) diff --git a/dune-project b/dune-project index 68fb616..ce9c8d6 100644 --- a/dune-project +++ b/dune-project @@ -24,6 +24,7 @@ (depends (ocaml (>= 5.0.0)) + alcotest ocaml-lsp-server ocamlformat ppx_deriving diff --git a/examples/1 - AST/README.md b/examples/1 - AST/README.md index 0334cad..69fd512 100644 --- a/examples/1 - AST/README.md +++ b/examples/1 - AST/README.md @@ -24,7 +24,7 @@ The OCaml Platform officially supports a library for creating these preprocessor - **Source Level**: Preprocessors work directly on the source code. - **AST Level**: Preprocessors manipulate the AST, offering more powerful and flexible transformations. (Covered in this guide) -> **⚠️ Warning** +> [!WARNING] > One of the key challenges with working with the Parsetree (the AST in OCaml) is that its API is not stable. For instance, in the OCaml 4.13 release, significant changes were made to the Parsetree type, which can impact the compatibility of your preprocessing tools. Read more about it in [The Future of PPX](https://discuss.ocaml.org/t/the-future-of-ppx/3766) ### AST Guide @@ -187,7 +187,7 @@ A **structure** refers to the content within a module. It is composed of various The structure represents the body of the module, where all these items are defined and implemented. Since each `.ml` file is implicitly a module, the entire content of a file can be viewed as the structure of that module. -> **:bulb: Tip** +> [!TIP] > Every module in OCaml creates a new structure, and nested modules create nested structures. Consider the following example: @@ -263,7 +263,7 @@ end As you can see, `Bar.ml` and `GameEnum` are modules, and their content is a **structure** that contain a list of **structure items**. -> **📝 Note** +> [!NOTE] > A structure item can either represent a top-level expression, a type definition, a `let` definition, etc. I'm not going to be able to cover all structure items, but you can find more about it in the [OCaml documentation](https://ocaml.org/learn/tutorials/modules.html). I strongly advise you to take a look at the [AST Explorer](https://astexplorer.net/) and play with it; it will help you a lot. Here is a [sample](https://astexplorer.net/#/gist/79e2c7cf04e26236bce5627e6d59a020/caa55456cfa6c30c37cc3a701979cf837c213b71). diff --git a/examples/1 - AST/a - Building AST/README.md b/examples/1 - AST/a - Building AST/README.md index bc56e63..ccdc506 100644 --- a/examples/1 - AST/a - Building AST/README.md +++ b/examples/1 - AST/a - Building AST/README.md @@ -11,20 +11,23 @@ make demo-building_ast ### Table of Contents -- [Description](#description) -- [Building ASTs with Pure OCaml](#building-asts-with-pure-ocaml) - - [Example: Building a Simple Integer AST Manually](#example-building-a-simple-integer-ast-manually) -- [Building ASTs with `AST_builder`](#building-asts-with-ast_builder) - - [Example 1: Using `pexp_constant` for Integer AST](#example-1-using-pexp_constant-for-integer-ast) - - [Example 2: Using `eint` for Simplified Integer AST](#example-2-using-eint-for-simplified-integer-ast) -- [Using Metaquot for AST Construction](#using-metaquot-for-ast-construction) - - [Example: Building an Integer AST with Metaquot](#example-building-an-integer-ast-with-metaquot) - - [Using Anti-Quotations in Metaquot](#using-anti-quotations-in-metaquot) - - [Example: Inserting Dynamic Expressions with Anti-Quotations](#example-inserting-dynamic-expressions-with-anti-quotations) -- [Building Complex Expressions](#building-complex-expressions) - - [Example 1: Constructing a Let Expression with `AST_builder`](#example-1-constructing-a-let-expression-with-ast_builder) - - [Example 2: Constructing a Let Expression with Metaquot](#example-2-constructing-a-let-expression-with-metaquot) -- [Conclusion](#conclusion) +- [Building AST](#building-ast) + - [Table of Contents](#table-of-contents) + - [Description](#description) + - [Building ASTs with Low-Level Builders](#building-asts-with-low-level-builders) + - [Example: Building a Simple Integer AST Manually](#example-building-a-simple-integer-ast-manually) + - [Building ASTs with `AST_builder`](#building-asts-with-ast_builder) + - [Example 1: Using `pexp_constant` for Integer AST](#example-1-using-pexp_constant-for-integer-ast) + - [Example 2: Using `eint` for Simplified Integer AST](#example-2-using-eint-for-simplified-integer-ast) + - [Using Metaquot for AST Construction](#using-metaquot-for-ast-construction) + - [Example: Building an Integer AST with Metaquot](#example-building-an-integer-ast-with-metaquot) + - [Using Anti-Quotations in Metaquot](#using-anti-quotations-in-metaquot) + - [Example: Inserting Dynamic Expressions with Anti-Quotations](#example-inserting-dynamic-expressions-with-anti-quotations) + - [Building Complex Expressions](#building-complex-expressions) + - [Example 1: Constructing a Let Expression with `AST_builder`](#example-1-constructing-a-let-expression-with-ast_builder) + - [Example 2: Constructing a Let Expression with Metaquot](#example-2-constructing-a-let-expression-with-metaquot) + - [Conclusion](#conclusion) + - [On the next section, we will learn how to destructure an AST.](#on-the-next-section-we-will-learn-how-to-destructure-an-ast) ## Description @@ -93,7 +96,7 @@ For even more simplicity, use `eint`: let two ~loc = Ast_builder.Default.eint ~loc 2 ``` -> **:bulb: Tip** +> [!TIP] > `eint` is an abbreviation for expression (`e`) integer (`int`). ## Using Metaquot for AST Construction @@ -110,7 +113,7 @@ With Metaquot, you can construct an integer AST like this: let three ~loc = [%expr 3] ``` -> **:bulb: Tip** +> [!TIP] > Metaquot is highly readable and intuitive but is static. For dynamic values, use Anti-Quotations. ### Using Anti-Quotations in Metaquot diff --git a/examples/1 - AST/a - Building AST/building_ast.ml b/examples/1 - AST/a - Building AST/building_ast.ml index 10c99a9..0d192bd 100644 --- a/examples/1 - AST/a - Building AST/building_ast.ml +++ b/examples/1 - AST/a - Building AST/building_ast.ml @@ -10,6 +10,41 @@ let zero ~loc : Ppxlib_ast.Ast.expression = pexp_attributes = []; } +let of_string_opt_expr ~loc fn name = + Ast_builder.Default.value_binding ~loc + ~pat:(Ast_builder.Default.pvar ~loc name) + ~expr: + (Ast_builder.Default.eapply ~loc + (Ast_builder.Default.evar ~loc fn) + [ Ast_builder.Default.evar ~loc name ]) + +let rec of_string_exp_let ~loc types = + match types with + | [] -> Ast_builder.Default.eunit ~loc + | (name, type_) :: rest -> + let of_string = of_string_opt_expr ~loc (type_ ^ "_of_string_opt") name in + let of_string = + Ast_builder.Default.pexp_let ~loc Nonrecursive [ of_string ] + (of_string_exp_let ~loc rest) + in + of_string + +let of_path_stri ~loc types = + [%stri let of_path path = [%e of_string_exp_let ~loc types]] + +let module_ = + Ast_builder.Default.module_binding ~loc + ~name:{ txt = Some "Routes"; loc } + ~expr: + (Ast_builder.Default.pmod_structure ~loc + ([%stri type t] :: [ of_path_stri ~loc [ ("foo", "name") ] ])) + +let _ = + print_endline + ("\nAST with AST pure tree build: " + ^ Astlib.Pprintast.string_of_structure + [ Ast_builder.Default.pstr_module ~loc module_ ]) + let _ = print_endline ("\nAST with AST pure tree build: " @@ -69,9 +104,14 @@ let _ = ("\nLet expression with metaquot: " ^ Astlib.Pprintast.string_of_expression let_expression) -let anti_quotation_expr expr= [%expr 1 + [%e expr]] +let anti_quotation_expr expr = [%expr 1 + [%e expr]] let _ = print_endline ("\nLet expression with metaquot and anti-quotation: " + ^ Astlib.Pprintast.string_of_expression (anti_quotation_expr (one ~loc))) + +let _ = + print_endline + ("\nAccess ./ast_playground.ml to play with ast" ^ Astlib.Pprintast.string_of_expression (anti_quotation_expr (one ~loc))) \ No newline at end of file diff --git a/examples/1 - AST/b - Destructing AST/destructuring_ast.ml b/examples/1 - AST/b - Destructing AST/destructuring_ast.ml index 6ca4fc5..cb99ebe 100644 --- a/examples/1 - AST/b - Destructing AST/destructuring_ast.ml +++ b/examples/1 - AST/b - Destructing AST/destructuring_ast.ml @@ -1,7 +1,6 @@ open Ppxlib let loc = Location.none - let one ~loc = [%expr 1] let structure_item loc = @@ -28,8 +27,7 @@ let test_match_pstr_eval () = let structure_item = structure_item loc in let structure = [ structure_item ] in match match_int_payload ~loc (PStr structure) with - | Ok _ -> - Printf.printf "\nMatched 1 using Ast_pattern" + | Ok _ -> Printf.printf "\nMatched 1 using Ast_pattern" | Error _ -> Printf.printf "\nDid not match pstr_eval" let _ = test_match_pstr_eval () @@ -41,7 +39,9 @@ let match_int_payload = let test_match_pstr_eval () = let structure_item = structure_item loc in let structure = [ structure_item ] in - try Ast_pattern.parse match_int_payload loc (PStr structure) Printf.printf "\nMatched 1 using Ast_pattern" + try + Ast_pattern.parse match_int_payload loc (PStr structure) Printf.printf + "\nMatched 1 using Ast_pattern" with _ -> Printf.printf "\nDid not match 1 payload using Ast_pattern" let _ = test_match_pstr_eval () @@ -53,8 +53,11 @@ let match_int_payload = let test_match_pstr_eval () = let structure_item = structure_item loc in let structure = [ structure_item ] in - try Ast_pattern.parse match_int_payload loc (PStr structure) Printf.printf "\nMatched 1 using Ast_patter with eint" - with _ -> Printf.printf "\nDid not match 1 payload using Ast_pattern with eint" + try + Ast_pattern.parse match_int_payload loc (PStr structure) Printf.printf + "\nMatched 1 using Ast_patter with eint" + with _ -> + Printf.printf "\nDid not match 1 payload using Ast_pattern with eint" let _ = test_match_pstr_eval () @@ -69,8 +72,7 @@ let match_int_payload expr = let test_match_pstr_eval () = let expr = one ~loc in match match_int_payload expr with - | Ok _ -> - Printf.printf "\nMatched 1 using metaquot" + | Ok _ -> Printf.printf "\nMatched 1 using metaquot" | Error _ -> Printf.printf "\nDid not match 1 using metaquot" let _ = test_match_pstr_eval () @@ -93,6 +95,8 @@ let test_match_pstr_eval () = | Ok value -> Printf.printf "\nMatched 1 + using metaquot and anti-quotation: %s" (value |> string_of_int) - | Error _ -> Printf.printf "\nDid not match matched 1 + using metaquot and anti-quotation" + | Error _ -> + Printf.printf + "\nDid not match matched 1 + using metaquot and anti-quotation" let _ = test_match_pstr_eval () diff --git a/examples/2 - Writing PPXs/a - Context Free/dune b/examples/2 - Writing PPXs/a - Context Free/dune index 935c83f..c235e73 100644 --- a/examples/2 - Writing PPXs/a - Context Free/dune +++ b/examples/2 - Writing PPXs/a - Context Free/dune @@ -3,4 +3,4 @@ (kind ppx_rewriter) (libraries ppxlib yojson ppxlib.astlib) (preprocess - (pps ppxlib.metaquot ppx_deriving.show ppx_deriving.ord))) \ No newline at end of file + (pps ppxlib.metaquot ppx_deriving.show ppx_deriving.ord))) diff --git a/examples/2 - Writing PPXs/b - Global/demo/dune b/examples/2 - Writing PPXs/b - Global/demo/dune index 33b3ce9..7d6fd58 100644 --- a/examples/2 - Writing PPXs/b - Global/demo/dune +++ b/examples/2 - Writing PPXs/b - Global/demo/dune @@ -2,4 +2,4 @@ (name global_demo) (public_name global_demo) (preprocess - (pps global))) + (pps global_context))) diff --git a/examples/2 - Writing PPXs/b - Global/demo/global_demo.ml b/examples/2 - Writing PPXs/b - Global/demo/global_demo.ml index 1bbaf1d..8696e9e 100644 --- a/examples/2 - Writing PPXs/b - Global/demo/global_demo.ml +++ b/examples/2 - Writing PPXs/b - Global/demo/global_demo.ml @@ -1,8 +1,8 @@ -let demo_name = "Global Demo" -let _ = demo_name +let name = "Global Demo" +let _ = name (* Uncomment the code bellow to see the lint error *) -(* let name = "John Doe" *) +(* let demo_name = "John Doe" *) (* module enum *) let _ = print_endline "\n# Enum" @@ -42,10 +42,12 @@ let _ = | None -> Printf.printf "Stick is not a valid value\n" (* Uncomment the code bellow to see the error *) -(* module GameEnumError = struct - type _t = Rock | Paper | Scissors - - module GameEnum = struct - type t = Rock | Paper | Scissors - end [@enum] -end [@enum] *) +(* + module GameEnumError = struct + type _t = Rock | Paper | Scissors + + module GameEnum = struct + type t = Rock | Paper | Scissors + end [@enum] + end [@enum] +*) diff --git a/examples/2 - Writing PPXs/b - Global/dune b/examples/2 - Writing PPXs/b - Global/dune index 642bb43..58d0d6d 100644 --- a/examples/2 - Writing PPXs/b - Global/dune +++ b/examples/2 - Writing PPXs/b - Global/dune @@ -1,6 +1,6 @@ (library - (name global) + (name global_context) (kind ppx_rewriter) (libraries context_free ppxlib ppxlib.astlib) (preprocess - (pps ppxlib.metaquot))) \ No newline at end of file + (pps ppxlib.metaquot))) diff --git a/examples/2 - Writing PPXs/b - Global/global.ml b/examples/2 - Writing PPXs/b - Global/global.ml index 29cf421..6f6a587 100644 --- a/examples/2 - Writing PPXs/b - Global/global.ml +++ b/examples/2 - Writing PPXs/b - Global/global.ml @@ -72,11 +72,11 @@ module Lint = struct let loc = mb.pvb_loc in match mb.pvb_pat.ppat_desc with | Ppat_var { txt = name; _ } -> - if String.starts_with name ~prefix:"demo_" then acc - else + if String.ends_with name ~suffix:"demo_" then Driver.Lint_error.of_string loc "Ops, variable name must not start with demo_" :: acc + else acc | _ -> acc end end diff --git a/examples/3 - Testing PPXs/README.md b/examples/3 - Testing PPXs/README.md new file mode 100644 index 0000000..5fd3df2 --- /dev/null +++ b/examples/3 - Testing PPXs/README.md @@ -0,0 +1,116 @@ +# Testing PPXs + +## Description + +Testing PPXs is crucial to ensure that your transformations work as expected and do not introduce bugs into your codebase. This section will guide you through the process of testing PPXs using both implementation tests and snapshot tests. + +## Table of Contents + +- [Types of Tests](#types-of-tests) +- [Implementation Tests](#implementation-tests) + - [Example: Testing a Simple Transformation](#example-testing-a-simple-transformation) + - [Example: Testing a More Complex Transformation](#example-testing-a-more-complex-transformation) +- [Snapshot Tests](#snapshot-tests) + - [Example: Creating a Snapshot Test](#example-creating-a-snapshot-test) + - [Example: Snapshot Test for a Module Transformation](#example-snapshot-test-for-a-module-transformation) + +## Types of Tests + +1. **Implementation Tests**: These tests verify that the PPX transformations produce the expected output for given input code. They are typically written using a testing framework like Alcotest. + +2. **Snapshot Tests**: These tests capture the output of a PPX transformation and compare it against a previously saved "snapshot". This is useful for ensuring that changes to the PPX do not unintentionally alter the output. + +## Implementation Tests + +Implementation tests involve writing test cases that check the behavior of your PPX transformations. You can use a testing framework like Alcotest to write these tests. + +### Example: Testing a Simple Transformation + +[:link: Sample Code](./demo/test/test_sample.ml#L3-L6) + +Consider a PPX that transforms `[%one]` into `1`. You can write a test case to verify this transformation: + +```ocaml +let test_one () = + let one = [%one] in + Alcotest.check Alcotest.int "should be equal" one 1 +```` + +### Example: Testing a More Complex Transformation + +[:link: Sample Code](./demo/test/test_sample.ml#L11-L23) + +For a more complex transformation, such as a PPX that generates `to_string` and `from_string` functions for a variant type, you can write a test case like this: + +````ocaml +let test_enum () = + let assert_string left right = + Alcotest.check Alcotest.string "should be equal" right left + in + let rock = GameEnum.to_string Rock in + let paper = GameEnum.to_string (GameEnum.from_string "Paper") in + let stick = + try GameEnum.to_string (GameEnum.from_string "Stick") + with _ -> "Stick is not a valid value" + in + let () = assert_string rock "Rock" in + let () = assert_string paper "Paper" in + assert_string stick "Stick is not a valid value" +```` + +## Snapshot Tests + +Snapshot tests are useful for verifying that the output of a PPX transformation remains consistent over time. They involve capturing the output of a transformation and comparing it against a saved snapshot. + +### Example: Creating a Snapshot Test + +[:link: Sample Code](./demo/test/mel_obj.t#L1-L6) + +To create a snapshot test, you can use a tool like `ocamlformat` to format the output of your PPX transformation and compare it against a saved snapshot: + +````sh +$ cat > input.ml << EOF +let one = [%one] +EOF + +$ ./standalone.exe -impl input.ml | ocamlformat - --enable-outside-detected-project --impl | tee output.ml +let one = 1 +```` + +### Example: Snapshot Test for a Module Transformation + +[:link: Sample Code](./demo/test/mel_obj.t#L15-L35) + +For a more complex transformation, such as a module with a `[@enum]` attribute, you can create a snapshot test like this: + +````sh +$ cat > input.ml << EOF +module GameEnum = struct + type t = Rock | Paper | Scissors +end [@enum] +EOF + +$ ./standalone.exe -impl input.ml | ocamlformat - --enable-outside-detected-project --impl | tee output.ml +module GameEnum = struct + type t = Rock | Paper | Scissors + + let from_string value = + match value with + | "Rock" -> Rock + | "Paper" -> Paper + | "Scissors" -> Scissors + | _ -> raise (Invalid_argument "Argument doesn't match variants") + [@@warning "-32"] + + let to_string value = + match value with + | Rock -> "Rock" + | Paper -> "Paper" + | Scissors -> "Scissors" + [@@warning "-32"] +end +```` + +## Conclusion + +Testing PPXs is essential to ensure that your transformations are correct and maintainable. By using both implementation tests and snapshot tests, you can verify that your PPX transformations produce the expected output and remain consistent over time. \ No newline at end of file diff --git a/examples/3 - Testing PPXs/demo/test/dune b/examples/3 - Testing PPXs/demo/test/dune new file mode 100644 index 0000000..c74b8bf --- /dev/null +++ b/examples/3 - Testing PPXs/demo/test/dune @@ -0,0 +1,12 @@ +(cram + (deps %{bin:ocamlformat} standalone.exe)) + +(executable + (name standalone) + (libraries ppxlib context_free global_context)) + +(test + (name test) + (libraries alcotest) + (preprocess + (pps context_free global_context))) diff --git a/examples/3 - Testing PPXs/demo/test/mel_obj.t b/examples/3 - Testing PPXs/demo/test/mel_obj.t new file mode 100644 index 0000000..359aa99 --- /dev/null +++ b/examples/3 - Testing PPXs/demo/test/mel_obj.t @@ -0,0 +1,44 @@ +[%one] + $ cat > input.ml << EOF + > let one = [%one] + > EOF + + $ ./standalone.exe -impl input.ml | ocamlformat - --enable-outside-detected-project --impl | tee output.ml + let one = 1 + +[%one] + $ cat > input.ml << EOF + > let demo_one = [%one] + > EOF + + $ ./standalone.exe -impl input.ml | ocamlformat - --enable-outside-detected-project --impl | tee output.ml + [@@@ocaml.ppwarning "Ops, variable name must not start with demo_"] + + let demo_one = 1 + +[%one] + $ cat > input.ml << EOF + > module GameEnum = struct + > type t = Rock | Paper | Scissors + > end [@enum] + > EOF + + $ ./standalone.exe -impl input.ml | ocamlformat - --enable-outside-detected-project --impl | tee output.ml + module GameEnum = struct + type t = Rock | Paper | Scissors + + let from_string value = + match value with + | "Rock" -> Rock + | "Paper" -> Paper + | "Scissors" -> Scissors + | _ -> raise (Invalid_argument "Argument doesn't match variants") + [@@warning "-32"] + + let to_string value = + match value with + | Rock -> "Rock" + | Paper -> "Paper" + | Scissors -> "Scissors" + [@@warning "-32"] + end diff --git a/examples/3 - Testing PPXs/demo/test/standalone.ml b/examples/3 - Testing PPXs/demo/test/standalone.ml new file mode 100644 index 0000000..e3cba40 --- /dev/null +++ b/examples/3 - Testing PPXs/demo/test/standalone.ml @@ -0,0 +1 @@ +let () = Ppxlib.Driver.standalone () diff --git a/examples/3 - Testing PPXs/demo/test/test.ml b/examples/3 - Testing PPXs/demo/test/test.ml new file mode 100644 index 0000000..5fc96f6 --- /dev/null +++ b/examples/3 - Testing PPXs/demo/test/test.ml @@ -0,0 +1 @@ +let () = Alcotest.run "React" [ Test_sample.tests ] \ No newline at end of file diff --git a/examples/3 - Testing PPXs/demo/test/test_sample.ml b/examples/3 - Testing PPXs/demo/test/test_sample.ml new file mode 100644 index 0000000..3a43d33 --- /dev/null +++ b/examples/3 - Testing PPXs/demo/test/test_sample.ml @@ -0,0 +1,26 @@ +let test title fn = Alcotest.test_case title `Quick fn + +let test_one () = + let one = [%one] in + Alcotest.check Alcotest.int "should be equal" one 1 + +module GameEnum = struct + type t = Rock | Paper | Scissors +end [@enum] + +let test_enum () = + let assert_string left right = + Alcotest.check Alcotest.string "should be equal" right left + in + let rock = GameEnum.to_string Rock in + let paper = GameEnum.to_string (GameEnum.from_string "Paper") in + let stick = + try GameEnum.to_string (GameEnum.from_string "Stick") + with _ -> "Stick is not a valid value" + in + let () = assert_string rock "Rock" in + let () = assert_string paper "Paper" in + assert_string stick "Stick is not a valid value" + +let tests = + ("Sample", [ test "test" test_one; test "enum" test_enum ]) diff --git a/ppx-studies.opam b/ppx-studies.opam index 28b0f9e..c2d3442 100644 --- a/ppx-studies.opam +++ b/ppx-studies.opam @@ -10,6 +10,7 @@ bug-reports: "https://github.com/pedrobslisboa/ppx-studies/issues" depends: [ "dune" {>= "3.8"} "ocaml" {>= "5.0.0"} + "alcotest" "ocaml-lsp-server" "ocamlformat" "ppx_deriving"