reconstructor & normalization
This commit is contained in:
parent
9f6cd2814a
commit
a5e5127bcd
497
new.exs
497
new.exs
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user