checkpoint before fixing consistency tests

This commit is contained in:
Kacper Marzecki 2025-06-18 15:56:12 +02:00
parent 130d369734
commit 36f63ed355

413
new.exs
View File

@ -682,8 +682,7 @@ defmodule Tdd.Consistency.Engine do
end)
# Attempt to merge the new implications into the set of all assumptions.
case Enum.reduce(implications, {:ok, %{}}, fn {implied_var, implied_val},
acc ->
case Enum.reduce(implications, {:ok, %{}}, fn {implied_var, implied_val}, acc ->
reduce_implication({implied_var, implied_val}, all_assumptions, acc)
end) do
{:error, :contradiction} ->
@ -792,7 +791,12 @@ defmodule Tdd.Consistency.Engine do
end
end)
case result, do: (:invalid -> :error; _ -> :ok)
case result,
do:
(
:invalid -> :error
_ -> :ok
)
end
# Helper to check if a new range is valid and continue/halt the reduction.
@ -814,7 +818,9 @@ defmodule Tdd.Consistency.Engine do
case Info.get_traits(var) do
%{type: :list_recursive_ambient, ambient_constraint_spec: spec} ->
Map.merge(acc, %{head: spec, tail: spec})
_ -> acc
_ ->
acc
end
_, acc ->
@ -869,8 +875,11 @@ defmodule Tdd.Algo do
def apply(op_name, op_lambda, u1_id, u2_id) do
# Memoization wrapper
cache_key = {:apply, op_name, Enum.sort([u1_id, u2_id])}
case Store.get_op_cache(cache_key) do
{:ok, result_id} -> result_id
{:ok, result_id} ->
result_id
:not_found ->
result_id = do_apply(op_name, op_lambda, u1_id, u2_id)
Store.put_op_cache(cache_key, result_id)
@ -884,19 +893,59 @@ defmodule Tdd.Algo do
cond do
(u1_details == :true_terminal or u1_details == :false_terminal) and
(u2_details == :true_terminal or u2_details == :false_terminal) ->
if op_lambda.(u1_details, u2_details) == :true_terminal, do: Store.true_node_id(), else: Store.false_node_id()
(u1_details == :true_terminal or u1_details == :false_terminal) ->
if op_lambda.(u1_details, u2_details) == :true_terminal,
do: Store.true_node_id(),
else: Store.false_node_id()
u1_details == :true_terminal or u1_details == :false_terminal ->
{var2, y2, n2, d2} = u2_details
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))
(u2_details == :true_terminal or u2_details == :false_terminal) ->
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)
)
u2_details == :true_terminal or u2_details == :false_terminal ->
{var1, y1, n1, d1} = u1_details
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))
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)
)
true ->
{var1, y1, n1, d1} = u1_details; {var2, y2, n2, d2} = u2_details
{var1, y1, n1, d1} = u1_details
{var2, y2, n2, d2} = u2_details
top_var = Enum.min([var1, var2])
res_y = apply(op_name, op_lambda, if(var1 == top_var, do: y1, else: u1_id), if(var2 == top_var, do: y2, else: u2_id))
res_n = apply(op_name, op_lambda, if(var1 == top_var, do: n1, else: u1_id), if(var2 == top_var, do: n2, else: u2_id))
res_d = apply(op_name, op_lambda, if(var1 == top_var, do: d1, else: u1_id), if(var2 == top_var, do: d2, else: u2_id))
res_y =
apply(
op_name,
op_lambda,
if(var1 == top_var, do: y1, else: u1_id),
if(var2 == top_var, do: y2, else: u2_id)
)
res_n =
apply(
op_name,
op_lambda,
if(var1 == top_var, do: n1, else: u1_id),
if(var2 == top_var, do: n2, else: u2_id)
)
res_d =
apply(
op_name,
op_lambda,
if(var1 == top_var, do: d1, else: u1_id),
if(var2 == top_var, do: d2, else: u2_id)
)
Store.find_or_create_node(top_var, res_y, res_n, res_d)
end
end
@ -906,15 +955,24 @@ defmodule Tdd.Algo do
@spec negate(non_neg_integer) :: non_neg_integer
def negate(tdd_id) do
cache_key = {:negate, tdd_id}
case Store.get_op_cache(cache_key) do
{:ok, result_id} -> result_id
{: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))
{: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
end
@ -926,8 +984,11 @@ defmodule Tdd.Algo do
# Sort assumptions to ensure the cache key is canonical.
sorted_assumptions = Enum.sort(assumptions)
cache_key = {:simplify, tdd_id, sorted_assumptions}
case Store.get_op_cache(cache_key) do
{:ok, result_id} -> result_id
{:ok, result_id} ->
result_id
:not_found ->
result_id = do_simplify(tdd_id, assumptions)
Store.put_op_cache(cache_key, result_id)
@ -942,16 +1003,25 @@ defmodule Tdd.Algo do
else
case Store.get_node(tdd_id) do
# 2. Terminal nodes are already simple.
{:ok, :true_terminal} -> Store.true_node_id()
{:ok, :false_terminal} -> Store.false_node_id()
{:ok, :true_terminal} ->
Store.true_node_id()
{:ok, :false_terminal} ->
Store.false_node_id()
# 3. Handle non-terminal nodes.
{:ok, {var, y, n, d}} ->
# 4. Check if the variable's value is already known or implied.
case Map.get(assumptions, var) do
true -> simplify(y, assumptions)
false -> simplify(n, assumptions)
:dc -> simplify(d, assumptions)
true ->
simplify(y, assumptions)
false ->
simplify(n, assumptions)
:dc ->
simplify(d, assumptions)
nil ->
# The variable is not explicitly constrained. Check for implied constraints.
# Note: For now, the Engine only performs flat checks.
@ -959,9 +1029,15 @@ defmodule Tdd.Algo do
implies_false = Engine.check(Map.put(assumptions, var, true)) == :contradiction
cond do
implies_true and implies_false -> Store.false_node_id()
implies_true -> simplify(y, assumptions)
implies_false -> simplify(n, assumptions)
implies_true and implies_false ->
Store.false_node_id()
implies_true ->
simplify(y, assumptions)
implies_false ->
simplify(n, assumptions)
true ->
# No constraint, so recursively simplify all branches.
s_y = simplify(y, Map.put(assumptions, var, true))
@ -974,6 +1050,7 @@ defmodule Tdd.Algo do
end
end
end
defmodule Tdd.TypeReconstructor do
@moduledoc """
Reconstructs a high-level `TypeSpec` from a low-level assumption map.
@ -1089,6 +1166,7 @@ defmodule Tdd.TypeReconstructor do
TypeSpec.normalize({:intersect, specs})
end
end
defmodule Tdd.Compiler do
@moduledoc "Compiles a `TypeSpec` into a canonical TDD ID."
alias Tdd.TypeSpec
@ -1104,7 +1182,9 @@ defmodule Tdd.Compiler do
cache_key = {:spec_to_id, normalized_spec}
case Store.get_op_cache(cache_key) do
{:ok, id} -> id
{:ok, id} ->
id
:not_found ->
id = do_spec_to_id(normalized_spec)
Store.put_op_cache(cache_key, id)
@ -1117,46 +1197,121 @@ defmodule Tdd.Compiler do
raw_id =
case spec do
# --- Base Types ---
:any -> Store.true_node_id()
:none -> Store.false_node_id()
:atom -> Store.find_or_create_node(Variable.v_is_atom(), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
:integer -> Store.find_or_create_node(Variable.v_is_integer(), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
:any ->
Store.true_node_id()
:none ->
Store.false_node_id()
:atom ->
Store.find_or_create_node(
Variable.v_is_atom(),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
:integer ->
Store.find_or_create_node(
Variable.v_is_integer(),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
# Add :list, :tuple etc. here. They are simple structural TDDs.
:list -> Store.find_or_create_node(Variable.v_is_list(), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
:list ->
Store.find_or_create_node(
Variable.v_is_list(),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
# --- Literal Types ---
{:literal, val} when is_atom(val) ->
eq_node = Store.find_or_create_node(Variable.v_atom_eq(val), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
Store.find_or_create_node(Variable.v_is_atom(), eq_node, Store.false_node_id(), Store.false_node_id())
eq_node =
Store.find_or_create_node(
Variable.v_atom_eq(val),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
Store.find_or_create_node(
Variable.v_is_atom(),
eq_node,
Store.false_node_id(),
Store.false_node_id()
)
{:literal, val} when is_integer(val) ->
eq_node = Store.find_or_create_node(Variable.v_int_eq(val), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
Store.find_or_create_node(Variable.v_is_integer(), eq_node, Store.false_node_id(), Store.false_node_id())
eq_node =
Store.find_or_create_node(
Variable.v_int_eq(val),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
Store.find_or_create_node(
Variable.v_is_integer(),
eq_node,
Store.false_node_id(),
Store.false_node_id()
)
{:literal, []} ->
empty_node = Store.find_or_create_node(Variable.v_list_is_empty(), Store.true_node_id(), Store.false_node_id(), Store.false_node_id())
Store.find_or_create_node(Variable.v_is_list(), empty_node, Store.false_node_id(), Store.false_node_id())
empty_node =
Store.find_or_create_node(
Variable.v_list_is_empty(),
Store.true_node_id(),
Store.false_node_id(),
Store.false_node_id()
)
Store.find_or_create_node(
Variable.v_is_list(),
empty_node,
Store.false_node_id(),
Store.false_node_id()
)
# Add other literals as needed
# --- Set-Theoretic Combinators ---
{:union, specs} ->
ids = Enum.map(specs, &spec_to_id/1)
Enum.reduce(ids, Store.false_node_id(), fn id, acc ->
Algo.apply(:sum, fn
Algo.apply(
:sum,
fn
:true_terminal, _ -> :true_terminal
_, :true_terminal -> :true_terminal
:false_terminal, t2 -> t2
t1, :false_terminal -> t1
end, id, acc)
end,
id,
acc
)
end)
{:intersect, specs} ->
ids = Enum.map(specs, &spec_to_id/1)
Enum.reduce(ids, Store.true_node_id(), fn id, acc ->
Algo.apply(:intersect, fn
Algo.apply(
:intersect,
fn
:false_terminal, _ -> :false_terminal
_, :false_terminal -> :false_terminal
:true_terminal, t2 -> t2
t1, :true_terminal -> t1
end, id, acc)
end,
id,
acc
)
end)
{:negation, sub_spec} ->
@ -1164,9 +1319,14 @@ defmodule Tdd.Compiler do
# --- Recursive Types (STUBS for now) ---
# These will be implemented in Step 3
{:list_of, _} -> raise "Tdd.Compiler: :list_of not yet implemented"
{:tuple, _} -> raise "Tdd.Compiler: {:tuple, [...]} not yet implemented"
{:cons, _, _} -> raise "Tdd.Compiler: :cons not yet implemented"
{:list_of, _} ->
raise "Tdd.Compiler: :list_of not yet implemented"
{:tuple, _} ->
raise "Tdd.Compiler: {:tuple, [...]} not yet implemented"
{:cons, _, _} ->
raise "Tdd.Compiler: :cons not yet implemented"
# --- Default ---
_ ->
@ -1178,6 +1338,7 @@ defmodule Tdd.Compiler do
Algo.simplify(raw_id)
end
end
####
# xxx
####
@ -2002,18 +2163,30 @@ defmodule TypeReconstructorTests do
end
end
end
defmodule CompilerAlgoTests do
alias Tdd.Compiler
alias Tdd.Store
alias Tdd.Algo
# High-level helpers that mimic the final API
defp is_subtype(spec1, spec2) do
id1 = Compiler.spec_to_id(spec1)
id2 = Compiler.spec_to_id(spec2)
# is_subtype(A, B) <=> (A & ~B) == none
neg_id2 = Tdd.Algo.negate(id2)
intersect_id = Tdd.Algo.apply(:intersect, &(&1 == :false_terminal or &2 == :false_terminal), id1, neg_id2)
final_id = Tdd.Algo.simplify(intersect_id)
# The subtyping check is: `A <: B` if and only if `A & ~B` is empty (`:none`).
neg_id2 = Algo.negate(id2)
op_intersect = fn
:false_terminal, _ -> :false_terminal
_, :false_terminal -> :false_terminal
t, :true_terminal -> t
:true_terminal, t -> t
# Default case for non-terminal nodes, though apply handles recursion
_t1, _t2 -> :non_terminal
end
intersect_id = Algo.apply(:intersect, op_intersect, id1, neg_id2)
final_id = Algo.simplify(intersect_id)
final_id == Store.false_node_id()
end
@ -2021,11 +2194,16 @@ defmodule CompilerAlgoTests do
Compiler.spec_to_id(spec1) == Compiler.spec_to_id(spec2)
end
defp is_contradiction(spec) do
Compiler.spec_to_id(spec) == Store.false_node_id()
end
defp test_subtype(name, expected, s1, s2), do: test(name, expected, is_subtype(s1, s2))
defp test_equiv(name, expected, s1, s2), do: test(name, expected, are_equivalent(s1, s2))
defp test(name, exp, res) do
defp test_contradiction(name, expected \\ true), do: &test(name, expected, is_contradiction(&1))
is_ok = exp==res
defp test(name, exp, res) do
is_ok = exp == res
status = if is_ok, do: "[PASS]", else: "[FAIL]"
IO.puts("#{status} #{name}")
@ -2036,19 +2214,22 @@ defmodule CompilerAlgoTests do
end
end
def run() do
IO.puts("\n--- Running Compiler & Algo Integration Tests ---")
Process.put(:test_failures, [])
# Setup
Tdd.Store.init()
# --- Section: Basic Compilation & Equivalence ---
IO.puts("\n--- Section: Basic Equivalences ---")
test_equiv("atom & any == atom", true, {:intersect, [:atom, :any]}, :atom)
test_equiv("atom | none == atom", true, {:union, [:atom, :none]}, :atom)
test_equiv("atom & int == none", true, {:intersect, [:atom, :integer]}, :none)
test_equiv("¬(¬atom) == atom", true, {:negation, {:negation, :atom}}, :atom)
test_equiv("atom | atom == atom", true, {:union, [:atom, :atom]}, :atom)
# --- Section: Subtyping Tests ---
# --- Section: Basic Subtyping ---
IO.puts("\n--- Section: Basic Subtyping ---")
test_subtype(":foo <: atom", true, {:literal, :foo}, :atom)
test_subtype("atom <: :foo", false, :atom, {:literal, :foo})
test_subtype(":foo <: integer", false, {:literal, :foo}, :integer)
@ -2056,26 +2237,136 @@ defmodule CompilerAlgoTests do
test_subtype("none <: atom", true, :none, :atom)
test_subtype("atom <: any", true, :atom, :any)
# --- Section: De Morgan's Law Check ---
# ¬(A | B) == (¬A & ¬B)
# --- Section: Integer Range Logic ---
IO.puts("\n--- Section: Integer Range Logic ---")
int_5_to_10 = {:integer_range, 5, 10}
int_7_to_8 = {:integer_range, 7, 8}
int_15_to_20 = {:integer_range, 15, 20}
int_0_to_100 = {:integer_range, 0, 100}
test_subtype("range(7..8) <: range(5..10)", true, int_7_to_8, int_5_to_10)
test_subtype("range(5..10) <: range(7..8)", false, int_5_to_10, int_7_to_8)
test_subtype("range(5..10) <: range(15..20)", false, int_5_to_10, int_15_to_20)
test_equiv(
"range(5..10) & range(7..8) == range(7..8)",
true,
{:intersect, [int_5_to_10, int_7_to_8]},
int_7_to_8
)
test_equiv(
"range(5..10) & range(0..100) == range(5..10)",
true,
{:intersect, [int_5_to_10, int_0_to_100]},
int_5_to_10
)
test_equiv(
"range(5..10) | range(7..8) == range(5..10)",
true,
{:union, [int_5_to_10, int_7_to_8]},
int_5_to_10
)
# --- Section: Contradictions & Simplifications ---
IO.puts("\n--- Section: Contradictions & Simplifications ---")
test_contradiction("atom & integer").({:intersect, [:atom, :integer]})
test_contradiction(":foo & :bar").({:intersect, [{:literal, :foo}, {:literal, :bar}]})
test_contradiction("atom & (int==5)").({:intersect, [:atom, {:literal, 5}]})
test_contradiction("range(5..10) & range(15..20)").({:intersect, [int_5_to_10, int_15_to_20]})
test_contradiction("integer & ¬integer").({:intersect, [:integer, {:negation, :integer}]})
# --- Section: Subtype Reduction in Normalization ---
IO.puts("\n--- Section: Subtype Reduction Logic ---")
test_equiv(
"(:foo | :bar | atom) simplifies to atom",
true,
{:union, [{:literal, :foo}, {:literal, :bar}, :atom]},
:atom
)
test_equiv(
"(range(5..10) | integer) simplifies to integer",
true,
{:union, [int_5_to_10, :integer]},
:integer
)
test_equiv(
"(:foo & atom) simplifies to :foo",
true,
{:intersect, [{:literal, :foo}, :atom]},
{:literal, :foo}
)
test_equiv(
"(range(5..10) & integer) simplifies to range(5..10)",
true,
{:intersect, [int_5_to_10, :integer]},
int_5_to_10
)
# --- Section: Logical Laws (Distribution and De Morgan's) ---
IO.puts("\n--- Section: Logical Laws ---")
# De Morgan's Law: ¬(A | B) == (¬A & ¬B)
spec_not_a_or_b = {:negation, {:union, [:atom, :integer]}}
spec_not_a_and_not_b = {:intersect, [{:negation, :atom}, {:negation, :integer}]}
test_equiv("De Morgan's Law holds", true, spec_not_a_or_b, spec_not_a_and_not_b)
test_equiv(
"De Morgan's (¬(A|B) == ¬A & ¬B) holds",
true,
spec_not_a_or_b,
spec_not_a_and_not_b
)
# De Morgan's Law: ¬(A & B) == (¬A | ¬B)
spec_not_a_and_b = {:negation, {:intersect, [{:literal, :foo}, int_5_to_10]}}
spec_not_a_or_not_b = {:union, [{:negation, {:literal, :foo}}, {:negation, int_5_to_10}]}
test_equiv(
"De Morgan's (¬(A&B) == ¬A | ¬B) holds",
true,
spec_not_a_and_b,
spec_not_a_or_not_b
)
# Distributive Law: A & (B | C) == (A & B) | (A & C)
spec_a = :integer
spec_b = {:integer_range, 0, 10}
spec_c = {:integer_range, 20, 30}
spec_dist_lhs = {:intersect, [spec_a, {:union, [spec_b, spec_c]}]}
spec_dist_rhs = {:union, [{:intersect, [spec_a, spec_b]}, {:intersect, [spec_a, spec_c]}]}
test_equiv(
"Distributive Law (A & (B|C)) holds",
true,
spec_dist_lhs,
spec_dist_rhs
)
# --- Final Report ---
failures = Process.get(:test_failures, [])
if failures == [] do
IO.puts("\n✅ All CompilerAlgoTests tests passed!")
IO.puts("\n✅ All Compiler & Algo Integration tests passed!")
else
IO.puts("\n❌ Found #{length(failures)} test failures.")
end
end
end
TypeSpecTests.run()
TddStoreTests.run()
TddVariableTests.run()
# TddAlgoTests.run()
# ConsistencyEngineTests.run()
TypeReconstructorTests.run()
CompilerAlgoTests.run()
ConsistencyEngineTests.run()
# TypeReconstructorTests.run()
# CompilerAlgoTests.run()