Skip to content

Repository files navigation

Build Status

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 => Role :: node_role(),
              validator => Validator :: term(),
              attribute => Attribute :: atom()}.

Step

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

  step()  :: pre | post | leaf. 

NodeRole

  traversal role of current node. It is not the Erlang AST type.

  node_role() :: form | expression | pattern | guard | type | clause. 

  Use astranaut_syntax:type/1 when you need the concrete Erlang AST type.

Validator

  validator is an opaque position token propagated by traversal. Normal users should not inspect or construct it. If a walker needs to validate a replacement node manually, pass Attr.validator unchanged to astranaut_syntax.

case astranaut_syntax:validate_node(NewNode, Validator) of
    ok -> NewNode;
    {error, Detail} -> {error, Detail}
end.

  For recursive normalization, pass the attr as context:

case astranaut_syntax:normalize(NewNode, Validator, #{attr => Attr}) of
    {ok, NewNode1} -> NewNode1;
    {error, Detail} -> {error, Detail}
end.

  Traversal may include extra diagnostic metadata in attrs. The stable way to validate a replacement is to pass the opaque validator token unchanged.

Attribute

  if node is related to an attribute body, attribute is the attribute name.

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(),
                 normalize => Normalize :: boolean(),
                 role => Role :: node_role(),
                 parse_transform => ParseTransform :: boolean(),
                 formatter => Formatter :: module(),
                 attr => Attr :: map(),
                 uniplate => Uniplate :: fun()}.

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.

Role

  role => Role explicitly sets the root traversal role. Use it when the root node is not a complete form or when the root role cannot be inferred safely.

