fix test
spec = {:mu, :X, {:union, [{:type_var, :X}, :integer]}}
assert_equivalent_specs(spec, :integer)
This commit is contained in:
parent
9c6b3998c2
commit
9a89757bcd
119
lib/til.ex
119
lib/til.ex
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user