checkpoint, still fucked debugging
This commit is contained in:
parent
a78fe0541a
commit
8d8b3607fc
289
new.exs
289
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 ->
|
||||
# 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)
|
||||
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
|
||||
defmacro def(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
|
||||
generate_traced_function(:def, call, clauses, __CALLER__)
|
||||
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(".")
|
||||
@doc false
|
||||
defmacro defp(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
|
||||
generate_traced_function(:defp, call, clauses, __CALLER__)
|
||||
end
|
||||
|
||||
IO.puts("#{indent}CALL: #{caller_module_name}.#{unquote(function_name)}(#{args_string})")
|
||||
|
||||
# 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
|
||||
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
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user