2102 lines
69 KiB
Elixir
2102 lines
69 KiB
Elixir
defmodule Tdd.TypeSpec do
|
|
@moduledoc """
|
|
Defines the `TypeSpec` structure and functions for its manipulation.
|
|
Normalization includes alpha-conversion, beta-reduction, and a final
|
|
canonical renaming pass for bound variables.
|
|
"""
|
|
|
|
# --- Core Types ---
|
|
@type t ::
|
|
:any
|
|
| :none
|
|
| :atom
|
|
| :integer
|
|
| :list
|
|
| :tuple
|
|
| {:literal, term()}
|
|
| {:union, [t()]}
|
|
| {:intersect, [t()]}
|
|
| {:negation, t()}
|
|
| {:tuple, [t()]}
|
|
| {:cons, head :: t(), tail :: t()}
|
|
| {:list_of, element :: t()}
|
|
| {:integer_range, min :: integer() | :neg_inf, max :: integer() | :pos_inf}
|
|
| {:type_var, name :: atom()}
|
|
| {:mu, type_variable_name :: atom(), body :: t()}
|
|
| {:type_lambda, param_names :: [atom()], body :: t()}
|
|
| {:type_apply, constructor_spec :: t(), arg_specs :: [t()]}
|
|
|
|
@doc """
|
|
Converts a `TypeSpec` into its canonical (normalized) form.
|
|
Performs structural normalization, alpha-conversion, beta-reduction,
|
|
and a final canonical renaming pass for all bound variables.
|
|
"""
|
|
@spec normalize(t()) :: t()
|
|
def normalize(spec) do
|
|
{intermediate_normalized, _counter_after_pass1} = normalize_pass1(spec, %{}, 0)
|
|
|
|
{final_spec_before_subtype_redux, _mu_counter, _lambda_counter} =
|
|
canonical_rename_pass(intermediate_normalized, %{}, 0, 0)
|
|
|
|
apply_subtype_reduction(final_spec_before_subtype_redux)
|
|
end
|
|
|
|
# Final pass for subtype-based reductions on fully canonical specs
|
|
defp apply_subtype_reduction(spec) do
|
|
case spec do
|
|
{:union, members} ->
|
|
recursively_reduced_members = Enum.map(members, &apply_subtype_reduction/1)
|
|
|
|
flattened_members =
|
|
Enum.flat_map(recursively_reduced_members, fn
|
|
{:union, sub_members} -> sub_members
|
|
m -> [m]
|
|
end)
|
|
|
|
unique_no_none =
|
|
flattened_members
|
|
|> Enum.reject(&(&1 == :none))
|
|
|> Enum.uniq()
|
|
|
|
if Enum.member?(unique_no_none, :any) do
|
|
:any
|
|
else
|
|
# Pass `true` for already_normalized flag to is_subtype?
|
|
final_members =
|
|
Enum.reject(unique_no_none, fn member_to_check ->
|
|
Enum.any?(unique_no_none, fn other_member ->
|
|
member_to_check != other_member and
|
|
is_subtype?(member_to_check, other_member, true)
|
|
end)
|
|
end)
|
|
|
|
case Enum.sort(final_members) do
|
|
[] -> :none
|
|
[single] -> single
|
|
list_members -> {:union, list_members}
|
|
end
|
|
end
|
|
|
|
{:intersect, members} ->
|
|
recursively_reduced_members = Enum.map(members, &apply_subtype_reduction/1)
|
|
|
|
expanded_flattened_members =
|
|
Enum.flat_map(recursively_reduced_members, fn
|
|
{:intersect, sub_members} -> sub_members
|
|
# get_supertypes expects normalized spec, and its output is also normalized
|
|
# Pass flag
|
|
m -> get_supertypes(m, true)
|
|
end)
|
|
|
|
unique_no_any =
|
|
expanded_flattened_members
|
|
|> Enum.reject(&(&1 == :any))
|
|
|> Enum.uniq()
|
|
|
|
if Enum.member?(unique_no_any, :none) do
|
|
:none
|
|
else
|
|
# Pass `true` for already_normalized flag to is_subtype?
|
|
final_members =
|
|
Enum.reject(unique_no_any, fn member_to_check ->
|
|
Enum.any?(unique_no_any, fn other_member ->
|
|
member_to_check != other_member and
|
|
is_subtype?(other_member, member_to_check, true)
|
|
end)
|
|
end)
|
|
|
|
case Enum.sort(final_members) do
|
|
[] -> :any
|
|
[single] -> single
|
|
list_members -> {:intersect, list_members}
|
|
end
|
|
end
|
|
|
|
{:negation, body} ->
|
|
{:negation, apply_subtype_reduction(body)}
|
|
|
|
{:tuple, elements} ->
|
|
{:tuple, Enum.map(elements, &apply_subtype_reduction/1)}
|
|
|
|
{:cons, head, tail} ->
|
|
{:cons, apply_subtype_reduction(head), apply_subtype_reduction(tail)}
|
|
|
|
{:mu, var_name, body} ->
|
|
{:mu, var_name, apply_subtype_reduction(body)}
|
|
|
|
{:type_lambda, params, body} ->
|
|
{:type_lambda, params, apply_subtype_reduction(body)}
|
|
|
|
{:type_apply, constructor, args} ->
|
|
{:type_apply, apply_subtype_reduction(constructor),
|
|
Enum.map(args, &apply_subtype_reduction/1)}
|
|
|
|
atomic_or_literal ->
|
|
atomic_or_literal
|
|
end
|
|
end
|
|
|
|
# ------------------------------------------------------------------
|
|
# Pass 1: Structural Normalization, Beta-Reduction, Initial Alpha-Conversion
|
|
# Returns: {normalized_spec, next_counter}
|
|
# ------------------------------------------------------------------
|
|
defp normalize_pass1(spec, env, counter) do
|
|
res_tuple =
|
|
case spec do
|
|
s when is_atom(s) and s in [:any, :none, :atom, :integer, :list, :tuple] ->
|
|
{s, counter}
|
|
|
|
{:literal, _val} = lit_spec ->
|
|
{lit_spec, counter}
|
|
|
|
{:type_var, name} ->
|
|
{Map.get(env, name, spec), counter}
|
|
|
|
{:negation, sub_spec} ->
|
|
normalize_negation_pass1(sub_spec, env, counter)
|
|
|
|
{:tuple, elements} ->
|
|
{normalized_elements, next_counter_after_elements} =
|
|
map_fold_counter_for_pass1(elements, env, counter, &normalize_pass1/3)
|
|
|
|
{{:tuple, normalized_elements}, next_counter_after_elements}
|
|
|
|
{:cons, head, tail} ->
|
|
{normalized_head, counter_after_head} = normalize_pass1(head, env, counter)
|
|
{normalized_tail, counter_after_tail} = normalize_pass1(tail, env, counter_after_head)
|
|
{{:cons, normalized_head, normalized_tail}, counter_after_tail}
|
|
|
|
{:integer_range, min, max} ->
|
|
range_spec =
|
|
if is_integer(min) and is_integer(max) and min > max do
|
|
:none
|
|
else
|
|
{:integer_range, min, max}
|
|
end
|
|
|
|
{range_spec, counter}
|
|
|
|
{:union, members} ->
|
|
normalize_union_pass1(members, env, counter)
|
|
|
|
{:intersect, members} ->
|
|
normalize_intersection_pass1(members, env, counter)
|
|
|
|
{:list_of, element_spec} ->
|
|
# We transform `list_of(E)` into a `mu` expression.
|
|
# This expression will then be normalized by a recursive call.
|
|
# First, normalize the element's spec.
|
|
{normalized_element, counter_after_element} =
|
|
normalize_pass1(element_spec, env, counter)
|
|
|
|
# Create a *temporary, non-canonical* name for the recursive variable.
|
|
# The subsequent `normalize_pass1` call on the `mu` form will perform
|
|
# the proper, canonical renaming.
|
|
temp_rec_var = :"$list_of_rec_var"
|
|
|
|
list_body =
|
|
{:union,
|
|
[
|
|
{:literal, []},
|
|
{:cons, normalized_element, {:type_var, temp_rec_var}}
|
|
]}
|
|
|
|
# Now, normalize the full mu-expression. This is the crucial step.
|
|
# It will handle alpha-conversion of `temp_rec_var` and normalization
|
|
# of the body's components.
|
|
normalize_pass1({:mu, temp_rec_var, list_body}, env, counter_after_element)
|
|
|
|
{:mu, var_name, body} ->
|
|
# This logic is correct. It creates a fresh canonical name and
|
|
# adds it to the environment for normalizing the body.
|
|
fresh_temp_name = fresh_var_name(:p1_m_var, counter)
|
|
body_env = Map.put(env, var_name, {:type_var, fresh_temp_name})
|
|
|
|
{normalized_body, next_counter_after_body} =
|
|
normalize_pass1(body, body_env, counter + 1)
|
|
|
|
{{:mu, fresh_temp_name, normalized_body}, next_counter_after_body}
|
|
|
|
{:type_lambda, param_names, body} ->
|
|
{reversed_fresh_temp_names, next_counter_after_params, body_env} =
|
|
Enum.reduce(param_names, {[], counter, env}, fn param_name,
|
|
{acc_fresh_names, cnt, current_env} ->
|
|
fresh_name = fresh_var_name(:p1_lambda_var, cnt)
|
|
|
|
{[fresh_name | acc_fresh_names], cnt + 1,
|
|
Map.put(current_env, param_name, {:type_var, fresh_name})}
|
|
end)
|
|
|
|
fresh_temp_param_names = Enum.reverse(reversed_fresh_temp_names)
|
|
|
|
{normalized_body, final_counter} =
|
|
normalize_pass1(body, body_env, next_counter_after_params)
|
|
|
|
{{:type_lambda, fresh_temp_param_names, normalized_body}, final_counter}
|
|
|
|
{:type_apply, constructor_spec, arg_specs} ->
|
|
{normalized_constructor, counter_after_constructor} =
|
|
normalize_pass1(constructor_spec, env, counter)
|
|
|
|
{normalized_arg_specs, counter_after_args} =
|
|
map_fold_counter_for_pass1(
|
|
arg_specs,
|
|
env,
|
|
counter_after_constructor,
|
|
&normalize_pass1/3
|
|
)
|
|
|
|
case normalized_constructor do
|
|
{:type_lambda, pass1_formal_params, pass1_lambda_body} ->
|
|
if length(pass1_formal_params) != length(normalized_arg_specs) do
|
|
raise "TypeSpec.normalize_pass1: Arity mismatch in application. Expected #{length(pass1_formal_params)} args, got #{length(normalized_arg_specs)}. Lambda: #{inspect(normalized_constructor)}, Args: #{inspect(normalized_arg_specs)}"
|
|
else
|
|
substitution_map = Map.new(Enum.zip(pass1_formal_params, normalized_arg_specs))
|
|
|
|
substituted_body =
|
|
substitute_vars_pass1(pass1_lambda_body, substitution_map, MapSet.new())
|
|
|
|
normalize_pass1(substituted_body, env, counter_after_args)
|
|
end
|
|
|
|
_other_constructor ->
|
|
{{:type_apply, normalized_constructor, normalized_arg_specs}, counter_after_args}
|
|
end
|
|
|
|
other_spec ->
|
|
raise "TypeSpec.normalize_pass1: Unhandled spec form: #{inspect(other_spec)}"
|
|
end
|
|
|
|
res_tuple
|
|
end
|
|
|
|
defp map_fold_counter_for_pass1(list, env, initial_counter, fun) do
|
|
Enum.map_reduce(list, initial_counter, fn item, acc_counter ->
|
|
fun.(item, env, acc_counter)
|
|
end)
|
|
end
|
|
|
|
defp substitute_vars_pass1(spec, substitutions, bound_in_scope) do
|
|
case spec do
|
|
{:type_var, name} ->
|
|
if MapSet.member?(bound_in_scope, name) do
|
|
spec
|
|
else
|
|
Map.get(substitutions, name, spec)
|
|
end
|
|
|
|
{:mu, var_name, body} ->
|
|
newly_bound_scope = MapSet.put(bound_in_scope, var_name)
|
|
active_substitutions = Map.delete(substitutions, var_name)
|
|
{:mu, var_name, substitute_vars_pass1(body, active_substitutions, newly_bound_scope)}
|
|
|
|
{:type_lambda, param_names, body} ->
|
|
newly_bound_scope = Enum.reduce(param_names, bound_in_scope, &MapSet.put(&2, &1))
|
|
active_substitutions = Enum.reduce(param_names, substitutions, &Map.delete(&2, &1))
|
|
|
|
{:type_lambda, param_names,
|
|
substitute_vars_pass1(body, active_substitutions, newly_bound_scope)}
|
|
|
|
{:negation, sub} ->
|
|
{:negation, substitute_vars_pass1(sub, substitutions, bound_in_scope)}
|
|
|
|
{:tuple, elements} ->
|
|
{:tuple, Enum.map(elements, &substitute_vars_pass1(&1, substitutions, bound_in_scope))}
|
|
|
|
{:cons, h, t} ->
|
|
{:cons, substitute_vars_pass1(h, substitutions, bound_in_scope),
|
|
substitute_vars_pass1(t, substitutions, bound_in_scope)}
|
|
|
|
{:list_of, e} ->
|
|
{:list_of, substitute_vars_pass1(e, substitutions, bound_in_scope)}
|
|
|
|
{:union, members} ->
|
|
{:union, Enum.map(members, &substitute_vars_pass1(&1, substitutions, bound_in_scope))}
|
|
|
|
{:intersect, members} ->
|
|
{:intersect, Enum.map(members, &substitute_vars_pass1(&1, substitutions, bound_in_scope))}
|
|
|
|
{:type_apply, con, args} ->
|
|
new_con = substitute_vars_pass1(con, substitutions, bound_in_scope)
|
|
new_args = Enum.map(args, &substitute_vars_pass1(&1, substitutions, bound_in_scope))
|
|
{:type_apply, new_con, new_args}
|
|
|
|
_atomic_or_simple_spec ->
|
|
spec
|
|
end
|
|
end
|
|
|
|
defp normalize_negation_pass1(sub_spec, env, counter) do
|
|
{normalized_sub, next_counter} = normalize_pass1(sub_spec, env, counter)
|
|
|
|
res_spec =
|
|
case normalized_sub do
|
|
{:negation, inner_spec} -> inner_spec
|
|
:any -> :none
|
|
:none -> :any
|
|
_ -> {:negation, normalized_sub}
|
|
end
|
|
|
|
{res_spec, next_counter}
|
|
end
|
|
|
|
defp normalize_union_pass1(members, env, initial_counter) do
|
|
{list_of_normalized_member_lists, final_counter_after_all_members} =
|
|
Enum.map_reduce(members, initial_counter, fn member_spec, current_processing_counter ->
|
|
{normalized_member_spec_term, counter_after_this_member_normalized} =
|
|
normalize_pass1(member_spec, env, current_processing_counter)
|
|
|
|
members_to_add_to_overall_list =
|
|
case normalized_member_spec_term do
|
|
{:union, sub_members} -> sub_members
|
|
_ -> [normalized_member_spec_term]
|
|
end
|
|
|
|
{members_to_add_to_overall_list, counter_after_this_member_normalized}
|
|
end)
|
|
|
|
normalized_and_flattened = List.flatten(list_of_normalized_member_lists)
|
|
|
|
unique_members =
|
|
normalized_and_flattened
|
|
|> Enum.reject(&(&1 == :none))
|
|
|> Enum.uniq()
|
|
|
|
if Enum.member?(unique_members, :any) do
|
|
{:any, final_counter_after_all_members}
|
|
else
|
|
sorted_for_pass1 = Enum.sort(unique_members)
|
|
|
|
resulting_spec =
|
|
case sorted_for_pass1 do
|
|
[] -> :none
|
|
[single_member] -> single_member
|
|
list_members -> {:union, list_members}
|
|
end
|
|
|
|
{resulting_spec, final_counter_after_all_members}
|
|
end
|
|
end
|
|
|
|
defp normalize_intersection_pass1(members, env, initial_counter) do
|
|
{list_of_member_groups, final_counter_after_all_members} =
|
|
Enum.map_reduce(members, initial_counter, fn member_spec, current_processing_counter ->
|
|
{normalized_member_spec_term, counter_after_this_member_normalized} =
|
|
normalize_pass1(member_spec, env, current_processing_counter)
|
|
|
|
expanded_members =
|
|
case normalized_member_spec_term do
|
|
{:intersect, sub_members} -> sub_members
|
|
_ -> get_supertypes_pass1(normalized_member_spec_term)
|
|
end
|
|
|
|
{expanded_members, counter_after_this_member_normalized}
|
|
end)
|
|
|
|
normalized_and_flattened_with_supertypes = List.flatten(list_of_member_groups)
|
|
|
|
unique_members =
|
|
normalized_and_flattened_with_supertypes
|
|
|> Enum.reject(&(&1 == :any))
|
|
|> Enum.uniq()
|
|
|
|
if Enum.member?(unique_members, :none) do
|
|
{:none, final_counter_after_all_members}
|
|
else
|
|
sorted_for_pass1 = Enum.sort(unique_members)
|
|
|
|
resulting_spec =
|
|
case sorted_for_pass1 do
|
|
[] -> :any
|
|
[single_member] -> single_member
|
|
list_members -> {:intersect, list_members}
|
|
end
|
|
|
|
{resulting_spec, final_counter_after_all_members}
|
|
end
|
|
end
|
|
|
|
defp get_supertypes_pass1(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]
|
|
{:mu, _v, _body} -> []
|
|
{:tuple, _} -> [:tuple]
|
|
{:integer_range, _, _} -> [:integer]
|
|
_ -> []
|
|
end
|
|
|
|
MapSet.to_list(MapSet.new([spec | supertypes]))
|
|
end
|
|
|
|
defp canonical_rename_pass(spec, env, mu_c, lambda_c) do
|
|
case spec do
|
|
{:mu, old_var_name, body} ->
|
|
new_canonical_name = fresh_var_name(:m_var, mu_c)
|
|
body_env = Map.put(env, old_var_name, {:type_var, new_canonical_name})
|
|
|
|
{renamed_body, next_mu_c, next_lambda_c} =
|
|
canonical_rename_pass(body, body_env, mu_c + 1, lambda_c)
|
|
|
|
{{:mu, new_canonical_name, renamed_body}, next_mu_c, next_lambda_c}
|
|
|
|
{:type_lambda, old_param_names, body} ->
|
|
{reversed_new_param_names, next_lambda_c_after_params, body_env} =
|
|
Enum.reduce(old_param_names, {[], lambda_c, env}, fn old_name,
|
|
{acc_new_names, current_lc,
|
|
current_env} ->
|
|
fresh_canonical_name = fresh_var_name(:lambda_var, current_lc)
|
|
|
|
{[fresh_canonical_name | acc_new_names], current_lc + 1,
|
|
Map.put(current_env, old_name, {:type_var, fresh_canonical_name})}
|
|
end)
|
|
|
|
new_canonical_param_names = Enum.reverse(reversed_new_param_names)
|
|
|
|
{renamed_body, final_mu_c, final_lambda_c} =
|
|
canonical_rename_pass(body, body_env, mu_c, next_lambda_c_after_params)
|
|
|
|
{{:type_lambda, new_canonical_param_names, renamed_body}, final_mu_c, final_lambda_c}
|
|
|
|
{:type_var, name} ->
|
|
{Map.get(env, name, spec), mu_c, lambda_c}
|
|
|
|
{:negation, sub_spec} ->
|
|
{renamed_sub, nmc, nlc} = canonical_rename_pass(sub_spec, env, mu_c, lambda_c)
|
|
{{:negation, renamed_sub}, nmc, nlc}
|
|
|
|
{:tuple, elements} ->
|
|
{renamed_elements, next_mu_c, next_lambda_c} =
|
|
map_foldl_counters_for_rename(elements, env, mu_c, lambda_c, &canonical_rename_pass/4)
|
|
|
|
{{:tuple, renamed_elements}, next_mu_c, next_lambda_c}
|
|
|
|
{:cons, head, tail} ->
|
|
{renamed_head, mu_c_after_head, lambda_c_after_head} =
|
|
canonical_rename_pass(head, env, mu_c, lambda_c)
|
|
|
|
{renamed_tail, mu_c_after_tail, lambda_c_after_tail} =
|
|
canonical_rename_pass(tail, env, mu_c_after_head, lambda_c_after_head)
|
|
|
|
{{:cons, renamed_head, renamed_tail}, mu_c_after_tail, lambda_c_after_tail}
|
|
|
|
{:union, members} ->
|
|
sorted_members = Enum.sort(members)
|
|
|
|
{renamed_members, next_mu_c, next_lambda_c} =
|
|
map_foldl_counters_for_rename(
|
|
sorted_members,
|
|
env,
|
|
mu_c,
|
|
lambda_c,
|
|
&canonical_rename_pass/4
|
|
)
|
|
|
|
{{:union, Enum.sort(renamed_members)}, next_mu_c, next_lambda_c}
|
|
|
|
{:intersect, members} ->
|
|
sorted_members = Enum.sort(members)
|
|
|
|
{renamed_members, next_mu_c, next_lambda_c} =
|
|
map_foldl_counters_for_rename(
|
|
sorted_members,
|
|
env,
|
|
mu_c,
|
|
lambda_c,
|
|
&canonical_rename_pass/4
|
|
)
|
|
|
|
{{:intersect, Enum.sort(renamed_members)}, next_mu_c, next_lambda_c}
|
|
|
|
{:type_apply, constructor_spec, arg_specs} ->
|
|
{renamed_constructor, mu_c_after_con, lambda_c_after_con} =
|
|
canonical_rename_pass(constructor_spec, env, mu_c, lambda_c)
|
|
|
|
{renamed_args, mu_c_after_args, lambda_c_after_args} =
|
|
map_foldl_counters_for_rename(
|
|
arg_specs,
|
|
env,
|
|
mu_c_after_con,
|
|
lambda_c_after_con,
|
|
&canonical_rename_pass/4
|
|
)
|
|
|
|
{{:type_apply, renamed_constructor, renamed_args}, mu_c_after_args, lambda_c_after_args}
|
|
|
|
s when is_atom(s) ->
|
|
{s, mu_c, lambda_c}
|
|
|
|
{:literal, _} = spec ->
|
|
{spec, mu_c, lambda_c}
|
|
|
|
{:integer_range, _, _} = spec ->
|
|
{spec, mu_c, lambda_c}
|
|
|
|
{:list_of, _} = spec ->
|
|
raise "TypeSpec.canonical_rename_pass: Unexpected :list_of, should be :mu. Spec: #{inspect(spec)}"
|
|
|
|
_other ->
|
|
raise "TypeSpec.canonical_rename_pass: Unhandled spec form: #{inspect(spec)}"
|
|
end
|
|
end
|
|
|
|
defp map_foldl_counters_for_rename(list, env, initial_mu_c, initial_lambda_c, fun) do
|
|
{reversed_results, final_mu_c, final_lambda_c} =
|
|
Enum.reduce(list, {[], initial_mu_c, initial_lambda_c}, fn item, {acc_items, mc, lc} ->
|
|
{processed_item, next_mc, next_lc} = fun.(item, env, mc, lc)
|
|
{[processed_item | acc_items], next_mc, next_lc}
|
|
end)
|
|
|
|
{Enum.reverse(reversed_results), final_mu_c, final_lambda_c}
|
|
end
|
|
|
|
defp fresh_var_name(prefix_atom, counter) do
|
|
:"#{Atom.to_string(prefix_atom)}#{counter}"
|
|
end
|
|
|
|
# Public API
|
|
@spec is_subtype?(t(), t()) :: boolean
|
|
def is_subtype?(spec1, spec2), do: is_subtype?(spec1, spec2, false)
|
|
|
|
# Internal helper with already_normalized flag
|
|
@spec is_subtype?(t(), t(), boolean) :: boolean
|
|
def is_subtype?(spec1, spec2, already_normalized) do
|
|
cond do
|
|
spec1 == spec2 ->
|
|
true
|
|
|
|
spec1 == :none ->
|
|
true
|
|
|
|
spec2 == :any ->
|
|
true
|
|
|
|
spec1 == :any and spec2 != :any ->
|
|
false
|
|
|
|
spec2 == :none and spec1 != :none ->
|
|
false
|
|
|
|
true ->
|
|
{norm_s1, norm_s2} =
|
|
if already_normalized do
|
|
{spec1, spec2}
|
|
else
|
|
{normalize(spec1), normalize(spec2)}
|
|
end
|
|
|
|
if norm_s1 == norm_s2 do
|
|
true
|
|
else
|
|
do_is_subtype_structural?(norm_s1, norm_s2, MapSet.new())
|
|
end
|
|
end
|
|
end
|
|
|
|
defp do_is_subtype_structural?(spec1, spec2, visited) do
|
|
if MapSet.member?(visited, {spec1, spec2}) do
|
|
true
|
|
else
|
|
cond do
|
|
spec1 == :none ->
|
|
true
|
|
|
|
spec2 == :any ->
|
|
true
|
|
|
|
spec1 == :any and spec2 != :any ->
|
|
false
|
|
|
|
spec2 == :none and spec1 != :none ->
|
|
false
|
|
|
|
spec1 == spec2 ->
|
|
true
|
|
|
|
true ->
|
|
new_visited = MapSet.put(visited, {spec1, spec2})
|
|
|
|
case {spec1, spec2} do
|
|
{{:union, members1}, _} ->
|
|
Enum.all?(members1, &do_is_subtype_structural?(&1, spec2, new_visited))
|
|
|
|
{_, {:union, members2}} ->
|
|
Enum.any?(members2, &do_is_subtype_structural?(spec1, &1, new_visited))
|
|
|
|
{{:intersect, members1}, _} ->
|
|
Enum.any?(members1, &do_is_subtype_structural?(&1, spec2, new_visited))
|
|
|
|
{_, {:intersect, members2}} ->
|
|
Enum.all?(members2, &do_is_subtype_structural?(spec1, &1, new_visited))
|
|
|
|
{s1, s2}
|
|
when is_atom(s1) and is_atom(s2) and s1 not in [:any, :none] and
|
|
s2 not in [:any, :none] ->
|
|
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
|
|
|
|
{{:tuple, elems1}, {:tuple, elems2}} when length(elems1) == length(elems2) ->
|
|
Enum.zip_with(elems1, elems2, &do_is_subtype_structural?(&1, &2, new_visited))
|
|
|> Enum.all?()
|
|
|
|
{{:tuple, _}, :tuple} ->
|
|
true
|
|
|
|
{{:integer_range, _, _}, :integer} ->
|
|
true
|
|
|
|
{{:integer_range, min1, max1}, {:integer_range, min2, max2}} ->
|
|
min1_gte_min2 =
|
|
case {min1, min2} do
|
|
{:neg_inf, _} -> min2 == :neg_inf
|
|
{_, :neg_inf} -> true
|
|
{m1_v, m2_v} when is_integer(m1_v) and is_integer(m2_v) -> m1_v >= m2_v
|
|
_ -> false
|
|
end
|
|
|
|
max1_lte_max2 =
|
|
case {max1, max2} do
|
|
{:pos_inf, _} -> max2 == :pos_inf
|
|
{_, :pos_inf} -> true
|
|
{m1_v, m2_v} when is_integer(m1_v) and is_integer(m2_v) -> m1_v <= m2_v
|
|
_ -> false
|
|
end
|
|
|
|
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)
|
|
|
|
{{:mu, v1, b1_body}, {:mu, v2, b2_body}} ->
|
|
cond do
|
|
is_list_mu_form(b1_body, v1) and is_list_mu_form(b2_body, v2) ->
|
|
e1 = extract_list_mu_element(b1_body, v1)
|
|
e2 = extract_list_mu_element(b2_body, v2)
|
|
do_is_subtype_structural?(e1, e2, new_visited)
|
|
|
|
true ->
|
|
unfolded_b1 = substitute_vars_canonical(b1_body, %{v1 => spec1})
|
|
do_is_subtype_structural?(unfolded_b1, spec2, new_visited)
|
|
end
|
|
|
|
{_non_mu_spec, {:mu, v2, b2_body} = mu_spec2} ->
|
|
unfolded_b2 = substitute_vars_canonical(b2_body, %{v2 => mu_spec2})
|
|
do_is_subtype_structural?(spec1, unfolded_b2, new_visited)
|
|
|
|
{{:mu, v1, b1_body} = mu_spec1, _non_mu_spec} ->
|
|
unfolded_b1 = substitute_vars_canonical(b1_body, %{v1 => mu_spec1})
|
|
do_is_subtype_structural?(unfolded_b1, spec2, new_visited)
|
|
|
|
{{:negation, n_body1}, {:negation, n_body2}} ->
|
|
do_is_subtype_structural?(n_body2, n_body1, new_visited)
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp substitute_vars_canonical(spec, substitutions) do
|
|
case spec do
|
|
{:type_var, name} ->
|
|
Map.get(substitutions, name, spec)
|
|
|
|
{:mu, var_name, body} ->
|
|
active_substitutions = Map.delete(substitutions, var_name)
|
|
{:mu, var_name, substitute_vars_canonical(body, active_substitutions)}
|
|
|
|
{:type_lambda, param_names, body} ->
|
|
active_substitutions = Enum.reduce(param_names, substitutions, &Map.delete(&2, &1))
|
|
{:type_lambda, param_names, substitute_vars_canonical(body, active_substitutions)}
|
|
|
|
{:negation, sub} ->
|
|
{:negation, substitute_vars_canonical(sub, substitutions)}
|
|
|
|
{:tuple, elements} ->
|
|
{:tuple, Enum.map(elements, &substitute_vars_canonical(&1, substitutions))}
|
|
|
|
{:cons, h, t} ->
|
|
{:cons, substitute_vars_canonical(h, substitutions),
|
|
substitute_vars_canonical(t, substitutions)}
|
|
|
|
{:list_of, e} ->
|
|
{:list_of, substitute_vars_canonical(e, substitutions)}
|
|
|
|
{:union, members} ->
|
|
{:union, Enum.map(members, &substitute_vars_canonical(&1, substitutions))}
|
|
|
|
{:intersect, members} ->
|
|
{:intersect, Enum.map(members, &substitute_vars_canonical(&1, substitutions))}
|
|
|
|
{:type_apply, con, args} ->
|
|
new_con = substitute_vars_canonical(con, substitutions)
|
|
new_args = Enum.map(args, &substitute_vars_canonical(&1, substitutions))
|
|
{:type_apply, new_con, new_args}
|
|
|
|
_atomic_or_simple_spec ->
|
|
spec
|
|
end
|
|
end
|
|
|
|
defp is_list_mu_form({:union, members}, rec_var_name) do
|
|
sorted_members = Enum.sort(members)
|
|
|
|
match?([{:literal, []}, {:cons, _elem, {:type_var, ^rec_var_name}}], sorted_members) or
|
|
match?([{:cons, _elem, {:type_var, ^rec_var_name}}, {:literal, []}], sorted_members)
|
|
end
|
|
|
|
defp is_list_mu_form(_, _), do: false
|
|
|
|
defp extract_list_mu_element({:union, members}, rec_var_name) do
|
|
Enum.find_value(members, fn
|
|
{:cons, elem_spec, {:type_var, ^rec_var_name}} -> elem_spec
|
|
_ -> nil
|
|
end) || :any
|
|
end
|
|
|
|
# Public API for get_supertypes
|
|
def get_supertypes(spec), do: get_supertypes(spec, false)
|
|
|
|
# Internal helper for get_supertypes
|
|
defp get_supertypes(spec_input, already_normalized) do
|
|
fully_normalized_spec = if already_normalized, do: spec_input, else: normalize(spec_input)
|
|
|
|
supertypes =
|
|
case fully_normalized_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]
|
|
{:mu, v, body} -> if is_list_mu_form(body, v), do: [:list], else: []
|
|
{:tuple, _} -> [:tuple]
|
|
{:integer_range, _, _} -> [:integer]
|
|
_ -> []
|
|
end
|
|
|
|
MapSet.to_list(MapSet.new([fully_normalized_spec | supertypes]))
|
|
end
|
|
end
|
|
|
|
defmodule Tdd.Store do
|
|
@moduledoc """
|
|
Manages the state of the TDD system's node graph and operation cache.
|
|
"""
|
|
|
|
# --- State Keys ---
|
|
@nodes_key :tdd_nodes
|
|
@node_by_id_key :tdd_node_by_id
|
|
@next_id_key :tdd_next_id
|
|
@op_cache_key :tdd_op_cache
|
|
|
|
# --- Terminal Node IDs ---
|
|
@false_node_id 0
|
|
@true_node_id 1
|
|
|
|
# --- Public API ---
|
|
|
|
@doc "Initializes the TDD store in the current process."
|
|
def init do
|
|
Process.put(@nodes_key, %{})
|
|
|
|
Process.put(@node_by_id_key, %{
|
|
@false_node_id => :false_terminal,
|
|
@true_node_id => :true_terminal
|
|
})
|
|
|
|
Process.put(@next_id_key, 2)
|
|
Process.put(@op_cache_key, %{})
|
|
:ok
|
|
end
|
|
|
|
@doc "Returns the ID for the TRUE terminal node (the 'any' type)."
|
|
@spec true_node_id() :: non_neg_integer()
|
|
def true_node_id, do: @true_node_id
|
|
|
|
@doc "Returns the ID for the FALSE terminal node (the 'none' type)."
|
|
@spec false_node_id() :: non_neg_integer()
|
|
def false_node_id, do: @false_node_id
|
|
|
|
@doc "Retrieves the details of a node by its ID."
|
|
@spec get_node(non_neg_integer()) ::
|
|
{:ok,
|
|
{variable :: term(), yes_id :: non_neg_integer(), no_id :: non_neg_integer(),
|
|
dc_id :: non_neg_integer()}}
|
|
| {:ok, :true_terminal | :false_terminal}
|
|
| {:error, :not_found}
|
|
def get_node(id) do
|
|
case Process.get(@node_by_id_key, %{}) do
|
|
%{^id => details} -> {:ok, details}
|
|
%{^id => {:placeholder, _spec}} -> {:ok, {:placeholder, "Knot-tying placeholder"}}
|
|
%{} -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Finds an existing node that matches the structure or creates a new one.
|
|
"""
|
|
@spec find_or_create_node(
|
|
variable :: term(),
|
|
yes_id :: non_neg_integer(),
|
|
no_id :: non_neg_integer(),
|
|
dc_id :: non_neg_integer()
|
|
) :: non_neg_integer()
|
|
def find_or_create_node(variable, yes_id, no_id, dc_id) do
|
|
if yes_id == no_id && yes_id == dc_id do
|
|
yes_id
|
|
else
|
|
node_tuple = {variable, yes_id, no_id, dc_id}
|
|
nodes = Process.get(@nodes_key, %{})
|
|
|
|
case Map.get(nodes, node_tuple) do
|
|
id when is_integer(id) ->
|
|
id
|
|
|
|
nil ->
|
|
next_id = Process.get(@next_id_key)
|
|
node_by_id = Process.get(@node_by_id_key)
|
|
|
|
Process.put(@nodes_key, Map.put(nodes, node_tuple, next_id))
|
|
Process.put(@node_by_id_key, Map.put(node_by_id, next_id, node_tuple))
|
|
Process.put(@next_id_key, next_id + 1)
|
|
|
|
next_id
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc "Retrieves a result from the operation cache."
|
|
@spec get_op_cache(term()) :: {:ok, term()} | :not_found
|
|
def get_op_cache(cache_key) do
|
|
case Process.get(@op_cache_key, %{}) do
|
|
%{^cache_key => result} -> {:ok, result}
|
|
%{} -> :not_found
|
|
end
|
|
end
|
|
|
|
@doc "Puts a result into the operation cache."
|
|
@spec put_op_cache(term(), term()) :: :ok
|
|
def put_op_cache(cache_key, result) do
|
|
cache = Process.get(@op_cache_key, %{})
|
|
Process.put(@op_cache_key, Map.put(cache, cache_key, result))
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Creates a unique, temporary placeholder node for a recursive spec.
|
|
Returns the ID of this placeholder.
|
|
"""
|
|
@spec create_placeholder(TypeSpec.t()) :: non_neg_integer()
|
|
def create_placeholder(spec) do
|
|
next_id = Process.get(@next_id_key)
|
|
node_by_id = Process.get(@node_by_id_key)
|
|
|
|
# Associate the ID with a temporary detail for debugging and identification.
|
|
# This does NOT create an entry in the main `nodes` (reverse-lookup) map.
|
|
Process.put(@node_by_id_key, Map.put(node_by_id, next_id, {:placeholder, spec}))
|
|
Process.put(@next_id_key, next_id + 1)
|
|
|
|
next_id
|
|
end
|
|
|
|
@doc """
|
|
Updates a node's details directly. Used for knot-tying.
|
|
"""
|
|
@spec update_node_in_place(
|
|
non_neg_integer(),
|
|
new_details ::
|
|
{:ok,
|
|
{term(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
|
|
| :true_terminal
|
|
| :false_terminal}
|
|
) :: :ok
|
|
def update_node_in_place(id, {:ok, new_details}) do
|
|
nodes = Process.get(@nodes_key)
|
|
node_by_id = Process.get(@node_by_id_key)
|
|
|
|
# The old_details will be `{:placeholder, ...}`
|
|
# which does not exist as a key in the `nodes` map, so Map.delete is a no-op.
|
|
old_details = Map.get(node_by_id, id)
|
|
nodes = Map.delete(nodes, old_details)
|
|
|
|
nodes =
|
|
case new_details do
|
|
{_v, _y, _n, _d} -> Map.put(nodes, new_details, id)
|
|
_ -> nodes
|
|
end
|
|
|
|
node_by_id = Map.put(node_by_id, id, new_details)
|
|
|
|
Process.put(@nodes_key, nodes)
|
|
Process.put(@node_by_id_key, node_by_id)
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defmodule Tdd.Variable do
|
|
@moduledoc """
|
|
Defines the canonical structure for all Tdd predicate variables.
|
|
"""
|
|
|
|
# --- Category 0: Primary Type Discriminators ---
|
|
@spec v_is_atom() :: term()
|
|
def v_is_atom, do: {0, :is_atom, nil, nil}
|
|
|
|
@spec v_is_integer() :: term()
|
|
def v_is_integer, do: {0, :is_integer, nil, nil}
|
|
|
|
@spec v_is_list() :: term()
|
|
def v_is_list, do: {0, :is_list, nil, nil}
|
|
|
|
@spec v_is_tuple() :: term()
|
|
def v_is_tuple, do: {0, :is_tuple, nil, nil}
|
|
|
|
# --- Category 1: Atom Properties ---
|
|
@spec v_atom_eq(atom()) :: term()
|
|
def v_atom_eq(atom_val) when is_atom(atom_val), do: {1, :value, atom_val, nil}
|
|
|
|
# --- Category 2: Integer Properties ---
|
|
@spec v_int_lt(integer()) :: term()
|
|
def v_int_lt(n) when is_integer(n), do: {2, :alt, n, nil}
|
|
|
|
@spec v_int_eq(integer()) :: term()
|
|
def v_int_eq(n) when is_integer(n), do: {2, :beq, n, nil}
|
|
|
|
@spec v_int_gt(integer()) :: term()
|
|
def v_int_gt(n) when is_integer(n), do: {2, :cgt, n, nil}
|
|
|
|
# --- Category 4: Tuple Properties ---
|
|
@spec v_tuple_size_eq(non_neg_integer()) :: term()
|
|
def v_tuple_size_eq(size) when is_integer(size) and size >= 0, do: {4, :a_size, size, nil}
|
|
|
|
@doc "Applies a predicate to a tuple element. The predicate is now represented by its TDD ID."
|
|
@spec v_tuple_elem_pred(non_neg_integer(), sub_problem_tdd_id :: non_neg_integer()) :: term()
|
|
def v_tuple_elem_pred(index, sub_problem_tdd_id)
|
|
when is_integer(index) and index >= 0 and is_integer(sub_problem_tdd_id) do
|
|
{4, :b_element, index, sub_problem_tdd_id}
|
|
end
|
|
|
|
# --- Category 5: List Properties ---
|
|
@doc "Predicate: The list is the empty list `[]`."
|
|
@spec v_list_is_empty() :: term()
|
|
def v_list_is_empty, do: {5, :b_is_empty, nil, nil}
|
|
|
|
@doc "Applies a predicate to the head. The predicate is now represented by its TDD ID."
|
|
@spec v_list_head_pred(sub_problem_tdd_id :: non_neg_integer()) :: term()
|
|
def v_list_head_pred(sub_problem_tdd_id) when is_integer(sub_problem_tdd_id),
|
|
do: {5, :c_head, sub_problem_tdd_id, nil}
|
|
|
|
@doc "Applies a predicate to the tail. The predicate is now represented by its TDD ID."
|
|
@spec v_list_tail_pred(sub_problem_tdd_id :: non_neg_integer()) :: term()
|
|
def v_list_tail_pred(sub_problem_tdd_id) when is_integer(sub_problem_tdd_id),
|
|
do: {5, :d_tail, sub_problem_tdd_id, nil}
|
|
end
|
|
|
|
defmodule Tdd.Predicate.Info do
|
|
@moduledoc "A knowledge base for the properties of TDD predicate variables."
|
|
alias Tdd.Variable
|
|
|
|
@doc "Returns a map of traits for a given predicate variable."
|
|
@spec get_traits(term()) :: map() | nil
|
|
|
|
def get_traits({0, :is_atom, _, _}) do
|
|
%{
|
|
type: :primary,
|
|
category: :atom,
|
|
implies: [
|
|
{Variable.v_is_integer(), false},
|
|
{Variable.v_is_list(), false},
|
|
{Variable.v_is_tuple(), false}
|
|
]
|
|
}
|
|
end
|
|
|
|
def get_traits({0, :is_integer, _, _}) do
|
|
%{
|
|
type: :primary,
|
|
category: :integer,
|
|
implies: [
|
|
{Variable.v_is_atom(), false},
|
|
{Variable.v_is_list(), false},
|
|
{Variable.v_is_tuple(), false}
|
|
]
|
|
}
|
|
end
|
|
|
|
def get_traits({0, :is_list, _, _}) do
|
|
%{
|
|
type: :primary,
|
|
category: :list,
|
|
implies: [
|
|
{Variable.v_is_atom(), false},
|
|
{Variable.v_is_integer(), false},
|
|
{Variable.v_is_tuple(), false}
|
|
]
|
|
}
|
|
end
|
|
|
|
def get_traits({0, :is_tuple, _, _}) do
|
|
%{
|
|
type: :primary,
|
|
category: :tuple,
|
|
implies: [
|
|
{Variable.v_is_atom(), false},
|
|
{Variable.v_is_integer(), false},
|
|
{Variable.v_is_list(), false}
|
|
]
|
|
}
|
|
end
|
|
|
|
def get_traits({1, :value, _val, _}) do
|
|
%{type: :atom_value, category: :atom, implies: [{Variable.v_is_atom(), true}]}
|
|
end
|
|
|
|
def get_traits({2, :alt, _, _}),
|
|
do: %{type: :integer_prop, category: :integer, implies: [{Variable.v_is_integer(), true}]}
|
|
|
|
def get_traits({2, :beq, _, _}),
|
|
do: %{type: :integer_prop, category: :integer, implies: [{Variable.v_is_integer(), true}]}
|
|
|
|
def get_traits({2, :cgt, _, _}),
|
|
do: %{type: :integer_prop, category: :integer, implies: [{Variable.v_is_integer(), true}]}
|
|
|
|
def get_traits({4, :a_size, _, _}) do
|
|
%{type: :tuple_prop, category: :tuple, implies: [{Variable.v_is_tuple(), true}]}
|
|
end
|
|
|
|
def get_traits({4, :b_element, index, _tdd_id}) do
|
|
%{
|
|
type: :tuple_recursive,
|
|
category: :tuple,
|
|
sub_key: {:elem, index},
|
|
implies: [{Variable.v_is_tuple(), true}]
|
|
}
|
|
end
|
|
|
|
def get_traits({5, :b_is_empty, _, _}) do
|
|
%{type: :list_prop, category: :list, implies: [{Variable.v_is_list(), true}]}
|
|
end
|
|
|
|
def get_traits({5, :c_head, _tdd_id, _}) do
|
|
%{
|
|
type: :list_recursive,
|
|
category: :list,
|
|
sub_key: :head,
|
|
implies: [{Variable.v_is_list(), true}, {Variable.v_list_is_empty(), false}]
|
|
}
|
|
end
|
|
|
|
def get_traits({5, :d_tail, _tdd_id, _}) do
|
|
%{
|
|
type: :list_recursive,
|
|
category: :list,
|
|
sub_key: :tail,
|
|
implies: [{Variable.v_is_list(), true}, {Variable.v_list_is_empty(), false}]
|
|
}
|
|
end
|
|
|
|
def get_traits(_), do: nil
|
|
end
|
|
|
|
defmodule Tdd.Consistency.Engine do
|
|
@moduledoc """
|
|
A rule-based engine for checking the semantic consistency of a set of assumptions.
|
|
"""
|
|
alias Tdd.Predicate.Info
|
|
alias Tdd.Variable
|
|
|
|
@doc "Checks if a map of assumptions is logically consistent."
|
|
@spec check(map()) :: :consistent | :contradiction
|
|
def check(assumptions), do: do_check(assumptions)
|
|
|
|
@doc "Expands a map of assumptions with all their logical implications."
|
|
@spec expand(map()) :: {:ok, map()} | {:error, :contradiction}
|
|
def expand(assumptions), do: expand_with_implications(assumptions)
|
|
|
|
# --- The Core Recursive Checker ---
|
|
defp do_check(assumptions) do
|
|
with {:ok, expanded} <- expand_with_implications(assumptions),
|
|
:ok <- check_flat_consistency(expanded) do
|
|
sub_problems =
|
|
expanded
|
|
|> Enum.group_by(fn {var, _val} -> (Info.get_traits(var) || %{})[:sub_key] end)
|
|
|> Map.drop([nil])
|
|
|
|
if map_size(sub_problems) == 0 do
|
|
:consistent
|
|
else
|
|
Enum.find_value(sub_problems, :consistent, fn {_sub_key, sub_assumptions_list} ->
|
|
remapped_assumptions = remap_sub_problem_vars(sub_assumptions_list)
|
|
|
|
case do_check(remapped_assumptions) do
|
|
:consistent -> nil
|
|
:contradiction -> :contradiction
|
|
end
|
|
end)
|
|
end
|
|
else
|
|
{:error, _reason} -> :contradiction
|
|
end
|
|
end
|
|
|
|
# --- Recursive Checking Helpers ---
|
|
|
|
@doc "Converts a list of scoped assumptions into a map of base assumptions for a sub-problem."
|
|
@spec remap_sub_problem_vars([{term(), boolean()}]) :: map()
|
|
def remap_sub_problem_vars(assumptions_list) do
|
|
Map.new(assumptions_list, fn {var, val} ->
|
|
{unwrap_var(var), val}
|
|
end)
|
|
end
|
|
|
|
@doc "Extracts the inner content from a recursive variable."
|
|
@spec unwrap_var(term()) :: term()
|
|
def unwrap_var(var) do
|
|
case var do
|
|
{4, :b_element, _index, inner_content} -> inner_content
|
|
{5, :c_head, inner_content, _} -> inner_content
|
|
{5, :d_tail, inner_content, _} -> inner_content
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
# --- Implication Expansion (Unchanged) ---
|
|
defp expand_with_implications(assumptions) do
|
|
expand_loop(assumptions, assumptions)
|
|
end
|
|
|
|
defp expand_loop(new_assumptions, all_assumptions) do
|
|
implications =
|
|
Enum.flat_map(new_assumptions, fn
|
|
{var, true} -> Map.get(Info.get_traits(var) || %{}, :implies, [])
|
|
_ -> []
|
|
end)
|
|
|
|
case Enum.reduce(implications, {:ok, %{}}, fn {implied_var, implied_val}, acc ->
|
|
reduce_implication({implied_var, implied_val}, all_assumptions, acc)
|
|
end) do
|
|
{:error, :contradiction} = err ->
|
|
err
|
|
|
|
{:ok, newly_added} when map_size(newly_added) == 0 ->
|
|
{:ok, all_assumptions}
|
|
|
|
{:ok, newly_added} ->
|
|
expand_loop(newly_added, Map.merge(all_assumptions, newly_added))
|
|
end
|
|
end
|
|
|
|
defp reduce_implication({var, val}, all_assumptions, {:ok, new_acc}) do
|
|
case Map.get(all_assumptions, var) do
|
|
nil -> {:ok, Map.put(new_acc, var, val)}
|
|
^val -> {:ok, new_acc}
|
|
_other_val -> {:error, :contradiction}
|
|
end
|
|
end
|
|
|
|
defp reduce_implication(_implication, _all_assumptions, error_acc), do: error_acc
|
|
|
|
# --- Flat Consistency Checks (Unchanged) ---
|
|
defp check_flat_consistency(assumptions) do
|
|
with :ok <- check_primary_type_exclusivity(assumptions),
|
|
:ok <- check_atom_consistency(assumptions),
|
|
:ok <- check_list_consistency(assumptions),
|
|
:ok <- check_integer_consistency(assumptions),
|
|
:ok <- check_tuple_consistency(assumptions) do
|
|
:ok
|
|
else
|
|
:error -> {:error, :consistency_error}
|
|
end
|
|
end
|
|
|
|
defp check_primary_type_exclusivity(assumptions) do
|
|
primary_types = [
|
|
Variable.v_is_atom(),
|
|
Variable.v_is_integer(),
|
|
Variable.v_is_list(),
|
|
Variable.v_is_tuple()
|
|
]
|
|
|
|
true_primary_types = Enum.count(primary_types, &(Map.get(assumptions, &1) == true))
|
|
|
|
if true_primary_types > 1, do: :error, else: :ok
|
|
end
|
|
|
|
defp check_atom_consistency(assumptions) do
|
|
true_atom_values =
|
|
Enum.reduce(assumptions, MapSet.new(), fn
|
|
{{1, :value, atom_val, _}, true}, acc -> MapSet.put(acc, atom_val)
|
|
_, acc -> acc
|
|
end)
|
|
|
|
if MapSet.size(true_atom_values) > 1, do: :error, else: :ok
|
|
end
|
|
|
|
defp check_tuple_consistency(assumptions) do
|
|
true_tuple_sizes =
|
|
Enum.reduce(assumptions, MapSet.new(), fn
|
|
{{4, :a_size, size, _}, true}, acc -> MapSet.put(acc, size)
|
|
_, acc -> acc
|
|
end)
|
|
|
|
if MapSet.size(true_tuple_sizes) > 1, do: :error, else: :ok
|
|
end
|
|
|
|
defp check_list_consistency(assumptions) do
|
|
is_empty = Map.get(assumptions, Variable.v_list_is_empty()) == true
|
|
has_head_prop = Enum.any?(assumptions, &match?({{5, :c_head, _, _}, true}, &1))
|
|
has_tail_prop = Enum.any?(assumptions, &match?({{5, :d_tail, _, _}, true}, &1))
|
|
|
|
if is_empty and (has_head_prop or has_tail_prop), do: :error, else: :ok
|
|
end
|
|
|
|
defp check_integer_consistency(assumptions) do
|
|
initial_range = {:neg_inf, :pos_inf}
|
|
|
|
result =
|
|
Enum.reduce_while(assumptions, initial_range, fn assumption, {min, max} ->
|
|
case assumption do
|
|
{{2, :alt, n, _}, true} -> narrow_range(min, safe_min(max, n - 1))
|
|
{{2, :alt, n, _}, false} -> narrow_range(safe_max(min, n), max)
|
|
{{2, :beq, n, _}, true} -> narrow_range(safe_max(min, n), safe_min(max, n))
|
|
{{2, :beq, n, _}, false} when min == n and max == n -> {:halt, :invalid}
|
|
{{2, :cgt, n, _}, true} -> narrow_range(safe_max(min, n + 1), max)
|
|
{{2, :cgt, n, _}, false} -> narrow_range(min, safe_min(max, n))
|
|
_ -> {:cont, {min, max}}
|
|
end
|
|
end)
|
|
|
|
case result,
|
|
do: (
|
|
:invalid -> :error
|
|
_ -> :ok
|
|
)
|
|
end
|
|
|
|
defp narrow_range(min, max) do
|
|
is_invalid =
|
|
case {min, max} do
|
|
{:neg_inf, _} -> false
|
|
{_, :pos_inf} -> false
|
|
{m, n} when is_integer(m) and is_integer(n) -> m > n
|
|
_ -> false
|
|
end
|
|
|
|
if is_invalid, do: {:halt, :invalid}, else: {:cont, {min, max}}
|
|
end
|
|
|
|
defp safe_max(:neg_inf, x), do: x
|
|
defp safe_max(x, :neg_inf), do: x
|
|
defp safe_max(:pos_inf, _), do: :pos_inf
|
|
defp safe_max(_, :pos_inf), do: :pos_inf
|
|
defp safe_max(a, b), do: :erlang.max(a, b)
|
|
|
|
defp safe_min(:pos_inf, x), do: x
|
|
defp safe_min(x, :pos_inf), do: x
|
|
defp safe_min(:neg_inf, _), do: :neg_inf
|
|
defp safe_min(_, :neg_inf), do: :neg_inf
|
|
defp safe_min(a, b), do: :erlang.min(a, b)
|
|
end
|
|
|
|
defmodule Tdd.Algo do
|
|
@moduledoc """
|
|
Implements the core, stateless algorithms for TDD manipulation.
|
|
"""
|
|
use Tdd.Debug
|
|
alias Tdd.Store
|
|
alias Tdd.Consistency.Engine
|
|
alias Tdd.Debug
|
|
|
|
# --- Binary Operation: Apply ---
|
|
# This function is correct and does not need to be changed.
|
|
@spec apply(atom, (atom, atom -> atom), non_neg_integer, non_neg_integer) :: non_neg_integer
|
|
def apply(op_name, op_lambda, u1_id, u2_id) do
|
|
cache_key = {:apply, op_name, Enum.sort([u1_id, u2_id])}
|
|
|
|
case Store.get_op_cache(cache_key) do
|
|
{: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)
|
|
result_id
|
|
end
|
|
|> simplify()
|
|
end
|
|
|
|
# This function is correct and does not need to be changed.
|
|
defp do_apply(op_name, op_lambda, u1_id, u2_id) do
|
|
with {:ok, u1_details} <- Store.get_node(u1_id),
|
|
{:ok, u2_details} <- Store.get_node(u2_id) 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()
|
|
|
|
# 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 ->
|
|
{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 ->
|
|
{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)
|
|
)
|
|
|
|
true ->
|
|
{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)
|
|
)
|
|
|
|
Store.find_or_create_node(top_var, res_y, res_n, res_d)
|
|
end
|
|
end
|
|
end
|
|
|
|
# --- Unary Operation: Negation ---
|
|
@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
|
|
|
|
: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))
|
|
|
|
# TODO: Handle placeholder nodes encountered during co-inductive simplification.
|
|
# A placeholder is treated as `any` for the check, so its negation is `none`.
|
|
{:ok, {:placeholder, _spec}} ->
|
|
Store.false_node_id()
|
|
end
|
|
|
|
Store.put_op_cache(cache_key, result_id)
|
|
result_id
|
|
end
|
|
end
|
|
|
|
# --- Unary Operation: Semantic Simplification ---
|
|
@spec simplify(non_neg_integer(), map()) :: non_neg_integer
|
|
def simplify(tdd_id, assumptions \\ %{}) do
|
|
sorted_assumptions = Enum.sort(assumptions)
|
|
cache_key = {:simplify, tdd_id, sorted_assumptions}
|
|
require Logger
|
|
|
|
case Store.get_op_cache(cache_key) do
|
|
{:ok, result_id} ->
|
|
Logger.info("result #{inspect([result_id])}")
|
|
|
|
if result_id == 1 do
|
|
Logger.info("ASD #{inspect([tdd_id, assumptions])}")
|
|
end
|
|
|
|
result_id
|
|
|
|
:not_found ->
|
|
result_id = do_simplify(tdd_id, sorted_assumptions, MapSet.new())
|
|
Store.put_op_cache(cache_key, result_id)
|
|
result_id
|
|
end
|
|
end
|
|
|
|
defp do_simplify(tdd_id, sorted_assumptions, context) do
|
|
current_state = {tdd_id, sorted_assumptions}
|
|
|
|
if MapSet.member?(context, current_state) do
|
|
Store.true_node_id()
|
|
else
|
|
new_context = MapSet.put(context, current_state)
|
|
assumptions = Map.new(sorted_assumptions)
|
|
|
|
if Engine.check(assumptions) == :contradiction do
|
|
Store.false_node_id()
|
|
else
|
|
case Store.get_node(tdd_id) do
|
|
{:ok, :true_terminal} ->
|
|
Store.true_node_id()
|
|
|
|
{:ok, :false_terminal} ->
|
|
Store.false_node_id()
|
|
|
|
{:ok, {var, y, n, d}} ->
|
|
# Dispatch to the handler for recursive variables.
|
|
case var do
|
|
{5, :c_head, constraint_id, _} ->
|
|
handle_recursive_subproblem(
|
|
:simplify,
|
|
:head,
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
{5, :d_tail, constraint_id, _} ->
|
|
handle_recursive_subproblem(
|
|
:simplify,
|
|
:tail,
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
{4, :b_element, index, constraint_id} ->
|
|
handle_recursive_subproblem(
|
|
:simplify,
|
|
{:elem, index},
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
_ ->
|
|
case Map.get(assumptions, var) do
|
|
true ->
|
|
do_simplify(y, sorted_assumptions, new_context)
|
|
|
|
false ->
|
|
do_simplify(n, sorted_assumptions, new_context)
|
|
|
|
:dc ->
|
|
do_simplify(d, sorted_assumptions, new_context)
|
|
|
|
nil ->
|
|
assumptions_imply_true =
|
|
Engine.check(Map.put(assumptions, var, false)) == :contradiction
|
|
|
|
assumptions_imply_false =
|
|
Engine.check(Map.put(assumptions, var, true)) == :contradiction
|
|
|
|
cond do
|
|
assumptions_imply_true and assumptions_imply_false ->
|
|
Store.false_node_id()
|
|
|
|
assumptions_imply_true ->
|
|
do_simplify(y, Enum.sort(Map.put(assumptions, var, true)), new_context)
|
|
|
|
assumptions_imply_false ->
|
|
do_simplify(n, Enum.sort(Map.put(assumptions, var, false)), new_context)
|
|
|
|
true ->
|
|
s_y =
|
|
do_simplify(y, Enum.sort(Map.put(assumptions, var, true)), new_context)
|
|
|
|
s_n =
|
|
do_simplify(n, Enum.sort(Map.put(assumptions, var, false)), new_context)
|
|
|
|
s_d =
|
|
do_simplify(d, Enum.sort(Map.put(assumptions, var, :dc)), new_context)
|
|
|
|
Store.find_or_create_node(var, s_y, s_n, s_d)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# --- Unary Operation: Substitute ---
|
|
# TODO: does the implementation of substitute need to change?
|
|
|
|
@spec substitute(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: non_neg_integer()
|
|
def substitute(root_id, from_id, to_id) do
|
|
if root_id == from_id, do: to_id, else: do_substitute(root_id, from_id, to_id)
|
|
end
|
|
|
|
# This helper inspects and replaces TDD IDs embedded in predicate variables.
|
|
defp substitute_in_var(var, from_id, to_id) do
|
|
case var do
|
|
{4, :b_element, index, ^from_id} -> {4, :b_element, index, to_id}
|
|
{5, :c_head, ^from_id, nil} -> {5, :c_head, to_id, nil}
|
|
{5, :d_tail, ^from_id, nil} -> {5, :d_tail, to_id, nil}
|
|
_other -> var
|
|
end
|
|
end
|
|
|
|
defp do_substitute(root_id, from_id, to_id) do
|
|
cache_key = {:substitute, root_id, from_id, to_id}
|
|
|
|
case Store.get_op_cache(cache_key) do
|
|
{:ok, result_id} ->
|
|
result_id
|
|
|
|
:not_found ->
|
|
result_id =
|
|
case Store.get_node(root_id) do
|
|
{:ok, :true_terminal} ->
|
|
Store.true_node_id()
|
|
|
|
{:ok, :false_terminal} ->
|
|
Store.false_node_id()
|
|
|
|
{:ok, {var, y, n, d}} ->
|
|
new_var = substitute_in_var(var, from_id, to_id)
|
|
new_y = substitute(y, from_id, to_id)
|
|
new_n = substitute(n, from_id, to_id)
|
|
new_d = substitute(d, from_id, to_id)
|
|
Store.find_or_create_node(new_var, new_y, new_n, new_d)
|
|
|
|
{:error, reason} ->
|
|
raise "substitute encountered an error getting node #{root_id}: #{reason}"
|
|
end
|
|
|
|
Store.put_op_cache(cache_key, result_id)
|
|
result_id
|
|
end
|
|
end
|
|
|
|
# --- Coinductive Emptiness Check ---
|
|
@spec check_emptiness(non_neg_integer()) :: non_neg_integer()
|
|
def check_emptiness(tdd_id) do
|
|
cache_key = {:check_emptiness, tdd_id}
|
|
|
|
case Store.get_op_cache(cache_key) do
|
|
{:ok, id} ->
|
|
id
|
|
|
|
:not_found ->
|
|
assumptions_list = []
|
|
result_id = do_check_emptiness(tdd_id, assumptions_list, MapSet.new())
|
|
Store.put_op_cache(cache_key, result_id)
|
|
result_id
|
|
end
|
|
end
|
|
|
|
defp do_check_emptiness(tdd_id, sorted_assumptions, context) do
|
|
current_state = {tdd_id, sorted_assumptions}
|
|
|
|
if MapSet.member?(context, current_state) do
|
|
Store.false_node_id()
|
|
else
|
|
new_context = MapSet.put(context, current_state)
|
|
assumptions = Map.new(sorted_assumptions)
|
|
|
|
if Engine.check(assumptions) == :contradiction do
|
|
Store.false_node_id()
|
|
else
|
|
# IO.inspect(tdd_id, label: "Fetching node in do_check_emptiness")
|
|
# Debug.print_tdd_graph(tdd_id)
|
|
|
|
case Store.get_node(tdd_id) do
|
|
{:ok, :true_terminal} ->
|
|
Store.true_node_id()
|
|
|
|
{:ok, :false_terminal} ->
|
|
Store.false_node_id()
|
|
|
|
{:ok, {var, y, n, d}} ->
|
|
# Dispatch to the handler for recursive variables.
|
|
case var do
|
|
{5, :c_head, constraint_id, _} ->
|
|
handle_recursive_subproblem(
|
|
:check_emptiness,
|
|
:head,
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
{5, :d_tail, constraint_id, _} ->
|
|
handle_recursive_subproblem(
|
|
:check_emptiness,
|
|
:tail,
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
{4, :b_element, index, constraint_id} ->
|
|
handle_recursive_subproblem(
|
|
:check_emptiness,
|
|
{:elem, index},
|
|
constraint_id,
|
|
{var, y, n, d},
|
|
sorted_assumptions,
|
|
new_context
|
|
)
|
|
|
|
_ ->
|
|
# The rest of the logic is the same as the do_simplify counterpart
|
|
case Map.get(assumptions, var) do
|
|
true ->
|
|
do_check_emptiness(y, sorted_assumptions, new_context)
|
|
|
|
false ->
|
|
do_check_emptiness(n, sorted_assumptions, new_context)
|
|
|
|
:dc ->
|
|
do_check_emptiness(d, sorted_assumptions, new_context)
|
|
|
|
nil ->
|
|
assumptions_imply_true =
|
|
Engine.check(Map.put(assumptions, var, false)) == :contradiction
|
|
|
|
assumptions_imply_false =
|
|
Engine.check(Map.put(assumptions, var, true)) == :contradiction
|
|
|
|
cond do
|
|
assumptions_imply_true and assumptions_imply_false ->
|
|
Store.false_node_id()
|
|
|
|
assumptions_imply_true ->
|
|
do_check_emptiness(
|
|
y,
|
|
Enum.sort(Map.put(assumptions, var, true)),
|
|
new_context
|
|
)
|
|
|
|
assumptions_imply_false ->
|
|
do_check_emptiness(
|
|
n,
|
|
Enum.sort(Map.put(assumptions, var, false)),
|
|
new_context
|
|
)
|
|
|
|
true ->
|
|
s_y =
|
|
do_check_emptiness(
|
|
y,
|
|
Enum.sort(Map.put(assumptions, var, true)),
|
|
new_context
|
|
)
|
|
|
|
s_n =
|
|
do_check_emptiness(
|
|
n,
|
|
Enum.sort(Map.put(assumptions, var, false)),
|
|
new_context
|
|
)
|
|
|
|
s_d =
|
|
do_check_emptiness(
|
|
d,
|
|
Enum.sort(Map.put(assumptions, var, :dc)),
|
|
new_context
|
|
)
|
|
|
|
Store.find_or_create_node(var, s_y, s_n, s_d)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp handle_recursive_subproblem(
|
|
algo_type,
|
|
sub_key,
|
|
# This is a TDD ID for the constraint on the sub-problem.
|
|
constraint_id,
|
|
node_details,
|
|
sorted_assumptions,
|
|
# This is the coinductive context (a MapSet).
|
|
context
|
|
) do
|
|
{var, y, n, d} = node_details
|
|
assumptions = Map.new(sorted_assumptions)
|
|
|
|
# 1. Build the TDD for the sub-problem's effective type by intersecting all
|
|
# its constraints from the current assumption set.
|
|
op_intersect = fn
|
|
:false_terminal, _ -> :false_terminal
|
|
_, :false_terminal -> :false_terminal
|
|
t, :true_terminal -> t
|
|
:true_terminal, t -> t
|
|
end
|
|
|
|
sub_problem_constraints =
|
|
Enum.filter(assumptions, fn {v, _} ->
|
|
(Tdd.Predicate.Info.get_traits(v) || %{})[:sub_key] == sub_key
|
|
end)
|
|
|
|
sub_problem_tdd_id =
|
|
Enum.reduce(sub_problem_constraints, Store.true_node_id(), fn {var, val}, acc_id ->
|
|
constraint_for_this_assumption = Engine.unwrap_var(var)
|
|
|
|
id_to_intersect =
|
|
if val, do: constraint_for_this_assumption, else: negate(constraint_for_this_assumption)
|
|
|
|
apply(:intersect, op_intersect, acc_id, id_to_intersect)
|
|
end)
|
|
|
|
# 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)
|
|
|
|
intersect_sub_with_neg_constraint =
|
|
apply(:intersect, op_intersect, sub_problem_tdd_id, neg_constraint_id)
|
|
|
|
implies_satisfied =
|
|
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 =
|
|
apply(:intersect, op_intersect, sub_problem_tdd_id, constraint_id)
|
|
|
|
implies_violated = check_emptiness(intersect_sub_with_constraint) == Store.false_node_id()
|
|
|
|
# 3. Branch based on the logical outcome.
|
|
cond do
|
|
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()
|
|
|
|
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
|
|
:simplify -> do_simplify(y, sorted_assumptions, context)
|
|
:check_emptiness -> do_check_emptiness(y, sorted_assumptions, context)
|
|
end
|
|
|
|
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()
|
|
|
|
case algo_type do
|
|
:simplify -> do_simplify(n, new_assumptions, context)
|
|
:check_emptiness -> do_check_emptiness(n, new_assumptions, context)
|
|
end
|
|
|
|
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()
|
|
|
|
case algo_type do
|
|
:check_emptiness ->
|
|
# Is there ANY path to true? Explore both and take their union.
|
|
res_y = do_check_emptiness(y, sorted_assumptions, context)
|
|
res_n = do_check_emptiness(n, new_assumptions_for_no_branch, context)
|
|
|
|
# Define local terminal logic for union
|
|
op_union = fn
|
|
:true_terminal, _ -> :true_terminal
|
|
_, :true_terminal -> :true_terminal
|
|
t, :false_terminal -> t
|
|
:false_terminal, t -> t
|
|
end
|
|
|
|
apply(:sum, op_union, res_y, res_n)
|
|
|
|
: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_n = do_simplify(n, new_assumptions_for_no_branch, context)
|
|
# 'dc' branch is independent of 'var'
|
|
res_d = do_simplify(d, sorted_assumptions, context)
|
|
Store.find_or_create_node(var, res_y, res_n, res_d)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Tdd.Compiler do
|
|
@moduledoc """
|
|
Compiles a `TypeSpec` into a canonical TDD ID.
|
|
"""
|
|
alias Tdd.TypeSpec
|
|
alias Tdd.Variable
|
|
alias Tdd.Store
|
|
alias Tdd.Algo
|
|
alias Tdd.Debug
|
|
|
|
@doc "The main public entry point. Takes a spec and returns its TDD ID."
|
|
@spec spec_to_id(TypeSpec.t()) :: non_neg_integer()
|
|
def spec_to_id(spec) do
|
|
normalized_spec = TypeSpec.normalize(spec)
|
|
compile_normalized_spec(normalized_spec, %{})
|
|
end
|
|
|
|
defp compile_normalized_spec(normalized_spec, context) do
|
|
sorted_context = Enum.sort(context)
|
|
cache_key = {:compile_spec_with_context, normalized_spec, sorted_context}
|
|
|
|
case Store.get_op_cache(cache_key) do
|
|
{:ok, id} ->
|
|
id
|
|
|
|
:not_found ->
|
|
id_to_cache =
|
|
case normalized_spec do
|
|
{:type_var, var_name} ->
|
|
Map.get(context, var_name) ||
|
|
raise "Tdd.Compiler: Unbound type variable: #{inspect(var_name)}"
|
|
|
|
{:mu, var_name, body_spec} ->
|
|
placeholder_id = Store.create_placeholder({:mu_knot_tying_for, var_name, body_spec})
|
|
new_context = Map.put(context, var_name, placeholder_id)
|
|
body_id = compile_normalized_spec(body_spec, new_context)
|
|
{:ok, body_details} = Store.get_node(body_id)
|
|
Store.update_node_in_place(placeholder_id, {:ok, body_details})
|
|
Algo.simplify(placeholder_id)
|
|
|
|
other_spec ->
|
|
raw_id = do_structural_compile(other_spec, context)
|
|
Algo.simplify(raw_id)
|
|
end
|
|
|
|
Store.put_op_cache(cache_key, id_to_cache)
|
|
id_to_cache
|
|
end
|
|
end
|
|
|
|
defp do_structural_compile(structural_spec, context) do
|
|
case structural_spec do
|
|
:any ->
|
|
Store.true_node_id()
|
|
|
|
:none ->
|
|
Store.false_node_id()
|
|
|
|
:atom ->
|
|
create_base_type_tdd(Variable.v_is_atom())
|
|
|
|
:integer ->
|
|
create_base_type_tdd(Variable.v_is_integer())
|
|
|
|
:list ->
|
|
create_base_type_tdd(Variable.v_is_list())
|
|
|
|
:tuple ->
|
|
create_base_type_tdd(Variable.v_is_tuple())
|
|
|
|
{:literal, val} when is_atom(val) ->
|
|
compile_value_equality(:atom, Variable.v_atom_eq(val), context)
|
|
|
|
{:literal, val} when is_integer(val) ->
|
|
compile_value_equality(:integer, Variable.v_int_eq(val), context)
|
|
|
|
{:literal, []} ->
|
|
compile_value_equality(:list, Variable.v_list_is_empty(), context)
|
|
|
|
{:integer_range, min, max} ->
|
|
compile_integer_range(min, max, context)
|
|
|
|
{:union, specs} ->
|
|
Enum.map(specs, &compile_normalized_spec(&1, context))
|
|
|> Enum.reduce(Store.false_node_id(), fn id, acc ->
|
|
Algo.apply(:sum, &op_union_terminals/2, id, acc)
|
|
end)
|
|
|
|
{:intersect, specs} ->
|
|
Enum.map(specs, &compile_normalized_spec(&1, context))
|
|
|> Enum.reduce(Store.true_node_id(), fn id, acc ->
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, id, acc)
|
|
end)
|
|
|
|
{:negation, sub_spec} ->
|
|
Algo.negate(compile_normalized_spec(sub_spec, context))
|
|
|
|
{:cons, head_spec, tail_spec} ->
|
|
id_list = compile_normalized_spec(:list, context)
|
|
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
|
id_not_is_empty = Algo.negate(id_is_empty)
|
|
|
|
non_empty_list_id =
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, id_list, id_not_is_empty)
|
|
|
|
head_id = compile_normalized_spec(head_spec, context)
|
|
tail_id = compile_normalized_spec(tail_spec, context)
|
|
|
|
head_checker_var = Variable.v_list_head_pred(head_id)
|
|
head_checker_tdd = create_base_type_tdd(head_checker_var)
|
|
|
|
tail_checker_var = Variable.v_list_tail_pred(tail_id)
|
|
tail_checker_tdd = create_base_type_tdd(tail_checker_var)
|
|
|
|
[non_empty_list_id, head_checker_tdd, tail_checker_tdd]
|
|
|> Enum.reduce(Store.true_node_id(), fn id, acc ->
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, id, acc)
|
|
end)
|
|
|
|
{:tuple, elements_specs} ->
|
|
size = length(elements_specs)
|
|
base_id = compile_normalized_spec(:tuple, context)
|
|
size_tdd = create_base_type_tdd(Variable.v_tuple_size_eq(size))
|
|
initial_id = Algo.apply(:intersect, &op_intersect_terminals/2, base_id, size_tdd)
|
|
|
|
elements_specs
|
|
|> Enum.with_index()
|
|
|> Enum.reduce(initial_id, fn {elem_spec, index}, acc_id ->
|
|
elem_id = compile_normalized_spec(elem_spec, context)
|
|
elem_checker_var = Variable.v_tuple_elem_pred(index, elem_id)
|
|
elem_checker_tdd = create_base_type_tdd(elem_checker_var)
|
|
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, acc_id, elem_checker_tdd)
|
|
end)
|
|
|
|
{:type_lambda, _, _} ->
|
|
raise "Tdd.Compiler: Cannot compile :type_lambda directly. Spec should be ground. Spec: #{inspect(structural_spec)}"
|
|
|
|
{:type_apply, _, _} ->
|
|
raise "Tdd.Compiler: Cannot compile :type_apply directly. Spec should be ground and fully beta-reduced. Spec: #{inspect(structural_spec)}"
|
|
|
|
_ ->
|
|
raise "Tdd.Compiler.do_structural_compile: Unhandled structural spec form: #{inspect(structural_spec)}"
|
|
end
|
|
end
|
|
|
|
defp create_base_type_tdd(var),
|
|
do:
|
|
Store.find_or_create_node(
|
|
var,
|
|
Store.true_node_id(),
|
|
Store.false_node_id(),
|
|
Store.false_node_id()
|
|
)
|
|
|
|
defp compile_value_equality(base_type_spec, value_var, context) do
|
|
eq_node = create_base_type_tdd(value_var)
|
|
base_node_id = compile_normalized_spec(base_type_spec, context)
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, base_node_id, eq_node)
|
|
end
|
|
|
|
defp compile_integer_range(min, max, context) do
|
|
base_id = compile_normalized_spec(:integer, context)
|
|
|
|
lt_min_tdd = if min != :neg_inf, do: create_base_type_tdd(Variable.v_int_lt(min))
|
|
|
|
gte_min_tdd =
|
|
if lt_min_tdd, do: Algo.negate(lt_min_tdd), else: compile_normalized_spec(:any, context)
|
|
|
|
id_with_min = Algo.apply(:intersect, &op_intersect_terminals/2, base_id, gte_min_tdd)
|
|
|
|
if max == :pos_inf do
|
|
id_with_min
|
|
else
|
|
lt_max_plus_1_tdd = create_base_type_tdd(Variable.v_int_lt(max + 1))
|
|
Algo.apply(:intersect, &op_intersect_terminals/2, id_with_min, lt_max_plus_1_tdd)
|
|
end
|
|
end
|
|
|
|
# --- Terminal Logic Helpers ---
|
|
defp op_union_terminals(:true_terminal, _), do: :true_terminal
|
|
defp op_union_terminals(_, :true_terminal), do: :true_terminal
|
|
defp op_union_terminals(t, :false_terminal), do: t
|
|
defp op_union_terminals(:false_terminal, t), do: t
|
|
defp op_intersect_terminals(:false_terminal, _), do: :false_terminal
|
|
defp op_intersect_terminals(_, :false_terminal), do: :false_terminal
|
|
defp op_intersect_terminals(t, :true_terminal), do: t
|
|
defp op_intersect_terminals(:true_terminal, t), do: t
|
|
|
|
# --- Public Subtyping Check ---
|
|
@doc "Checks if spec1 is a subtype of spec2 using TDDs."
|
|
@spec is_subtype(TypeSpec.t(), TypeSpec.t()) :: boolean
|
|
def is_subtype(spec1, spec2) do
|
|
Store.init()
|
|
|
|
id1 = spec_to_id(spec1)
|
|
Debug.print_tdd_graph(id1, "IS_SUBTYPE id1")
|
|
id2 = spec_to_id(spec2)
|
|
Debug.print_tdd_graph(id2, "IS_SUBTYPE id2")
|
|
neg_id2 = Algo.negate(id2)
|
|
Debug.print_tdd_graph(neg_id2, "IS_SUBTYPE neg_id2")
|
|
intersect_id = Algo.apply(:intersect, &op_intersect_terminals/2, id1, neg_id2)
|
|
Debug.print_tdd_graph(intersect_id, "IS_SUBTYPE intersect")
|
|
|
|
final_id = Algo.check_emptiness(intersect_id)
|
|
final_id == Store.false_node_id()
|
|
end
|
|
end
|