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}" |> String.to_atom), new_map} end defp transform_arguments({atom, _, _} = var, map, :restore) do value = case Map.fetch(map, atom) do {:ok, value} -> "$#{value}" |> String.to_atom :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