项目作者: slepher

项目描述 :
traverse erlang ast and elixir macro in erlang.
高级语言: Erlang
项目地址: git://github.com/slepher/astranaut.git
创建时间: 2019-01-17T12:24:17Z
项目社区:https://github.com/slepher/astranaut

开源协议:MIT License

下载


Build Status

requirements

   erlang R19 or higher

traverse

traverse functions:

  1. astranaut_traverse:map(map_fun(), form(), Opts :: opts()) ->
  2. traverse_return(node()) | parse_transform_return(node()).
  3. astranaut_traverse:reduce(reduce_fun(), state(), form(), Opts :: opts()) ->
  4. traverse_return(state()).
  5. astranaut_traverse:map_with_state(map_state_fun(), state(), form(), Opts :: opts()) ->
  6. traverse_return(node()) | parse_transform_return(node()).
  7. astranaut_traverse:mapfold(mapfold_fun(), state(), form(), Opts :: opts()) ->
  8. traverse_return({form(), state()}).

arguments

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

traverse_fun()

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

Attr

  1. 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.

  1. step() :: pre | post | leaf.

NodeType

  ast node type.

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

Attribute

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

TraverseFunReturn

  1. traverse_fun_return(SA) :: SA | {error, error()} | {error, SA, error()} |
  2. {warning, SA, error()} | {warning, error()} |
  3. continue | {continue, SA} |
  4. astranaut_walk_return:astranaut_walk_return(A) |
  5. astranaut_traverse_m:astranaut_traverse_m(S, A) |
  6. astranaut_return_m:astranaut_return_m(A) |
  7. astranaut_base_m:astranaut_base_m(A).
  8. SA is same return type in traverse_fun(), but A is always node(), and S is always state().

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()

  1. error() :: Reason.

Pos

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

Module

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