astranaut:smap(Fun, Expr, #{traverse => pre, role => expression}).

Normalize

  if normalize => true, traversal validates or normalizes the node directly returned by the walker before putting it back into the current position. This option only applies to the direct return value of the walker. A parent node rebuilt because a child changed is not treated as a direct replacement of that parent.

TraverseStyle

  pre | post | all | leaf | subtree | none.

Attr

  initial attrs merged into traversal context.

Uniplate

  advanced traversal implementation hook. Most users do not need this option.

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

astranaut_syntax

validation and normalization

  astranaut_syntax:validate_node(NodeOrNodes, Validator) -> ok | {error, map()}.
  astranaut_syntax:validate_node(NodeOrNodes, Validator, Opts) -> ok | {error, map()}.

  astranaut_syntax:normalize(NodeOrNodes, Validator) ->
    {ok, NodeOrNodes1} | {error, map()}.

  astranaut_syntax:normalize(NodeOrNodes, Validator, Opts) ->
    {ok, NodeOrNodes1} | {error, map()}.

Validator

  Validator is usually copied from Attr.validator. It is an internal position contract and should be treated as an opaque token.

validate_node

  validates the current node against the provided validator. It does not recursively validate child nodes.

normalize

  validates the current node, recursively normalizes its children, rebuilds the AST with subtrees/1 and update_tree/2, and returns the normalized node.

Forms

  guard validation may require record definitions. Attr.validator does not contain original forms, and traversal callbacks may not receive the original module forms. If guard validation depends on records, pass proper record forms explicitly:

astranaut_syntax:validate_node(Node, Validator, #{forms => RecordForms}).
astranaut_syntax:normalize(Node, Validator, #{forms => RecordForms}).

helper api

  astranaut_syntax:child_specs(Type, Subtrees, Attr) -> [child_spec()].
  astranaut_syntax:node_roles(Type) -> [node_role()].
  astranaut_syntax:otp_vsn() -> integer() | 'pre-21'.

  astranaut_syntax:type(Node) -> Type.
  astranaut_syntax:get_pos(Node) -> Pos.
  astranaut_syntax:set_pos(Node, Pos) -> Node1.
  astranaut_syntax:subtrees(Node) -> Subtrees.
  astranaut_syntax:update_tree(Node, Subtrees) -> Node1.
  astranaut_syntax:revert(Node) -> Node1.

  child_specs/3 is an advanced API used by traversal and normalization. It may contain internal validator data; user code should normally keep that data opaque.

astranaut_forms

astranaut_forms owns form ordering, generated-form insertion, and __original__ function merging:

  astranaut_forms:reorder_updated_forms(Forms) -> Forms1.
  astranaut_forms:sort_forms(Forms) -> Forms1.
  astranaut_forms:insert_forms(NewForms, Forms) -> Forms1.

The corresponding functions in astranaut_syntax remain compatibility proxies. New code should call astranaut_forms directly.

astranaut:map_m/3 preserves the input list order. Use astranaut:map_m_forms/3 when a traversal intentionally inserts or replaces module forms and requires one final reorder_updated_forms pass, including __original__ merging.

astranaut_lib

astranaut_lib is the shared toolkit used both by callers and Astranaut's own modules for common AST and parse-transform workflows:

  astranaut_lib:abstract_form(Term) -> Ast.
  astranaut_lib:replace_pos(Ast, Pos) -> Ast1.
  astranaut_lib:gen_function(Name, Body) -> FunctionForm.
  astranaut_lib:analyze_forms_module(Forms) -> Module.
  astranaut_lib:validate(Validator, Options) -> Return.
  astranaut_lib:compile_forms(Forms, CompileOpts) -> Return.
  astranaut_lib:load_forms(Forms, CompileOpts) -> Return.
  astranaut_lib:reload_forms(Forms, CompileOpts) -> Return.
  astranaut_lib:with_module_lock(Module, Fun) -> Result.
  astranaut_lib:reload_binary(Module, Binary) -> Result.

reload_forms/2 serializes replacements per module and uses code:soft_purge/1. It returns an Astranaut error containing {module_in_use, Module} rather than purging code that is still executing. with_module_lock/2 and reload_binary/2 expose the same lower-level operations for internal code and callers that already have a compiled binary.

astranaut_uniplate

  astranaut_uniplate is the internal uniplate/context implementation used by traversal. Most users should use astranaut, astranaut_traverse, and astranaut_syntax instead of depending on its internal context shape.

monad modules

astranaut_traverse

astranaut_traverse is the traversal monad used by astranaut:map_m/3. It combines reader attributes, traversal state, update tracking, and structured errors and warnings.

astranaut_return

astranaut_return is the result monad used by traversal and parse-transform helpers. to_compiler/1 converts it to compiler return format and from_compiler/1 converts compiler results back.

astranaut_error

astranaut_error stores pending, formatted, and per-file errors and warnings.

astranaut_monad

astranaut_monad provides the lower-level identity, maybe, either, reader, state, and writer combinators used to assemble the traversal implementation.

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(),
  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_lib: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

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 enables the astranaut macro parse transform.

Internally, astranaut_macro orchestrates the attribute and function passes, astranaut_macro_scan owns the source-ordered scan-and-splice state, astranaut_macro_registry owns macro declarations and point-in-time macro environments, astranaut_macro_expander owns shared attribute/function macro matching and recursive expansion, and astranaut_macro_local owns local closure and generation lifecycle. All expansion paths use the same expander.

export_macro

  used in the module where macros are defined. Exported macros can be imported by other modules. This declaration alone does not make the function a local macro in its defining module: an unqualified call there remains an ordinary Erlang function call. Combine it with -local_macro on the same FA when both local and exported macro behaviour are required.

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

local_macro

  declare local functions as macros without exporting them. The transformer freezes each declared function and its statically discovered local function closure. A declaration containing several functions gives those functions the same declaration-time macro environment; calls between members of that declaration remain ordinary Erlang calls. The declaration is only a shared registration input: after the scan, each Function/Arity has its own macro entry and no persistent group identity is retained.

Each parse-transform invocation compiles its local macros into a uniquely named temporary module. Concurrent compilations of the same source module therefore do not share local macro code or require source-module locking.

-local_macro([MacroA/A, MacroB/B]).
-local_macro({Macro/A, local_macro_opts()}).
-local_macro({[MacroA/A, MacroB/B], local_macro_opts()}).

Local macro closure forms are expanded using the macro environment and injectable attributes that appear before the -local_macro declaration. Functions later in the source may be discovered as closure helpers, but later imports, uses, options, and attributes do not change that declaration-time environment.

Static closure discovery follows direct local call expressions such as helper(Arg). It does not infer indirect references such as fun helper/1, dynamically selected functions, or apply/3; add those helpers explicitly with closure_roots.

local_macro_retain

  retain a local macro function or helper in the transformed module. Retaining any member of a frozen local macro closure retains that closure's functions and specs. -export and -export_macro are also retain roots. Applying local_macro_retain to an ordinary function outside every local macro closure has no extra effect and produces an ineffective_local_macro_retain warning. Referring to an FA that is not defined in the module instead produces undefined_local_macro_retain.

A retained frozen function is both part of its local macro definition and a function in the final module. It is therefore expanded again from its original form under the final function context and checked against the canonical result established from its declaration context. A mismatch is an intentional conflict rather than a choice of one context over the other.

-local_macro_retain([Macro/A, Helper/B]).

import_macro

  declare a module that exports macros. Macro selection and call options should be configured with -use_macro.

-import_macro(Module).

use_macro

  use an imported or local macro with extra call options.

-use_macro({Macro/A, use_macro_opts()}).
-use_macro({[MacroA/A, MacroB/B], use_macro_opts()}).
-use_macro({Module, Macro/A, use_macro_opts()}).
-use_macro({Module, [MacroA/A, MacroB/B], use_macro_opts()}).

exec_macro

  execute macro and add result to current ast.

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

macro_options

  declare source-ordered module-level macro defaults and final module debug switches.

-macro_options(macro_options_opts()).

debug, debug_ast, and max_depth are per-macro defaults. They are copied into every external macro imported and every local macro declared after the -macro_options form. They do not become arguments to the macro function, and they do not retroactively change macros already imported or declared. Definition-level max_depth overrides the global default; use_macro can override debug and debug_ast for the selected macro.

debug_module and debug_module_ast are module-only switches. Their final source-ordered values print the complete transformed module after expansion; they are not per-macro settings.

Option maps

Options are scoped by attribute; there is no single option map accepted by all four attributes.

macro_options_opts() ::
  #{debug => boolean(),
    debug_ast => boolean(),
    debug_module => boolean(),
    debug_module_ast => boolean(),
    max_depth => non_neg_integer()}.

macro_definition_opts() ::
  #{order => Order,
    inject_attrs => InjectAttrs,
    as_attr => AsAttr,
    group_args => GroupArgs,
    force_override => ForceOverride,
    max_depth => MaxDepth}.

local_macro_opts() ::
  #{order => Order,
    inject_attrs => InjectAttrs,
    as_attr => AsAttr,
    group_args => GroupArgs,
    force_override => ForceOverride,
    max_depth => MaxDepth,
    closure_roots => [Function/Arity]}.

