# --- 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() 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() test_all = fn -> 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) 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", false, 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, [])) end defmodule IntegerTests do def run(test_fn) do Process.put(:test_failures, []) # Reset for each test group if needed, or once globally Tdd.init_tdd_system() # Integer types tdd_int = Tdd.type_integer() tdd_int_5 = Tdd.type_int_eq(5) tdd_int_7 = Tdd.type_int_eq(7) # x < 10 tdd_int_lt_10 = Tdd.type_int_lt(10) # x > 3 tdd_int_gt_3 = Tdd.type_int_gt(3) # x < 3 tdd_int_lt_3 = Tdd.type_int_lt(3) # x > 10 tdd_int_gt_10 = Tdd.type_int_gt(10) tdd_atom_foo = Tdd.type_atom_literal(:foo) # # IO.puts("\n--- Integer Type Structures ---") # IO.puts("Integer:") # Tdd.print_tdd(tdd_int) # IO.puts("Int == 5:") # Tdd.print_tdd(tdd_int_5) # IO.puts("Int < 10:") # Tdd.print_tdd(tdd_int_lt_10) IO.puts("\n--- Integer Subtyping Tests ---") test_fn.("int_5 <: integer", true, Tdd.is_subtype(tdd_int_5, tdd_int)) test_fn.("integer <: int_5", false, Tdd.is_subtype(tdd_int, tdd_int_5)) test_fn.("int_5 <: int_7", false, Tdd.is_subtype(tdd_int_5, tdd_int_7)) test_fn.("int_5 <: int_5", true, Tdd.is_subtype(tdd_int_5, tdd_int_5)) test_fn.("int_5 <: atom_foo", false, Tdd.is_subtype(tdd_int_5, tdd_atom_foo)) test_fn.("int_lt_10 <: integer", true, Tdd.is_subtype(tdd_int_lt_10, tdd_int)) test_fn.("integer <: int_lt_10", false, Tdd.is_subtype(tdd_int, tdd_int_lt_10)) # 5 < 10 test_fn.("int_5 <: int_lt_10", true, Tdd.is_subtype(tdd_int_5, tdd_int_lt_10)) test_fn.("int_lt_10 <: int_5", false, Tdd.is_subtype(tdd_int_lt_10, tdd_int_5)) test_fn.("int_gt_3 <: integer", true, Tdd.is_subtype(tdd_int_gt_3, tdd_int)) # 5 > 3 test_fn.("int_5 <: int_gt_3", true, Tdd.is_subtype(tdd_int_5, tdd_int_gt_3)) test_fn.("int_gt_3 <: int_5", false, Tdd.is_subtype(tdd_int_gt_3, tdd_int_5)) # x < 3 implies x < 10 test_fn.("int_lt_3 <: int_lt_10", true, Tdd.is_subtype(tdd_int_lt_3, tdd_int_lt_10)) # x > 10 implies x > 3 test_fn.("int_gt_10 <: int_gt_3", true, Tdd.is_subtype(tdd_int_gt_10, tdd_int_gt_3)) test_fn.("int_lt_10 <: int_lt_3", false, Tdd.is_subtype(tdd_int_lt_10, tdd_int_lt_3)) test_fn.("int_gt_3 <: int_gt_10", false, Tdd.is_subtype(tdd_int_gt_3, tdd_int_gt_10)) IO.puts("\n--- Integer Intersection Tests (should resolve to none for contradictions) ---") intersect_5_7 = Tdd.intersect(tdd_int_5, tdd_int_7) test_fn.("int_5 & int_7 == none", true, intersect_5_7 == Tdd.type_none()) # IO.puts("Structure of int_5 & int_7 (should be ID 0):") # Tdd.print_tdd(intersect_5_7) # x < 3 AND x > 10 intersect_lt3_gt10 = Tdd.intersect(tdd_int_lt_3, tdd_int_gt_10) test_fn.("int_lt_3 & int_gt_10 == none", true, intersect_lt3_gt10 == Tdd.type_none()) # IO.puts("Structure of int_lt_3 & int_gt_10 (should be ID 0):") # Tdd.print_tdd(intersect_lt3_gt10) # x < 10 AND x > 3 (e.g. 4,5..9) intersect_lt10_gt3 = Tdd.intersect(tdd_int_lt_10, tdd_int_gt_3) test_fn.("int_lt_10 & int_gt_3 != none", true, intersect_lt10_gt3 != Tdd.type_none()) # IO.puts("Structure of int_lt_10 & int_gt_3 (should be non-empty):") # Tdd.print_tdd(intersect_lt10_gt3) # Test a value within this intersection test_fn.( "int_5 <: (int_lt_10 & int_gt_3)", true, Tdd.is_subtype(tdd_int_5, intersect_lt10_gt3) ) # x == 5 AND x < 3 intersect_5_lt3 = Tdd.intersect(tdd_int_5, tdd_int_lt_3) test_fn.("int_5 & int_lt_3 == none", true, intersect_5_lt3 == Tdd.type_none()) IO.puts("\n--- Integer Union Tests ---") union_5_7 = Tdd.sum(tdd_int_5, tdd_int_7) test_fn.("int_5 <: (int_5 | int_7)", true, Tdd.is_subtype(tdd_int_5, union_5_7)) test_fn.("int_7 <: (int_5 | int_7)", true, Tdd.is_subtype(tdd_int_7, union_5_7)) test_fn.("int_lt_3 <: (int_5 | int_7)", false, Tdd.is_subtype(tdd_int_lt_3, union_5_7)) # IO.puts("Structure of int_5 | int_7:") # Tdd.print_tdd(union_5_7) # (int < 3) | (int > 10) union_disjoint_ranges = Tdd.sum(tdd_int_lt_3, tdd_int_gt_10) test_fn.( "int_eq(0) <: (int < 3 | int > 10)", true, Tdd.is_subtype(Tdd.type_int_eq(0), union_disjoint_ranges) ) test_fn.( "int_eq(5) <: (int < 3 | int > 10)", false, Tdd.is_subtype(Tdd.type_int_eq(5), union_disjoint_ranges) ) test_fn.( "int_eq(12) <: (int < 3 | int > 10)", true, Tdd.is_subtype(Tdd.type_int_eq(12), union_disjoint_ranges) ) IO.inspect(Process.get(:test_failures, [])) end end defmodule TupleTests do import Tdd def run(test_fn) do Process.put(:test_failures, []) # Re-init the system for a clean slate for these tests Tdd.init_tdd_system() IO.puts("\n--- Running TupleTests ---") # --- Basic Types for convenience --- t_atom = type_atom() t_int = type_integer() t_foo = type_atom_literal(:foo) t_bar = type_atom_literal(:bar) t_int_5 = type_int_eq(5) t_int_6 = type_int_eq(6) t_int_pos = type_int_gt(0) t_any = type_any() t_none = type_none() # any tuple t_tuple = type_tuple() t_empty_tuple = type_empty_tuple() # --- Specific Tuple Types --- # {atom(), integer()} tup_atom_int = type_tuple([t_atom, t_int]) # {:foo, 5} tup_foo_5 = type_tuple([t_foo, t_int_5]) # {pos_integer(), atom()} tup_pos_atom = type_tuple([t_int_pos, t_atom]) # {atom(), any} tup_atom_any = type_tuple([t_atom, t_any]) # {any, integer()} tup_any_int = type_tuple([t_any, t_int]) # a tuple of size 2, {any, any} tup_s2_any = type_tuple_sized_any(2) # a tuple of size 3, {any, any, any} tup_s3_any = type_tuple_sized_any(3) # {integer(), atom()} tup_int_atom = type_tuple([t_int, t_atom]) # {{:foo}} tup_nested_foo = type_tuple([type_tuple([t_foo])]) # {{atom()}} tup_nested_atom = type_tuple([type_tuple([t_atom])]) # {any, none} -> this should resolve to none tup_with_none = type_tuple([t_any, t_none]) IO.puts("\n--- Section: Basic Subtyping ---") test_fn.("{:foo, 5} <: {atom, int}", true, is_subtype(tup_foo_5, tup_atom_int)) test_fn.("{atom, int} <: {:foo, 5}", false, is_subtype(tup_atom_int, tup_foo_5)) test_fn.("{:foo, 5} <: {pos_int, atom}", false, is_subtype(tup_foo_5, tup_pos_atom)) test_fn.("{pos_int, atom} <: {atom, int}", false, is_subtype(tup_pos_atom, tup_atom_int)) test_fn.("{atom, int} <: tuple()", true, is_subtype(tup_atom_int, t_tuple)) test_fn.("tuple() <: {atom, int}", false, is_subtype(t_tuple, tup_atom_int)) IO.puts("\n--- Section: Size-related Subtyping ---") test_fn.("{atom, int} <: tuple_size_2_any", true, is_subtype(tup_atom_int, tup_s2_any)) test_fn.("tuple_size_2_any <: {atom, int}", false, is_subtype(tup_s2_any, tup_atom_int)) test_fn.("{atom, int} <: tuple_size_3_any", false, is_subtype(tup_atom_int, tup_s3_any)) test_fn.("tuple_size_2_any <: tuple_size_3_any", false, is_subtype(tup_s2_any, tup_s3_any)) test_fn.("{} <: tuple()", true, is_subtype(t_empty_tuple, t_tuple)) test_fn.("{} <: tuple_size_2_any", false, is_subtype(t_empty_tuple, tup_s2_any)) IO.puts("\n--- Section: Intersection ---") # {atom, any} & {any, int} -> {atom, int} intersect1 = intersect(tup_atom_any, tup_any_int) test_fn.("({atom,any} & {any,int}) == {atom,int}", true, intersect1 == tup_atom_int) # {atom, int} & {int, atom} -> none intersect2 = intersect(tup_atom_int, tup_int_atom) test_fn.("({atom,int} & {int,atom}) == none", true, intersect2 == t_none) # tuple_size_2 & tuple_size_3 -> none intersect3 = intersect(tup_s2_any, tup_s3_any) test_fn.("(tuple_size_2 & tuple_size_3) == none", true, intersect3 == t_none) # tuple() & {atom, int} -> {atom, int} intersect4 = intersect(t_tuple, tup_atom_int) test_fn.("(tuple() & {atom,int}) == {atom,int}", true, intersect4 == tup_atom_int) IO.puts("\n--- Section: Union ---") # {:foo, 5} | {pos_int, atom} union1 = sum(tup_foo_5, tup_pos_atom) test_fn.("{:foo, 5} <: ({:foo, 5} | {pos_int, atom})", true, is_subtype(tup_foo_5, union1)) test_fn.( "{pos_int, atom} <: ({:foo, 5} | {pos_int, atom})", true, is_subtype(tup_pos_atom, union1) ) test_fn.( "{atom, int} <: ({:foo, 5} | {pos_int, atom})", false, is_subtype(tup_atom_int, union1) ) # {atom, any} | {any, int} -> a complex type, let's check subtyping against it union2 = sum(tup_atom_any, tup_any_int) # {atom, int} is in both parts of the union. test_fn.("{atom, int} <: ({atom,any} | {any,int})", true, is_subtype(tup_atom_int, union2)) # {:foo, :bar} is only in {atom, any}. test_fn.( "{:foo, :bar} <: ({atom,any} | {any,int})", true, is_subtype(type_tuple([t_foo, t_bar]), union2) ) # {5, 6} is only in {any, int}. test_fn.( "{5, 6} <: ({atom,any} | {any,int})", true, is_subtype(type_tuple([t_int_5, t_int_6]), union2) ) # {5, :foo} is in neither part of the union. test_fn.( "{5, :foo} <: ({atom,any} | {any,int})", false, is_subtype(type_tuple([t_int_5, t_foo]), union2) ) IO.puts("\n--- Section: Negation and Type Difference ---") # atom is disjoint from tuple, so atom <: ¬tuple test_fn.("atom <: ¬tuple", true, is_subtype(t_atom, negate(t_tuple))) # A specific tuple should not be a subtype of the negation of a more general tuple type it belongs to test_fn.("{atom, int} <: ¬tuple()", false, is_subtype(tup_atom_int, negate(t_tuple))) # {int, atom} is a subtype of ¬{atom, int} because their elements differ test_fn.("{int, atom} <: ¬{atom, int}", true, is_subtype(tup_int_atom, negate(tup_atom_int))) # tuple_size_3 is a subtype of ¬tuple_size_2 because their sizes differ test_fn.("tuple_size_3 <: ¬tuple_size_2", true, is_subtype(tup_s3_any, negate(tup_s2_any))) # Type difference: tuple_size_2 - {atom, any} -> should be {¬atom, any} for size 2 tuples. diff1 = intersect(tup_s2_any, negate(tup_atom_any)) # {integer, integer} has a first element that is not an atom, so it should be in the difference. tup_int_int = type_tuple([t_int, t_int]) test_fn.("{int, int} <: (tuple_size_2 - {atom, any})", true, is_subtype(tup_int_int, diff1)) test_fn.( "{atom, int} <: (tuple_size_2 - {atom, any})", false, is_subtype(tup_atom_int, diff1) ) IO.puts("\n--- Section: Nested Tuples ---") test_fn.("{{:foo}} <: {{atom}}", true, is_subtype(tup_nested_foo, tup_nested_atom)) test_fn.("{{atom}} <: {{:foo}}", false, is_subtype(tup_nested_atom, tup_nested_foo)) # Intersection of disjoint nested types: {{:foo}} & {{:bar}} intersect_nested = intersect(tup_nested_foo, type_tuple([type_tuple([t_bar])])) test_fn.("{{:foo}} & {{:bar}} == none", true, intersect_nested == t_none) # Union of nested types union_nested = sum(tup_nested_foo, type_tuple([type_tuple([t_bar])])) test_fn.("{{:foo}} <: ({{:foo}} | {{:bar}})", true, is_subtype(tup_nested_foo, union_nested)) test_fn.( "{{:bar}} <: ({{:foo}} | {{:bar}})", true, is_subtype(type_tuple([type_tuple([t_bar])]), union_nested) ) test_fn.( "{{atom}} <: ({{:foo}} | {{:bar}})", false, is_subtype(tup_nested_atom, union_nested) ) IO.puts("\n--- Section: Edge Cases (any, none) ---") # A type `{any, none}` should not be possible. The value `none` cannot exist. # The simplification logic should reduce this to `type_none`. test_fn.("{any, none} == none", true, tup_with_none == t_none) # Intersection with a tuple containing none should result in none intersect_with_none_tuple = intersect(tup_atom_int, tup_with_none) test_fn.("{atom,int} & {any,none} == none", true, intersect_with_none_tuple == t_none) # Union with a tuple containing none should be a no-op union_with_none_tuple = sum(tup_atom_int, tup_with_none) test_fn.("{atom,int} | {any,none} == {atom,int}", true, union_with_none_tuple == tup_atom_int) # --- Original tests from problem description for regression --- IO.puts("\n--- Specific Tuple Subtyping Test (Original) ---") test_fn.( "{1, :foo} <: {int_gt_0, :foo | :bar}", true, is_subtype( type_tuple([type_int_eq(1), type_atom_literal(:foo)]), type_tuple([type_int_gt(0), sum(type_atom_literal(:foo), type_atom_literal(:bar))]) ) ) test_fn.( "{0, :foo} <: {int_gt_0, :foo | :bar}", false, is_subtype( type_tuple([type_int_eq(0), type_atom_literal(:foo)]), type_tuple([type_int_gt(0), sum(type_atom_literal(:foo), type_atom_literal(:bar))]) ) ) test_fn.( "{1, :kek} <: {int_gt_0, :foo | :bar}", false, is_subtype( type_tuple([ type_int_eq(1), type_atom_literal(:kek) ]), type_tuple([type_int_gt(0), sum(type_atom_literal(:foo), type_atom_literal(:bar))]) ) ) IO.inspect(Process.get(:test_failures, []), label: "TupleTests failures") end end defmodule ListTests do import Tdd def run(test_fn) do Process.put(:test_failures, []) Tdd.init_tdd_system() IO.puts("\n--- Running ListTests ---") # --- Basic Types --- t_atom = type_atom() t_int = type_integer() t_foo = type_atom_literal(:foo) t_bar = type_atom_literal(:bar) t_any = type_any() t_none = type_none() # --- List Types --- t_list = type_list() t_empty_list = type_empty_list() # [atom | list] t_cons_atom_list = type_cons(t_atom, t_list) # [:foo | []] t_cons_foo_empty = type_cons(t_foo, t_empty_list) # [atom | []] t_cons_atom_empty = type_cons(t_atom, t_empty_list) # [any | []] t_cons_any_empty = type_cons(t_any, t_empty_list) # [integer | list] t_cons_int_list = type_cons(t_int, t_list) IO.puts("\n--- Section: Basic List Subtyping ---") test_fn.("[] <: list", true, is_subtype(t_empty_list, t_list)) test_fn.("list <: []", false, is_subtype(t_list, t_empty_list)) test_fn.("[atom|list] <: list", true, is_subtype(t_cons_atom_list, t_list)) test_fn.("list <: [atom|list]", false, is_subtype(t_list, t_cons_atom_list)) test_fn.("[] <: [atom|list]", false, is_subtype(t_empty_list, t_cons_atom_list)) test_fn.("[atom|list] <: []", false, is_subtype(t_cons_atom_list, t_empty_list)) test_fn.("list <: atom", false, is_subtype(t_list, t_atom)) test_fn.("atom <: list", false, is_subtype(t_atom, t_list)) IO.puts("\n--- Section: Cons Subtyping (Covariance) ---") # Head is a subtype test_fn.("[:foo|[]] <: [atom|[]]", true, is_subtype(t_cons_foo_empty, t_cons_atom_empty)) test_fn.("[atom|[]] <: [:foo|[]]", false, is_subtype(t_cons_atom_empty, t_cons_foo_empty)) # Tail is a subtype test_fn.("[atom|[]] <: [atom|list]", true, is_subtype(t_cons_atom_empty, t_cons_atom_list)) test_fn.("[atom|list] <: [atom|[]]", false, is_subtype(t_cons_atom_list, t_cons_atom_empty)) # Both are subtypes test_fn.("[:foo|[]] <: [atom|list]", true, is_subtype(t_cons_foo_empty, t_cons_atom_list)) # Neither is a subtype test_fn.("[atom|list] <: [:foo|[]]", false, is_subtype(t_cons_atom_list, t_cons_foo_empty)) # A list of length 1 is a subtype of a list of any element of length 1 test_fn.("[atom|[]] <: [any|[]]", true, is_subtype(t_cons_atom_empty, t_cons_any_empty)) IO.puts("\n--- Section: List Intersection ---") # [atom|list] & [integer|list] -> should be none due to head conflict intersect1 = intersect(t_cons_atom_list, t_cons_int_list) test_fn.("[atom|list] & [integer|list] == none", true, intersect1 == t_none) # [any|[]] & [atom|list] -> should be [atom|[]] intersect2 = intersect(t_cons_any_empty, t_cons_atom_list) test_fn.("([any|[]] & [atom|list]) == [atom|[]]", true, intersect2 == t_cons_atom_empty) # [] & [atom|list] -> should be none because one is empty and one is not intersect3 = intersect(t_empty_list, t_cons_atom_list) test_fn.("[] & [atom|list] == none", true, intersect3 == t_none) IO.puts("\n--- Section: List Union ---") # [] | [atom|[]] union1 = sum(t_empty_list, t_cons_atom_empty) test_fn.("[] <: ([] | [atom|[]])", true, is_subtype(t_empty_list, union1)) test_fn.("[atom|[]] <: ([] | [atom|[]])", true, is_subtype(t_cons_atom_empty, union1)) test_fn.( "[integer|[]] <: ([] | [atom|[]])", false, is_subtype(type_cons(t_int, t_empty_list), union1) ) # [:foo|[]] | [:bar|[]] union2 = sum(t_cons_foo_empty, type_cons(t_bar, t_empty_list)) # This union is a subtype of [atom|[]] test_fn.("([:foo|[]] | [:bar|[]]) <: [atom|[]]", true, is_subtype(union2, t_cons_atom_empty)) test_fn.("[atom|[]] <: ([:foo|[]] | [:bar|[]])", false, is_subtype(t_cons_atom_empty, union2)) IO.puts("\n--- Section: List Negation ---") # list is a subtype of not(atom) test_fn.("list <: ¬atom", true, is_subtype(t_list, negate(t_atom))) # A non-empty list is a subtype of not an empty list test_fn.("[atom|list] <: ¬[]", true, is_subtype(t_cons_atom_list, negate(t_empty_list))) # [integer|list] is a subtype of not [atom|list] test_fn.( "[integer|list] <: ¬[atom|list]", true, is_subtype(t_cons_int_list, negate(t_cons_atom_list)) ) IO.inspect(Process.get(:test_failures, []), label: "ListTests failures") end end defmodule ListOfTests do import Tdd def run(test_fn) do Process.put(:test_failures, []) Tdd.init_tdd_system() IO.puts("\n--- Running ListOfTests ---") # --- Basic Types --- t_atom = type_atom() t_int = type_integer() t_pos_int = type_int_gt(0) t_int_5 = type_int_eq(5) # --- list(X) Types --- t_list_of_int = type_list_of(t_int) t_list_of_pos_int = type_list_of(t_pos_int) t_list_of_atom = type_list_of(t_atom) # --- Specific List Types --- t_list = type_list() t_empty_list = type_empty_list() # [5] t_list_one_int = type_cons(t_int_5, t_empty_list) # [:foo] t_list_one_atom = type_cons(type_atom_literal(:foo), t_empty_list) # [5, :foo] t_list_int_and_atom = type_cons(t_int_5, type_cons(type_atom_literal(:foo), t_empty_list)) IO.puts("\n--- Section: Basic list(X) Subtyping ---") test_fn.("list(integer) <: list()", true, is_subtype(t_list_of_int, t_list)) test_fn.("list() <: list(integer)", false, is_subtype(t_list, t_list_of_int)) test_fn.("[] <: list(integer)", true, is_subtype(t_empty_list, t_list_of_int)) test_fn.("[5] <: list(integer)", true, is_subtype(t_list_one_int, t_list_of_int)) test_fn.("[:foo] <: list(integer)", false, is_subtype(t_list_one_atom, t_list_of_int)) test_fn.("[5, :foo] <: list(integer)", false, is_subtype(t_list_int_and_atom, t_list_of_int)) test_fn.( "[5, :foo] <: list(any)", true, is_subtype(t_list_int_and_atom, type_list_of(type_any())) ) IO.puts("\n--- Section: Covariance of list(X) ---") test_fn.( "list(pos_integer) <: list(integer)", true, is_subtype(t_list_of_pos_int, t_list_of_int) ) test_fn.( "list(integer) <: list(pos_integer)", false, is_subtype(t_list_of_int, t_list_of_pos_int) ) IO.puts("\n--- Section: Intersection of list(X) ---") # list(integer) & list(pos_integer) should be list(pos_integer) intersect1 = intersect(t_list_of_int, t_list_of_pos_int) test_fn.( "(list(int) & list(pos_int)) == list(pos_int)", true, intersect1 == t_list_of_pos_int ) # list(integer) & list(atom) should be just [] (empty list is the only common member) # The system simplifies this intersection to a type that only accepts the empty list. intersect2 = intersect(t_list_of_int, t_list_of_atom) test_fn.("[] <: (list(int) & list(atom))", true, is_subtype(t_empty_list, intersect2)) test_fn.("[5] <: (list(int) & list(atom))", false, is_subtype(t_list_one_int, intersect2)) test_fn.("[:foo] <: (list(int) & list(atom))", false, is_subtype(t_list_one_atom, intersect2)) # It should be equivalent to `type_empty_list` test_fn.("(list(int) & list(atom)) == []", true, intersect2 == t_empty_list) IO.puts("\n--- Section: Intersection of list(X) with cons ---") # list(integer) & [:foo | []] -> should be none intersect3 = intersect(t_list_of_int, t_list_one_atom) test_fn.("list(integer) & [:foo] == none", true, intersect3 == type_none()) # list(integer) & [5 | []] -> should be [5 | []] intersect4 = intersect(t_list_of_int, t_list_one_int) test_fn.("list(integer) & [5] == [5]", true, intersect4 == t_list_one_int) # list(integer) & [5, :foo] -> should be none intersect5 = intersect(t_list_of_int, t_list_int_and_atom) test_fn.("list(integer) & [5, :foo] == none", true, intersect5 == type_none()) IO.inspect(Process.get(:test_failures, []), label: "ListOfTests failures") end end defmodule AdhocTest do import Tdd def run(test_fn) do # --- Basic Types --- t_atom = type_atom() t_int = type_integer() t_pos_int = type_int_gt(0) t_int_5 = type_int_eq(5) # --- list(X) Types --- t_list_of_int = type_list_of(t_int) t_list_of_pos_int = type_list_of(t_pos_int) t_list_of_atom = type_list_of(t_atom) # --- Specific List Types --- t_list = type_list() t_empty_list = type_empty_list() # [5] t_list_one_int = type_cons(t_int_5, t_empty_list) # [:foo] t_list_one_atom = type_cons(type_atom_literal(:foo), t_empty_list) # [5, :foo] t_list_int_and_atom = type_cons(t_int_5, type_cons(type_atom_literal(:foo), t_empty_list)) intersect4 = intersect(t_list_of_int, t_list_one_int) IO.inspect("first_subtype") a = is_subtype(intersect4, t_list_one_int) IO.inspect("second_subtype") b = is_subtype(t_list_one_int, intersect4) test_fn.("list(integer) & [5] == [5]", true, a == b) end end test_all.()