astranaut

Erlang Ast traverse and macro.


Keywords
ast, macro, quote, traverse
License
MIT

Documentation

Travis CI

requirements

erlang R19 or higher

traverse

traverse functions:

  astranaut_traverse:map(map_fun(), form(), Opts :: opts()) -> 
    traverse_return(node()) | parse_transform_return(node()).
    
  astranaut_traverse:reduce(reduce_fun(), state(), form(), Opts :: opts()) -> 
    traverse_return(state()).
    
  astranaut_traverse:map_with_state(map_state_fun(), state(), form(), Opts :: opts()) -> 
    traverse_return(node()) | parse_transform_return(node()).
    
  astranaut_traverse:mapfold(mapfold_fun(), state(), form(), Opts :: opts()) -> 
    traverse_return({form(), state()}).

arguments

  form()    :: node() | [node()].
  node()    :: erlang ast node.
  state()   :: any().

traverse_fun()

  map_fun()       :: (node(), Attr :: attr()) -> TraverseFunReturn :: traverse_fun_return(node()).
  reduce_fun()    :: (node(), state(), Attr :: attr()) -> TraverseFunReturn :: traverse_fun_return(state()).
  map_state_fun() :: (node(), state(), Attr :: attr()) -> TraverseFunReturn :: traverse_fun_return(node()).
  mapfold_fun()   :: (node(), state(), Attr :: attr()) -> TraverseFunReturn :: traverse_fun_return({node(), state()}).

Attr

  attr() :: #{step => Step :: step(), node :: NodeType :: node_type(), attribute :: Attribute}.

Step

  which traverse step while traversing, very useful while traverse_style() in opts() is all.

  step()  :: pre | post | leaf. 

NodeType

  ast node type.

  node_type() :: form | attribute | pattern | expression | guard. 

Attribute

  if NodeType is attribute, Attribute is name of attribute, or Attribute does not exists.

TraverseFunReturn

  traverse_fun_return(A) :: A | {error, error()} | continue | {continue, A} |
                            #{'__struct__' => astranaut_traverse_fun_return, 
                              node := Node :: node(), state := State :: state(), continue := Continue :: boolean(),
                              error := error(), warning := error(), errors := [error()], warnings => [error()]}.

Node

  node transformed to new node in traverse_walk_fun(), default is node() provided in traverse_walk_fun().

State

  state used in traverse_walk_fun(), default is state() provided in traverse_walk_fun().

Continue

  if Continue is true or traverse_fun_return(A) is continue | {continue, A}, and Step of attr() is pre
  skip traverse children of currrent node and go to next node, nothing affected when Step of attr() is leaf or post.

error()

  error()   :: #{'__struct__' => astranaut_traverse_error, 
                 line := Line :: integer(), module := Module :: module(), reason => Reason :: term()} | 
                {Line, Module, Reason} | {Line, Reason} | Reason.

Line

  expected error line, default is line of node in traverse_walk_fun().

Module

  error formatter module which provide format_error/1, default is formatter option in opts().

Opts

  opts()    :: {traverse => TraverseStyle :: traverse_style(), parse_transform => ParseTransform :: boolean(),
                node => FormType :: form_type(), formatter => Formatter, 
                children => Children, sequence_children => SequenceChildren}.

Formatter

  error formatter module which provide format_error/1, default is astranaut_traverse.

ParseTransform

  traverse_return(node()) will be transformed to parse_transform_return()
  which could directed used as return in parse_transform/2, useful in map/3, map_with_state/3.

NodeType

  node_type(). if from() is incomplete erlang ast, this should be provided to help generate node_type() in attr().
  if top node is function or attribute, default top node_type() in attr() is form.
  else, default top node_type() in attr() is expression.

TraverseStyle

  pre | post | all | leaf.

Children

   true: Only traverse children of node, not traverse node its self.

SequenceChildren

   callback to defined your own traverse children method

SequenceChildren = fun(DeepListOfChildrenM) -> MChildren end.

   traverse right expression first in match expression

SequenceChildren = 
  fun([PatternMs, ExpressionMs]) -> 
    %% reverse the traverse order, traverse ExpressionMs first
    %% deep_r_sequence_m means reverse sequence_m the first level of deep list.
    astranaut_traverse_monad:deep_r_sequence_m([PatternMs, ExpressionMs]) 
  end.

   do something special to Clause Patterns

SequenceChildren = 
  fun([PatternMs|GuardsAndExpressionMs]) -> 
    %% PatternMs is a list of monad, sequence_m it to get a monad of list.
    PatternsM = astranaut_traverse_monad:sequence_m(PatternMs),
    %% do something special to PatternsM monad.
    PatternsM1 = do_something_special(PatternsM),
    %% deep_sequence_m the new tree.
    astranaut_traverse_monad:deep_sequence_m([PatternsM1|GuardsAndExpressionMs]) 
  end.

   do something special to Each Clause Patterns