Opts

  1. opts() :: {traverse => TraverseStyle :: traverse_style(), parse_transform => ParseTransform :: boolean(),
  2. node => FormType :: form_type(), formatter => Formatter,
  3. 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

  1. SequenceChildren = fun(DeepListOfChildrenM) -> MChildren end.

   traverse right expression first in match expression

  1. SequenceChildren =
  2. fun([PatternMs, ExpressionMs]) ->
  3. %% reverse the traverse order, traverse ExpressionMs first
  4. %% deep_r_sequence_m means reverse sequence_m the first level of deep list.
  5. astranaut_traverse:deep_r_sequence_m([PatternMs, ExpressionMs])
  6. end.

   do something special to Clause Patterns

  1. SequenceChildren =
  2. fun([PatternMs|GuardsAndExpressionMs]) ->
  3. %% PatternMs is a list of monad, sequence_m it to get a monad of list.
  4. PatternsM = astranaut_traverse:deep_sequence_m(PatternMs),
  5. %% do something special to PatternsM monad.
  6. PatternsM1 = do_something_special(PatternsM),
  7. %% deep_sequence_m the new tree.
  8. astranaut_traverse:deep_sequence_m([PatternsM1|GuardsAndExpressionMs])
  9. end.

   do something special to Each Clause Patterns

  1. SequenceChildren =
  2. fun([PatternMs|GuardsAndExpressionMs]) ->
  3. %% PatternMs is a list of monad, sequence_m it to get a monad of list.
  4. PatternMs1 = lists:map(fun(PatternM) -> do_something_special(PatternM) end, PatternMs),
  5. %% deep_sequence_m the new tree.
  6. astranaut_traverse:deep_sequence_m([PatternMs1|GuardsAndExpressionMs])
  7. end.

traverse_return(Return)

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

parse_transform_return(Return)

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

ReturnError

  1. traverse_return_error() :: [{Pos :: pos(), Module :: module(), Reason :: term()}].
  2. parse_transform_error() :: [{File, traverse_retrun_error()}].

Structs

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

Advanced

  powerful map_m function if you famillar with monad.

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

monad modules

astranaut_traverse_m

   the main monad of astranaut_traverse.

astranaut_base_m

  a monad with errors and warnings.
  you could just append errors or warnings to it.

  1. astranaut_base_m:then(
  2. astranaut_base_m:warning(warning_0),
  3. astranaut_base_m:return(ok)).

astranaut_return_m

  the monad result of astranaut_traverse_m:run(MA, Formatter, State).
  could be transformed to compiler return format with astranaut_return_m:to_compiler/1.
  could transforme compiler return format to astranaut_return_m with astranaut_return_m:from_compiler/1.

astranaut_error_state

astranaut_walk_return

   return type of Fun in astranut_traverse:(map_m|map|reduce|map_with_state|mapfold|)(Fun, Forms, Opts).

Quote

quick start

with

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

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

  1. quote(Code) | quote(Code, Options)

Options

  1. atom() => {atom() => true}
  2. proplists() => map(),
  3. Pos => #{pos => Pos}
  4. #{pos => Pos, code_pos => CodePos, debug => Debug}.

Pos

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

  1. quote(
  2. fun(_) ->
  3. ok
  4. end, 10).
  5. =>
  6. astranaut:replace_pos_zero(quote(fun(_) -> ok end), 10).
  7. =>
  8. {'fun', 10, {clauses, [{clause, 10, [{var, 10, '_'}], [], [{atom, 10, ok}]}]}}.

CodePos

   if CodePos is true

  1. 10: quote(
  2. 11: fun(_) ->
  3. 12: ok
  4. 13: end, code_pos).
  5. =>
  6. {'fun', {11, 2}, {clauses, [{clause, {11,5}, [{var, {11,5}, '_'}], [], [{atom, {12, 3}, ok}]}]}}.

Debug

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

unquote

  1. unquote(Ast)
  2. unquote = Ast.
  3. unquote_splicing(Asts)
  4. unquote_splicing = Asts.

why two forms

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

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

variable binding

bind one ast

  _@V, same as unquote(V)

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

bind a list of ast

  _L@Vs,same as unquote_splicing(Vs)

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

bind a value

  1. Atom = hello,
  2. Integer = 10,
  3. Float = 1.3,
  4. String = "123",
  5. Variable = 'Var',
  6. _A@Atom => {atom, 0, Atom} => {atom, 0, hello}
  7. _I@Integer => {integer, 0, Integer} => {integer, 0, 10}
  8. _F@Float => {float, 0, Float} => {float, 0, 1.3}
  9. _S@String => {string, 0, String} => {string, 0, "123"}
  10. _V@Variable => {var, 0, Variable} => {var, 0, 'Var'}

why binding

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

  1. Class = 'Class0',
  2. Exception = 'Exception0',
  3. StackTrace = 'StackTrace0',
  4. quote(
  5. try
  6. throw(hello)
  7. catch
  8. _V@Class:_V@Exception:_V@StackTrace ->
  9. erlang:raise(_V@Class, _V@Exception, _V@StackTrace)
  10. end).

  it’s illegal

  1. Class = {var, 0, 'Class0'},
  2. Exception = {var, 0, 'Exception0'},
  3. StackTrace = {var, 0, 'StackTrace0'},
  4. quote(
  5. try
  6. A
  7. catch
  8. unquote(Class):unquote(Exception):unquote(StackTrace) ->
  9. erlang:raise(_@Class, _@Exception, _@StackTrace)
  10. 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

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

  function pattern

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

  case clause pattern:

  1. case Ast of
  2. quote(_A@Atom) ->
  3. Atom;
  4. _ ->
  5. other
  6. end.
  7. =>
  8. case ast of
  9. {atom, _, Atom} ->
  10. Atom;
  11. _ ->
  12. other
  13. end.

Macro

Usage

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

macro.hrl add three attribute: use_macro, exec_macro debug_macro

use_macro

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

exec_macro

  execute macro and add result to current ast.

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

export_macro

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

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

debug_macro

  1. -debug_macro(true).

   module will be printed to console after astranaut_macro transform.

opts()

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

   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.

  1. -module(a).
  2. -behaviour(gen_server).
  3. -use_macro({macro/2, [{attrs, [module, pos, behaviour]}]}).
  4. hello() ->
  5. macro_a:macro(world).
  6. macro(Ast, #{module => Module, pos => Pos, behaviour => Behaviours} = Attributes) ->
  7. {warning, Ast, {attributes, Module, Pos, 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

  1. -use_macro({a, [group_args]}).
  2. test() ->
  3. a(hello, world).
  4. a(Asts) ->
  5. 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().

  1. -use_macro({macro_1/1, []}).
  2. -use_macro({macro_2/1, []}).
  3. -export([test/0]).
  4. test() ->
  5. macro_1(hello()).
  6. macro_1(Ast) ->
  7. quote(
  8. fun() -> unquote(Ast) end
  9. ).
  10. -exec_macro({macro_2, [hello]}).
  11. macro_2(Name) ->
  12. astranaut:function(
  13. Name,
  14. quote(
  15. fun() ->
  16. unquote_atom(Name)
  17. end)).

=>

  1. -use_macro({macro_1/1, []}).
  2. -export([test/0]).
  3. -export([hello/0]).
  4. test_macro_1() ->
  5. fun() -> hello() end.
  6. macro_1(Ast) ->
  7. quote(
  8. fun() -> unquote(Ast) end
  9. ).
  10. hello() ->
  11. hello.
  12. macro_2(Name) ->
  13. astranaut:function(
  14. Name,
  15. quote(
  16. fun() ->
  17. unquote_atom(Name)
  18. end)).

hygienic macro

   each macro expansion has it’s unique namespace.

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

  1. -module(macro_example).
  2. macro_with_vars_1(Ast) ->
  3. quote(
  4. begin
  5. A = 10,
  6. B = unquote(Ast),
  7. A + B
  8. end
  9. ).
  10. macro_with_vars_2(Ast) ->
  11. quote(
  12. begin
  13. A = 10,
  14. B = unquote(Ast),
  15. A + B
  16. end
  17. ).
  1. test_macro_with_vars(N) ->
  2. A1 = macro_with_vars_1(N),
  3. A2 = macro_with_vars_2(A1),
  4. A3 = macro_with_vars_2(N),
  5. A4 = macro_with_vars_1(A1),
  6. A1 + A2.

=>

  1. test_macro_with_vars(N) ->
  2. A1 =
  3. begin
  4. A@macro_example@_1 = 10,
  5. B@macro_example@_1 = N,
  6. A@macro_example@_1 + B@macro_example@_1
  7. end,
  8. A2 =
  9. begin
  10. A@macro_example@_3 = 10,
  11. B@macro_example@_3 = A1,
  12. A@macro_example@_3 + B@macro_example@_3
  13. end,
  14. A3 =
  15. begin
  16. A@macro_example@_4 = 10,
  17. B@macro_example@_4 = N,
  18. A@macro_example@_4 + B@macro_example@_4
  19. end,
  20. A4 =
  21. begin
  22. A@macro_example@_2 = 10,
  23. B@macro_example@_2 = A1,
  24. A@macro_example@_2 + B@macro_example@_2
  25. end,
  26. A1 + A2 + A3 + A4.

parse_transform

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

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

   example:

  1. -module(do).
  2. -include_lib("astranaut/include/quote.hrl").
  3. -export([parse_transform/2]).
  4. parse_transform(Forms, _Options) ->
  5. astranaut_macro:transform_macro(do_macro, do, 1, [{alias, do}, formatter], Forms).

Rebinding

  1. -include_lib("erlando/include/rebinding.hrl").
  2. -rebinding_all(Opts).
  3. -rebinding_fun(FAs).
  4. -rebinding_fun({FAs, Opts}).
  5. FAs = FA | [FA...].
  6. FA = F | F/A.
  7. Opts = Opt | [Opt...] | #{OptKey => OptValue}.
  8. Opt = OptKey | {OptKey, OptValue}.
  9. #{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

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

=>

  1. hello(A, A, B) ->
  2. {A_1, A_1, B_1} = {A + 1, A + 1, B + 1},
  3. {A_1, A_1, B_1}.
  1. hello(A, B) ->
  2. A =
  3. case A of
  4. B ->
  5. B = A + B,
  6. A = A + B,
  7. B = A + B,
  8. B;
  9. A ->
  10. B = A + B
  11. B
  12. end,
  13. B =
  14. case A of
  15. B ->
  16. B = A + B,
  17. A = A + B,
  18. B = A + B,
  19. B;
  20. A ->
  21. B = A + B
  22. B
  23. end,
  24. {A, B}.

=>

  1. hello(A, B) ->
  2. A_2 =
  3. case A of
  4. B ->
  5. B_1 = A + B,
  6. A_1 = A + B_1,
  7. B_2 = A_1 + B_1,
  8. B_2;
  9. A ->
  10. B_1 = A + B
  11. B_1
  12. end,
  13. B_5 =
  14. case A_2 of
  15. B ->
  16. %% B_1 and B_2 is already used, next var name is B_3, last var name in scope is B.
  17. B_3 = A_2 + B,
  18. A_3 = A_2 + B_3,
  19. B_4 = A_3 + B_3,
  20. B_4,
  21. A_2 ->
  22. B_3 = A_2 + B
  23. B_3
  24. end,
  25. {A_2, B_5}.
  1. hello_f(A) ->
  2. A = A + 1,
  3. F = fun F (0) -> 0; F (A) -> A = F(A - 1), A end,
  4. A = F(A),
  5. A.

=>

  1. hello_f(A) ->
  2. A_1 = A + 1,
  3. F = fun F(0) -> 0; F(A_2) -> A_3 = F(A_2 - 1), A_3 end,
  4. A_2 = F(A_1),
  5. F_1 = fun F_1(0) -> 0; F_1(A_3) -> A_4 = F_1(A_3 - 1), A_4 end,
  6. A_3 = F_1(A_2),
  7. A_3.

Struct

Usage

  1. -include_lib("erlando/include/struct.hrl").
  2. -record(test, {name = hello, value}).
  3. -astranaut_struct([test]).
  4. -export([new/0, update_name/2]).
  5. new() ->
  6. #test{}.
  7. update_name(Name, #test{} = Test) ->
  8. Test#test{name = Name}.

Desc

   convert erlang record to elixir like struct
   code above is converted to code below

  1. -include_lib("erlando/include/struct.hrl").
  2. -record(test, {name = hello, value}).
  3. -astranaut_struct([test]).
  4. -export([new/0, update_name/2]).
  5. new() ->
  6. #{'__struct__' => test, name => hello, value => undefined}.
  7. update_name(Name, #{'__struct__' := test} = Test) ->
  8. Test#{name => Name}.

Struct Options

   -astranaut_struct could have extra options:
   non_auto_fill : means fields will not set default to undefined when not defined and initialized.
   enforce_keys : means compile will failed when field is not setted when construct struct, works like elixir.

  1. -astranaut_struct({test, [non_auto_fill, {enforce_keys, [name]}]}).
  2. test_failed() ->
  3. #test{}.
  4. %% compile failed
  5. %% the following keys must also be given when building struct test: [name]
  6. test_non_auto_fill() ->
  7. #test{name = test}.
  8. %% ==>
  9. test_non_auto_fill_transformed() ->
  10. #{'__struct__' => test, name => test}. %% value is not set to undefined
  11. test_auto_fill() ->
  12. #test{name = test}.
  13. %% ==>
  14. test_auto_fill_transformed() ->
  15. #{'__struct__' => test, name => test, value => undefined}. %% value is set to undefined at default.

Macros

  1. astranaut_struct:from_record(StructName, Record) -> Struct. %% convert a recrod to struct with same name.
  2. astranaut_struct:to_record(StructName, Struct) -> Record. %% convert a struct to record with same name.
  3. astranaut_struct:from_map(StructName, Struct) -> Struct. %% build a struct from map, enforce_keys will be checked.
  4. astranaut_struct:update(StructName, Struct) -> Struct. %% update a struct from it's old version.