2860 lines
86 KiB
Elixir
2860 lines
86 KiB
Elixir
import Kernel, except: [to_string: 1]
|
|
|
|
defmodule Macro do
|
|
@moduledoc ~S"""
|
|
Functions for manipulating AST and implementing macros.
|
|
|
|
Macros are compile-time constructs that receive Elixir's AST as input
|
|
and return Elixir's AST as output.
|
|
|
|
Many of the functions in this module exist precisely to work with Elixir
|
|
AST, to traverse, query, and transform it.
|
|
|
|
Let's see a simple example that shows the difference between functions
|
|
and macros:
|
|
|
|
defmodule Example do
|
|
defmacro macro_inspect(value) do
|
|
IO.inspect(value)
|
|
value
|
|
end
|
|
|
|
def fun_inspect(value) do
|
|
IO.inspect(value)
|
|
value
|
|
end
|
|
end
|
|
|
|
Now let's give it a try:
|
|
|
|
import Example
|
|
|
|
macro_inspect(1)
|
|
#=> 1
|
|
#=> 1
|
|
|
|
fun_inspect(1)
|
|
#=> 1
|
|
#=> 1
|
|
|
|
So far they behave the same, as we are passing an integer as argument.
|
|
But let's see what happens when we pass an expression:
|
|
|
|
macro_inspect(1 + 2)
|
|
#=> {:+, [line: 3], [1, 2]}
|
|
#=> 3
|
|
|
|
fun_inspect(1 + 2)
|
|
#=> 3
|
|
#=> 3
|
|
|
|
The macro receives the representation of the code given as argument,
|
|
while a function receives the result of the code given as argument.
|
|
A macro must return a superset of the code representation. See
|
|
`t:input/0` and `t:output/0` for more information.
|
|
|
|
To learn more about Elixir's AST and how to build them programmatically,
|
|
see `quote/2`.
|
|
|
|
> #### Evaluating code {: .tip}
|
|
>
|
|
> The functions in this module do not evaluate code. In fact,
|
|
> evaluating code from macros is often an anti-pattern. For code
|
|
> evaluation, see the `Code` module.
|
|
|
|
## Custom Sigils
|
|
|
|
Macros are also commonly used to implement custom sigils.
|
|
|
|
Sigils start with `~` and are followed by one lowercase letter or by one
|
|
or more uppercase letters, and then a separator
|
|
(see the [Syntax Reference](syntax-reference.md)). One example is
|
|
`~D[2020-10-13]` to define a date.
|
|
|
|
To create a custom sigil, define a macro with the name `sigil_{identifier}`
|
|
that takes two arguments. The first argument will be the string, the second
|
|
will be a charlist containing any modifiers. If the sigil is lower case
|
|
(such as `sigil_x`) then the string argument will allow interpolation.
|
|
If the sigil is one or more upper case letters (such as `sigil_X` and
|
|
`sigil_EXAMPLE`) then the string will not be interpolated.
|
|
|
|
Valid modifiers are ASCII letters and digits. Any other character will
|
|
cause a syntax error.
|
|
|
|
Single-letter sigils are typically reserved to the language. Multi-letter
|
|
sigils are uppercased and extensively used by the community to embed
|
|
alternative markups and data-types within Elixir source code.
|
|
|
|
The module containing the custom sigil must be imported before the sigil
|
|
syntax can be used.
|
|
|
|
### Examples
|
|
|
|
As an example, let's define a sigil `~x` and sigil `~X` which
|
|
return its contents as a string. However, if the `r` modifier
|
|
is given, it reverses the string instead:
|
|
|
|
defmodule MySigils do
|
|
defmacro sigil_x(term, [?r]) do
|
|
quote do
|
|
unquote(term) |> String.reverse()
|
|
end
|
|
end
|
|
|
|
defmacro sigil_x(term, _modifiers) do
|
|
term
|
|
end
|
|
|
|
defmacro sigil_X(term, [?r]) do
|
|
quote do
|
|
unquote(term) |> String.reverse()
|
|
end
|
|
end
|
|
|
|
defmacro sigil_X(term, _modifiers) do
|
|
term
|
|
end
|
|
end
|
|
|
|
import MySigils
|
|
|
|
~x(with #{"inter" <> "polation"})
|
|
#=> "with interpolation"
|
|
|
|
~x(with #{"inter" <> "polation"})r
|
|
#=> "noitalopretni htiw"
|
|
|
|
~X(without #{"interpolation"})
|
|
#=> "without \#{"interpolation"}"
|
|
|
|
~X(without #{"interpolation"})r
|
|
#=> "}\"noitalopretni\"{# tuohtiw"
|
|
|
|
"""
|
|
|
|
alias Code.Identifier
|
|
|
|
@typedoc "Abstract Syntax Tree (AST)"
|
|
@type t :: input
|
|
|
|
@typedoc "The inputs of a macro"
|
|
@type input ::
|
|
input_expr
|
|
| {input, input}
|
|
| [input]
|
|
| atom
|
|
| number
|
|
| binary
|
|
|
|
@typep input_expr :: {input_expr | atom, metadata, atom | [input]}
|
|
|
|
@typedoc "The output of a macro"
|
|
@type output ::
|
|
output_expr
|
|
| {output, output}
|
|
| [output]
|
|
| atom
|
|
| number
|
|
| binary
|
|
| captured_remote_function
|
|
| pid
|
|
|
|
@typep output_expr :: {output_expr | atom, metadata, atom | [output]}
|
|
|
|
@typedoc """
|
|
A keyword list of AST metadata.
|
|
|
|
The metadata in Elixir AST is a keyword list of values. Any key can be used
|
|
and different parts of the compiler may use different keys. For example,
|
|
the AST received by a macro will always include the `:line` annotation,
|
|
while the AST emitted by `quote/2` will only have the `:line` annotation if
|
|
the `:line` option is provided.
|
|
|
|
The following metadata keys are public:
|
|
|
|
* `:context` - Defines the context in which the AST was generated.
|
|
For example, `quote/2` will include the module calling `quote/2`
|
|
as the context. This is often used to distinguish regular code from code
|
|
generated by a macro or by `quote/2`.
|
|
|
|
* `:counter` - The variable counter used for variable hygiene. In terms of
|
|
the compiler, each variable is identified by the combination of either
|
|
`name` and `metadata[:counter]`, or `name` and `context`.
|
|
|
|
* `:generated` - Whether the code should be considered as generated by
|
|
the compiler or not. This means the compiler and tools like Dialyzer may not
|
|
emit certain warnings.
|
|
|
|
* `:if_undefined` - How to expand a variable that is undefined. Set it to
|
|
`:apply` if you want a variable to become a nullary call without warning
|
|
or `:raise`
|
|
|
|
* `:keep` - Used by `quote/2` with the option `location: :keep` to annotate
|
|
the file and the line number of the quoted source.
|
|
|
|
* `:line` - The line number of the AST node.
|
|
|
|
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
|
|
bracket syntax or a function call.
|
|
|
|
The following metadata keys are enabled by `Code.string_to_quoted/2`:
|
|
|
|
* `:closing` - contains metadata about the closing pair, such as a `}`
|
|
in a tuple or in a map, or such as the closing `)` in a function call
|
|
with parens. The `:closing` does not delimit the end of expression if
|
|
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
|
* `:column` - the column number of the AST node (when `:columns` is true)
|
|
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
|
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
|
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
|
* `:do` - contains metadata about the `do` location in a function call with
|
|
`do`-`end` blocks (when `:token_metadata` is true)
|
|
* `:end` - contains metadata about the `end` location in a function call with
|
|
`do`-`end` blocks (when `:token_metadata` is true)
|
|
* `:end_of_expression` - denotes when the end of expression effectively
|
|
happens. Available for all expressions except the last one inside a
|
|
`__block__` (when `:token_metadata` is true)
|
|
* `:indentation` - indentation of a sigil heredoc
|
|
|
|
The following metadata keys are private:
|
|
|
|
* `:alias` - Used for alias hygiene.
|
|
* `:ambiguous_op` - Used for improved error messages in the compiler.
|
|
* `:imports` - Used for import hygiene.
|
|
* `:var` - Used for improved error messages on undefined variables.
|
|
|
|
Do not rely on them as they may change or be fully removed in future versions
|
|
of the language. They are often used by `quote/2` and the compiler to provide
|
|
features like hygiene, better error messages, and so forth.
|
|
|
|
If you introduce custom keys into the AST metadata, please make sure to prefix
|
|
them with the name of your library or application, so that they will not conflict
|
|
with keys that could potentially be introduced by the compiler in the future.
|
|
"""
|
|
@type metadata :: keyword
|
|
|
|
@typedoc "A captured remote function in the format of &Mod.fun/arity"
|
|
@type captured_remote_function :: fun
|
|
|
|
@doc """
|
|
Breaks a pipeline expression into a list.
|
|
|
|
The AST for a pipeline (a sequence of applications of `|>/2`) is similar to the
|
|
AST of a sequence of binary operators or function applications: the top-level
|
|
expression is the right-most `:|>` (which is the last one to be executed), and
|
|
its left-hand and right-hand sides are its arguments:
|
|
|
|
quote do: 100 |> div(5) |> div(2)
|
|
#=> {:|>, _, [arg1, arg2]}
|
|
|
|
In the example above, the `|>/2` pipe is the right-most pipe; `arg1` is the AST
|
|
for `100 |> div(5)`, and `arg2` is the AST for `div(2)`.
|
|
|
|
It's often useful to have the AST for such a pipeline as a list of function
|
|
applications. This function does exactly that:
|
|
|
|
Macro.unpipe(quote do: 100 |> div(5) |> div(2))
|
|
#=> [{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]
|
|
|
|
We get a list that follows the pipeline directly: first the `100`, then the
|
|
`div(5)` (more precisely, its AST), then `div(2)`. The `0` as the second
|
|
element of the tuples is the position of the previous element in the pipeline
|
|
inside the current function application: `{{:div, [], [5]}, 0}` means that the
|
|
previous element (`100`) will be inserted as the 0th (first) argument to the
|
|
`div/2` function, so that the AST for that function will become `{:div, [],
|
|
[100, 5]}` (`div(100, 5)`).
|
|
"""
|
|
@spec unpipe(t()) :: [t()]
|
|
def unpipe(expr) do
|
|
:lists.reverse(unpipe(expr, []))
|
|
end
|
|
|
|
defp unpipe({:|>, _, [left, right]}, acc) do
|
|
unpipe(right, unpipe(left, acc))
|
|
end
|
|
|
|
defp unpipe(other, acc) do
|
|
[{other, 0} | acc]
|
|
end
|
|
|
|
@doc """
|
|
Pipes `expr` into the `call_args` at the given `position`.
|
|
|
|
This function can be used to implement `|>` like functionality. For example,
|
|
`|>` itself is implemented as:
|
|
|
|
defmacro left |> right do
|
|
Macro.pipe(left, right, 0)
|
|
end
|
|
|
|
`expr` is the AST of an expression. `call_args` must be the AST *of a call*,
|
|
otherwise this function will raise an error. As an example, consider the pipe
|
|
operator `|>/2`, which uses this function to build pipelines.
|
|
|
|
Even if the expression is piped into the AST, it doesn't necessarily mean that
|
|
the AST is valid. For example, you could pipe an argument to `div/2`, effectively
|
|
turning it into a call to `div/3`, which is a function that doesn't exist by
|
|
default. The code will raise unless a `div/3` function is locally defined.
|
|
"""
|
|
@spec pipe(t(), t(), integer) :: t()
|
|
def pipe(expr, call_args, position)
|
|
|
|
def pipe(expr, {:&, _, _} = call_args, _integer) do
|
|
raise ArgumentError, bad_pipe(expr, call_args)
|
|
end
|
|
|
|
def pipe(expr, {tuple_or_map, _, _} = call_args, _integer) when tuple_or_map in [:{}, :%{}] do
|
|
raise ArgumentError, bad_pipe(expr, call_args)
|
|
end
|
|
|
|
# Without this, `Macro |> Env == Macro.Env`.
|
|
def pipe(expr, {:__aliases__, _, _} = call_args, _integer) do
|
|
raise ArgumentError, bad_pipe(expr, call_args)
|
|
end
|
|
|
|
def pipe(expr, {:<<>>, _, _} = call_args, _integer) do
|
|
raise ArgumentError, bad_pipe(expr, call_args)
|
|
end
|
|
|
|
def pipe(expr, {unquote, _, []}, _integer) when unquote in [:unquote, :unquote_splicing] do
|
|
raise ArgumentError,
|
|
"cannot pipe #{to_string(expr)} into the special form #{unquote}/1 " <>
|
|
"since #{unquote}/1 is used to build the Elixir AST itself"
|
|
end
|
|
|
|
# {:fn, _, _} is what we get when we pipe into an anonymous function without
|
|
# calling it, for example, `:foo |> (fn x -> x end)`.
|
|
def pipe(expr, {:fn, _, _}, _integer) do
|
|
raise ArgumentError,
|
|
"cannot pipe #{to_string(expr)} into an anonymous function without" <>
|
|
" calling the function; use Kernel.then/2 instead or" <>
|
|
" define the anonymous function as a regular private function"
|
|
end
|
|
|
|
def pipe(expr, {call, line, atom}, integer) when is_atom(atom) do
|
|
{call, line, List.insert_at([], integer, expr)}
|
|
end
|
|
|
|
def pipe(_expr, {op, _line, [arg]}, _integer) when op == :+ or op == :- do
|
|
raise ArgumentError,
|
|
"piping into a unary operator is not supported, please use the qualified name: " <>
|
|
"Kernel.#{op}(#{to_string(arg)}), instead of #{op}#{to_string(arg)}"
|
|
end
|
|
|
|
# Piping to an Access.get/2,3 call in the form of brackets
|
|
# (foo |> bar[]) raises a nice error.
|
|
def pipe(
|
|
expr,
|
|
{{_, meta, [Access, :get] = op}, _meta, [first, second]} = _op_args,
|
|
integer
|
|
) do
|
|
if {:from_brackets, true} in meta do
|
|
raise ArgumentError, """
|
|
wrong operator precedence when piping into bracket-based access
|
|
|
|
Instead of:
|
|
|
|
#{to_string(expr)} |> #{to_string(first)}[#{to_string(second)}]
|
|
|
|
You should write:
|
|
|
|
(#{to_string(expr)} |> #{to_string(first)})[#{to_string(second)}]
|
|
"""
|
|
else
|
|
{op, meta, List.insert_at([first, second], integer, expr)}
|
|
end
|
|
end
|
|
|
|
def pipe(expr, {op, line, args} = op_args, integer) when is_list(args) do
|
|
cond do
|
|
is_atom(op) and operator?(op, 1) ->
|
|
raise ArgumentError,
|
|
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
|
|
"the #{to_string(op)} operator can only take one argument"
|
|
|
|
is_atom(op) and operator?(op, 2) ->
|
|
raise ArgumentError,
|
|
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
|
|
"the #{to_string(op)} operator can only take two arguments"
|
|
|
|
true ->
|
|
{op, line, List.insert_at(args, integer, expr)}
|
|
end
|
|
end
|
|
|
|
def pipe(expr, call_args, _integer) do
|
|
raise ArgumentError, bad_pipe(expr, call_args)
|
|
end
|
|
|
|
defp bad_pipe(expr, call_args) do
|
|
"cannot pipe #{to_string(expr)} into #{to_string(call_args)}, " <>
|
|
"can only pipe into local calls foo(), remote calls Foo.bar() or anonymous function calls foo.()"
|
|
end
|
|
|
|
@doc """
|
|
Applies the given function to the node metadata if it contains one.
|
|
|
|
This is often useful when used with `Macro.prewalk/2` to remove
|
|
information like lines and hygienic counters from the expression
|
|
for either storage or comparison.
|
|
|
|
## Examples
|
|
|
|
iex> quoted = quote line: 10, do: sample()
|
|
{:sample, [line: 10], []}
|
|
iex> Macro.update_meta(quoted, &Keyword.delete(&1, :line))
|
|
{:sample, [], []}
|
|
|
|
"""
|
|
@spec update_meta(t, (keyword -> keyword)) :: t
|
|
def update_meta(quoted, fun)
|
|
|
|
def update_meta({left, meta, right}, fun) when is_list(meta) do
|
|
{left, fun.(meta), right}
|
|
end
|
|
|
|
def update_meta(other, _fun) do
|
|
other
|
|
end
|
|
|
|
@doc """
|
|
Generates AST nodes for a given number of required argument
|
|
variables using `Macro.var/2`.
|
|
|
|
Note the arguments are not unique. If you later on want
|
|
to access the same variables, you can invoke this function
|
|
with the same inputs. Use `generate_unique_arguments/2` to
|
|
generate a unique arguments that can't be overridden.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.generate_arguments(2, __MODULE__)
|
|
[{:arg1, [], __MODULE__}, {:arg2, [], __MODULE__}]
|
|
|
|
"""
|
|
@doc since: "1.5.0"
|
|
@spec generate_arguments(0, context :: atom) :: []
|
|
@spec generate_arguments(pos_integer, context) :: [{atom, [], context}, ...] when context: atom
|
|
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
|
|
|
|
@doc """
|
|
Returns the path to the node in `ast` which `fun` returns `true`.
|
|
|
|
The path is a list, starting with the node in which `fun` returns
|
|
true, followed by all of its parents.
|
|
|
|
Computing the path can be an efficient operation when you want
|
|
to find a particular node in the AST within its context and then
|
|
assert something about it.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.path(quote(do: [1, 2, 3]), & &1 == 3)
|
|
[3, [1, 2, 3]]
|
|
|
|
iex> Macro.path(quote(do: Foo.bar(3)), & &1 == 3)
|
|
[3, quote(do: Foo.bar(3))]
|
|
|
|
iex> Macro.path(quote(do: %{foo: [bar: :baz]}), & &1 == :baz)
|
|
[
|
|
:baz,
|
|
{:bar, :baz},
|
|
[bar: :baz],
|
|
{:foo, [bar: :baz]},
|
|
{:%{}, [], [foo: [bar: :baz]]}
|
|
]
|
|
|
|
"""
|
|
@doc since: "1.14.0"
|
|
def path(ast, fun) when is_function(fun, 1) do
|
|
path(ast, [], fun)
|
|
end
|
|
|
|
defp path({form, _, args} = ast, acc, fun) when is_atom(form) do
|
|
acc = [ast | acc]
|
|
|
|
if fun.(ast) do
|
|
acc
|
|
else
|
|
path_args(args, acc, fun)
|
|
end
|
|
end
|
|
|
|
defp path({form, _meta, args} = ast, acc, fun) do
|
|
acc = [ast | acc]
|
|
|
|
if fun.(ast) do
|
|
acc
|
|
else
|
|
path(form, acc, fun) || path_args(args, acc, fun)
|
|
end
|
|
end
|
|
|
|
defp path({left, right} = ast, acc, fun) do
|
|
acc = [ast | acc]
|
|
|
|
if fun.(ast) do
|
|
acc
|
|
else
|
|
path(left, acc, fun) || path(right, acc, fun)
|
|
end
|
|
end
|
|
|
|
defp path(list, acc, fun) when is_list(list) do
|
|
acc = [list | acc]
|
|
|
|
if fun.(list) do
|
|
acc
|
|
else
|
|
path_list(list, acc, fun)
|
|
end
|
|
end
|
|
|
|
defp path(ast, acc, fun) do
|
|
if fun.(ast) do
|
|
[ast | acc]
|
|
end
|
|
end
|
|
|
|
defp path_args(atom, _acc, _fun) when is_atom(atom), do: nil
|
|
defp path_args(list, acc, fun) when is_list(list), do: path_list(list, acc, fun)
|
|
|
|
defp path_list([], _acc, _fun) do
|
|
nil
|
|
end
|
|
|
|
defp path_list([arg | args], acc, fun) do
|
|
path(arg, acc, fun) || path_list(args, acc, fun)
|
|
end
|
|
|
|
@doc """
|
|
Generates AST nodes for a given number of required argument
|
|
variables using `Macro.unique_var/2`.
|
|
|
|
## Examples
|
|
|
|
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
|
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
|
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
|
iex> is_integer(c1) and is_integer(c2)
|
|
true
|
|
|
|
"""
|
|
@doc since: "1.11.3"
|
|
@spec generate_unique_arguments(0, context :: atom) :: []
|
|
@spec generate_unique_arguments(pos_integer, context) :: [
|
|
{atom, [counter: integer], context},
|
|
...
|
|
]
|
|
when context: atom
|
|
def generate_unique_arguments(amount, context),
|
|
do: generate_arguments(amount, context, &unique_var/2)
|
|
|
|
defp generate_arguments(0, context, _fun) when is_atom(context), do: []
|
|
|
|
defp generate_arguments(amount, context, fun)
|
|
when is_integer(amount) and amount > 0 and is_atom(context) do
|
|
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
|
end
|
|
|
|
@doc """
|
|
Generates an AST node representing the variable given
|
|
by the atoms `var` and `context`.
|
|
|
|
Note this variable is not unique. If you later on want
|
|
to access this same variable, you can invoke `var/2`
|
|
again with the same arguments. Use `unique_var/2` to
|
|
generate a unique variable that can't be overridden.
|
|
|
|
## Examples
|
|
|
|
In order to build a variable, a context is expected.
|
|
Most of the times, in order to preserve hygiene, the
|
|
context must be `__MODULE__/0`:
|
|
|
|
iex> Macro.var(:foo, __MODULE__)
|
|
{:foo, [], __MODULE__}
|
|
|
|
However, if there is a need to access the user variable,
|
|
nil can be given:
|
|
|
|
iex> Macro.var(:foo, nil)
|
|
{:foo, [], nil}
|
|
|
|
"""
|
|
@spec var(var, context) :: {var, [], context} when var: atom, context: atom
|
|
def var(var, context) when is_atom(var) and is_atom(context) do
|
|
{var, [], context}
|
|
end
|
|
|
|
@doc """
|
|
Generates an AST node representing a unique variable
|
|
given by the atoms `var` and `context`.
|
|
|
|
Calling this function with the same arguments will
|
|
generate another variable, with its own unique counter.
|
|
See `var/2` for an alternative.
|
|
|
|
## Examples
|
|
|
|
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
|
iex> is_integer(c)
|
|
true
|
|
|
|
"""
|
|
@doc since: "1.11.3"
|
|
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
|
when var: atom, context: atom
|
|
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
|
{var, [counter: :elixir_module.next_counter(context)], context}
|
|
end
|
|
|
|
@doc """
|
|
Performs a depth-first traversal of quoted expressions
|
|
using an accumulator.
|
|
|
|
Returns a tuple where the first element is a new AST and the second one is
|
|
the final accumulator. The new AST is the result of invoking `pre` on each
|
|
node of `ast` during the pre-order phase and `post` during the post-order
|
|
phase.
|
|
|
|
## Examples
|
|
|
|
iex> ast = quote do: 5 + 3 * 7
|
|
iex> {:+, _, [5, {:*, _, [3, 7]}]} = ast
|
|
iex> {new_ast, acc} =
|
|
...> Macro.traverse(
|
|
...> ast,
|
|
...> [],
|
|
...> fn
|
|
...> {:+, meta, children}, acc -> {{:-, meta, children}, [:- | acc]}
|
|
...> {:*, meta, children}, acc -> {{:/, meta, children}, [:/ | acc]}
|
|
...> other, acc -> {other, acc}
|
|
...> end,
|
|
...> fn
|
|
...> {:-, meta, children}, acc -> {{:min, meta, children}, [:min | acc]}
|
|
...> {:/, meta, children}, acc -> {{:max, meta, children}, [:max | acc]}
|
|
...> other, acc -> {other, acc}
|
|
...> end
|
|
...> )
|
|
iex> {:min, _, [5, {:max, _, [3, 7]}]} = new_ast
|
|
iex> [:min, :max, :/, :-] = acc
|
|
iex> Code.eval_quoted(new_ast)
|
|
{5, []}
|
|
|
|
"""
|
|
@spec traverse(t, any, (t, any -> {t, any}), (t, any -> {t, any})) :: {t, any}
|
|
def traverse(ast, acc, pre, post) when is_function(pre, 2) and is_function(post, 2) do
|
|
{ast, acc} = pre.(ast, acc)
|
|
do_traverse(ast, acc, pre, post)
|
|
end
|
|
|
|
defp do_traverse({form, meta, args}, acc, pre, post) when is_atom(form) do
|
|
{args, acc} = do_traverse_args(args, acc, pre, post)
|
|
post.({form, meta, args}, acc)
|
|
end
|
|
|
|
defp do_traverse({form, meta, args}, acc, pre, post) do
|
|
{form, acc} = pre.(form, acc)
|
|
{form, acc} = do_traverse(form, acc, pre, post)
|
|
{args, acc} = do_traverse_args(args, acc, pre, post)
|
|
post.({form, meta, args}, acc)
|
|
end
|
|
|
|
defp do_traverse({left, right}, acc, pre, post) do
|
|
{left, acc} = pre.(left, acc)
|
|
{left, acc} = do_traverse(left, acc, pre, post)
|
|
{right, acc} = pre.(right, acc)
|
|
{right, acc} = do_traverse(right, acc, pre, post)
|
|
post.({left, right}, acc)
|
|
end
|
|
|
|
defp do_traverse(list, acc, pre, post) when is_list(list) do
|
|
{list, acc} = do_traverse_args(list, acc, pre, post)
|
|
post.(list, acc)
|
|
end
|
|
|
|
defp do_traverse(x, acc, _pre, post) do
|
|
post.(x, acc)
|
|
end
|
|
|
|
defp do_traverse_args(args, acc, _pre, _post) when is_atom(args) do
|
|
{args, acc}
|
|
end
|
|
|
|
defp do_traverse_args(args, acc, pre, post) when is_list(args) do
|
|
:lists.mapfoldl(
|
|
fn x, acc ->
|
|
{x, acc} = pre.(x, acc)
|
|
do_traverse(x, acc, pre, post)
|
|
end,
|
|
acc,
|
|
args
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Performs a depth-first, pre-order traversal of quoted expressions.
|
|
|
|
Returns a new AST where each node is the result of invoking `fun` on each
|
|
corresponding node of `ast`.
|
|
|
|
## Examples
|
|
|
|
iex> ast = quote do: 5 + 3 * 7
|
|
iex> {:+, _, [5, {:*, _, [3, 7]}]} = ast
|
|
iex> new_ast = Macro.prewalk(ast, fn
|
|
...> {:+, meta, children} -> {:*, meta, children}
|
|
...> {:*, meta, children} -> {:+, meta, children}
|
|
...> other -> other
|
|
...> end)
|
|
iex> {:*, _, [5, {:+, _, [3, 7]}]} = new_ast
|
|
iex> Code.eval_quoted(ast)
|
|
{26, []}
|
|
iex> Code.eval_quoted(new_ast)
|
|
{50, []}
|
|
|
|
"""
|
|
@spec prewalk(t, (t -> t)) :: t
|
|
def prewalk(ast, fun) when is_function(fun, 1) do
|
|
elem(prewalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
|
end
|
|
|
|
@doc """
|
|
Performs a depth-first, pre-order traversal of quoted expressions
|
|
using an accumulator.
|
|
|
|
Returns a tuple where the first element is a new AST where each node is the
|
|
result of invoking `fun` on each corresponding node and the second one is the
|
|
final accumulator.
|
|
|
|
## Examples
|
|
|
|
iex> ast = quote do: 5 + 3 * 7
|
|
iex> {:+, _, [5, {:*, _, [3, 7]}]} = ast
|
|
iex> {new_ast, acc} = Macro.prewalk(ast, [], fn
|
|
...> {:+, meta, children}, acc -> {{:*, meta, children}, [:+ | acc]}
|
|
...> {:*, meta, children}, acc -> {{:+, meta, children}, [:* | acc]}
|
|
...> other, acc -> {other, acc}
|
|
...> end)
|
|
iex> {{:*, _, [5, {:+, _, [3, 7]}]}, [:*, :+]} = {new_ast, acc}
|
|
iex> Code.eval_quoted(ast)
|
|
{26, []}
|
|
iex> Code.eval_quoted(new_ast)
|
|
{50, []}
|
|
|
|
"""
|
|
@spec prewalk(t, any, (t, any -> {t, any})) :: {t, any}
|
|
def prewalk(ast, acc, fun) when is_function(fun, 2) do
|
|
traverse(ast, acc, fun, fn x, a -> {x, a} end)
|
|
end
|
|
|
|
@doc """
|
|
This function behaves like `prewalk/2`, but performs a depth-first,
|
|
post-order traversal of quoted expressions.
|
|
"""
|
|
@spec postwalk(t, (t -> t)) :: t
|
|
def postwalk(ast, fun) when is_function(fun, 1) do
|
|
elem(postwalk(ast, nil, fn x, nil -> {fun.(x), nil} end), 0)
|
|
end
|
|
|
|
@doc """
|
|
This functions behaves like `prewalk/3`, but performs a depth-first,
|
|
post-order traversal of quoted expressions using an accumulator.
|
|
"""
|
|
@spec postwalk(t, any, (t, any -> {t, any})) :: {t, any}
|
|
def postwalk(ast, acc, fun) when is_function(fun, 2) do
|
|
traverse(ast, acc, fn x, a -> {x, a} end, fun)
|
|
end
|
|
|
|
@doc """
|
|
Decomposes a local or remote call into its remote part (when provided),
|
|
function name and argument list.
|
|
|
|
Returns `:error` when an invalid call syntax is provided.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.decompose_call(quote(do: foo))
|
|
{:foo, []}
|
|
|
|
iex> Macro.decompose_call(quote(do: foo()))
|
|
{:foo, []}
|
|
|
|
iex> Macro.decompose_call(quote(do: foo(1, 2, 3)))
|
|
{:foo, [1, 2, 3]}
|
|
|
|
iex> Macro.decompose_call(quote(do: Elixir.M.foo(1, 2, 3)))
|
|
{{:__aliases__, [], [:Elixir, :M]}, :foo, [1, 2, 3]}
|
|
|
|
iex> Macro.decompose_call(quote(do: 42))
|
|
:error
|
|
|
|
iex> Macro.decompose_call(quote(do: {:foo, [], []}))
|
|
:error
|
|
|
|
"""
|
|
@spec decompose_call(t()) :: {atom, [t()]} | {t(), atom, [t()]} | :error
|
|
def decompose_call(ast)
|
|
|
|
def decompose_call({:{}, _, args}) when is_list(args), do: :error
|
|
|
|
def decompose_call({{:., _, [remote, function]}, _, args})
|
|
when is_tuple(remote) or is_atom(remote),
|
|
do: {remote, function, args}
|
|
|
|
def decompose_call({name, _, args}) when is_atom(name) and is_atom(args), do: {name, []}
|
|
|
|
def decompose_call({name, _, args}) when is_atom(name) and is_list(args), do: {name, args}
|
|
|
|
def decompose_call(_), do: :error
|
|
|
|
@doc """
|
|
Recursively escapes a value so it can be inserted into a syntax tree.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.escape(:foo)
|
|
:foo
|
|
|
|
iex> Macro.escape({:a, :b, :c})
|
|
{:{}, [], [:a, :b, :c]}
|
|
|
|
iex> Macro.escape({:unquote, [], [1]}, unquote: true)
|
|
1
|
|
|
|
## Options
|
|
|
|
* `:unquote` - when true, this function leaves `unquote/1` and
|
|
`unquote_splicing/1` statements unescaped, effectively unquoting
|
|
the contents on escape. This option is useful only when escaping
|
|
ASTs which may have quoted fragments in them. Defaults to false.
|
|
|
|
* `:prune_metadata` - when true, removes metadata from escaped AST
|
|
nodes. Note this option changes the semantics of escaped code and
|
|
it should only be used when escaping ASTs. Defaults to false.
|
|
|
|
As an example, `ExUnit` stores the AST of every assertion, so when
|
|
an assertion fails we can show code snippets to users. Without this
|
|
option, each time the test module is compiled, we get a different
|
|
MD5 of the module bytecode, because the AST contains metadata,
|
|
such as counters, specific to the compilation environment. By pruning
|
|
the metadata, we ensure that the module is deterministic and reduce
|
|
the amount of data `ExUnit` needs to keep around. Only the minimal
|
|
amount of metadata is kept, such as `:line` and `:no_parens`.
|
|
|
|
## Comparison to `quote/2`
|
|
|
|
The `escape/2` function is sometimes confused with `quote/2`,
|
|
because the above examples behave the same with both. The key difference is
|
|
best illustrated when the value to escape is stored in a variable.
|
|
|
|
iex> Macro.escape({:a, :b, :c})
|
|
{:{}, [], [:a, :b, :c]}
|
|
iex> quote do: {:a, :b, :c}
|
|
{:{}, [], [:a, :b, :c]}
|
|
|
|
iex> value = {:a, :b, :c}
|
|
iex> Macro.escape(value)
|
|
{:{}, [], [:a, :b, :c]}
|
|
|
|
iex> quote do: value
|
|
{:value, [], __MODULE__}
|
|
|
|
iex> value = {:a, :b, :c}
|
|
iex> quote do: unquote(value)
|
|
{:a, :b, :c}
|
|
|
|
`escape/2` is used to escape *values* (either directly passed or variable
|
|
bound), while `quote/2` produces syntax trees for
|
|
expressions.
|
|
"""
|
|
@spec escape(term, keyword) :: t()
|
|
def escape(expr, opts \\ []) do
|
|
unquote = Keyword.get(opts, :unquote, false)
|
|
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
|
|
:elixir_quote.escape(expr, kind, unquote)
|
|
end
|
|
|
|
@doc """
|
|
Expands the struct given by `module` in the given `env`.
|
|
|
|
This is useful when a struct needs to be expanded at
|
|
compilation time and the struct being expanded may or may
|
|
not have been compiled. This function is also capable of
|
|
expanding structs defined under the module being compiled.
|
|
|
|
It will raise `CompileError` if the struct is not available.
|
|
From Elixir v1.12, calling this function also adds an export
|
|
dependency on the given struct.
|
|
"""
|
|
@doc since: "1.8.0"
|
|
@spec struct!(module, Macro.Env.t()) ::
|
|
%{required(:__struct__) => module, optional(atom) => any}
|
|
when module: module()
|
|
def struct!(module, env) when is_atom(module) do
|
|
if module == env.module do
|
|
Module.get_attribute(module, :__struct__)
|
|
end ||
|
|
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
|
|
{:ok, struct} -> struct
|
|
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Validates the given expressions are valid quoted expressions.
|
|
|
|
Check the type `t:Macro.t/0` for a complete specification of a
|
|
valid quoted expression.
|
|
|
|
It returns `:ok` if the expression is valid. Otherwise it returns
|
|
a tuple in the form of `{:error, remainder}` where `remainder` is
|
|
the invalid part of the quoted expression.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.validate({:two_element, :tuple})
|
|
:ok
|
|
iex> Macro.validate({:three, :element, :tuple})
|
|
{:error, {:three, :element, :tuple}}
|
|
|
|
iex> Macro.validate([1, 2, 3])
|
|
:ok
|
|
iex> Macro.validate([1, 2, 3, {4}])
|
|
{:error, {4}}
|
|
|
|
"""
|
|
@spec validate(term) :: :ok | {:error, term}
|
|
def validate(expr) do
|
|
find_invalid(expr) || :ok
|
|
end
|
|
|
|
defp find_invalid({left, right}), do: find_invalid(left) || find_invalid(right)
|
|
|
|
defp find_invalid({left, meta, right})
|
|
when is_list(meta) and (is_atom(right) or is_list(right)),
|
|
do: find_invalid(left) || find_invalid(right)
|
|
|
|
defp find_invalid(list) when is_list(list), do: Enum.find_value(list, &find_invalid/1)
|
|
|
|
defp find_invalid(pid) when is_pid(pid), do: nil
|
|
defp find_invalid(atom) when is_atom(atom), do: nil
|
|
defp find_invalid(num) when is_number(num), do: nil
|
|
defp find_invalid(bin) when is_binary(bin), do: nil
|
|
|
|
defp find_invalid(fun) when is_function(fun) do
|
|
unless Function.info(fun, :env) == {:env, []} and
|
|
Function.info(fun, :type) == {:type, :external} do
|
|
{:error, fun}
|
|
end
|
|
end
|
|
|
|
defp find_invalid(other), do: {:error, other}
|
|
|
|
@doc """
|
|
Returns an enumerable that traverses the `ast` in depth-first,
|
|
pre-order traversal.
|
|
|
|
## Examples
|
|
|
|
iex> ast = quote do: foo(1, "abc")
|
|
iex> Enum.map(Macro.prewalker(ast), & &1)
|
|
[{:foo, [], [1, "abc"]}, 1, "abc"]
|
|
|
|
"""
|
|
@doc since: "1.13.0"
|
|
@spec prewalker(t()) :: Enumerable.t()
|
|
def prewalker(ast) do
|
|
&prewalker([ast], &1, &2)
|
|
end
|
|
|
|
defp prewalker(_buffer, {:halt, acc}, _fun) do
|
|
{:halted, acc}
|
|
end
|
|
|
|
defp prewalker(buffer, {:suspend, acc}, fun) do
|
|
{:suspended, acc, &prewalker(buffer, &1, fun)}
|
|
end
|
|
|
|
defp prewalker([], {:cont, acc}, _fun) do
|
|
{:done, acc}
|
|
end
|
|
|
|
defp prewalker([{left, right} = node | tail], {:cont, acc}, fun) do
|
|
prewalker([left, right | tail], fun.(node, acc), fun)
|
|
end
|
|
|
|
defp prewalker([{left, meta, right} = node | tail], {:cont, acc}, fun)
|
|
when is_atom(left) and is_list(meta) do
|
|
if is_atom(right) do
|
|
prewalker(tail, fun.(node, acc), fun)
|
|
else
|
|
prewalker(right ++ tail, fun.(node, acc), fun)
|
|
end
|
|
end
|
|
|
|
defp prewalker([{left, meta, right} = node | tail], {:cont, acc}, fun) when is_list(meta) do
|
|
if is_atom(right) do
|
|
prewalker([left | tail], fun.(node, acc), fun)
|
|
else
|
|
prewalker([left | right] ++ tail, fun.(node, acc), fun)
|
|
end
|
|
end
|
|
|
|
defp prewalker([list | tail], {:cont, acc}, fun) when is_list(list) do
|
|
prewalker(list ++ tail, fun.(list, acc), fun)
|
|
end
|
|
|
|
defp prewalker([head | tail], {:cont, acc}, fun) do
|
|
prewalker(tail, fun.(head, acc), fun)
|
|
end
|
|
|
|
@doc """
|
|
Returns an enumerable that traverses the `ast` in depth-first,
|
|
post-order traversal.
|
|
|
|
## Examples
|
|
|
|
iex> ast = quote do: foo(1, "abc")
|
|
iex> Enum.map(Macro.postwalker(ast), & &1)
|
|
[1, "abc", {:foo, [], [1, "abc"]}]
|
|
|
|
"""
|
|
@doc since: "1.13.0"
|
|
@spec postwalker(t()) :: Enumerable.t()
|
|
def postwalker(ast) do
|
|
&postwalker([ast], make_ref(), &1, &2)
|
|
end
|
|
|
|
defp postwalker(_buffer, _ref, {:halt, acc}, _fun) do
|
|
{:halted, acc}
|
|
end
|
|
|
|
defp postwalker(buffer, ref, {:suspend, acc}, fun) do
|
|
{:suspended, acc, &postwalker(buffer, ref, &1, fun)}
|
|
end
|
|
|
|
defp postwalker([], _ref, {:cont, acc}, _fun) do
|
|
{:done, acc}
|
|
end
|
|
|
|
defp postwalker([{ref, head} | tail], ref, {:cont, acc}, fun) do
|
|
postwalker(tail, ref, fun.(head, acc), fun)
|
|
end
|
|
|
|
defp postwalker([{left, right} = node | tail], ref, {:cont, acc}, fun) do
|
|
postwalker([right, {ref, node} | tail], ref, fun.(left, acc), fun)
|
|
end
|
|
|
|
defp postwalker([{left, meta, right} = node | tail], ref, {:cont, acc}, fun)
|
|
when is_atom(left) and is_list(meta) do
|
|
if is_atom(right) do
|
|
postwalker(tail, ref, fun.(node, acc), fun)
|
|
else
|
|
postwalker(right ++ [{ref, node} | tail], ref, {:cont, acc}, fun)
|
|
end
|
|
end
|
|
|
|
defp postwalker([{left, meta, right} = node | tail], ref, cont_acc, fun)
|
|
when is_list(meta) do
|
|
if is_atom(right) do
|
|
postwalker([left, {ref, node} | tail], ref, cont_acc, fun)
|
|
else
|
|
postwalker([left | right] ++ [{ref, node} | tail], ref, cont_acc, fun)
|
|
end
|
|
end
|
|
|
|
defp postwalker([list | tail], ref, cont_acc, fun) when is_list(list) do
|
|
postwalker(list ++ [{ref, list} | tail], ref, cont_acc, fun)
|
|
end
|
|
|
|
defp postwalker([head | tail], ref, {:cont, acc}, fun) do
|
|
postwalker(tail, ref, fun.(head, acc), fun)
|
|
end
|
|
|
|
@doc ~S"""
|
|
Unescapes characters in a string.
|
|
|
|
This is the unescaping behaviour used by default in Elixir
|
|
single- and double-quoted strings. Check `unescape_string/2`
|
|
for information on how to customize the escaping map.
|
|
|
|
In this setup, Elixir will escape the following: `\0`, `\a`, `\b`,
|
|
`\d`, `\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Bytes can be
|
|
given as hexadecimals via `\xNN` and Unicode code points as
|
|
`\uNNNN` escapes.
|
|
|
|
This function is commonly used on sigil implementations
|
|
(like `~r`, `~s` and others), which receive a raw, unescaped
|
|
string, and it can be used anywhere that needs to mimic how
|
|
Elixir parses strings.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.unescape_string("example\\n")
|
|
"example\n"
|
|
|
|
In the example above, we pass a string with `\n` escaped
|
|
and return a version with it unescaped.
|
|
"""
|
|
@spec unescape_string(String.t()) :: String.t()
|
|
def unescape_string(string) do
|
|
:elixir_interpolation.unescape_string(string)
|
|
end
|
|
|
|
@doc ~S"""
|
|
Unescapes characters in a string according to the given mapping.
|
|
|
|
Check `unescape_string/1` if you want to use the same mapping
|
|
as Elixir single- and double-quoted strings.
|
|
|
|
## Mapping function
|
|
|
|
The mapping function receives an integer representing the code point
|
|
of the character it wants to unescape. There are also the special atoms
|
|
`:newline`, `:unicode`, and `:hex`, which control newline, unicode,
|
|
and escaping respectively.
|
|
|
|
Here is the default mapping function implemented by Elixir:
|
|
|
|
def unescape_map(:newline), do: true
|
|
def unescape_map(:unicode), do: true
|
|
def unescape_map(:hex), do: true
|
|
def unescape_map(?0), do: ?0
|
|
def unescape_map(?a), do: ?\a
|
|
def unescape_map(?b), do: ?\b
|
|
def unescape_map(?d), do: ?\d
|
|
def unescape_map(?e), do: ?\e
|
|
def unescape_map(?f), do: ?\f
|
|
def unescape_map(?n), do: ?\n
|
|
def unescape_map(?r), do: ?\r
|
|
def unescape_map(?s), do: ?\s
|
|
def unescape_map(?t), do: ?\t
|
|
def unescape_map(?v), do: ?\v
|
|
def unescape_map(e), do: e
|
|
|
|
If the `unescape_map/1` function returns `false`, the char is
|
|
not escaped and the backslash is kept in the string.
|
|
|
|
## Examples
|
|
|
|
Using the `unescape_map/1` function defined above is easy:
|
|
|
|
Macro.unescape_string("example\\n", &unescape_map(&1))
|
|
|
|
"""
|
|
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
|
|
def unescape_string(string, map) do
|
|
:elixir_interpolation.unescape_string(string, map)
|
|
end
|
|
|
|
@doc false
|
|
@deprecated "Traverse over the arguments using Enum.map/2 instead"
|
|
def unescape_tokens(tokens) do
|
|
for token <- tokens do
|
|
if is_binary(token), do: unescape_string(token), else: token
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
@deprecated "Traverse over the arguments using Enum.map/2 instead"
|
|
def unescape_tokens(tokens, map) do
|
|
for token <- tokens do
|
|
if is_binary(token), do: unescape_string(token, map), else: token
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given expression AST to a string.
|
|
|
|
This is a convenience function for converting AST into
|
|
a string, which discards all formatting of the original
|
|
code and wraps newlines around 98 characters. See
|
|
`Code.quoted_to_algebra/2` as a lower level function
|
|
with more control around formatting.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
|
|
"foo.bar(1, 2, 3)"
|
|
|
|
"""
|
|
@spec to_string(t()) :: String.t()
|
|
# TODO: Allow line_length to be configurable on v1.17
|
|
def to_string(tree) do
|
|
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
|
|
IO.iodata_to_binary(doc)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given expression AST to a string.
|
|
|
|
The given `fun` is called for every node in the AST with two arguments: the
|
|
AST of the node being printed and the string representation of that same
|
|
node. The return value of this function is used as the final string
|
|
representation for that AST node.
|
|
|
|
This function discards all formatting of the original code.
|
|
|
|
## Examples
|
|
|
|
Macro.to_string(quote(do: 1 + 2), fn
|
|
1, _string -> "one"
|
|
2, _string -> "two"
|
|
_ast, string -> string
|
|
end)
|
|
#=> "one + two"
|
|
|
|
"""
|
|
@deprecated "Use Macro.to_string/1 instead"
|
|
@spec to_string(t(), (t(), String.t() -> String.t())) :: String.t()
|
|
def to_string(tree, fun)
|
|
|
|
# Variables
|
|
def to_string({var, _, context} = ast, fun) when is_atom(var) and is_atom(context) do
|
|
fun.(ast, Atom.to_string(var))
|
|
end
|
|
|
|
# Aliases
|
|
def to_string({:__aliases__, _, refs} = ast, fun) do
|
|
fun.(ast, Enum.map_join(refs, ".", &call_to_string(&1, fun)))
|
|
end
|
|
|
|
# Blocks
|
|
def to_string({:__block__, _, [expr]} = ast, fun) do
|
|
fun.(ast, to_string(expr, fun))
|
|
end
|
|
|
|
def to_string({:__block__, _, _} = ast, fun) do
|
|
block = adjust_new_lines(block_to_string(ast, fun), "\n ")
|
|
fun.(ast, "(\n " <> block <> "\n)")
|
|
end
|
|
|
|
# Bits containers
|
|
def to_string({:<<>>, _, parts} = ast, fun) do
|
|
if interpolated?(ast) do
|
|
fun.(ast, interpolate(ast, fun))
|
|
else
|
|
result =
|
|
Enum.map_join(parts, ", ", fn part ->
|
|
str = bitpart_to_string(part, fun)
|
|
|
|
if :binary.first(str) == ?< or :binary.last(str) == ?> do
|
|
"(" <> str <> ")"
|
|
else
|
|
str
|
|
end
|
|
end)
|
|
|
|
fun.(ast, "<<" <> result <> ">>")
|
|
end
|
|
end
|
|
|
|
# Tuple containers
|
|
def to_string({:{}, _, args} = ast, fun) do
|
|
tuple = "{" <> Enum.map_join(args, ", ", &to_string(&1, fun)) <> "}"
|
|
fun.(ast, tuple)
|
|
end
|
|
|
|
# Map containers
|
|
def to_string({:%{}, _, args} = ast, fun) do
|
|
map = "%{" <> map_to_string(args, fun) <> "}"
|
|
fun.(ast, map)
|
|
end
|
|
|
|
def to_string({:%, _, [struct_name, map]} = ast, fun) do
|
|
{:%{}, _, args} = map
|
|
struct = "%" <> to_string(struct_name, fun) <> "{" <> map_to_string(args, fun) <> "}"
|
|
fun.(ast, struct)
|
|
end
|
|
|
|
# Fn keyword
|
|
def to_string({:fn, _, [{:->, _, [_, tuple]}] = arrow} = ast, fun)
|
|
when not is_tuple(tuple) or elem(tuple, 0) != :__block__ do
|
|
fun.(ast, "fn " <> arrow_to_string(arrow, fun) <> " end")
|
|
end
|
|
|
|
def to_string({:fn, _, [{:->, _, _}] = block} = ast, fun) do
|
|
fun.(ast, "fn " <> block_to_string(block, fun) <> "\nend")
|
|
end
|
|
|
|
def to_string({:fn, _, block} = ast, fun) do
|
|
block = adjust_new_lines(block_to_string(block, fun), "\n ")
|
|
fun.(ast, "fn\n " <> block <> "\nend")
|
|
end
|
|
|
|
# left -> right
|
|
def to_string([{:->, _, _} | _] = ast, fun) do
|
|
fun.(ast, "(" <> arrow_to_string(ast, fun, true) <> ")")
|
|
end
|
|
|
|
# left when right
|
|
def to_string({:when, _, [left, right]} = ast, fun) do
|
|
right =
|
|
if right != [] and Keyword.keyword?(right) do
|
|
kw_list_to_string(right, fun)
|
|
else
|
|
fun.(ast, op_to_string(right, fun, :when, :right))
|
|
end
|
|
|
|
fun.(ast, op_to_string(left, fun, :when, :left) <> " when " <> right)
|
|
end
|
|
|
|
# Splat when
|
|
def to_string({:when, _, args} = ast, fun) do
|
|
{left, right} = split_last(args)
|
|
|
|
result =
|
|
"(" <> Enum.map_join(left, ", ", &to_string(&1, fun)) <> ") when " <> to_string(right, fun)
|
|
|
|
fun.(ast, result)
|
|
end
|
|
|
|
# Capture
|
|
def to_string({:&, _, [{:/, _, [{name, _, ctx}, arity]}]} = ast, fun)
|
|
when is_atom(name) and is_atom(ctx) and is_integer(arity) do
|
|
result = "&" <> Atom.to_string(name) <> "/" <> to_string(arity, fun)
|
|
fun.(ast, result)
|
|
end
|
|
|
|
def to_string({:&, _, [{:/, _, [{{:., _, [mod, name]}, _, []}, arity]}]} = ast, fun)
|
|
when is_atom(name) and is_integer(arity) do
|
|
result =
|
|
"&" <> to_string(mod, fun) <> "." <> Atom.to_string(name) <> "/" <> to_string(arity, fun)
|
|
|
|
fun.(ast, result)
|
|
end
|
|
|
|
def to_string({:&, _, [arg]} = ast, fun) when not is_integer(arg) do
|
|
fun.(ast, "&(" <> to_string(arg, fun) <> ")")
|
|
end
|
|
|
|
# left not in right
|
|
def to_string({:not, _, [{:in, _, [left, right]}]} = ast, fun) do
|
|
fun.(ast, to_string(left, fun) <> " not in " <> to_string(right, fun))
|
|
end
|
|
|
|
# Access
|
|
def to_string({{:., _, [Access, :get]}, _, [left, right]} = ast, fun) do
|
|
if op_expr?(left) do
|
|
fun.(ast, "(" <> to_string(left, fun) <> ")" <> to_string([right], fun))
|
|
else
|
|
fun.(ast, to_string(left, fun) <> to_string([right], fun))
|
|
end
|
|
end
|
|
|
|
# foo.{bar, baz}
|
|
def to_string({{:., _, [left, :{}]}, _, args} = ast, fun) do
|
|
fun.(ast, to_string(left, fun) <> ".{" <> args_to_string(args, fun) <> "}")
|
|
end
|
|
|
|
# All other calls
|
|
def to_string({{:., _, [left, _]} = target, meta, []} = ast, fun) do
|
|
to_string = call_to_string(target, fun)
|
|
|
|
if is_tuple(left) && meta[:no_parens] do
|
|
fun.(ast, to_string)
|
|
else
|
|
fun.(ast, to_string <> "()")
|
|
end
|
|
end
|
|
|
|
def to_string({target, _, args} = ast, fun) when is_list(args) do
|
|
with :error <- unary_call(ast, fun),
|
|
:error <- op_call(ast, fun),
|
|
:error <- sigil_call(ast, fun) do
|
|
{list, last} = split_last(args)
|
|
|
|
result =
|
|
if kw_blocks?(last) do
|
|
case list do
|
|
[] -> call_to_string(target, fun) <> kw_blocks_to_string(last, fun)
|
|
_ -> call_to_string_with_args(target, list, fun) <> kw_blocks_to_string(last, fun)
|
|
end
|
|
else
|
|
call_to_string_with_args(target, args, fun)
|
|
end
|
|
|
|
fun.(ast, result)
|
|
else
|
|
{:ok, value} -> value
|
|
end
|
|
end
|
|
|
|
# Two-element tuples
|
|
def to_string({left, right}, fun) do
|
|
to_string({:{}, [], [left, right]}, fun)
|
|
end
|
|
|
|
# Lists
|
|
def to_string(list, fun) when is_list(list) do
|
|
result =
|
|
cond do
|
|
list == [] ->
|
|
"[]"
|
|
|
|
:io_lib.printable_list(list) ->
|
|
{escaped, _} = Identifier.escape(IO.chardata_to_string(list), ?")
|
|
IO.iodata_to_binary([?~, ?c, ?", escaped, ?"])
|
|
|
|
Inspect.List.keyword?(list) ->
|
|
"[" <> kw_list_to_string(list, fun) <> "]"
|
|
|
|
true ->
|
|
"[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]"
|
|
end
|
|
|
|
fun.(list, result)
|
|
end
|
|
|
|
# All other structures
|
|
def to_string(other, fun) do
|
|
fun.(other, inspect_no_limit(other))
|
|
end
|
|
|
|
defp inspect_no_limit(value) do
|
|
Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)
|
|
end
|
|
|
|
defp bitpart_to_string({:"::", meta, [left, right]} = ast, fun) do
|
|
result =
|
|
if meta[:inferred_bitstring_spec] do
|
|
to_string(left, fun)
|
|
else
|
|
op_to_string(left, fun, :"::", :left) <>
|
|
"::" <> bitmods_to_string(right, fun, :"::", :right)
|
|
end
|
|
|
|
fun.(ast, result)
|
|
end
|
|
|
|
defp bitpart_to_string(ast, fun) do
|
|
to_string(ast, fun)
|
|
end
|
|
|
|
defp bitmods_to_string({op, _, [left, right]} = ast, fun, _, _) when op in [:*, :-] do
|
|
result =
|
|
bitmods_to_string(left, fun, op, :left) <>
|
|
Atom.to_string(op) <> bitmods_to_string(right, fun, op, :right)
|
|
|
|
fun.(ast, result)
|
|
end
|
|
|
|
defp bitmods_to_string(other, fun, parent_op, side) do
|
|
op_to_string(other, fun, parent_op, side)
|
|
end
|
|
|
|
# Block keywords
|
|
kw_keywords = [:do, :rescue, :catch, :else, :after]
|
|
|
|
defp kw_blocks?([{:do, _} | _] = kw) do
|
|
Enum.all?(kw, &match?({x, _} when x in unquote(kw_keywords), &1))
|
|
end
|
|
|
|
defp kw_blocks?(_), do: false
|
|
|
|
# Check if we have an interpolated string.
|
|
defp interpolated?({:<<>>, _, [_ | _] = parts}) do
|
|
Enum.all?(parts, fn
|
|
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true
|
|
binary when is_binary(binary) -> true
|
|
_ -> false
|
|
end)
|
|
end
|
|
|
|
defp interpolated?(_) do
|
|
false
|
|
end
|
|
|
|
defp interpolate(ast, fun), do: interpolate(ast, "\"", "\"", fun)
|
|
|
|
defp interpolate({:<<>>, _, [parts]}, left, right, _) when left in [~s["""\n], ~s['''\n]] do
|
|
<<left::binary, parts::binary, right::binary>>
|
|
end
|
|
|
|
defp interpolate({:<<>>, _, parts}, left, right, fun) do
|
|
parts =
|
|
Enum.map_join(parts, "", fn
|
|
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [arg]}, {:binary, _, _}]} ->
|
|
"\#{" <> to_string(arg, fun) <> "}"
|
|
|
|
binary when is_binary(binary) ->
|
|
escape_sigil(binary, left)
|
|
end)
|
|
|
|
<<left::binary, parts::binary, right::binary>>
|
|
end
|
|
|
|
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
|
|
defp escape_sigil(parts, "{"), do: String.replace(parts, "}", ~S"\}")
|
|
defp escape_sigil(parts, "["), do: String.replace(parts, "]", ~S"\]")
|
|
defp escape_sigil(parts, "<"), do: String.replace(parts, ">", ~S"\>")
|
|
defp escape_sigil(parts, delimiter), do: String.replace(parts, delimiter, "\\#{delimiter}")
|
|
|
|
defp module_to_string(atom, _fun) when is_atom(atom) do
|
|
inspect_no_limit(atom)
|
|
end
|
|
|
|
defp module_to_string({:&, _, [val]} = expr, fun) when not is_integer(val) do
|
|
"(" <> to_string(expr, fun) <> ")"
|
|
end
|
|
|
|
defp module_to_string({:fn, _, _} = expr, fun) do
|
|
"(" <> to_string(expr, fun) <> ")"
|
|
end
|
|
|
|
defp module_to_string({_, _, [_ | _] = args} = expr, fun) do
|
|
if kw_blocks?(List.last(args)) do
|
|
"(" <> to_string(expr, fun) <> ")"
|
|
else
|
|
to_string(expr, fun)
|
|
end
|
|
end
|
|
|
|
defp module_to_string(expr, fun) do
|
|
to_string(expr, fun)
|
|
end
|
|
|
|
defp unary_call({op, _, [arg]} = ast, fun) when is_atom(op) do
|
|
if operator?(op, 1) do
|
|
if op == :not or op_expr?(arg) do
|
|
{:ok, fun.(ast, Atom.to_string(op) <> "(" <> to_string(arg, fun) <> ")")}
|
|
else
|
|
{:ok, fun.(ast, Atom.to_string(op) <> to_string(arg, fun))}
|
|
end
|
|
else
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp unary_call(_, _) do
|
|
:error
|
|
end
|
|
|
|
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
|
|
left = op_to_string(left, fun, :.., :left)
|
|
middle = op_to_string(middle, fun, :.., :right)
|
|
right = op_to_string(right, fun, :"//", :right)
|
|
{:ok, fun.(ast, left <> ".." <> middle <> "//" <> right)}
|
|
end
|
|
|
|
defp op_call({op, _, [left, right]} = ast, fun) when is_atom(op) do
|
|
if operator?(op, 2) do
|
|
left = op_to_string(left, fun, op, :left)
|
|
right = op_to_string(right, fun, op, :right)
|
|
op = if op in [:..], do: "#{op}", else: " #{op} "
|
|
{:ok, fun.(ast, left <> op <> right)}
|
|
else
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp op_call(_, _) do
|
|
:error
|
|
end
|
|
|
|
defp sigil_call({sigil, meta, [{:<<>>, _, _} = parts, args]} = ast, fun)
|
|
when is_atom(sigil) and is_list(args) do
|
|
delimiter = Keyword.get(meta, :delimiter, "\"")
|
|
{left, right} = delimiter_pair(delimiter)
|
|
|
|
case Atom.to_string(sigil) do
|
|
<<"sigil_", first, rest::binary>> when first >= ?A and first <= ?Z ->
|
|
if upcase_letters?(rest) do
|
|
args = sigil_args(args, fun)
|
|
{:<<>>, _, [binary]} = parts
|
|
|
|
formatted =
|
|
<<?~, first, rest::binary, left::binary, binary::binary, right::binary, args::binary>>
|
|
|
|
{:ok, fun.(ast, formatted)}
|
|
else
|
|
:error
|
|
end
|
|
|
|
<<"sigil_", name>> when name >= ?a and name <= ?z ->
|
|
args = sigil_args(args, fun)
|
|
formatted = "~" <> <<name>> <> interpolate(parts, left, right, fun) <> args
|
|
{:ok, fun.(ast, formatted)}
|
|
|
|
_ ->
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp sigil_call(_other, _fun) do
|
|
:error
|
|
end
|
|
|
|
defp upcase_letters?(<<letter, rest::binary>>) when letter >= ?A and letter <= ?Z,
|
|
do: upcase_letters?(rest)
|
|
|
|
defp upcase_letters?(<<_>>),
|
|
do: false
|
|
|
|
defp upcase_letters?(<<>>),
|
|
do: true
|
|
|
|
defp delimiter_pair("["), do: {"[", "]"}
|
|
defp delimiter_pair("{"), do: {"{", "}"}
|
|
defp delimiter_pair("("), do: {"(", ")"}
|
|
defp delimiter_pair("<"), do: {"<", ">"}
|
|
defp delimiter_pair("\"\"\""), do: {"\"\"\"\n", "\"\"\""}
|
|
defp delimiter_pair("'''"), do: {"'''\n", "'''"}
|
|
defp delimiter_pair(str), do: {str, str}
|
|
|
|
defp sigil_args([], _fun), do: ""
|
|
defp sigil_args(args, fun), do: fun.(args, List.to_string(args))
|
|
|
|
defp op_expr?(expr) do
|
|
case expr do
|
|
{op, _, [_, _]} -> operator?(op, 2)
|
|
{op, _, [_]} -> operator?(op, 1)
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
defp call_to_string(atom, _fun) when is_atom(atom), do: Atom.to_string(atom)
|
|
defp call_to_string({:., _, [arg]}, fun), do: module_to_string(arg, fun) <> "."
|
|
|
|
defp call_to_string({:., _, [left, right]}, fun) when is_atom(right),
|
|
do: module_to_string(left, fun) <> "." <> call_to_string_for_atom(right)
|
|
|
|
defp call_to_string({:., _, [left, right]}, fun),
|
|
do: module_to_string(left, fun) <> "." <> call_to_string(right, fun)
|
|
|
|
defp call_to_string(other, fun), do: to_string(other, fun)
|
|
|
|
defp call_to_string_with_args(target, args, fun) do
|
|
target = call_to_string(target, fun)
|
|
args = args_to_string(args, fun)
|
|
target <> "(" <> args <> ")"
|
|
end
|
|
|
|
defp call_to_string_for_atom(atom) do
|
|
Macro.inspect_atom(:remote_call, atom)
|
|
end
|
|
|
|
defp args_to_string(args, fun) do
|
|
{list, last} = split_last(args)
|
|
|
|
if last != [] and Inspect.List.keyword?(last) do
|
|
prefix =
|
|
case list do
|
|
[] -> ""
|
|
_ -> Enum.map_join(list, ", ", &to_string(&1, fun)) <> ", "
|
|
end
|
|
|
|
prefix <> kw_list_to_string(last, fun)
|
|
else
|
|
Enum.map_join(args, ", ", &to_string(&1, fun))
|
|
end
|
|
end
|
|
|
|
defp kw_blocks_to_string(kw, fun) do
|
|
Enum.reduce(unquote(kw_keywords), " ", fn x, acc ->
|
|
case Keyword.has_key?(kw, x) do
|
|
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
|
|
false -> acc
|
|
end
|
|
end) <> "end"
|
|
end
|
|
|
|
defp kw_block_to_string(key, value, fun) do
|
|
block = adjust_new_lines(block_to_string(value, fun), "\n ")
|
|
Atom.to_string(key) <> "\n " <> block <> "\n"
|
|
end
|
|
|
|
defp block_to_string([{:->, _, _} | _] = block, fun) do
|
|
Enum.map_join(block, "\n", fn {:->, _, [left, right]} ->
|
|
left = comma_join_or_empty_paren(left, fun, false)
|
|
left <> "->\n " <> adjust_new_lines(block_to_string(right, fun), "\n ")
|
|
end)
|
|
end
|
|
|
|
defp block_to_string({:__block__, _, exprs}, fun) do
|
|
Enum.map_join(exprs, "\n", &to_string(&1, fun))
|
|
end
|
|
|
|
defp block_to_string(other, fun), do: to_string(other, fun)
|
|
|
|
defp map_to_string([{:|, _, [update_map, update_args]}], fun) do
|
|
to_string(update_map, fun) <> " | " <> map_to_string(update_args, fun)
|
|
end
|
|
|
|
defp map_to_string(list, fun) do
|
|
cond do
|
|
Inspect.List.keyword?(list) -> kw_list_to_string(list, fun)
|
|
true -> map_list_to_string(list, fun)
|
|
end
|
|
end
|
|
|
|
defp kw_list_to_string(list, fun) do
|
|
Enum.map_join(list, ", ", fn {key, value} ->
|
|
Macro.inspect_atom(:key, key) <> " " <> to_string(value, fun)
|
|
end)
|
|
end
|
|
|
|
defp map_list_to_string(list, fun) do
|
|
Enum.map_join(list, ", ", fn
|
|
{key, value} -> to_string(key, fun) <> " => " <> to_string(value, fun)
|
|
other -> to_string(other, fun)
|
|
end)
|
|
end
|
|
|
|
defp wrap_in_parenthesis(expr, fun) do
|
|
"(" <> to_string(expr, fun) <> ")"
|
|
end
|
|
|
|
defp op_to_string({op, _, [_, _]} = expr, fun, parent_op, side) when is_atom(op) do
|
|
case Identifier.binary_op(op) do
|
|
{_, prec} ->
|
|
{parent_assoc, parent_prec} = Identifier.binary_op(parent_op)
|
|
|
|
cond do
|
|
parent_prec < prec -> to_string(expr, fun)
|
|
parent_prec > prec -> wrap_in_parenthesis(expr, fun)
|
|
parent_assoc == side -> to_string(expr, fun)
|
|
true -> wrap_in_parenthesis(expr, fun)
|
|
end
|
|
|
|
:error ->
|
|
to_string(expr, fun)
|
|
end
|
|
end
|
|
|
|
defp op_to_string(expr, fun, _, _), do: to_string(expr, fun)
|
|
|
|
defp arrow_to_string(pairs, fun, paren \\ false) do
|
|
Enum.map_join(pairs, "; ", fn {:->, _, [left, right]} ->
|
|
left = comma_join_or_empty_paren(left, fun, paren)
|
|
left <> "-> " <> to_string(right, fun)
|
|
end)
|
|
end
|
|
|
|
defp comma_join_or_empty_paren([], _fun, true), do: "() "
|
|
defp comma_join_or_empty_paren([], _fun, false), do: ""
|
|
|
|
defp comma_join_or_empty_paren(left, fun, _) do
|
|
Enum.map_join(left, ", ", &to_string(&1, fun)) <> " "
|
|
end
|
|
|
|
defp split_last([]) do
|
|
{[], []}
|
|
end
|
|
|
|
defp split_last(args) do
|
|
{left, [right]} = Enum.split(args, -1)
|
|
{left, right}
|
|
end
|
|
|
|
defp adjust_new_lines(block, replacement) do
|
|
for <<x <- block>>, into: "" do
|
|
case x == ?\n do
|
|
true -> replacement
|
|
false -> <<x>>
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
|
|
|
|
This is used when you want to programatically invoke a macro at
|
|
compile-time.
|
|
"""
|
|
@doc since: "1.16.0"
|
|
def compile_apply(mod, fun, args, caller) do
|
|
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
|
|
Kernel.apply(mod, fun, args)
|
|
end
|
|
|
|
@doc """
|
|
Receives an AST node and expands it once.
|
|
|
|
The following contents are expanded:
|
|
|
|
* Macros (local or remote)
|
|
* Aliases are expanded (if possible) and return atoms
|
|
* Compilation environment macros (`__CALLER__/0`, `__DIR__/0`, `__ENV__/0` and `__MODULE__/0`)
|
|
* Module attributes reader (`@foo`)
|
|
|
|
If the expression cannot be expanded, it returns the expression
|
|
itself. This function does not traverse the AST, only the root
|
|
node is expanded.
|
|
|
|
`expand_once/2` performs the expansion just once. Check `expand/2`
|
|
to perform expansion until the node can no longer be expanded.
|
|
|
|
## Examples
|
|
|
|
In the example below, we have a macro that generates a module
|
|
with a function named `name_length` that returns the length
|
|
of the module name. The value of this function will be calculated
|
|
at compilation time and not at runtime.
|
|
|
|
Consider the implementation below:
|
|
|
|
defmacro defmodule_with_length(name, do: block) do
|
|
length = length(Atom.to_charlist(name))
|
|
|
|
quote do
|
|
defmodule unquote(name) do
|
|
def name_length, do: unquote(length)
|
|
unquote(block)
|
|
end
|
|
end
|
|
end
|
|
|
|
When invoked like this:
|
|
|
|
defmodule_with_length My.Module do
|
|
def other_function, do: ...
|
|
end
|
|
|
|
The compilation will fail because `My.Module` when quoted
|
|
is not an atom, but a syntax tree as follows:
|
|
|
|
{:__aliases__, [], [:My, :Module]}
|
|
|
|
That said, we need to expand the aliases node above to an
|
|
atom, so we can retrieve its length. Expanding the node is
|
|
not straightforward because we also need to expand the
|
|
caller aliases. For example:
|
|
|
|
alias MyHelpers, as: My
|
|
|
|
defmodule_with_length My.Module do
|
|
def other_function, do: ...
|
|
end
|
|
|
|
The final module name will be `MyHelpers.Module` and not
|
|
`My.Module`. With `Macro.expand/2`, such aliases are taken
|
|
into consideration. Local and remote macros are also
|
|
expanded. We could rewrite our macro above to use this
|
|
function as:
|
|
|
|
defmacro defmodule_with_length(name, do: block) do
|
|
expanded = Macro.expand(name, __CALLER__)
|
|
length = length(Atom.to_charlist(expanded))
|
|
|
|
quote do
|
|
defmodule unquote(name) do
|
|
def name_length, do: unquote(length)
|
|
unquote(block)
|
|
end
|
|
end
|
|
end
|
|
|
|
"""
|
|
@spec expand_once(input(), Macro.Env.t()) :: output()
|
|
def expand_once(ast, env) do
|
|
elem(do_expand_once(ast, env), 0)
|
|
end
|
|
|
|
defp do_expand_once({:__aliases__, meta, _} = original, env) do
|
|
case :elixir_aliases.expand_or_concat(original, env) do
|
|
receiver when is_atom(receiver) ->
|
|
:elixir_env.trace({:alias_reference, meta, receiver}, env)
|
|
{receiver, true}
|
|
|
|
aliases ->
|
|
aliases = :lists.map(&elem(do_expand_once(&1, env), 0), aliases)
|
|
|
|
case :lists.all(&is_atom/1, aliases) do
|
|
true ->
|
|
receiver = :elixir_aliases.concat(aliases)
|
|
:elixir_env.trace({:alias_reference, meta, receiver}, env)
|
|
{receiver, true}
|
|
|
|
false ->
|
|
{original, false}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Expand compilation environment macros
|
|
defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom), do: {env.module, true}
|
|
|
|
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
|
|
do: {:filename.dirname(env.file), true}
|
|
|
|
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
|
|
env = update_in(env.versioned_vars, &maybe_escape_map/1)
|
|
{maybe_escape_map(env), true}
|
|
end
|
|
|
|
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
|
when is_atom(atom) and is_atom(field) do
|
|
if Map.has_key?(env, field) do
|
|
{maybe_escape_map(Map.get(env, field)), true}
|
|
else
|
|
{original, false}
|
|
end
|
|
end
|
|
|
|
defp do_expand_once({atom, meta, context} = original, _env)
|
|
when is_atom(atom) and is_list(meta) and is_atom(context) do
|
|
{original, false}
|
|
end
|
|
|
|
defp do_expand_once({atom, meta, args} = original, env)
|
|
when is_atom(atom) and is_list(args) and is_list(meta) do
|
|
arity = length(args)
|
|
|
|
if special_form?(atom, arity) do
|
|
{original, false}
|
|
else
|
|
module = env.module
|
|
|
|
extra =
|
|
if function_exported?(module, :__info__, 1) do
|
|
[{module, module.__info__(:macros)}]
|
|
else
|
|
[]
|
|
end
|
|
|
|
s = :elixir_env.env_to_ex(env)
|
|
|
|
expand =
|
|
:elixir_dispatch.expand_import(meta, {atom, length(args)}, args, s, env, extra, true)
|
|
|
|
case expand do
|
|
{:ok, receiver, quoted} ->
|
|
next = :elixir_module.next_counter(module)
|
|
# We don't want the line to propagate yet, but generated might!
|
|
meta = Keyword.take(meta, [:generated])
|
|
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
|
|
|
|
{:ok, Kernel, op, [arg]} when op in [:+, :-] ->
|
|
case expand_once(arg, env) do
|
|
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
|
|
_ -> {original, false}
|
|
end
|
|
|
|
{:ok, _receiver, _name, _args} ->
|
|
{original, false}
|
|
|
|
:error ->
|
|
{original, false}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Expand possible macro require invocation
|
|
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
|
|
{receiver, _} = do_expand_once(left, env)
|
|
|
|
case is_atom(receiver) do
|
|
false ->
|
|
{original, false}
|
|
|
|
true ->
|
|
s = :elixir_env.env_to_ex(env)
|
|
name_arity = {right, length(args)}
|
|
expand = :elixir_dispatch.expand_require(meta, receiver, name_arity, args, s, env)
|
|
|
|
case expand do
|
|
{:ok, receiver, quoted} ->
|
|
next = :elixir_module.next_counter(env.module)
|
|
# We don't want the line to propagate yet, but generated might!
|
|
meta = Keyword.take(meta, [:generated])
|
|
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
|
|
|
|
:error ->
|
|
{original, false}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Anything else is just returned
|
|
defp do_expand_once(other, _env), do: {other, false}
|
|
|
|
defp maybe_escape_map(map) when is_map(map), do: {:%{}, [], Map.to_list(map)}
|
|
defp maybe_escape_map(other), do: other
|
|
|
|
@doc """
|
|
Returns `true` if the given name and arity is a special form.
|
|
"""
|
|
@doc since: "1.7.0"
|
|
@spec special_form?(name :: atom(), arity()) :: boolean()
|
|
def special_form?(name, arity) when is_atom(name) and is_integer(arity) do
|
|
:elixir_import.special_form(name, arity)
|
|
end
|
|
|
|
@doc """
|
|
Returns `true` if the given name and arity is an operator.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.operator?(:not_an_operator, 3)
|
|
false
|
|
iex> Macro.operator?(:.., 0)
|
|
true
|
|
iex> Macro.operator?(:+, 1)
|
|
true
|
|
iex> Macro.operator?(:++, 2)
|
|
true
|
|
iex> Macro.operator?(:..//, 3)
|
|
true
|
|
|
|
"""
|
|
@doc since: "1.7.0"
|
|
@spec operator?(name :: atom(), arity()) :: boolean()
|
|
def operator?(name, arity)
|
|
|
|
def operator?(:"..//", 3),
|
|
do: true
|
|
|
|
# Code.Identifier treats :// as a binary operator for precedence
|
|
# purposes but it isn't really one, so we explicitly skip it.
|
|
def operator?(name, 2) when is_atom(name),
|
|
do: Identifier.binary_op(name) != :error and name != :"//"
|
|
|
|
def operator?(name, 1) when is_atom(name),
|
|
do: Identifier.unary_op(name) != :error
|
|
|
|
def operator?(:.., 0),
|
|
do: true
|
|
|
|
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
|
|
|
|
@doc """
|
|
Returns `true` if the given quoted expression represents a quoted literal.
|
|
|
|
Atoms and numbers are always literals. Binaries, lists, tuples,
|
|
maps, and structs are only literals if all of their terms are also literals.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.quoted_literal?(quote(do: "foo"))
|
|
true
|
|
iex> Macro.quoted_literal?(quote(do: {"foo", 1}))
|
|
true
|
|
iex> Macro.quoted_literal?(quote(do: {"foo", 1, :baz}))
|
|
true
|
|
iex> Macro.quoted_literal?(quote(do: %{foo: "bar"}))
|
|
true
|
|
iex> Macro.quoted_literal?(quote(do: %URI{path: "/"}))
|
|
true
|
|
iex> Macro.quoted_literal?(quote(do: URI.parse("/")))
|
|
false
|
|
iex> Macro.quoted_literal?(quote(do: {foo, var}))
|
|
false
|
|
|
|
"""
|
|
@doc since: "1.7.0"
|
|
@spec quoted_literal?(t) :: boolean
|
|
def quoted_literal?(term)
|
|
|
|
def quoted_literal?({:__aliases__, _, args}),
|
|
do: quoted_literal?(args)
|
|
|
|
def quoted_literal?({:%, _, [left, right]}),
|
|
do: quoted_literal?(left) and quoted_literal?(right)
|
|
|
|
def quoted_literal?({:%{}, _, args}), do: quoted_literal?(args)
|
|
def quoted_literal?({:{}, _, args}), do: quoted_literal?(args)
|
|
def quoted_literal?({:__MODULE__, _, ctx}) when is_atom(ctx), do: true
|
|
def quoted_literal?({:<<>>, _, segments}), do: Enum.all?(segments, "ed_bitstring_segment?/1)
|
|
def quoted_literal?({left, right}), do: quoted_literal?(left) and quoted_literal?(right)
|
|
def quoted_literal?(list) when is_list(list), do: :lists.all("ed_literal?/1, list)
|
|
def quoted_literal?(term), do: is_atom(term) or is_number(term) or is_binary(term)
|
|
|
|
defp quoted_bitstring_segment?(term) when is_integer(term) or is_binary(term), do: true
|
|
|
|
defp quoted_bitstring_segment?({:"::", _, [term, modifier]})
|
|
when is_integer(term) or is_binary(term),
|
|
do: quoted_bitstring_modifier?(modifier)
|
|
|
|
defp quoted_bitstring_segment?(_other), do: false
|
|
|
|
defp quoted_bitstring_modifier?({:-, _, [left, right]}),
|
|
do: quoted_bitstring_modifier?(left) and quoted_bitstring_modifier?(right)
|
|
|
|
defp quoted_bitstring_modifier?({atom, _, [size]})
|
|
when atom in [:size, :unit] and is_integer(size),
|
|
do: true
|
|
|
|
defp quoted_bitstring_modifier?({:*, _, [left, right]})
|
|
when is_integer(left) and is_integer(right),
|
|
do: true
|
|
|
|
defp quoted_bitstring_modifier?({modifier, _, ctx}) when is_atom(ctx) or ctx == [],
|
|
do: :elixir_bitstring.validate_spec(modifier, nil) != :none
|
|
|
|
defp quoted_bitstring_modifier?(_other), do: false
|
|
|
|
@doc false
|
|
@deprecated "Use Macro.expand_literals/2 instead"
|
|
def expand_literal(ast, env) do
|
|
expand_literals(ast, env)
|
|
end
|
|
|
|
@doc """
|
|
Expands all literals in `ast` with the given `env`.
|
|
|
|
This function is mostly used to remove compile-time dependencies
|
|
from AST nodes. In such cases, the given environment is usually
|
|
manipulated to represent a function:
|
|
|
|
Macro.expand_literals(ast, %{env | function: {:my_code, 1}})
|
|
|
|
At the moment, the only expandable literal nodes in an AST are
|
|
aliases, so this function only expands aliases (and it does so
|
|
anywhere in a literal).
|
|
|
|
However, be careful when removing compile-time dependencies between
|
|
modules. If you remove them but you still invoke the module at
|
|
compile-time, Elixir will be unable to properly recompile modules
|
|
when they change.
|
|
"""
|
|
@doc since: "1.14.1"
|
|
@spec expand_literals(input(), Macro.Env.t()) :: output()
|
|
def expand_literals(ast, env) do
|
|
{ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end)
|
|
ast
|
|
end
|
|
|
|
@doc """
|
|
Expands all literals in `ast` with the given `acc` and `fun`.
|
|
|
|
`fun` will be invoked with an expandable AST node and `acc` and
|
|
must return a new node with `acc`. This is a general version of
|
|
`expand_literals/2` which supports a custom expansion function.
|
|
Please check `expand_literals/2` for use cases and pitfalls.
|
|
"""
|
|
@doc since: "1.14.1"
|
|
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
|
|
def expand_literals(ast, acc, fun)
|
|
|
|
def expand_literals({:__aliases__, meta, args}, acc, fun) do
|
|
{args, acc} = expand_literals(args, acc, fun)
|
|
|
|
if :lists.all(&is_atom/1, args) do
|
|
fun.({:__aliases__, meta, args}, acc)
|
|
else
|
|
{{:__aliases__, meta, args}, acc}
|
|
end
|
|
end
|
|
|
|
def expand_literals({:__MODULE__, _meta, ctx} = node, acc, fun) when is_atom(ctx) do
|
|
fun.(node, acc)
|
|
end
|
|
|
|
def expand_literals({:%, meta, [left, right]}, acc, fun) do
|
|
{left, acc} = expand_literals(left, acc, fun)
|
|
{right, acc} = expand_literals(right, acc, fun)
|
|
{{:%, meta, [left, right]}, acc}
|
|
end
|
|
|
|
def expand_literals({:%{}, meta, args}, acc, fun) do
|
|
{args, acc} = expand_literals(args, acc, fun)
|
|
{{:%{}, meta, args}, acc}
|
|
end
|
|
|
|
def expand_literals({:{}, meta, args}, acc, fun) do
|
|
{args, acc} = expand_literals(args, acc, fun)
|
|
{{:{}, meta, args}, acc}
|
|
end
|
|
|
|
def expand_literals({left, right}, acc, fun) do
|
|
{left, acc} = expand_literals(left, acc, fun)
|
|
{right, acc} = expand_literals(right, acc, fun)
|
|
{{left, right}, acc}
|
|
end
|
|
|
|
def expand_literals(list, acc, fun) when is_list(list) do
|
|
:lists.mapfoldl(&expand_literals(&1, &2, fun), acc, list)
|
|
end
|
|
|
|
def expand_literals(
|
|
{{:., _, [{:__aliases__, _, [:Application]}, :compile_env]} = node, meta,
|
|
[app, key, default]},
|
|
acc,
|
|
fun
|
|
) do
|
|
{default, acc} = expand_literals(default, acc, fun)
|
|
{{node, meta, [app, key, default]}, acc}
|
|
end
|
|
|
|
def expand_literals(term, acc, _fun), do: {term, acc}
|
|
|
|
@doc """
|
|
Receives an AST node and expands it until it can no longer
|
|
be expanded.
|
|
|
|
Note this function does not traverse the AST, only the root
|
|
node is expanded.
|
|
|
|
This function uses `expand_once/2` under the hood. Check
|
|
it out for more information and examples.
|
|
"""
|
|
@spec expand(input(), Macro.Env.t()) :: output()
|
|
def expand(ast, env) do
|
|
expand_until({ast, true}, env)
|
|
end
|
|
|
|
defp expand_until({ast, true}, env) do
|
|
expand_until(do_expand_once(ast, env), env)
|
|
end
|
|
|
|
defp expand_until({ast, false}, _env) do
|
|
ast
|
|
end
|
|
|
|
@doc """
|
|
Converts the given argument to a string with the underscore-slash format.
|
|
|
|
The argument must either be an atom or a string.
|
|
If an atom is given, it is assumed to be an Elixir module,
|
|
so it is converted to a string and then processed.
|
|
|
|
This function was designed to format language identifiers/tokens with the underscore-slash format,
|
|
that's why it belongs to the `Macro` module. Do not use it as a general
|
|
mechanism for underscoring strings as it does not support Unicode or
|
|
characters that are not valid in Elixir identifiers.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.underscore("FooBar")
|
|
"foo_bar"
|
|
|
|
iex> Macro.underscore("Foo.Bar")
|
|
"foo/bar"
|
|
|
|
iex> Macro.underscore(Foo.Bar)
|
|
"foo/bar"
|
|
|
|
In general, `underscore` can be thought of as the reverse of
|
|
`camelize`, however, in some cases formatting may be lost:
|
|
|
|
iex> Macro.underscore("SAPExample")
|
|
"sap_example"
|
|
|
|
iex> Macro.camelize("sap_example")
|
|
"SapExample"
|
|
|
|
iex> Macro.camelize("hello_10")
|
|
"Hello10"
|
|
|
|
iex> Macro.camelize("foo/bar")
|
|
"Foo.Bar"
|
|
|
|
"""
|
|
@spec underscore(module() | atom() | String.t()) :: String.t()
|
|
def underscore(atom_or_string)
|
|
|
|
def underscore(atom) when is_atom(atom) do
|
|
"Elixir." <> rest = Atom.to_string(atom)
|
|
underscore(rest)
|
|
end
|
|
|
|
def underscore(<<h, t::binary>>) do
|
|
<<to_lower_char(h)>> <> do_underscore(t, h)
|
|
end
|
|
|
|
def underscore("") do
|
|
""
|
|
end
|
|
|
|
defp do_underscore(<<h, t, rest::binary>>, _)
|
|
when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and not (t >= ?0 and t <= ?9) and
|
|
t != ?. and t != ?_ do
|
|
<<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
|
|
end
|
|
|
|
defp do_underscore(<<h, t::binary>>, prev)
|
|
when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ do
|
|
<<?_, to_lower_char(h)>> <> do_underscore(t, h)
|
|
end
|
|
|
|
defp do_underscore(<<?., t::binary>>, _) do
|
|
<<?/>> <> underscore(t)
|
|
end
|
|
|
|
defp do_underscore(<<h, t::binary>>, _) do
|
|
<<to_lower_char(h)>> <> do_underscore(t, h)
|
|
end
|
|
|
|
defp do_underscore(<<>>, _) do
|
|
<<>>
|
|
end
|
|
|
|
@doc """
|
|
Converts the given string to CamelCase format.
|
|
|
|
This function was designed to camelize language identifiers/tokens,
|
|
that's why it belongs to the `Macro` module. Do not use it as a general
|
|
mechanism for camelizing strings as it does not support Unicode or
|
|
characters that are not valid in Elixir identifiers.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.camelize("foo_bar")
|
|
"FooBar"
|
|
|
|
iex> Macro.camelize("foo/bar")
|
|
"Foo.Bar"
|
|
|
|
If uppercase characters are present, they are not modified in any way
|
|
as a mechanism to preserve acronyms:
|
|
|
|
iex> Macro.camelize("API.V1")
|
|
"API.V1"
|
|
iex> Macro.camelize("API_SPEC")
|
|
"API_SPEC"
|
|
|
|
"""
|
|
@spec camelize(String.t()) :: String.t()
|
|
def camelize(string)
|
|
|
|
def camelize(""), do: ""
|
|
def camelize(<<?_, t::binary>>), do: camelize(t)
|
|
def camelize(<<h, t::binary>>), do: <<to_upper_char(h)>> <> do_camelize(t)
|
|
|
|
defp do_camelize(<<?_, ?_, t::binary>>), do: do_camelize(<<?_, t::binary>>)
|
|
|
|
defp do_camelize(<<?_, h, t::binary>>) when h >= ?a and h <= ?z,
|
|
do: <<to_upper_char(h)>> <> do_camelize(t)
|
|
|
|
defp do_camelize(<<?_, h, t::binary>>) when h >= ?0 and h <= ?9, do: <<h>> <> do_camelize(t)
|
|
defp do_camelize(<<?_>>), do: <<>>
|
|
defp do_camelize(<<?/, t::binary>>), do: <<?.>> <> camelize(t)
|
|
defp do_camelize(<<h, t::binary>>), do: <<h>> <> do_camelize(t)
|
|
defp do_camelize(<<>>), do: <<>>
|
|
|
|
defp to_upper_char(char) when char >= ?a and char <= ?z, do: char - 32
|
|
defp to_upper_char(char), do: char
|
|
|
|
defp to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32
|
|
defp to_lower_char(char), do: char
|
|
|
|
## Atom handling
|
|
|
|
@doc """
|
|
Classifies an `atom` based on its possible AST placement.
|
|
|
|
It returns one of the following atoms:
|
|
|
|
* `:alias` - the atom represents an alias
|
|
|
|
* `:identifier` - the atom can be used as a variable or local function
|
|
call (as well as be an unquoted atom)
|
|
|
|
* `:unquoted` - the atom can be used in its unquoted form,
|
|
includes operators and atoms with `@` in them
|
|
|
|
* `:quoted` - all other atoms which can only be used in their quoted form
|
|
|
|
Most operators are going to be `:unquoted`, such as `:+`, with
|
|
some exceptions returning `:quoted` due to ambiguity, such as
|
|
`:"::"`. Use `operator?/2` to check if a given atom is an operator.
|
|
|
|
## Examples
|
|
|
|
iex> Macro.classify_atom(:foo)
|
|
:identifier
|
|
iex> Macro.classify_atom(Foo)
|
|
:alias
|
|
iex> Macro.classify_atom(:foo@bar)
|
|
:unquoted
|
|
iex> Macro.classify_atom(:+)
|
|
:unquoted
|
|
iex> Macro.classify_atom(:Foo)
|
|
:unquoted
|
|
iex> Macro.classify_atom(:"with spaces")
|
|
:quoted
|
|
|
|
"""
|
|
@doc since: "1.14.0"
|
|
@spec classify_atom(atom) :: :alias | :identifier | :quoted | :unquoted
|
|
def classify_atom(atom) do
|
|
case inner_classify(atom) do
|
|
:alias -> :alias
|
|
:identifier -> :identifier
|
|
type when type in [:unquoted_operator, :not_callable] -> :unquoted
|
|
_ -> :quoted
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Inspects `atom` according to different source formats.
|
|
|
|
The atom can be inspected according to the three different
|
|
formats it appears in the AST: as a literal (`:literal`),
|
|
as a key (`:key`), or as the function name of a remote call
|
|
(`:remote_call`).
|
|
|
|
## Examples
|
|
|
|
### As a literal
|
|
|
|
Literals include regular atoms, quoted atoms, operators,
|
|
aliases, and the special `nil`, `true`, and `false` atoms.
|
|
|
|
iex> Macro.inspect_atom(:literal, nil)
|
|
"nil"
|
|
iex> Macro.inspect_atom(:literal, :foo)
|
|
":foo"
|
|
iex> Macro.inspect_atom(:literal, :<>)
|
|
":<>"
|
|
iex> Macro.inspect_atom(:literal, :Foo)
|
|
":Foo"
|
|
iex> Macro.inspect_atom(:literal, Foo.Bar)
|
|
"Foo.Bar"
|
|
iex> Macro.inspect_atom(:literal, :"with spaces")
|
|
":\"with spaces\""
|
|
|
|
### As a key
|
|
|
|
Inspect an atom as a key of a keyword list or a map.
|
|
|
|
iex> Macro.inspect_atom(:key, :foo)
|
|
"foo:"
|
|
iex> Macro.inspect_atom(:key, :<>)
|
|
"<>:"
|
|
iex> Macro.inspect_atom(:key, :Foo)
|
|
"Foo:"
|
|
iex> Macro.inspect_atom(:key, :"with spaces")
|
|
"\"with spaces\":"
|
|
|
|
### As a remote call
|
|
|
|
Inspect an atom the function name of a remote call.
|
|
|
|
iex> Macro.inspect_atom(:remote_call, :foo)
|
|
"foo"
|
|
iex> Macro.inspect_atom(:remote_call, :<>)
|
|
"<>"
|
|
iex> Macro.inspect_atom(:remote_call, :Foo)
|
|
"\"Foo\""
|
|
iex> Macro.inspect_atom(:remote_call, :"with spaces")
|
|
"\"with spaces\""
|
|
|
|
"""
|
|
@doc since: "1.14.0"
|
|
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
|
|
def inspect_atom(source_format, atom)
|
|
|
|
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
|
|
Atom.to_string(atom)
|
|
end
|
|
|
|
def inspect_atom(:literal, atom) when is_atom(atom) do
|
|
binary = Atom.to_string(atom)
|
|
|
|
case classify_atom(atom) do
|
|
:alias ->
|
|
case binary do
|
|
binary when binary in ["Elixir", "Elixir.Elixir"] -> binary
|
|
"Elixir.Elixir." <> _rest -> binary
|
|
"Elixir." <> rest -> rest
|
|
end
|
|
|
|
:quoted ->
|
|
{escaped, _} = Code.Identifier.escape(binary, ?")
|
|
IO.iodata_to_binary([?:, ?", escaped, ?"])
|
|
|
|
_ ->
|
|
":" <> binary
|
|
end
|
|
end
|
|
|
|
def inspect_atom(:key, atom) when is_atom(atom) do
|
|
binary = Atom.to_string(atom)
|
|
|
|
case classify_atom(atom) do
|
|
:alias ->
|
|
IO.iodata_to_binary([?", binary, ?", ?:])
|
|
|
|
:quoted ->
|
|
{escaped, _} = Code.Identifier.escape(binary, ?")
|
|
IO.iodata_to_binary([?", escaped, ?", ?:])
|
|
|
|
_ ->
|
|
IO.iodata_to_binary([binary, ?:])
|
|
end
|
|
end
|
|
|
|
def inspect_atom(:remote_call, atom) when is_atom(atom) do
|
|
binary = Atom.to_string(atom)
|
|
|
|
case inner_classify(atom) do
|
|
type when type in [:identifier, :unquoted_operator, :quoted_operator] ->
|
|
binary
|
|
|
|
type ->
|
|
escaped =
|
|
if type in [:not_callable, :alias] do
|
|
binary
|
|
else
|
|
elem(Code.Identifier.escape(binary, ?"), 0)
|
|
end
|
|
|
|
IO.iodata_to_binary([?", escaped, ?"])
|
|
end
|
|
end
|
|
|
|
# Classifies the given atom into one of the following categories:
|
|
#
|
|
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
|
|
#
|
|
# * `:identifier` - an atom that can be used as a variable/local call;
|
|
# this category includes identifiers like `:foo`
|
|
#
|
|
# * `:unquoted_operator` - all callable operators, such as `:<>`. Note
|
|
# operators such as `:..` are not callable because of ambiguity
|
|
#
|
|
# * `:quoted_operator` - callable operators that must be wrapped in quotes when
|
|
# defined as an atom. For example, `::` must be written as `:"::"` to avoid
|
|
# the ambiguity between the atom and the keyword identifier
|
|
#
|
|
# * `:not_callable` - an atom that cannot be used as a function call after the
|
|
# `.` operator. Those are typically AST nodes that are special forms (such as
|
|
# `:%{}` and `:<<>>>`) as well as nodes that are ambiguous in calls (such as
|
|
# `:..` and `:...`). This category also includes atoms like `:Foo`, since
|
|
# they are valid identifiers but they need quotes to be used in function
|
|
# calls (`Foo."Bar"`)
|
|
#
|
|
# * `:other` - any other atom (these are usually escaped when inspected, like
|
|
# `:"foo and bar"`)
|
|
#
|
|
defp inner_classify(atom) when is_atom(atom) do
|
|
cond do
|
|
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
|
|
:not_callable
|
|
|
|
# <|>, ^^^, and ~~~ are deprecated
|
|
atom in [:"::", :"^^^", :"~~~", :"<|>"] ->
|
|
:quoted_operator
|
|
|
|
operator?(atom, 1) or operator?(atom, 2) ->
|
|
:unquoted_operator
|
|
|
|
true ->
|
|
charlist = Atom.to_charlist(atom)
|
|
|
|
if valid_alias?(charlist) do
|
|
:alias
|
|
else
|
|
case :elixir_config.identifier_tokenizer().tokenize(charlist) do
|
|
{kind, _acc, [], _, _, special} ->
|
|
cond do
|
|
kind != :identifier or :lists.member(:at, special) ->
|
|
:not_callable
|
|
|
|
# identifier_tokenizer used to return errors for non-nfc, but
|
|
# now it nfc-normalizes everything. However, lack of nfc is
|
|
# still a good reason to quote an atom when printing.
|
|
:lists.member(:nfkc, special) ->
|
|
:other
|
|
|
|
true ->
|
|
:identifier
|
|
end
|
|
|
|
_ ->
|
|
:other
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp valid_alias?([?E, ?l, ?i, ?x, ?i, ?r] ++ rest), do: valid_alias_piece?(rest)
|
|
defp valid_alias?(_other), do: false
|
|
|
|
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
|
|
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
|
|
|
|
defp valid_alias_piece?([]), do: true
|
|
defp valid_alias_piece?(_other), do: false
|
|
|
|
defp trim_leading_while_valid_identifier([char | rest])
|
|
when char >= ?a and char <= ?z
|
|
when char >= ?A and char <= ?Z
|
|
when char >= ?0 and char <= ?9
|
|
when char == ?_ do
|
|
trim_leading_while_valid_identifier(rest)
|
|
end
|
|
|
|
defp trim_leading_while_valid_identifier(other) do
|
|
other
|
|
end
|
|
|
|
@doc """
|
|
Default backend for `Kernel.dbg/2`.
|
|
|
|
This function provides a default backend for `Kernel.dbg/2`. See the
|
|
`Kernel.dbg/2` documentation for more information.
|
|
|
|
This function:
|
|
|
|
* prints information about the given `env`
|
|
* prints information about `code` and its returned value (using `opts` to inspect terms)
|
|
* returns the value returned by evaluating `code`
|
|
|
|
You can call this function directly to build `Kernel.dbg/2` backends that fall back
|
|
to this function.
|
|
|
|
This function raises if the context of the given `env` is `:match` or `:guard`.
|
|
"""
|
|
@doc since: "1.14.0"
|
|
@spec dbg(t, t, Macro.Env.t()) :: t
|
|
def dbg(code, options, %Macro.Env{} = env) do
|
|
case env.context do
|
|
:match ->
|
|
raise ArgumentError,
|
|
"invalid expression in match, dbg is not allowed in patterns " <>
|
|
"such as function clauses, case clauses or on the left side of the = operator"
|
|
|
|
:guard ->
|
|
raise ArgumentError,
|
|
"invalid expression in guard, dbg is not allowed in guards. " <>
|
|
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
|
|
header = dbg_format_header(env)
|
|
|
|
quote do
|
|
to_debug = unquote(dbg_ast_to_debuggable(code))
|
|
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
|
|
end
|
|
end
|
|
|
|
# Pipelines.
|
|
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
|
|
value_var = unique_var(:value, __MODULE__)
|
|
values_acc_var = unique_var(:values, __MODULE__)
|
|
|
|
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
|
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
|
|
|
|
initial_acc =
|
|
quote do
|
|
unquote(value_var) = unquote(start_ast)
|
|
unquote(values_acc_var) = [unquote(value_var)]
|
|
end
|
|
|
|
values_ast =
|
|
for step_ast <- rest_asts, reduce: initial_acc do
|
|
ast_acc ->
|
|
quote do
|
|
unquote(ast_acc)
|
|
unquote(value_var) = unquote(step_ast)
|
|
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
|
|
end
|
|
end
|
|
|
|
quote do
|
|
unquote(values_ast)
|
|
|
|
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
|
|
end
|
|
end
|
|
|
|
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
|
|
|
|
# Logic operators.
|
|
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
|
|
when op in unquote(dbg_decomposed_binary_operators) do
|
|
acc_var = unique_var(:acc, __MODULE__)
|
|
result_var = unique_var(:result, __MODULE__)
|
|
|
|
quote do
|
|
unquote(acc_var) = []
|
|
unquote(dbg_boolean_tree(ast, acc_var, result_var))
|
|
{:logic_op, Enum.reverse(unquote(acc_var))}
|
|
end
|
|
end
|
|
|
|
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
|
|
clauses_returning_index =
|
|
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
|
|
{:->, meta, [left, {right, index}]}
|
|
end)
|
|
|
|
quote do
|
|
expr = unquote(expr)
|
|
|
|
{result, clause_index} =
|
|
case expr do
|
|
unquote(clauses_returning_index)
|
|
end
|
|
|
|
{:case, unquote(escape(ast)), expr, clause_index, result}
|
|
end
|
|
end
|
|
|
|
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
|
|
modified_clauses =
|
|
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
|
|
hd(
|
|
quote do
|
|
clause_value = unquote(left) ->
|
|
{unquote(escape(left)), clause_value, unquote(index), unquote(right)}
|
|
end
|
|
)
|
|
end)
|
|
|
|
quote do
|
|
{clause_ast, clause_value, clause_index, value} =
|
|
cond do
|
|
unquote(modified_clauses)
|
|
end
|
|
|
|
{:cond, unquote(escape(ast)), clause_ast, clause_value, clause_index, value}
|
|
end
|
|
end
|
|
|
|
# Any other AST.
|
|
defp dbg_ast_to_debuggable(ast) do
|
|
quote do: {:value, unquote(escape(ast)), unquote(ast)}
|
|
end
|
|
|
|
# This is a binary operator. We replace the left side with a recursive call to
|
|
# this function to decompose it, and then execute the operation and add it to the acc.
|
|
defp dbg_boolean_tree({op, _meta, [left, right]} = ast, acc_var, result_var)
|
|
when op in unquote(dbg_decomposed_binary_operators) do
|
|
replaced_left = dbg_boolean_tree(left, acc_var, result_var)
|
|
|
|
quote do
|
|
unquote(result_var) = unquote(op)(unquote(replaced_left), unquote(right))
|
|
|
|
unquote(acc_var) = [
|
|
{unquote(escape(ast)), unquote(result_var)} | unquote(acc_var)
|
|
]
|
|
|
|
unquote(result_var)
|
|
end
|
|
end
|
|
|
|
# This is finally an expression, so we assign "result = expr", add it to the acc, and
|
|
# return the result.
|
|
defp dbg_boolean_tree(ast, acc_var, result_var) do
|
|
quote do
|
|
unquote(result_var) = unquote(ast)
|
|
unquote(acc_var) = [{unquote(escape(ast)), unquote(result_var)} | unquote(acc_var)]
|
|
unquote(result_var)
|
|
end
|
|
end
|
|
|
|
# Made public to be called from Macro.dbg/3, so that we generate as little code
|
|
# as possible and call out into a function as soon as we can.
|
|
@doc false
|
|
def __dbg__(header_string, to_debug, options) do
|
|
{print_location?, options} = Keyword.pop(options, :print_location, true)
|
|
syntax_colors = if IO.ANSI.enabled?(), do: IO.ANSI.syntax_colors(), else: []
|
|
options = Keyword.merge([width: 80, pretty: true, syntax_colors: syntax_colors], options)
|
|
|
|
{formatted, result} = dbg_format_ast_to_debug(to_debug, options)
|
|
|
|
formatted =
|
|
if print_location? do
|
|
[:cyan, :italic, header_string, :reset, "\n", formatted, "\n"]
|
|
else
|
|
[formatted, "\n"]
|
|
end
|
|
|
|
ansi_enabled? = options[:syntax_colors] != []
|
|
:ok = IO.write(IO.ANSI.format(formatted, ansi_enabled?))
|
|
|
|
result
|
|
end
|
|
|
|
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
|
|
result = List.last(values)
|
|
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
|
|
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
|
|
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
|
|
|
|
rest_formatted =
|
|
Enum.map(asts_with_values, fn {code_ast, value} ->
|
|
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
|
|
end)
|
|
|
|
{[first_formatted | rest_formatted], result}
|
|
end
|
|
|
|
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
|
|
{_ast, final_value} = List.last(components)
|
|
|
|
formatted =
|
|
Enum.map(components, fn {ast, value} ->
|
|
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
|
|
end)
|
|
|
|
{formatted, final_value}
|
|
end
|
|
|
|
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
|
|
{:case, _meta, [expr_ast, _]} = ast
|
|
|
|
formatted = [
|
|
dbg_maybe_underline("Case argument", options),
|
|
":\n",
|
|
dbg_format_ast_with_value(expr_ast, expr_value, options),
|
|
?\n,
|
|
dbg_maybe_underline("Case expression", options),
|
|
" (clause ##{clause_index + 1} matched):\n",
|
|
dbg_format_ast_with_value(ast, value, options)
|
|
]
|
|
|
|
{formatted, value}
|
|
end
|
|
|
|
defp dbg_format_ast_to_debug(
|
|
{:cond, ast, clause_ast, clause_value, clause_index, value},
|
|
options
|
|
) do
|
|
formatted = [
|
|
dbg_maybe_underline("Cond clause", options),
|
|
" (clause ##{clause_index + 1} matched):\n",
|
|
dbg_format_ast_with_value(clause_ast, clause_value, options),
|
|
?\n,
|
|
dbg_maybe_underline("Cond expression", options),
|
|
":\n",
|
|
dbg_format_ast_with_value(ast, value, options)
|
|
]
|
|
|
|
{formatted, value}
|
|
end
|
|
|
|
defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do
|
|
{dbg_format_ast_with_value(code_ast, value, options), value}
|
|
end
|
|
|
|
defp dbg_format_ast_with_value(ast, value, options) do
|
|
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
|
|
end
|
|
|
|
defp to_string_with_colors(ast, options) do
|
|
options = Keyword.take(options, [:syntax_colors])
|
|
|
|
algebra = Code.quoted_to_algebra(ast, options)
|
|
IO.iodata_to_binary(Inspect.Algebra.format(algebra, 98))
|
|
end
|
|
|
|
defp dbg_format_header(env) do
|
|
env = Map.update!(env, :file, &(&1 && Path.relative_to_cwd(&1)))
|
|
[stacktrace_entry] = Macro.Env.stacktrace(env)
|
|
"[" <> Exception.format_stacktrace_entry(stacktrace_entry) <> "]"
|
|
end
|
|
|
|
defp dbg_maybe_underline(string, options) do
|
|
if options[:syntax_colors] != [] do
|
|
IO.ANSI.format([:underline, string, :reset])
|
|
else
|
|
string
|
|
end
|
|
end
|
|
|
|
defp dbg_format_ast(ast) do
|
|
[ast, :faint, " #=>", :reset]
|
|
end
|
|
end
|