SequenceChildren = 
  fun([PatternMs|GuardsAndExpressionMs]) -> 
    %% PatternMs is a list of monad, sequence_m it to get a monad of list.
    PatternMs1 = lists:map(fun(PatternM) -> do_something_special(PatternM) end, PatternMs),
    %% deep_sequence_m the new tree.
    astranaut_traverse_monad:deep_sequence_m([PatternMs1|GuardsAndExpressionMs]) 
  end.

traverse_return(Return)

  traverse_return(Return) :: Return | {ok, Return, Errors :: traverse_return_error(), Warnings :: traverse_return_error()} | 
                             {error, Errors, Warnings}.

parse_transform_return(Return)

  parse_transform_return(Return) :: Return | {warning, Return, Warnings :: prase_transform_error()} |
                                    {error, Errors :: parse_transform_error(), Warnings}.

ReturnError

  traverse_return_error() :: [{Line :: line(), Module :: module(), Reason :: term()}].
  parse_transform_error() :: [{File, traverse_retrun_error()}].

Structs

  astranaut_traverse:traverse_fun_return(#{}) -> traverse_fun_return(). 
  astranaut_traverse:traverse_error(#{}) -> error(). 

Advanced

  powerful map_m function if you famillar with monad.

  astranaut_traverse:map_m((A, attr()) => monad(A), map_m_opts()) -> monad(A). 

Quote

quick start

with

-include_lib("astranaut/include/quote.hrl").

you can use quote(Code) to represent ast of the code.

quote(Code) | quote(Code, Options)

Options

  atom() => {atom() => true}
  proplists() => map(),
  Line => #{line => Line}
  #{line => Line, code_line => CodeLine, debug => Debug}.

Line

   Line could be any expression, the ast will be transformed.

    quote(
      fun(_) ->
        ok
      end, 10). 
    =>
    astranaut:replace_line_zero(quote(fun(_) -> ok end), 10).
    =>
    {'fun', 10, {clauses, [{clause, 10, [{var, 10, '_'}], [], [{atom, 10, ok}]}]}}.

CodeLine

   if CodeLine is true

    10: quote(
    11:   fun(_) ->
    12:     ok
    13: end, code_line).
    =>  
    {'fun' 10, {clauses, [{clause, 11, [{var, 11, '_'}], [], [{atom, 12, ok}]}]}}.

Debug

   if Debug is true, ast generated by quote will be printed to console at compile time.

unquote

unquote(Ast)
unquote = Ast.
unquote_splicing(Asts)
unquote_splicing = Asts.

why two forms

   unquote(Var) is not a valid ast in function clause pattern.

Var = {var, 0, A}
quote(fun(unquote = Var) -> unquote(Var) end).

variable binding

bind one ast

  _@V, same as unquote(V)

    V = {var, 10, 'Var'},
    quote({hello, World, unquote(V)}) =>
    {tuple, 1, [{atom, 1, hello}, {var, 1, 'World'}, V]} =>
    {tuple, 1, [{atom, 1, hello}, {var, 1, 'World'}, {var, 10, 'Var'}]}

bind a list of ast

  _L@Vs,same as unquote_splicing(Vs)

    Vs = [{var, 2, 'Var'}, {atom, 2, atom}],
    quote({A, unquote_splicing(Vs), B}) => 
    {tuple, 1, [{var, 1, 'A'}, Vs ++ [{var, 1, 'B'}]]} =>
    {tuple, 1, [{var, 1, 'A'}, {var, 2, 'Var'}, {atom, 2, atom}, {var, 1, 'B'}]}

bind a value

  Atom = hello,
  Integer = 10,
  Float = 1.3,
  String = "123",
  Variable = 'Var',

  _A@Atom => {atom, 0, Atom} => {atom, 0, hello}
  _I@Integer => {integer, 0, Integer} => {integer, 0, 10}
  _F@Float => {float, 0, Float} => {float, 0, 1.3}
  _S@String => {string, 0, String} => {string, 0, "123"}
  _V@Variable => {var, 0, Variable} => {var, 0, 'Var'}

why binding

  _X@V could be used in any part of quoted ast.
  it's legal:

    Class = 'Class0',
    Exception = 'Exception0',
    StackTrace = 'StackTrace0',
    quote(
      try
        throw(hello)
      catch
        _V@Class:_V@Exception:_V@StackTrace ->
          erlang:raise(_V@Class, _V@Exception, _V@StackTrace)
      end).

  it's illegal

    Class = {var, 0, 'Class0'},
    Exception = {var, 0, 'Exception0'},
    StackTrace = {var, 0, 'StackTrace0'},   

    quote(
      try
        A
      catch
        unquote(Class):unquote(Exception):unquote(StackTrace) ->
          erlang:raise(_@Class, _@Exception, _@StackTrace)
      end).

in other hand, V in unquote_xxx(V) could be any expression, it's more powerful than _X@V

unquote and variable binding in pattern

  quote macro could also be used in pattern match such as
  for limit of erlang ast format in pattern, some special forms is used

left side of match

     quote(_A@Atom) = {atom, 1, A}
     
     =>
     
     {atom, _, Atom} = {atom, 1, A}

  function pattern

     macro_clause(quote = {hello, _A@World = World2} = C) ->
       quote({hello2, _A@World, _@World2,_@C});
     
     => 
     
     macro_clause({tuple, _, [{atom, _, hello}, {atom, _, World} = World2]} = C) ->
       {tuple, 2, {atom, 2, hello2}, {atom, 2, World}, World2, C}

  case clause pattern:

     case Ast of
       quote(_A@Atom) ->
         Atom;
       _ ->
         other
     end.
     
     =>
     
     case ast of
         {atom, _, Atom} ->
             Atom;
         _ ->
             other
     end.

Macro

Usage

-include_lib("astranaut/include/macro.hrl").

macro.hrl add three attribute: use_macro, exec_macro debug_macro

use_macro

-use_macro({Macro/A, opts()}).
-use_macro({Module, Macro/A, opts()}).

exec_macro

  execute macro and add result to current ast.

-exec_macro({Macro, Arguments}).
-exec_macro({Module, Macro, Arguments}).

export_macro

  used in where macro defined, options in export_macro will be merged to options in use_macro.

-export_macro({[MacroA/A, MacroB/B], opts()}).

debug_macro

-debug_macro(true).

   module will be printed to console after astranaut_macro transform.

opts()

  #{debug => Debug, debug_ast => DebugAst, alias => Alias, 
    formatter => Formatter, attrs => Attrs, order => Order,
    as_attr => AsAttr, merge_function => MergeFunction, auto_export => AutoExport,
    group_args => GroupArgs}
  }

   opts() could also be proplists, same usage of map().

