checkpoint, still fucked debugging

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

275
new.exs
View File

@ -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 ->
# 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)
{:DOWN, ^ref, :process, ^pid_to_trace, _reason} ->
remove_traced_pid(pid_to_trace)
after
3_600_000 -> remove_traced_pid(pid_to_trace)
# 1 hour timeout as a fallback
3_600_000 ->
remove_traced_pid(pid_to_trace)
end
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)
defmacro def(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
generate_traced_function(:def, call, clauses, __CALLER__)
end
defmacro defp(call, clauses \\ nil) do
generate_traced_function(:defp, call, clauses)
@doc false
defmacro defp(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
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 || []
block_ast =
case clauses do
[do: block_content] -> block_content
kw when is_list(kw) -> Keyword.get(kw, :do)
_ -> clauses
# 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)
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()
# 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)
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})")
# 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
result = unquote(block_ast)
# 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}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(result)}")
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 = __catch__(exception_class)
error_instance = exception_class
stacktrace = __STACKTRACE__
IO.puts("#{indent}ERROR in #{caller_module_name}.#{unquote(function_name)}: #{Tdd.Debug.format_arg_value(error_instance)}")
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
unquote(block_ast)
# 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
# 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 ->
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)