erl_pp(3erl)

πŸ’… erl_pp β€” the Erlang pretty printer (yes bestie, erl_pp(3erl), you're in the right place)

okay so picture this: you've got abstract forms lying around and they're giving unreadable, and you want them to look pretty. That's literally this whole module. The functions here generate aesthetically attractive representations of abstract forms that are suitable for printing. Slay.

All the functions return (possibly deep) lists of characters and generate an error if the form is wrong. No cap β€” feed it garbage, it will throw.

All functions can have an optional argument that specifies a hook that gets called if an attempt is made to print an unknown form. Very "if you know, you know" of them.

🫢 important psa: if you're using these functions to convert abstract code back to Erlang source code, the enclosing function should first be processed by legalize_vars/1 so the output is semantically equivalent to the abstract code. Don't skip this step, I'm begging.

✨ DATA TYPES ✨

hook_function()

hook_function() =
    none |
    fun((Expr :: erl_parse:abstract_expr(),
         CurrentIndentation :: integer(),
         CurrentPrecedence :: integer() >= 0,
         Options :: options()) ->
            io_lib:chars())

That optional argument HookFunction you keep seeing in the functions below? That's this. It defines a function that's called when an unknown form occurs where there is to be a valid expression.

If HookFunction is equal to none, there is no hook function. Simple as that.

The called hook function is to return a (possibly deep) list of characters. And expr/4 is useful in a hook β€” iconic teamwork tbh.

If CurrentIndentation is negative, there are no line breaks and only a space is used as a separator. noted. πŸŽ€

option()

option() =
    {hook, hook_function()} |
    {encoding, latin1 | unicode | utf8} |
    {quote_singleton_atom_types, boolean()} |
    {linewidth, integer() >= 1} |
    {indent, integer() >= 1}

Here's the vibe check on the notable ones:

  • The option quote_singleton_atom_types is used to add quotes to all singleton atom types.
  • The option linewidth controls the maximum line width for formatted lines (defaults to 72 characters).
  • The option indent controls the indention for formatted lines (defaults to 4 spaces).

options()

options() = hook_function() | [option()]

yep. that's it. a hook or a list of options. lowkey elegant.


πŸ”₯ EXPORTS πŸ”₯

attribute(Attribute) -> io_lib:chars()

attribute(Attribute, Options) -> io_lib:chars()

Types:

Attribute = erl_parse:abstract_form()
Options = options()

Same as form/1,2, but only for attribute Attribute. main character energy, but make it scoped.


expr(Expression) -> io_lib:chars()

expr(Expression, Options) -> io_lib:chars()

expr(Expression, Indent, Options) -> io_lib:chars()

expr(Expression, Indent, Precedence, Options) -> io_lib:chars()

Types:

Expression = erl_parse:abstract_expr()
Indent = integer()
Precedence = integer() >= 0
Options = options()

Prints one expression. It's useful for implementing hooks (see section Known Limitations). πŸ¦‹


exprs(Expressions) -> io_lib:chars()

exprs(Expressions, Options) -> io_lib:chars()

exprs(Expressions, Indent, Options) -> io_lib:chars()

Types:

Expressions = [erl_parse:abstract_expr()]
Indent = integer()
Options = options()

Same as form/1,2, but only for the sequence of expressions in Expressions. a whole squad of expressions, handled. πŸ’…


form(Form) -> io_lib:chars()

form(Form, Options) -> io_lib:chars()

Types:

Form = erl_parse:abstract_form() | erl_parse:form_info()
Options = options()

Pretty prints a Form, which is an abstract form of a type that is returned by erl_parse:parse_form/1. this is the one, the moment, the whole point of the module honestly ✨


function(Function) -> io_lib:chars()

function(Function, Options) -> io_lib:chars()

Types:

Function = erl_parse:abstract_form()
Options = options()

Same as form/1,2, but only for function Function. one function, all the attention. πŸŽ€


guard(Guard) -> io_lib:chars()

guard(Guard, Options) -> io_lib:chars()

Types:

Guard = [erl_parse:abstract_expr()]
Options = options()

Same as form/1,2, but only for the guard test Guard. guards deserve to be pretty too, periodt.


legalize_vars(Function) -> erl_parse:abstract_form()

Types:

Function = erl_parse:abstract_form()

okay bestie this one needs a whole story time, because it's the why behind that psa at the top.

So: the Erlang compiler will, when expanding records to tuples, introduce new variables in the abstract representation. And because that expansion happens on the abstract representation, the compiler can safely name the new variables with names that are not syntactically valid in Erlang source code β€” specifically, the name starts with a lowercase letter β€” which guarantees the uniqueness of the new names. clever! πŸ™ˆ

But β€” and this is the problem β€” that strategy leads to problems if a user wants to convert the abstract representation back to Erlang source code using the functions of this module. Typically, pattern variables get output as atoms, thus changing the sematics of the program. 😭 (yes, that's how it's spelled in the source, we don't fix it, we respect it.)

To solve this problem, legalize_vars/1, when run on the abstract representation of a function, will return an equivalent function where all variables will have syntactically valid names. fixed. healed. slayed. πŸ‘‘


πŸ’€ KNOWN LIMITATIONS πŸ’€

It is not possible to have hook functions for unknown forms at other places than expressions. that's the rule, that's the boundary, we don't argue with it. πŸ˜”


🌈 SEE ALSO 🌈

go read those next, i'm literally so serious, they're the supporting cast and they ate πŸ’–


mwah πŸ’‹ β€” brought to you by Ericsson AB, running stdlib 4.3.1.3, and this little erl_pp(3erl) era of our lives. take care of your abstract forms and they'll take care of you. 🫢✨

πŸ“¦ where this page comes from