Debug

  print code generated when macro called compile time.

DebugAst

  print ast generated when macro called compile time.

Alias

   use Alias(Arguments) instead of Module:Macro(Arguments).

Formatter

   module include format_error/1 to format macro errors,
   if formatter is true, formatter is the module where macro defined,
   default is astranaut_traverse.

Attrs

   module attributes as extra args while calling macro.

-module(a).
-behaviour(gen_server).
-use_macro({macro/2, [{attrs, [module, line, behaviour]}]}).

hello() ->
  macro_a:macro(world).

macro(Ast, #{module => Module, line => Line, behaviour => Behaviours} = Attributes) ->
    {warning, Ast, {attributes, Module, Line, Behaviours}}.

Order

   macro expand order for nested macro , value is pre | post. default is post.
   pre is expand macro from outside to inside, post is expand macro from inside to outside.

AsAttr

   user defined attribute name replace of -exec_macro.

MergeFunction

   -exec_macro ast function merge to function with same name and arity if exists.

AutoExport

   -exec_macro ast function auto export, merge to current export if exists.

GroupArgs

   treat macro arguments as list

-use_macro({a, [group_args]}).

test() ->
    a(hello, world).

a(Asts) ->
  quote({unquote_splicing(Asts)}).

  define macro as normal erlang functions.
  macro expand order is the order of -use_macro in file.
  macro will be expand at compile time by parse_transformer astranaut_macro.
  macro does not know runtime value of arguments.
  arguments passed in macro is erlang ast.
  arguments passed in -exec_macro is term.
  -export will be moved to appropriate location in ast forms.
  macro return value is same meaning of traverse_fun_return().

-use_macro({macro_1/1, []}).
-use_macro({macro_2/1, []}).

-export([test/0]).

test() ->
  macro_1(hello()).

macro_1(Ast) ->
  quote(
      fun() -> unquote(Ast) end
  ).

-exec_macro({macro_2, [hello]}).

macro_2(Name) ->
  astranaut:function(
    Name,
    quote(
      fun() ->
          unquote_atom(Name)
      end)).

=>

-use_macro({macro_1/1, []}).
-export([test/0]).
-export([hello/0]).

test_macro_1() ->
  fun() -> hello() end.

macro_1(Ast) ->
  quote(
      fun() -> unquote(Ast) end
  ).

hello() ->
  hello.

macro_2(Name) ->
  astranaut:function(
    Name,
    quote(
      fun() ->
          unquote_atom(Name)
      end)).

hygienic macro

   each macro expansion has it's unique namespace.

   @{macro_module_name}@_{counter} is added to it's original name.

-module(macro_example).
macro_with_vars_1(Ast) ->
    quote(
      begin
          A = 10,
          B = unquote(Ast),
          A + B
      end
     ).
macro_with_vars_2(Ast) ->
    quote(
      begin
          A = 10,
          B = unquote(Ast),
          A + B
      end
     ).
test_macro_with_vars(N) ->
    A1 = macro_with_vars_1(N),
    A2 = macro_with_vars_2(A1),
    A3 = macro_with_vars_2(N),
    A4 = macro_with_vars_1(A1),
    A1 + A2.

=>

test_macro_with_vars(N) ->
A1 =
begin
  A@macro_example@_1 = 10,
  B@macro_example@_1 = N,
  A@macro_example@_1 + B@macro_example@_1
end,
A2 = 
begin
  A@macro_example@_3 = 10,
  B@macro_example@_3 = A1,
  A@macro_example@_3 + B@macro_example@_3
end,
A3 = 
begin
  A@macro_example@_4 = 10,
  B@macro_example@_4 = N,
  A@macro_example@_4 + B@macro_example@_4
end,
A4 =
begin
  A@macro_example@_2 = 10,
  B@macro_example@_2 = A1,
  A@macro_example@_2 + B@macro_example@_2
end,
A1 + A2 + A3 + A4.

parse_transform

   for old parse_transform module which is used widely, two function is provided.

*astranaut_macro:transform_macro(Module, Function, Arity, Opts, Forms).
*astranaut_macro:transform_macros([Macro...], Forms).
Macro = {Module, Function, Arity, Opts}.

   example:

-module(do).

-include_lib("astranaut/include/quote.hrl").

-export([parse_transform/2]).

parse_transform(Forms, _Options) ->
    astranaut_macro:transform_macro(do_macro, do, 1, [{alias, do}, formatter], Forms).

Rebinding

-include_lib("erlando/include/rebinding.hrl").

-rebinding_all(Opts).
-rebinding_fun(FAs).
-rebinding_fun({FAs, Opts}).

FAs = FA | [FA...].
FA = F | F/A.
Opts = Opt | [Opt...] | #{OptKey => OptValue}.
Opt = OptKey | {OptKey, OptValue}.
#{OptKey => OptValue} = #{debug => true | false}.

Rebinding Attributes

   -rebinding_all -rebinding_fun defines rebinding scope.
   -rebinding_all meaning rebinding scope is all function.
   -rebinding_fun meaning rebinding scope is in functions mentioned.
   rebinding options is avaliable in scope mentioned.
   rebinding option debug means print code after rebinding rules applied.
   if neither -rebinding_fun nor -rebinding_all is used, rebinding scope is all function and rebinding options is [].

Rebinding Rules

   pattern variables will be renamed while already used include:
     function pattern variables
     match pattern variables
     list comprehension pattern variables
     bitstring comprehension pattern variables
   pattern variables with same name in same pattern scope will be renamed to same name.
   other variable will be renamed follow last renamed vaiable last avaliable scope used.
   +{pattern variable} means pinned variable like Elixir ^{pattern variable}, also works like other variable.

Examples

hello(A, A, B) ->
    {A, A, B} = {A + 1, A + 1, B + 1},
    {A, A, B}.

=>

hello(A, A, B) ->
  {A_1, A_1, B_1} = {A + 1, A + 1, B + 1},
  {A_1, A_1, B_1}.
hello(A, B) ->
  A = 
    case A of
        B -> 
          B = A + B,
          A = A + B,
          B = A + B,
          B;
        A ->
          B = A + B
          B
    end,
  B = 
    case A of
        B -> 
          B = A + B,
          A = A + B,
          B = A + B,
          B;
        A ->
          B = A + B
          B
    end,
  {A, B}.

=>

hello(A, B) ->
  A_2 = 
    case A of
        B -> 
          B_1 = A + B,
          A_1 = A + B_1,
          B_2 = A_1 + B_1,
          B_2;
        A ->
          B_1 = A + B
          B_1
    end,
    
  B_5 = 
    case A_2 of
        B -> 
          %% B_1 and B_2 is already used, next var name is B_3, last var name in scope is B.
          B_3 = A_2 + B,
          A_3 = A_2 + B_3,
          B_4 = A_3 + B_3,
          B_4,
        A_2 ->
          B_3 = A_2 + B
          B_3
    end,
  {A_2, B_5}.
hello_f(A) ->
    A = A + 1,
    F = fun F (0) -> 0; F (A) -> A = F(A - 1), A end,
    A = F(A),
    A.

=>

hello_f(A) ->
    A_1 = A + 1,
    F = fun F(0) -> 0; F(A_2) -> A_3 = F(A_2 - 1), A_3 end,
    A_2 = F(A_1),
    F_1 = fun F_1(0) -> 0; F_1(A_3) -> A_4 = F_1(A_3 - 1), A_4 end,
    A_3 = F_1(A_2),
    A_3.