diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b046507a5c1..0e8e51d74d7 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -23,15 +23,15 @@ defmodule Module.Types.Descr do @bit_reference 1 <<< 6 @bit_non_empty_list 1 <<< 7 - @bit_tuple 1 <<< 8 - @bit_fun 1 <<< 9 - @bit_top (1 <<< 10) - 1 + @bit_fun 1 <<< 8 + @bit_top (1 <<< 9) - 1 @bit_list @bit_empty_list ||| @bit_non_empty_list @bit_number @bit_integer ||| @bit_float - @bit_optional 1 <<< 10 + @bit_optional 1 <<< 9 @atom_top {:negation, :sets.new(version: 2)} + @tuple_top [{:open, [], []}] @map_top [{:open, %{}, []}] @map_empty [{:closed, %{}, []}] @none %{} @@ -44,7 +44,7 @@ defmodule Module.Types.Descr do defp unfold(:term), do: unfolded_term() defp unfold(other), do: other - defp unfolded_term, do: %{bitmap: @bit_top, atom: @atom_top, map: @map_top} + defp unfolded_term, do: %{bitmap: @bit_top, atom: @atom_top, tuple: @tuple_top, map: @map_top} def atom(as), do: %{atom: atom_new(as)} def atom(), do: %{atom: @atom_top} @@ -62,7 +62,9 @@ defmodule Module.Types.Descr do def pid(), do: %{bitmap: @bit_pid} def port(), do: %{bitmap: @bit_port} def reference(), do: %{bitmap: @bit_reference} - def tuple(), do: %{bitmap: @bit_tuple} + def open_tuple(elements), do: %{tuple: tuple_new(:open, elements)} + def tuple(elements), do: %{tuple: tuple_new(:closed, elements)} + def tuple(), do: %{tuple: @tuple_top} @boolset :sets.from_list([true, false], version: 2) def boolean(), do: %{atom: {:union, @boolset}} @@ -80,7 +82,12 @@ defmodule Module.Types.Descr do # `not_set()` has no meaning outside of map types. @not_set %{bitmap: @bit_optional} - @term_or_optional %{bitmap: @bit_top ||| @bit_optional, atom: @atom_top, map: @map_top} + @term_or_optional %{ + bitmap: @bit_top ||| @bit_optional, + atom: @atom_top, + tuple: @tuple_top, + map: @map_top + } def not_set(), do: @not_set defp term_or_optional(), do: @term_or_optional @@ -98,6 +105,9 @@ defmodule Module.Types.Descr do defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :bitmap) and (map.bitmap &&& @bit_optional) != 0 + defp descr_key?(:term, _key), do: true + defp descr_key?(descr, key), do: is_map_key(descr, key) + ## Set operations def term_type?(:term), do: true @@ -149,6 +159,7 @@ defmodule Module.Types.Descr do defp union(:atom, v1, v2), do: atom_union(v1, v2) defp union(:dynamic, v1, v2), do: dynamic_union(v1, v2) defp union(:map, v1, v2), do: map_union(v1, v2) + defp union(:tuple, v1, v2), do: tuple_union(v1, v2) @doc """ Computes the intersection of two descrs. @@ -182,6 +193,7 @@ defmodule Module.Types.Descr do defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2) defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2) defp intersection(:map, v1, v2), do: map_intersection(v1, v2) + defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2) @doc """ Computes the difference between two types. @@ -218,6 +230,7 @@ defmodule Module.Types.Descr do defp difference(:atom, v1, v2), do: atom_difference(v1, v2) defp difference(:dynamic, v1, v2), do: dynamic_difference(v1, v2) defp difference(:map, v1, v2), do: map_difference(v1, v2) + defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2) @doc """ Compute the negation of a type. @@ -245,6 +258,7 @@ defmodule Module.Types.Descr do descr -> not Map.has_key?(descr, :bitmap) and not Map.has_key?(descr, :atom) and + (not Map.has_key?(descr, :tuple) or tuple_empty?(descr.tuple)) and (not Map.has_key?(descr, :map) or map_empty?(descr.map)) end end @@ -268,6 +282,7 @@ defmodule Module.Types.Descr do defp to_quoted(:atom, val), do: atom_to_quoted(val) defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) defp to_quoted(:map, dnf), do: map_to_quoted(dnf) + defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf) @doc """ Converts a descr to its quoted string representation. @@ -416,7 +431,6 @@ defmodule Module.Types.Descr do port: @bit_port, reference: @bit_reference, non_empty_list: @bit_non_empty_list, - tuple: @bit_tuple, fun: @bit_fun ] @@ -698,8 +712,8 @@ defmodule Module.Types.Descr do {acc, dynamic?} end - defp map_tag_to_type(:open), do: term_or_optional() - defp map_tag_to_type(:closed), do: not_set() + defp tag_to_type(:open), do: term_or_optional() + defp tag_to_type(:closed), do: not_set() defp map_new(tag, fields = %{}), do: [{tag, fields, []}] @@ -717,7 +731,7 @@ defmodule Module.Types.Descr do def map_fetch(%{} = descr, key) do case :maps.take(:dynamic, descr) do :error -> - if is_map_key(descr, :map) and map_only?(descr) do + if descr_key?(descr, :map) and map_only?(descr) do {static_optional?, static_type} = map_fetch_static(descr, key) if static_optional? or empty?(static_type) do @@ -729,11 +743,8 @@ defmodule Module.Types.Descr do :badmap end - {:term, _static} -> - {true, dynamic()} - {dynamic, static} -> - if is_map_key(dynamic, :map) and map_only?(static) do + if descr_key?(dynamic, :map) and map_only?(static) do {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key) {static_optional?, static_type} = map_fetch_static(static, key) @@ -750,6 +761,10 @@ defmodule Module.Types.Descr do defp map_only?(descr), do: empty?(Map.delete(descr, :map)) + defp map_fetch_static(:term, _key) do + {true, term()} + end + defp map_fetch_static(descr, key) when is_atom(key) do case descr do # Optimization: if the key does not exist in the map, @@ -926,7 +941,7 @@ defmodule Module.Types.Descr do false else # an absent key in a open negative map can be ignored - diff = difference(type, map_tag_to_type(neg_tag)) + diff = difference(type, tag_to_type(neg_tag)) empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) end end @@ -945,7 +960,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives # and the key does not exist, return the default one. {tag, %{}, []} -> - [map_tag_to_type(tag)] + [tag_to_type(tag)] {tag, fields, negs} -> {fst, snd} = map_pop_key(tag, fields, key) @@ -960,7 +975,7 @@ defmodule Module.Types.Descr do defp map_pop_key(tag, fields, key) do case :maps.take(key, fields) do {value, fields} -> {value, %{map: map_new(tag, fields)}} - :error -> {map_tag_to_type(tag), %{map: map_new(tag, fields)}} + :error -> {tag_to_type(tag), %{map: map_new(tag, fields)}} end end @@ -1066,6 +1081,314 @@ defmodule Module.Types.Descr do end end + ## Tuple + + # Represents tuple types in two forms: + # 1. Closed tuples: Fixed-length tuples with specific element types + # Example: {integer(), atom()} + # 2. Open tuples: Variable-length tuples with a minimum set of element types + # Example: {atom(), boolean(), ...} + # + # Internal representation: + # - Closed tuple: {:closed, [element_type, ...]} + # - Open tuple: {:open, [element_type, ...]} + # + # Examples: + # - {integer(), atom()} is encoded as {:closed, [integer(), atom()]} + # - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]} + + defp tuple_new(tag, elements), do: [{tag, elements, []}] + + defp tuple_intersection(dnf1, dnf2) do + for {tag1, elements1, negs1} <- dnf1, + {tag2, elements2, negs2} <- dnf2, + reduce: [] do + acc -> + try do + {tag, fields} = tuple_literal_intersection(tag1, elements1, tag2, elements2) + [{tag, fields, negs1 ++ negs2} | acc] + catch + :empty -> acc + end + end + |> case do + [] -> 0 + acc -> acc + end + end + + defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do + n = length(elements1) + m = length(elements2) + + cond do + (tag1 == :closed and n < m) or (tag2 == :closed and n > m) -> throw(:empty) + tag1 == :open and tag2 == :open -> {:open, zip_intersection(elements1, elements2, [])} + true -> {:closed, zip_intersection(elements1, elements2, [])} + end + end + + # Intersects two lists of types, and _appends_ the extra elements to the result. + defp zip_intersection([], types2, acc), do: Enum.reverse(acc, types2) + defp zip_intersection(types1, [], acc), do: Enum.reverse(acc, types1) + + defp zip_intersection([type1 | rest1], [type2 | rest2], acc) do + zip_intersection(rest1, rest2, [non_empty_intersection!(type1, type2) | acc]) + end + + defp tuple_difference(dnf1, dnf2) do + Enum.reduce(dnf2, dnf1, fn {tag2, elements2, negs2}, dnf1 -> + Enum.reduce(dnf1, [], fn {tag1, elements1, negs1}, acc -> + acc = [{tag1, elements1, [{tag2, elements2} | negs1]}] ++ acc + + Enum.reduce(negs2, acc, fn {neg_tag2, neg_elements2}, acc -> + try do + {tag, fields} = tuple_literal_intersection(tag1, elements1, neg_tag2, neg_elements2) + [{tag, fields, negs1}] ++ acc + catch + :empty -> acc + end + end) + end) + end) + |> case do + [] -> 0 + acc -> acc + end + end + + defp tuple_union(left, right), do: left ++ right + + defp tuple_to_quoted(dnf) do + dnf + |> tuple_normalize() + |> Enum.map(&tuple_each_to_quoted/1) + |> case do + [] -> [] + dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap() + end + end + + defp tuple_each_to_quoted({tag, positive_map, negative_maps}) do + case negative_maps do + [] -> + tuple_literal_to_quoted({tag, positive_map}) + + _ -> + negative_maps + |> Enum.map(&tuple_literal_to_quoted/1) + |> Enum.reduce(&{:or, [], [&2, &1]}) + |> Kernel.then( + &{:and, [], [tuple_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]} + ) + end + end + + defp tuple_literal_to_quoted({:closed, []}), do: {:{}, [], []} + + defp tuple_literal_to_quoted({tag, elements}) do + case tag do + :closed -> {:{}, [], Enum.map(elements, &to_quoted/1)} + :open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]} + end + end + + # Pads a list of elements with term(). + defp tuple_fill(elements, desired_length) do + pad_length = desired_length - length(elements) + + if pad_length < 0 do + raise ArgumentError, "tuple_fill: elements are longer than the desired length" + else + elements ++ List.duplicate(term(), pad_length) + end + end + + # Check if a tuple represented in DNF is empty + defp tuple_empty?(dnf) do + Enum.all?(dnf, fn {tag, pos, negs} -> tuple_empty?(tag, pos, negs) end) + end + + # No negations, so not empty + defp tuple_empty?(_, _, []), do: false + # Open empty negation makes it empty + defp tuple_empty?(_, _, [{:open, []} | _]), do: true + # Open positive can't be emptied by a single closed negative + defp tuple_empty?(:open, _, [{:closed, _}]), do: false + + defp tuple_empty?(tag, elements, [{neg_tag, neg_elements} | negs]) do + n = length(elements) + m = length(neg_elements) + + # Scenarios where the difference is guaranteed to be empty: + # 1. When removing larger tuples from a fixed-size positive tuple + # 2. When removing smaller tuples from larger tuples + if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do + tuple_empty?(tag, elements, negs) + else + tuple_elements([], tag, elements, neg_elements, negs) and + tuple_compatibility(n, m, tag, elements, neg_tag, negs) + end + end + + # Recursively check elements for emptiness + defp tuple_elements(_, _, _, [], _), do: true + + defp tuple_elements(acc, tag, elements, [neg_type | neg_elements], negs) do + {ty, elements} = List.pop_at(elements, 0, term()) + diff = difference(ty, neg_type) + + (empty?(diff) or tuple_empty?(tag, Enum.reverse(acc, [diff | elements]), negs)) and + tuple_elements([ty | acc], tag, elements, neg_elements, negs) + end + + # Determines if the set difference is empty when: + # - Positive tuple: {tag, elements} of size n + # - Negative tuple: open or closed tuples of size m + defp tuple_compatibility(n, m, tag, elements, neg_tag, negs) do + # The tuples to consider are all those of size n to m - 1, and if the negative tuple is + # closed, we also need to consider tuples of size greater than m + 1. + tag == :closed or + (Enum.all?(n..(m - 1)//1, &tuple_empty?(:closed, tuple_fill(elements, &1), negs)) and + (neg_tag == :open or tuple_empty?(:open, tuple_fill(elements, m + 1), negs))) + end + + @doc """ + Fetches the type of the value returned by accessing `index` on `tuple` + with the assumption that the descr is exclusively a tuple (or dynamic). + + Returns one of: + - `{false, type}` if the element is always accessible and has the given `type`. + - `{true, type}` if the element may exist and has the given `type`. + - `:badindex` if the index is never accessible in the tuple type. + - `:badtuple` if the descr is not a tuple type. + + ## Examples + + iex> tuple_fetch(tuple([integer(), atom()]), 0) + {false, integer()} + + :badindex + + iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1) + {true, atom()} + + iex> tuple_fetch(dynamic(), 0) + {true, dynamic()} + + iex> tuple_fetch(integer(), 0) + :badtuple + + """ + def tuple_fetch(_, index) when index < 0, do: :badindex + def tuple_fetch(:term, _key), do: :badtuple + + def tuple_fetch(%{} = descr, key) do + case :maps.take(:dynamic, descr) do + :error -> + if descr_key?(descr, :tuple) and tuple_only?(descr) do + {static_optional?, static_type} = tuple_fetch_static(descr, key) + + # If I access a static tuple at a "open position", we have two options: + # + # 1. Do not allow the access and return :badindex, + # you must use dynamic for these cases (this is what we chose for maps) + # + # 2. Allow the access and return the static type + # + # The trouble with allowing the access is that it is a potential runtime + # error not being caught by the type system. + # + # Furthermore, our choice here, needs to be consistent with elem/put_elem + # when the index is the `integer()` type. If we choose to return `:badindex`, + # then all elem/put_elem with an `integer()` and the tuple is not dynamic + # should also be a static typing error. We chose to go with 1. + if static_optional? or empty?(static_type) do + :badindex + else + {false, static_type} + end + else + :badtuple + end + + {dynamic, static} -> + if descr_key?(dynamic, :tuple) and tuple_only?(static) do + {dynamic_optional?, dynamic_type} = tuple_fetch_static(dynamic, key) + {static_optional?, static_type} = tuple_fetch_static(static, key) + + if empty?(dynamic_type) do + :badindex + else + {static_optional? or dynamic_optional?, union(dynamic(dynamic_type), static_type)} + end + else + :badtuple + end + end + end + + defp tuple_only?(descr), do: empty?(Map.delete(descr, :tuple)) + + defp tuple_fetch_static(descr, index) when is_integer(index) do + case descr do + :term -> + {true, term()} + + %{tuple: tuple} -> + tuple_split_on_index(tuple, index) + |> Enum.reduce(none(), &union/2) + |> pop_optional_static() + + %{} -> + {false, none()} + end + end + + defp tuple_split_on_index(dnf, index) do + Enum.flat_map(dnf, fn + {tag, elements, []} -> + [Enum.at(elements, index, tag_to_type(tag))] + + {tag, elements, negs} -> + {fst, snd} = tuple_pop_index(tag, elements, index) + + case tuple_split_negative(negs, index) do + :empty -> [] + negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations(fst, snd) + end + end) + end + + defp tuple_pop_index(tag, elements, index) do + case List.pop_at(elements, index) do + {nil, _} -> {tag_to_type(tag), %{tuple: [{tag, elements, []}]}} + {type, rest} -> {type, %{tuple: [{tag, rest, []}]}} + end + end + + defp tuple_split_negative(negs, index) do + Enum.reduce_while(negs, [], fn + {:open, []}, _acc -> {:halt, :empty} + {tag, elements}, acc -> {:cont, [tuple_pop_index(tag, elements, index) | acc]} + end) + end + + # Use heuristics to normalize a tuple dnf for pretty printing. + defp tuple_normalize(dnf) do + for {tag, elements, negs} <- dnf, + not tuple_empty?([{tag, elements, negs}]) do + n = length(elements) + {tag, elements, Enum.reject(negs, &tuple_empty_negation?(tag, n, &1))} + end + end + + # Remove useless negations, which denote tuples of incompatible sizes. + defp tuple_empty_negation?(tag, n, {neg_tag, neg_elements}) do + m = length(neg_elements) + (tag == :closed and n < m) or (neg_tag == :closed and n > m) + end + ## Pairs # # To simplify disjunctive normal forms of e.g., map types, it is useful to diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 98acbc79822..86a2151d681 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -56,6 +56,27 @@ defmodule Module.Types.DescrTest do assert equal?(union(intersection(dynamic(), atom()), atom()), atom()) end + test "tuple" do + assert equal?(union(tuple(), tuple()), tuple()) + + t = tuple([integer(), atom()]) + assert equal?(union(t, t), t) + + assert union(tuple([integer(), atom()]), tuple([float(), atom()])) + |> equal?(tuple([union(integer(), float()), atom()])) + + assert union(tuple([integer(), atom()]), tuple([integer(), binary()])) + |> equal?(tuple([integer(), union(atom(), binary())])) + + assert open_tuple([atom()]) + |> union(tuple([atom(), integer()])) + |> equal?(open_tuple([atom()])) + + assert tuple([union(integer(), atom())]) + |> difference(open_tuple([atom()])) + |> equal?(tuple([integer()])) + end + test "map" do assert equal?(union(open_map(), open_map()), open_map()) assert equal?(union(closed_map(a: integer()), open_map()), open_map()) @@ -101,6 +122,16 @@ defmodule Module.Types.DescrTest do assert empty?(intersection(intersection(dynamic(), atom()), integer())) end + test "tuple" do + assert empty?(intersection(open_tuple([atom()]), open_tuple([integer()]))) + + assert intersection(open_tuple([atom()]), tuple([term(), integer()])) + |> equal?(tuple([atom(), integer()])) + + assert intersection(tuple([term(), integer()]), tuple([atom(), term()])) + |> equal?(tuple([atom(), integer()])) + end + test "map" do assert intersection(open_map(), open_map()) == open_map() assert equal?(intersection(closed_map(a: integer()), open_map()), closed_map(a: integer())) @@ -151,6 +182,62 @@ defmodule Module.Types.DescrTest do assert empty?(difference(none(), dynamic())) end + defp empty_tuple(), do: tuple([]) + defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n)) + defp tuple_of_size(n) when is_integer(n), do: tuple(List.duplicate(term(), n)) + + test "tuple" do + assert empty?(difference(open_tuple([atom()]), open_tuple([term()]))) + refute empty?(difference(tuple(), empty_tuple())) + refute tuple_of_size_at_least(2) |> difference(tuple_of_size(2)) |> empty?() + assert tuple_of_size_at_least(2) |> difference(tuple_of_size_at_least(1)) |> empty?() + assert tuple_of_size_at_least(3) |> difference(tuple_of_size_at_least(3)) |> empty?() + refute tuple_of_size_at_least(2) |> difference(tuple_of_size_at_least(3)) |> empty?() + refute tuple([term(), term()]) |> difference(tuple([atom(), term()])) |> empty?() + refute tuple([term(), term()]) |> difference(tuple([atom()])) |> empty?() + assert tuple([term(), term()]) |> difference(tuple([term(), term()])) |> empty?() + + # {term(), term(), ...} and not ({term(), term(), term(), ...} or {term(), term()}) + assert tuple_of_size_at_least(2) + |> difference(tuple_of_size(2)) + |> difference(tuple_of_size_at_least(3)) + |> empty?() + + assert tuple([term(), term()]) + |> difference(tuple([atom()])) + |> difference(open_tuple([term()])) + |> difference(empty_tuple()) + |> empty?() + + refute difference(tuple(), empty_tuple()) + |> difference(open_tuple([term(), term()])) + |> empty? + + assert difference(open_tuple([term()]), open_tuple([term(), term()])) + |> difference(tuple([term()])) + |> empty?() + + assert open_tuple([atom()]) + |> difference(tuple([integer(), integer()])) + |> equal?(open_tuple([atom()])) + + assert tuple([union(atom(), integer()), term()]) + |> difference(open_tuple([atom(), term()])) + |> equal?(tuple([integer(), term()])) + + assert tuple([union(atom(), integer()), term()]) + |> difference(open_tuple([atom(), term()])) + |> difference(open_tuple([integer(), term()])) + |> empty?() + + assert tuple([term(), union(atom(), integer()), term()]) + |> difference(open_tuple([term(), integer()])) + |> equal?(tuple([term(), atom(), term()])) + + assert difference(tuple(), open_tuple([term(), term()])) + |> equal?(union(tuple([term()]), tuple([]))) + end + test "map" do assert empty?(difference(open_map(), open_map())) assert empty?(difference(open_map(), term())) @@ -166,6 +253,8 @@ defmodule Module.Types.DescrTest do assert difference(open_map(a: atom()), closed_map(b: integer())) |> equal?(open_map(a: atom())) + + refute empty?(difference(open_map(), empty_map())) end end @@ -204,6 +293,20 @@ defmodule Module.Types.DescrTest do assert subtype?(integer(), union(dynamic(), integer())) end + test "tuple" do + assert subtype?(empty_tuple(), tuple()) + assert subtype?(tuple([integer(), atom()]), tuple()) + refute subtype?(empty_tuple(), open_tuple([term()])) + assert subtype?(tuple([integer(), atom()]), tuple([term(), term()])) + refute subtype?(tuple([integer(), atom()]), tuple([integer(), integer()])) + refute subtype?(tuple([integer(), atom()]), tuple([atom(), atom()])) + + assert subtype?(tuple([integer(), atom()]), open_tuple([integer(), atom()])) + refute subtype?(tuple([term()]), open_tuple([term(), term()])) + refute subtype?(tuple([integer(), atom()]), open_tuple([integer(), integer()])) + refute subtype?(open_tuple([integer(), atom()]), tuple([integer(), integer()])) + end + test "map" do assert subtype?(open_map(), term()) assert subtype?(closed_map(a: integer()), open_map()) @@ -244,6 +347,10 @@ defmodule Module.Types.DescrTest do assert compatible?(integer(), dynamic()) end + test "tuple" do + assert compatible?(dynamic(tuple()), tuple([integer(), atom()])) + end + test "map" do assert compatible?(closed_map(a: integer()), open_map()) assert compatible?(intersection(dynamic(), open_map()), closed_map(a: integer())) @@ -251,6 +358,25 @@ defmodule Module.Types.DescrTest do end describe "empty" do + test "tuple" do + assert intersection(tuple([integer(), atom()]), open_tuple([atom()])) |> empty? + refute open_tuple([integer(), integer()]) |> difference(empty_tuple()) |> empty? + refute open_tuple([integer(), integer()]) |> difference(open_tuple([atom()])) |> empty? + refute open_tuple([term()]) |> difference(tuple([term()])) |> empty? + assert difference(tuple(), empty_tuple()) |> difference(open_tuple([term()])) |> empty? + assert difference(tuple(), open_tuple([term()])) |> difference(empty_tuple()) |> empty? + + refute open_tuple([term()]) + |> difference(tuple([term()])) + |> difference(tuple([term()])) + |> empty? + + assert tuple([integer(), union(integer(), atom())]) + |> difference(tuple([integer(), integer()])) + |> difference(tuple([integer(), atom()])) + |> empty? + end + test "map" do assert intersection(closed_map(b: atom()), open_map(a: integer())) |> empty?() end @@ -292,6 +418,59 @@ defmodule Module.Types.DescrTest do assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} end + test "tuple_fetch" do + assert tuple_fetch(term(), 0) == :badtuple + assert tuple_fetch(integer(), 0) == :badtuple + assert tuple_fetch(tuple([integer(), atom()]), 0) == {false, integer()} + assert tuple_fetch(tuple([integer(), atom()]), 1) == {false, atom()} + assert tuple_fetch(tuple([integer(), atom()]), 2) == :badindex + assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex + assert tuple_fetch(empty_tuple(), 0) == :badindex + assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badindex + assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == {false, atom()} + + assert difference(tuple([union(integer(), atom())]), open_tuple([atom()])) + |> tuple_fetch(0) == {false, integer()} + + assert tuple_fetch(union(tuple([integer(), atom()]), dynamic(open_tuple([atom()]))), 1) + |> Kernel.then(fn {opt, ty} -> opt and equal?(ty, union(atom(), dynamic())) end) + + assert tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0) == + {false, union(integer(), atom())} + + assert tuple([integer(), atom(), union(atom(), integer())]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} + + assert tuple([integer(), atom(), union(union(atom(), integer()), list())]) + |> difference(tuple([integer(), term(), atom()])) + |> difference(open_tuple([term(), atom(), list()])) + |> tuple_fetch(2) == {false, integer()} + + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), integer()])) + |> tuple_fetch(1) == :badindex + + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} + + assert tuple_fetch(tuple(), 0) == :badindex + end + + test "tuple_fetch with dynamic" do + assert tuple_fetch(dynamic(), 0) == {true, dynamic()} + assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex + assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex + assert tuple_fetch(union(dynamic(), integer()), 0) == :badtuple + + assert tuple_fetch(dynamic(tuple()), 0) + |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) + + assert tuple_fetch(union(dynamic(), open_tuple([atom()])), 0) == + {true, union(atom(), dynamic())} + end + test "map_fetch" do assert map_fetch(term(), :a) == :badmap assert map_fetch(union(open_map(), integer()), :a) == :badmap @@ -369,6 +548,10 @@ defmodule Module.Types.DescrTest do test "map_fetch with dynamic" do assert map_fetch(dynamic(), :a) == {true, dynamic()} + assert map_fetch(union(dynamic(), integer()), :a) == :badmap + assert map_fetch(union(dynamic(open_map(a: integer())), integer()), :a) == :badmap + assert map_fetch(union(dynamic(integer()), integer()), :a) == :badmap + assert intersection(dynamic(), open_map(a: integer())) |> map_fetch(:a) == {false, intersection(integer(), dynamic())} @@ -430,6 +613,20 @@ defmodule Module.Types.DescrTest do "dynamic(%{a: integer()})" end + test "tuples" do + assert tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom()}" + assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}" + + assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == + "{integer(), atom()} or {atom(), ...}" + + assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == + "{integer(), atom()} and not {atom(), ...}" + + assert tuple([closed_map(a: integer()), open_map()]) |> to_quoted_string() == + "{%{a: integer()}, %{...}}" + end + test "map" do assert empty_map() |> to_quoted_string() == "empty_map()" assert open_map() |> to_quoted_string() == "%{...}"