Mix.install([:ex_unit_summary]) # Start ExUnitSummary application, with recommended config ExUnitSummary.start(:normal, %{__struct__: ExUnitSummary.Config, filter_results: :failed, print_delay: 100}) # Add ExUnitSummary.Formatter to list of ExUnit's formatters. ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitSummary.Formatter]) Code.require_file("./debug.exs") 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}} -> # This logic is from the original file, which is correct in principle # but was failing due to the TDD layer bug. 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 # NOTE: This module remains unchanged. # The original provided code for this module is correct and complete. @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} %{} -> {: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 find_or_create_node({:placeholder, spec}, 1, 0, 0) 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) 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. REFAC: This module is unchanged, but its functions for recursive types will now be called with TDD IDs instead of TypeSpecs by the Tdd.Compiler. """ # --- 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 # REFAC: The nested term is now a TDD ID, not a spec or a variable. {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 # NOTE: This module remains largely unchanged. The traits for recursive variables # correctly identify them by structure, independent of what's inside. @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 # REFAC: The trait recognizes the structure. The content `_tdd_id` is opaque here. 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 # REFAC: The trait recognizes the structure. The content `_tdd_id` is opaque here. 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 # REFAC: The trait recognizes the structure. The content `_tdd_id` is opaque here. 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. REFAC: This module is largely unchanged, but we now make `remap_sub_problem_vars` and `unwrap_var` public so they can be shared with Tdd.Algo. """ 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 # REFAC: These variables now contain TDD IDs, but this function just extracts # whatever is inside. The consumer (`handle_recursive_subproblem`) will know it's an ID. {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 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() 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 --- # This function is correct and does not need to be changed. @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)) end Store.put_op_cache(cache_key, result_id) result_id end end # --- Unary Operation: Semantic Simplification --- # This function is correct and does not need to be changed. @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} case Store.get_op_cache(cache_key) do {:ok, result_id} -> 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 # This function is correct and does not need to be changed. 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 ) _ -> # The rest of the logic for standard variables is unchanged. 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 --- # FIX: The implementation of substitute needs 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}} -> # FIX: Substitute within the variable term itself. 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 --- # This function is correct and does not need to be changed. @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 # This function is correct and does not need to be changed. 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 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 # This function, containing our previous fix, is correct and does not need to be changed. defp handle_recursive_subproblem( algo_type, sub_key, constraint_id, # This is a TDD ID for the constraint on the sub-problem. node_details, sorted_assumptions, context # This is the coinductive context (a MapSet). ) 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) res_d = do_simplify(d, sorted_assumptions, context) # 'dc' branch is independent of 'var' 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. REFAC: This module now embeds TDD IDs into recursive predicate variables instead of raw TypeSpecs, a key part of the architectural decoupling. """ 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 # It's crucial to initialize the store for each top-level compilation # to ensure a clean slate for caches and node IDs. This makes calls independent. normalized_spec = TypeSpec.normalize(spec) compile_normalized_spec(normalized_spec, %{}) end defp compile_normalized_spec(normalized_spec, context) do cache_key = {:spec_to_id, normalized_spec} case normalized_spec do {:type_var, var_name} -> case Map.get(context, var_name) do nil -> raise "Tdd.Compiler: Unbound type variable during TDD compilation: #{inspect(var_name)}. Full spec: #{inspect(normalized_spec)}. Context: #{inspect(context)}" placeholder_id when is_integer(placeholder_id) -> placeholder_id end _other_form -> case Store.get_op_cache(cache_key) do {:ok, id} -> id :not_found -> id_to_cache = case normalized_spec do {:mu, var_name, body_spec} -> placeholder_node_variable_tag = {:mu_placeholder_for_var, var_name} placeholder_id = Store.create_placeholder(placeholder_node_variable_tag) new_context = Map.put(context, var_name, placeholder_id) compiled_body_id = compile_normalized_spec(body_spec, new_context) # The substitution is the "knot-tying" step for recursion final_id = Algo.substitute(compiled_body_id, placeholder_id, compiled_body_id) Algo.simplify(final_id) other -> raw_id = do_structural_compile(other, context) Algo.simplify(raw_id) end Store.put_op_cache(cache_key, id_to_cache) id_to_cache end 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)) # REFAC: This is a key change. We now compile sub-specs to TDD IDs # and embed those IDs in the predicate variables. {: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) # 1. Compile sub-specs to get their TDD IDs. head_id = compile_normalized_spec(head_spec, context) tail_id = compile_normalized_spec(tail_spec, context) # 2. Embed the TDD IDs into the variables. 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) # REFAC: Same change for tuples. {: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 -> # 1. Compile element spec to get its TDD ID. elem_id = compile_normalized_spec(elem_spec, context) # 2. Embed the TDD ID into the variable. 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 id1 = spec_to_id(spec1) id2 = spec_to_id(spec2) neg_id2 = Algo.negate(id2) intersect_id = Algo.apply(:intersect, &op_intersect_terminals/2, id1, neg_id2) final_id = Algo.check_emptiness(intersect_id) final_id == Store.false_node_id() end end #### # xxx #### ExUnit.start(autorun: false) defmodule TddSystemTest do use ExUnit.Case, async: false # Most tests mutate Tdd.Store, so they cannot run concurrently. alias Tdd.TypeSpec alias Tdd.Store alias Tdd.Variable alias Tdd.Compiler alias Tdd.Consistency.Engine alias Tdd.Algo # Helper to mimic the old test structure and provide better failure messages # for spec comparisons. defp assert_spec_normalized(expected, input_spec) do result = TypeSpec.normalize(input_spec) # The normalization process should produce a canonical, sorted form. assert expected == result, """ Input Spec: #{inspect(input_spec, pretty: true)} Expected Normalized: #{inspect(expected, pretty: true)} Actual Normalized: #{inspect(result, pretty: true)} """ end # Helper to check for equivalence by comparing TDD IDs. defmacro assert_equivalent_specs(spec1, spec2) do quote do assert Compiler.spec_to_id(unquote(spec1)) == Compiler.spec_to_id(unquote(spec2)) end end # Helper to check for subtyping using the TDD compiler. defmacro assert_subtype(spec1, spec2) do quote do assert Compiler.is_subtype(unquote(spec1), unquote(spec2)) end end defmacro refute_subtype(spec1, spec2) do quote do refute Compiler.is_subtype(unquote(spec1), unquote(spec2)) end end # Setup block that initializes the Tdd.Store before each test. # This ensures that node IDs and caches are clean for every test case. setup do Tdd.Store.init() :ok end # --- # Tdd.Store Tests # These tests validate the lowest-level state management of the TDD system. # The Store is responsible for creating and storing the nodes of the decision diagram graph. # --- describe "Tdd.Store: Core state management for the TDD graph" do @doc """ Tests that the store initializes with the correct, reserved IDs for the terminal nodes representing TRUE (:any) and FALSE (:none). """ test "initialization and terminals" do assert Store.true_node_id() == 1 assert Store.false_node_id() == 0 assert Store.get_node(1) == {:ok, :true_terminal} assert Store.get_node(0) == {:ok, :false_terminal} assert Store.get_node(99) == {:error, :not_found} end @doc """ Tests the core functionality of creating nodes. It verifies that new nodes receive incrementing IDs and that requesting an identical node reuses the existing one (structural sharing), which is fundamental to the efficiency of TDDs. """ test "node creation and structural sharing" do var_a = {:is_atom} var_b = {:is_integer} true_id = Store.true_node_id() false_id = Store.false_node_id() # First created node gets ID 2 (after 0 and 1 are taken by terminals) id1 = Store.find_or_create_node(var_a, true_id, false_id, false_id) assert id1 == 2 assert Store.get_node(id1) == {:ok, {var_a, true_id, false_id, false_id}} # Second, different node gets the next ID id2 = Store.find_or_create_node(var_b, id1, false_id, false_id) assert id2 == 3 # Creating the first node again returns the same ID, not a new one id1_again = Store.find_or_create_node(var_a, true_id, false_id, false_id) assert id1_again == id1 # Next new node gets the correct subsequent ID, proving no ID was wasted id3 = Store.find_or_create_node(var_b, true_id, false_id, false_id) assert id3 == 4 end @doc """ Tests a key reduction rule: if a node's 'yes', 'no', and 'don't care' branches all point to the same child node, the parent node is redundant and should be replaced by the child node itself. """ test "node reduction rule for identical children" do var_a = {:is_atom} id3 = 4 # from previous test logic id_redundant = Store.find_or_create_node(var_a, id3, id3, id3) assert id_redundant == id3 end @doc """ Tests the memoization cache for operations like 'apply', 'negate', etc. This ensures that repeated operations with the same inputs do not trigger redundant computations. """ test "operation caching" do cache_key = {:my_op, 1, 2} assert Store.get_op_cache(cache_key) == :not_found Store.put_op_cache(cache_key, :my_result) assert Store.get_op_cache(cache_key) == {:ok, :my_result} Store.put_op_cache(cache_key, :new_result) assert Store.get_op_cache(cache_key) == {:ok, :new_result} end end # --- # Tdd.TypeSpec.normalize/1 Tests # These tests focus on ensuring the `normalize` function correctly transforms # any TypeSpec into its canonical, simplified form. # --- describe "Tdd.TypeSpec.normalize/1: Base & Simple Types" do @doc "Tests that normalizing already-simple specs doesn't change them (idempotency)." test "normalizing :any is idempotent" do assert_spec_normalized(:any, :any) end test "normalizing :none is idempotent" do assert_spec_normalized(:none, :none) end test "normalizing :atom is idempotent" do assert_spec_normalized(:atom, :atom) end test "normalizing a literal is idempotent" do assert_spec_normalized({:literal, :foo}, {:literal, :foo}) end end describe "Tdd.TypeSpec.normalize/1: Double Negation" do @doc "Tests the logical simplification that ¬(¬A) is equivalent to A." test "¬(¬atom) simplifies to atom" do assert_spec_normalized(:atom, {:negation, {:negation, :atom}}) end @doc "Tests that a single negation is preserved when it cannot be simplified further." test "A single negation is preserved" do assert_spec_normalized({:negation, :integer}, {:negation, :integer}) end @doc "Tests that an odd number of negations simplifies to a single negation." test "¬(¬(¬atom)) simplifies to ¬atom" do assert_spec_normalized({:negation, :atom}, {:negation, {:negation, {:negation, :atom}}}) end end describe "Tdd.TypeSpec.normalize/1: Union Normalization" do @doc """ Tests that unions are canonicalized by flattening nested unions, sorting the members, and removing duplicates. e.g., `int | (list | atom | int)` becomes `(atom | int | list)`. """ test "flattens, sorts, and uniques members" do input = {:union, [:integer, {:union, [:list, :atom, :integer]}]} expected = {:union, [:atom, :integer, :list]} assert_spec_normalized(expected, input) end @doc "Tests `A | none` simplifies to `A`, as `:none` is the identity for union." test "simplifies a union with :none (A | none -> A)" do assert_spec_normalized(:atom, {:union, [:atom, :none]}) end @doc "Tests `A | any` simplifies to `any`, as `:any` is the absorbing element for union." test "simplifies a union with :any (A | any -> any)" do assert_spec_normalized(:any, {:union, [:atom, :any]}) end @doc "An empty set of types is logically equivalent to `:none`." test "an empty union simplifies to :none" do assert_spec_normalized(:none, {:union, []}) end @doc "A union containing just one type should simplify to that type itself." test "a union of a single element simplifies to the element itself" do assert_spec_normalized(:atom, {:union, [:atom]}) end end describe "Tdd.TypeSpec.normalize/1: Intersection Normalization" do @doc "Tests that intersections are canonicalized like unions (flatten, sort, unique)." test "flattens, sorts, and uniques members" do input = {:intersect, [:integer, {:intersect, [:list, :atom, :integer]}]} expected = {:intersect, [:atom, :integer, :list]} assert_spec_normalized(expected, input) end @doc "Tests `A & any` simplifies to `A`, as `:any` is the identity for intersection." test "simplifies an intersection with :any (A & any -> A)" do assert_spec_normalized(:atom, {:intersect, [:atom, :any]}) end @doc "Tests `A & none` simplifies to `none`, as `:none` is the absorbing element." test "simplifies an intersection with :none (A & none -> none)" do assert_spec_normalized(:none, {:intersect, [:atom, :none]}) end @doc "An intersection of zero types is logically `any` (no constraints)." test "an empty intersection simplifies to :any" do assert_spec_normalized(:any, {:intersect, []}) end @doc "An intersection of one type simplifies to the type itself." test "an intersection of a single element simplifies to the element itself" do assert_spec_normalized(:atom, {:intersect, [:atom]}) end end describe "Tdd.TypeSpec.normalize/1: Subtype Reduction" do @doc """ Tests a key simplification: if a union contains a type and its own subtype, the subtype is redundant and should be removed. E.g., `(1 | integer)` is just `integer`. Here, `:foo` and `:bar` are subtypes of `:atom`, so the union simplifies to `:atom`. """ test "(:foo | :bar | atom) simplifies to atom" do input = {:union, [{:literal, :foo}, {:literal, :bar}, :atom]} expected = :atom assert_spec_normalized(expected, input) end end describe "Tdd.TypeSpec: Advanced Normalization (μ, Λ, Apply)" do @doc """ Tests alpha-conversion for recursive types. The bound variable name (`:X`) should be renamed to a canonical name (`:m_var0`) to ensure structural equality regardless of the name chosen by the user. """ test "basic alpha-conversion for μ-variable" do input = {:mu, :X, {:type_var, :X}} expected = {:mu, :m_var0, {:type_var, :m_var0}} assert_spec_normalized(expected, input) end @doc """ Tests that the syntactic sugar `{:list_of, T}` is correctly desugared into its underlying recursive definition: `μT.[] | cons(T, μT)`. """ test "list_of(integer) normalizes to a μ-expression with canonical var" do input = {:list_of, :integer} expected = {:mu, :m_var0, {:union, [{:literal, []}, {:cons, :integer, {:type_var, :m_var0}}]}} assert_spec_normalized(expected, input) end @doc """ Tests beta-reduction (function application). Applying the identity function `(ΛT.T)` to `integer` should result in `integer`. """ test "simple application: (ΛT.T) integer -> integer" do input = {:type_apply, {:type_lambda, [:T], {:type_var, :T}}, [:integer]} expected = :integer assert_spec_normalized(expected, input) end @doc """ Tests a more complex beta-reduction. Applying a list constructor lambda to `:atom` should produce the normalized form of `list_of(atom)`. """ test "application with structure: (ΛT. list_of(T)) atom -> list_of(atom) (normalized form)" do input = {:type_apply, {:type_lambda, [:T], {:list_of, {:type_var, :T}}}, [:atom]} expected = {:mu, :m_var0, {:union, [{:literal, []}, {:cons, :atom, {:type_var, :m_var0}}]}} assert_spec_normalized(expected, input) end end # --- # Tdd.Consistency.Engine Tests # These tests validate the logic that detects contradictions in a set of predicate assumptions. # --- describe "Tdd.Consistency.Engine: Logic for detecting contradictions" do # This setup is local to this describe block, which is fine. setup do Tdd.Store.init() id_atom = Tdd.Compiler.spec_to_id(:atom) %{id_atom: id_atom} end @doc "An empty set of assumptions has no contradictions." test "an empty assumption map is consistent" do assert Engine.check(%{}) == :consistent end @doc """ Tests that the engine uses predicate traits to find implied contradictions. `v_atom_eq(:foo)` implies `v_is_atom()` is true, which contradicts the explicit assumption that `v_is_atom()` is false. """ test "an implied contradiction is caught by expander" do assumptions = %{Variable.v_atom_eq(:foo) => true, Variable.v_is_atom() => false} assert Engine.check(assumptions) == :contradiction end @doc "A term cannot belong to two different primary types like :atom and :integer." test "two primary types cannot both be true" do assumptions = %{Variable.v_is_atom() => true, Variable.v_is_integer() => true} assert Engine.check(assumptions) == :contradiction end @doc "A list cannot be empty and simultaneously have properties on its head (which wouldn't exist)." test "a list cannot be empty and have a head property", %{id_atom: id_atom} do assumptions = %{ Variable.v_list_is_empty() => true, Variable.v_list_head_pred(id_atom) => true } assert Engine.check(assumptions) == :contradiction end @doc "Tests for logical contradictions in integer ranges." test "int < 10 AND int > 20 is a contradiction" do assumptions = %{ Variable.v_int_lt(10) => true, Variable.v_int_gt(20) => true } assert Engine.check(assumptions) == :contradiction end end # --- # Compiler & Algo Integration Tests # These tests ensure that the high-level public APIs (`is_subtype`, `spec_to_id`) # work correctly by integrating the compiler and the graph algorithms. # --- describe "Tdd.Compiler and Tdd.Algo Integration: High-level API validation" do @doc "Verifies semantic equivalence of types using TDD IDs. e.g., `atom & any` is the same type as `atom`." test "basic equivalences" do assert_equivalent_specs({:intersect, [:atom, :any]}, :atom) assert_equivalent_specs({:union, [:atom, :none]}, :atom) assert_equivalent_specs({:intersect, [:atom, :integer]}, :none) end @doc "Tests the main `is_subtype` public API for simple, non-recursive types." test "basic subtyping" do assert_subtype({:literal, :foo}, :atom) refute_subtype(:atom, {:literal, :foo}) assert_subtype(:none, :atom) assert_subtype(:atom, :any) end @doc "Tests that impossible type intersections compile to the `:none` (FALSE) node." test "contradictions" do assert Compiler.spec_to_id({:intersect, [:atom, :integer]}) == Store.false_node_id() assert Compiler.spec_to_id({:intersect, [{:literal, :foo}, {:literal, :bar}]}) == Store.false_node_id() end end # --- # Tdd.Compiler Advanced Feature Tests # These tests target the most complex features: recursive and polymorphic types. # --- describe "Tdd.Compiler: Advanced Features (μ, Λ, Apply)" do @doc """ It checks for covariance in generic types: a list of integers is a subtype of a list of anything, but the reverse is not true. This requires the system to correctly handle coinductive reasoning on the recursive TDD nodes. """ test "the previously crashing recursive subtype test now passes" do int_list = {:list_of, :integer} any_list = {:list_of, :any} assert_subtype(:integer, :any) # The key test that was failing due to the bug assert_subtype(int_list, any_list) refute_subtype(any_list, int_list) # Also test instances against the recursive type assert_subtype({:cons, {:literal, 1}, {:literal, []}}, int_list) refute_subtype({:cons, {:literal, :a}, {:literal, []}}, int_list) end @doc "Tests that manually-defined recursive types (like a binary tree) can be compiled and checked correctly." test "explicit μ-types" do leaf_node = {:literal, :empty_tree} tree_spec = {:mu, :Tree, {:union, [ leaf_node, {:tuple, [:atom, {:type_var, :Tree}, {:type_var, :Tree}]} ]}} # Test that it compiles to a valid TDD ID assert is_integer(Compiler.spec_to_id(tree_spec)) # Test that an instance of the tree is correctly identified as a subtype simple_tree_instance = {:tuple, [{:literal, :a}, leaf_node, leaf_node]} assert_subtype(simple_tree_instance, tree_spec) end @doc """ Tests that a polymorphic type created via lambda application is equivalent to its manually specialized counterpart. e.g., `(List)(int)` should be the same as `List`. """ test "polymorphism (Λ, Apply)" do gen_list_lambda = {:type_lambda, [:Tparam], {:list_of, {:type_var, :Tparam}}} list_of_int_from_apply = {:type_apply, gen_list_lambda, [:integer]} int_list = {:list_of, :integer} assert_equivalent_specs(list_of_int_from_apply, int_list) end end end # Run all defined tests ExUnit.run()