erlang R19 or higher
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(SA) :: SA | {error, error()} | {error, SA, error()} |
{warning, SA, error()} | {warning, error()} |
continue | {continue, SA} |
astranaut_walk_return:astranaut_walk_return(A) |
astranaut_traverse_m:astranaut_traverse_m(S, A) |
astranaut_return_m:astranaut_return_m(A) |
astranaut_base_m:astranaut_base_m(A).
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()
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
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: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:deep_sequence_m(PatternMs),
%% do something special to PatternsM monad.
PatternsM1 = do_something_special(PatternsM),
%% deep_sequence_m the new tree.
astranaut_traverse: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: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() :: [{Pos :: pos(), 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).
the main monad of astranaut_traverse.
a monad with errors and warnings.
you could just append errors or warnings to it.
astranaut_base_m:then(
astranaut_base_m:warning(warning_0),
astranaut_base_m:return(ok)).
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.
return type of Fun in astranut_traverse:(map_m|map|reduce|map_with_state|mapfold|)(Fun, Forms, Opts).
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(),
Pos => #{pos => Pos}
#{pos => Pos, code_pos => CodePos, debug => Debug}.
Pos
Pos could be any expression, the ast will be transformed.
quote(
fun(_) ->
ok
end, 10).
=>
astranaut:replace_pos_zero(quote(fun(_) -> ok end), 10).
=>
{'fun', 10, {clauses, [{clause, 10, [{var, 10, '_'}], [], [{atom, 10, ok}]}]}}.
CodePos
if CodePos is true
10: quote(
11: fun(_) ->
12: ok
13: end, code_pos).
=>
{'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(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).
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
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.
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, pos, behaviour]}]}).
hello() ->
macro_a:macro(world).
macro(Ast, #{module => Module, pos => Pos, behaviour => Behaviours} = Attributes) ->
{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
-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).
-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.
Usage
-include_lib("erlando/include/struct.hrl").
-record(test, {name = hello, value}).
-astranaut_struct([test]).
-export([new/0, update_name/2]).
new() ->
#test{}.
update_name(Name, #test{} = Test) ->
Test#test{name = Name}.
Desc
convert erlang record to elixir like struct
code above is converted to code below
-include_lib("erlando/include/struct.hrl").
-record(test, {name = hello, value}).
-astranaut_struct([test]).
-export([new/0, update_name/2]).
new() ->
#{'__struct__' => test, name => hello, value => undefined}.
update_name(Name, #{'__struct__' := test} = Test) ->
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.
-astranaut_struct({test, [non_auto_fill, {enforce_keys, [name]}]}).
test_failed() ->
#test{}.
%% compile failed
%% the following keys must also be given when building struct test: [name]
test_non_auto_fill() ->
#test{name = test}.
%% ==>
test_non_auto_fill_transformed() ->
#{'__struct__' => test, name => test}. %% value is not set to undefined
test_auto_fill() ->
#test{name = test}.
%% ==>
test_auto_fill_transformed() ->
#{'__struct__' => test, name => test, value => undefined}. %% value is set to undefined at default.
Macros
astranaut_struct:from_record(StructName, Record) -> Struct. %% convert a recrod to struct with same name.
astranaut_struct:to_record(StructName, Struct) -> Record. %% convert a struct to record with same name.
astranaut_struct:from_map(StructName, Struct) -> Struct. %% build a struct from map, enforce_keys will be checked.
astranaut_struct:update(StructName, Struct) -> Struct. %% update a struct from it's old version.