%%% -*- erlang-indent-level: 2 -*-
%%%-------------------------------------------------------------------
%%% File    : redbg.erl
%%% Author  : Mats Cronqvist <masse@cronqvi.st>
%%% Description :
%%%
%%% Created : 24 Jan 2007 by Mats Cronqvist <locmacr@mwlx084>
%%%-------------------------------------------------------------------
-module(redbug).

-export([help/0]).
-export([start/1, start/2]).
-export([stop/0]).
-export([dtop/0, dtop/1]).

-define(log(T), error_logger:info_report(
                  [process_info(self(), current_function),
                   {line, ?LINE}|T])).

%% erlang:get_stacktrace/0 was made obsolete in OTP21
-ifdef(OTP_RELEASE). %% this implies 21 or higher
-define(EXCEPTION(Class, Reason, StackToken), Class:Reason:StackToken).
-define(GET_STACK(StackToken), StackToken).
-else.
-define(EXCEPTION(Class, Reason, _), Class:Reason).
-define(GET_STACK(_), erlang:get_stacktrace()).
-endif.

%% the redbug server data structure
%% most can be set in the input proplist
-record(cnf,
        {
         %% general
         time         = 15000,       % stop trace after this time [ms]
         msgs         = 10,          % stop trace after this # msgs [unit]
         target       = node(),      % target node
         cookie       = '',          % target node cookie
         blocking     = false,       % run blocking; return a list of msgs
         procs        = all,         % list of procs (or 'all')
         max_queue    = 5000,        % max # of msgs before suicide
         max_msg_size = 50000,       % max message size before suicide
         debug        = false,       % big error messages
         trace_child  = false,       % children gets traced (set_on_spawn)
         arity        = false,       % arity instead of args
         discard      = false,       % discard messages (when counting)
         %% print-related
         buffered     = false,       % output buffering
         records      = [],          % list of module names to get records from
         print_calls  = true,        % print calls
         print_file   = "",          % file to print to (standard_io)
         print_msec   = false,       % print milliseconds in timestamps?
         print_depth  = 999999,      % Limit for "~P" formatting depth
         print_re     = "",          % regexp that must match to print
         print_return = true,        % print return value
         print_fun    = '',          % custom print handler
         %% trc file-related
         file         = "",          % file to write trace msgs to
         file_size    = 1,           % file size (per file [Mb])
         file_count   = 8,           % number of files in wrap log
         %% internal
         trc          = [],          % cannot be set by user
         shell_pid    = [],          % cannot be set by user
         print_pid    = [],          % cannot be set by user
         trc_pid      = []           % cannot be set by user
        }).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Prints help.
