reconstructor & normalization

This commit is contained in:
Kacper Marzecki 2025-06-18 15:10:58 +02:00
parent 9f6cd2814a
commit a5e5127bcd

497
new.exs
View File

@ -69,123 +69,277 @@ defmodule Tdd.TypeSpec do
@spec normalize(t()) :: t() @spec normalize(t()) :: t()
def normalize(spec) do def normalize(spec) do
case spec do case spec do
# --- Base cases: already normalized --- # Base cases are unchanged
:any -> s when is_atom(s) -> s
:any {:literal, _} -> spec
{:type_var, _} -> spec
# Recursive cases now call helper functions
{:negation, sub_spec} -> normalize_negation(sub_spec)
{:tuple, elements} -> {:tuple, Enum.map(elements, &normalize/1)}
{:cons, head, tail} -> {:cons, normalize(head), normalize(tail)}
{:list_of, element} -> {:list_of, normalize(element)}
# A new rule for integer ranges
{:integer_range, min, max} -> normalize_integer_range(min, max)
{:union, members} -> normalize_union(members)
{:intersect, members} -> normalize_intersection(members)
end
end
:none -> # ------------------------------------------------------------------
:none # Private Normalization Helpers
# ------------------------------------------------------------------
:atom -> defp normalize_negation(sub_spec) do
:atom normalized_sub = normalize(sub_spec)
:integer -> case normalized_sub do
:integer # ¬(¬A) -> A
{:negation, inner_spec} -> inner_spec
:any -> :none
:none -> :any
_ -> {:negation, normalized_sub}
end
end
:list -> defp normalize_integer_range(min, max) do
:list # An invalid range simplifies to `none`.
if is_integer(min) and is_integer(max) and min > max do
:none
else
# An intersection with integer is implied, so we add it for canonical form.
{:intersect, [:integer, {:integer_range, min, max}]}
end
end
:tuple -> defp normalize_union(members) do
:tuple # 1. Recursively normalize and flatten members
normalized_and_flattened =
Enum.flat_map(members, fn member ->
normalized = normalize(member)
{:literal, _} -> case normalized,
spec do:
(
{:type_var, _} ->
spec
# Assume range is already canonical
{:integer_range, _, _} ->
spec
# --- Recursive cases ---
{:negation, sub_spec} ->
normalized_sub = normalize(sub_spec)
# Apply double negation law: ¬(¬A) -> A
case normalized_sub do
{:negation, inner_spec} -> inner_spec
_ -> {:negation, normalized_sub}
end
{:tuple, elements} ->
{:tuple, Enum.map(elements, &normalize/1)}
{:cons, head, tail} ->
{:cons, normalize(head), normalize(tail)}
{:list_of, element} ->
{:list_of, normalize(element)}
# --- Complex cases: Union and Intersection ---
{:union, members} ->
# 1. Recursively normalize, then flatten nested unions.
normalized_and_flattened =
Enum.flat_map(members, fn member ->
normalized = normalize(member)
case normalized do
{:union, sub_members} -> sub_members {:union, sub_members} -> sub_members
_ -> [normalized] _ -> [normalized]
end )
end)
# 2. Apply simplification rules
simplified_members =
normalized_and_flattened
# A | none -> A
|> Enum.reject(&(&1 == :none))
|> MapSet.new()
if MapSet.member?(simplified_members, :any) do
# A | any -> any
:any
else
# 3. NEW: Reduce by removing subtypes
# If we have {A, B} and A <: B, the union is just B. So we keep only supersets.
# We achieve this by removing any member that is a subtype of another member.
reduced_members =
Enum.reject(simplified_members, fn member_to_check ->
Enum.any?(simplified_members, fn other_member ->
member_to_check != other_member and is_subtype?(member_to_check, other_member)
end) end)
end)
# 2. Apply simplification rules and sort. # 4. Finalize the structure
simplified_members = case reduced_members do
normalized_and_flattened [] -> :none
# Annihilation: A | none -> A [single_member] -> single_member
|> Enum.reject(&(&1 == :none)) list -> {:union, Enum.sort(list)}
# Uniq members end
|> MapSet.new()
# Annihilation: if `any` is a member, the whole union is `any`.
if MapSet.member?(simplified_members, :any) do
:any
else
# 3. Finalize the structure.
case MapSet.to_list(simplified_members) do
# An empty union is the empty set.
[] -> :none
# Idempotency: A | A -> A
[single_member] -> single_member
sorted_members -> {:union, Enum.sort(sorted_members)}
end
end
{:intersect, members} ->
# 1. Recursively normalize, then flatten.
normalized_and_flattened =
Enum.flat_map(members, fn member ->
normalized = normalize(member)
case normalized do
{:intersect, sub_members} -> sub_members
_ -> [normalized]
end
end)
# 2. Apply simplification rules and sort.
simplified_members =
normalized_and_flattened
# Annihilation: A & any -> A
|> Enum.reject(&(&1 == :any))
|> MapSet.new()
# Annihilation: if `none` is a member, the whole intersection is `none`.
if MapSet.member?(simplified_members, :none) do
:none
else
# 3. Finalize the structure.
case MapSet.to_list(simplified_members) do
# An empty intersection is the universal set.
[] -> :any
# Idempotency: A & A -> A
[single_member] -> single_member
sorted_members -> {:intersect, Enum.sort(sorted_members)}
end
end
end end
end end
defp normalize_intersection(members) do
IO.inspect("Normalize intersection")
# 1. Recursively normalize and flatten, but also add implied supertypes
normalized_and_flattened =
Enum.flat_map(members, fn member ->
IO.inspect(member, label: "normalize member")
normalized = normalize(member)
# Expand a type into itself and its implied supertypes
# e.g., `:foo` becomes `[:foo, :atom]`
expanded =
case normalized do
{:intersect, sub_members} -> sub_members
_ -> get_supertypes(normalized)
end
expanded
end)
# 2. Apply simplification rules
simplified_members =
normalized_and_flattened
# A & any -> A
|> Enum.reject(&(&1 == :any))
|> MapSet.new()
if MapSet.member?(simplified_members, :none) do
# A & none -> none
:none
else
# 3. NEW: Reduce by removing supertypes
IO.inspect("Reduce by removing supertypes")
# If we have {A, B} and A <: B, the intersection is just A. So we keep only subsets.
# We achieve this by removing any member for which a proper subtype exists in the set.
reduced_members =
Enum.reject(simplified_members, fn member_to_check ->
Enum.any?(simplified_members, fn other_member ->
member_to_check != other_member and is_subtype?(other_member, member_to_check)
end)
end)
# 4. Finalize the structure
IO.inspect("4. Finalize the structure")
case reduced_members do
[] -> :any
[single_member] -> single_member
list -> {:intersect, Enum.sort(list)}
end
end
end
# ------------------------------------------------------------------
# Private Semantic Helpers
# ------------------------------------------------------------------
@doc """
A preliminary, non-TDD check if `spec1` is a subtype of `spec2`.
This check is not exhaustive but covers many common, structural cases,
allowing for significant simplification at the `TypeSpec` level.
"""
@spec is_subtype?(t(), t()) :: boolean
def is_subtype?(spec1, spec2) do
# Avoid infinite recursion by not re-normalizing.
# The callers are assumed to be working with normalized data.
# Base cases are handled first for efficiency.
cond do
spec1 == spec2 -> true
spec1 == :none -> true
spec2 == :any -> true
spec1 == :any or spec2 == :none -> false
# Defer to pattern-matching helper
true -> do_is_subtype?(spec1, spec2)
end
end
# Private helper that uses `case` for proper pattern matching.
defp do_is_subtype?(spec1, spec2) do
case {spec1, spec2} do
# --- Set-theoretic rules ---
# (A | B) <: C if A <: C and B <: C
{{:union, members1}, _} ->
Enum.all?(members1, &is_subtype?(&1, spec2))
# A <: (B | C) if A <: B or A <: C
{_, {:union, members2}} ->
Enum.any?(members2, &is_subtype?(spec1, &1))
# (A & B) <: C if A <: C or B <: C
{{:intersect, members1}, _} ->
Enum.any?(members1, &is_subtype?(&1, spec2))
# A <: (B & C) if A <: B and A <: C
{_, {:intersect, members2}} ->
Enum.all?(members2, &is_subtype?(spec1, &1))
# --- Literal and Base Type Rules ---
{s1, s2} when is_atom(s1) and is_atom(s2) ->
s1 == s2
{{:literal, v1}, {:literal, v2}} ->
v1 == v2
{{:literal, val}, :atom} when is_atom(val) ->
true
{{:literal, val}, :integer} when is_integer(val) ->
true
{{:literal, val}, :list} when is_list(val) ->
true
{{:literal, val}, :tuple} when is_tuple(val) ->
true
# --- List and Cons Rules ---
{{:literal, []}, :list} ->
true
{{:cons, _, _}, :list} ->
true
{{:list_of, _}, :list} ->
true
{{:cons, h1, t1}, {:cons, h2, t2}} ->
is_subtype?(h1, h2) and is_subtype?(t1, t2)
{{:list_of, e1}, {:list_of, e2}} ->
is_subtype?(e1, e2)
{{:cons, h, t}, {:list_of, x}} ->
is_subtype?(h, x) and is_subtype?(t, {:list_of, x})
{{:literal, []}, {:list_of, _}} ->
true
# --- Tuple Rules ---
{{:literal, {}}, :tuple} ->
true
{{:tuple, _}, :tuple} ->
true
{{:tuple, elems1}, {:tuple, elems2}} when length(elems1) == length(elems2) ->
Enum.zip_with(elems1, elems2, &is_subtype?/2) |> Enum.all?()
# --- Integer Range Rules ---
{{:integer_range, _, _}, :integer} ->
true
{{:integer_range, min1, max1}, {:integer_range, min2, max2}} ->
min1_gte_min2 = if min1 == :neg_inf, do: true, else: min2 != :neg_inf and min1 >= min2
max1_lte_max2 = if max1 == :pos_inf, do: true, else: max2 != :pos_inf and max1 <= max2
min1_gte_min2 and max1_lte_max2
{{:literal, val}, {:integer_range, min, max}} when is_integer(val) ->
(min == :neg_inf or val >= min) and (max == :pos_inf or val <= max)
# --- Default fallback ---
# If no specific rule matches, they are not considered subtypes.
{_, _} ->
false
end
end
# Gets a list of immediate, known supertypes for a given simple spec.
defp get_supertypes(spec) do
supertypes =
case spec do
{:literal, val} when is_atom(val) -> [:atom]
{:literal, val} when is_integer(val) -> [:integer]
{:literal, val} when is_list(val) -> [:list]
{:literal, val} when is_tuple(val) -> [:tuple]
{:cons, _, _} -> [:list]
{:list_of, _} -> [:list]
{:tuple, _} -> [:tuple]
{:integer_range, _, _} -> [:integer]
_ -> []
end
# Use a MapSet to ensure the spec and its supertypes are unique.
MapSet.to_list(MapSet.new([spec | supertypes]))
end
end end
defmodule Tdd.Store do defmodule Tdd.Store do
@ -848,8 +1002,10 @@ defmodule Tdd.TypeReconstructor do
partitions = partitions =
Enum.group_by(assumptions, fn {var, _val} -> Enum.group_by(assumptions, fn {var, _val} ->
case Info.get_traits(var) do case Info.get_traits(var) do
%{type: :list_recursive, sub_key: key} -> key # :head or :tail # :head or :tail
%{type: :tuple_recursive, sub_key: key} -> key # {:elem, index} %{type: :list_recursive, sub_key: key} -> key
# {:elem, index}
%{type: :tuple_recursive, sub_key: key} -> key
# All other predicates apply to the top-level entity # All other predicates apply to the top-level entity
_ -> :top_level _ -> :top_level
end end
@ -871,7 +1027,10 @@ defmodule Tdd.TypeReconstructor do
|> Map.new(fn {var, val} -> |> Map.new(fn {var, val} ->
# This pattern matching is a bit simplified for clarity # This pattern matching is a bit simplified for clarity
{_cat, _pred, nested_var_or_idx, maybe_nested_var} = var {_cat, _pred, nested_var_or_idx, maybe_nested_var} = var
nested_var = if is_nil(maybe_nested_var), do: nested_var_or_idx, else: maybe_nested_var
nested_var =
if is_nil(maybe_nested_var), do: nested_var_or_idx, else: maybe_nested_var
{nested_var, val} {nested_var, val}
end) end)
@ -880,14 +1039,21 @@ defmodule Tdd.TypeReconstructor do
# Wrap it in a constructor that describes its relationship to the parent # Wrap it in a constructor that describes its relationship to the parent
case sub_key do case sub_key do
:head -> {:cons, sub_spec, :any} # Partial spec: just describes the head # Partial spec: just describes the head
:tail -> {:cons, :any, sub_spec} # Partial spec: just describes the tail :head ->
{:cons, sub_spec, :any}
# Partial spec: just describes the tail
:tail ->
{:cons, :any, sub_spec}
{:elem, index} -> {:elem, index} ->
# Create a sparse tuple spec, e.g., {any, any, <sub_spec>, any} # Create a sparse tuple spec, e.g., {any, any, <sub_spec>, any}
# This is complex, a simpler approach is needed for now. # This is complex, a simpler approach is needed for now.
# For now, we'll just return a tuple spec that isn't fully specific. # For now, we'll just return a tuple spec that isn't fully specific.
# A full implementation would need to know the tuple's size. # A full implementation would need to know the tuple's size.
{:tuple, [sub_spec]} # This is an oversimplification but works for demo # This is an oversimplification but works for demo
{:tuple, [sub_spec]}
end end
end) end)
@ -896,7 +1062,6 @@ defmodule Tdd.TypeReconstructor do
TypeSpec.normalize({:intersect, final_spec_list}) TypeSpec.normalize({:intersect, final_spec_list})
end end
@doc "Handles only the 'flat' (non-recursive) assumptions for a single entity." @doc "Handles only the 'flat' (non-recursive) assumptions for a single entity."
defp spec_from_flat_assumptions(assumptions) do defp spec_from_flat_assumptions(assumptions) do
specs = specs =
@ -912,10 +1077,13 @@ defmodule Tdd.TypeReconstructor do
{0, :is_tuple, _, _} -> :tuple {0, :is_tuple, _, _} -> :tuple
{1, :value, val, _} -> {:literal, val} {1, :value, val, _} -> {:literal, val}
# For integer properties, we create a range spec. This part could be more detailed. # For integer properties, we create a range spec. This part could be more detailed.
{2, :alt, n, _} -> {:integer_range, :neg_inf, n - 1} # x < n # x < n
{2, :alt, n, _} -> {:integer_range, :neg_inf, n - 1}
{2, :beq, n, _} -> {:literal, n} {2, :beq, n, _} -> {:literal, n}
{2, :cgt, n, _} -> {:integer_range, n + 1, :pos_inf} # x > n # x > n
{4, :a_size, _, _} -> :tuple # Simplified for now {2, :cgt, n, _} -> {:integer_range, n + 1, :pos_inf}
# Simplified for now
{4, :a_size, _, _} -> :tuple
{5, :b_is_empty, _, _} -> {:literal, []} {5, :b_is_empty, _, _} -> {:literal, []}
# Ignore recursive and ambient vars at this flat level # Ignore recursive and ambient vars at this flat level
_ -> :any _ -> :any
@ -928,6 +1096,7 @@ defmodule Tdd.TypeReconstructor do
TypeSpec.normalize({:intersect, specs}) TypeSpec.normalize({:intersect, specs})
end end
end end
#### ####
# xxx # xxx
#### ####
@ -1353,7 +1522,6 @@ defmodule TddVariableTests do
end end
end end
defmodule ConsistencyEngineTests do defmodule ConsistencyEngineTests do
alias Tdd.Consistency.Engine alias Tdd.Consistency.Engine
alias Tdd.Variable alias Tdd.Variable
@ -1361,9 +1529,10 @@ defmodule ConsistencyEngineTests do
defp test(name, expected, assumptions_map) do defp test(name, expected, assumptions_map) do
result = Engine.check(assumptions_map) result = Engine.check(assumptions_map)
# ... test reporting logic ... # ... test reporting logic ...
is_ok = (expected == result) is_ok = expected == result
status = if is_ok, do: "[PASS]", else: "[FAIL]" status = if is_ok, do: "[PASS]", else: "[FAIL]"
IO.puts("#{status} #{name}") IO.puts("#{status} #{name}")
unless is_ok do unless is_ok do
IO.puts(" Expected: #{inspect(expected)}, Got: #{inspect(result)}") IO.puts(" Expected: #{inspect(expected)}, Got: #{inspect(result)}")
Process.put(:test_failures, [name | Process.get(:test_failures, [])]) Process.put(:test_failures, [name | Process.get(:test_failures, [])])
@ -1378,48 +1547,56 @@ defmodule ConsistencyEngineTests do
IO.puts("\n--- Section: Basic & Implication Tests ---") IO.puts("\n--- Section: Basic & Implication Tests ---")
test("An empty assumption map is consistent", :consistent, %{}) test("An empty assumption map is consistent", :consistent, %{})
test("A single valid assumption is consistent", :consistent, %{Variable.v_is_atom() => true}) test("A single valid assumption is consistent", :consistent, %{Variable.v_is_atom() => true})
test( test(
"An explicit contradiction is caught", "An explicit contradiction is caught",
:contradiction, :contradiction,
%{Variable.v_is_atom() => true, Variable.v_is_atom() => false} %{Variable.v_is_atom() => true, Variable.v_is_atom() => false}
) )
test( test(
"An implied contradiction is caught by expander", "An implied contradiction is caught by expander",
:contradiction, :contradiction,
%{Variable.v_atom_eq(:foo) => true, Variable.v_is_atom() => false} %{Variable.v_atom_eq(:foo) => true, Variable.v_is_atom() => false}
) )
test( test(
"Implication creates a consistent set", "Implication creates a consistent set",
:consistent, :consistent,
%{Variable.v_atom_eq(:foo) => true} # implies is_atom=true # implies is_atom=true
%{Variable.v_atom_eq(:foo) => true}
) )
# --- Section: Primary Type Exclusivity --- # --- Section: Primary Type Exclusivity ---
IO.puts("\n--- Section: Primary Type Exclusivity ---") IO.puts("\n--- Section: Primary Type Exclusivity ---")
test( test(
"Two primary types cannot both be true", "Two primary types cannot both be true",
:contradiction, :contradiction,
%{Variable.v_is_atom() => true, Variable.v_is_integer() => true} %{Variable.v_is_atom() => true, Variable.v_is_integer() => true}
) )
test( test(
"Two primary types implied to be true is a contradiction", "Two primary types implied to be true is a contradiction",
:contradiction, :contradiction,
%{Variable.v_atom_eq(:foo) => true, Variable.v_int_eq(5) => true} %{Variable.v_atom_eq(:foo) => true, Variable.v_int_eq(5) => true}
) )
test( test(
"One primary type true and another false is consistent", "One primary type true and another false is consistent",
:consistent, :consistent,
%{Variable.v_is_atom() => true, Variable.v_is_integer() => false} %{Variable.v_is_atom() => true, Variable.v_is_integer() => false}
) )
# --- Section: Atom Consistency --- # --- Section: Atom Consistency ---
IO.puts("\n--- Section: Atom Consistency ---") IO.puts("\n--- Section: Atom Consistency ---")
test( test(
"An atom cannot equal two different values", "An atom cannot equal two different values",
:contradiction, :contradiction,
%{Variable.v_atom_eq(:foo) => true, Variable.v_atom_eq(:bar) => true} %{Variable.v_atom_eq(:foo) => true, Variable.v_atom_eq(:bar) => true}
) )
test( test(
"An atom can equal one value", "An atom can equal one value",
:consistent, :consistent,
@ -1428,35 +1605,75 @@ defmodule ConsistencyEngineTests do
# --- Section: List Flat Consistency --- # --- Section: List Flat Consistency ---
IO.puts("\n--- Section: List Flat Consistency ---") IO.puts("\n--- Section: List Flat Consistency ---")
test( test(
"A list cannot be empty and have a head property", "A list cannot be empty and have a head property",
:contradiction, :contradiction,
%{Variable.v_list_is_empty() => true, Variable.v_list_head_pred(Variable.v_is_atom()) => true} %{
Variable.v_list_is_empty() => true,
Variable.v_list_head_pred(Variable.v_is_atom()) => true
}
) )
test( test(
"A non-empty list can have a head property", "A non-empty list can have a head property",
:consistent, :consistent,
%{Variable.v_list_is_empty() => false, Variable.v_list_head_pred(Variable.v_is_atom()) => true} %{
Variable.v_list_is_empty() => false,
Variable.v_list_head_pred(Variable.v_is_atom()) => true
}
) )
test( test(
"A non-empty list is implied by head property", "A non-empty list is implied by head property",
:consistent, :consistent,
%{Variable.v_list_head_pred(Variable.v_is_atom()) => true} # implies is_empty=false # implies is_empty=false
%{Variable.v_list_head_pred(Variable.v_is_atom()) => true}
) )
# --- Section: Integer Consistency --- # --- Section: Integer Consistency ---
IO.puts("\n--- Section: Integer Consistency ---") IO.puts("\n--- Section: Integer Consistency ---")
test("int == 5 is consistent", :consistent, %{Variable.v_int_eq(5) => true}) test("int == 5 is consistent", :consistent, %{Variable.v_int_eq(5) => true})
test("int == 5 AND int == 10 is a contradiction", :contradiction, %{Variable.v_int_eq(5) => true, Variable.v_int_eq(10) => true})
test("int < 10 AND int > 20 is a contradiction", :contradiction, %{Variable.v_int_lt(10) => true, Variable.v_int_gt(20) => true}) test("int == 5 AND int == 10 is a contradiction", :contradiction, %{
test("int > 5 AND int < 4 is a contradiction", :contradiction, %{Variable.v_int_gt(5) => true, Variable.v_int_lt(4) => true}) Variable.v_int_eq(5) => true,
test("int > 5 AND int < 7 is consistent", :consistent, %{Variable.v_int_gt(5) => true, Variable.v_int_lt(7) => true}) # 6 Variable.v_int_eq(10) => true
test("int == 5 AND int < 3 is a contradiction", :contradiction, %{Variable.v_int_eq(5) => true, Variable.v_int_lt(3) => true}) })
test("int == 5 AND int > 10 is a contradiction", :contradiction, %{Variable.v_int_eq(5) => true, Variable.v_int_gt(10) => true})
test("int == 5 AND int > 3 is consistent", :consistent, %{Variable.v_int_eq(5) => true, Variable.v_int_gt(3) => true}) test("int < 10 AND int > 20 is a contradiction", :contradiction, %{
Variable.v_int_lt(10) => true,
Variable.v_int_gt(20) => true
})
test("int > 5 AND int < 4 is a contradiction", :contradiction, %{
Variable.v_int_gt(5) => true,
Variable.v_int_lt(4) => true
})
# 6
test("int > 5 AND int < 7 is consistent", :consistent, %{
Variable.v_int_gt(5) => true,
Variable.v_int_lt(7) => true
})
test("int == 5 AND int < 3 is a contradiction", :contradiction, %{
Variable.v_int_eq(5) => true,
Variable.v_int_lt(3) => true
})
test("int == 5 AND int > 10 is a contradiction", :contradiction, %{
Variable.v_int_eq(5) => true,
Variable.v_int_gt(10) => true
})
test("int == 5 AND int > 3 is consistent", :consistent, %{
Variable.v_int_eq(5) => true,
Variable.v_int_gt(3) => true
})
# --- Final Report --- # --- Final Report ---
failures = Process.get(:test_failures, []) failures = Process.get(:test_failures, [])
if failures == [] do if failures == [] do
IO.puts("\n✅ All Consistency.Engine tests passed!") IO.puts("\n✅ All Consistency.Engine tests passed!")
else else
@ -1464,6 +1681,7 @@ defmodule ConsistencyEngineTests do
end end
end end
end end
# defmodule TddAlgoTests do # defmodule TddAlgoTests do
# alias Tdd.Store # alias Tdd.Store
# alias Tdd.Variable # alias Tdd.Variable
@ -1617,9 +1835,10 @@ defmodule TypeReconstructorTests do
expected = TypeSpec.normalize(expected_spec) expected = TypeSpec.normalize(expected_spec)
result = TypeSpec.normalize(TypeReconstructor.spec_from_assumptions(assumptions)) result = TypeSpec.normalize(TypeReconstructor.spec_from_assumptions(assumptions))
is_ok = (expected == result) is_ok = expected == result
status = if is_ok, do: "[PASS]", else: "[FAIL]" status = if is_ok, do: "[PASS]", else: "[FAIL]"
IO.puts("#{status} #{name}") IO.puts("#{status} #{name}")
unless is_ok do unless is_ok do
IO.puts(" Expected: #{inspect(expected)}") IO.puts(" Expected: #{inspect(expected)}")
IO.puts(" Got: #{inspect(result)}") IO.puts(" Got: #{inspect(result)}")
@ -1635,21 +1854,25 @@ defmodule TypeReconstructorTests do
IO.puts("\n--- Section: Basic Flat Reconstructions ---") IO.puts("\n--- Section: Basic Flat Reconstructions ---")
test("is_atom=true -> atom", :atom, %{Variable.v_is_atom() => true}) test("is_atom=true -> atom", :atom, %{Variable.v_is_atom() => true})
test("is_atom=false -> ¬atom", {:negation, :atom}, %{Variable.v_is_atom() => false}) test("is_atom=false -> ¬atom", {:negation, :atom}, %{Variable.v_is_atom() => false})
test( test(
"is_atom=true AND value==:foo -> :foo", "is_atom=true AND value==:foo -> :foo",
{:literal, :foo}, {:literal, :foo},
%{Variable.v_is_atom() => true, Variable.v_atom_eq(:foo) => true} %{Variable.v_is_atom() => true, Variable.v_atom_eq(:foo) => true}
) )
test( test(
"is_atom=true AND value!=:foo -> atom & ¬:foo", "is_atom=true AND value!=:foo -> atom & ¬:foo",
{:intersect, [:atom, {:negation, {:literal, :foo}}]}, {:intersect, [:atom, {:negation, {:literal, :foo}}]},
%{Variable.v_is_atom() => true, Variable.v_atom_eq(:foo) => false} %{Variable.v_is_atom() => true, Variable.v_atom_eq(:foo) => false}
) )
test( test(
"is_integer=true AND int==5 -> 5", "is_integer=true AND int==5 -> 5",
{:literal, 5}, {:literal, 5},
%{Variable.v_is_integer() => true, Variable.v_int_eq(5) => true} %{Variable.v_is_integer() => true, Variable.v_int_eq(5) => true}
) )
test( test(
"is_list=true AND is_empty=true -> []", "is_list=true AND is_empty=true -> []",
{:literal, []}, {:literal, []},
@ -1658,16 +1881,18 @@ defmodule TypeReconstructorTests do
# --- Section: Combined Flat Reconstructions --- # --- Section: Combined Flat Reconstructions ---
IO.puts("\n--- Section: Combined Flat Reconstructions ---") IO.puts("\n--- Section: Combined Flat Reconstructions ---")
test( test(
"int > 10 AND int < 20", "int > 10 AND int < 20",
# This is complex. Our simple reconstructor makes two separate ranges. # This is complex. Our simple reconstructor makes two separate ranges.
# A more advanced one would combine them into a single {:integer_range, 11, 19}. # A more advanced one would combine them into a single {:integer_range, 11, 19}.
# For now, we test the current behavior. # For now, we test the current behavior.
{:intersect, [ {:intersect,
:integer, [
{:integer_range, 11, :pos_inf}, :integer,
{:integer_range, :neg_inf, 19} {:integer_range, 11, :pos_inf},
]}, {:integer_range, :neg_inf, 19}
]},
%{Variable.v_int_gt(10) => true, Variable.v_int_lt(20) => true} %{Variable.v_int_gt(10) => true, Variable.v_int_lt(20) => true}
) )
@ -1688,6 +1913,7 @@ defmodule TypeReconstructorTests do
# --- Final Report --- # --- Final Report ---
failures = Process.get(:test_failures, []) failures = Process.get(:test_failures, [])
if failures == [] do if failures == [] do
IO.puts("\n✅ All TypeReconstructor tests passed!") IO.puts("\n✅ All TypeReconstructor tests passed!")
else else
@ -1695,6 +1921,7 @@ defmodule TypeReconstructorTests do
end end
end end
end end
TypeSpecTests.run() TypeSpecTests.run()
TddStoreTests.run() TddStoreTests.run()
TddVariableTests.run() TddVariableTests.run()