use_macro_opts() ::
  #{debug => boolean(),
    debug_ast => boolean(),
    alias => atom(),
    force_override => boolean()}.

Each option map may also be written as a proplist.

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

InjectAttrs

   module attributes as extra args while calling macro. Attribute macros see only attributes that have already passed the source-ordered attribute scan at their call site. Local macro closure forms use the attributes visible before their -local_macro declaration. Ordinary function macros are expanded after the attribute scan and use the final module attributes. At each of these boundaries the selector is resolved into the macro environment before recursive expansion starts.

-module(a).
-behaviour(gen_server).
-local_macro({macro/2, [{inject_attrs, [module, behaviour]}]}).

hello() ->
  macro(world).

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

Order

   macro expand order for nested macros. inner is default and expands inside before outside. outer expands outside before inside.

AsAttr

   user defined attribute name replace of -exec_macro.

GroupArgs

   treat macro arguments as list

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

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

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

ForceOverride

   allow an intentional macro alias override. Without this option, conflicting macro names fail with macro_override.

MaxDepth

   maximum nested macro expansion chain depth. The default module-level value is 100.

ClosureRoots

   explicitly add local functions to a local macro's statically discovered closure. Use this when a helper cannot be discovered from ordinary local calls in the macro function. Each entry must be Function/Arity and must exist in the module.

