From 8d8b3607fc2489a931810885a163ba864a064f3a Mon Sep 17 00:00:00 2001 From: Kacper Marzecki Date: Fri, 20 Jun 2025 15:52:18 +0200 Subject: [PATCH] checkpoint, still fucked debugging --- new.exs | 329 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 206 insertions(+), 123 deletions(-) diff --git a/new.exs b/new.exs index 18471b8..1b64797 100644 --- a/new.exs +++ b/new.exs @@ -8,32 +8,33 @@ defmodule Tdd.Debug do def init do case Process.whereis(@agent_name) do nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name) - _pid -> :ok + _pid -> :ok # Agent already started end - :ok end defp add_traced_pid(pid) when is_pid(pid) do - init() + init() # Ensure agent is started Agent.update(@agent_name, &MapSet.put(&1, pid)) end defp remove_traced_pid(pid) when is_pid(pid) do - if agent_pid = Agent.whereis(@agent_name) do - Agent.cast(agent_pid, fn state -> MapSet.delete(state, pid) end) + # Use Process.whereis to avoid race condition if agent stops between calls + case Process.whereis(@agent_name) do + nil -> :ok # Agent not running, nothing to remove from + agent_pid -> Agent.cast(agent_pid, fn state -> MapSet.delete(state, pid) end) end end - defp is_pid_traced?(pid) when is_pid(pid) do - case Agent.whereis(@agent_name) do + 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 + # Catches if agent dies or is not an agent anymore _e in [Exit, ArgumentError] -> false end end @@ -43,7 +44,13 @@ defmodule Tdd.Debug do @arg_length_limit 80 @total_args_length_limit 200 - defp format_args_list(args_list) when is_list(args_list) do + # Helper function to return a unique atom for ignored arguments + # This is called from the macro-generated code. + def __internal_placeholder_for_ignored_arg__ do + :__tdd_debug_ignored_arg__ # A unique atom + end + + 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, ", ") @@ -54,8 +61,16 @@ defmodule Tdd.Debug do end end - defp format_arg_value(arg) do - inspected = inspect(arg, limit: :infinity, pretty: false, structs: true) + def format_arg_value(arg) do + inspected = + case arg do + # Check if it's our specific placeholder atom for `_` + :__tdd_debug_ignored_arg__ -> "_" + # For other arguments, inspect them normally. + # This will handle runtime values for simple variables/literals, + # and ASTs for complex patterns (if that's what's passed). + _ -> inspect(arg, limit: :infinity, pretty: false, structs: true) + end if String.length(inspected) > @arg_length_limit do String.slice(inspected, 0, @arg_length_limit - 3) <> "..." @@ -69,29 +84,38 @@ defmodule Tdd.Debug do init() pid_to_trace = self() add_traced_pid(pid_to_trace) - Process.flag(:trap_exit, true) + # Trap exits only if not already trapping, to avoid interfering with other code. + # However, monitoring is generally safer and less intrusive. + # Process.flag(:trap_exit, true) # Consider if this is truly needed or if monitoring is enough. 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 -> remove_traced_pid(pid_to_trace) - end - end) + # Spawn a separate process to monitor. + # Use spawn_opt to avoid linking if this monitoring process crashes. + Process.spawn( + fn -> + receive do + {:DOWN, ^ref, :process, ^pid_to_trace, _reason} -> + remove_traced_pid(pid_to_trace) + after + # 1 hour timeout as a fallback + 3_600_000 -> + remove_traced_pid(pid_to_trace) + end + end, + [:monitor] #:link option removed, monitor is explicit + ) :ok end def disable_tracing() do - # Ensure agent is available for removal - init() + init() # Ensure agent is available for removal if it was started by another call remove_traced_pid(self()) + :ok # Good practice to return :ok end def run(fun) when is_function(fun, 0) do enable_tracing() - try do fun.() after @@ -100,18 +124,16 @@ defmodule Tdd.Debug do end # --- Process Dictionary for Depth --- - defp get_depth do - Process.get(:tdd_debug_depth, 0) - end + defp get_depth, do: Process.get(:tdd_debug_depth, 0) - defp increment_depth do + def increment_depth do new_depth = get_depth() + 1 Process.put(:tdd_debug_depth, new_depth) new_depth end - defp decrement_depth do - new_depth = max(0, get_depth() - 1) + def decrement_depth do + new_depth = max(0, get_depth() - 1) # Ensure depth doesn't go below 0 Process.put(:tdd_debug_depth, new_depth) new_depth end @@ -119,149 +141,195 @@ defmodule Tdd.Debug do # --- Core Macro Logic --- defmacro __using__(_opts) do quote do - # Make Tdd.Debug functions (like do_instrument, format_args_list) callable - # from the macros we are about to define locally in the user's module. import Kernel, except: [def: 2, def: 1, defp: 2, defp: 1] require Tdd.Debug + # Import Tdd.Debug's def/defp macros and other public functions/macros import Tdd.Debug end end - # This is an internal macro helper, not intended for direct user call + # The `do_instrument` macro seems to be an older/alternative version. + # The primary mechanism used by `def`/`defp` is `generate_traced_function`. + # We'll keep `do_instrument` as it was in your example, in case it's used elsewhere, + # but ensure it's not the cause of the current problem. @doc false - defmacro do_instrument(type, call, clauses, env) do - # CORRECTED: Decompose the function call AST + defmacro do_instrument(type, call, clauses, _env) do {function_name, _meta_call, original_args_ast_nodes} = call - # Ensure it's a list for `def foo` vs `def foo()` original_args_ast_nodes = original_args_ast_nodes || [] - # CORRECTED: Generate argument variable ASTs for runtime access. - # These vars will hold the actual values of arguments after pattern matching. - arg_vars_for_runtime_access = original_args_ast_nodes + # This part was problematic if original_args_ast_nodes contained `_` + # and was used directly to form a list of expressions. + arg_vars_for_runtime_access = + Enum.map(original_args_ast_nodes, fn + {:_, _, Elixir} -> quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end + {:=, _, [var_ast, _]} -> var_ast + pattern_ast -> pattern_ast + end) actual_code_ast = case clauses do - [do: block_content] -> block_content + # [do: block_content | _] -> block_content kw when is_list(kw) -> Keyword.get(kw, :do) - # `do: :atom` or `do: variable` _ -> clauses end - # Keep original line numbers for stacktraces from user code traced_body = quote location: :keep do - if Tdd.Debug.is_pid_traced?(self()) do - current_print_depth = Tdd.Debug.increment_depth() + if is_pid_traced?(self()) do + current_print_depth = increment_depth() indent = String.duplicate(" ", current_print_depth - 1) - # CORRECTED: Use the generated vars for runtime access to get actual argument values. - # We unquote_splicing them into a list literal. - runtime_arg_values = arg_vars_for_runtime_access - args_string = Tdd.Debug.format_args_list(runtime_arg_values) - - # __MODULE__ here refers to the module where `use Tdd.Debug` is called. + runtime_arg_values = [unquote_splicing(arg_vars_for_runtime_access)] + args_string = format_args_list(runtime_arg_values) caller_module_name = Module.split(__MODULE__) |> Enum.join(".") IO.puts( - "#{indent}CALL: #{caller_module_name}.#{unquote(function_name)}(#{args_string})" + "#{indent}CALL: #{caller_module_name}.#{unquote(Macro.escape(function_name))}(#{args_string})" ) try do - # Execute original function body result = unquote(actual_code_ast) - _ = Tdd.Debug.decrement_depth() - + _ = decrement_depth() IO.puts( - "#{indent}RETURN from #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(result)}" + "#{indent}RETURN from #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(result)}" ) - result rescue exception_class -> - _ = Tdd.Debug.decrement_depth() - error_instance = __catch__(exception_class) + _ = decrement_depth() + error_instance = exception_class stacktrace = __STACKTRACE__ - IO.puts( - "#{indent}ERROR in #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(error_instance)}" + "#{indent}ERROR in #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(error_instance)}" ) - reraise error_instance, stacktrace end else - # Tracing not enabled, execute original code unquote(actual_code_ast) end end - # Construct the final definition (def or defp) using Kernel's versions quote do Kernel.unquote(type)(unquote(call), do: unquote(traced_body)) end end - # Define the overriding def/defp macros directly in the user's module. - # These will take precedence over Kernel.def/defp. @doc false -defmacro def(call, clauses \\ nil) do - generate_traced_function(:def, call, clauses) -end - -defmacro defp(call, clauses \\ nil) do - generate_traced_function(:defp, call, clauses) -end - -def generate_traced_function(type, call, clauses) do - {function_name, _meta_call, original_args_ast_nodes} = call - args = original_args_ast_nodes || [] - - block_ast = - case clauses do - [do: block_content] -> block_content - kw when is_list(kw) -> Keyword.get(kw, :do) - _ -> clauses - end - - quote location: :keep do - Kernel.unquote(type)(unquote(call)) do - if Tdd.Debug.is_pid_traced?(self()) do - current_print_depth = Tdd.Debug.increment_depth() - indent = String.duplicate(" ", current_print_depth - 1) - args_string = Tdd.Debug.format_args_list([unquote_splicing(args)]) - caller_module_name = Module.split(__MODULE__) |> Enum.join(".") - - IO.puts("#{indent}CALL: #{caller_module_name}.#{unquote(function_name)}(#{args_string})") - - try do - result = unquote(block_ast) - _ = Tdd.Debug.decrement_depth() - - IO.puts("#{indent}RETURN from #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(result)}") - result - rescue - exception_class -> - _ = Tdd.Debug.decrement_depth() - error_instance = __catch__(exception_class) - stacktrace = __STACKTRACE__ - - IO.puts("#{indent}ERROR in #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(error_instance)}") - reraise error_instance, stacktrace - end - else - unquote(block_ast) - end - end + defmacro def(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or [] + generate_traced_function(:def, call, clauses, __CALLER__) + end + + @doc false + defmacro defp(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or [] + generate_traced_function(:defp, call, clauses, __CALLER__) + end + + + # Capture __CALLER__ to ensure hygiene for variables generated by the macro + # if needed, though direct AST manipulation often bypasses some hygiene issues. + defp generate_traced_function(type, call_ast, clauses, _caller_env) do + {function_name_ast, _meta_call, original_args_ast_nodes} = call_ast + args_patterns_ast = original_args_ast_nodes || [] + + original_body_ast = + if Keyword.keyword?(clauses) do + Keyword.get(clauses, :do, clauses) + else + clauses + end || quote(do: nil) # Default to an empty body if none provided + + # Transform argument patterns into expressions suitable for logging at runtime. + logging_expressions_ast_list = + Enum.map(args_patterns_ast, fn arg_pattern_ast -> + case arg_pattern_ast do + {:=, _meta_assign, [var_ast, _sub_pattern_ast]} -> + var_ast + _ -> + Macro.postwalk(arg_pattern_ast, fn + current_node_ast -> + case current_node_ast do + # Match any underscore, regardless of context if it's a bare underscore node + {:_, _, context} when is_atom(context) -> # context is often Elixir or nil + quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end + _ -> + current_node_ast + end + end) + end + end) + + # This is the AST for the code that will become the *actual body* of the traced function. + traced_body_inner_ast = + quote do + if is_pid_traced?(self()) do + current_print_depth = increment_depth() + indent = String.duplicate(" ", current_print_depth - 1) + + # Note: Macro.escape is used here because logging_expressions_ast_list + # and function_name_ast are ASTs being injected into this quote. + __runtime_arg_values_for_logging__ = + [unquote_splicing(Macro.escape(logging_expressions_ast_list, unquote: true))] + + args_string = format_args_list(__runtime_arg_values_for_logging__) + caller_module_name_str = Module.split(__MODULE__) |> Enum.join(".") + + printable_function_name_str = + case unquote(Macro.escape(function_name_ast, unquote: true)) 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) + end + + IO.puts( + "#{indent}CALL: #{caller_module_name_str}.#{printable_function_name_str}(#{args_string})" + ) + + try do + # Original body is injected here, also escaped. + result = unquote(Macro.escape(original_body_ast, unquote: true)) + _ = Tdd.Debug.decrement_depth() + IO.puts( + "#{indent}RETURN from #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(result)}" + ) + result + rescue + exception_class -> + _ = Tdd.Debug.decrement_depth() + error_instance = exception_class + stacktrace = __STACKTRACE__ + IO.puts( + "#{indent}ERROR in #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(error_instance)}" + ) + reraise error_instance, stacktrace + end + else + # Tracing not enabled, execute original body directly (escaped). + unquote(Macro.escape(original_body_ast, unquote: true)) + end + end + + # Construct the final `Kernel.def` or `Kernel.defp` call. + # `call_ast` is the original function head. + # `traced_body_inner_ast` is the AST for the body we just constructed. + IO.inspect(call_ast, label: "call_ast") + IO.inspect(Macro.escape(call_ast, unquote: true), label: "Macro.escape(call_ast, unquote: true)") + final_definition_ast = + quote location: :keep do # unquote: false is default and implied if not set + Kernel.unquote(type)( + unquote(call_ast), + do: unquote(traced_body_inner_ast) # The body AST is passed via `do:` + ) + end + + # Uncomment for debugging the generated code: + # IO.inspect(Macro.to_string(final_definition_ast), label: "Generated for #{Macro.to_string(call_ast)}") + + final_definition_ast end -end - # REMOVE the defmacro def/2 and defmacro defp/2 from here. - # They are now defined by the __using__ macro. # --- Your TDD Graph Printing (unrelated to the tracing error, kept for completeness) --- @doc "Prints a formatted representation of a TDD graph starting from an ID." def print(id) do - # Assuming Tdd.Store is defined elsewhere and aliased if needed - # Or ensure it's globally available if defined in another file - alias Tdd.Store + alias Tdd.Store # Assuming Tdd.Store is available IO.puts("--- TDD Graph (ID: #{id}) ---") do_print(id, 0, MapSet.new()) IO.puts("------------------------") @@ -275,14 +343,12 @@ end :ok else new_visited = MapSet.put(visited, id) - # Assuming Tdd.Store.get_node/1 is available + alias Tdd.Store # Assuming Tdd.Store is available case Tdd.Store.get_node(id) do {:ok, :true_terminal} -> IO.puts("#{prefix}ID #{id} -> TRUE") - {:ok, :false_terminal} -> IO.puts("#{prefix}ID #{id} -> FALSE") - {:ok, {var, y, n, d}} -> IO.puts("#{prefix}ID #{id}: IF #{inspect(var)}") IO.puts("#{prefix} ├─ Yes:") @@ -291,7 +357,6 @@ end do_print(n, indent_level + 2, new_visited) IO.puts("#{prefix} └─ DC:") do_print(d, indent_level + 2, new_visited) - {:error, reason} -> IO.puts("#{prefix}ID #{id}: ERROR - #{reason}") end @@ -1349,12 +1414,29 @@ defmodule Tdd.Algo do @spec negate(non_neg_integer) :: non_neg_integer def negate(tdd_id) do cache_key = {:negate, tdd_id} - + IO.inspect(tdd_id) case Store.get_op_cache(cache_key) do {:ok, result_id} -> result_id - :not_found -> + + :not_found -> + + result_id = + case Store.get_node(tdd_id) do + {:ok, :true_terminal} -> + Store.false_node_id() + + {:ok, :false_terminal} -> + Store.true_node_id() + + {:ok, {var, y, n, d}} -> + Store.find_or_create_node(var, negate(y), negate(n), negate(d)) + end + + Store.put_op_cache(cache_key, result_id) + result_id + { :error, :not_found } -> result_id = case Store.get_node(tdd_id) do {:ok, :true_terminal} -> @@ -3016,6 +3098,7 @@ defmodule CompilerAlgoTests do # --- Section: Basic Subtyping --- IO.puts("\n--- Section: Basic Subtyping ---") + Tdd.Debug.enable_tracing() test_subtype(":foo <: atom", true, {:literal, :foo}, :atom) test_subtype("atom <: :foo", false, :atom, {:literal, :foo}) test_subtype(":foo <: integer", false, {:literal, :foo}, :integer)