489 lines
14 KiB
Elixir
489 lines
14 KiB
Elixir
import Kernel, except: [to_binary: 1]
|
|
|
|
defmodule Macro do
|
|
@moduledoc """
|
|
This module provides conveniences for working with macros.
|
|
"""
|
|
|
|
@doc """
|
|
Returns a list of binary operators. This is available
|
|
as a macro so it can be used in guard clauses.
|
|
"""
|
|
defmacro binary_ops do
|
|
[
|
|
:===, :!==,
|
|
:==, :!=, :<=, :>=,
|
|
:&&, :||, :<>, :++, :--, :**, ://, :::, :<-, :.., :/>, :=~,
|
|
:<, :>,
|
|
:+, :-, :*, :/, :=, :|, :.,
|
|
:and, :or, :xor, :when, :in, :inlist, :inbits,
|
|
:<<<, :>>>, :|||, :&&&, :^^^, :~~~
|
|
]
|
|
end
|
|
|
|
@doc """
|
|
Returns a list of unary operators. This is available
|
|
as a macro so it can be used in guard clauses.
|
|
"""
|
|
defmacro unary_ops do
|
|
[:!, :@, :^, :not, :+, :-]
|
|
end
|
|
|
|
@doc """
|
|
Recursively escapes the given value so it can be inserted
|
|
into a syntax tree. Structures that are valid syntax nodes
|
|
(like atoms, integers, binaries) are represented by themselves.
|
|
|
|
## Examples
|
|
|
|
Macro.escape(:foo)
|
|
#=> :foo
|
|
|
|
Macro.escape({ :a, :b, :c })
|
|
#=> { :{}, 0, [:a, :b, :c] }
|
|
"""
|
|
def escape({ left, right }) do
|
|
{ escape(left), escape(right) }
|
|
end
|
|
|
|
def escape(tuple) when is_tuple(tuple) do
|
|
{ :{}, 0, escape(tuple_to_list(tuple)) }
|
|
end
|
|
|
|
def escape(list) when is_list(list) do
|
|
lc item inlist list, do: escape(item)
|
|
end
|
|
|
|
def escape(other), do: other
|
|
|
|
@doc %B"""
|
|
Unescape the given chars. This is the unescaping behavior
|
|
used by default in Elixir single- and double-quoted strings.
|
|
Check `unescape_binary/2` for information on how to customize
|
|
the escaping map.
|
|
|
|
In this setup, Elixir will escape the following: `\b`, `\d`,
|
|
`\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Octals are also
|
|
escaped according to the latin1 set they represent.
|
|
|
|
This function is commonly used on sigil implementations
|
|
(like `%r`, `%b` and others).
|
|
|
|
## Examples
|
|
|
|
Macro.unescape_binary "example\\n"
|
|
#=> "example\n"
|
|
|
|
In the example above, we pass a string with `\n` escaped
|
|
and we return a version with it unescaped.
|
|
"""
|
|
def unescape_binary(chars) do
|
|
:elixir_interpolation.unescape_chars(chars)
|
|
end
|
|
|
|
@doc %B"""
|
|
Unescape the given chars according to the map given.
|
|
Check `unescape/1` if you want to use the same map as
|
|
Elixir single- and double-quoted strings.
|
|
|
|
## Map
|
|
|
|
The map must be a function. The function receives an integer
|
|
representing the number of the characters it wants to unescape.
|
|
Here is the default mapping function implemented by Elixir:
|
|
|
|
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` function returns false. The char is
|
|
not escaped and `\` is kept in the char list.
|
|
|
|
## Octals
|
|
|
|
Octals will by default be escaped unless the map function
|
|
returns false for ?0.
|
|
|
|
## Examples
|
|
|
|
Using the unescape_map defined above is easy:
|
|
|
|
Macro.unescape_binary "example\\n", unescape_map(&1)
|
|
|
|
"""
|
|
def unescape_binary(chars, map) do
|
|
:elixir_interpolation.unescape_chars(chars, map)
|
|
end
|
|
|
|
@doc """
|
|
Unescape the given tokens according to the default map.
|
|
Check `unescape/1` and `unescape/2` for more information
|
|
about unescaping. Only tokens that are binaries are
|
|
unescaped, all others are ignored. This method is useful
|
|
when implementing your own sigils. Check the implementation
|
|
of `Kernel.__b__` for examples.
|
|
"""
|
|
def unescape_tokens(tokens) do
|
|
:elixir_interpolation.unescape_tokens(tokens)
|
|
end
|
|
|
|
@doc """
|
|
Unescape the given tokens according to the given map.
|
|
Check `unescape_tokens/1` and `unescaped/2` for more information.
|
|
"""
|
|
def unescape_tokens(tokens, map) do
|
|
:elixir_interpolation.unescape_tokens(tokens, map)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given expression to a binary.
|
|
|
|
## Examples
|
|
|
|
Macro.to_binary(quote do: foo.bar(1, 2, 3))
|
|
#=> "foo.bar(1, 2, 3)"
|
|
|
|
"""
|
|
def to_binary(tree)
|
|
|
|
# Variables
|
|
def to_binary({ var, _, atom }) when is_atom(atom) do
|
|
atom_to_binary(var, :utf8)
|
|
end
|
|
|
|
# Aliases
|
|
def to_binary({ :__aliases__, _, refs }) do
|
|
Enum.map_join(refs, ".", call_to_binary(&1))
|
|
end
|
|
|
|
# Blocks
|
|
def to_binary({ :__block__, _, [expr] }) do
|
|
to_binary(expr)
|
|
end
|
|
|
|
def to_binary({ :__block__, _, _ } = expr) do
|
|
block = adjust_new_lines block_to_binary(expr), "\n "
|
|
"(\n " <> block <> "\n)"
|
|
end
|
|
|
|
# Bits containers
|
|
def to_binary({ :<<>>, _, args }) do
|
|
"<<" <> Enum.map_join(args, ", ", to_binary(&1)) <> ">>"
|
|
end
|
|
|
|
# Tuple containers
|
|
def to_binary({ :{}, _, args }) do
|
|
"{" <> Enum.map_join(args, ", ", to_binary(&1)) <> "}"
|
|
end
|
|
|
|
# List containers
|
|
def to_binary({ :[], _, args }) do
|
|
"[" <> Enum.map_join(args, ", ", to_binary(&1)) <> "]"
|
|
end
|
|
|
|
# Fn keyword
|
|
def to_binary({ :fn, _, [[do: block]] }) do
|
|
"fn " <> block_to_binary(block) <> "\nend"
|
|
end
|
|
|
|
# Partial call
|
|
def to_binary({ :&, _, [num] }) do
|
|
"&#{num}"
|
|
end
|
|
|
|
# Binary ops
|
|
def to_binary({ op, _, [left, right] }) when op in binary_ops do
|
|
op_to_binary(left) <> " #{op} " <> op_to_binary(right)
|
|
end
|
|
|
|
# Unary ops
|
|
def to_binary({ op, _, [arg] }) when op in unary_ops do
|
|
atom_to_binary(op, :utf8) <> to_binary(arg)
|
|
end
|
|
|
|
# All other calls
|
|
def to_binary({ target, _, args }) when is_list(args) do
|
|
{ list, last } = :elixir_tree_helpers.split_last(args)
|
|
case is_kw_blocks?(last) do
|
|
true -> call_to_binary_with_args(target, list) <> kw_blocks_to_binary(last)
|
|
false -> call_to_binary_with_args(target, args)
|
|
end
|
|
end
|
|
|
|
# Two-item tuples
|
|
def to_binary({ left, right }) do
|
|
to_binary({ :{}, 0, [left, right] })
|
|
end
|
|
|
|
# Lists
|
|
def to_binary(list) when is_list(list) do
|
|
to_binary({ :[], 0, list })
|
|
end
|
|
|
|
# All other structures
|
|
def to_binary(other), do: Binary.Inspect.inspect(other, raw: true)
|
|
|
|
# Block keywords
|
|
defmacrop kw_keywords, do: [:do, :catch, :rescue, :after, :else]
|
|
|
|
defp is_kw_blocks?([_|_] = kw) do
|
|
Enum.all?(kw, match?({x, _} when x in kw_keywords, &1))
|
|
end
|
|
defp is_kw_blocks?(_), do: false
|
|
|
|
defp call_to_binary(atom) when is_atom(atom), do: atom_to_binary(atom, :utf8)
|
|
defp call_to_binary({ :., _, [arg] }), do: call_to_binary(arg) <> "."
|
|
defp call_to_binary({ :., _, [left, right] }), do: call_to_binary(left) <> "." <> call_to_binary(right)
|
|
defp call_to_binary(other), do: to_binary(other)
|
|
|
|
defp call_to_binary_with_args(target, args) do
|
|
args = Enum.map_join(args, ", ", to_binary(&1))
|
|
call_to_binary(target) <> "(" <> args <> ")"
|
|
end
|
|
|
|
defp kw_blocks_to_binary(kw) do
|
|
Enum.reduce(kw_keywords, " ", fn(x, acc) ->
|
|
case Keyword.has_key?(kw, x) do
|
|
true -> acc <> kw_block_to_binary(x, Keyword.get(kw, x))
|
|
false -> acc
|
|
end
|
|
end) <> "end"
|
|
end
|
|
|
|
defp kw_block_to_binary(key, value) do
|
|
block = adjust_new_lines block_to_binary(value), "\n "
|
|
atom_to_binary(key, :utf8) <> "\n " <> block <> "\n"
|
|
end
|
|
|
|
defp block_to_binary({ :->, _, exprs }) do
|
|
Enum.map_join(exprs, "\n", fn({ left, right }) ->
|
|
left = Enum.map_join(left, ", ", to_binary(&1))
|
|
left <> " ->\n " <> adjust_new_lines block_to_binary(right), "\n "
|
|
end)
|
|
end
|
|
|
|
defp block_to_binary({ :__block__, _, exprs }) do
|
|
Enum.map_join(exprs, "\n", to_binary(&1))
|
|
end
|
|
|
|
defp block_to_binary(other), do: to_binary(other)
|
|
|
|
defp op_to_binary({ op, _, [_, _] } = expr) when op in binary_ops do
|
|
"(" <> to_binary(expr) <> ")"
|
|
end
|
|
|
|
defp op_to_binary(expr), do: to_binary(expr)
|
|
|
|
defp adjust_new_lines(block, replacement) do
|
|
bc <<x>> inbits block do
|
|
<< case x == ?\n do
|
|
true -> replacement
|
|
false -> <<x>>
|
|
end :: binary >>
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Receives an expression representation and expands it. The following
|
|
contents are expanded:
|
|
|
|
* Macros (local or remote);
|
|
* Aliases are expanded (if possible) and return atoms;
|
|
* All pseudo-variables (__FILE__, __MODULE__, etc);
|
|
|
|
In case the expression cannot be expanded, it returns the expression itself.
|
|
|
|
Notice that `Macro.expand` is not recursive and it does not
|
|
expand child expressions. For example, `!some_macro` will expand as:
|
|
|
|
iex> IO.puts Macro.to_binary Macro.expand(quote(do: !some_macro), __ENV__)
|
|
case some_macro do
|
|
false -> true
|
|
nil -> true
|
|
_ -> false
|
|
end
|
|
|
|
Notice that the `!` operator is a macro that expands to a case.
|
|
Even though `some_macro` is also a macro, it is not expanded
|
|
because it is a child expression given to `!` as argument.
|
|
|
|
## 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_list(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 follow:
|
|
|
|
{:__aliases__, 0, [: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 straight-forward 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`, 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_list(expanded))
|
|
|
|
quote do
|
|
defmodule unquote(name) do
|
|
def name_length, do: unquote(length)
|
|
unquote(block)
|
|
end
|
|
end
|
|
end
|
|
|
|
"""
|
|
def expand(aliases, env)
|
|
|
|
# The first case we handle is __aliases__. In case
|
|
# aliases just contain one item, we are sure it is
|
|
# an atom, so we just expand it based on the aliases
|
|
# dict.
|
|
def expand({ :__aliases__, _, [h] }, env) when h != Elixir do
|
|
expand_alias(h, env)
|
|
end
|
|
|
|
# In case aliases contains more than one item, we need
|
|
# to loop them checking if they are all atoms or not.
|
|
# Macros and pseudo-variables are then expanded.
|
|
def expand({ :__aliases__, _, [h|t] } = original, env) do
|
|
aliases = case h do
|
|
x when is_atom(x) and x != Elixir -> [expand_alias(x, env)|t]
|
|
_ -> [h|t]
|
|
end
|
|
|
|
aliases = lc alias inlist aliases, do: expand(alias, env)
|
|
|
|
case :lists.all(is_atom(&1), aliases) do
|
|
true -> :elixir_aliases.concat(aliases)
|
|
false -> original
|
|
end
|
|
end
|
|
|
|
# Expand @ calls
|
|
def expand({ :@, _, [{ name, _, args }] } = original, env) when is_atom(args) or args == [] do
|
|
case (module = env.module) && Module.open?(module) do
|
|
true -> Module.get_attribute(module, name)
|
|
false -> original
|
|
end
|
|
end
|
|
|
|
# Expand pseudo-variables
|
|
def expand({ :__MODULE__, _, atom }, env) when is_atom(atom), do: env.module
|
|
def expand({ :__FILE__, _, atom }, env) when is_atom(atom), do: env.file
|
|
def expand({ :__ENV__, _, atom }, env) when is_atom(atom), do: env
|
|
|
|
# Expand possible macro import invocation
|
|
def expand({ atom, line, args } = original, env) when is_atom(atom) do
|
|
args = case is_atom(args) do
|
|
true -> []
|
|
false -> args
|
|
end
|
|
|
|
case not is_partial?(args) do
|
|
false -> original
|
|
true ->
|
|
expand = :elixir_dispatch.expand_import(line, { atom, length(args) }, args,
|
|
env.module, env.function, env.requires, env.macros, env)
|
|
case expand do
|
|
{ :ok, _, expanded } -> expanded
|
|
{ :error, _ } -> original
|
|
end
|
|
end
|
|
end
|
|
|
|
# Expand possible macro require invocation
|
|
def expand({ { :., _, [left, right] }, line, args } = original, env) when is_atom(right) do
|
|
receiver = expand(left, env)
|
|
|
|
case is_atom(receiver) and not is_partial?(args) do
|
|
false -> original
|
|
true ->
|
|
expand = :elixir_dispatch.expand_require(line, receiver, { right, length(args) },
|
|
args, env.module, env.function, env.requires, env)
|
|
case expand do
|
|
{ :ok, expanded } -> expanded
|
|
{ :error, _ } -> original
|
|
end
|
|
end
|
|
end
|
|
|
|
# Anything else is just returned
|
|
def expand(other, _env), do: other
|
|
|
|
## Helpers
|
|
|
|
defp is_partial?(args) do
|
|
:lists.any(match?({ :&, _, [_] }, &1), args)
|
|
end
|
|
|
|
defp expand_alias(h, env) do
|
|
atom = list_to_atom('Elixir-' ++ atom_to_list(h))
|
|
:elixir_aliases.lookup(atom, env.aliases)
|
|
end
|
|
|
|
@doc """
|
|
Recurs the quoted expression checking if all sub terms are
|
|
safe (i.e. they represented data structured and don't actually
|
|
evaluate code) and returns `:ok` unless a given term is unsafe,
|
|
which is returned as `{ :unsafe, term }`.
|
|
"""
|
|
def safe_term(terms) do
|
|
do_safe_term(terms) || :ok
|
|
end
|
|
|
|
def do_safe_term({ local, _, terms }) when local in [:{}, :[], :__aliases__] do
|
|
do_safe_term(terms)
|
|
end
|
|
|
|
def do_safe_term({ unary, _, [term] }) when unary in [:+, :-] do
|
|
do_safe_term(term)
|
|
end
|
|
|
|
def do_safe_term({ left, right }), do: do_safe_term(left) || do_safe_term(right)
|
|
def do_safe_term(terms) when is_list(terms), do: Enum.find_value(terms, do_safe_term(&1))
|
|
def do_safe_term(terms) when is_tuple(terms), do: { :unsafe, terms }
|
|
def do_safe_term(_), do: nil
|
|
end |