src/gradualizer_tracer.erl

%% @doc A utility module to make troubleshooting problems
%% by tracing Gradualizer internals more efficient.
%% Configuration of this module is compile time, but using the tracing facilities is more efficient
%% than traditional printf-debugging anyway.
%%
%% Together with the shell interface of `gradualizer', we can use this module to inspect
%% or debug the inner workings of the type checker live.
%% Watch out for the trace sizes - they can grow huge!
%%
%% ```
%% > gradualizer_tracer:start().
%% ok
%% > gradualizer:type_of("[a]").
%% {trace,<0.1296.0>,call,
%%        {typechecker,type_check_expr,[{venv,#{}},{cons,1,{atom,1,a},{nil,1}}]}}
%% {trace,<0.1296.0>,call,{typechecker,type_check_expr,[{venv,#{}},{atom,1,a}]}}
%% {trace,<0.1296.0>,return_from,
%%        {typechecker,type_check_expr,2},
%%        {{atom,0,a},{venv,#{}},{constraints,#{},#{},#{}}}}
%% {trace,<0.1296.0>,return_to,{typechecker,do_type_check_expr,2}}
%% {trace,<0.1296.0>,call,{typechecker,type_check_expr,[{venv,#{}},{nil,1}]}}
%% {trace,<0.1296.0>,return_from,
%%        {typechecker,type_check_expr,2},
%%        {{type,0,nil,[]},{venv,#{}},{constraints,#{},#{},#{}}}}
%% {trace,<0.1296.0>,return_to,{typechecker,do_type_check_expr,2}}
%% {trace,<0.1296.0>,return_from,
%%        {typechecker,type_check_expr,2},
%%        {{type,0,nonempty_list,[{atom,0,a}]},
%%         {venv,#{}},
%%         {constraints,#{},#{},#{}}}}
%% {trace,<0.1296.0>,return_to,{g,type_of,2}}
%% {type,0,nonempty_list,[{atom,0,a}]}
%% '''

-module(gradualizer_tracer).

-export([start/0,
         stop/0,
         flush/0,
         debug/1]).

-include("typechecker.hrl").

-compile([{nowarn_unused_function, [{just_tenv, 1},
                                    {just_venv, 1},
                                    {skip_env, 1}]}]).

%% @doc This is the trace function that the tracer will use.
%% Customize it as you see fit, then recompile.
trace_fun() ->
    fun
        %% Here are some examples of what might come in handy when troubleshooting
        %% typechecker internals.
        %% For example, it's possible to only print the actual types in calls to `compat/4'
        %% and `compat_ty/4', but skip the lengthy `Seen' and `Env' arguments.
        %% Just uncomment the below:
        %%
        %% ({trace, _Pid, call, {_M, _F = compat, [_Ty1, _Ty2, _, _]}}, ok) ->
        %%     Trace = {trace, _Pid, call, {_M, _F, [_Ty1, _Ty2, seen, env]}},
        %%     io:format("~p\n", [Trace]);
        %% ({trace, _Pid, call, {_M, _F = compat_ty, [_Ty1, _Ty2, _, _]}}, ok) ->
        %%     Trace = {trace, _Pid, call, {_M, _F, [_Ty1, _Ty2, seen, env]}},
        %%     io:format("~p\n", [Trace]);
        %%
        %% In a similar fashion we can modify the return values from `subtype/2',
        %% `compat/4' etc:
        %%
        %% ({trace, _Pid, return_from, {_M, _F = subtype, _Arity}, {Ret, _Constraints}}, ok) ->
        %%     Trace = {trace, _Pid, return_from, {_M, _F, _Arity}, {Ret, constraints}},
        %%     io:format("~p\n", [Trace]);
        %% ({trace, _Pid, return_from, {_M, _F = compat, _Arity}, {_Ret, _Constraints}}, ok) ->
        %%     Trace = {trace, _Pid, return_from, {_M, _F, _Arity}, {true, constraints}},
        %%     io:format("~p\n", [Trace]);
        %% ({trace, _Pid, return_from, {_M, _F = compat_ty, _Arity}, {_Ret, _Constraints}}, ok) ->
        %%     Trace = {trace, _Pid, return_from, {_M, _F, _Arity}, {true, constraints}},
        %%     io:format("~p\n", [Trace]);

        %% In the general case, however, it might be more convenient to use one of the already
        %% available helpers like `just_venv/1' or `skip_env/1' in the clauses below:
        ({trace, _Pid, call, {_M, _F, _Args}}, ok) when is_list(_Args) ->
            Trace = {trace, _Pid, call, {_M, _F, simplify(_Args)}},
            io:format("~p\n", [Trace]);

        ({trace, _Pid, return_from, {_M, _F, _Arity}, RetVal}, ok) ->
            RV = case RetVal of
                     _ when is_list(RetVal) ->
                         simplify(RetVal);
                     _ when is_tuple(RetVal) ->
                         list_to_tuple(simplify(tuple_to_list(RetVal)));
                     _ ->
                         RetVal
                 end,
            Trace = {trace, _Pid, return_from, {_M, _F, _Arity}, RV},
            io:format("~p\n", [Trace]);

        (Trace, ok) ->
            io:format("~p\n", [Trace])
    end.

%% @doc Simplify traces with predefined transformations.
simplify(Args) ->
    %skip_env(Args).
    just_venv(Args).
    %just_tenv(Args).

skip_env(Args) ->
    lists:map(fun
                  (Arg) when element(1, Arg) =:= env -> env;
                  (Arg) -> Arg
              end, Args).

just_venv(Args) ->
    lists:map(fun
                  (Arg) when element(1, Arg) =:= env ->
                      {venv, Arg#env.venv};
                  (Arg) -> Arg
              end, Args).

just_tenv(Args) ->
    lists:map(fun
                  (Arg) when element(1, Arg) =:= env ->
                      {tenv, Arg#env.tenv};
                  (Arg) -> Arg
              end, Args).

%% @doc Start tracing.
%%
%% Check out the source code of this function to see trace pattern examples
%% matching specific parts of the type checker.
%% Uncomment the relevant ones or add your own, recompile the project, and start tracing.
%% @end
start() ->
    stop(),
    {ok, Tracer} = dbg:tracer(process, {trace_fun(), ok}),
    %dbg:p(all, [call, arity, return_to]),
    dbg:p(all, [call, return_to]),

    %dbg:tpl(typechecker, type_check_function, x),
    %dbg:tpl(typechecker, check_clauses_fun, x),
    %dbg:tpl(typechecker, check_clauses, x),
    %dbg:tpl(typechecker, check_clause, x),
    %dbg:tpl(typechecker, refine_vars_by_mismatching_clause, x),
    %dbg:tpl(typechecker, check_arg_exhaustiveness, x),

    %dbg:tpl(typechecker, check_arg_exhaustiveness, x),
    %dbg:tpl(typechecker, exhaustiveness_checking, x),
    %dbg:tpl(typechecker, all_refinable, x),
    %dbg:tpl(typechecker, no_clause_has_guards, x),
    %dbg:tpl(typechecker, some_type_not_none, x),

    %dbg:tpl(typechecker, check_clause, x),
    %dbg:tpl(typechecker, check_clause, x),
    %dbg:tpl(typechecker, add_types_pats, x),
    %dbg:tpl(typechecker, check_guards, x),
    %dbg:tpl(typechecker, add_var_binds, x),
    %dbg:tpl(typechecker, type_check_block_in, x),
    %dbg:tpl(typechecker, refine_clause_arg_tys, x),
    %dbg:tpl(typechecker, refine_mismatch_using_guards, x),

    %dbg:tpl(typechecker, expect_tuple_type, x),
    %dbg:tpl(typechecker, expect_tuple_union, x),
    %dbg:tpl(typechecker, refine_clause_arg_tys, x),
    %dbg:tpl(typechecker, refine_ty, x),

    %dbg:tpl(typechecker, add_types_pats, 4, x),
    %dbg:tpl(typechecker, add_types_pats, 6, x),
    %dbg:tpl(typechecker, add_type_pat, 3, x),
    %dbg:tpl(typechecker, add_type_pat_union, 3, x),
    %dbg:tpl(typechecker, denormalize, x),
    %dbg:tpl(typechecker, type_check_block_in, x),
    %dbg:tpl(typechecker, type_check_block_in, x),

    %dbg:tpl(typechecker, type_check_expr, x),
    %dbg:tpl(typechecker, do_type_check_expr, x),

    %dbg:tpl(typechecker, type_check_expr_in, x),
    %dbg:tpl(typechecker, do_type_check_expr_in, x),
    %dbg:tpl(typechecker, type_check_logic_op_in, x),

    %dbg:tpl(typechecker, subtype, x),
    %dbg:tpl(typechecker, compat, x),
    %dbg:tpl(typechecker, compat_seen, x),
    %dbg:tpl(typechecker, compat_ty, x),

    %dbg:tpl(typechecker, glb_ty, x),
    %dbg:tpl(typechecker, normalize, x),
    %dbg:tpl(typechecker, do_add_types_pats, x),

    %dbg:tpl(typechecker, check_clauses_intersection, []),
    %dbg:tpl(typechecker, check_reachable_clauses, x),
    %dbg:tpl(typechecker, check_clause, x),
    %dbg:tpl(typechecker, check_arg_exhaustiveness, x),
    %dbg:tpl(typechecker, check_clauses_intersection_throw_if_seen, x),
    %dbg:tpl(typechecker, refine_clause_arg_tys, x),
    %dbg:tpl(typechecker, refine_mismatch_using_guards, x),
    %dbg:tpl(typechecker, are_patterns_matching_all_input, x),
    %dbg:tpl(typechecker, check_guard_call, x),
    %dbg:tpl(typechecker, type_diff, x),

    %dbg:tpl(?MODULE, debug, x),
    %dbg:tpl(erlang, throw, x),

    application:set_env(gradualizer, tracer, Tracer),
    ok.

%% @doc Stop tracing.
stop() ->
    dbg:stop_clear().

%% @doc `debug/1' is a trace point to trace when pinpointing issues across several candidate
%% locations. Uncomment the below in `start/0':
%%
%% ```
%% dbg:tpl(?MODULE, debug, x)
%% '''
%%
%% Then insert the following somewhere in code, recompile and check the trace for the line from
%% which `debug/1' was called:
%%
%% ```
%% gradualizer_tracer:debug(?LINE)
%% '''
%% @end
debug(_) -> ok.

%% @doc Wait for flushing all the trace messages to stdout.
flush() ->
    case application:get_env(gradualizer, tracer, no_tracer) of
        no_tracer -> ok;
        Tracer when is_pid(Tracer) ->
            timer:sleep(100)
    end.