defmodule Tdd do @moduledoc """ # Ternary Decision Diagram (TDD) for Set-Theoretic Types in Elixir ## 1. Introduction This document outlines the design and implementation of a Ternary Decision Diagram (TDD) based system for representing and manipulating set-theoretic types, inspired by systems like CDuce. The goal is to create a robust way to perform type checking, type inference, and other type-level computations for a rich set of datatypes, similar to those found in Elixir. A TDD is a directed acyclic graph (DAG) used to represent a function `f(v1, v2, ..., vn) -> {true, false, dont_care}`. In our context, it represents a characteristic function for a type: given a value, the TDD determines if the value belongs to the type (`true`), does not belong (`false`), or if the specific predicates tested so far are insufficient or irrelevant for this particular type operation (`dont_care`). The TDDs are kept **ordered** and **reduced** to ensure a canonical representation for each type, making type equivalence checks (and other operations) efficient. - **Ordered**: Variables (predicates) appear in the same fixed global order on all paths from the root to a terminal. - **Reduced**: Isomorphic subgraphs are merged (shared), and nodes whose children would make the test redundant under certain TDD algebra rules are eliminated or simplified. ## 2. Core TDD Structure and Operations ### 2.1. Nodes There are two kinds of nodes: 1. **Terminal Nodes**: * `TRUE_TERMINAL` (ID: `1`): Represents the universal set (type `any`). A path ending here means the value (or part of it) satisfies the type constraints along that path. * `FALSE_TERMINAL` (ID: `0`): Represents the empty set (type `none`). A path ending here means the value fails the type constraints. 2. **Variable Nodes**: * Represented as a tuple: `{variable_identifier, yes_child_id, no_child_id, dc_child_id}`. * `variable_identifier`: A unique, globally ordered term identifying the predicate being tested at this node (e.g., "is the value an atom?", "is the integer value < 10?"). * `yes_child_id`: The ID of the next TDD node if the predicate is true. * `no_child_id`: The ID of the next TDD node if the predicate is false. * `dc_child_id` (Don't Care): The ID of the next TDD node if the predicate is irrelevant for the current type or operation. The semantic interpretation of `dc` is crucial and aligns with common TDD usage (e.g., for a union operation, `dc(A | B) = dc(A) | dc(B)`). ### 2.2. Node Management (`Tdd` module state) The `Tdd` module maintains global state (currently via `Process.put/get` for simplicity, ideally a `GenServer`): * `@nodes`: A map from `node_tuple ({variable, yes_id, no_id, dc_id})` to `node_id`. This ensures that structurally identical nodes are shared (part of the "reduced" property). * `@node_by_id`: A map from `node_id` to its `node_tuple` or a terminal symbol (`:true_terminal`, `:false_terminal`). * `@next_id`: The next available integer ID for a new node. * `@op_cache`: A map for memoizing results of operations like `apply` (binary ops), `negate`, and `simplify_with_constraints`. Keys are typically `{{op_name, id1, id2}, result_id}` or `{{op_name, id1}, result_id}`. ### 2.3. Variable Ordering A strict global total order of all possible `variable_identifier`s is essential. This is achieved by defining variable identifiers as Elixir tuples, which have a natural sort order. The proposed structure for variable identifiers is: `{category_integer, predicate_type_atom, specific_value_or_nested_id}` Example categories: * `0`: Primary type discriminators (e.g., `is_atom`, `is_integer`, `is_list`). * `1`: Atom-specific predicates (e.g., `value == :foo`). * `2`: Integer-specific predicates (e.g., `value < 10`). * `4`: Tuple-specific predicates (e.g., `size == 2`, `element 0 has_type X`). * And so on for other types. ### 2.4. Core Operations 1. **`make_node_raw(variable, yes_id, no_id, dc_id)`**: * The fundamental private function for creating or retrieving unique structural nodes. * Implements structural sharing via the `@nodes` table. * Implements a basic reduction rule: if `yes_id == no_id == dc_id`, the node is redundant, and that common child ID is returned. 2. **`check_assumptions_consistency(assumptions_map)`**: * A private helper function crucial for semantic reduction. * Takes a map `%{variable_id => value (true/false/:dc)}` representing current path assumptions. * Returns `:consistent` or `:contradiction` based on predefined semantic rules of the type system (e.g., `is_atom=true` AND `is_tuple=true` is a contradiction). * This function will be expanded as more types and predicates are added. 3. **`simplify_with_constraints(tdd_id, assumptions_map)`**: * A private, memoized, recursive function that takes a `tdd_id` and an `assumptions_map`. * It produces a new `tdd_id` that is semantically equivalent to the input `tdd_id` under the given assumptions, but potentially simpler. * **Crucial Behavior**: If `check_assumptions_consistency(assumptions_map)` returns `:contradiction` at any point, `simplify_with_constraints` immediately returns `@false_node_id`. * If the TDD's variable is already in `assumptions_map`, it follows the constrained path. * Otherwise, it recursively simplifies children, adding the current node's variable assignment to the assumptions for those deeper calls, and rebuilds the node using `make_node_raw`. 4. **`apply_raw(op_name, op_lambda, u1_id, u2_id)`**: * The private, memoized, recursive Shannon expansion algorithm for binary set operations (union, intersection). * `op_lambda` defines the operation on terminal nodes. * It selects the `top_var` based on the global variable order. * Recursively calls `apply_raw` on the children. * Uses `make_node_raw` to construct result nodes. * This function computes the *structural* result of the operation. 5. **Public API Operations (`sum/2`, `intersect/2`, `negate/1`)**: * These functions orchestrate the operation: 1. Call the respective `_raw` version (e.g., `apply_raw` for `sum`/`intersect`, `negate_raw` for `negate`). 2. Take the `raw_result_id` from step 1. 3. Return `simplify_with_constraints(raw_result_id, %{})`. This final step ensures that all TDDs exposed through the public API are not only structurally canonical (via `make_node_raw` and `apply_raw`) but also *semantically canonical* (i.e., known impossible paths or contradictions are resolved to `@false_node_id`). 6. **Type Constructors (e.g., `type_atom()`, `type_atom_literal(:foo)`)**: * These public functions build the TDD for a specific type. * They use `make_node_raw` to define the basic structure. * They then return `simplify_with_constraints(raw_id, %{})` to ensure the constructed type is in its simplest semantic form. 7. **`is_subtype(sub_id, super_id)`**: * Defined as `simplify_with_constraints(intersect(sub_id, negate(super_id)), %{}) == @false_node_id`. * Since `intersect` and `negate` now return semantically simplified TDDs, if `A ∩ ¬B` represents an empty set, the result of the intersection will be `@false_node_id`. ## 3. Datatype Representation Details This section outlines how various Elixir-like datatypes are (or will be) represented using TDD variables and constructors. All constructors ensure the final TDD is passed through `simplify_with_constraints(raw_id, %{})`. ### 3.1. Atoms * **Variables**: * `@v_is_atom = {0, :is_atom}`: Primary type check. * `v_atom_eq_A = {1, :value, A}`: Checks if the atom's value is `A`. Order by `A`. * **Constructors**: * `type_atom()`: Represents any atom. TDD: `make_node_raw(@v_is_atom, @true_node_id, @false_node_id, @false_node_id)`. * `type_atom_literal(val)`: Represents a specific atom. TDD: `make_node_raw(@v_is_atom, node_for_val_eq, @false_node_id, @false_node_id)` where `node_for_val_eq = make_node_raw(v_atom_eq_A, @true_node_id, @false_node_id, @false_node_id)`. * **Semantic Constraints for `check_assumptions_consistency`**: * If `assumptions_map` contains `{{0, :is_atom}, true}` and `{{0, other_primary_type}, true}` -> contradiction. * If `assumptions_map` contains `{{1, :value, A}, true}` and `{{1, :value, B}, true}` where `A != B` -> contradiction. ### 3.2. Tuples * **Variables**: * `@v_is_tuple = {0, :is_tuple}`: Primary type check. * `v_tuple_size_eq_N = {4, :size, N}`: Checks if tuple size is `N`. Order by `N`. * `v_tuple_elem_I_PRED = {4, :element, Index_I, NESTED_PREDICATE_ID}`: Predicate for element at `Index_I`. `NESTED_PREDICATE_ID` is a variable from the global order, applied to the element. (e.g., `{4, :element, 0, {0, :is_atom}}` checks if element 0 is an atom). Order by `Index_I`, then by `NESTED_PREDICATE_ID`. * **Constructors**: * `type_tuple()`: Any tuple. * `type_empty_tuple()`: The tuple `{}`. * `type_tuple_sized_any(size)`: Any tuple of a given size. * `type_tuple_specific(element_type_ids_list)`: e.g., for `{atom(), integer()}`. This will involve creating nodes for size, then for each element, applying the TDD for that element's type. * **Semantic Constraints**: * `is_tuple=true` vs. other primary types. * If `{{4, :size, N}, true}` and `{{4, :size, M}, true}` where `N != M` -> contradiction. * If `{{4, :size, N}, true}` and a predicate `{{4, :element, I, _}, _}` exists where `I >= N` -> potential contradiction or path simplification (element doesn't exist). ### 3.3. Integers (Next to Implement) * **Variables**: * `@v_is_integer = {0, :is_integer}` (or a new category, e.g., `2` for integer properties). * INT_CAT variables (names of variables prefixed with `a b c` to force ordering * `v_int_lt_N = {INT_CAT, :alt, N}` (value < N). * `v_int_eq_N = {INT_CAT, :beq, N}`. * `v_int_gt_N = {INT_CAT, :cgt, N}` (value > N). * *(Consider also: `lte` (less than or equal), `gte` (greater than or equal) to simplify some range logic, or derive them).* * **Constructors**: * `type_integer()`: Any integer. * `type_int_eq(n)`: A specific integer value. * `type_int_lt(n)`, `type_int_gt(n)`. * `type_int_range(min, max, min_inclusive, max_inclusive)`: Integers within a specific range. * **Semantic Constraints**: * `is_integer=true` vs. other primary types. * `eq(N)` and `eq(M)` with `N != M` -> contradiction. * `eq(N)` and `lt(M)` if `N >= M` -> contradiction. * `eq(N)` and `gt(M)` if `N <= M` -> contradiction. * `lt(N)` and `gt(M)` if `N <= M+1` (or `N <= M` if `gt` means `>=`) -> contradiction. (e.g., `x < 5` and `x > 4` has no integer solution). * *Strategy for complex integer constraints*: Maintain a "current allowed interval" `[min_assumed, max_assumed]` based on `assumptions_map`. If this interval becomes empty or invalid, it's a contradiction. Each new integer assumption (`lt, gt, eq`) refines this interval. ### 3.4. Lists (Implemented) * **Variables**: * `@v_is_list = {0, :is_list}`. * `v_list_is_empty = {5, :is_empty}`. * *If not empty*: * `v_list_head_pred = {5, :head, NESTED_PREDICATE_ID}`: Applies a global predicate to the head. * `v_list_tail_pred = {5, :tail, NESTED_PREDICATE_ID_FOR_TAIL}`: Applies a global predicate (usually list predicates) to the tail. * **Constructors**: * `type_list()`: Represents any list. * `type_empty_list()`: Represents the empty list `[]`. * `type_cons(head_type_id, tail_type_id)`: Represents a non-empty list `[H|T]` where `H` is of type `head_type_id` and `T` is of type `tail_type_id`. * **Semantic Constraints**: * `is_list=true` vs. other primary types. * If `is_empty=true`, any predicate on the `head` or `tail` is a contradiction. * Recursive consistency checks on `head` and `tail` sub-types. ### 3.5. Strings & Binaries (Planned) * **Variables**: * `@v_is_binary = {0, :is_binary}`. * `@v_is_string = {0, :is_string}` (can be a check after `is_binary` or a distinct primary type if model demands). * Size/length predicates: `v_binary_size_eq_N`, `v_string_length_eq_N`. * Content predicates: `v_string_eq_S`, `v_string_prefix_P`, `v_string_suffix_S`, `v_string_matches_regex_R`. * **Semantic Constraints**: Size vs content (e.g., `size=1` and `prefix="foo"` is a contradiction). `eq(S1)` and `eq(S2)` if `S1 != S2`. ### 3.6. Maps (Planned - Complex) * **Variables**: * `@v_is_map = {0, :is_map}`. * `v_map_size_eq_N`. * `v_map_has_key_K`: (K is a canonical representation of an Elixir term). * *If `has_key_K` is true*: * `v_map_key_K_value_VAR = {MAP_CAT, :key_value, K, NESTED_PREDICATE_ID}`: Applies a global predicate to the value associated with key K. * For `%{pattern_key => pattern_value}` types: * This requires careful thought. Might involve predicates like `v_map_all_keys_matching_TYPE_X_have_values_matching_TYPE_Y`. * **Semantic Constraints**: `is_map` vs. others. Size vs. `has_key` interactions. Contradictory type requirements for the same key's value. ### 3.7. Functions (Planned - Very Complex) * Representation of function types (`fun((Arg1Type, Arg2Type, ...) -> ReturnType)`) is a significant challenge for TDDs. * **Variables (Tentative)**: * `@v_is_function = {0, :is_function}`. * `v_fun_arity_eq_A`. * Predicates for argument types at specific positions (e.g., `v_fun_arg_I_type_VAR`). * Predicates for return type (e.g., `v_fun_return_type_VAR`). * Intersection and union of function types involve concepts like contravariance of arguments and covariance of return types. This may require specialized logic beyond simple TDD operations or a very elaborate variable scheme. Often, function types are handled with auxiliary structures in type systems. ## 4. Current Status & Next Steps * **Implemented**: Atoms, basic Tuples (any, empty, sized_any). Core TDD operations (`sum`, `intersect`, `negate`, `is_subtype`) with semantic simplification framework (`simplify_with_constraints` and `check_assumptions_consistency`). * **Passing Tests**: A suite of tests for atom/tuple interactions, unions, intersections, negations, and subtyping, including resolution of contradictions like `atom & tuple == none`. * **Next Immediate Step**: Implement **Integer types** as outlined in section 3.3. This will involve: 1. Defining integer-specific predicates and their global order. 2. Creating integer type constructors. 3. Significantly expanding `check_assumptions_consistency` to handle integer comparisons (`eq`, `lt`, `gt`) and their interactions. 4. Adding comprehensive tests for integers. ## 5. Future Considerations * **Performance**: For very large TDDs or complex types, the number of nodes and cache sizes can grow. Investigate optimizations if needed. * **Generality of `check_assumptions_consistency`**: Designing this to be easily extensible and correct for many interacting predicates is challenging. A rule-based system or a more abstract way to define predicate interactions might be beneficial. * **"Don't Care" (`dc`) branch semantics**: Ensure the `dc` branch is consistently and correctly handled in all operations, especially `simplify_with_constraints` if assumptions can make a variable "don't care". Currently, `simplify_with_constraints` assumes `true/false/:dc` values in the `assumptions_map` if a variable is already constrained. * **Type Inference**: Using the TDD operations to infer types or solve type constraints. * **Polymorphism**: Representing and operating on types with free type variables. Typically, free variables are treated as `any` or involve substitution before TDD construction. This document provides a snapshot of the current TDD system and a roadmap for its extension. The core principle is the combination of structurally canonical ROBDDs (via `make_node_raw` and `apply_raw`) with a semantic simplification layer (`simplify_with_constraints`) that embeds knowledge of the type system's rules. """ # --- 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) --- 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 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 --- # 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 --- # Helper function to calculate the final interval from true/false integer predicates # and check for internal contradictions among them. defp calculate_final_interval_from_bounds(bounds_acc) do derived_min_b = bounds_acc.min_b derived_max_b = bounds_acc.max_b # Stage 1: Check for immediate conflict from <, >, <=, >= derived from true/false predicates if is_integer(derived_min_b) && is_integer(derived_max_b) && derived_min_b > derived_max_b do # Invalid interval {:contradiction, nil, nil} else # Stage 2: Incorporate equality constraint cond do bounds_acc.eq_val == :conflict -> # IO.inspect({bounds_acc}, label: "CAC Int Interval: eq_val conflict") {:contradiction, nil, nil} is_integer(bounds_acc.eq_val) -> eq_v = bounds_acc.eq_val min_ok = is_nil(derived_min_b) || eq_v >= derived_min_b max_ok = is_nil(derived_max_b) || eq_v <= derived_max_b if min_ok && max_ok do # IO.inspect({bounds_acc, eq_v}, label: "CAC Int Interval: eq consistent") # Interval is a single point {:consistent, eq_v, eq_v} else # IO.inspect({bounds_acc, eq_v, derived_min_b, derived_max_b}, label: "CAC Int Interval: eq contradicts interval") # Equality conflicts with bounds {:contradiction, nil, nil} end true -> # No equality constraint, or no conflict from it. The initial interval check (derived_min_b > derived_max_b) suffices. # IO.inspect({bounds_acc, derived_min_b, derived_max_b}, label: "CAC Int Interval: consistent (no eq or eq compatible)") {:consistent, derived_min_b, derived_max_b} end end end defp check_assumptions_consistency(assumptions_map, ambient_constraints \\ %{}) do # 1. Merge ambient constraints into the main map. # This ensures, for example, that if we are checking the `head` of a `list(X)`, # the constraint `is_subtype(head, X)` is enforced. assumptions_map = Map.merge(ambient_constraints, assumptions_map) # 1. Partition assumptions by the entity they apply to. partitioned_assumptions = Enum.group_by(assumptions_map, fn # Tuple element property: {4, :element, 0, {0, :is_integer}} {{4, :element, index, _nested_var}, _value} -> {:elem, index} # List head property: {5, :head, {0, :is_atom}} {{5, :head, _nested_var}, _value} -> :head # List tail property: {5, :tail, {5, :is_empty}} {{5, :tail, _nested_var}, _value} -> :tail # All other variables _ -> :top_level end) # 2. Check the assumptions for the top-level entity. top_level_assumptions = Map.get(partitioned_assumptions, :top_level, []) |> Map.new() case do_check_flat_consistency(top_level_assumptions) do :contradiction -> :contradiction :consistent -> # 4. If top-level is consistent, gather new ambient constraints and check sub-problems. all_elems_constraints = Enum.reduce(top_level_assumptions, [], fn {{5, :all_elements, type_id}, true}, acc -> [type_id | acc] _, acc -> acc end) sub_problems = Map.drop(partitioned_assumptions, [:top_level]) Enum.reduce_while(sub_problems, :consistent, fn # For a tuple element (no ambient constraints from parent needed for now) {{:elem, _index}, assumptions_list}, _acc -> sub_assumptions = assumptions_list |> Map.new(fn {{_, :element, _, nested_var}, value} -> {nested_var, value} end) case check_assumptions_consistency(sub_assumptions) do :contradiction -> {:halt, :contradiction} :consistent -> {:cont, :consistent} end # For a list head {:head, assumptions_list}, _acc -> # The head must conform to every `all_elements` constraint on the list. # We build a TDD for the intersection of all these constraints. ambient_type_for_head = Enum.reduce(all_elems_constraints, type_any(), &intersect/2) head_sub_assumptions = assumptions_list |> Map.new(fn {{_, :head, nested_var}, value} -> {nested_var, value} end) # Check if the explicitly assumed head type contradicts the ambient one. head_type_from_assumptions = simplify_with_constraints(@true_node_id, head_sub_assumptions) if is_subtype(head_type_from_assumptions, ambient_type_for_head) do # Recursively check internal consistency of the head's own assumptions. case check_assumptions_consistency(head_sub_assumptions) do :contradiction -> {:halt, :contradiction} :consistent -> {:cont, :consistent} end else {:halt, :contradiction} end # For a list tail {:tail, assumptions_list}, _acc -> # The tail must also be a list conforming to the same `all_elements` constraints. # So we pass the parent's `all_elements` assumptions down as ambient constraints for the tail. ambient_for_tail = Enum.reduce(all_elems_constraints, %{}, fn type_id, acc -> Map.put(acc, v_list_all_elements_are(type_id), true) end) tail_sub_assumptions = assumptions_list |> Map.new(fn {{_, :tail, nested_var}, value} -> {nested_var, value} end) # Recursively check the tail's assumptions *with the ambient constraints*. case check_assumptions_consistency(tail_sub_assumptions, ambient_for_tail) do :contradiction -> {:halt, :contradiction} :consistent -> {:cont, :consistent} end end) end end # The original check_assumptions_consistency function is renamed to this. # It performs the actual logic for a "flat" set of assumptions about a single entity. defp do_check_flat_consistency(assumptions_map) do # Check 1: Primary type mutual exclusivity primary_true_predicates = Enum.reduce(assumptions_map, MapSet.new(), fn {{0, predicate_name}, true}, acc_set -> MapSet.put(acc_set, predicate_name) _otherwise, acc_set -> acc_set end) if MapSet.size(primary_true_predicates) > 1 do :contradiction else # Perform checks for each type category... # (Existing atom, tuple, integer checks are chained via `cond`) # If no specific checks resulted in contradiction, it's consistent. check_atom_logic(assumptions_map, primary_true_predicates) || check_tuple_logic(assumptions_map, primary_true_predicates) || check_integer_logic(assumptions_map, primary_true_predicates) || check_list_logic(assumptions_map, primary_true_predicates) || :consistent end end # Helper functions to break down the massive cond block defp check_atom_logic(assumptions_map, primary_true_predicates) do has_true_atom_specific_pred = Enum.any?(assumptions_map, fn {var_id, truth_value} -> elem(var_id, 0) == 1 && truth_value == true end) is_explicitly_not_atom = Map.get(assumptions_map, @v_is_atom) == false || (MapSet.size(primary_true_predicates) == 1 && !MapSet.member?(primary_true_predicates, :is_atom)) if has_true_atom_specific_pred && is_explicitly_not_atom do :contradiction else atom_value_trues = Enum.reduce(assumptions_map, MapSet.new(), fn {{1, :value, atom_val}, true}, acc_set -> MapSet.put(acc_set, atom_val) _otherwise, acc_set -> acc_set end) if MapSet.size(atom_value_trues) > 1, do: :contradiction, else: false end end defp check_tuple_logic(assumptions_map, primary_true_predicates) do has_true_tuple_specific_pred = Enum.any?(assumptions_map, fn {var_id, truth_value} -> elem(var_id, 0) == 4 && truth_value == true end) is_explicitly_not_tuple = Map.get(assumptions_map, @v_is_tuple) == false || (MapSet.size(primary_true_predicates) == 1 && !MapSet.member?(primary_true_predicates, :is_tuple)) if has_true_tuple_specific_pred && is_explicitly_not_tuple do :contradiction else tuple_size_trues = Enum.reduce(assumptions_map, MapSet.new(), fn {{4, :size, size_val}, true}, acc_set -> MapSet.put(acc_set, size_val) _otherwise, acc_set -> acc_set end) if MapSet.size(tuple_size_trues) > 1, do: :contradiction, else: false end end # (The original integer checking logic is moved into this helper) defp check_integer_logic(assumptions_map, primary_true_predicates) do has_true_integer_specific_pred = Enum.any?(assumptions_map, fn {var_id, truth_value} -> elem(var_id, 0) == 2 && truth_value == true end) is_explicitly_not_integer_or_different_primary = Map.get(assumptions_map, @v_is_integer) == false || (MapSet.size(primary_true_predicates) == 1 && !MapSet.member?(primary_true_predicates, :is_integer)) should_check_integer_logic = Enum.any?(assumptions_map, fn {var_id, _} -> elem(var_id, 0) == 2 end) || MapSet.member?(primary_true_predicates, :is_integer) cond do has_true_integer_specific_pred && is_explicitly_not_integer_or_different_primary -> :contradiction should_check_integer_logic -> initial_bounds_from_true_false = %{eq_val: nil, min_b: nil, max_b: nil} bounds_acc = Enum.reduce( assumptions_map, initial_bounds_from_true_false, fn {{2, :beq, n}, true}, acc -> cond do acc.eq_val == :conflict -> acc is_nil(acc.eq_val) -> %{acc | eq_val: n} acc.eq_val != n -> %{acc | eq_val: :conflict} true -> acc end # value < n => value <= n-1 {{2, :alt, n}, true}, acc -> new_max_b = if is_nil(acc.max_b), do: n - 1, else: min(acc.max_b, n - 1) %{acc | max_b: new_max_b} # value > n => value >= n+1 {{2, :cgt, n}, true}, acc -> new_min_b = if is_nil(acc.min_b), do: n + 1, else: max(acc.min_b, n + 1) %{acc | min_b: new_min_b} # value >= n {{2, :alt, n}, false}, acc -> new_min_b = if is_nil(acc.min_b), do: n, else: max(acc.min_b, n) %{acc | min_b: new_min_b} # value <= n {{2, :cgt, n}, false}, acc -> new_max_b = if is_nil(acc.max_b), do: n, else: min(acc.max_b, n) %{acc | max_b: new_max_b} # Ignore other preds for this pass _otherwise, acc -> acc end ) case calculate_final_interval_from_bounds(bounds_acc) do {:contradiction, _, _} -> :contradiction {:consistent, current_interval_min, current_interval_max} -> # Interval from true/false preds is consistent. Now check for other implied contradictions. # This logic was missing from my simplified version and is critical. res = Enum.reduce_while(assumptions_map, :consistent, fn {{2, pred_type, n_val}, :dc}, _acc_status -> is_implied_true = case pred_type do :beq -> is_integer(current_interval_min) && current_interval_min == n_val && (is_integer(current_interval_max) && current_interval_max == n_val) :alt -> is_integer(current_interval_max) && current_interval_max < n_val :cgt -> is_integer(current_interval_min) && current_interval_min > n_val _ -> false end is_implied_false = case pred_type do :beq -> (is_integer(current_interval_min) && current_interval_min > n_val) || (is_integer(current_interval_max) && current_interval_max < n_val) :alt -> is_integer(current_interval_min) && current_interval_min >= n_val :cgt -> is_integer(current_interval_max) && current_interval_max <= n_val _ -> false end if is_implied_true || is_implied_false do {:halt, :contradiction} else {:cont, :consistent} end _other_assumption, acc_status -> {:cont, acc_status} end) # Return :contradiction if found, otherwise `false` to allow the `||` chain to continue. if res == :contradiction, do: :contradiction, else: false end true -> false end end ### NEW ### # Logic for list consistency checks defp check_list_logic(assumptions_map, primary_true_predicates) do # A predicate like {5, :is_empty} or {5, :head, ...} exists has_list_specific_pred = Enum.any?(assumptions_map, fn {var_id, _} -> elem(var_id, 0) == 5 end) is_explicitly_not_list = Map.get(assumptions_map, v_is_list()) == false || (MapSet.size(primary_true_predicates) == 1 && !MapSet.member?(primary_true_predicates, :is_list)) # A predicate on head or tail exists, e.g. {{5, :head, _}, _} has_head_or_tail_pred = Enum.any?(assumptions_map, fn {{_cat, ptype, _}, _} -> ptype == :head or ptype == :tail _ -> false end) cond do # Contradiction: list-specific rule is assumed, but type is not a list. has_list_specific_pred && is_explicitly_not_list -> :contradiction # Contradiction: assumed to be an empty list, but also has assumptions about head/tail. Map.get(assumptions_map, v_list_is_empty()) == true && has_head_or_tail_pred -> :contradiction # No flat contradictions found for lists. Recursive checks are done in the main function. true -> false end end # Helper for min, treating nil as infinity defp min_opt(nil, x), do: x defp min_opt(x, nil), do: x defp min_opt(x, y), do: min(x, y) # Helper for max, treating nil as -infinity defp max_opt(nil, x), do: x defp max_opt(x, nil), do: x defp max_opt(x, y), do: max(x, y) # --- Semantic Simplification (Memoized) --- def simplify_with_constraints(tdd_id, assumptions_map) do state = get_state() # Sort assumptions for cache key consistency sorted_assumptions_list = Enum.sort(Map.to_list(assumptions_map)) cache_key = {:simplify_constr, tdd_id, sorted_assumptions_list} # 1. Check if the current assumptions_map itself is contradictory # This initial check is crucial. current_consistency = check_assumptions_consistency(assumptions_map) if current_consistency == :contradiction do # update_state(%{op_cache: Map.put(state.op_cache, cache_key, @false_node_id)}) # Cache if desired @false_node_id else # 2. Handle terminal nodes if is_terminal_id(tdd_id) do # Terminals are final, assumptions (if consistent) don't change them tdd_id else # 3. Cache lookup for non-terminal nodes if Map.has_key?(state.op_cache, cache_key) do state.op_cache[cache_key] else {var, y, n, d} = get_node_details(tdd_id) # 4. Determine how to proceed based on 'var' and 'assumptions_map' result_id = case Map.get(assumptions_map, var) do # 'var' is explicitly assumed true true -> simplify_with_constraints(y, assumptions_map) # 'var' is explicitly assumed false false -> simplify_with_constraints(n, assumptions_map) # 'var' is explicitly assumed don't care :dc -> simplify_with_constraints(d, assumptions_map) # 'var' is NOT explicitly in assumptions_map. Check for implied truth value. nil -> # If (assumptions_map + var=false) is a contradiction, then var MUST be true. implies_var_true = check_assumptions_consistency(Map.put(assumptions_map, var, false)) == :contradiction # If (assumptions_map + var=true) is a contradiction, then var MUST be false. implies_var_false = check_assumptions_consistency(Map.put(assumptions_map, var, true)) == :contradiction # Note: We don't check for implies_var_dc here, as that's more complex. # The original recursion handles the DC case exploration. # IO.inspect( # %{ # tdd_id: tdd_id, # var: var, # assumptions: assumptions_map, # implies_var_true: implies_var_true, # implies_var_false: implies_var_false # }, # label: "Simplify NIL branch" # ) cond do implies_var_true && implies_var_false -> # This means assumptions_map itself is contradictory. # This should ideally be caught by the check at the very top of simplify_with_constraints. # If reached, it implies an issue or a very complex interaction. Safest is False. # IO.inspect({assumptions_map, var}, label: "Simplify: Contradiction from implies_var_true/false") @false_node_id implies_var_true -> # Var is implied true by other assumptions. Follow the 'yes' branch. # Pass the original 'assumptions_map' because 'var's truth is derived, not added. simplify_with_constraints(y, assumptions_map) implies_var_false -> # Var is implied false. Follow the 'no' branch. simplify_with_constraints(n, assumptions_map) true -> # Var's value is not forced by current assumptions. Recurse normally. 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)) make_node_raw(var, simplified_y, simplified_n, simplified_d) end end update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id end end end end # --- Public Node Creation (Used by Type Constructors) --- # Type constructors will create a raw TDD and then simplify it. defp make_node_for_constructors(variable, yes_id, no_id, dc_id) do raw_id = make_node_raw(variable, yes_id, no_id, dc_id) # Simplify with no initial assumptions simplify_with_constraints(raw_id, %{}) 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} @v_is_integer {0, :is_integer} @v_is_list {0, :is_list} def v_is_atom, do: @v_is_atom def v_is_tuple, do: @v_is_tuple def v_is_integer, do: @v_is_integer ### NEW ### def v_is_list, do: @v_is_list 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} # List Predicates (Category 5) def v_list_is_empty, do: {5, :is_empty} def v_list_head_pred(nested_var), do: {5, :head, nested_var} def v_list_tail_pred(nested_var), do: {5, :tail, nested_var} def v_list_all_elements_are(element_type_id), do: {5, :all_elements, element_type_id} # Integer Predicates (Category 2) # strictly less than n def v_int_lt(n) when is_integer(n), do: {2, :alt, n} def v_int_eq(n) when is_integer(n), do: {2, :beq, n} # strictly greater than n def v_int_gt(n) when is_integer(n), do: {2, :cgt, n} def type_any, do: @true_node_id def type_none, do: @false_node_id def type_atom do make_node_for_constructors(@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_raw(var_eq, @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_atom, atom_val_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end def type_tuple do make_node_for_constructors(@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_raw(var_size_0, @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_tuple, tuple_size_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end def type_tuple_sized_any(size) do var_size = v_tuple_size_eq(size) tuple_size_node = make_node_raw(var_size, @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_tuple, tuple_size_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end def type_integer do make_node_for_constructors(@v_is_integer, @true_node_id, @false_node_id, @false_node_id) end # A recursive helper that maps a TDD onto a component (e.g., list head, tuple element). # It takes a tdd_id, a `wrapper_fun` (like `&v_list_head_pred/1`), and the ID to jump to on success. defp map_tdd_to_component(tdd_id, wrapper_fun, success_id) do case get_node_details(tdd_id) do :true_terminal -> success_id :false_terminal -> @false_node_id {var, y, n, d} -> # Wrap the original variable to be specific to this component. component_var = wrapper_fun.(var) # Recurse on children, passing the success_id down. res_y = map_tdd_to_component(y, wrapper_fun, success_id) res_n = map_tdd_to_component(n, wrapper_fun, success_id) res_d = map_tdd_to_component(d, wrapper_fun, success_id) make_node_raw(component_var, res_y, res_n, res_d) end end def type_tuple_elem(element_index, element_type_id, success_path_id) do map_tdd_to_component(element_type_id, &v_tuple_elem_pred(element_index, &1), success_path_id) end def type_tuple(element_type_ids) do num_elements = length(element_type_ids) final_elements_check_tdd = Enum.reduce(Enum.reverse(0..(num_elements - 1)), type_any(), fn i, acc_tdd -> element_type_id = Enum.at(element_type_ids, i) type_tuple_elem(i, element_type_id, acc_tdd) end) size_check_node = make_node(v_tuple_size_eq(num_elements), final_elements_check_tdd, type_none(), type_none()) raw_final_tdd = make_node(v_is_tuple(), size_check_node, type_none(), type_none()) simplify_with_constraints(raw_final_tdd, %{}) end # List Type Constructors def type_list, do: make_node_for_constructors(v_is_list(), @true_node_id, @false_node_id, @false_node_id) def type_empty_list, do: make_node_for_constructors( v_is_list(), make_node_raw(v_list_is_empty(), @true_node_id, @false_node_id, @false_node_id), @false_node_id, @false_node_id ) def type_cons(head_type_id, tail_type_id) do # 1. Build the TDD for the tail constraint. # On success, this will proceed to the head constraint check. tail_check_tdd = map_tdd_to_component(tail_type_id, &v_list_tail_pred/1, @true_node_id) # 2. Build the TDD for the head constraint. # On success, it proceeds to the TDD we just built for the tail. head_and_tail_check_tdd = map_tdd_to_component(head_type_id, &v_list_head_pred/1, tail_check_tdd) # 3. A cons cell is never empty. # If is_empty is true, it's a failure. If false, proceed to head/tail checks. is_empty_check_node = make_node(v_list_is_empty(), @false_node_id, head_and_tail_check_tdd, @false_node_id) # 4. Wrap in the primary list type check. raw_final_tdd = make_node(v_is_list(), is_empty_check_node, @false_node_id, @false_node_id) # 5. Simplify the final result. simplify_with_constraints(raw_final_tdd, %{}) end def type_list_of(element_type_id) when is_integer(element_type_id) do # An empty list satisfies any list_of constraint vacuously. # The type is effectively `[] | [X | list(X)]` # We can't build this recursively, so we use a specialized predicate. # This type is trivially `any` if element type is `any` if element_type_id == type_any() do type_list() else all_elems_check = make_node_raw( v_list_all_elements_are(element_type_id), @true_node_id, @false_node_id, @false_node_id ) raw_node = make_node_raw(v_is_list(), all_elems_check, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end end def type_int_eq(n) do int_eq_node = make_node_raw(v_int_eq(n), @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_integer, int_eq_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end # Represents integers x where x < n def type_int_lt(n) do int_lt_node = make_node_raw(v_int_lt(n), @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_integer, int_lt_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end # Represents integers x where x > n def type_int_gt(n) do int_gt_node = make_node_raw(v_int_gt(n), @true_node_id, @false_node_id, @false_node_id) raw_node = make_node_raw(@v_is_integer, int_gt_node, @false_node_id, @false_node_id) simplify_with_constraints(raw_node, %{}) end # --- The APPLY Algorithm (Core Logic, uses make_node_raw) --- # This function computes the raw structural result. Semantic simplification is applied by the caller. defp apply_raw(op_name, op_lambda, u1_id, u2_id) do state = get_state() # apply_raw cache key 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 -> u1_details = get_node_details(u1_id) u2_details = get_node_details(u2_id) result_id = cond do u1_details == :true_terminal or u1_details == :false_terminal -> {var2, y2, n2, d2} = u2_details res_y = apply_raw(op_name, op_lambda, u1_id, y2) res_n = apply_raw(op_name, op_lambda, u1_id, n2) res_d = apply_raw(op_name, op_lambda, u1_id, d2) make_node_raw(var2, res_y, res_n, res_d) u2_details == :true_terminal or u2_details == :false_terminal -> {var1, y1, n1, d1} = u1_details res_y = apply_raw(op_name, op_lambda, y1, u2_id) res_n = apply_raw(op_name, op_lambda, n1, u2_id) res_d = apply_raw(op_name, op_lambda, d1, u2_id) make_node_raw(var1, res_y, res_n, res_d) true -> {var1, y1, n1, d1} = u1_details {var2, y2, n2, d2} = u2_details # Elixir tuple comparison top_var = Enum.min([var1, var2]) res_y = apply_raw( 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_raw( 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_raw( op_name, op_lambda, if(var1 == top_var, do: d1, else: u1_id), if(var2 == top_var, do: d2, else: u2_id) ) make_node_raw(top_var, res_y, res_n, res_d) end update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id end end # --- Public Set Operations (API) --- 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 raw_result_id = apply_raw(:sum, op_lambda_sum, tdd1_id, tdd2_id) simplify_with_constraints(raw_result_id, %{}) end def intersect(tdd1_id, tdd2_id) do op_lambda_intersect = fn :false_terminal, _ -> :false_terminal _, :false_terminal -> :false_terminal :true_terminal, t2_val -> t2_val t1_val, :true_terminal -> t1_val end raw_result_id = apply_raw(:intersect, op_lambda_intersect, tdd1_id, tdd2_id) simplify_with_constraints(raw_result_id, %{}) end def negate(tdd_id) do # Negation also needs semantic simplification wrapper if it can create complex structures, # but typically negation is structurally simple enough that raw ops are fine if children are simplified. # However, to be safe and ensure canonical form for ¬(A & B) vs ¬A | ¬B. raw_negated_id = negate_raw(tdd_id) simplify_with_constraints(raw_negated_id, %{}) end # Renamed original negate defp negate_raw(tdd_id) do state = get_state() cache_key = {:negate_raw, tdd_id} cond do tdd_id == @true_node_id -> @false_node_id tdd_id == @false_node_id -> @true_node_id Map.has_key?(state.op_cache, cache_key) -> state.op_cache[cache_key] true -> {var, y, n, d} = get_node_details(tdd_id) # Negate children recursively using the public `negate` which includes simplification # Public negate to ensure children are simplified res_y = negate(y) res_n = negate(n) res_d = negate(d) result_id = make_node_raw(var, res_y, res_n, res_d) update_state(%{op_cache: Map.put(state.op_cache, cache_key, result_id)}) result_id end end # --- Subtyping (API) --- def is_subtype(sub_type_id, super_type_id) do cond do sub_type_id == super_type_id -> true # none is subtype of anything sub_type_id == @false_node_id -> true # anything is subtype of any super_type_id == @true_node_id -> true true -> # A <: B <=> A ∩ (¬B) == ∅ # All operations (intersect, negate) now produce semantically simplified results. # IO.puts("\n--- is_subtype debug ---") # IO.inspect(sub_type_id, # label: "is_subtype: sub_type_id (#{inspect(Tdd.get_node_details(sub_type_id))})" # ) # IO.inspect(super_type_id, # label: "is_subtype: super_type_id (#{inspect(Tdd.get_node_details(super_type_id))})" # ) # Tdd.print_tdd(sub_type_id) # Tdd.print_tdd(super_type_id) negated_super = negate(super_type_id) # IO.inspect(negated_super, # label: "is_subtype: negated_super_id (#{inspect(Tdd.get_node_details(negated_super))})" # ) # IO.puts("Structure of negated_super:") # Tdd.print_tdd(negated_super) intersection_result = intersect(sub_type_id, negated_super) # IO.inspect(intersection_result, # label: # "is_subtype: intersection_result_id (#{inspect(Tdd.get_node_details(intersection_result))})" # ) # IO.puts("Structure of intersection_result:") # Tdd.print_tdd(intersection_result) result = intersection_result == @false_node_id # IO.inspect(result, label: "is_subtype: final result") # IO.puts("--- end is_subtype debug ---\n") result 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() 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 test_all.() IntegerTests.run(test) TupleTests.run(test) ListTests.run(test) ListOfTests.run(test)