defmodule Exp do
@moduledoc """
Execute and inline expressions at compile time.
* In a regular code, all you need is `Exp.inline/1`.
* For usage inside of macros when you don't know if arguments are safe,
see `Exp.maybe_inline/1`.
* `Exp.abs/1` is a real-world example of using `Exp.maybe_inline/1`.
* `Exp.pure_func?/3` and `Exp.safe_node?/1` are utility function that
`Exp.maybe_inline/1` uses to make decision if expression is safe to inline.
Source: [github.com/orsinium-labs/exp](https://github.com/orsinium-labs/exp)
"""
@doc """
Macro to statically execute and inline an expression at compile time.
If the expression cannot be inlined, it will explode at compile time.
This is your responsibility to make sure the code can be inlined and is safe to inline.
## What to inline
There are requirements for a good candidate to be inlined:
* It doesn't make network requests.
* It doesn't depend on a service or ecto.
* For the same input, it always produces the same output.
* It's not too slow.
* Its result doesn't take too much memory.
A good example of such function is `Regex.compile!/2`.
## Examples
The usage is straightforward, just wrap whatever you want to inline:
iex> require Exp
iex> Exp.inline(1 + 2)
3
The difference is that the expression inside will be executed at compile time
(when expanding the AST) and included into the compiled code:
iex(21)> q = quote do: 1 + 2
iex(22)> {:+, _, [1, 2]} = Macro.expand(q, __ENV__)
iex(23)> q = quote do: Exp.inline(1 + 2)
iex(24)> 3 = Macro.expand(q, __ENV__)
"""
@spec inline(Macro.t()) :: Macro.t()
defmacro inline(call) do
{term, _} = Code.eval_quoted(call)
Macro.escape(term)
end
# list of pure BIFs
@pure_funcs [
{:is_atom, 1},
{:is_binary, 1},
{:is_bitstring, 1},
{:is_boolean, 1},
{:is_float, 1},
{:is_function, 1},
{:is_function, 2},
{:is_integer, 1},
{:is_list, 1},
{:is_map, 1},
{:is_map_key, 2},
{:is_number, 1},
{:is_pid, 1},
{:is_port, 1},
{:is_reference, 1},
{:is_tuple, 1},
{:<, 2},
{:"=<", 2},
{:>, 2},
{:>=, 2},
{:"/=", 2},
{:"=/=", 2},
{:==, 2},
{:"=:=", 2},
{:*, 2},
{:+, 1},
{:+, 2},
{:-, 1},
{:-, 2},
{:/, 2},
{:abs, 1},
{:ceil, 1},
{:floor, 1},
{:round, 1},
{:trunc, 1},
{:element, 2},
{:hd, 1},
{:length, 1},
{:map_get, 2},
{:map_size, 1},
{:tl, 1},
{:tuple_size, 1},
{:binary_part, 3},
{:bit_size, 1},
{:byte_size, 1},
{:size, 1},
{:div, 2},
{:rem, 2},
{:bnot, 1},
{:band, 2},
{:bor, 2},
{:bxor, 2},
{:bsl, 2},
{:bsr, 2},
{:or, 2},
{:and, 2},
{:xor, 2},
{:not, 1},
{:andalso, 2},
{:orelse, 2}
]
# List of modules that contain only pure functions
@pure_modules [
# basic types
Atom,
Base,
Bitwise,
Float,
Integer,
Kernel,
String,
Tuple,
URI,
Version,
# collections
Access,
Enum,
Keyword,
List,
Map,
MapSet,
Range,
Stream,
# popular third-party
Jason,
Poison
]
@doc """
Check if the function of given name and arity is pure.
## Examples
iex> Exp.pure_func?(:abs, 1)
true
iex> Exp.pure_func?(:abs, 2) # bad arity
false
iex> Exp.pure_func?(:send, 2)
false
"""
@spec pure_func?(atom(), non_neg_integer()) :: boolean()
def pure_func?(name, arity)
Enum.each(@pure_funcs, fn {name, arity} ->
def pure_func?(unquote(name), unquote(arity)), do: true
end)
def pure_func?(_, _), do: false
@doc """
Check if the function of given module, name, and arity is pure.
## Examples
iex> Exp.pure_func?(String, :upcase, 1)
true
iex> Exp.pure_func?(String, :upcase, 4) # bad arity
false
iex> Exp.pure_func?(File, :cwd, 0)
false
iex> Exp.pure_func?(Kernel, :abs, 1)
true
iex> Exp.pure_func?(:erlang, :abs, 1)
true
"""
@spec pure_func?(atom(), atom(), non_neg_integer()) :: boolean()
def pure_func?(module, function, arity)
Enum.each(@pure_modules, fn module ->
def pure_func?(m = unquote(module), f, a),
do: Code.ensure_loaded?(m) and function_exported?(m, f, a)
end)
def pure_func?(:Kernel, name, arity), do: pure_func?(name, arity)
def pure_func?(:erlang, name, arity), do: pure_func?(name, arity)
def pure_func?(_, _, _), do: false
@doc """
Check if the given AST node is safe to be statically executed.
This check is conservative. If it doesn't know anything about the function,
the result is `false`.
It's used be `Exp.maybe_inline/1` to make a decision if the code should be inlined.
## Examples
iex> Exp.safe_node?(quote do: 1)
true
iex> Exp.safe_node?(quote do: "hello")
true
iex> Exp.safe_node?(quote do: 1+2)
true
iex> Exp.safe_node?(quote do: div(2, 3))
true
iex> Exp.safe_node?(quote do: File.cwd())
false
"""
@spec safe_node?(Macro.t()) :: boolean()
def safe_node?(term)
def safe_node?({:__aliases__, _, args}), do: safe_node?(args)
def safe_node?({:%, _, [left, right]}), do: safe_node?(left) and safe_node?(right)
def safe_node?({:%{}, _, args}), do: safe_node?(args)
def safe_node?({:{}, _, args}), do: safe_node?(args)
def safe_node?({left, right}), do: safe_node?(left) and safe_node?(right)
def safe_node?(list) when is_list(list), do: Enum.all?(list, &safe_node?/1)
def safe_node?({{:., _, [{_, _, [mname]}, fname]}, [], list}) when is_list(list),
do: pure_func?(mname, fname, length(list)) and safe_node?(list)
def safe_node?({fname, _, list}) when is_list(list),
do: pure_func?(fname, length(list)) and safe_node?(list)
def safe_node?(term), do: is_atom(term) or is_number(term) or is_binary(term)
defp get_unquote({:unquote, _, [expr]}), do: [expr]
defp get_unquote(_), do: []
@doc """
A safe implementation of `inline/1` for macros.
It inlines the given quoted expression if and only if all `unquote` arguments
are safe to execute at compile time. The safety of the expression itself isn't checked.
The result is also a quoted expression.
It's supposed to be used from macros when you don't know if the macros
will be used for a safe to execute code or not.
## Examples
Here, `var` is safe and so the expression is inlined:
iex> var = quote do: 13
iex> 13 = Exp.maybe_inline(quote do: abs(unquote(var)))
Here, `var` is not safe and so the expression is left as-is:
iex> var = quote do: node()
iex> {:abs, _, _} = Exp.maybe_inline(quote do: abs(unquote(var)))
Here, the expression isn't safe but it's still inlined because `maybe_inline/2`
checks only safety of expressions inside `Kernel.SpecialForms.unquote/1`:
iex> Exp.maybe_inline(quote do: node())
:nonode@nohost
See also `Exp.abs/1` for a real-world usage example.
"""
@spec maybe_inline(Macro.t()) :: Macro.t()
defmacro maybe_inline(block) do
params = Enum.flat_map(Macro.prewalker(block), &get_unquote/1)
quote generated: true do
if Exp.safe_node?(unquote(params)) do
{term, _} = unquote(block) |> Code.eval_quoted()
Macro.escape(term)
else
unquote(block)
end
end
end
@doc """
Inlined version of `Kernel.abs/1`.
Returns the arithmetical absolute value of the `number`.
## Examples
iex> Exp.abs(-2)
2
iex> Exp.abs(2)
2
This is how you can check if it was inlined:
iex> # inlined:
iex> q = quote do: Exp.abs(-2)
iex> 2 = Macro.expand(q, __ENV__)
iex> # not inlined because unsafe:
iex> q = quote do: Exp.abs(node())
iex> {:abs, _, _} = Macro.expand(q, __ENV__)
"""
@spec abs(Macro.t()) :: Macro.t()
defmacro abs(number) do
maybe_inline(quote(do: abs(unquote(number))))
end
@doc false
@spec to_charlist(Macro.t()) :: Macro.t()
defmacro to_charlist(str) do
maybe_inline(quote(do: Kernel.to_charlist(unquote(str))))
end
@doc false
@spec to_string(Macro.t()) :: Macro.t()
defmacro to_string(str) do
maybe_inline(quote(do: Kernel.to_string(unquote(str))))
end
end