defmodule Tdd do # --- Existing code from your previous version --- # (init_tdd_system, get_state, update_state, make_node, get_node_details, # variable definitions, basic type constructors, apply, sum, print_tdd) # Ensure your `apply/4` function is the corrected version from the MatchError fix. @moduledoc """ Ternary decision diagram for set-theoretic types. """ # --- Terminal Node IDs --- @false_node_id 0 @true_node_id 1 defguard is_terminal_id(id) when id == @false_node_id or id == @true_node_id def init_tdd_system do Process.put(:nodes, %{}) Process.put(:node_by_id, %{@false_node_id => :false_terminal, @true_node_id => :true_terminal}) Process.put(:next_id, 2) Process.put(:op_cache, %{}) IO.puts("TDD system initialized.") end defp get_state do %{ nodes: Process.get(:nodes, %{}), node_by_id: Process.get(:node_by_id, %{ @false_node_id => :false_terminal, @true_node_id => :true_terminal }), next_id: Process.get(:next_id, 2), op_cache: Process.get(:op_cache, %{}) } end defp update_state(changes) do current_state = get_state() new_state = Map.merge(current_state, changes) Process.put(:nodes, new_state.nodes) Process.put(:node_by_id, new_state.node_by_id) Process.put(:next_id, new_state.next_id) Process.put(:op_cache, new_state.op_cache) end # --- Raw Node Creation (Structural) --- # This is the original make_node, focused on structural uniqueness and basic reduction rule. defp make_node_raw(variable, yes_id, no_id, dc_id) do # Basic reduction: if all children are identical, this node is redundant. if yes_id == no_id && yes_id == dc_id do yes_id else state = get_state() node_tuple = {variable, yes_id, no_id, dc_id} if Map.has_key?(state.nodes, node_tuple) do # Node already exists (structural sharing) state.nodes[node_tuple] else # Create new node new_id = state.next_id update_state(%{ nodes: Map.put(state.nodes, node_tuple, new_id), node_by_id: Map.put(state.node_by_id, new_id, node_tuple), next_id: new_id + 1 }) new_id end end end # --- Public Node Creation (Currently same as raw, apply will handle context) --- # The `apply` algorithm inherently creates the necessary structure. # Semantic simplification is applied *after* `apply` completes. def make_node(variable, yes_id, no_id, dc_id) do make_node_raw(variable, yes_id, no_id, dc_id) end # --- Semantic Constraint Checking --- defp check_assumptions_consistency(assumptions_map) do # Primary type mutual exclusivity: # Check if more than one primary type (category 0) is assumed to be true. primary_true_predicates = Enum.reduce(assumptions_map, MapSet.new(), fn ({{0, predicate_name}, true}, acc_set) -> MapSet.put(acc_set, predicate_name) # ({var, false}, acc_set) -> # If is_atom is false, that's fine with is_tuple being true. # Check if var is a primary type and its assumed value contradicts another assumed primary type. # E.g. assumptions_map has { {0, :is_atom}, true } and we are checking an assumption for { {0, :is_tuple}, true } # This is implicitly handled by iterating through assumptions_map. _otherwise, acc_set -> acc_set end) if MapSet.size(primary_true_predicates) > 1 do # IO.puts "Debug: Contradiction! Assumptions: #{inspect assumptions_map}, True primary types: #{inspect primary_true_predicates}" :contradiction else # Add other semantic checks here, e.g.: # - If assumptions include `{v_int_lt_N, true}` and `{v_int_gt_M, true}` where N <= M. # - If assumptions include `{v_tuple_size_eq_2, true}` and `{v_tuple_size_eq_3, true}`. :consistent end end # --- Semantic Simplification --- # Simplifies a TDD given a map of assumptions {variable => value}. # Value can be true, false, or :dc (if we want to model "don't care" assumptions). defp simplify_with_constraints(tdd_id, assumptions_map) do state = get_state() # Canonicalize assumptions_map for cache key: sort map by variable # Convert map to sorted list of tuples for stable cache key sorted_assumptions_list = Enum.sort_by(assumptions_map, fn {var, _val} -> var end) cache_key = {:simplify_constr, tdd_id, sorted_assumptions_list} cond do is_terminal_id(tdd_id) -> tdd_id Map.has_key?(state.op_cache, cache_key) -> state.op_cache[cache_key] check_assumptions_consistency(assumptions_map) == :contradiction -> # If assumptions are contradictory, the entire path is impossible. # update_state(%{op_cache: Map.put(state.op_cache, cache_key, @false_node_id)}) # Cache this finding @false_node_id true -> {var, y, n, d} = get_node_details(tdd_id) result_id = case Map.get(assumptions_map, var) do true -> # Current var is assumed true, follow yes branch with same assumptions simplify_with_constraints(y, assumptions_map) false -> # Current var is assumed false, follow no branch with same assumptions simplify_with_constraints(n, assumptions_map) :dc -> # Current var is assumed don't care, follow dc branch simplify_with_constraints(d, assumptions_map) nil -> # Current var is not in assumptions, so it's a decision point. # Recursively simplify children by adding this var's assignment to assumptions. simplified_y = simplify_with_constraints(y, Map.put(assumptions_map, var, true)) simplified_n = simplify_with_constraints(n, Map.put(assumptions_map, var, false)) simplified_d = simplify_with_constraints(d, Map.put(assumptions_map, var, :dc)) # Assuming :dc can be an assumption value make_node_raw(var, simplified_y, simplified_n, simplified_d) # Use raw, as children are now simplified end update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id end end def get_node_details(id) when is_terminal_id(id) do if id == @true_node_id, do: :true_terminal, else: :false_terminal end def get_node_details(id) do state = get_state() state.node_by_id[id] end @v_is_atom {0, :is_atom} @v_is_tuple {0, :is_tuple} def v_atom_eq(atom_val), do: {1, :value, atom_val} def v_tuple_size_eq(size), do: {4, :size, size} def v_tuple_elem_pred(index, nested_pred_id), do: {4, :element, index, nested_pred_id} def type_any, do: @true_node_id def type_none, do: @false_node_id def type_atom do make_node(@v_is_atom, @true_node_id, @false_node_id, @false_node_id) end def type_atom_literal(atom_val) do var_eq = v_atom_eq(atom_val) atom_val_node = make_node(var_eq, @true_node_id, @false_node_id, @false_node_id) make_node(@v_is_atom, atom_val_node, @false_node_id, @false_node_id) end def type_tuple do make_node(@v_is_tuple, @true_node_id, @false_node_id, @false_node_id) end def type_empty_tuple do var_size_0 = v_tuple_size_eq(0) tuple_size_node = make_node(var_size_0, @true_node_id, @false_node_id, @false_node_id) make_node(@v_is_tuple, tuple_size_node, @false_node_id, @false_node_id) end def type_tuple_sized_any(size) do var_size = v_tuple_size_eq(size) tuple_size_node = make_node(var_size, @true_node_id, @false_node_id, @false_node_id) make_node(@v_is_tuple, tuple_size_node, @false_node_id, @false_node_id) end # --- The APPLY Algorithm (uses make_node, which is currently make_node_raw) --- def apply(op_name, op_lambda, u1_id, u2_id) do state = get_state() cache_key = {op_name, Enum.sort([u1_id, u2_id])} cond do Map.has_key?(state.op_cache, cache_key) -> state.op_cache[cache_key] is_terminal_id(u1_id) && is_terminal_id(u2_id) -> res_terminal_symbol = op_lambda.(get_node_details(u1_id), get_node_details(u2_id)) if res_terminal_symbol == :true_terminal, do: @true_node_id, else: @false_node_id true -> # Recursive case u1_details = get_node_details(u1_id) u2_details = get_node_details(u2_id) result_id = cond do # Case: u1 is terminal, u2 is not u1_details == :true_terminal or u1_details == :false_terminal -> {var2, y2, n2, d2} = u2_details res_y = apply(op_name, op_lambda, u1_id, y2) res_n = apply(op_name, op_lambda, u1_id, n2) res_d = apply(op_name, op_lambda, u1_id, d2) make_node(var2, res_y, res_n, res_d) # make_node is make_node_raw # Case: u2 is terminal, u1 is not u2_details == :true_terminal or u2_details == :false_terminal -> {var1, y1, n1, d1} = u1_details res_y = apply(op_name, op_lambda, y1, u2_id) res_n = apply(op_name, op_lambda, n1, u2_id) res_d = apply(op_name, op_lambda, d1, u2_id) make_node(var1, res_y, res_n, res_d) # make_node is make_node_raw # Case: Both u1 and u2 are non-terminal nodes true -> {var1, y1, n1, d1} = u1_details {var2, y2, n2, d2} = u2_details top_var = cond do var1 == var2 -> var1 var1 < var2 -> var1 true -> var2 # var2 < var1 end # Recursive calls based on top_var res_y = cond do top_var == var1 && top_var == var2 -> apply(op_name, op_lambda, y1, y2) top_var == var1 -> apply(op_name, op_lambda, y1, u2_id) # u2_id passed through true -> apply(op_name, op_lambda, u1_id, y2) # u1_id passed through end res_n = cond do top_var == var1 && top_var == var2 -> apply(op_name, op_lambda, n1, n2) top_var == var1 -> apply(op_name, op_lambda, n1, u2_id) true -> apply(op_name, op_lambda, u1_id, n2) end res_d = cond do top_var == var1 && top_var == var2 -> apply(op_name, op_lambda, d1, d2) top_var == var1 -> apply(op_name, op_lambda, d1, u2_id) true -> apply(op_name, op_lambda, u1_id, d2) end make_node(top_var, res_y, res_n, res_d) # make_node is make_node_raw end update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id end end def sum(tdd1_id, tdd2_id) do op_lambda_sum = fn :true_terminal, _ -> :true_terminal _, :true_terminal -> :true_terminal :false_terminal, t2_val -> t2_val t1_val, :false_terminal -> t1_val end apply(:sum, op_lambda_sum, tdd1_id, tdd2_id) end # --- Intersection --- def intersect(tdd1_id, tdd2_id) do op_lambda_intersect = fn # AND logic for terminals # false and X = false :false_terminal, _ -> :false_terminal # X and false = false _, :false_terminal -> :false_terminal # true and X = X (e.g. true and true = true, true and false = false) :true_terminal, t2_val -> t2_val # X and true = X t1_val, :true_terminal -> t1_val end apply(:intersect, op_lambda_intersect, tdd1_id, tdd2_id) end # --- Negation --- def negate(tdd_id) do state = get_state() # Unary operation cache key cache_key = {:negate, tdd_id} cond do # Handle terminals directly, no caching needed for these simple cases. tdd_id == @true_node_id -> @false_node_id tdd_id == @false_node_id -> @true_node_id # Check cache for non-terminal IDs Map.has_key?(state.op_cache, cache_key) -> state.op_cache[cache_key] # Compute for non-terminal ID if not in cache true -> case get_node_details(tdd_id) do {var, y, n, d} -> res_y = negate(y) res_n = negate(n) res_d = negate(d) result_id = make_node(var, res_y, res_n, res_d) # Cache the computed result for this non-terminal tdd_id update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id nil -> # This should ideally not be reached if IDs are managed correctly raise "Tdd.negate: Unknown node ID #{inspect(tdd_id)} during get_node_details. State: #{inspect(state.node_by_id)}" end end end # --- Subtyping --- # A <: B (A is a subtype of B) def is_subtype(sub_type_id, super_type_id) do cond do # Optimization: A is always a subtype of A sub_type_id == super_type_id -> true # Optimization: `none` is a subtype of anything sub_type_id == @false_node_id -> true # Optimization: Anything is a subtype of `any` super_type_id == @true_node_id -> true true -> # Definition: A <: B <=> A ∩ (¬B) == ∅ (type_none / @false_node_id) negated_super = negate(super_type_id) intersection_result = intersect(sub_type_id, negated_super) intersection_result == @false_node_id end end def print_tdd(id, indent \\ 0) do prefix = String.duplicate(" ", indent) details = get_node_details(id) IO.puts("#{prefix}ID #{id}: #{inspect(details)}") case details do {_var, y, n, d} -> IO.puts("#{prefix} Yes ->") print_tdd(y, indent + 1) IO.puts("#{prefix} No ->") print_tdd(n, indent + 1) IO.puts("#{prefix} DC ->") print_tdd(d, indent + 1) :true_terminal -> :ok :false_terminal -> :ok nil -> IO.puts("#{prefix} Error: Unknown ID #{id}") end end end # --- Example Usage --- Tdd.init_tdd_system() # Basic Types tdd_foo = Tdd.type_atom_literal(:foo) tdd_bar = Tdd.type_atom_literal(:bar) tdd_atom = Tdd.type_atom() tdd_empty_tuple = Tdd.type_empty_tuple() tdd_any = Tdd.type_any() tdd_none = Tdd.type_none() IO.puts("\n--- TDD for :foo ---") Tdd.print_tdd(tdd_foo) IO.puts("\n--- TDD for not :foo ---") Tdd.print_tdd(Tdd.negate(tdd_foo)) IO.puts("\n--- TDD for atom ---") Tdd.print_tdd(tdd_atom) IO.puts("\n--- TDD for not atom ---") # Expected: make_node(@v_is_atom, @false_node_id, @true_node_id, @true_node_id) # This represents "anything that is not an atom". The DC branch becomes true because if # "is_atom" test is irrelevant for "not atom", it means it's part of "not atom". Tdd.print_tdd(Tdd.negate(tdd_atom)) IO.puts("\n--- TDD for :foo and :bar (should be none) ---") tdd_foo_and_bar = Tdd.intersect(tdd_foo, tdd_bar) # Expected ID 0: :false_terminal Tdd.print_tdd(tdd_foo_and_bar) IO.puts("\n--- TDD for :foo and atom (should be :foo) ---") tdd_foo_and_atom = Tdd.intersect(tdd_foo, tdd_atom) # Expected to be structurally identical to tdd_foo Tdd.print_tdd(tdd_foo_and_atom) test = fn name, expected, result -> current_failures = Process.get(:test_failures, []) if expected != result do Process.put(:test_failures, [name | current_failures]) end status = if expected == result, do: "PASSED", else: "FAILED" IO.puts("#{name} (Expected: #{expected}) -> Result: #{result} - #{status}") end # Basic Types tdd_foo = Tdd.type_atom_literal(:foo) tdd_bar = Tdd.type_atom_literal(:bar) tdd_baz = Tdd.type_atom_literal(:baz) tdd_atom = Tdd.type_atom() tdd_empty_tuple = Tdd.type_empty_tuple() tdd_tuple = Tdd.type_tuple() # Tuple of size 2, e.g. {any, any} tdd_tuple_s2 = Tdd.type_tuple_sized_any(2) tdd_any = Tdd.type_any() tdd_none = Tdd.type_none() IO.puts("\n--- Basic Subtyping Tests ---") test.(":foo <: atom", true, Tdd.is_subtype(tdd_foo, tdd_atom)) test.("atom <: :foo", false, Tdd.is_subtype(tdd_atom, tdd_foo)) test.(":foo <: :bar", false, Tdd.is_subtype(tdd_foo, tdd_bar)) test.(":foo <: :foo", true, Tdd.is_subtype(tdd_foo, tdd_foo)) test.("{} <: tuple", true, Tdd.is_subtype(tdd_empty_tuple, tdd_tuple)) test.("tuple <: {}", false, Tdd.is_subtype(tdd_tuple, tdd_empty_tuple)) test.(":foo <: {}", false, Tdd.is_subtype(tdd_foo, tdd_empty_tuple)) test.("tuple_size_2 <: tuple", true, Tdd.is_subtype(tdd_tuple_s2, tdd_tuple)) test.("tuple <: tuple_size_2", false, Tdd.is_subtype(tdd_tuple, tdd_tuple_s2)) test.("tuple_size_2 <: {}", false, Tdd.is_subtype(tdd_tuple_s2, tdd_empty_tuple)) IO.puts("\n--- Any/None Subtyping Tests ---") test.("any <: :foo", false, Tdd.is_subtype(tdd_any, tdd_foo)) test.(":foo <: any", true, Tdd.is_subtype(tdd_foo, tdd_any)) test.("none <: :foo", true, Tdd.is_subtype(tdd_none, tdd_foo)) test.(":foo <: none", false, Tdd.is_subtype(tdd_foo, tdd_none)) test.("none <: any", true, Tdd.is_subtype(tdd_none, tdd_any)) test.("any <: none", false, Tdd.is_subtype(tdd_any, tdd_none)) test.("any <: any", true, Tdd.is_subtype(tdd_any, tdd_any)) test.("none <: none", true, Tdd.is_subtype(tdd_none, tdd_none)) IO.puts("\n--- Union related Subtyping ---") tdd_foo_or_bar = Tdd.sum(tdd_foo, tdd_bar) tdd_foo_or_bar_or_baz = Tdd.sum(tdd_foo_or_bar, tdd_baz) test.(":foo <: (:foo | :bar)", true, Tdd.is_subtype(tdd_foo, tdd_foo_or_bar)) test.(":baz <: (:foo | :bar)", false, Tdd.is_subtype(tdd_baz, tdd_foo_or_bar)) test.("(:foo | :bar) <: atom", true, Tdd.is_subtype(tdd_foo_or_bar, tdd_atom)) test.("atom <: (:foo | :bar)", false, Tdd.is_subtype(tdd_atom, tdd_foo_or_bar)) test.( "(:foo | :bar) <: (:foo | :bar | :baz)", true, Tdd.is_subtype(tdd_foo_or_bar, tdd_foo_or_bar_or_baz) ) test.( "(:foo | :bar | :baz) <: (:foo | :bar)", false, Tdd.is_subtype(tdd_foo_or_bar_or_baz, tdd_foo_or_bar) ) # Test against a non-member of the union test.("(:foo | :bar) <: :baz", false, Tdd.is_subtype(tdd_foo_or_bar, tdd_baz)) IO.puts("\n--- Intersection related Subtyping ---") # Should be equivalent to tdd_foo tdd_atom_and_foo = Tdd.intersect(tdd_atom, tdd_foo) # Should be tdd_none tdd_atom_and_tuple = Tdd.intersect(tdd_atom, tdd_tuple) test.("(atom & :foo) <: :foo", true, Tdd.is_subtype(tdd_atom_and_foo, tdd_foo)) test.(":foo <: (atom & :foo)", true, Tdd.is_subtype(tdd_foo, tdd_atom_and_foo)) test.("(atom & tuple) <: none", true, Tdd.is_subtype(tdd_atom_and_tuple, tdd_none)) test.("none <: (atom & tuple)", true, Tdd.is_subtype(tdd_none, tdd_atom_and_tuple)) test.("(atom & :foo) <: :bar", false, Tdd.is_subtype(tdd_atom_and_foo, tdd_bar)) # An intersection is a subtype of its components test.("(atom & :foo) <: atom", true, Tdd.is_subtype(tdd_atom_and_foo, tdd_atom)) # (none <: atom) test.("(atom & tuple) <: atom", true, Tdd.is_subtype(tdd_atom_and_tuple, tdd_atom)) # (none <: tuple) test.("(atom & tuple) <: tuple", true, Tdd.is_subtype(tdd_atom_and_tuple, tdd_tuple)) IO.puts("\n--- Negation related Subtyping (Contrapositives) ---") # Reminder: ¬A <: ¬B is equivalent to B <: A (contrapositive) # Test 1: ¬atom <: ¬:foo (Equivalent to :foo <: atom, which is true) test.("¬atom <: ¬:foo", true, Tdd.is_subtype(Tdd.negate(tdd_atom), Tdd.negate(tdd_foo))) # Test 2: ¬:foo <: ¬atom (Equivalent to atom <: :foo, which is false) test.("¬:foo <: ¬atom", false, Tdd.is_subtype(Tdd.negate(tdd_foo), Tdd.negate(tdd_atom))) # Double negation test.("¬(¬:foo) <: :foo", true, Tdd.is_subtype(Tdd.negate(Tdd.negate(tdd_foo)), tdd_foo)) test.(":foo <: ¬(¬:foo)", true, Tdd.is_subtype(tdd_foo, Tdd.negate(Tdd.negate(tdd_foo)))) # Disjoint types test.("atom <: ¬tuple", true, Tdd.is_subtype(tdd_atom, Tdd.negate(tdd_tuple))) test.("tuple <: ¬atom", true, Tdd.is_subtype(tdd_tuple, Tdd.negate(tdd_atom))) test.(":foo <: ¬{}", true, Tdd.is_subtype(tdd_foo, Tdd.negate(tdd_empty_tuple))) IO.puts("\n--- Mixed Types & Complex Subtyping ---") tdd_atom_or_tuple = Tdd.sum(tdd_atom, tdd_tuple) tdd_foo_or_empty_tuple = Tdd.sum(tdd_foo, tdd_empty_tuple) test.( "(:foo | {}) <: (atom | tuple)", true, Tdd.is_subtype(tdd_foo_or_empty_tuple, tdd_atom_or_tuple) ) test.( "(atom | tuple) <: (:foo | {})", false, Tdd.is_subtype(tdd_atom_or_tuple, tdd_foo_or_empty_tuple) ) test.(":foo <: (atom | tuple)", true, Tdd.is_subtype(tdd_foo, tdd_atom_or_tuple)) test.("{} <: (atom | tuple)", true, Tdd.is_subtype(tdd_empty_tuple, tdd_atom_or_tuple)) # De Morgan's Law illustration (A | B = ¬(¬A & ¬B)) # (:foo | :bar) <: ¬(¬:foo & ¬:bar) tdd_not_foo_and_not_bar = Tdd.intersect(Tdd.negate(tdd_foo), Tdd.negate(tdd_bar)) test.( "(:foo | :bar) <: ¬(¬:foo & ¬:bar)", true, Tdd.is_subtype(tdd_foo_or_bar, Tdd.negate(tdd_not_foo_and_not_bar)) ) test.( "¬(¬:foo & ¬:bar) <: (:foo | :bar)", true, Tdd.is_subtype(Tdd.negate(tdd_not_foo_and_not_bar), tdd_foo_or_bar) ) # Type difference: atom - :foo (represented as atom & ¬:foo) tdd_atom_minus_foo = Tdd.intersect(tdd_atom, Tdd.negate(tdd_foo)) test.("(atom - :foo) <: atom", true, Tdd.is_subtype(tdd_atom_minus_foo, tdd_atom)) test.("(atom - :foo) <: :foo", false, Tdd.is_subtype(tdd_atom_minus_foo, tdd_foo)) # True if :bar is in (atom - :foo) test.("(atom - :foo) <: :bar", true, Tdd.is_subtype(tdd_atom_minus_foo, tdd_bar)) test.(":bar <: (atom - :foo)", true, Tdd.is_subtype(tdd_bar, tdd_atom_minus_foo)) # (atom - :foo) | :foo should be atom tdd_recombined_atom = Tdd.sum(tdd_atom_minus_foo, tdd_foo) test.("((atom - :foo) | :foo) <: atom", true, Tdd.is_subtype(tdd_recombined_atom, tdd_atom)) test.("atom <: ((atom - :foo) | :foo)", true, Tdd.is_subtype(tdd_atom, tdd_recombined_atom)) # (atom | {}) & (tuple | :foo) must be (:foo | {}) # Represents `atom() | {}` tdd_atom_or_empty = Tdd.sum(tdd_atom, tdd_empty_tuple) # Represents `tuple() | :foo` tdd_tuple_or_foo = Tdd.sum(tdd_tuple, tdd_foo) intersected_complex = Tdd.intersect(tdd_atom_or_empty, tdd_tuple_or_foo) # Expected result for intersected_complex is tdd_foo_or_empty_tuple test.( "(atom | {}) & (tuple | :foo) <: (:foo | {})", true, Tdd.is_subtype(intersected_complex, tdd_foo_or_empty_tuple) ) test.( "(:foo | {}) <: (atom | {}) & (tuple | :foo)", true, Tdd.is_subtype(tdd_foo_or_empty_tuple, intersected_complex) ) # {} | tuple_size_2 should be a subtype of tuple tdd_empty_or_s2 = Tdd.sum(tdd_empty_tuple, tdd_tuple_s2) test.("({} | tuple_size_2) <: tuple", true, Tdd.is_subtype(tdd_empty_or_s2, tdd_tuple)) test.( "({} | tuple_size_2) <: ({} | tuple_size_2)", true, Tdd.is_subtype(tdd_empty_or_s2, tdd_empty_or_s2) ) test.("({} | tuple_size_2) <: tuple_size_2", false, Tdd.is_subtype(tdd_empty_or_s2, tdd_tuple_s2)) IO.puts("\n--- TDD structure for (atom - :foo) ---") Tdd.print_tdd(tdd_atom_minus_foo) IO.puts("\n--- TDD structure for ((atom - :foo) | :foo) which should be 'atom' ---") Tdd.print_tdd(tdd_recombined_atom) IO.puts("\n--- TDD structure for 'atom' for comparison ---") Tdd.print_tdd(tdd_atom) IO.inspect(Process.get(:test_failures, [])) IO.inspect("tdd_atom_and_tuple") Tdd.print_tdd(tdd_atom_and_tuple) IO.inspect("tdd_none") Tdd.print_tdd(tdd_none)