defmodule Tdd.Debug do @moduledoc """ Provides macros to wrap `def` and `defp` for simple function call/return tracing. Logs arguments as a list and return values using `IO.inspect`. """ # --- Agent for Tracing State --- @agent_name Tdd.Debug.StateAgent def init_agent_if_needed do case Process.whereis(@agent_name) do nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name) _pid -> :ok end :ok end def add_traced_pid(pid) when is_pid(pid) do init_agent_if_needed() Agent.update(@agent_name, &MapSet.put(&1, pid)) end def remove_traced_pid(pid) when is_pid(pid) do case Process.whereis(@agent_name) do nil -> :ok agent_pid -> Agent.cast(agent_pid, fn state -> MapSet.delete(state, pid) end) end end def is_pid_traced?(pid) when is_pid(pid) do case Process.whereis(@agent_name) do nil -> false agent_pid -> try do Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity) rescue _e in [Exit, ArgumentError] -> false end end end # --- Tracing Control Functions --- @doc "Enables function call tracing for the current process." def enable_tracing do pid_to_trace = self() add_traced_pid(pid_to_trace) ref = Process.monitor(pid_to_trace) Process.spawn(fn -> receive do {:DOWN, ^ref, :process, ^pid_to_trace, _reason} -> remove_traced_pid(pid_to_trace) after 3_600_000 -> # 1 hour safety timeout remove_traced_pid(pid_to_trace) end end, [:monitor]) :ok end @doc "Disables function call tracing for the current process." def disable_tracing do remove_traced_pid(self()) :ok end @doc "Runs the given 0-arity function with tracing enabled, then disables it." def run(fun) when is_function(fun, 0) do enable_tracing() try do fun.() after disable_tracing() end end # --- Process Dictionary for Call Depth --- defp get_depth, do: Process.get(:tdd_debug_depth, 0) def increment_depth do new_depth = get_depth() + 1 Process.put(:tdd_debug_depth, new_depth) new_depth end def decrement_depth do new_depth = max(0, get_depth() - 1) Process.put(:tdd_debug_depth, new_depth) new_depth end # --- Core Macro Logic --- defmacro __using__(_opts) do quote do import Kernel, except: [def: 1, def: 2, defp: 1, defp: 2] import Tdd.Debug require Tdd.Debug end end @doc false defmacro def(call, clauses \\ Keyword.new()) do generate_traced_function(:def, call, clauses, __CALLER__) end @doc false defmacro defp(call, clauses \\ Keyword.new()) do generate_traced_function(:defp, call, clauses, __CALLER__) end defp is_simple_variable_ast?(ast_node) do case ast_node do {var_name, _meta, _context} when is_atom(var_name) -> var_name != :_ _ -> false end end defp generate_traced_function(type, call_ast, clauses, caller_env) do require Macro # Good practice {function_name_ast, meta_call, original_args_patterns_ast_nullable} = call_ast original_args_patterns_ast_list = original_args_patterns_ast_nullable || [] original_body_ast = (if Keyword.keyword?(clauses) do Keyword.get(clauses, :do, clauses) else clauses end) || quote(do: nil) mapped_and_generated_vars_tuples = Enum.map(Enum.with_index(original_args_patterns_ast_list), fn {original_pattern_ast, index} -> # __td_arg_N__ is for logging, make it hygienic with `nil` context (or __MODULE__) td_arg_var = Macro.var(String.to_atom("__td_arg_#{index}__"), nil) {final_pattern_for_head, rhs_for_td_arg_assignment} = case original_pattern_ast do # 1. Ignored variable: `_` # AST: {:_, meta, context_module_or_nil} {:_, _, _} = underscore_ast -> {underscore_ast, quote(do: :__td_ignored_argument__)} # 2. Assignment pattern: `var = pattern` or `var = _` # AST: {:=, meta, [lhs, rhs_of_assign]} {:=, _meta_assign, [lhs_of_assign, _rhs_of_assign]} = assignment_pattern_ast -> if is_simple_variable_ast?(lhs_of_assign) do {assignment_pattern_ast, lhs_of_assign} # Head uses `var = pattern`, log `var` else # LHS is complex (e.g., `%{key: v} = pattern`), capture the whole value. captured_val_var = Macro.unique_var(String.to_atom("tdc_assign_#{index}"), Elixir) new_head_pattern = quote do unquote(captured_val_var) = unquote(assignment_pattern_ast) end {new_head_pattern, captured_val_var} end # 3. Default argument: `pattern_before_default \\ default_value` # AST: {:\|, meta, [pattern_before_default, default_value_ast]} {:\\, _meta_default, [pattern_before_default, _default_value_ast]} = default_arg_pattern_ast -> cond do # 3a. `var \\ default` is_simple_variable_ast?(pattern_before_default) -> {default_arg_pattern_ast, pattern_before_default} # 3b. `(var = inner_pattern) \\ default` match?({:=, _, [lhs_inner_assign, _]}, pattern_before_default) and is_simple_variable_ast?(pattern_before_default |> elem(2) |> Enum.at(0)) -> {:=, _, [lhs_inner_assign, _]}= pattern_before_default # `lhs_inner_assign` is the var on the left of `=` {default_arg_pattern_ast, lhs_inner_assign} # 3c. `(complex_pattern) \\ default` or `(_ = inner_pattern) \\ default` etc. true -> captured_val_var = Macro.unique_var(String.to_atom("tdc_def_#{index}"), Elixir) new_head_pattern = quote do unquote(captured_val_var) = unquote(default_arg_pattern_ast) end {new_head_pattern, captured_val_var} end # 4. Simple variable `var` (checked using our helper) # or other complex patterns/literals not caught above. ast_node -> if is_simple_variable_ast?(ast_node) do {ast_node, ast_node} # Head uses `var`, log `var` else # It's a complex pattern (e.g., `%{a:x}`, `[h|t]`) or a literal not assignable to. captured_val_var = Macro.unique_var(String.to_atom("tdc_pat_#{index}"), Elixir) new_head_pattern = quote do unquote(captured_val_var) = unquote(ast_node) end {new_head_pattern, captured_val_var} end end assignment_ast = quote do unquote(td_arg_var) = unquote(rhs_for_td_arg_assignment) end {final_pattern_for_head, assignment_ast, td_arg_var} end) {new_args_patterns_for_head_list, assignments_for_logging_vars_ast_list, generated_vars_to_log_asts} = if mapped_and_generated_vars_tuples == [], do: {[], [], []}, else: mapped_and_generated_vars_tuples |> Enum.map(&Tuple.to_list(&1)) |> Enum.zip()|> Enum.map(&Tuple.to_list(&1)) |> then(fn [a, b, c] -> {a, b, c} end) # Enum.unzip(mapped_and_generated_vars_tuples) new_call_ast = {function_name_ast, meta_call, new_args_patterns_for_head_list} traced_body_inner_ast = quote do unquote_splicing(assignments_for_logging_vars_ast_list) if Tdd.Debug.is_pid_traced?(self()) do current_print_depth = Tdd.Debug.increment_depth() indent = String.duplicate(" ", current_print_depth - 1) runtime_arg_values = [unquote_splicing(generated_vars_to_log_asts)] actual_module_name_str = Atom.to_string(unquote(caller_env.module)) # The function_name_ast is resolved at macro expansion time. # If it's `def foo(...)`, `unquote(function_name_ast)` becomes `:foo`. # If `def unquote(name_var)(...)`, it resolves `name_var`. resolved_fn_name = unquote(function_name_ast) printable_function_name_str = if is_atom(resolved_fn_name) do Atom.to_string(resolved_fn_name) else Macro.to_string(resolved_fn_name) # For complex names / operators if AST passed end IO.puts( "#{indent}CALL: #{actual_module_name_str}.#{printable_function_name_str}" ) IO.puts( "#{indent} ARGS: #{inspect(runtime_arg_values)}" ) try do result = unquote(original_body_ast) _ = Tdd.Debug.decrement_depth() IO.puts( "#{indent}RETURN from #{actual_module_name_str}.#{printable_function_name_str}: #{inspect(result)}" ) result rescue exception_class -> error_instance = exception_class stacktrace = __STACKTRACE__ _ = Tdd.Debug.decrement_depth() IO.puts( "#{indent}ERROR in #{actual_module_name_str}.#{printable_function_name_str}: #{inspect(error_instance)}" ) reraise error_instance, stacktrace end else unquote(original_body_ast) end end final_definition_ast = quote location: :keep do Kernel.unquote(type)( unquote(new_call_ast), do: unquote(traced_body_inner_ast) ) end final_definition_ast end # --- TDD Graph Printing (Kept as it was, not directly related to call tracing simplification) --- @doc "Prints a formatted representation of a TDD graph structure." def print_tdd_graph(id, store_module \\ Tdd.Store) do IO.puts("--- TDD Graph (ID: #{id}) ---") do_print_tdd_node(id, 0, MapSet.new(), store_module) IO.puts("------------------------") end defp do_print_tdd_node(id, indent_level, visited, store_module) do prefix = String.duplicate(" ", indent_level) if MapSet.member?(visited, id) do IO.puts("#{prefix}ID #{id} -> [Seen, recursive link]") :ok else new_visited = MapSet.put(visited, id) case store_module.get_node(id) do # Assumes store_module.get_node/1 exists {:ok, :true_terminal} -> IO.puts("#{prefix}ID #{id} -> TRUE") {:ok, :false_terminal} -> IO.puts("#{prefix}ID #{id} -> FALSE") {:ok, {var, y_id, n_id, dc_id}} -> IO.puts("#{prefix}ID #{id}: IF #{inspect(var)}") IO.puts("#{prefix} ├─ Yes (to ID #{y_id}):") do_print_tdd_node(y_id, indent_level + 2, new_visited, store_module) IO.puts("#{prefix} ├─ No (to ID #{n_id}):") do_print_tdd_node(n_id, indent_level + 2, new_visited, store_module) IO.puts("#{prefix} └─ DC (to ID #{dc_id}):") do_print_tdd_node(dc_id, indent_level + 2, new_visited, store_module) {:error, reason} -> IO.puts("#{prefix}ID #{id}: ERROR - #{reason}") end end end end defmodule Asd do use Tdd.Debug def kekistan(dupsko, {:sommething, _}) do IO.inspect("inside ") dupsko + 1 end end Process.sleep(100) Tdd.Debug.enable_tracing() # Asd.kekistan(1, 2) Asd.kekistan(1, {:sommething, 2})