checkpoint, still fucked debugging

This commit is contained in:
Kacper Marzecki 2025-06-20 15:52:18 +02:00
parent a78fe0541a
commit 8d8b3607fc

329
new.exs
View File

@ -8,32 +8,33 @@ defmodule Tdd.Debug do
def init do def init do
case Process.whereis(@agent_name) do case Process.whereis(@agent_name) do
nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name) nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name)
_pid -> :ok _pid -> :ok # Agent already started
end end
:ok :ok
end end
defp add_traced_pid(pid) when is_pid(pid) do defp add_traced_pid(pid) when is_pid(pid) do
init() init() # Ensure agent is started
Agent.update(@agent_name, &MapSet.put(&1, pid)) Agent.update(@agent_name, &MapSet.put(&1, pid))
end end
defp remove_traced_pid(pid) when is_pid(pid) do defp remove_traced_pid(pid) when is_pid(pid) do
if agent_pid = Agent.whereis(@agent_name) do # Use Process.whereis to avoid race condition if agent stops between calls
Agent.cast(agent_pid, fn state -> MapSet.delete(state, pid) end) 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
end end
defp is_pid_traced?(pid) when is_pid(pid) do def is_pid_traced?(pid) when is_pid(pid) do
case Agent.whereis(@agent_name) do case Process.whereis(@agent_name) do
nil -> nil ->
false false
agent_pid -> agent_pid ->
try do try do
Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity) Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity)
rescue rescue
# Catches if agent dies or is not an agent anymore
_e in [Exit, ArgumentError] -> false _e in [Exit, ArgumentError] -> false
end end
end end
@ -43,7 +44,13 @@ defmodule Tdd.Debug do
@arg_length_limit 80 @arg_length_limit 80
@total_args_length_limit 200 @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) formatted_args = Enum.map(args_list, &format_arg_value/1)
combined = Enum.join(formatted_args, ", ") combined = Enum.join(formatted_args, ", ")
@ -54,8 +61,16 @@ defmodule Tdd.Debug do
end end
end end
defp format_arg_value(arg) do def format_arg_value(arg) do
inspected = inspect(arg, limit: :infinity, pretty: false, structs: true) 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 if String.length(inspected) > @arg_length_limit do
String.slice(inspected, 0, @arg_length_limit - 3) <> "..." String.slice(inspected, 0, @arg_length_limit - 3) <> "..."
@ -69,29 +84,38 @@ defmodule Tdd.Debug do
init() init()
pid_to_trace = self() pid_to_trace = self()
add_traced_pid(pid_to_trace) 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) ref = Process.monitor(pid_to_trace)
Process.spawn(fn -> # Spawn a separate process to monitor.
receive do # Use spawn_opt to avoid linking if this monitoring process crashes.
{:DOWN, ^ref, :process, ^pid_to_trace, _reason} -> remove_traced_pid(pid_to_trace) Process.spawn(
after fn ->
3_600_000 -> remove_traced_pid(pid_to_trace) receive do
end {:DOWN, ^ref, :process, ^pid_to_trace, _reason} ->
end) 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 :ok
end end
def disable_tracing() do def disable_tracing() do
# Ensure agent is available for removal init() # Ensure agent is available for removal if it was started by another call
init()
remove_traced_pid(self()) remove_traced_pid(self())
:ok # Good practice to return :ok
end end
def run(fun) when is_function(fun, 0) do def run(fun) when is_function(fun, 0) do
enable_tracing() enable_tracing()
try do try do
fun.() fun.()
after after
@ -100,18 +124,16 @@ defmodule Tdd.Debug do
end end
# --- Process Dictionary for Depth --- # --- Process Dictionary for Depth ---
defp get_depth do defp get_depth, do: Process.get(:tdd_debug_depth, 0)
Process.get(:tdd_debug_depth, 0)
end
defp increment_depth do def increment_depth do
new_depth = get_depth() + 1 new_depth = get_depth() + 1
Process.put(:tdd_debug_depth, new_depth) Process.put(:tdd_debug_depth, new_depth)
new_depth new_depth
end end
defp decrement_depth do def decrement_depth do
new_depth = max(0, get_depth() - 1) new_depth = max(0, get_depth() - 1) # Ensure depth doesn't go below 0
Process.put(:tdd_debug_depth, new_depth) Process.put(:tdd_debug_depth, new_depth)
new_depth new_depth
end end
@ -119,149 +141,195 @@ defmodule Tdd.Debug do
# --- Core Macro Logic --- # --- Core Macro Logic ---
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote 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] import Kernel, except: [def: 2, def: 1, defp: 2, defp: 1]
require Tdd.Debug require Tdd.Debug
# Import Tdd.Debug's def/defp macros and other public functions/macros
import Tdd.Debug import Tdd.Debug
end end
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 @doc false
defmacro do_instrument(type, call, clauses, env) do defmacro do_instrument(type, call, clauses, _env) do
# CORRECTED: Decompose the function call AST
{function_name, _meta_call, original_args_ast_nodes} = call {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 || [] original_args_ast_nodes = original_args_ast_nodes || []
# CORRECTED: Generate argument variable ASTs for runtime access. # This part was problematic if original_args_ast_nodes contained `_`
# These vars will hold the actual values of arguments after pattern matching. # and was used directly to form a list of expressions.
arg_vars_for_runtime_access = original_args_ast_nodes 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 = actual_code_ast =
case clauses do case clauses do
[do: block_content] -> block_content # [do: block_content | _] -> block_content
kw when is_list(kw) -> Keyword.get(kw, :do) kw when is_list(kw) -> Keyword.get(kw, :do)
# `do: :atom` or `do: variable`
_ -> clauses _ -> clauses
end end
# Keep original line numbers for stacktraces from user code
traced_body = traced_body =
quote location: :keep do quote location: :keep do
if Tdd.Debug.is_pid_traced?(self()) do if is_pid_traced?(self()) do
current_print_depth = Tdd.Debug.increment_depth() current_print_depth = increment_depth()
indent = String.duplicate(" ", current_print_depth - 1) indent = String.duplicate(" ", current_print_depth - 1)
# CORRECTED: Use the generated vars for runtime access to get actual argument values. runtime_arg_values = [unquote_splicing(arg_vars_for_runtime_access)]
# We unquote_splicing them into a list literal. args_string = format_args_list(runtime_arg_values)
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.
caller_module_name = Module.split(__MODULE__) |> Enum.join(".") caller_module_name = Module.split(__MODULE__) |> Enum.join(".")
IO.puts( 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 try do
# Execute original function body
result = unquote(actual_code_ast) result = unquote(actual_code_ast)
_ = Tdd.Debug.decrement_depth() _ = decrement_depth()
IO.puts( 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 result
rescue rescue
exception_class -> exception_class ->
_ = Tdd.Debug.decrement_depth() _ = decrement_depth()
error_instance = __catch__(exception_class) error_instance = exception_class
stacktrace = __STACKTRACE__ stacktrace = __STACKTRACE__
IO.puts( 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 reraise error_instance, stacktrace
end end
else else
# Tracing not enabled, execute original code
unquote(actual_code_ast) unquote(actual_code_ast)
end end
end end
# Construct the final definition (def or defp) using Kernel's versions
quote do quote do
Kernel.unquote(type)(unquote(call), do: unquote(traced_body)) Kernel.unquote(type)(unquote(call), do: unquote(traced_body))
end end
end end
# Define the overriding def/defp macros directly in the user's module.
# These will take precedence over Kernel.def/defp.
@doc false @doc false
defmacro def(call, clauses \\ nil) do defmacro def(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
generate_traced_function(:def, call, clauses) generate_traced_function(:def, call, clauses, __CALLER__)
end end
defmacro defp(call, clauses \\ nil) do @doc false
generate_traced_function(:defp, call, clauses) defmacro defp(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
end generate_traced_function(:defp, call, clauses, __CALLER__)
end
def generate_traced_function(type, call, clauses) do
{function_name, _meta_call, original_args_ast_nodes} = call
args = original_args_ast_nodes || [] # Capture __CALLER__ to ensure hygiene for variables generated by the macro
# if needed, though direct AST manipulation often bypasses some hygiene issues.
block_ast = defp generate_traced_function(type, call_ast, clauses, _caller_env) do
case clauses do {function_name_ast, _meta_call, original_args_ast_nodes} = call_ast
[do: block_content] -> block_content args_patterns_ast = original_args_ast_nodes || []
kw when is_list(kw) -> Keyword.get(kw, :do)
_ -> clauses original_body_ast =
end if Keyword.keyword?(clauses) do
Keyword.get(clauses, :do, clauses)
quote location: :keep do else
Kernel.unquote(type)(unquote(call)) do clauses
if Tdd.Debug.is_pid_traced?(self()) do end || quote(do: nil) # Default to an empty body if none provided
current_print_depth = Tdd.Debug.increment_depth()
indent = String.duplicate(" ", current_print_depth - 1) # Transform argument patterns into expressions suitable for logging at runtime.
args_string = Tdd.Debug.format_args_list([unquote_splicing(args)]) logging_expressions_ast_list =
caller_module_name = Module.split(__MODULE__) |> Enum.join(".") Enum.map(args_patterns_ast, fn arg_pattern_ast ->
case arg_pattern_ast do
IO.puts("#{indent}CALL: #{caller_module_name}.#{unquote(function_name)}(#{args_string})") {:=, _meta_assign, [var_ast, _sub_pattern_ast]} ->
var_ast
try do _ ->
result = unquote(block_ast) Macro.postwalk(arg_pattern_ast, fn
_ = Tdd.Debug.decrement_depth() current_node_ast ->
case current_node_ast do
IO.puts("#{indent}RETURN from #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(result)}") # Match any underscore, regardless of context if it's a bare underscore node
result {:_, _, context} when is_atom(context) -> # context is often Elixir or nil
rescue quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end
exception_class -> _ ->
_ = Tdd.Debug.decrement_depth() current_node_ast
error_instance = __catch__(exception_class) end
stacktrace = __STACKTRACE__ end)
end
IO.puts("#{indent}ERROR in #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(error_instance)}") end)
reraise error_instance, stacktrace
end # This is the AST for the code that will become the *actual body* of the traced function.
else traced_body_inner_ast =
unquote(block_ast) quote do
end if is_pid_traced?(self()) do
end 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
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) --- # --- 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." @doc "Prints a formatted representation of a TDD graph starting from an ID."
def print(id) do def print(id) do
# Assuming Tdd.Store is defined elsewhere and aliased if needed alias Tdd.Store # Assuming Tdd.Store is available
# Or ensure it's globally available if defined in another file
alias Tdd.Store
IO.puts("--- TDD Graph (ID: #{id}) ---") IO.puts("--- TDD Graph (ID: #{id}) ---")
do_print(id, 0, MapSet.new()) do_print(id, 0, MapSet.new())
IO.puts("------------------------") IO.puts("------------------------")
@ -275,14 +343,12 @@ end
:ok :ok
else else
new_visited = MapSet.put(visited, id) 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 case Tdd.Store.get_node(id) do
{:ok, :true_terminal} -> {:ok, :true_terminal} ->
IO.puts("#{prefix}ID #{id} -> TRUE") IO.puts("#{prefix}ID #{id} -> TRUE")
{:ok, :false_terminal} -> {:ok, :false_terminal} ->
IO.puts("#{prefix}ID #{id} -> FALSE") IO.puts("#{prefix}ID #{id} -> FALSE")
{:ok, {var, y, n, d}} -> {:ok, {var, y, n, d}} ->
IO.puts("#{prefix}ID #{id}: IF #{inspect(var)}") IO.puts("#{prefix}ID #{id}: IF #{inspect(var)}")
IO.puts("#{prefix} ├─ Yes:") IO.puts("#{prefix} ├─ Yes:")
@ -291,7 +357,6 @@ end
do_print(n, indent_level + 2, new_visited) do_print(n, indent_level + 2, new_visited)
IO.puts("#{prefix} └─ DC:") IO.puts("#{prefix} └─ DC:")
do_print(d, indent_level + 2, new_visited) do_print(d, indent_level + 2, new_visited)
{:error, reason} -> {:error, reason} ->
IO.puts("#{prefix}ID #{id}: ERROR - #{reason}") IO.puts("#{prefix}ID #{id}: ERROR - #{reason}")
end end
@ -1349,12 +1414,29 @@ defmodule Tdd.Algo do
@spec negate(non_neg_integer) :: non_neg_integer @spec negate(non_neg_integer) :: non_neg_integer
def negate(tdd_id) do def negate(tdd_id) do
cache_key = {:negate, tdd_id} cache_key = {:negate, tdd_id}
IO.inspect(tdd_id)
case Store.get_op_cache(cache_key) do case Store.get_op_cache(cache_key) do
{:ok, result_id} -> {:ok, result_id} ->
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 = result_id =
case Store.get_node(tdd_id) do case Store.get_node(tdd_id) do
{:ok, :true_terminal} -> {:ok, :true_terminal} ->
@ -3016,6 +3098,7 @@ defmodule CompilerAlgoTests do
# --- Section: Basic Subtyping --- # --- Section: Basic Subtyping ---
IO.puts("\n--- Section: Basic Subtyping ---") IO.puts("\n--- Section: Basic Subtyping ---")
Tdd.Debug.enable_tracing()
test_subtype(":foo <: atom", true, {:literal, :foo}, :atom) test_subtype(":foo <: atom", true, {:literal, :foo}, :atom)
test_subtype("atom <: :foo", false, :atom, {:literal, :foo}) test_subtype("atom <: :foo", false, :atom, {:literal, :foo})
test_subtype(":foo <: integer", false, {:literal, :foo}, :integer) test_subtype(":foo <: integer", false, {:literal, :foo}, :integer)