diff --git a/debug.exs b/debug.exs new file mode 100644 index 0000000..e0f0e17 --- /dev/null +++ b/debug.exs @@ -0,0 +1,295 @@ +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 diff --git a/new.exs b/new.exs index 18551a6..7624cf4 100644 --- a/new.exs +++ b/new.exs @@ -1,298 +1,5 @@ -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 + +Code.require_file("./debug.exs") defmodule Tdd.TypeSpec do @moduledoc """ @@ -640,7 +347,6 @@ defmodule Tdd.TypeSpec do end defmodule Tdd.Store do - use Tdd.Debug @moduledoc """ Manages the state of the TDD system's node graph and operation cache. @@ -824,7 +530,7 @@ defmodule Tdd.Store do end defmodule Tdd.Variable do - use Tdd.Debug + # use Tdd.Debug @moduledoc """ Defines the canonical structure for all Tdd predicate variables. @@ -876,9 +582,6 @@ defmodule Tdd.Variable do end # --- Category 5: List Properties --- - # All are now 4-element tuples. The sorting will be correct. - # @spec v_list_all_elements_are(TypeSpec.t()) :: term() - # def v_list_all_elements_are(element_spec), do: {5, :a_all_elements, element_spec, nil} @doc "Predicate: The list is the empty list `[]`." @spec v_list_is_empty() :: term() @@ -900,10 +603,6 @@ defmodule Tdd.Predicate.Info do @doc "Returns a map of traits for a given predicate variable." @spec get_traits(term()) :: map() | nil - # --- THIS IS THE FIX --- - # Add explicit negative implications for primary types. When a value is proven - # to be of one primary type, it is implicitly proven NOT to be of the others. - def get_traits({0, :is_atom, _, _}) do %{ type: :primary, @@ -1016,7 +715,7 @@ end # in a new file, e.g., lib/tdd/consistency/engine.ex defmodule Tdd.Consistency.Engine do - use Tdd.Debug + # use Tdd.Debug @moduledoc """ A rule-based engine for checking the semantic consistency of a set of assumptions. diff --git a/scratch.exs b/scratch.exs new file mode 100644 index 0000000..0001189 --- /dev/null +++ b/scratch.exs @@ -0,0 +1,308 @@ +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})