Intro to PPXs, for Reason newcomers
Published: 2020-04-28 21:52:43

ReasonML (also known as Reason) has gotten very popular these days. It's a new syntax for OCaml that looks a lot like JavaScript, allowing you to develop with a rock-solid type system while still having strong interop with JavaScript and its strong ecosystem.

That was a mouthful, I know. For a proper introduction (if you aren't familiar), I'd highly suggest reading through the [official Reason docs](https://reasonml.github.io/docs/en/what-and-why)!

Now let's get into the point of this post - Reason PPXs.

## What is a PPX and Why Do I Care?

A Pre-Processor eXtension (PPX) is a preprocessor that is applied to your code before it gets passed to the compiler. It allows for arbitrary manipulation of your code's Abstract Syntax Tree, rather than the code itself. 

For example, if you have ever written GraphQL queries in your Reason code, chances are you've used `[%graphql {|QUERY|}]` to validate your queries - [this is a PPX](https://github.com/reasonml-community/graphql_ppx)! In the case of `graphql_ppx` , your query will be validated and is guaranteed to be type-safe. The query is then transformed into a normal string before your code is compiled.

Another example is [`tailwind-ppx`](https://github.com/dylanirlbeck/tailwind-ppx), a PPX I wrote to validate your Tailwind CSS classes at compile-time. Using this PPX, you can write your class names like `<Component className=[%tw "flex flex-row"] />` and get validation for zero extra cost (in case there's a typo in a class name or you included a class name twice, for example). 

PPXs are very powerful, but I found it rather difficult to get a barebones one up and running. Though I tried [`hello-ppx-esy`](https://github.com/jchavarri/hello-ppx-esy/) and eventually found some success, it seemed that the process should be friendly to both Reason newcomers and PPX newcomers, rather than just the latter. Namely, I felt that (1) Reason newcomers should not have to use [esy](https://esy.sh), (2) it should be more natural to run tests, and (3) publishing your PPX should be straightforward.

## `hello-ppx-bucklescript`

[`hello-ppx-bucklescript`](https://github.com/dylanirlbeck/hello-ppx-bucklescript) is a small Github project I wrote a while back that provides the boilerplate for writing a Reason PPX. It takes care of configuring the BuckleScript compiler, hooking up tests, and generating your PPX executable. `hello-ppx-bucklescript` was inspired by `hello-ppx-esy`, but differs in that you do not need `esy`, running tests is simple with `yarn test`, and it includes a publish folder for making your PPX immediately available on the NPM registry.

## Project structure

`publish/` - basic setup for publishing your PPX to the NPM registry.
`src/` - contains the source code for the PPX.
`tests/` - a single test file that can be run with `yarn test`

## PPX source code

The ppx included in the project essentially turns the `[%hello]` expression into the integer literal `42` at compile-time. The code to do this operation is this:

```reason
open Ast_mapper;
open Parsetree;
let expr = (mapper, e) =>  
   switch (e.pexp_desc) {
   /* If the expression is [%hello] */  
   | Pexp_extension(({txt: "hello", loc, _}, _payload)) =>    
     /* Then replace by 42 */
     Ast_helper.Exp.constant(Asttypes.Const_int(42))  
   | _ => default_mapper.expr(mapper, e)  
   }; 
let mapper = _ => {...default_mapper, expr}; 
let () = run_main(mapper);
```

Let's walk through each part.

```reason
open Ast_mapper;
open Parsetree;
Use the open directive to make the Ast_mapper and Parsetree modules directly accessible by the Hello module. See here for more information on modules.
let expr = (mapper, e) =>  
   switch (e.pexp_desc) {
   /* If the expression is [%hello] */  
   | Pexp_extension(({txt: "hello"}, _)) =>    
     /* Then replace by 42 */
     Ast_helper.Exp.constant(Asttypes.Const_int(42))  
   | _ => default_mapper.expr(mapper, e)  
   };
```

expr is a function that takes in a mapper and an expression and returns a new expression. For the `[%hello]` PPX, `expr` is the function that we care about most.

```reason
switch (e.pexp_desc) {
   ...
}
```

Here we're going to switch on the value of the `pexp_desc` property of the input expression. `pexp_desc` is of type `expression_desc`, and thus can have many different constructors (see its type [here](https://caml.inria.fr/pub/docs/manual-ocaml/compilerlibref/Parsetree.html#TYPEexpression_desc)) - however, all we really care about is if it's a PPX extension (`Pexp_extension`) and the extension name is `hello`!

```reason
| Pexp_extension(({txt: "hello", _}, _)) =>
   ...
```

If we fall into this case our expression matches the `Pexp_extension` constructor, and as a result, we need to modify the AST! We can ignore the other properties of a `Pexp_extension`, since all we care about is that the `txt` (the extension name) is `hello` and not something like `graphql` or `tw`. We'll replace the AST with the integer literal `42`, and then we're done!

```reason
| Pexp_extension(({txt: "hello", loc, _}, _payload)) =>
  /* Then replace by 42 */
  Ast_helper.Exp.constant(Asttypes.Const_int(42))
``` 

That's it! The rest of the code is just PPX boilerplate that doesn't need much conceptual understanding. Feel free to ask questions below if you're confused, though!


## Sources

I could not have written this article, nor created `hello-ppx-bucklescript` or `tailwind-ppx`, without the following `[%incredible]` sources:

- https://blog.hackages.io/reasonml-ppx-8ecd663d5640
- https://github.com/jchavarri/hello-ppx-esy/
- https://tarides.com/blog/2019-05-09-an-introduction-to-ocaml-ppx-ecosystem

Thank you for reading! And please check out [`tailwind-ppx`](https://github.com/dylanirlbeck/tailwind-ppx) if you're thinking about building a new ReasonReact + TailwindCSS project!