help() ->
  Text =
    ["redbug - the (sensibly) Restrictive Debugger"
    , ""
    , "  redbug:start(Trc) -> start(Trc, [])."
    , "  redbug:start(Trc, Opts)."
    , ""
    , "  redbug is a tool to interact with the Erlang trace facility."
    , "  It will instruct the Erlang VM to generate so called "
    , "  'trace messages' when certain events (such as a particular"
    , "  function being called) occur."
    , "  The trace messages are either printed (i.e. human readable)"
    , "  to a file or to the screen; or written to a trc file."
    , "  Using a trc file puts less stress on the system, but"
    , "  there is no way to count the messages (so the msgs opt"
    , "  is ignored), and the files can only be read by special tools"
    , "  (such as 'bread'). Printing and trc files cannot be combined."
    , "  By default (i.e. if the 'file' opt is not given), messages"
    , "  are printed."
    , ""
    , "Trc: list('send'|'receive'|string(RTP))"
    , "RTP:  restricted trace pattern"
    , "  the RTP has the form: \"<mfa> when <guards> -> <actions>\""
    , "  where <mfa> can be;"
    , "  \"mod\", \"mod:fun\", \"mod:fun/3\" or \"mod:fun('_', atom, X)\""
    , "  <guard> is something like;"
    , "  \"X==1\" or \"is_atom(A)\""
    , "  and <action> is;"
    , "  \"return\" and/or \"stack\" (separated by \";\")"
    , ""
    , "  E.g."
    , "  ets:lookup(T, hostname) when is_integer(T) -> stack"
    , ""
    , "Opts: list({Opt, Val})"
    , "  general opts:"
    , "time         (15000)       stop trace after this many ms"
    , "msgs         (10)          stop trace after this many msgs"
    , "target       (node())      node to trace on"
    , "cookie       (host cookie) target node cookie"
    , "blocking     (false)       block start/2, return a list of messages"
    , "arity        (false)       print arity instead of arg list"
    , "trace_child  (false)       children gets traced (set_on_spawn)"
    , "records      ([])          list of module names to get records from"
    , "buffered     (false)       buffer messages till end of trace"
    , "discard      (false)       discard messages (when counting)"
    , "max_queue    (5000)        fail if internal queue gets this long"
    , "max_msg_size (50000)       fail if seeing a msg this big"
    , "procs        (all)         (list of) Erlang process(es)"
    , "                             all|pid()|atom(RegName)|{pid, I2, I3}"
    , "  print-related opts"
    , "print_calls  (true)        print calls"
    , "print_file   (standard_io) print to this file"
    , "print_msec   (false)       print milliseconds on timestamps"
    , "print_depth  (999999)      formatting depth for \"~P\""
    , "print_re     (\"\")          print only strings that match this RE"
    , "print_return (true)        print the return value"
    , "print_fun    ()            custom print handler, fun/1 or fun/2;"
    , "                             fun(TrcMsg) -> <ignored>"
    , "                             fun(TrcMsg, AccOld) -> AccNew"
    , "  trc file related opts"
    , "file         (none)        use a trc file based on this name"
    , "file_size    (1)           size of each trc file"
    , "file_count   (8)           number of trc files"
    , ""
    ],
  lists:foreach(fun(S) -> io:fwrite(standard_io, "~s~n", [S])end, Text).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% dtop facade

