defmodule Tracer.Pattern do
@moduledoc """
Module for transformation of an Elixir AST to a pattern, that possible to use for tracing
"""
@doc """
Compile pattern from elixir AST and options
"""
def compile(pattern, options \\ []) do
exported_opt = Enum.member?(options, :exported)
{mfa, [{args, conditions, trace_options}]} = compile_intern(pattern)
{
mfa,
[{args, conditions, set_trace_options(options, trace_options)}],
set_transform_opions(exported_opt)
}
end
defp compile_intern({:when, _, [{{:., _, [module, function]}, _, args}, conditions]}) do
{args, map} = transform_arguments(args)
conditions = [transform_conditions(conditions, map)]
{mfa(module, function), [match_spec(args, conditions)]}
end
defp compile_intern({:/, _, [{{:., _, [module, function]}, _, []}, arity]}) do
{mfa(module, function, arity), [match_spec(:_)]}
end
defp compile_intern({{:., _, [module, function]}, _, args}) do
{mfa(module, function), [match_spec(args |> transform_arguments |> elem(0))]}
end
defp compile_intern(module_name) when is_atom(module_name) or is_tuple(module_name) do
{mfa(module_name), [match_spec()]}
end
defp compile_intern(pattern) when is_binary(pattern) do
{:ok, pattern} = Code.string_to_quoted(pattern)
compile_intern(pattern)
end
defp module_name(module_ast), do: Macro.expand(module_ast, __ENV__)
defp mfa(module, function \\ :_, arity \\ :_), do: {module_name(module), function, arity}
defp match_spec(args \\ :_, conditions \\ [])
defp match_spec([], conditions), do: {:_, conditions, trace_options()}
defp match_spec(args, conditions), do: {args, conditions, trace_options()}
defp trace_options, do: [{:return_trace}, {:exception_trace}]
defp set_transform_opions(true), do: []
defp set_transform_opions(false), do: [:local]
defp set_trace_options([], args), do: args
defp set_trace_options([:stack | options], args) do
set_trace_options(options, [{:message, {:process_dump}} | args])
end
defp set_trace_options([:no_return | options], args) do
set_trace_options(options, args -- [{:exception_trace}, {:return_trace}])
end
defp set_trace_options([_ | options], args) do
set_trace_options(options, args)
end
defp transform_arguments(args, map \\ %{count: 1}, action \\ :save)
defp transform_arguments(args, map, action) when is_list(args) do
{new_args, new_map} =
Enum.reduce(args, {[], map}, fn arg, {acc, map} ->
{arg, new_map} = transform_arguments(arg, map, action)
{[arg | acc], new_map}
end)
{Enum.reverse(new_args), new_map}
end
defp transform_arguments({type, _, args}, map, action) when type in [:{}, :<<>>, :%{}] do
{args, new_map} = transform_arguments(args, map, action)
{transform_back_fun(type).(args), new_map}
end
defp transform_arguments({key, value}, map, action) do
{[key, value], new_map} = transform_arguments([key, value], map, action)
{{key, value}, new_map}
end
defp transform_arguments({:__aliases__, _, _} = alias_ast, map, _) do
{Macro.expand(alias_ast, __ENV__), map}
end
defp transform_arguments({:_, _, _}, map, :save), do: {:_, map}
defp transform_arguments({atom, _, _}, %{count: count} = map, :save) do
{count, new_map} =
case Map.fetch(map, atom) do
{:ok, exists_id} ->
{exists_id, map}
:error ->
{count, Enum.into([{:count, count + 1}, {atom, count}], map)}
end
{:"$#{count}", new_map}
end
defp transform_arguments({atom, _, _} = var, map, :restore) do
value =
case Map.fetch(map, atom) do
{:ok, value} -> :"$#{value}"
:error -> {:unquote, [], [quote(do: unquote(var))]}
end
{value, map}
end
defp transform_arguments(arg, map, _action)
when is_atom(arg) or is_number(arg) or is_binary(arg) do
{arg, map}
end
defp transform_back_fun(:{}), do: &List.to_tuple/1
defp transform_back_fun(:<<>>), do: &List.to_string/1
defp transform_back_fun(:%{}), do: &:maps.from_list/1
@function [
:is_atom,
:is_float,
:is_integer,
:is_list,
:is_number,
:is_map,
:is_pid,
:is_port,
:is_reference,
:is_tuple,
:is_binary,
:is_boolean,
:abs,
:hd,
:length,
:round,
:tl,
:trunc,
:not
]
defp transform_conditions({name, _, [var]}, map) when name in @function do
{name, transform_conditions(var, map)}
end
@operators [
{:and, :andalso},
{:or, :orelse},
:xor,
:>,
:>=,
:<,
{:<=, :"=<"},
:==,
{:===, :"=:="},
{:!=, :"/="},
{:!==, :"=/="},
:+,
:-,
:*,
{:/, :div},
:rem
]
Enum.map(@operators, fn
{elixir_op, erlang_op} ->
defp transform_conditions({unquote(elixir_op), _, [left, right]}, map) do
{unquote(erlang_op), transform_conditions(left, map), transform_conditions(right, map)}
end
op ->
defp transform_conditions({unquote(op), _, [left, right]}, map) do
{unquote(op), transform_conditions(left, map), transform_conditions(right, map)}
end
end)
defp transform_conditions({:elem, _, [var, index]}, map) do
{:element, index + 1, transform_conditions(var, map)}
end
@function [:node, :self]
defp transform_conditions({name, _, []}, _) when name in @function do
{name}
end
defp transform_conditions(data, map) do
transform_arguments(data, map, :restore) |> elem(0)
end
# Not supported at the moment: is_record/{1,2}
end