spec = {:mu, :X, {:union, [{:type_var, :X}, :integer]}}
assert_equivalent_specs(spec, :integer)
This commit is contained in:
Kacper Marzecki 2025-07-15 00:59:22 +02:00
parent 9c6b3998c2
commit 9a89757bcd
2 changed files with 156 additions and 92 deletions

View File

@ -1373,37 +1373,80 @@ defmodule Tdd.Algo do
# This function is correct and does not need to be changed. # This function is correct and does not need to be changed.
defp do_apply(op_name, op_lambda, u1_id, u2_id) do defp do_apply(op_name, op_lambda, u1_id, u2_id) do
if u1_id == u2_id do
# If both nodes are the same, the result of the operation is idempotent.
u1_id
else
# The core logic for distinct nodes.
with {:ok, u1_details} <- Store.get_node(u1_id), with {:ok, u1_details} <- Store.get_node(u1_id),
{:ok, u2_details} <- Store.get_node(u2_id) do {:ok, u2_details} <- Store.get_node(u2_id) do
cond do cond do
# --- Start of Logic for Placeholders ---
# Case 1: u2 is a placeholder. We must handle this based on u1.
match?({:placeholder, _}, u2_details) ->
case u1_details do
# op(true, placeholder) depends on the operation.
:true_terminal ->
case op_name do
:sum -> Store.true_node_id()
# intersect(any, p) = p
:intersect -> u2_id
end
# op(false, placeholder) also depends on the operation.
:false_terminal ->
case op_name do
# sum(none, p) = p
:sum -> u2_id
:intersect -> Store.false_node_id()
end
# op(node, placeholder) -> recursively apply op to children and placeholder.
{var1, y1, n1, d1} ->
Store.find_or_create_node(
var1,
apply(op_name, op_lambda, y1, u2_id),
apply(op_name, op_lambda, n1, u2_id),
apply(op_name, op_lambda, d1, u2_id)
)
end
# Case 2: Symmetric case where u1 is the placeholder.
match?({:placeholder, _}, u1_details) ->
case u2_details do
:true_terminal ->
case op_name do
:sum -> Store.true_node_id()
# intersect(p, any) = p
:intersect -> u1_id
end
:false_terminal ->
case op_name do
# sum(p, none) = p
:sum -> u1_id
:intersect -> Store.false_node_id()
end
{var2, y2, n2, d2} ->
Store.find_or_create_node(
var2,
apply(op_name, op_lambda, u1_id, y2),
apply(op_name, op_lambda, u1_id, n2),
apply(op_name, op_lambda, u1_id, d2)
)
end
# --- End of Placeholder Logic ---
# The rest of the function is the standard apply algorithm.
(u1_details == :true_terminal or u1_details == :false_terminal) and (u1_details == :true_terminal or u1_details == :false_terminal) and
(u2_details == :true_terminal or u2_details == :false_terminal) -> (u2_details == :true_terminal or u2_details == :false_terminal) ->
if op_lambda.(u1_details, u2_details) == :true_terminal, if op_lambda.(u1_details, u2_details) == :true_terminal,
do: Store.true_node_id(), do: Store.true_node_id(),
else: Store.false_node_id() else: Store.false_node_id()
# TODO: Add cases to handle placeholder nodes co-inductively.
# Treat the placeholder as `any`.
match?({:ok, {:placeholder, _}}, {:ok, u1_details}) ->
# op(placeholder, u2) -> op(any, u2)
op_lambda.(:true_terminal, u2_details)
|> case do
:true_terminal -> Store.true_node_id()
:false_terminal -> Store.false_node_id()
# This happens if op(any, u2) = u2 (e.g., intersection)
_other -> u2_id
end
match?({:ok, {:placeholder, _}}, {:ok, u2_details}) ->
# op(u1, placeholder) -> op(u1, any)
op_lambda.(u1_details, :true_terminal)
|> case do
:true_terminal -> Store.true_node_id()
:false_terminal -> Store.false_node_id()
# This happens if op(u1, any) = u1 (e.g., intersection)
_other -> u1_id
end
u1_details == :true_terminal or u1_details == :false_terminal -> u1_details == :true_terminal or u1_details == :false_terminal ->
{var2, y2, n2, d2} = u2_details {var2, y2, n2, d2} = u2_details
@ -1457,6 +1500,7 @@ defmodule Tdd.Algo do
end end
end end
end end
end
# --- Unary Operation: Negation --- # --- Unary Operation: Negation ---
@spec negate(non_neg_integer) :: non_neg_integer @spec negate(non_neg_integer) :: non_neg_integer
@ -1539,12 +1583,13 @@ defmodule Tdd.Algo do
end end
defp do_simplify(tdd_id, sorted_assumptions, context) do defp do_simplify(tdd_id, sorted_assumptions, context) do
current_state = {tdd_id, sorted_assumptions} # The context now only tracks the node ID. If we see the same ID again,
# it's an unproductive loop, regardless of the path taken to get here.
if MapSet.member?(context, current_state) do if MapSet.member?(context, tdd_id) do
Store.true_node_id() Store.false_node_id()
else else
new_context = MapSet.put(context, current_state) # Add the current ID to the context for all subsequent recursive calls.
new_context = MapSet.put(context, tdd_id)
assumptions = Map.new(sorted_assumptions) assumptions = Map.new(sorted_assumptions)
if Engine.check(assumptions) == :contradiction do if Engine.check(assumptions) == :contradiction do
@ -1557,8 +1602,13 @@ defmodule Tdd.Algo do
{:ok, :false_terminal} -> {:ok, :false_terminal} ->
Store.false_node_id() Store.false_node_id()
{:ok, {:placeholder, _}} ->
# A placeholder should not exist in a fully formed graph being simplified.
# This indicates that the knot-tying in the compiler did not fully resolve
# the cycle. Encountering it means this path is unsatisfiable.
Store.false_node_id()
{:ok, {var, y, n, d}} -> {:ok, {var, y, n, d}} ->
# Dispatch to the handler for recursive variables.
case var do case var do
{5, :c_head, constraint_id, _} -> {5, :c_head, constraint_id, _} ->
handle_recursive_subproblem( handle_recursive_subproblem(
@ -1975,6 +2025,8 @@ defmodule Tdd.Compiler do
id_to_cache = id_to_cache =
case normalized_spec do case normalized_spec do
{:type_var, var_name} -> {:type_var, var_name} ->
Debug.log(Map.get(context, var_name), "{:type_var, var_name}")
Map.get(context, var_name) || Map.get(context, var_name) ||
raise "Tdd.Compiler: Unbound type variable: #{inspect(var_name)}" raise "Tdd.Compiler: Unbound type variable: #{inspect(var_name)}"
@ -1989,7 +2041,6 @@ defmodule Tdd.Compiler do
# "Tie the knot": substitute the placeholder with the ID of the body's root. # "Tie the knot": substitute the placeholder with the ID of the body's root.
# This correctly forms the cyclic TDD graph using the standard algorithm. # This correctly forms the cyclic TDD graph using the standard algorithm.
# The `substitute` function is designed for this and does not corrupt the store.
final_id = final_id =
Algo.substitute( Algo.substitute(
body_id_with_placeholder, body_id_with_placeholder,
@ -1998,10 +2049,12 @@ defmodule Tdd.Compiler do
) )
# The resulting graph may have new simplification opportunities after the knot is tied. # The resulting graph may have new simplification opportunities after the knot is tied.
# This call to simplify, with the corrected algorithm from Fix #1, is CRITICAL.
Algo.simplify(final_id) Algo.simplify(final_id)
other_spec -> other_spec ->
raw_id = do_structural_compile(other_spec, context) raw_id = do_structural_compile(other_spec, context)
# Debug.build_graph_map(raw_id, "other spec id, before simplify")
Algo.simplify(raw_id) Algo.simplify(raw_id)
end end
@ -2058,6 +2111,13 @@ defmodule Tdd.Compiler do
Algo.negate(compile_normalized_spec(sub_spec, context)) Algo.negate(compile_normalized_spec(sub_spec, context))
{:cons, head_spec, tail_spec} -> {:cons, head_spec, tail_spec} ->
# --- NEW GUARD CLAUSE ---
# If the head or tail of a cons is known to be impossible (:none),
# then the entire cons expression is impossible to construct.
if head_spec == :none or tail_spec == :none do
Store.false_node_id()
else
# --- EXISTING LOGIC (now safely in the 'else' block) ---
id_list = compile_normalized_spec(:list, context) id_list = compile_normalized_spec(:list, context)
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)
@ -2078,6 +2138,7 @@ defmodule Tdd.Compiler do
|> Enum.reduce(Store.true_node_id(), fn id, acc -> |> Enum.reduce(Store.true_node_id(), fn id, acc ->
Algo.apply(:intersect, &op_intersect_terminals/2, id, acc) Algo.apply(:intersect, &op_intersect_terminals/2, id, acc)
end) end)
end
{:tuple, elements_specs} -> {:tuple, elements_specs} ->
size = length(elements_specs) size = length(elements_specs)

View File

@ -32,8 +32,8 @@ defmodule TddSystemTest do
quote do quote do
id1 = Compiler.spec_to_id(unquote(spec1)) id1 = Compiler.spec_to_id(unquote(spec1))
id2 = Compiler.spec_to_id(unquote(spec2)) id2 = Compiler.spec_to_id(unquote(spec2))
Debug.build_graph_map(id1, "assert_equivalent_specs id1") # Debug.build_graph_map(id1, "assert_equivalent_specs id1")
Debug.build_graph_map(id2, "assert_equivalent_specs id2") # Debug.build_graph_map(id2, "assert_equivalent_specs id2")
Process.get(:tdd_node_by_id) |> IO.inspect(label: ":tdd_node_by_id") Process.get(:tdd_node_by_id) |> IO.inspect(label: ":tdd_node_by_id")
Process.get(:tdd_nodes) |> IO.inspect(label: ":tdd_nodes") Process.get(:tdd_nodes) |> IO.inspect(label: ":tdd_nodes")
IO.inspect("equvalent specs? #{id1} #{id2}") IO.inspect("equvalent specs? #{id1} #{id2}")
@ -533,6 +533,9 @@ defmodule TddSystemTest do
# list_of(integer & atom) is list_of(:none). # list_of(integer & atom) is list_of(:none).
# A list of :none can only be the empty list, as no element can ever exist. # A list of :none can only be the empty list, as no element can ever exist.
spec = {:list_of, {:intersect, [:integer, :atom]}} spec = {:list_of, {:intersect, [:integer, :atom]}}
Tdd.Store.init()
Debug.print_tdd_graph(Compiler.spec_to_id(spec))
Debug.print_tdd_graph(Compiler.spec_to_id({:literal, []}))
assert_equivalent_specs(spec, {:literal, []}) assert_equivalent_specs(spec, {:literal, []})
end end
end end