-local_macro({macro/1, [{closure_roots, [helper/1]}]}).

When a macro implementation needs to invoke a function that is also exported as a macro, call it through a distinct helper that is not registered in the macro environment, or use ordinary Erlang indirect invocation such as erlang:apply/3 or a function value. Direct calls matching the macro environment are always macro calls.

Option Scope

Option macro_options export_macro local_macro use_macro Role
debug, debug_ast global default per-use override print each macro result as source or AST
debug_module, debug_module_ast module only print the final transformed module
max_depth global default definition override definition override maximum nested expansion depth
order definition definition nested expansion order
as_attr definition definition expose a macro as an attribute
inject_attrs definition definition append selected module attributes to invocation arguments
group_args definition definition pass source arguments as one list
force_override definition definition per-use permit this incoming macro mapping to replace a conflicting key
alias use only rename the selected macro at its use site
closure_roots local definition only add statically undiscovered roots to local closure discovery

closure_roots describes local macro closure construction. It is relevant only when the definition is declared with -local_macro; an -export_macro declaration alone only publishes the macro for importing modules. It is not a module-level -macro_options setting. Supplying it to -macro_options or -export_macro reports it as an unexpected option and ignores it.

Expansion Phases and Source Order

Macro expansion has an ordered attribute pass followed by a function-body pass:

  1. External and callable local attribute macros are scanned together from left to right. Their generated forms are inserted at the current queue position and scanned in order before the remaining source forms.
  2. import_macro, use_macro, macro_options, and generated forms of those kinds affect only forms scanned after them. Already processed attributes are not revisited.
  3. A generated local_macro declaration enters the same scan and can make its macro available to later attribute calls. An attribute call only invokes the canonical definition captured at that declaration; it never expands or redefines the local macro at the call site. Multiple attribute calls reuse the same definition and callable generation.
  4. After the attribute pass and local macro finalization, ordinary function bodies are recursively expanded with the completed macro environment. An ordinary function can therefore use all callable final local macros, while frozen local macro closure forms preserve their declaration-time local macro dependencies. Retained frozen functions also run through this final context and must reproduce their declaration-context canonical result.

This source ordering is separate from the order option: source order controls when macro declarations and environment updates become visible, while inner or outer controls nested expansion at an individual macro call.

Errors

Error Meaning
macro_override macro name or alias already exists and force_override was not set
max_macro_expansion_depth_exceeded nested macro expansion exceeded max_depth
invalid_macro_return macro returned AST that does not fit the current traversal position
invalid_import_macro_attr invalid -import_macro attribute
import_macro_failed imported macro module could not be loaded
invalid_closure_roots closure_roots names functions that are not defined in the module
duplicate_local_macro_declaration a local macro FA was declared more than once, including duplicates in one declaration
conflicting_local_macro_closure_environment a retained or reused local macro closure expands differently under another required environment
conflicting_local_macro_whitelist repeated expansion of a frozen closure observes a different set of local macro dependencies
illegal_locked_form_mutation attribute expansion attempted to replace a frozen local macro closure form
illegal_macro_environment_mutation local macro expansion generated a macro-environment form where environment mutation is not allowed
illegal_local_macro_definition_mutation local macro expansion attempted to change a locked local macro definition

Warnings

Warning Meaning
undefined_local_macro_retain an explicitly retained FA is not defined in the module
ineffective_local_macro_retain an explicitly retained FA belongs to no local macro closure and is therefore handled only as an ordinary function

  define macro as normal erlang functions.
  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_lib:gen_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_lib:gen_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.

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.

Struct

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.

About

traverse erlang ast and elixir macro in erlang.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages