some progress, still fucked

This commit is contained in:
Kacper Marzecki 2025-06-20 16:43:19 +02:00
parent 3c7edc67da
commit bb7187b0c7

332
new.exs
View File

@ -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__ -> "<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