checkpoint, some test pass

This commit is contained in:
Kacper Marzecki 2025-07-12 13:08:07 +02:00
parent c51c35bfe4
commit 3ea8d80801

View File

@ -1435,10 +1435,12 @@ defmodule Tdd.Algo do
cache_key = {:negate, tdd_id} cache_key = {:negate, tdd_id}
case Store.get_op_cache(cache_key) do case Store.get_op_cache(cache_key) do
# If the result is already cached (or in progress), return it.
{:ok, result_id} -> {:ok, result_id} ->
result_id result_id
:not_found -> :not_found ->
# On cache miss, compute the negation.
result_id = result_id =
case Store.get_node(tdd_id) do case Store.get_node(tdd_id) do
{:ok, :true_terminal} -> {:ok, :true_terminal} ->
@ -1447,17 +1449,44 @@ defmodule Tdd.Algo do
{:ok, :false_terminal} -> {:ok, :false_terminal} ->
Store.true_node_id() Store.true_node_id()
# This is the main recursive case for an internal node.
{:ok, {var, y, n, d}} -> {:ok, {var, y, n, d}} ->
Store.find_or_create_node(var, negate(y), negate(n), negate(d)) # 1. Create a temporary placeholder and cache it immediately.
# This acts as a co-inductive "hook". Any recursive call to negate
# this same `tdd_id` will hit the cache and get this placeholder,
# breaking the infinite recursion.
placeholder = Store.create_placeholder({:negation_of, tdd_id})
Store.put_op_cache(cache_key, placeholder)
# TODO: Handle placeholder nodes encountered during co-inductive simplification. # 2. Recursively negate the children.
# A placeholder is treated as `any` for the check, so its negation is `none`. neg_y = negate(y)
neg_n = negate(n)
neg_d = negate(d)
# 3. Build the main body of the negated TDD. If there was a cycle,
# this structure will refer to the `placeholder` ID at its leaves.
body_id = Store.find_or_create_node(var, neg_y, neg_n, neg_d)
# 4. "Tie the knot": Replace any occurrences of the placeholder within the
# newly created graph with the ID of the graph's root (`body_id`).
# This correctly forms the cycle in the negated graph.
final_id = substitute(body_id, placeholder, body_id)
# 5. The canonical graph is now built. Update the cache with the
# final, correct ID before returning.
Store.put_op_cache(cache_key, final_id)
final_id
# This case indicates a logical error. A fully compiled TDD passed to `negate`
# should not be a placeholder itself. This new implementation avoids the
# previous incorrect handling.
{:ok, {:placeholder, _spec}} -> {:ok, {:placeholder, _spec}} ->
IO.inspect("RETURNING FALSE FOR NEGATE PLACEHOLDER") raise "Tdd.Algo.negate: Attempted to negate a raw placeholder node. This indicates a logic error in the calling algorithm."
Store.false_node_id()
{:error, reason} ->
raise "Tdd.Algo.negate: Failed to get node #{tdd_id}: #{reason}"
end end
Store.put_op_cache(cache_key, result_id)
result_id result_id
end end
end end
@ -1779,6 +1808,24 @@ defmodule Tdd.Algo do
# This is the coinductive context (a MapSet). # This is the coinductive context (a MapSet).
context context
) do ) do
# --- BEGIN IMPROVEMENT ---
# Co-inductive check for the constraint ID itself. If we are being asked to
# reason about a constraint that is a placeholder (i.e., part of a cycle
# currently being built), we must avoid operating on it directly. The correct
# co-inductive assumption is that the check succeeds, so we proceed down
# the 'yes' path without performing invalid logic on the placeholder.
case Store.get_node(constraint_id) do
{:ok, {:placeholder, _}} ->
{_var, y, _n, _d} = node_details
case algo_type do
:simplify -> do_simplify(y, sorted_assumptions, context)
:check_emptiness -> do_check_emptiness(y, sorted_assumptions, context)
end
# If the constraint_id is a valid, fully-formed node, proceed with the original logic.
_ ->
# --- END IMPROVEMENT ---
{var, y, n, d} = node_details {var, y, n, d} = node_details
assumptions = Map.new(sorted_assumptions) assumptions = Map.new(sorted_assumptions)
@ -1801,18 +1848,14 @@ defmodule Tdd.Algo do
constraint_for_this_assumption = Engine.unwrap_var(var) constraint_for_this_assumption = Engine.unwrap_var(var)
id_to_intersect = id_to_intersect =
if val, do: constraint_for_this_assumption, else: negate(constraint_for_this_assumption) if val,
do: constraint_for_this_assumption,
else: negate(constraint_for_this_assumption)
apply(:intersect, op_intersect, acc_id, id_to_intersect) apply(:intersect, op_intersect, acc_id, id_to_intersect)
end) end)
# 2. Check for the three logical outcomes: # 2. Check for the three logical outcomes...
# - Does the path imply the constraint is satisfied?
# - Does the path imply the constraint is violated?
# - Or are both outcomes still possible?
# Implies satisfied: `sub_problem_tdd_id <: constraint_id`
# This is equivalent to `(sub_problem_tdd_id & !constraint_id)` being empty.
neg_constraint_id = negate(constraint_id) neg_constraint_id = negate(constraint_id)
intersect_sub_with_neg_constraint = intersect_sub_with_neg_constraint =
@ -1821,8 +1864,6 @@ defmodule Tdd.Algo do
implies_satisfied = implies_satisfied =
check_emptiness(intersect_sub_with_neg_constraint) == Store.false_node_id() check_emptiness(intersect_sub_with_neg_constraint) == Store.false_node_id()
# Implies violated: `sub_problem_tdd_id` and `constraint_id` are disjoint.
# This is equivalent to `(sub_problem_tdd_id & constraint_id)` being empty.
intersect_sub_with_constraint = intersect_sub_with_constraint =
apply(:intersect, op_intersect, sub_problem_tdd_id, constraint_id) apply(:intersect, op_intersect, sub_problem_tdd_id, constraint_id)
@ -1831,21 +1872,15 @@ defmodule Tdd.Algo do
# 3. Branch based on the logical outcome. # 3. Branch based on the logical outcome.
cond do cond do
implies_satisfied and implies_violated -> implies_satisfied and implies_violated ->
# The sub-problem itself must be empty/impossible under the current assumptions.
# This whole path is a contradiction.
Store.false_node_id() Store.false_node_id()
implies_satisfied -> implies_satisfied ->
# The constraint is guaranteed by the path. Follow the 'yes' branch.
# The assumptions already imply this, so no change to them is needed.
case algo_type do case algo_type do
:simplify -> do_simplify(y, sorted_assumptions, context) :simplify -> do_simplify(y, sorted_assumptions, context)
:check_emptiness -> do_check_emptiness(y, sorted_assumptions, context) :check_emptiness -> do_check_emptiness(y, sorted_assumptions, context)
end end
implies_violated -> implies_violated ->
# The constraint is impossible given the path. Follow the 'no' branch.
# We can add this new fact `{var, false}` to strengthen the assumptions.
new_assumptions = Map.put(assumptions, var, false) |> Enum.sort() new_assumptions = Map.put(assumptions, var, false) |> Enum.sort()
case algo_type do case algo_type do
@ -1854,17 +1889,13 @@ defmodule Tdd.Algo do
end end
true -> true ->
# Neither outcome is guaranteed. Both are possible.
# We must explore both branches and combine the results.
new_assumptions_for_no_branch = Map.put(assumptions, var, false) |> Enum.sort() new_assumptions_for_no_branch = Map.put(assumptions, var, false) |> Enum.sort()
case algo_type do case algo_type do
:check_emptiness -> :check_emptiness ->
# Is there ANY path to true? Explore both and take their union.
res_y = do_check_emptiness(y, sorted_assumptions, context) res_y = do_check_emptiness(y, sorted_assumptions, context)
res_n = do_check_emptiness(n, new_assumptions_for_no_branch, context) res_n = do_check_emptiness(n, new_assumptions_for_no_branch, context)
# Define local terminal logic for union
op_union = fn op_union = fn
:true_terminal, _ -> :true_terminal :true_terminal, _ -> :true_terminal
_, :true_terminal -> :true_terminal _, :true_terminal -> :true_terminal
@ -1875,16 +1906,15 @@ defmodule Tdd.Algo do
apply(:sum, op_union, res_y, res_n) apply(:sum, op_union, res_y, res_n)
:simplify -> :simplify ->
# Simplify both sub-trees and rebuild the node, as we cannot simplify this node away.
res_y = do_simplify(y, sorted_assumptions, context) res_y = do_simplify(y, sorted_assumptions, context)
res_n = do_simplify(n, new_assumptions_for_no_branch, context) res_n = do_simplify(n, new_assumptions_for_no_branch, context)
# 'dc' branch is independent of 'var'
res_d = do_simplify(d, sorted_assumptions, context) res_d = do_simplify(d, sorted_assumptions, context)
Store.find_or_create_node(var, res_y, res_n, res_d) Store.find_or_create_node(var, res_y, res_n, res_d)
end end
end end
end end
end end
end
defmodule Tdd.Compiler do defmodule Tdd.Compiler do
@moduledoc """ @moduledoc """