wip debug
This commit is contained in:
parent
f1243084c7
commit
a78fe0541a
10
debug.md
10
debug.md
@ -2,7 +2,9 @@ i have a compiler Im writing in elixir. I need to trace execution of logic to p
|
|||||||
I have many tests I want to debug individually but I can run only the full test suite.
|
I have many tests I want to debug individually but I can run only the full test suite.
|
||||||
I want to create a couple of macros/functions that'd enable me to debug my code.
|
I want to create a couple of macros/functions that'd enable me to debug my code.
|
||||||
the scenario I imagine:
|
the scenario I imagine:
|
||||||
before the test I want to debug I write `Tdd.Debug.enable`
|
before the test I want to debug I write `Tdd.Debug.enable` and then :
|
||||||
And after the test line I add `Tdd.Debug.print_and_disable`.
|
as the code is being executed, it prints out a tree of called functions, their arguments and return values, effectively building a stacktrace as its being executed.
|
||||||
The last line prints a tree of called functions, their arguments and return values.
|
first lets design how we'd print this information and if/what should we store in memory to be able to render it in a shape sufficient for good debugging.
|
||||||
We can modify compiler functions.
|
Then lets design an elixir module that compiler modules would 'use'
|
||||||
|
e.g. a Tdd.Debug module that'd have and override for elixir `def`.
|
||||||
|
|
||||||
|
|||||||
385
new.exs
385
new.exs
@ -1,9 +1,267 @@
|
|||||||
defmodule Tdd.Debug do
|
defmodule Tdd.Debug do
|
||||||
@moduledoc "Helpers for debugging TDD structures."
|
@moduledoc "Helpers for debugging TDD structures and tracing function calls."
|
||||||
alias Tdd.Store
|
# alias Tdd.Store # Keep if used by your print functions
|
||||||
|
|
||||||
|
@agent_name Tdd.Debug.StateAgent
|
||||||
|
|
||||||
|
# --- Agent State Management ---
|
||||||
|
def init do
|
||||||
|
case Process.whereis(@agent_name) do
|
||||||
|
nil -> Agent.start_link(fn -> MapSet.new() end, name: @agent_name)
|
||||||
|
_pid -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_traced_pid(pid) when is_pid(pid) do
|
||||||
|
init()
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp is_pid_traced?(pid) when is_pid(pid) do
|
||||||
|
case Agent.whereis(@agent_name) do
|
||||||
|
nil ->
|
||||||
|
false
|
||||||
|
|
||||||
|
agent_pid ->
|
||||||
|
try do
|
||||||
|
Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity)
|
||||||
|
rescue
|
||||||
|
_e in [Exit, ArgumentError] -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Formatting Helpers ---
|
||||||
|
@arg_length_limit 80
|
||||||
|
@total_args_length_limit 200
|
||||||
|
|
||||||
|
defp 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
|
||||||
|
|
||||||
|
defp format_arg_value(arg) do
|
||||||
|
inspected = inspect(arg, limit: :infinity, pretty: false, structs: true)
|
||||||
|
|
||||||
|
if String.length(inspected) > @arg_length_limit do
|
||||||
|
String.slice(inspected, 0, @arg_length_limit - 3) <> "..."
|
||||||
|
else
|
||||||
|
inspected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Tracing Control ---
|
||||||
|
def enable_tracing do
|
||||||
|
init()
|
||||||
|
pid_to_trace = self()
|
||||||
|
add_traced_pid(pid_to_trace)
|
||||||
|
Process.flag(:trap_exit, true)
|
||||||
|
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)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_tracing() do
|
||||||
|
# Ensure agent is available for removal
|
||||||
|
init()
|
||||||
|
remove_traced_pid(self())
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(fun) when is_function(fun, 0) do
|
||||||
|
enable_tracing()
|
||||||
|
|
||||||
|
try do
|
||||||
|
fun.()
|
||||||
|
after
|
||||||
|
disable_tracing()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Process Dictionary for Depth ---
|
||||||
|
defp get_depth do
|
||||||
|
Process.get(:tdd_debug_depth, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp 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)
|
||||||
|
Process.put(:tdd_debug_depth, new_depth)
|
||||||
|
new_depth
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is an internal macro helper, not intended for direct user call
|
||||||
|
@doc false
|
||||||
|
defmacro do_instrument(type, call, clauses, env) do
|
||||||
|
# CORRECTED: Decompose the function call AST
|
||||||
|
{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
|
||||||
|
|
||||||
|
actual_code_ast =
|
||||||
|
case clauses do
|
||||||
|
[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()
|
||||||
|
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.
|
||||||
|
caller_module_name = Module.split(__MODULE__) |> Enum.join(".")
|
||||||
|
|
||||||
|
IO.puts(
|
||||||
|
"#{indent}CALL: #{caller_module_name}.#{unquote(function_name)}(#{args_string})"
|
||||||
|
)
|
||||||
|
|
||||||
|
try do
|
||||||
|
# Execute original function body
|
||||||
|
result = unquote(actual_code_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
|
||||||
|
# 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
|
||||||
|
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."
|
@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
|
||||||
|
# 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("------------------------")
|
||||||
@ -17,8 +275,8 @@ defmodule Tdd.Debug do
|
|||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
new_visited = MapSet.put(visited, id)
|
new_visited = MapSet.put(visited, id)
|
||||||
|
# Assuming Tdd.Store.get_node/1 is available
|
||||||
case 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")
|
||||||
|
|
||||||
@ -163,8 +421,7 @@ defmodule Tdd.TypeSpec do
|
|||||||
normalized = normalize(member)
|
normalized = normalize(member)
|
||||||
|
|
||||||
case normalized,
|
case normalized,
|
||||||
do:
|
do: (
|
||||||
(
|
|
||||||
{:union, sub_members} -> sub_members
|
{:union, sub_members} -> sub_members
|
||||||
_ -> [normalized]
|
_ -> [normalized]
|
||||||
)
|
)
|
||||||
@ -518,7 +775,7 @@ defmodule Tdd.Store do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a unique, temporary placeholder node for a recursive spec.
|
Creates a unique, temporary placeholder node for a recursive spec.
|
||||||
Returns the ID of this placeholder.
|
Returns the ID of this placeholder.
|
||||||
"""
|
"""
|
||||||
@ -971,8 +1228,7 @@ defmodule Tdd.Consistency.Engine do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
case result,
|
case result,
|
||||||
do:
|
do: (
|
||||||
(
|
|
||||||
:invalid -> :error
|
:invalid -> :error
|
||||||
_ -> :ok
|
_ -> :ok
|
||||||
)
|
)
|
||||||
@ -1219,13 +1475,18 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@doc """
|
|
||||||
|
@doc """
|
||||||
Recursively traverses a TDD graph from `root_id`, creating a new graph
|
Recursively traverses a TDD graph from `root_id`, creating a new graph
|
||||||
where all references to `from_id` are replaced with `to_id`.
|
where all references to `from_id` are replaced with `to_id`.
|
||||||
|
|
||||||
This is a pure function used to "tie the knot" in recursive type compilation.
|
This is a pure function used to "tie the knot" in recursive type compilation.
|
||||||
"""
|
"""
|
||||||
@spec substitute(root_id :: non_neg_integer(), from_id :: non_neg_integer(), to_id :: non_neg_integer()) ::
|
@spec substitute(
|
||||||
|
root_id :: non_neg_integer(),
|
||||||
|
from_id :: non_neg_integer(),
|
||||||
|
to_id :: non_neg_integer()
|
||||||
|
) ::
|
||||||
non_neg_integer()
|
non_neg_integer()
|
||||||
def substitute(root_id, from_id, to_id) do
|
def substitute(root_id, from_id, to_id) do
|
||||||
# Handle the trivial case where the root is the node to be replaced.
|
# Handle the trivial case where the root is the node to be replaced.
|
||||||
@ -1244,8 +1505,11 @@ defmodule Tdd.Algo do
|
|||||||
result_id =
|
result_id =
|
||||||
case Store.get_node(root_id) do
|
case Store.get_node(root_id) do
|
||||||
# Terminal nodes are unaffected unless they are the target of substitution.
|
# Terminal nodes are unaffected unless they are the target of substitution.
|
||||||
{:ok, :true_terminal} -> Store.true_node_id()
|
{:ok, :true_terminal} ->
|
||||||
{:ok, :false_terminal} -> Store.false_node_id()
|
Store.true_node_id()
|
||||||
|
|
||||||
|
{:ok, :false_terminal} ->
|
||||||
|
Store.false_node_id()
|
||||||
|
|
||||||
# For internal nodes, recursively substitute in all children.
|
# For internal nodes, recursively substitute in all children.
|
||||||
{:ok, {var, y, n, d}} ->
|
{:ok, {var, y, n, d}} ->
|
||||||
@ -1263,6 +1527,7 @@ defmodule Tdd.Algo do
|
|||||||
result_id
|
result_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# defp do_simplify(tdd_id, assumptions) do
|
# defp do_simplify(tdd_id, assumptions) do
|
||||||
# IO.inspect([tdd_id, assumptions], label: "do_simplify(tdd_id, assumptions)")
|
# IO.inspect([tdd_id, assumptions], label: "do_simplify(tdd_id, assumptions)")
|
||||||
# # First, check if the current assumption set is already a contradiction.
|
# # First, check if the current assumption set is already a contradiction.
|
||||||
@ -1452,11 +1717,14 @@ defmodule Tdd.TypeReconstructor do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defmodule Tdd.Compiler do
|
defmodule Tdd.Compiler do
|
||||||
|
use Tdd.Debug
|
||||||
|
|
||||||
@moduledoc "Compiles a `TypeSpec` into a canonical TDD ID."
|
@moduledoc "Compiles a `TypeSpec` into a canonical TDD ID."
|
||||||
alias Tdd.TypeSpec
|
alias Tdd.TypeSpec
|
||||||
alias Tdd.Variable
|
alias Tdd.Variable
|
||||||
alias Tdd.Store
|
alias Tdd.Store
|
||||||
alias Tdd.Algo
|
alias Tdd.Algo
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
The main public entry point. Takes a spec and returns its TDD ID.
|
The main public entry point. Takes a spec and returns its TDD ID.
|
||||||
This now delegates to a private function with a context for recursion.
|
This now delegates to a private function with a context for recursion.
|
||||||
@ -1466,9 +1734,10 @@ defmodule Tdd.Compiler do
|
|||||||
# Start with an empty context map.
|
# Start with an empty context map.
|
||||||
spec_to_id(spec, %{})
|
spec_to_id(spec, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is the new core compilation function with a context map.
|
# This is the new core compilation function with a context map.
|
||||||
# The context tracks `{spec => placeholder_id}` for in-progress compilations.
|
# The context tracks `{spec => placeholder_id}` for in-progress compilations.
|
||||||
defp spec_to_id(spec, context) do
|
defp spec_to_id(spec, context) do
|
||||||
normalized_spec = TypeSpec.normalize(spec)
|
normalized_spec = TypeSpec.normalize(spec)
|
||||||
cache_key = {:spec_to_id, normalized_spec}
|
cache_key = {:spec_to_id, normalized_spec}
|
||||||
|
|
||||||
@ -1494,6 +1763,7 @@ defp spec_to_id(spec, context) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp is_recursive_spec?({:list_of, _}), do: true
|
defp is_recursive_spec?({:list_of, _}), do: true
|
||||||
defp is_recursive_spec?(_), do: false
|
defp is_recursive_spec?(_), do: false
|
||||||
|
|
||||||
@ -1530,21 +1800,41 @@ defp spec_to_id(spec, context) do
|
|||||||
Store.put_op_cache({:spec_to_id, spec}, simplified_id)
|
Store.put_op_cache({:spec_to_id, spec}, simplified_id)
|
||||||
simplified_id
|
simplified_id
|
||||||
end
|
end
|
||||||
|
|
||||||
# This helper does the raw, structural compilation.
|
# This helper does the raw, structural compilation.
|
||||||
# It now takes and passes down the context.
|
# It now takes and passes down the context.
|
||||||
defp do_spec_to_id(spec, context) do
|
defp do_spec_to_id(spec, context) do
|
||||||
case spec do
|
case spec do
|
||||||
# Pass context on all recursive calls to spec_to_id/2
|
# Pass context on all recursive calls to spec_to_id/2
|
||||||
:any -> Store.true_node_id()
|
:any ->
|
||||||
:none -> Store.false_node_id()
|
Store.true_node_id()
|
||||||
:atom -> create_base_type_tdd(Variable.v_is_atom())
|
|
||||||
:integer -> create_base_type_tdd(Variable.v_is_integer())
|
:none ->
|
||||||
:list -> create_base_type_tdd(Variable.v_is_list())
|
Store.false_node_id()
|
||||||
:tuple -> create_base_type_tdd(Variable.v_is_tuple())
|
|
||||||
{:literal, val} when is_atom(val) -> compile_value_equality(:atom, Variable.v_atom_eq(val), context)
|
:atom ->
|
||||||
{:literal, val} when is_integer(val) -> compile_value_equality(:integer, Variable.v_int_eq(val), context)
|
create_base_type_tdd(Variable.v_is_atom())
|
||||||
{:literal, []} -> compile_value_equality(:list, Variable.v_list_is_empty(), context)
|
|
||||||
{:integer_range, min, max} -> compile_integer_range(min, max, context)
|
:integer ->
|
||||||
|
create_base_type_tdd(Variable.v_is_integer())
|
||||||
|
|
||||||
|
:list ->
|
||||||
|
create_base_type_tdd(Variable.v_is_list())
|
||||||
|
|
||||||
|
:tuple ->
|
||||||
|
create_base_type_tdd(Variable.v_is_tuple())
|
||||||
|
|
||||||
|
{:literal, val} when is_atom(val) ->
|
||||||
|
compile_value_equality(:atom, Variable.v_atom_eq(val), context)
|
||||||
|
|
||||||
|
{:literal, val} when is_integer(val) ->
|
||||||
|
compile_value_equality(:integer, Variable.v_int_eq(val), context)
|
||||||
|
|
||||||
|
{:literal, []} ->
|
||||||
|
compile_value_equality(:list, Variable.v_list_is_empty(), context)
|
||||||
|
|
||||||
|
{:integer_range, min, max} ->
|
||||||
|
compile_integer_range(min, max, context)
|
||||||
|
|
||||||
{:union, specs} ->
|
{:union, specs} ->
|
||||||
Enum.map(specs, &spec_to_id(&1, context))
|
Enum.map(specs, &spec_to_id(&1, context))
|
||||||
@ -1586,7 +1876,14 @@ defp spec_to_id(spec, context) do
|
|||||||
|
|
||||||
# --- Private Helpers ---
|
# --- Private Helpers ---
|
||||||
|
|
||||||
defp create_base_type_tdd(var), do: Store.find_or_create_node(var, Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
|
defp create_base_type_tdd(var),
|
||||||
|
do:
|
||||||
|
Store.find_or_create_node(
|
||||||
|
var,
|
||||||
|
Store.true_node_id(),
|
||||||
|
Store.false_node_id(),
|
||||||
|
Store.false_node_id()
|
||||||
|
)
|
||||||
|
|
||||||
defp compile_value_equality(base_type_spec, value_var, context) do
|
defp compile_value_equality(base_type_spec, value_var, context) do
|
||||||
eq_node = create_base_type_tdd(value_var)
|
eq_node = create_base_type_tdd(value_var)
|
||||||
@ -1608,15 +1905,20 @@ defp spec_to_id(spec, context) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compile_cons_from_ids(h_id, t_id, context) do # Pass context
|
# Pass context
|
||||||
|
defp compile_cons_from_ids(h_id, t_id, context) do
|
||||||
# Build `list & !is_empty` manually and safely.
|
# Build `list & !is_empty` manually and safely.
|
||||||
id_list = create_base_type_tdd(Variable.v_is_list())
|
id_list = create_base_type_tdd(Variable.v_is_list())
|
||||||
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
||||||
id_not_is_empty = Algo.negate(id_is_empty)
|
id_not_is_empty = Algo.negate(id_is_empty)
|
||||||
non_empty_list_id = Algo.apply(:intersect, &op_intersect_terminals/2, id_list, id_not_is_empty)
|
|
||||||
|
|
||||||
head_checker = sub_problem(&Variable.v_list_head_pred/1, h_id, context) # Pass context
|
non_empty_list_id =
|
||||||
tail_checker = sub_problem(&Variable.v_list_tail_pred/1, t_id, context) # Pass context
|
Algo.apply(:intersect, &op_intersect_terminals/2, id_list, id_not_is_empty)
|
||||||
|
|
||||||
|
# Pass context
|
||||||
|
head_checker = sub_problem(&Variable.v_list_head_pred/1, h_id, context)
|
||||||
|
# Pass context
|
||||||
|
tail_checker = sub_problem(&Variable.v_list_tail_pred/1, t_id, context)
|
||||||
|
|
||||||
[non_empty_list_id, head_checker, tail_checker]
|
[non_empty_list_id, head_checker, tail_checker]
|
||||||
|> Enum.reduce(Store.true_node_id(), fn id, acc ->
|
|> Enum.reduce(Store.true_node_id(), fn id, acc ->
|
||||||
@ -1624,7 +1926,7 @@ defp compile_cons_from_ids(h_id, t_id, context) do # Pass context
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compile_tuple(elements, context) do
|
defp compile_tuple(elements, context) do
|
||||||
size = length(elements)
|
size = length(elements)
|
||||||
base_id = spec_to_id(:tuple, context)
|
base_id = spec_to_id(:tuple, context)
|
||||||
size_tdd = create_base_type_tdd(Variable.v_tuple_size_eq(size))
|
size_tdd = create_base_type_tdd(Variable.v_tuple_size_eq(size))
|
||||||
@ -1635,12 +1937,13 @@ defp compile_tuple(elements, context) do
|
|||||||
|> Enum.reduce(initial_id, fn {elem_spec, index}, acc_id ->
|
|> Enum.reduce(initial_id, fn {elem_spec, index}, acc_id ->
|
||||||
elem_id = spec_to_id(elem_spec)
|
elem_id = spec_to_id(elem_spec)
|
||||||
elem_key_constructor = &Variable.v_tuple_elem_pred(index, &1)
|
elem_key_constructor = &Variable.v_tuple_elem_pred(index, &1)
|
||||||
elem_checker = sub_problem(elem_key_constructor, elem_id, context) # Pass context
|
# Pass context
|
||||||
|
elem_checker = sub_problem(elem_key_constructor, elem_id, context)
|
||||||
Algo.apply(:intersect, &op_intersect_terminals/2, acc_id, elem_checker)
|
Algo.apply(:intersect, &op_intersect_terminals/2, acc_id, elem_checker)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_sub_problem(sub_key_constructor, tdd_id, context) do
|
defp do_sub_problem(sub_key_constructor, tdd_id, context) do
|
||||||
# The `if` block is now a standard multi-clause `cond` or `case` at the top.
|
# The `if` block is now a standard multi-clause `cond` or `case` at the top.
|
||||||
# Let's use a `cond` to make the guard explicit.
|
# Let's use a `cond` to make the guard explicit.
|
||||||
cond do
|
cond do
|
||||||
@ -1691,7 +1994,7 @@ defp do_sub_problem(sub_key_constructor, tdd_id, context) do
|
|||||||
raise "sub_problem received an unknown tdd_id: #{tdd_id}"
|
raise "sub_problem received an unknown tdd_id: #{tdd_id}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp sub_problem(sub_key_constructor, tdd_id, context) do
|
defp sub_problem(sub_key_constructor, tdd_id, context) do
|
||||||
cache_key = {:sub_problem, sub_key_constructor, tdd_id}
|
cache_key = {:sub_problem, sub_key_constructor, tdd_id}
|
||||||
@ -1732,19 +2035,21 @@ end
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
# THIS IS THE FINAL, CORRECTED LOOP
|
# THIS IS THE FINAL, CORRECTED LOOP
|
||||||
defp loop_until_stable(prev_id, step_function, iteration \\ 0) do
|
defp loop_until_stable(prev_id, step_function, iteration \\ 0) do
|
||||||
IO.puts("\n--- Fixed-Point Iteration: #{iteration} ---")
|
IO.puts("\n--- Fixed-Point Iteration: #{iteration} ---")
|
||||||
IO.inspect(prev_id, label: "prev_id")
|
IO.inspect(prev_id, label: "prev_id")
|
||||||
# Tdd.Debug.print(prev_id) # Optional: uncomment for full graph
|
# Tdd.Debug.print(prev_id) # Optional: uncomment for full graph
|
||||||
|
|
||||||
raw_next_id = step_function.(prev_id)
|
raw_next_id = step_function.(prev_id)
|
||||||
IO.inspect(raw_next_id, label: "raw_next_id (after step_function)")
|
IO.inspect(raw_next_id, label: "raw_next_id (after step_function)")
|
||||||
Tdd.Debug.print(raw_next_id) # Let's see the raw graph
|
# Let's see the raw graph
|
||||||
|
Tdd.Debug.print(raw_next_id)
|
||||||
|
|
||||||
# Crucially, simplify after each step for canonical comparison.
|
# Crucially, simplify after each step for canonical comparison.
|
||||||
next_id = Algo.simplify(raw_next_id)
|
next_id = Algo.simplify(raw_next_id)
|
||||||
IO.inspect(next_id, label: "next_id (after simplify)")
|
IO.inspect(next_id, label: "next_id (after simplify)")
|
||||||
Tdd.Debug.print(next_id) # Let's see the simplified graph
|
# Let's see the simplified graph
|
||||||
|
Tdd.Debug.print(next_id)
|
||||||
|
|
||||||
if next_id == prev_id do
|
if next_id == prev_id do
|
||||||
IO.puts("--- Fixed-Point Reached! ---")
|
IO.puts("--- Fixed-Point Reached! ---")
|
||||||
@ -1752,12 +2057,13 @@ defp loop_until_stable(prev_id, step_function, iteration \\ 0) do
|
|||||||
else
|
else
|
||||||
# Add a safety break for debugging
|
# Add a safety break for debugging
|
||||||
if iteration > 2 do
|
if iteration > 2 do
|
||||||
IO.inspect Process.info(self(), :current_stacktrace)
|
IO.inspect(Process.info(self(), :current_stacktrace))
|
||||||
raise "Fixed-point iteration did not converge after 2 steps. Halting."
|
raise "Fixed-point iteration did not converge after 2 steps. Halting."
|
||||||
end
|
end
|
||||||
|
|
||||||
loop_until_stable(next_id, step_function, iteration + 1)
|
loop_until_stable(next_id, step_function, iteration + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# --- Private Functions for Terminal Logic ---
|
# --- Private Functions for Terminal Logic ---
|
||||||
defp op_union_terminals(:true_terminal, _), do: :true_terminal
|
defp op_union_terminals(:true_terminal, _), do: :true_terminal
|
||||||
@ -3052,6 +3358,9 @@ defmodule TddCompilerRecursiveTests do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Ensure the tracing state manager is started
|
||||||
|
Tdd.Debug.init()
|
||||||
Process.sleep(100)
|
Process.sleep(100)
|
||||||
# To run this new test, add the following to your main test runner script:
|
# To run this new test, add the following to your main test runner script:
|
||||||
# TddCompilerRecursiveTests.run()
|
# TddCompilerRecursiveTests.run()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user