%% @equiv dtop(#{})
dtop() ->
  dtop(#{}).

%% @docfile "doc/dtop.edoc"
dtop(Cfg) ->
  try
    redbug_dtop:start(),
    dtop_cfg(Cfg)
  catch
    exit:already_started ->
      case maps:size(Cfg) of
        0 -> redbug_dtop:stop();
        _ -> dtop_cfg(Cfg)
      end
  end.

dtop_cfg(Cfg) ->
  [redbug_dtop:sort(S) || (S = maps:get(sort, Cfg, "")) =/= ""],
  [redbug_dtop:max_prcs(M) || (M = maps:get(max_procs, Cfg, "")) =/= ""].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% API from erlang shell
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc
%% Stops a trace.
%% @end
stop() ->
  case whereis(redbug) of
    undefined -> not_started;
    Pid -> Pid ! stop, stopped
  end.

%% @equiv start(RTPs, [])
start(RTPs) ->
  start(RTPs, []).

%% @spec start(RTPs::list(), Opts::map()) -> {Procs::integer(), Functions::integer()}
%% @docfile "doc/start.edoc"
start('send', Props)    -> start([send], Props);
start('receive', Props) -> start(['receive'], Props);
start(Trc, Props) when is_map(Props) ->
  start(Trc, maps:to_list(Props));
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% the real start function!
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
start(Trc, Props) when is_list(Props) ->
  case whereis(redbug) of
    undefined ->
      try
        Cnf = assert_print_fun(make_cnf(Trc, [{shell_pid, self()}|Props])),
        assert_cookie(Cnf),
        register(redbug, spawn(fun() -> init(Cnf) end)),
        maybe_block(Cnf, block_a_little())
      catch
        R   -> R;
        C:R -> {oops, {C, R}}
      end;
    _ ->
      redbug_already_started
  end.

assert_print_fun(Cnf) ->
  case is_function(Cnf#cnf.print_fun) of
    true -> Cnf;
    false-> Cnf#cnf{print_fun=make_print_fun(Cnf)}
  end.

make_print_fun(Cnf) ->
  case Cnf#cnf.blocking of
    false-> mk_outer(Cnf);
    true -> mk_blocker()
  end.

assert_cookie(#cnf{cookie=''}) -> ok;
assert_cookie(Cnf) -> erlang:set_cookie(Cnf#cnf.target, Cnf#cnf.cookie).

block_a_little() ->
  Ref = erlang:monitor(process, redbug),
  receive
    {running, NoP, NoF}  -> erlang:demonitor(Ref), {NoP, NoF};
    {'DOWN', Ref, _, _, R} -> R
  end.

maybe_block(#cnf{blocking=true}, {I, _}) when is_integer(I) -> block();
maybe_block(_, R) -> R.

block() ->
  Ref = erlang:monitor(process, redbug),
  receive
    {'DOWN', Ref, _, _, R} -> R
  end.

%% turn the proplist inta a #cnf{}
make_cnf(Trc, Props) ->
  make_cnf(proplists:unfold(Props), #cnf{trc=Trc}, record_info(fields, cnf)).

make_cnf([], Cnf, _) -> Cnf;
make_cnf([{Tag, Val}|Props], Cnf, Tags) ->
  make_cnf(Props, setelement(findex(Tag, Tags)+1, Cnf, Val), Tags).

findex(Tag, [])       -> throw({no_such_option, Tag});
findex(Tag, [Tag|_])  -> 1;
findex(Tag, [_|Tags]) -> findex(Tag, Tags)+1.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% the main redbug process
%%% a state machine. init, starting, running, stopping, wait_for_trc.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
init(Cnf) ->
  process_flag(trap_exit, true),
  try
    starting(do_start(Cnf))
  catch
    throw:R ->
      exit({argument_error, R});
    ?EXCEPTION(C, R, S) when Cnf#cnf.debug andalso not Cnf#cnf.blocking ->
      ?log([{C, R}, {stack, ?GET_STACK(S)}]);
    _:R ->
      exit(R)
  end.

starting(Cnf = #cnf{trc_pid=TrcPid}) ->
  receive
    {{starting, TrcPid, P, F}} -> running(run(Cnf, P, F));
    {'EXIT', TrcPid, R}   -> throw(R)
  end.

running(Cnf = #cnf{trc_pid=TrcPid, print_pid=PrintPid}) ->
  receive
    stop ->
      TrcPid ! stop,
      wait_for_trc(Cnf),
      PrintPid ! stop,
      done(Cnf, {stopped, wait_for_printer(Cnf)});
    {'EXIT', TrcPid, R} ->
      PrintPid ! stop,
      done(Cnf, {R, wait_for_printer(Cnf)});
    {'EXIT', PrintPid, R} ->
      TrcPid ! stop,
      wait_for_trc(Cnf),
      done(Cnf, {printer_crash, R});
    X ->
      ?log([{unknown_message, X}])
  end.

wait_for_trc(#cnf{trc_pid=TrcPid}) ->
  receive
    {'EXIT', TrcPid, stop} ->
      ok;
    {'EXIT', TrcPid, R} ->
      ?log([{trace_control_died, R}])
  end.

wait_for_printer(#cnf{print_pid=PrintPid}) ->
  receive
    {'EXIT', PrintPid, R} -> R
  end.

done(#cnf{blocking=false}, {Reason, Answer}) ->
  io:fwrite("~s", [done_string(Reason)]),
  io:fwrite("redbug done, ~p - ~p~n", [Reason, Answer]);
done(#cnf{blocking=true}, Answer) ->
  exit(Answer).

done_string(Reason) ->
  case is_tuple(Reason) andalso element(1, Reason) of
    msg_queue ->
      "you might want to set the max_queue option (see redbug:help/0)\n";
    stack_size ->
      "you might want to set the max_msg_size option (see redbug:help/0)\n";
    arg_length ->
      "you might want to set the max_msg_size option (see redbug:help/0)\n";
    arg_size ->
      "you might want to set the max_msg_size option (see redbug:help/0)\n";
    _ ->
      ""
  end.

run(Cnf, P, F) ->
  [Cnf#cnf.shell_pid ! {running, P, F} || is_pid(Cnf#cnf.shell_pid)],
  Cnf.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

do_start(OCnf) ->
  Cnf = spawn_printer(wrap_print_fun(OCnf), maybe_new_target(OCnf)),
  Cnf#cnf{trc_pid=redbug_targ:start(Cnf#cnf.target, pack(Cnf))}.

maybe_new_target(Cnf = #cnf{target=Target}) ->
  case lists:member($@, Str=atom_to_list(Target)) of
    true -> Cnf;
    false-> Cnf#cnf{target=to_atom(Str++"@"++element(2, inet:gethostname()))}
  end.

to_atom(L) -> list_to_atom(L).

spawn_printer(PrintFun, Cnf) ->
  Cnf#cnf{print_pid=spawn_link(fun() -> print_init(PrintFun) end)}.

wrap_print_fun(#cnf{print_fun=PF}) ->
  case erlang:fun_info(PF, arity) of
    {arity, 1} -> fun(M, N) -> PF(M), maybe_update_count(M, N) end;
    {arity, 2} -> PF
  end.

maybe_update_count(M, N) ->
  case element(1, M) of
    call -> N+1;
    send -> N+1;
    recv -> N+1;
    _    -> N
  end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% the default print fun for blocking mode (i.e. return a term).
%% the printer proc will execute this (or a user-provided fun) for
%% each message from the target.
%% we throw away call_time, call_count, and meta messages.

mk_blocker() ->
  fun({call_time, {_, false}, _, _}, A)  -> A;
     ({call_time, {_, []}, _, _}, A)     -> A;
     ({call_count, {_, false}, _, _}, A) -> A;
     ({call_count, {_, 0}, _, _}, A)     -> A;
     ({meta, {recs, Recs}, _, _}, A)     -> put_recs(Recs), A;
     ({meta, stop, _, _}, 0)             -> [];
     ({meta, stop, _, _}, A)             -> lists:reverse(A);
     (X, 0)                              -> [expand(X)];
     (X, A)                              -> [expand(X)|A]
  end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% the default print fun in non-blocking mode (i.e. to the shell).
%% the printer proc will execute this (or a user-provided fun) for
%% each message from the target.

mk_outer(#cnf{file=[_|_]}) ->
  fun(_) -> ok end;
mk_outer(#cnf{print_depth=Depth, print_msec=MS, print_return=Ret} = Cnf) ->
  OutFun = mk_out(Cnf),
  fun({Tag, Data, PI, TS}) ->
      MTS = fix_ts(MS, TS),
      case {Tag, Data} of
        {'meta', {recs, Recs}} ->
          put_recs(Recs);
        {'meta', _} ->
          ok;
        {'call_time', {_, false}} ->
          ok;
        {'call_time', {{M, F, A}, PerProcCT}} ->
          PerProc =
            fun({_, Count, Sec, Usec}, {AC, AT}) ->
                {Count+AC, Sec*1000000+Usec+AT}
            end,
          {Count, Time} = lists:foldl(PerProc, {0, 0}, PerProcCT),
          [OutFun("% ~6s : ~6s : ~6s : ~w:~w/~w",
                  [human(Count), human(Time), human(Time/Count), M, F, A]) || 0 < Count];
        {'call_count', {_, false}} ->
          ok;
        {'call_count', {{M, F, A}, Count}} ->
          [OutFun("~n% ~6s : ~w:~w/~w", [human(Count), M, F, A]) || 0 < Count];
        {'call', {{M, F, A}, Bin}} ->
          case Cnf#cnf.print_calls of
            true ->
              case is_integer(A) of
                true ->
                  OutFun("~n% ~s ~s~n% ~w:~w/~w", [MTS, to_str(PI), M, F, A]);
                false->
                  Args = [flat("~P", [expand(A0), Depth]) || A0 <- A],
                  AL = string:join(Args, ", "),
                  OutFun("~n% ~s ~s~n% ~w:~w(~s)", [MTS, to_str(PI), M, F, AL])
              end,
              lists:foreach(fun(L) -> OutFun("%   ~s", [L]) end, stak(Bin));
            false->
              ok
          end;
        {'retn', {{M, F, A}, Val0}} ->
          Val = case Ret of
                  true  -> expand(Val0);
                  false -> '...'
                end,
          OutFun("~n% ~s ~s~n% ~p:~p/~p -> ~P",
                 [MTS, to_str(PI), M, F, A, Val, Depth]);
        {'send', {MSG, ToPI}} ->
          OutFun("~n% ~s ~s~n% ~s <<< ~P",
                 [MTS, to_str(PI), to_str(ToPI), expand(MSG), Depth]);
        {'recv', MSG} ->
          OutFun("~n% ~s ~s~n% <<< ~P",
                 [MTS, to_str(PI), expand(MSG), Depth])
      end
  end.

to_str({Pid, Reg}) ->
  flat("~w(~p)", [Pid, Reg]);
to_str(RegisteredName) ->
  flat("~p", [RegisteredName]).

%% expand records, if any
expand(T) when is_list(T) -> improper_map(fun expand/1, T);
expand(T) when is_map(T) -> maps:map(fun(_K, V) -> expand(V) end, T);
expand(T) when is_tuple(T) andalso 0 < size(T) ->
  case get({element(1, T), size(T)-1}) of
    undefined -> list_to_tuple(expand(tuple_to_list(T)));
    Fields -> maps:from_list(lists:zip(['_RECORD'|Fields], expand(tuple_to_list(T))))
  end;
expand(T) -> T.

%% handle improper lists
improper_map(_, []) -> [];
improper_map(F, [E]) -> [F(E)];
improper_map(F, [A|B]) when not is_list(B) -> [F(A)|F(B)];
improper_map(F, [A|B]) -> [F(A)|improper_map(F, B)].

mk_out(#cnf{print_re=RE, print_file=File}) ->
  FD = get_fd(File),
  fun(F, A) ->
      Str=flat(F, A),
      case RE =:= "" orelse re:run(Str, RE) =/= nomatch of
        true  -> io:fwrite(FD, "~s~n", [Str]);
        false -> ok
      end
  end.

get_fd(FD) when is_atom(FD); is_pid(FD) ->
  FD;
get_fd("") ->
  standard_io;
get_fd(FN) ->
  case file:open(FN, [write]) of
    {ok, FD} -> FD;
    _ -> throw({cannot_open, FN})
  end.

fix_ts(MS, TS) ->
  case MS of
    true -> ts_ms(TS);
    false-> ts(TS)
  end.

ts({H, M, S, _Us}) ->
  flat("~2.2.0w:~2.2.0w:~2.2.0w", [H, M, S]).
ts_ms({H, M, S, Us}) ->
  flat("~2.2.0w:~2.2.0w:~2.2.0w.~3.3.0w", [H, M, S, Us div 1000]).

%%% call stack handler
stak(Bin) ->
  L = string:tokens(binary_to_list(Bin), "\n"),
  lists:reverse(lists:foldl(fun munge/2, [], L)).

munge(I, Out) ->
  case lists:reverse(I) of
    "..."++_ -> [truncated|Out];
    _ ->
      case string:str(I, "Return addr") of
        0 ->
          case string:str(I, "cp = ") of
            0 -> Out;
            _ -> [mfaf(I)|Out]
          end;
        _ ->
          case string:str(I, "erminate process normal") of
            0 -> [mfaf(I)|Out];
            _ -> Out
          end
      end
  end.

mfaf(I) ->
  [_, C|_] = string:tokens(I, "()+"),
  C.

put_recs(Recs) ->
  [put({Name, length(Fields)}, Fields) || {Name, Fields} <- Recs].


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% pack data into a proplist for target consumption
%%% Proplist = list({Tag, Val})
%%% Tag = time | flags | asts | procs | where
%%% Where = {buffer, Pid, Count, MaxQueue, MaxSize} |
%%%         {stream, Pid, Count, MaxQueue, MaxSize} |
%%%         {discard, Pid, Count, MaxQueue, MaxSize} |
%%%         {file, File, Size, Count} |
%%%         {ip, Port, Queue}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
pack(Cnf) ->
  Flags0 = [call, timestamp],
  {Flags, ASTs} = lists:foldl(fun chk_trc/2, {Flags0, []}, slist(Cnf#cnf.trc)),
  [{time, chk_time(Cnf#cnf.time)},
   {flags, maybe_arity(Cnf, maybe_trace_child(Cnf, Flags))},
   {asts, ASTs},
   {procs, [chk_proc(P) || P <- mk_list(Cnf#cnf.procs)]},
   {records, chk_records(Cnf#cnf.records)},
   {where, where(Cnf)}].

mk_list([]) -> throw(no_procs);
mk_list([_|_] = L) -> L;
mk_list(E) -> [E].

where(Cnf) ->
  case Cnf#cnf.file of
    "" -> conf_term(Cnf);
    _  -> conf_file(Cnf)
  end.

conf_file(Cnf) ->
  {file, Cnf#cnf.file, Cnf#cnf.file_size, Cnf#cnf.file_count}.

conf_term(Cnf) ->
  {chk_buffered(Cnf#cnf.buffered, Cnf#cnf.discard),
   Cnf#cnf.print_pid,
   chk_msgs(Cnf#cnf.msgs),
   Cnf#cnf.max_queue,
   Cnf#cnf.max_msg_size}.

maybe_arity(#cnf{arity=true}, Flags) -> [arity|Flags];
maybe_arity(_, Flags)                -> Flags.

maybe_trace_child(#cnf{trace_child=true}, Flags) -> [set_on_spawn|Flags];
maybe_trace_child(_, Flags)                      -> Flags.

chk_time(Time) when is_integer(Time) -> Time;
chk_time(X) -> throw({bad_time, X}).

chk_buffered(_, true)  -> discard;
chk_buffered(true, _)  -> buffer;
chk_buffered(false, _) -> stream.

chk_proc(Pid) when is_pid(Pid) -> Pid;
chk_proc(Atom) when is_atom(Atom) -> Atom;
chk_proc({pid, I1, I2}) when is_integer(I1), is_integer(I2) -> {pid, I1, I2};
chk_proc(X) -> throw({bad_proc, X}).

chk_records([]) -> [];
chk_records(Mod) when is_atom(Mod) -> [Mod];
chk_records([Mod|Mods]) when is_atom(Mod) -> [Mod|chk_records(Mods)];
chk_records(What) -> throw({bad_module_name, What}).

chk_msgs(Msgs) when is_integer(Msgs) -> Msgs;
chk_msgs(X) -> throw({bad_msgs, X}).

chk_trc('send', {Flags, Trc})    -> {['send'|Flags], Trc};
chk_trc('receive', {Flags, Trc}) -> {['receive'|Flags], Trc};
chk_trc(Trc, {Flags, ASTs})      -> {Flags, [mk_ast(Trc)|ASTs]}.

mk_ast(Str) -> redbug_compiler:parse(Str).

-define(is_string(Str), (Str=="" orelse (9=<hd(Str) andalso hd(Str)=<255))).

slist(S) when ?is_string(S) -> [S];
slist(L) when is_list(L) -> lists:usort(L);
slist(X) -> [X].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% the print_loop process
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
print_init(PrintFun) ->
  print_loop(PrintFun, 0, running).

print_loop(PrintFun, Acc, State) ->
  maybe_exit(State, PrintFun, Acc),
  receive
    Ms = [_|_] -> print_loop(PrintFun, lists:foldl(PrintFun, Acc, Ms), State);
    stop -> print_loop(PrintFun, Acc, stopping)
  end.

maybe_exit(State, PrintFun, Acc) ->
  case State == stopping andalso process_info(self(), message_queue_len) of
    {_, 0} -> exit(PrintFun({meta, stop, dummy, {0, 0, 0, 0}}, Acc));
    _ -> ok
  end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%make ints human readable

human(X) when not is_number(X) -> X;
human(I) when I < 0 -> "-"++human(-I);
human(I) when 0 < I ->
  case math:log10(I) of
    M when 15=<M -> human(M-15, "P");
    M when 12=<M -> human(M-12, "T");
    M when  9=<M -> human(M-9, "G");
    M when  6=<M -> human(M-6, "M");
    M when  3=<M -> human(M-3, "k");
    _            -> flat("~w", [I])
  end;
human(_) -> "0".

human(E, M) ->
  flat("~.1f~s", [math:pow(10, E), M]).

flat(Format, Args) ->
  lists:flatten(io_lib:fwrite(Format, Args)).
