From bb7187b0c78984a89afceb2926e41cde4272213b Mon Sep 17 00:00:00 2001 From: Kacper Marzecki Date: Fri, 20 Jun 2025 16:43:19 +0200 Subject: [PATCH] some progress, still fucked --- new.exs | 332 +++++++++++++++++--------------------------------------- 1 file changed, 102 insertions(+), 230 deletions(-) diff --git a/new.exs b/new.exs index dca8fc1..0867d87 100644 --- a/new.exs +++ b/new.exs @@ -1,20 +1,22 @@ defmodule Tdd.Debug do - @moduledoc "Helpers for debugging TDD structures and tracing function calls." - # alias Tdd.Store # Keep if used by your print functions + @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 - # --- Agent State Management --- - def init do + defp init_agent_if_needed do case Process.whereis(@agent_name) do nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name) - _pid -> :ok # Agent already started + _pid -> :ok end :ok end defp add_traced_pid(pid) when is_pid(pid) do - init() # Ensure agent is started + init_agent_if_needed() Agent.update(@agent_name, &MapSet.put(&1, pid)) end @@ -25,7 +27,7 @@ defmodule Tdd.Debug do end end - def is_pid_traced?(pid) when is_pid(pid) do + defp is_pid_traced?(pid) when is_pid(pid) do case Process.whereis(@agent_name) do nil -> false @@ -33,77 +35,37 @@ defmodule Tdd.Debug do try do Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity) rescue - _e in [Exit, ArgumentError] -> false # More specific rescue + _e in [Exit, ArgumentError] -> false end end end - # --- Formatting Helpers --- - @arg_length_limit 80 - @total_args_length_limit 200 - - # MODIFIED: Return the specific atoms format_arg_value expects - def __internal_placeholder_for_ignored_arg__, do: :__tdd_debug_ignored_arg__ - def __internal_placeholder_for_complex_pattern__, do: :__tdd_debug_complex_pattern__ - - def format_args_list(args_list) when is_list(args_list) do - formatted_args = Enum.map(args_list, &format_arg_value/1) - combined = Enum.join(formatted_args, ", ") - - if String.length(combined) > @total_args_length_limit do - String.slice(combined, 0, @total_args_length_limit - 3) <> "..." - else - combined - end - end - - def format_arg_value(arg) do - inspected = - case arg do - :__tdd_debug_ignored_arg__ -> "_" - :__tdd_debug_complex_pattern__ -> "" - _ -> inspect(arg, limit: @arg_length_limit, pretty: false, structs: true) # Use limit here too - end - - # The inspect limit is now applied within the inspect call itself for individual args mostly. - # This outer limit is more for the overall string representation from inspect if it didn't truncate. - if String.length(inspected) > @arg_length_limit do - String.slice(inspected, 0, @arg_length_limit - 3) <> "..." - else - inspected - end - end - - # --- Tracing Control --- + # --- Tracing Control Functions --- + @doc "Enables function call tracing for the current process." def enable_tracing do - init() pid_to_trace = self() add_traced_pid(pid_to_trace) ref = Process.monitor(pid_to_trace) - # Spawn linked to ensure monitor process dies if current process dies unexpectedly - Process.spawn( - fn -> - receive do - {:DOWN, ^ref, :process, ^pid_to_trace, _reason} -> - remove_traced_pid(pid_to_trace) - # after # Consider if timeout is strictly needed or if DOWN message is sufficient - # 3_600_000 -> # 1 hour timeout - # remove_traced_pid(pid_to_trace) - end - end, - [:monitor] - # Removed [:monitor] option as spawn_link and Process.monitor achieve desired effect - ) + Process.spawn_link(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) :ok end - def disable_tracing() do - # init() # Not strictly necessary here as remove_traced_pid handles agent not existing + @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 @@ -113,103 +75,27 @@ defmodule Tdd.Debug do end end - # --- Process Dictionary for Depth --- + # --- Process Dictionary for Call Depth --- defp get_depth, do: Process.get(:tdd_debug_depth, 0) - - def increment_depth do # Made private as it's an internal helper + defp increment_depth do new_depth = get_depth() + 1 Process.put(:tdd_debug_depth, new_depth) new_depth end - - def decrement_depth do # Made private + defp decrement_depth do new_depth = max(0, get_depth() - 1) Process.put(:tdd_debug_depth, new_depth) new_depth end # --- Core Macro Logic --- + @inspect_limit 100 # Default limit for inspect calls by this module + defmacro __using__(_opts) do quote do - import Kernel, except: [def: 2, def: 1, defp: 2, defp: 1] + import Kernel, except: [def: 1, def: 2, defp: 1, defp: 2] + import Tdd.Debug require Tdd.Debug - import Tdd.Debug # Imports def/defp from this module, and helper functions - end - end - - @doc false - defmacro do_instrument(type, call, clauses, _env) do - # As per your note, this macro might need updates. - # Main issues to check if used: - # 1. Hygiene of `actual_code_ast`: use Macro.escape/2. - # 2. Robustness of `arg_vars_for_runtime_access` for all pattern types (similar to generate_traced_function). - # Kept original logic for now based on your description. - {function_name, _meta_call, original_args_ast_nodes} = call - original_args_ast_nodes = original_args_ast_nodes || [] - - arg_vars_for_runtime_access = - Enum.map(original_args_ast_nodes, fn - {:_, _, ctx} when is_atom(ctx) or is_nil(ctx) -> - quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end - {:=, _, [var_ast, _]} -> - var_ast - {var_name, _meta, ctx} = pattern_ast when is_atom(var_name) and (is_atom(ctx) or is_nil(ctx)) and - not (var_name in [:_ , :%{}, :{}, :|, :<<>>, :fn, :->, :&, :^]) -> - pattern_ast - {constructor, _, _} = pattern_ast when constructor in [:%{}, :{}, :|, :<<>>] -> - quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end - pattern_ast -> - pattern_ast - end) - - actual_code_ast = - case clauses do - kw when is_list(kw) -> Keyword.get(kw, :do) - _ -> clauses - end - - traced_body = - quote location: :keep do - if Tdd.Debug.is_pid_traced?(self()) do - current_print_depth = Tdd.Debug.increment_depth() # Qualified - indent = String.duplicate(" ", current_print_depth - 1) - - runtime_arg_values = [unquote_splicing(arg_vars_for_runtime_access)] - IO.inspect(runtime_arg_values, label: "runtime_arg_values") - IO.puts("runtime_arg_values 1") - args_string = Tdd.Debug.format_args_list(runtime_arg_values) # Qualified - caller_module_name = Module.split(__MODULE__) |> Enum.join(".") - - IO.puts( - "#{indent}CALL: #{caller_module_name}.#{unquote(Macro.escape(function_name))}(#{args_string})" - ) - - try do - # Potential hygiene issue here if actual_code_ast clashes with macro vars - result = unquote(actual_code_ast) # Consider Macro.escape(actual_code_ast, unquote: true) - _ = Tdd.Debug.decrement_depth() # Qualified - IO.puts( - "#{indent}RETURN from #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(result)}" # Qualified - ) - result - rescue - exception_class -> - _ = Tdd.Debug.decrement_depth() # Qualified - error_instance = exception_class - stacktrace = __STACKTRACE__ - IO.puts( - "#{indent}ERROR in #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(error_instance)}" # Qualified - ) - reraise error_instance, stacktrace - end - else - IO.puts("runtime_arg_values 2") - unquote(actual_code_ast) # Consider Macro.escape - end - end - - quote do - Kernel.unquote(type)(unquote(call), do: unquote(traced_body)) end end @@ -223,102 +109,99 @@ defmodule Tdd.Debug do generate_traced_function(:defp, call, clauses, __CALLER__) end - defp generate_traced_function(type, call_ast, clauses, _caller_env) do - {function_name_ast, _meta_call, original_args_patterns_ast} = call_ast - args_patterns_ast = original_args_patterns_ast || [] + defp generate_traced_function(type, call_ast, clauses, caller_env) do + {function_name_ast, meta_call, original_args_patterns_ast} = call_ast + original_args_patterns_ast_list = original_args_patterns_ast || [] original_body_ast = (if Keyword.keyword?(clauses) do - Keyword.get(clauses, :do, clauses) # clauses could be `[do: actual_body]` or just `actual_body` + Keyword.get(clauses, :do, clauses) else - clauses - end) || quote(do: nil) # Default to a nil body if nothing provided + clauses # Body is directly provided + end) || quote(do: nil) # Default to `do: nil` if no body - # --- REVISED logging_expressions_ast_list --- - logging_expressions_ast_list = - Enum.map(args_patterns_ast, fn arg_pattern_ast_outer -> - core_pattern_ast_for_logging = - case arg_pattern_ast_outer do - {:when, _, [pattern, _guard]} -> pattern - other -> other - end + # Transform arguments: `pattern` becomes `__td_arg_N__ = pattern` + # And collect the `__td_arg_N__` variables for logging. - cond do - match?({:_, _, ctx} when is_atom(ctx) or is_nil(ctx), core_pattern_ast_for_logging) -> - quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end + # Step 1: Map original patterns to a list of {new_pattern_ast, generated_var_ast} tuples + # Enum.with_index provides the index for unique variable naming. + mapped_and_generated_vars_tuples = + Enum.map(Enum.with_index(original_args_patterns_ast_list), fn {pattern_ast, index} -> + # Create a unique, hygienic variable name like __td_arg_0__ + # Using caller_env.module for context makes the variable hygienic to the calling module. + generated_var_name = String.to_atom("__td_arg_#{index}__") + generated_var_ast = Macro.var(generated_var_name, caller_env.module) - match?({:=, _, [{_var_ast, _, _}, _sub_pattern_ast]}, core_pattern_ast_for_logging) -> - {:=, _, [var_ast, _]} = core_pattern_ast_for_logging - var_ast - - match?({constructor, _, _args} when constructor in [:%{}, :{}, :|, :<<>>], core_pattern_ast_for_logging) -> - quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end - - match?({name_atom, _, context} when is_atom(name_atom) and name_atom != :_ and (is_atom(context) or is_nil(context)), core_pattern_ast_for_logging) -> - {name_atom_inner, _meta, context_inner} = core_pattern_ast_for_logging - if is_nil(context_inner) and name_atom_inner not in [:true, :false, :nil] do - name_atom_inner - else - core_pattern_ast_for_logging - end - - # Macro.is_literal(core_pattern_ast_for_logging) -> - # core_pattern_ast_for_logging - - # Fallback for unhandled complex patterns or other ASTs. - # If it's a variable AST that somehow slipped through (e.g. context wasn't atom/nil for a var), - # it might be okay to pass `core_pattern_ast_for_logging` directly. - # However, to be safe and avoid `_` or other non-value ASTs, placeholder is better. - true -> - # This case implies a pattern that is not `_`, not `var=pattern`, not a common structure, - # not a simple var/literal atom, and not a known literal. - # It's likely a more complex pattern we haven't explicitly handled for logging. - quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end + # This AST represents: __td_arg_N__ = original_pattern_N + new_pattern_ast = quote do + unquote(generated_var_ast) = unquote(pattern_ast) end + + {new_pattern_ast, generated_var_ast} end) - # --- END REVISED logging_expressions_ast_list --- + + # Step 2: Unzip the list of tuples into two separate lists + {new_args_patterns_ast_list, generated_arg_vars_asts} = + Enum.unzip(mapped_and_generated_vars_tuples) + + # Reconstruct the call_ast with the new argument patterns + # new_args_patterns_ast_list now contains ASTs like `[__td_arg_0__ = pattern0, __td_arg_1__ = pattern1, ...]` + new_call_ast = {function_name_ast, meta_call, new_args_patterns_ast_list} traced_body_inner_ast = quote do if Tdd.Debug.is_pid_traced?(self()) do - current_print_depth = Tdd.Debug.increment_depth() # Qualified + current_print_depth = Tdd.Debug.increment_depth() indent = String.duplicate(" ", current_print_depth - 1) - __runtime_arg_values_for_logging__ = - [unquote_splicing(logging_expressions_ast_list)] + # runtime_arg_values will be a list of the actual values bound to __td_arg_0__, __td_arg_1__, etc. + # generated_arg_vars_asts is `[__td_arg_0_ast, __td_arg_1_ast, ...]` + runtime_arg_values = [unquote_splicing(generated_arg_vars_asts)] - args_string = Tdd.Debug.format_args_list(__runtime_arg_values_for_logging__) # Qualified caller_module_name_str = Module.split(__MODULE__) |> Enum.join(".") - - __Printable_fn_name_intermediate__ = unquote(function_name_ast) printable_function_name_str = - case __Printable_fn_name_intermediate__ do + case unquote(function_name_ast) do fn_name_atom when is_atom(fn_name_atom) -> Atom.to_string(fn_name_atom) - fn_name_ast_complex -> Macro.to_string(fn_name_ast_complex) # Handles operators etc. + complex_fn_ast -> Macro.to_string(complex_fn_ast) end IO.puts( - "#{indent}CALL: #{caller_module_name_str}.#{printable_function_name_str}(#{args_string})" + "#{indent}CALL: #{caller_module_name_str}.#{printable_function_name_str}" + ) + IO.puts( + "#{indent} ARGS: #{inspect(runtime_arg_values)}" ) try do + # The original_body_ast will execute in a context where __td_arg_N__ are bound + # to the values of the original patterns. result = unquote(Macro.escape(original_body_ast, unquote: true)) - _ = Tdd.Debug.decrement_depth() # Qualified + + _ = Tdd.Debug.decrement_depth() IO.puts( - "#{indent}RETURN from #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(result)}" # Qualified + "#{indent}RETURN from #{caller_module_name_str}.#{printable_function_name_str}: #{inspect(result)}" ) result rescue exception_class -> - _ = Tdd.Debug.decrement_depth() # Qualified error_instance = exception_class stacktrace = __STACKTRACE__ + _ = Tdd.Debug.decrement_depth() IO.puts( - "#{indent}ERROR in #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(error_instance)}" # Qualified + "#{indent}ERROR in #{caller_module_name_str}.#{printable_function_name_str}: #{inspect(error_instance)}" ) reraise error_instance, stacktrace end else + # If not traced, execute the original body. Note: this branch will *not* have + # the __td_arg_N__ variables bound. The `new_call_ast` with these assignments + # is only used if we go into the traced path. This is a subtle point. + # To ensure the __td_arg_N__ = pattern bindings always happen, + # the final_definition_ast should *always* use new_call_ast. + # The `if` condition should only gate the logging. + # Let's adjust this: the bindings MUST happen for the body to work with the new var names if it were changed. + # However, the original_body_ast uses the original pattern variable names. + # So, the original_body_ast is fine. The `new_call_ast` is what defines the function signature. unquote(Macro.escape(original_body_ast, unquote: true)) end end @@ -326,31 +209,23 @@ defmodule Tdd.Debug do final_definition_ast = quote location: :keep do Kernel.unquote(type)( - unquote(call_ast), # This unquotes the function head {name, meta, args_patterns} + unquote(new_call_ast), # Use the call_ast with instrumented args: `def my_fun(__td_arg_0__ = pattern0, ...)` do: unquote(traced_body_inner_ast) ) end - # For debugging the macro itself: - # require Logger - # Logger.debug("Generated AST for #{type} #{Macro.to_string(call_ast)}:\n#{Macro.to_string(final_definition_ast)}") - final_definition_ast end - # --- TDD Graph Printing (Kept for completeness) --- - @doc "Prints a formatted representation of a TDD graph starting from an ID." - def print(id) do - # It's better to alias Tdd.Store at the top of the module if consistently used, - # or pass it as an argument if it's a dependency. - # For now, keeping alias local to this function if Tdd.Store is not used elsewhere in Debug. - alias Tdd.Store # Assuming Tdd.Store is available + # --- 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(id, 0, MapSet.new(), Tdd.Store) # Pass Store if it's an external module + do_print_tdd_node(id, 0, MapSet.new(), store_module) IO.puts("------------------------") end - defp do_print(id, indent_level, visited, store_module) do # Accept store_module + defp do_print_tdd_node(id, indent_level, visited, store_module) do prefix = String.duplicate(" ", indent_level) if MapSet.member?(visited, id) do @@ -358,20 +233,17 @@ defmodule Tdd.Debug do :ok else new_visited = MapSet.put(visited, id) - # alias Tdd.Store # Removed from here - case store_module.get_node(id) do # Use passed module - {:ok, :true_terminal} -> - IO.puts("#{prefix}ID #{id} -> TRUE") - {:ok, :false_terminal} -> - IO.puts("#{prefix}ID #{id} -> FALSE") - {:ok, {var, y, n, d}} -> + 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:") - do_print(y, indent_level + 2, new_visited, store_module) - IO.puts("#{prefix} ├─ No:") - do_print(n, indent_level + 2, new_visited, store_module) - IO.puts("#{prefix} └─ DC:") - do_print(d, indent_level + 2, new_visited, store_module) + 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