From eee8786a56f33312e2969def04ad028e01f0daaf Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 6 May 2025 14:33:54 +0200 Subject: [PATCH 1/6] Add domain key types in maps - Introduced tests for union, intersection, and difference operations involving domain key types. - Validated subtype relationships and intersection results for maps with domain keys. - Enhanced map fetch and delete functionalities to handle domain key types. - Ensured correct behavior of dynamic types with domain keys in various scenarios. --- lib/elixir/lib/module/types/descr.ex | 669 ++++++++++++++++-- .../test/elixir/module/types/descr_test.exs | 289 +++++++- 2 files changed, 908 insertions(+), 50 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b4bd5dc257f..6228100f70e 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -27,6 +27,22 @@ defmodule Module.Types.Descr do @bit_top (1 <<< 8) - 1 @bit_number @bit_integer ||| @bit_float + # Domain key types + @domain_key_types [ + :binary, + :empty_list, + :integer, + :float, + :pid, + :port, + :reference, + :fun, + :atom, + :tuple, + :map, + :list + ] + @atom_top {:negation, :sets.new(version: 2)} @map_top [{:open, %{}, []}] @non_empty_list_top [{:term, :term, []}] @@ -67,7 +83,17 @@ defmodule Module.Types.Descr do def atom(as), do: %{atom: atom_new(as)} def atom(), do: %{atom: @atom_top} def binary(), do: %{bitmap: @bit_binary} - def closed_map(pairs), do: map_descr(:closed, pairs) + + def closed_map(pairs) do + {regular_pairs, domain_pairs} = split_domain_key_pairs(pairs) + # Validate domain keys and make their types optional + domain_pairs = validate_domain_keys(domain_pairs) + + if domain_pairs == [], + do: map_descr(:closed, regular_pairs), + else: map_descr(:closed, regular_pairs, domain_pairs) + end + def empty_list(), do: %{bitmap: @bit_empty_list} def empty_map(), do: %{map: @map_empty} def integer(), do: %{bitmap: @bit_integer} @@ -75,8 +101,30 @@ defmodule Module.Types.Descr do def fun(), do: %{bitmap: @bit_fun} def list(type), do: list_descr(type, @empty_list, true) def non_empty_list(type, tail \\ @empty_list), do: list_descr(type, tail, false) + def open_map(), do: %{map: @map_top} - def open_map(pairs), do: map_descr(:open, pairs) + + @doc "An open map with a default type %{term() => default}" + def open_map_with_default(default) do + map_descr( + :open, + [], + Enum.map(@domain_key_types, fn key_type -> + {{:domain_key, key_type}, if_set(default)} + end) + ) + end + + def open_map(pairs) do + {regular_pairs, domain_pairs} = split_domain_key_pairs(pairs) + # Validate domain keys and make their types optional + domain_pairs = validate_domain_keys(domain_pairs) + + if domain_pairs == [], + do: map_descr(:open, regular_pairs), + else: map_descr(:open, regular_pairs, domain_pairs) + end + def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements) def pid(), do: %{bitmap: @bit_pid} def port(), do: %{bitmap: @bit_port} @@ -106,7 +154,15 @@ defmodule Module.Types.Descr do def not_set(), do: @not_set def if_set(:term), do: term_or_optional() - def if_set(type), do: Map.put(type, :optional, 1) + # actually, if type contains a :dynamic part, :optional gets added there because + # the dynamic + def if_set(type) do + case type do + %{dynamic: dyn} -> Map.put(%{type | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1) + _ -> Map.put(type, :optional, 1) + end + end + defp term_or_optional(), do: @term_or_optional @compile {:inline, @@ -1284,6 +1340,24 @@ defmodule Module.Types.Descr do end end + def map_descr(tag, fields, domains) do + {fields, fields_dynamic?} = map_descr_pairs(fields, [], false) + {domains, domains_dynamic?} = map_descr_pairs(domains, [], false) + + fields_map = :maps.from_list(if fields_dynamic?, do: Enum.reverse(fields), else: fields) + + domains_map = + :maps.from_list(if domains_dynamic?, do: Enum.reverse(domains), else: domains) + + # |> dbg() + + if fields_dynamic? or domains_dynamic? do + %{dynamic: %{map: map_new(tag, fields_map, domains_map)}} + else + %{map: map_new(tag, fields_map, domains_map)} + end + end + defp map_descr_pairs([{key, :term} | rest], acc, dynamic?) do map_descr_pairs(rest, [{key, :term} | acc], dynamic?) end @@ -1302,10 +1376,17 @@ defmodule Module.Types.Descr do defp tag_to_type(:open), do: term_or_optional() defp tag_to_type(:closed), do: not_set() + defp tag_to_type({:open, domains}), + do: Map.get(domains, {:domain_key, :atom}, term_or_optional()) |> if_set() + + defp tag_to_type({:closed, domains}), + do: Map.get(domains, {:domain_key, :atom}, not_set()) |> if_set() + defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :optional) defp map_new(tag, fields = %{}), do: [{tag, fields, []}] + defp map_new(tag, fields = %{}, domains = %{}), do: [{{tag, domains}, fields, []}] defp map_only?(descr), do: empty?(Map.delete(descr, :map)) @@ -1481,6 +1562,76 @@ defmodule Module.Types.Descr do :maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed) end + # Both arguments are tags with domains + defp map_literal_intersection(tag1, map1, tag2, map2) do + # For a closed map with domains intersected with an open map with domains: + # 1. The result is closed (more restrictive) + # 2. We need to check each domain in the open map against the closed map + default1 = tag_to_type(tag1) + default2 = tag_to_type(tag2) + + # Compute the new domain + tag = map_domain_intersection(tag1, tag2) + + # Go over all fields in map1 and map2 with default atom types atom1 and atom2 + # 1. If key is in both maps, compute non empty intersection (:error if it is none) + # 2. If key is only in map1, compute non empty intersection with atom2 + # 3. If key is only in map2, compute non empty intersection with atom1 + # Can be considered an intersection with default values where I iterate on all + # key labels in both map1 and map2. + keys1_set = :sets.from_list(Map.keys(map1), version: 2) + keys2_set = :sets.from_list(Map.keys(map2), version: 2) + + # Combine all unique keys using :sets.union + all_keys_set = :sets.union(keys1_set, keys2_set) + all_keys = :sets.to_list(all_keys_set) + + new_fields = + for key <- all_keys do + in_map1? = Map.has_key?(map1, key) + in_map2? = Map.has_key?(map2, key) + + cond do + in_map1? and in_map2? -> {key, non_empty_intersection!(map1[key], map2[key])} + in_map1? -> {key, non_empty_intersection!(map1[key], default2)} + in_map2? -> {key, non_empty_intersection!(default1, map2[key])} + end + end + |> :maps.from_list() + + {tag, new_fields} + end + + defp map_domain_intersection(:closed, _), do: :closed + defp map_domain_intersection(_, :closed), do: :closed + defp map_domain_intersection(:open, tag), do: tag + defp map_domain_intersection(tag, :open), do: tag + + defp map_domain_intersection({tag1, domains1}, {tag2, domains2}) do + default1 = tag_to_type(tag1) + default2 = tag_to_type(tag2) + + new_domains = + for domain_key <- @domain_key_types, reduce: %{} do + acc_domains -> + type1 = Map.get(domains1, {:domain_key, domain_key}, default1) + type2 = Map.get(domains2, {:domain_key, domain_key}, default2) + + inter = intersection(type1, type2) + + if empty?(inter) do + acc_domains + else + Map.put(acc_domains, {:domain_key, domain_key}, inter) + end + end + + new_tag = map_domain_intersection(tag1, tag2) + + # If the explicit domains are empty, use simple atom tags + if map_size(new_domains) == 0, do: new_tag, else: {new_tag, new_domains} + end + defp map_literal_intersection_loop(:none, acc), do: {:closed, acc} defp map_literal_intersection_loop({key, type1, iterator}, acc) do @@ -1582,10 +1733,7 @@ defmodule Module.Types.Descr do # Optimization: if the key does not exist in the map, avoid building # if_set/not_set pairs and return the popped value directly. defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do - case tag do - :open -> {true, term()} - :closed -> {true, none()} - end + tag_to_type(tag) |> pop_optional_static() end # Takes a map dnf and returns the union of types it can take for a given key. @@ -1593,13 +1741,11 @@ defmodule Module.Types.Descr do defp map_fetch_static(%{map: dnf}, key) do dnf |> Enum.reduce(none(), fn - # Optimization: if there are no negatives, - # we can return the value directly. + # Optimization: if there are no negatives and key exists, return its value {_tag, %{^key => value}, []}, acc -> value |> union(acc) - # Optimization: if there are no negatives - # and the key does not exist, return the default one. + # Optimization: if there are no negatives and the key does not exist, return the default one. {tag, %{}, []}, acc -> tag_to_type(tag) |> union(acc) @@ -1678,6 +1824,30 @@ defmodule Module.Types.Descr do defp map_put_static(descr, _key, _type), do: descr + @doc """ + Removes a key from a given type from a map type. + """ + # defp map_delete_static(descr, :term), do: raise(:todo) + + # def map_delete_static(descr, key = %{}) do + # # 1 this only is useful for atom types. the others are already optional + # case key do + # %{atom: atoms} -> + # case atoms do + # {:union, set} -> map_ + # end + # end + # end + + # Make a key optional in a map type. + defp map_make_optional_static(descr, key) do + # We pass nil as the initial value so we can avoid computing the unions. + with {nil, descr} <- + map_take(descr, key, nil, &union(&1, open_map([]))) do + {:ok, descr} + end + end + @doc """ Removes a key from a map type. """ @@ -1689,6 +1859,205 @@ defmodule Module.Types.Descr do end end + @doc """ + Computes the union of types for keys matching `key_type` within the `map_type`. + + This generalizes `map_fetch/2` (which operates on a single literal key) to + work with a key type (e.g., `atom()`, `integer()`, `:a or :b`). It's based + on the map-selection operator t.[t'] described in "Types for Tables" + (Castagna et al., ICFP 2023). + + ## Return Values + + The function returns a tuple indicating the outcome and the resulting type union: + + * `{:ok, type}`: Standard success. `type` is the resulting union of types + found for the matching keys. This covers two sub-cases: + * **Keys definitely exist:** If `disjoint?(type, not_set())` is true, + all keys matching `key_type` are guaranteed to exist. + * **Keys may exist:** If `type` includes `not_set()`, some keys + matching `key_type` might exist (contributing their types) while + others might be absent (contributing `not_set()`). + + * `{:ok_absent, type}`: Success, but the resulting `type` is `none()` or a + subtype of `not_set()`. This indicates that no key matching `key_type` + can exist with a value other than `not_set()`. The caller may wish to + issue a warning, as this often implies selecting a field that is + effectively undefined. + + * `{:ok_spillover, type}`: Success, and `type` is the resulting union. + However, this indicates that the `key_type` included keys not explicitly + covered by the `map_type`'s fields or domain specifications. The + projection relied on the map's default behavior (e.g., the `term()` + value type for unspecified keys in an open map). The caller may wish to + issue a warning, as this could conceal issues like selecting keys + not intended by the map's definition. + + * `:badmap`: The input `map_type` was invalid (e.g., not a map type or + a dynamic type wrapping a map type). + + * `:badkeytype`: The input `key_type` was invalid (e.g., not a subtype + of the allowed key types like `atom()`, `integer()`, etc.). + """ + def map_get(descr, key_descr) do + case :maps.take(:dynamic, descr) do + :error -> + case :maps.take(:dynamic, key_descr) do + :error -> + type_selected = map_get_static(descr, key_descr) + {optional?, type_selected} = pop_optional_static(type_selected) + + cond do + empty?(type_selected) -> {:ok_absent, atom([nil])} + optional? -> {:ok, type_selected |> nil_or_type()} + true -> {:ok_present, type_selected} + end + + {dynamic, static} -> + map_get_static(dynamic, key_descr) + |> union(dynamic(map_get_static(static, key_descr))) + end + + {dynamic, static} -> + case :maps.take(:dynamic, key_descr) do + :error -> + map_get_static(dynamic, key_descr) + |> union(dynamic(map_get_static(static, key_descr))) + + {dynamic_key, static_key} -> + map_get_static(dynamic, dynamic_key) + |> union(dynamic(map_get_static(static, static_key))) + end + end + end + + # Returns the list of key types that are covered by the key_descr. + # E.g., for `{atom([:ok]), term} or integer()` it returns `[:tuple, :integer]`. + # We treat bitmap types as a separate key type. + defp covered_key_types(key_descr) do + for {type_kind, type} <- key_descr, reduce: [] do + acc -> + cond do + type_kind == :atom -> [{:atom, type} | acc] + type_kind == :bitmap -> bitmap_to_domain_keys(type) ++ acc + not empty?(%{type_kind => type}) -> [type_kind | acc] + true -> acc + end + end + end + + defp bitmap_to_domain_keys(bitmap) do + [ + if((bitmap &&& @bit_binary) != 0, do: :binary), + if((bitmap &&& @bit_empty_list) != 0, do: :empty_list), + if((bitmap &&& @bit_integer) != 0, do: :integer), + if((bitmap &&& @bit_float) != 0, do: :float), + if((bitmap &&& @bit_pid) != 0, do: :pid), + if((bitmap &&& @bit_port) != 0, do: :port), + if((bitmap &&& @bit_reference) != 0, do: :reference), + if((bitmap &&& @bit_fun) != 0, do: :fun) + ] + |> Enum.reject(&is_nil/1) + end + + def nil_or_type(type), do: union(type, atom([nil])) + + def map_get_static(%{map: [{tag, fields, []}]}, key_descr) when is_atom(tag) do + map_get_static(%{map: [{{tag, %{}}, fields, []}]}, key_descr) + end + + # TODO: handle impact from explicit keys (like, having a: integer() when + # selecting on atom() keys. + def map_get_static(%{map: [{{tag, domains}, fields, []}]}, key_descr) do + # For each non-empty kind of type in the key_descr, we add the corresponding key domain in a union. + key_descr + |> covered_key_types() + |> Enum.reduce(none(), fn + {:atom, atom_type}, acc -> + map_get_single_atom([{{tag, domains}, fields, []}], atom_type) |> union(acc) + + key_type, acc -> + # Note: we could stop if we reach term()_or_optional() + Map.get(domains, {:domain_key, key_type}, tag_to_type(tag)) |> union(acc) + end) + end + + # TODO: handle the atom type in key_descr + # - do the atom singletons [at1, at2, ...] + # -> can just do map_fetch_key maybe? + # - what to do for the negation? not (a1 or a2 or ...) + def map_get_static(%{map: dnf}, key_descr) do + key_descr + |> covered_key_types() + |> Enum.reduce(none(), fn + {:atom, atom_type}, acc -> + map_get_single_atom(dnf, atom_type) |> union(acc) + + key_type, acc -> + map_get_single_domain(dnf, key_type) |> union(acc) + end) + end + + # Take a map dnf and return the union of types when selecting atoms. + # This includes cases: + # - union of atoms {a1, a2, ...}, in which case the defined ones are selected as well. If all of those are certainly defined, then the result does not contain nil. Otherwise, it spills over the atom domain. + # - a negation of atoms not {a1, a2, ...}, in which case we just take care not to include + # the negated atoms in the result. + def map_get_single_atom(dnf, atom_type) do + case atom_type do + {:union, atoms} -> + atoms = :sets.to_list(atoms) + + atoms + |> Enum.reduce(none(), fn atom, acc -> + {static_optional?, type} = map_fetch_static(%{map: dnf}, atom) + + if static_optional? do + union(type, acc) |> nil_or_type() |> if_set() + else + union(type, acc) + end + end) + + {:negation, atoms} -> + atoms = :sets.to_list(atoms) + + # TODO: do the "don't take this set of atoms" things + map_get_single_domain(dnf, :atom) + end + end + + # Take a map dnf and return the union of types for the given key domain. + def map_get_single_domain(dnf, key_domain) when is_atom(key_domain) do + dnf + |> Enum.reduce(none(), fn + {tag, _fields, []}, acc when is_atom(tag) -> + tag_to_type(tag) |> union(acc) + + # Optimization: if there are no negatives and domains exists, return its value + {{_tag, %{{:domain_key, ^key_domain} => value}}, _fields, []}, acc -> + value |> union(acc) + + # Optimization: if there are no negatives and the key does not exist, return the default type. + {{tag, %{}}, _fields, []}, acc -> + tag_to_type(tag) |> union(acc) + + {tag, fields, negs}, acc -> + {fst, snd} = map_pop_domain(tag, fields, key_domain) + + case map_split_negative_domain(negs, key_domain) do + :empty -> + acc + + negative -> + negative + |> pair_make_disjoint() + |> pair_eliminate_negations_fst(fst, snd) + |> union(acc) + end + end) + end + @doc """ Removes a key from a map type and return its type. @@ -1799,44 +2168,168 @@ defmodule Module.Types.Descr do defp map_empty?(:open, fs, [{:closed, _} | negs]), do: map_empty?(:open, fs, negs) defp map_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do - (Enum.all?(neg_fields, fn {neg_key, neg_type} -> - cond do - # Keys that are present in the negative map, but not in the positive one - is_map_key(fields, neg_key) -> - true - - # The key is not shared between positive and negative maps, - # if the negative type is optional, then there may be a value in common - tag == :closed -> - is_optional_static(neg_type) - - # There may be value in common - tag == :open -> - diff = difference(term_or_optional(), neg_type) - empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs) - end - end) and - Enum.all?(fields, fn {key, type} -> - case neg_fields do - %{^key => neg_type} -> - diff = difference(type, neg_type) - empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) - - %{} -> - cond do - neg_tag == :open -> - true - - neg_tag == :closed and not is_optional_static(type) -> - false - - true -> - # an absent key in a open negative map can be ignored - diff = difference(type, tag_to_type(neg_tag)) - empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) - end + if map_check_domain_keys(tag, neg_tag) do + atom_default = tag_to_type(tag) + neg_atom_default = tag_to_type(neg_tag) + + (Enum.all?(neg_fields, fn {neg_key, neg_type} -> + cond do + # Ignore keys present in both maps; will be handled below + is_map_key(fields, neg_key) -> + true + + # The key is not shared between positive and negative maps, + # if the negative type is optional, then there may be a value in common + tag == :closed -> + is_optional_static(neg_type) + + # There may be value in common + tag == :open -> + diff = difference(term_or_optional(), neg_type) + empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs) + + true -> + diff = difference(atom_default, neg_type) + empty?(diff) or map_empty?(tag, Map.put(fields, neg_key, diff), negs) end - end)) or map_empty?(tag, fields, negs) + end) and + Enum.all?(fields, fn {key, type} -> + case neg_fields do + %{^key => neg_type} -> + diff = difference(type, neg_type) + empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) + + %{} -> + cond do + neg_tag == :open -> + true + + neg_tag == :closed and not is_optional_static(type) -> + false + + true -> + # an absent key in a open negative map can be ignored + diff = difference(type, neg_atom_default) + empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) + end + end + end)) or map_empty?(tag, fields, negs) + else + map_empty?(tag, fields, negs) + end + end + + # Verify the domain condition from equation (22) in paper ICFP'23 https://www.irif.fr/~gc/papers/icfp23.pdf + # which is that every domain key type in the positive map is a subtype + # of the corresponding domain key type in the negative map. + def map_check_domain_keys(tag, neg_tag) do + # Those are the difference cases: + # - {:closed, _}, {:closed, _} -> all keys present in the positive map are either not_set(), or a subtype of the (present corresponding) key in the negative map + # - {:closed, _}, {:open, _} -> for all keys present in both domains, the positive key is a subtype of the negative key + # - {:open, _}, {:closed, _} -> all keys in the positive map must be present in the negative map, and be subtype of the negative key. the negative map must contain all possible domain key types, and all those not in the positive map must be at least term_or_optional() + # - {:open, _}, {:open, _} -> for all keys in the negative map, either it is a supertype of the existing key in the positive map, or if it is not present in the positive map, it must be at least term_or_optional() + # - :open, {:open, neg_domains} -> every present domain key type in the negative domains is at least term_or_optional() + # - :open, {:closed, neg_domains} -> the domains must include all possible domain key types, and they must be at least term_or_optional() + # - :closed, _ -> true + # - _, :open -> true + # - {:closed, pos_domains}, :closed -> every present domain key type is a subtype of not_set() + # - {:open, pos_domains}, :closed -> the pos_domains must include all possible domain key types, and they must be subtypes of not_set() + case {tag, neg_tag} do + {:closed, _} -> + true + + {_, :open} -> + true + + {{:closed, pos_domains}, {:closed, neg_domains}} -> + Enum.all?(pos_domains, fn {{:domain_key, key}, type} -> + subtype?(type, not_set()) || + case Map.get(neg_domains, {:domain_key, key}) do + nil -> false + neg_type -> subtype?(type, neg_type) + end + end) + + # Closed positive with open negative domains + {{:closed, pos_domains}, {:open, neg_domains}} -> + Enum.all?(pos_domains, fn {{:domain_key, key}, pos_type} -> + case Map.get(neg_domains, {:domain_key, key}) do + # Key not in both, so condition passes + nil -> true + neg_type -> subtype?(pos_type, neg_type) + end + end) + + # Open positive with closed negative domains + {{:open, pos_domains}, {:closed, neg_domains}} -> + # All keys in positive domains must be in negative and be subtypes + positive_check = + Enum.all?(pos_domains, fn {{:domain_key, key}, pos_type} -> + case Map.get(neg_domains, {:domain_key, key}) do + # Key not in negative map + nil -> false + neg_type -> subtype?(pos_type, neg_type) + end + end) + + # Negative must contain all domain key types + negative_check = + Enum.all?(@domain_key_types, fn domain_key -> + domain_key_present = Map.has_key?(neg_domains, {:domain_key, domain_key}) + pos_has_key = Map.has_key?(pos_domains, {:domain_key, domain_key}) + + domain_key_present && + (pos_has_key || + subtype?(term_or_optional(), Map.get(neg_domains, {:domain_key, domain_key}))) + end) + + positive_check && negative_check + + # Both open domains + {{:open, pos_domains}, {:open, neg_domains}} -> + Enum.all?(neg_domains, fn {{:domain_key, key}, neg_type} -> + case Map.get(pos_domains, {:domain_key, key}) do + nil -> subtype?(term_or_optional(), neg_type) + pos_type -> subtype?(pos_type, neg_type) + end + end) + + # Open map with open negative domains + {:open, {:open, neg_domains}} -> + # Every present domain key type in the negative domains is at least term_or_optional() + Enum.all?(neg_domains, fn {{:domain_key, _}, type} -> + subtype?(term_or_optional(), type) + end) + + # Open map with closed negative domains + {:open, {:closed, neg_domains}} -> + # The domains must include all possible domain key types, and they must be at least term_or_optional() + Enum.all?(@domain_key_types, fn domain_key -> + case Map.get(neg_domains, {:domain_key, domain_key}) do + # Not all domain keys are present + nil -> false + type -> subtype?(term_or_optional(), type) + end + end) + + # Closed positive domains with closed negative tag + {{:closed, pos_domains}, :closed} -> + # Every present domain key type is a subtype of not_set() + Enum.all?(pos_domains, fn {{:domain_key, _}, type} -> + subtype?(type, not_set()) + end) + + # Open positive domains with closed negative tag + {{:open, pos_domains}, :closed} -> + # The pos_domains must include all possible domain key types, and they must be subtypes of not_set() + Enum.all?(@domain_key_types, fn domain_key -> + case Map.get(pos_domains, {:domain_key, domain_key}) do + # Not all domain keys are present + nil -> false + type -> subtype?(type, not_set()) + end + end) + end end defp map_pop_key(tag, fields, key) do @@ -1846,6 +2339,19 @@ defmodule Module.Types.Descr do end end + # Pop a domain type, e.g. popping integers from %{integer() => if_set(binary())} + # returns {if_set(integer()), %{integer() => if_set(binary())}} + # If the domain is not present, use the tag to type as default. + defp map_pop_domain({tag, domains}, fields, domain_key) do + case :maps.take({:domain_key, domain_key}, domains) do + {value, domains} -> {value, %{map: map_new(tag, fields, domains)}} + :error -> {tag_to_type(tag), %{map: map_new(tag, fields, domains)}} + end + end + + defp map_pop_domain(tag, fields, _domain_key), + do: {tag_to_type(tag), %{map: map_new(tag, fields)}} + defp map_split_negative(negs, key) do Enum.reduce_while(negs, [], fn # A negation with an open map means the whole thing is empty. @@ -1854,6 +2360,13 @@ defmodule Module.Types.Descr do end) end + defp map_split_negative_domain(negs, domain_key) do + Enum.reduce_while(negs, [], fn + {:open, fields}, _acc when map_size(fields) == 0 -> {:halt, :empty} + {tag, fields}, neg_acc -> {:cont, [map_pop_domain(tag, fields, domain_key) | neg_acc]} + end) + end + # Use heuristics to normalize a map dnf for pretty printing. defp map_normalize(dnfs) do for dnf <- dnfs, not map_empty?([dnf]) do @@ -1972,11 +2485,43 @@ defmodule Module.Types.Descr do {:map, [], []} end + def map_literal_to_quoted({{:closed, domains}, fields}, _opts) + when map_size(domains) == 0 and map_size(fields) == 0 do + {:empty_map, [], []} + end + + def map_literal_to_quoted({{:open, domains}, fields}, _opts) + when map_size(domains) == 0 and map_size(fields) == 0 do + {:map, [], []} + end + def map_literal_to_quoted({:open, %{__struct__: @not_atom_or_optional} = fields}, _opts) when map_size(fields) == 1 do {:non_struct_map, [], []} end + def map_literal_to_quoted({{:closed, domains}, fields}, opts) do + domain_fields = + for {{:domain_key, domain_type}, value_type} <- domains do + key = {:string, [], ["#{domain_type}() => "]} + {key, to_quoted(value_type, opts)} + end + + regular_fields_quoted = map_fields_to_quoted(:closed, Enum.sort(fields), opts) + {:%{}, [], domain_fields ++ regular_fields_quoted} + end + + def map_literal_to_quoted({{:open, domains}, fields}, opts) do + domain_fields = + for {{:domain_key, domain_type}, value_type} <- domains do + key = {:string, [], ["#{domain_type}() => "]} + {key, to_quoted(value_type, opts)} + end + + regular_fields_quoted = map_fields_to_quoted(:open, Enum.sort(fields), opts) + {:%{}, [], [{:..., [], nil}] ++ domain_fields ++ regular_fields_quoted} + end + def map_literal_to_quoted({tag, fields}, opts) do case tag do :closed -> @@ -2976,4 +3521,32 @@ defmodule Module.Types.Descr do defp non_empty_map_or([head | tail], fun) do Enum.reduce(tail, fun.(head), &{:or, [], [&2, fun.(&1)]}) end + + # Helpers for domain key validation + defp split_domain_key_pairs(pairs) do + Enum.split_with(pairs, fn + {{:domain_key, _}, _} -> false + _ -> true + end) + end + + defp validate_domain_keys(pairs) do + # Check if domain keys are valid and don't overlap + domains = Enum.map(pairs, fn {{:domain_key, domain}, _} -> domain end) + + if length(domains) != length(Enum.uniq(domains)) do + raise ArgumentError, "Domain key types should not overlap" + end + + # Check that all domain keys are valid + invalid_domains = Enum.reject(domains, &(&1 in @domain_key_types)) + + if invalid_domains != [] do + raise ArgumentError, + "Invalid domain key types: #{inspect(invalid_domains)}. " <> + "Valid types are: #{inspect(@domain_key_types)}" + end + + Enum.map(pairs, fn {key, type} -> {key, if_set(type)} end) + end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 90285845a75..d68bd81e7cb 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -97,8 +97,28 @@ defmodule Module.Types.DescrTest do a_integer_open = open_map(a: integer()) assert equal?(union(closed_map(a: integer()), a_integer_open), a_integer_open) - assert difference(open_map(a: integer()), closed_map(b: boolean())) - |> equal?(open_map(a: integer())) + # Domain key types + atom_to_atom = open_map([{{:domain_key, :atom}, atom()}]) + atom_to_integer = open_map([{{:domain_key, :atom}, integer()}]) + + # Test union identity and different type maps + assert union(atom_to_atom, atom_to_atom) == atom_to_atom + + # Test subtype relationships with domain key maps + refute open_map([{{:domain_key, :atom}, union(atom(), integer())}]) + |> subtype?(union(atom_to_atom, atom_to_integer)) + + assert union(atom_to_atom, atom_to_integer) + |> subtype?(open_map([{{:domain_key, :atom}, union(atom(), integer())}])) + + # Test unions with empty and open maps + assert union(empty_map(), open_map([{{:domain_key, :integer}, atom()}])) + |> equal?(open_map([{{:domain_key, :integer}, atom()}])) + + assert union(open_map(), open_map([{{:domain_key, :integer}, atom()}])) == open_map() + + # Test union of open map and map with domain key + assert union(open_map(), open_map([{{:domain_key, :integer}, atom()}])) == open_map() end test "list" do @@ -325,6 +345,51 @@ defmodule Module.Types.DescrTest do # Intersection with proper list (should result in empty list) assert intersection(list(integer(), atom()), list(integer())) == empty_list() end + + test "intersection with domain key types" do + # %{..., int => t1, atom => t2} and %{int => t3} + # intersection is %{int => t1 and t3, atom => none} + map1 = open_map([{{:domain_key, :integer}, integer()}, {{:domain_key, :atom}, atom()}]) + map2 = closed_map([{{:domain_key, :integer}, number()}]) + + intersection = intersection(map1, map2) + + expected = + closed_map([{{:domain_key, :integer}, integer()}, {{:domain_key, :atom}, none()}]) + + assert equal?(intersection, expected) + + # %{..., int => t1, atom => t2} and %{int => t3, pid => t4} + # intersection is %{int =>t1 and t3, atom => none, pid => t4} + map1 = open_map([{{:domain_key, :integer}, integer()}, {{:domain_key, :atom}, atom()}]) + map2 = closed_map([{{:domain_key, :integer}, float()}, {{:domain_key, :pid}, binary()}]) + + intersection = intersection(map1, map2) + + expected = + closed_map([ + {{:domain_key, :integer}, intersection(integer(), float())}, + {{:domain_key, :atom}, none()}, + {{:domain_key, :pid}, binary()} + ]) + + assert equal?(intersection, expected) + + # %{..., int => t1, string => t3} and %{int => t4} + # intersection is %{int => t1 and t4, string => none} + map1 = open_map([{{:domain_key, :integer}, integer()}, {{:domain_key, :binary}, binary()}]) + map2 = closed_map([{{:domain_key, :integer}, float()}]) + + intersection = intersection(map1, map2) + + assert equal?( + intersection, + closed_map([ + {{:domain_key, :integer}, intersection(integer(), float())}, + {{:domain_key, :binary}, none()} + ]) + ) + end end describe "difference" do @@ -433,6 +498,56 @@ defmodule Module.Types.DescrTest do |> equal?(open_map(a: atom())) refute empty?(difference(open_map(), empty_map())) + + assert difference(open_map(a: integer()), closed_map(b: boolean())) + |> equal?(open_map(a: integer())) + end + + test "map with domain key types" do + # Non-overlapping domain keys + t1 = closed_map([{{:domain_key, :integer}, atom()}]) + t2 = closed_map([{{:domain_key, :atom}, binary()}]) + assert equal?(difference(t1, t2) |> union(empty_map()), t1) + assert empty?(difference(t1, t1)) + + # %{atom() => t1} and not %{atom() => t2} is not %{atom() => t1 and not t2} + t3 = closed_map([{{:domain_key, :integer}, atom()}]) + t4 = closed_map([{{:domain_key, :integer}, atom([:ok])}]) + assert subtype?(difference(t3, t4), t3) + + refute difference(t3, t4) + |> equal?(closed_map([{{:domain_key, :integer}, difference(atom(), atom([:ok]))}])) + + # Difference with a non-domain key map + t5 = closed_map([{{:domain_key, :integer}, union(atom(), integer())}]) + t6 = closed_map(a: atom()) + assert equal?(difference(t5, t6), t5) + + # Removing atom keys from a map with defined atom keys + a_number = closed_map(a: number()) + a_number_and_pids = closed_map([{:a, number()}, {{:domain_key, :atom}, pid()}]) + atom_to_float = closed_map([{{:domain_key, :atom}, float()}]) + atom_to_term = closed_map([{{:domain_key, :atom}, term()}]) + atom_to_pid = closed_map([{{:domain_key, :atom}, pid()}]) + t_diff = difference(a_number, atom_to_float) + + # Removing atom keys that map to float, make the :a key point to integer only. + assert map_fetch(t_diff, :a) == {false, integer()} + # %{a => number, atom => pid} and not %{atom => float} gives numbers on :a + assert map_fetch(difference(a_number_and_pids, atom_to_float), :a) == {false, number()} + + assert map_fetch(t_diff, :foo) == :badkey + + assert subtype?(a_number, atom_to_term) + refute subtype?(a_number, atom_to_float) + + # Removing all atom keys from map %{:a => type} means there is nothing left. + assert empty?(difference(a_number, atom_to_term)) + refute empty?(intersection(atom_to_term, a_number)) + assert empty?(intersection(atom_to_pid, a_number)) + + # (%{:a => number} and not %{:a => float}) is %{:a => integer} + assert equal?(difference(a_number, atom_to_float), closed_map(a: integer())) end defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) @@ -521,6 +636,18 @@ defmodule Module.Types.DescrTest do assert dynamic(open_map(a: union(integer(), binary()))) == open_map(a: dynamic(integer()) |> union(binary())) + + # For domains too + t1 = dynamic(open_map([{{:domain_key, :integer}, integer()}])) + t2 = open_map([{{:domain_key, :integer}, dynamic(integer())}]) + + assert dynamic(open_map([{{:domain_key, :integer}, integer()}])) == + open_map([{{:domain_key, :integer}, dynamic(integer())}]) + + # if_set on dynamic fields also must work + t1 = dynamic(open_map(a: if_set(integer()))) + t2 = open_map(a: if_set(dynamic(integer()))) + assert t1 == t2 end end @@ -657,6 +784,159 @@ defmodule Module.Types.DescrTest do end end + describe "domain key types" do + # for intersection + test "map domain key types" do + assert subtype?(empty_map(), closed_map([{{:domain_key, :integer}, atom()}])) + + t1 = closed_map([{{:domain_key, :integer}, atom()}]) + t2 = closed_map([{{:domain_key, :integer}, binary()}]) + + assert equal?(intersection(t1, t2), empty_map()) + + t1 = closed_map([{{:domain_key, :integer}, atom()}]) + t2 = closed_map([{{:domain_key, :atom}, term()}]) + + # their intersection is the empty map + refute empty?(intersection(t1, t2)) + assert equal?(intersection(t1, t2), empty_map()) + end + + test "basic map with domain key type and fetch" do + integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) + assert map_fetch(integer_to_atom, :foo) == :badkey + + # the key :a is for sure of type pid and exists in type + # %{atom() => pid()} and not %{:a => not_set()} + t1 = closed_map([{{:domain_key, :atom}, pid()}]) + t2 = closed_map(a: not_set()) + t3 = open_map(a: not_set()) + + # Indeed, t2 is equivalent to the empty map + assert map_fetch(difference(t1, t2), :a) == :badkey + assert map_fetch(difference(t1, t3), :a) == {false, pid()} + + t4 = closed_map([{{:domain_key, :pid}, atom()}]) + assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()} + + assert map_fetch(closed_map([{{:domain_key, :atom}, pid()}]), :a) == :badkey + + assert map_fetch(dynamic(closed_map([{{:domain_key, :atom}, pid()}])), :a) == + {true, dynamic(pid())} + + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(open_map(a: if_set(integer()))) + |> map_fetch(:a) == {false, float()} + + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(closed_map(b: if_set(integer()))) + |> map_fetch(:a) == :badkey + end + + test "map get" do + map_type = closed_map([{{:domain_key, :tuple}, binary()}]) + assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())} + # assert map_fetch(map_type, :b) == :badkey + + # Type with all domain types + # %{:bar => :ok, integer() => :int, float() => :float, atom() => binary(), binary() => integer(), tuple() => float(), map() => pid(), reference() => port(), pid() => boolean()} + all_domains = + closed_map([ + {:bar, atom([:ok])}, + {{:domain_key, :integer}, atom([:int])}, + {{:domain_key, :float}, atom([:float])}, + {{:domain_key, :atom}, binary()}, + {{:domain_key, :binary}, integer()}, + {{:domain_key, :tuple}, float()}, + {{:domain_key, :map}, pid()}, + {{:domain_key, :reference}, port()}, + {{:domain_key, :pid}, reference()}, + {{:domain_key, :port}, boolean()} + ]) + + # TODO + assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])} + + assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()} + assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()} + assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])} + # # This fails but should work imo + # # TODO + assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()} + assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()} + assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())} + assert map_get(all_domains, empty_map()) == {:ok, pid() |> nil_or_type()} + + # Union + assert map_get(all_domains, union(tuple(), empty_map())) == + {:ok, union(float(), pid() |> nil_or_type())} + + # Removing all maps with tuple keys + t_no_tuple = difference(all_domains, closed_map([{{:domain_key, :tuple}, float()}])) + t_really_no_tuple = difference(all_domains, open_map([{{:domain_key, :tuple}, float()}])) + assert subtype?(all_domains, open_map()) + # It's only closed maps, so it should not change + assert map_get(t_no_tuple, tuple()) == {:ok, float() |> nil_or_type()} + # This time we actually removed all tuple to float keys + assert map_get(t_really_no_tuple, tuple()) == {:ok_absent, atom([nil])} + + t1 = closed_map([{{:domain_key, :tuple}, integer()}]) + t2 = closed_map([{{:domain_key, :tuple}, float()}]) + t3 = union(t1, t2) + assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()} + end + + test "more complex map get over atoms" do + map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}]) + assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])} + assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())} + assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()} + + assert map_get(map, atom() |> difference(atom([:a]))) == + {:ok, union(atom([:b]), pid() |> nil_or_type())} + end + + test "subtyping with domain key types" do + t1 = closed_map([{{:domain_key, :integer}, number()}]) + t2 = closed_map([{{:domain_key, :integer}, integer()}]) + + assert subtype?(t2, t1) + + t1_minus_t2 = difference(t1, t2) + refute empty?(t1_minus_t2) + + assert subtype?(open_map_with_default(number()), open_map()) + t = difference(open_map(), open_map_with_default(number())) + refute empty?(t) + refute subtype?(open_map(), open_map_with_default(number())) + assert subtype?(open_map_with_default(integer()), open_map_with_default(number())) + refute subtype?(open_map_with_default(float()), open_map_with_default(atom())) + + assert equal?( + intersection(open_map_with_default(number()), open_map_with_default(float())), + open_map_with_default(float()) + ) + end + + # for operator t\[t'] + test "map delete" do + t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) + + assert map_delete(t1, atom([:a])) + |> equal?(closed_map([{:a, not_set()}, {{:domain_key, :integer}, number()}])) + + assert map_delete(t1, atom([:a, :b])) + |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) + + assert map_delete(t1, term()) + |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) + end + + test "map update" do + assert false + end + end + describe "projections" do test "fun_fetch" do assert fun_fetch(term(), 1) == :error @@ -1097,6 +1377,11 @@ defmodule Module.Types.DescrTest do {:ok, type} = map_delete(difference(open_map(), open_map(a: not_set())), :a) assert equal?(type, open_map(a: not_set())) + + ## Delete from maps with domain + assert closed_map([{:a, integer()}, {:b, atom()}, {{:domain_key, :atom}, pid()}]) + |> map_delete(:a) == + {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {{:domain_key, :atom}, pid()}])} end test "map_take" do From 40c52121393f0c91e2bc80f6bce913f1fe14c0be Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 9 May 2025 11:01:09 +0200 Subject: [PATCH 2/6] Refactor domain key types handling and improve map_get functionality --- lib/elixir/lib/module/types/descr.ex | 200 ++++++++++-------- .../test/elixir/module/types/descr_test.exs | 44 ++-- 2 files changed, 144 insertions(+), 100 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 6228100f70e..6588ad4cd44 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -27,7 +27,6 @@ defmodule Module.Types.Descr do @bit_top (1 <<< 8) - 1 @bit_number @bit_integer ||| @bit_float - # Domain key types @domain_key_types [ :binary, :empty_list, @@ -820,6 +819,7 @@ defmodule Module.Types.Descr do end end + defp atom_only?(:term), do: false defp atom_only?(descr), do: empty?(Map.delete(descr, :atom)) defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)} @@ -1329,6 +1329,17 @@ defmodule Module.Types.Descr do # is the union of `%{..., a: atom(), b: if_set(not integer())}` and # `%{..., a: if_set(not atom()), b: integer()}`. For maps with more keys, # each key in a negated literal may create a new union when eliminated. + # + # Instead of a tag :open or :closed, we can also use a pair {tag, domains} which + # specifies for each defined key domain (@domain_key_types) the type associated with + # those keys. + # + # For instance, the type `%{atom() => integer()}` is the type of maps where atom keys + # map to integers, without any non-atom keys. It is represented using the tag-domain pair + # {{:closed, %{atom: integer()}}, %{}, []}, with no defined keys or negations. + # + # The type %{..., atom() => integer()} represents maps with atom keys bound to integers, + # and other keys bound to any type, represented by {{:closed, %{atom: integer()}}, %{}, []}. defp map_descr(tag, fields) do case map_descr_pairs(fields, [], false) do @@ -1345,11 +1356,7 @@ defmodule Module.Types.Descr do {domains, domains_dynamic?} = map_descr_pairs(domains, [], false) fields_map = :maps.from_list(if fields_dynamic?, do: Enum.reverse(fields), else: fields) - - domains_map = - :maps.from_list(if domains_dynamic?, do: Enum.reverse(domains), else: domains) - - # |> dbg() + domains_map = :maps.from_list(if domains_dynamic?, do: Enum.reverse(domains), else: domains) if fields_dynamic? or domains_dynamic? do %{dynamic: %{map: map_new(tag, fields_map, domains_map)}} @@ -1375,12 +1382,8 @@ defmodule Module.Types.Descr do defp tag_to_type(:open), do: term_or_optional() defp tag_to_type(:closed), do: not_set() - - defp tag_to_type({:open, domains}), - do: Map.get(domains, {:domain_key, :atom}, term_or_optional()) |> if_set() - - defp tag_to_type({:closed, domains}), - do: Map.get(domains, {:domain_key, :atom}, not_set()) |> if_set() + defp tag_to_type({:closed, domain}), do: Map.get(domain, {:domain_key, :atom}, not_set()) + defp tag_to_type({:open, domain}), do: Map.get(domain, {:domain_key, :atom}, term_or_optional()) defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :optional) @@ -1562,7 +1565,7 @@ defmodule Module.Types.Descr do :maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed) end - # Both arguments are tags with domains + # At least one tag is a tag-domain pair. defp map_literal_intersection(tag1, map1, tag2, map2) do # For a closed map with domains intersected with an open map with domains: # 1. The result is closed (more restrictive) @@ -1577,8 +1580,8 @@ defmodule Module.Types.Descr do # 1. If key is in both maps, compute non empty intersection (:error if it is none) # 2. If key is only in map1, compute non empty intersection with atom2 # 3. If key is only in map2, compute non empty intersection with atom1 - # Can be considered an intersection with default values where I iterate on all - # key labels in both map1 and map2. + # We do that by computing intersection on all key labels in both map1 and map2, + # using default values when a key is not present. keys1_set = :sets.from_list(Map.keys(map1), version: 2) keys2_set = :sets.from_list(Map.keys(map2), version: 2) @@ -1602,6 +1605,7 @@ defmodule Module.Types.Descr do {tag, new_fields} end + # Compute the intersection of two tags or tag-domain pairs. defp map_domain_intersection(:closed, _), do: :closed defp map_domain_intersection(_, :closed), do: :closed defp map_domain_intersection(:open, tag), do: tag @@ -1792,6 +1796,8 @@ defmodule Module.Types.Descr do @doc """ Puts a `key` of a given type, assuming that the descr is exclusively a map (or dynamic). + + The key may be an atom, or a key type. """ def map_put(:term, _key, _type), do: :badmap def map_put(descr, key, :term) when is_atom(key), do: map_put_shared(descr, key, :term) @@ -1803,6 +1809,18 @@ defmodule Module.Types.Descr do end end + def map_put(descr, key_descr = %{}, type) do + case atom_fetch(key_descr) do + {:finite, [single_key]} -> + map_put(descr, single_key, type) + + # In this case, we iterate on key_descr to add type to each key type it covers. + _ -> + # TODO: handle general case + raise("TODO") + end + end + defp map_put_shared(descr, key, type) do with {nil, descr} <- map_take(descr, key, nil, &map_put_static(&1, key, type)) do {:ok, descr} @@ -1824,30 +1842,6 @@ defmodule Module.Types.Descr do defp map_put_static(descr, _key, _type), do: descr - @doc """ - Removes a key from a given type from a map type. - """ - # defp map_delete_static(descr, :term), do: raise(:todo) - - # def map_delete_static(descr, key = %{}) do - # # 1 this only is useful for atom types. the others are already optional - # case key do - # %{atom: atoms} -> - # case atoms do - # {:union, set} -> map_ - # end - # end - # end - - # Make a key optional in a map type. - defp map_make_optional_static(descr, key) do - # We pass nil as the initial value so we can avoid computing the unions. - with {nil, descr} <- - map_take(descr, key, nil, &union(&1, open_map([]))) do - {:ok, descr} - end - end - @doc """ Removes a key from a map type. """ @@ -1864,8 +1858,8 @@ defmodule Module.Types.Descr do This generalizes `map_fetch/2` (which operates on a single literal key) to work with a key type (e.g., `atom()`, `integer()`, `:a or :b`). It's based - on the map-selection operator t.[t'] described in "Types for Tables" - (Castagna et al., ICFP 2023). + on the map-selection operator t.[t'] described in Section 4.2 of "Typing Records, + Maps, and Structs" (Castagna et al., ICFP 2023). ## Return Values @@ -1885,6 +1879,7 @@ defmodule Module.Types.Descr do issue a warning, as this often implies selecting a field that is effectively undefined. + # TODO: implement/decide if worth it (it's from the paper) * `{:ok_spillover, type}`: Success, and `type` is the resulting union. However, this indicates that the `key_type` included keys not explicitly covered by the `map_type`'s fields or domain specifications. The @@ -1899,34 +1894,40 @@ defmodule Module.Types.Descr do * `:badkeytype`: The input `key_type` was invalid (e.g., not a subtype of the allowed key types like `atom()`, `integer()`, etc.). """ - def map_get(descr, key_descr) do + def map_get(:term, _key_descr), do: :badmap + + def map_get(%{} = descr, key_descr) do case :maps.take(:dynamic, descr) do :error -> - case :maps.take(:dynamic, key_descr) do - :error -> - type_selected = map_get_static(descr, key_descr) - {optional?, type_selected} = pop_optional_static(type_selected) - - cond do - empty?(type_selected) -> {:ok_absent, atom([nil])} - optional? -> {:ok, type_selected |> nil_or_type()} - true -> {:ok_present, type_selected} - end + if descr_key?(descr, :map) and map_only?(descr) do + {optional?, type_selected} = map_get_static(descr, key_descr) |> pop_optional_static() - {dynamic, static} -> - map_get_static(dynamic, key_descr) - |> union(dynamic(map_get_static(static, key_descr))) + cond do + empty?(type_selected) -> {:ok_absent, atom([nil])} + optional? -> {:ok, nil_or_type(type_selected)} + true -> {:ok_present, type_selected} + end + else + :badmap end {dynamic, static} -> - case :maps.take(:dynamic, key_descr) do - :error -> - map_get_static(dynamic, key_descr) - |> union(dynamic(map_get_static(static, key_descr))) + if descr_key?(dynamic, :map) and map_only?(static) do + {optional_dynamic?, dynamic_type} = + map_get_static(dynamic, key_descr) |> pop_optional_static() + + {optional_static?, static_type} = + map_get_static(static, key_descr) |> pop_optional_static() + + type_selected = union(dynamic(dynamic_type), static_type) - {dynamic_key, static_key} -> - map_get_static(dynamic, dynamic_key) - |> union(dynamic(map_get_static(static, static_key))) + cond do + empty?(type_selected) -> {:ok_absent, atom([nil])} + optional_dynamic? or optional_static? -> {:ok, nil_or_type(type_selected)} + true -> {:ok_present, type_selected} + end + else + :badmap end end end @@ -1966,15 +1967,13 @@ defmodule Module.Types.Descr do map_get_static(%{map: [{{tag, %{}}, fields, []}]}, key_descr) end - # TODO: handle impact from explicit keys (like, having a: integer() when - # selecting on atom() keys. def map_get_static(%{map: [{{tag, domains}, fields, []}]}, key_descr) do # For each non-empty kind of type in the key_descr, we add the corresponding key domain in a union. key_descr |> covered_key_types() |> Enum.reduce(none(), fn {:atom, atom_type}, acc -> - map_get_single_atom([{{tag, domains}, fields, []}], atom_type) |> union(acc) + map_get_atom([{{tag, domains}, fields, []}], atom_type) |> union(acc) key_type, acc -> # Note: we could stop if we reach term()_or_optional() @@ -1982,33 +1981,40 @@ defmodule Module.Types.Descr do end) end - # TODO: handle the atom type in key_descr - # - do the atom singletons [at1, at2, ...] - # -> can just do map_fetch_key maybe? - # - what to do for the negation? not (a1 or a2 or ...) def map_get_static(%{map: dnf}, key_descr) do key_descr |> covered_key_types() |> Enum.reduce(none(), fn {:atom, atom_type}, acc -> - map_get_single_atom(dnf, atom_type) |> union(acc) + map_get_atom(dnf, atom_type) |> union(acc) key_type, acc -> - map_get_single_domain(dnf, key_type) |> union(acc) + map_get_domain(dnf, key_type) |> union(acc) end) end - # Take a map dnf and return the union of types when selecting atoms. - # This includes cases: - # - union of atoms {a1, a2, ...}, in which case the defined ones are selected as well. If all of those are certainly defined, then the result does not contain nil. Otherwise, it spills over the atom domain. - # - a negation of atoms not {a1, a2, ...}, in which case we just take care not to include - # the negated atoms in the result. - def map_get_single_atom(dnf, atom_type) do + def map_get_static(%{}, _key), do: not_set() + def map_get_static(:term, _key), do: term_or_optional() + + # Given a map dnf return the union of types for a given atom type. Handles two cases: + # 1. A union of atoms (e.g., `{:union, atoms}`): + # - Iterates through each atom in the union. + # - Fetches the type for each atom and combines them into a union. + # + # 2. A negation of atoms (e.g., `{:negation, atoms}`): + # - Fetches all possible keys in the map's DNF. + # - Excludes the negated atoms from the considered keys. + # - Includes the domain of all atoms in the map's DNF. + # + # Example: + # Fetching a key of type `atom() and not (:a)` from a map of type + # `%{a: atom(), b: float(), atom() => pid()}` + # would return either `nil` or `float()` (key `:b`) or `pid()` (key `atom()`), but not `atom()` (key `:a`). + def map_get_atom(dnf, atom_type) do case atom_type do {:union, atoms} -> - atoms = :sets.to_list(atoms) - atoms + |> :sets.to_list() |> Enum.reduce(none(), fn atom, acc -> {static_optional?, type} = map_fetch_static(%{map: dnf}, atom) @@ -2020,15 +2026,43 @@ defmodule Module.Types.Descr do end) {:negation, atoms} -> - atoms = :sets.to_list(atoms) + # 1) Fetch all the possible keys in the dnf + # 2) Get them all, except the ones in neg_atoms + possible_keys = map_fetch_all_key_names(dnf) + considered_keys = :sets.subtract(possible_keys, atoms) + + considered_keys + |> :sets.to_list() + |> Enum.reduce(none(), fn atom, acc -> + {static_optional?, type} = map_fetch_static(%{map: dnf}, atom) - # TODO: do the "don't take this set of atoms" things - map_get_single_domain(dnf, :atom) + if static_optional? do + union(type, acc) |> nil_or_type() |> if_set() + else + union(type, acc) + end + end) + |> union(map_get_domain(dnf, :atom)) end end + # Fetch all present keys in a map dnf (including negated ones). + defp map_fetch_all_key_names(dnf) do + dnf + |> Enum.reduce(:sets.new(version: 2), fn {_tag, fields, negs}, acc -> + keys = :sets.from_list(Map.keys(fields)) + + # Add all the negative keys + # Example: %{...} and not %{a: not_set()} makes key :a present in the map + Enum.reduce(negs, keys, fn {_tag, neg_fields}, acc -> + :sets.from_list(Map.keys(neg_fields)) |> :sets.union(acc) + end) + |> :sets.union(acc) + end) + end + # Take a map dnf and return the union of types for the given key domain. - def map_get_single_domain(dnf, key_domain) when is_atom(key_domain) do + def map_get_domain(dnf, key_domain) when is_atom(key_domain) do dnf |> Enum.reduce(none(), fn {tag, _fields, []}, acc when is_atom(tag) -> diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index d68bd81e7cb..429efd6ce85 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -640,9 +640,7 @@ defmodule Module.Types.DescrTest do # For domains too t1 = dynamic(open_map([{{:domain_key, :integer}, integer()}])) t2 = open_map([{{:domain_key, :integer}, dynamic(integer())}]) - - assert dynamic(open_map([{{:domain_key, :integer}, integer()}])) == - open_map([{{:domain_key, :integer}, dynamic(integer())}]) + assert t1 == t2 # if_set on dynamic fields also must work t1 = dynamic(open_map(a: if_set(integer()))) @@ -834,6 +832,8 @@ defmodule Module.Types.DescrTest do end test "map get" do + assert map_get(term(), term()) == :badmap + map_type = closed_map([{{:domain_key, :tuple}, binary()}]) assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())} # assert map_fetch(map_type, :b) == :badkey @@ -886,6 +886,11 @@ defmodule Module.Types.DescrTest do assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()} end + test "map get with dynamic" do + {_answer, type_selected} = map_get(dynamic(), term()) + assert equal?(type_selected, dynamic() |> nil_or_type()) + end + test "more complex map get over atoms" do map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}]) assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])} @@ -918,23 +923,24 @@ defmodule Module.Types.DescrTest do ) end - # for operator t\[t'] - test "map delete" do - t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) + # TODO: operator t\[t'] + # test "map delete" do + # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) - assert map_delete(t1, atom([:a])) - |> equal?(closed_map([{:a, not_set()}, {{:domain_key, :integer}, number()}])) + # assert map_delete(t1, atom([:a])) + # |> equal?(closed_map([{:a, not_set()}, {{:domain_key, :integer}, number()}])) - assert map_delete(t1, atom([:a, :b])) - |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) + # assert map_delete(t1, atom([:a, :b])) + # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) - assert map_delete(t1, term()) - |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) - end + # assert map_delete(t1, term()) + # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) + # end - test "map update" do - assert false - end + # TODO + # test "map update" do + # assert false + # end end describe "projections" do @@ -1316,7 +1322,6 @@ 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 @@ -1500,6 +1505,11 @@ defmodule Module.Types.DescrTest do {false, type} = map_fetch(map, :a) assert equal?(type, atom()) end + + test "map put with key type" do + # Using a literal key or an expression of that singleton key is the same + assert map_put(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} + end end describe "disjoint" do From f571b9dbb135e3593a56214ee2c193f59ea9b52a Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 9 May 2025 15:33:46 +0200 Subject: [PATCH 3/6] Refactor map functions to improve clarity and consistency; rename open_map_with_default to map_with_default and implement map_update functionality --- lib/elixir/lib/module/types/descr.ex | 157 +++++++++++++++++- .../test/elixir/module/types/descr_test.exs | 73 ++++++-- 2 files changed, 208 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 6588ad4cd44..fd40721abe3 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -103,10 +103,10 @@ defmodule Module.Types.Descr do def open_map(), do: %{map: @map_top} - @doc "An open map with a default type %{term() => default}" - def open_map_with_default(default) do + @doc "A map (closed or open is the same) with a default type %{term() => default}" + def map_with_default(default) do map_descr( - :open, + :closed, [], Enum.map(@domain_key_types, fn key_type -> {{:domain_key, key_type}, if_set(default)} @@ -1809,15 +1809,158 @@ defmodule Module.Types.Descr do end end - def map_put(descr, key_descr = %{}, type) do + # Map.put but because we are inserting in a key type, we use refresh (keep the previous type) + def map_update(:term, _key, _type), do: :badmap + + def map_update(descr, key_descr, type) do + {dynamic_descr, static_descr} = Map.pop(descr, :dynamic) + key_descr = unfold(key_descr) + type = unfold(type) + + cond do + # Either 1) static part is a map, or 2) static part is empty and dynamic part contains maps + not map_only?(static_descr) -> + :badmap + + empty?(static_descr) and not (not is_nil(dynamic_descr) and descr_key?(dynamic_descr, :map)) -> + :badmap + + # Either of those three types could be dynamic. + not (not is_nil(dynamic_descr) or Map.has_key?(key_descr, :dynamic) or + Map.has_key?(type, :dynamic)) -> + map_update_static(descr, key_descr, type) + + true -> + # If one of those is dynamic, we just compute the union + {descr_dynamic, descr_static} = Map.pop(descr, :dynamic, descr) + {key_dynamic, key_static} = Map.pop(key_descr, :dynamic, key_descr) + {type_dynamic, type_static} = Map.pop(type, :dynamic, type) + + with {:ok, new_static} <- map_update_static(descr_static, key_static, type_static), + {:ok, new_dynamic} <- map_update_static(descr_dynamic, key_dynamic, type_dynamic) do + {:ok, union(new_static, dynamic(new_dynamic))} + end + end + end + + def map_update_static(%{map: _} = descr, key_descr = %{}, type) do + # Check if descr is a valid map, case atom_fetch(key_descr) do + # If the key_descr is a singleton, we directly put the type into the map. {:finite, [single_key]} -> map_put(descr, single_key, type) # In this case, we iterate on key_descr to add type to each key type it covers. + # Since we do not know which key will be used, we do the union with previous types. _ -> - # TODO: handle general case - raise("TODO") + new_descr = + key_descr + |> covered_key_types() + |> Enum.reduce(descr, fn + {:atom, atom_key}, acc -> + map_put_atom(acc, atom_key, type) + + key, acc -> + map_put_domain(acc, key, type) + end) + + {:ok, new_descr} + end + end + + def map_update_static(:term, _key_descr, _type), do: {:ok, open_map()} + def map_update_static(_, _, _), do: {:ok, none()} + + @doc """ + Updates a key in a map type by fetching its current type, unioning it with a + `new_additional_type`, and then putting the resulting union type back. + + Returns: + - `{:ok, new_map_descr}`: If successful. + - `:badmap`: If the input `descr` is not a valid map type. + - `:badkey`: If the key is considered invalid during the take operation (e.g., + an optional key that resolves to an empty type). + """ + def map_refresh_key(descr, key, new_additional_type) when is_atom(key) do + case map_fetch(descr, key) do + :badmap -> + :badmap + + # Key is not present: we just add the new one and make it optional. + :badkey -> + with {:ok, descr} <- map_put(descr, key, if_set(new_additional_type)) do + descr + end + + {_optional?, current_key_type} -> + type_to_put = union(current_key_type, new_additional_type) + + case map_fetch_and_put(descr, key, type_to_put) do + {_taken_type, new_map_descr} -> new_map_descr + # Propagates :badmap or :badkey from map_fetch_and_put + error -> error + end + end + end + + def map_put_domain(%{map: [{tag, fields, []}]}, domain, type) do + %{map: [{map_update_domain(tag, domain, type), fields, []}]} + end + + def map_put_domain(%{map: dnf}, domain, type) do + Enum.map(dnf, fn + {tag, fields, []} -> + {map_update_domain(tag, domain, type), fields, []} + + {tag, fields, negs} -> + # For negations, we count on the idea that a negation will not remove any + # type from a domain unless it completely cancels out the type. + # So for any non-empty map dnf, we just update the domain with the new type, + # as well as its negations to keep them accurate. + {map_update_domain(tag, domain, type), fields, + Enum.map(negs, fn {neg_tag, neg_fields} -> + {map_update_domain(neg_tag, domain, type), neg_fields} + end)} + end) + end + + def map_put_atom(descr = %{map: dnf}, atom_key, type) do + case atom_key do + {:union, keys} -> + keys + |> :sets.to_list() + |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end) + + {:negation, keys} -> + # 1) Fetch all the possible keys in the dnf + # 2) Get them all, except the ones in neg_atoms + possible_keys = map_fetch_all_key_names(dnf) + considered_keys = :sets.subtract(possible_keys, keys) + + considered_keys + |> :sets.to_list() + |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end) + |> map_put_domain(:atom, type) + end + end + + def map_update_domain(tag, domain, type) do + case tag do + :open -> + :open + + :closed -> + {:closed, %{{:domain_key, domain} => if_set(type)}} + + {:open, domains} -> + if Map.has_key?(domains, {:domain_key, domain}) do + {:open, Map.update!(domains, {:domain_key, domain}, &union(&1, type))} + else + {:open, domains} + end + + {:closed, domains} -> + {:closed, Map.update(domains, {:domain_key, domain}, if_set(type), &union(&1, type))} end end @@ -1935,6 +2078,8 @@ defmodule Module.Types.Descr do # Returns the list of key types that are covered by the key_descr. # E.g., for `{atom([:ok]), term} or integer()` it returns `[:tuple, :integer]`. # We treat bitmap types as a separate key type. + defp covered_key_types(:term), do: @domain_key_types + defp covered_key_types(key_descr) do for {type_kind, type} <- key_descr, reduce: [] do acc -> diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 429efd6ce85..7c4d777fe2d 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -854,14 +854,11 @@ defmodule Module.Types.DescrTest do {{:domain_key, :port}, boolean()} ]) - # TODO assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])} assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()} assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()} assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])} - # # This fails but should work imo - # # TODO assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()} assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()} assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())} @@ -910,16 +907,16 @@ defmodule Module.Types.DescrTest do t1_minus_t2 = difference(t1, t2) refute empty?(t1_minus_t2) - assert subtype?(open_map_with_default(number()), open_map()) - t = difference(open_map(), open_map_with_default(number())) + assert subtype?(map_with_default(number()), open_map()) + t = difference(open_map(), map_with_default(number())) refute empty?(t) - refute subtype?(open_map(), open_map_with_default(number())) - assert subtype?(open_map_with_default(integer()), open_map_with_default(number())) - refute subtype?(open_map_with_default(float()), open_map_with_default(atom())) + refute subtype?(open_map(), map_with_default(number())) + assert subtype?(map_with_default(integer()), map_with_default(number())) + refute subtype?(map_with_default(float()), map_with_default(atom())) assert equal?( - intersection(open_map_with_default(number()), open_map_with_default(float())), - open_map_with_default(float()) + intersection(map_with_default(number()), map_with_default(float())), + map_with_default(float()) ) end @@ -936,11 +933,6 @@ defmodule Module.Types.DescrTest do # assert map_delete(t1, term()) # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) # end - - # TODO - # test "map update" do - # assert false - # end end describe "projections" do @@ -1508,7 +1500,56 @@ defmodule Module.Types.DescrTest do test "map put with key type" do # Using a literal key or an expression of that singleton key is the same - assert map_put(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} + assert map_update(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} + + # Several keys + assert map_update(empty_map(), atom([:a, :b]), integer()) == + {:ok, closed_map(a: if_set(integer()), b: if_set(integer()))} + + assert map_update(empty_map(), integer(), integer()) == + {:ok, closed_map([{{:domain_key, :integer}, integer()}])} + + assert map_update(closed_map([{{:domain_key, :integer}, integer()}]), integer(), float()) == + {:ok, closed_map([{{:domain_key, :integer}, number()}])} + + assert map_update(open_map(), integer(), integer()) == {:ok, open_map()} + + {:ok, type} = map_update(empty_map(), integer(), dynamic()) + assert equal?(type, dynamic(closed_map([{{:domain_key, :integer}, term()}]))) + + # Adding a key of type float to a dynamic only guarantees that we have a map + # as we cannot express "has at least one key of type float => float" + {:ok, type} = map_update(dynamic(), float(), float()) + assert equal?(type, dynamic(open_map())) + + assert closed_map([{{:domain_key, :integer}, integer()}]) + |> difference(open_map()) + |> empty?() + + assert closed_map([{{:domain_key, :integer}, integer()}]) + |> difference(open_map()) + |> map_update(integer(), float()) == :badmap + + assert map_update(empty_map(), number(), float()) == + {:ok, + closed_map([{{:domain_key, :integer}, float()}, {{:domain_key, :float}, float()}])} + + # Tricky cases with atoms: + # We add one atom fields that maps to an integer, which is not :a. So we do not touch + # :a, add integer to :b, and add a domain field. + assert map_update( + closed_map(a: pid(), b: pid()), + atom() |> difference(atom([:a])), + integer() + ) == + {:ok, + closed_map([ + {:a, pid()}, + {:b, union(pid(), integer())}, + {{:domain_key, :atom}, integer()} + ])} + + assert map_update(empty_map(), term(), integer()) == {:ok, map_with_default(integer())} end end From 6bc989e4ec2b8165f0f03a623d1d1d0dc22a0cb5 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 9 May 2025 15:56:44 +0200 Subject: [PATCH 4/6] Refactor code structure for improved readability and maintainability --- lib/elixir/lib/module/types/descr.ex | 45 +- .../test/elixir/module/types/descr_test.exs | 918 +++++++++--------- 2 files changed, 478 insertions(+), 485 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index fd40721abe3..e4ff6007e6d 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1796,8 +1796,6 @@ defmodule Module.Types.Descr do @doc """ Puts a `key` of a given type, assuming that the descr is exclusively a map (or dynamic). - - The key may be an atom, or a key type. """ def map_put(:term, _key, _type), do: :badmap def map_put(descr, key, :term) when is_atom(key), do: map_put_shared(descr, key, :term) @@ -1809,10 +1807,13 @@ defmodule Module.Types.Descr do end end - # Map.put but because we are inserting in a key type, we use refresh (keep the previous type) - def map_update(:term, _key, _type), do: :badmap + @doc """ + Refreshes the type of map after assuming some type was given to a key of a given type. + Assuming that the descr is exclusively a map (or dynamic). + """ + def map_refresh(:term, _key, _type), do: :badmap - def map_update(descr, key_descr, type) do + def map_refresh(descr, key_descr, type) do {dynamic_descr, static_descr} = Map.pop(descr, :dynamic) key_descr = unfold(key_descr) type = unfold(type) @@ -1828,7 +1829,7 @@ defmodule Module.Types.Descr do # Either of those three types could be dynamic. not (not is_nil(dynamic_descr) or Map.has_key?(key_descr, :dynamic) or Map.has_key?(type, :dynamic)) -> - map_update_static(descr, key_descr, type) + map_refresh_static(descr, key_descr, type) true -> # If one of those is dynamic, we just compute the union @@ -1836,14 +1837,14 @@ defmodule Module.Types.Descr do {key_dynamic, key_static} = Map.pop(key_descr, :dynamic, key_descr) {type_dynamic, type_static} = Map.pop(type, :dynamic, type) - with {:ok, new_static} <- map_update_static(descr_static, key_static, type_static), - {:ok, new_dynamic} <- map_update_static(descr_dynamic, key_dynamic, type_dynamic) do + with {:ok, new_static} <- map_refresh_static(descr_static, key_static, type_static), + {:ok, new_dynamic} <- map_refresh_static(descr_dynamic, key_dynamic, type_dynamic) do {:ok, union(new_static, dynamic(new_dynamic))} end end end - def map_update_static(%{map: _} = descr, key_descr = %{}, type) do + def map_refresh_static(%{map: _} = descr, key_descr = %{}, type) do # Check if descr is a valid map, case atom_fetch(key_descr) do # If the key_descr is a singleton, we directly put the type into the map. @@ -1858,18 +1859,18 @@ defmodule Module.Types.Descr do |> covered_key_types() |> Enum.reduce(descr, fn {:atom, atom_key}, acc -> - map_put_atom(acc, atom_key, type) + map_refresh_atom(acc, atom_key, type) key, acc -> - map_put_domain(acc, key, type) + map_refresh_domain(acc, key, type) end) {:ok, new_descr} end end - def map_update_static(:term, _key_descr, _type), do: {:ok, open_map()} - def map_update_static(_, _, _), do: {:ok, none()} + def map_refresh_static(:term, _key_descr, _type), do: {:ok, open_map()} + def map_refresh_static(_, _, _), do: {:ok, none()} @doc """ Updates a key in a map type by fetching its current type, unioning it with a @@ -1903,28 +1904,28 @@ defmodule Module.Types.Descr do end end - def map_put_domain(%{map: [{tag, fields, []}]}, domain, type) do - %{map: [{map_update_domain(tag, domain, type), fields, []}]} + def map_refresh_domain(%{map: [{tag, fields, []}]}, domain, type) do + %{map: [{map_refresh_tag(tag, domain, type), fields, []}]} end - def map_put_domain(%{map: dnf}, domain, type) do + def map_refresh_domain(%{map: dnf}, domain, type) do Enum.map(dnf, fn {tag, fields, []} -> - {map_update_domain(tag, domain, type), fields, []} + {map_refresh_tag(tag, domain, type), fields, []} {tag, fields, negs} -> # For negations, we count on the idea that a negation will not remove any # type from a domain unless it completely cancels out the type. # So for any non-empty map dnf, we just update the domain with the new type, # as well as its negations to keep them accurate. - {map_update_domain(tag, domain, type), fields, + {map_refresh_tag(tag, domain, type), fields, Enum.map(negs, fn {neg_tag, neg_fields} -> - {map_update_domain(neg_tag, domain, type), neg_fields} + {map_refresh_tag(neg_tag, domain, type), neg_fields} end)} end) end - def map_put_atom(descr = %{map: dnf}, atom_key, type) do + def map_refresh_atom(descr = %{map: dnf}, atom_key, type) do case atom_key do {:union, keys} -> keys @@ -1940,11 +1941,11 @@ defmodule Module.Types.Descr do considered_keys |> :sets.to_list() |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end) - |> map_put_domain(:atom, type) + |> map_refresh_domain(:atom, type) end end - def map_update_domain(tag, domain, type) do + def map_refresh_tag(tag, domain, type) do case tag do :open -> :open diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 7c4d777fe2d..318afe00b26 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -389,6 +389,20 @@ defmodule Module.Types.DescrTest do {{:domain_key, :binary}, none()} ]) ) + + assert subtype?(empty_map(), closed_map([{{:domain_key, :integer}, atom()}])) + + t1 = closed_map([{{:domain_key, :integer}, atom()}]) + t2 = closed_map([{{:domain_key, :integer}, binary()}]) + + assert equal?(intersection(t1, t2), empty_map()) + + t1 = closed_map([{{:domain_key, :integer}, atom()}]) + t2 = closed_map([{{:domain_key, :atom}, term()}]) + + # their intersection is the empty map + refute empty?(intersection(t1, t2)) + assert equal?(intersection(t1, t2), empty_map()) end end @@ -702,6 +716,27 @@ defmodule Module.Types.DescrTest do assert subtype?(closed_map(a: integer()), closed_map(a: if_set(integer()))) refute subtype?(closed_map(a: if_set(term())), closed_map(a: term())) assert subtype?(closed_map(a: term()), closed_map(a: if_set(term()))) + + # With domains + t1 = closed_map([{{:domain_key, :integer}, number()}]) + t2 = closed_map([{{:domain_key, :integer}, integer()}]) + + assert subtype?(t2, t1) + + t1_minus_t2 = difference(t1, t2) + refute empty?(t1_minus_t2) + + assert subtype?(map_with_default(number()), open_map()) + t = difference(open_map(), map_with_default(number())) + refute empty?(t) + refute subtype?(open_map(), map_with_default(number())) + assert subtype?(map_with_default(integer()), map_with_default(number())) + refute subtype?(map_with_default(float()), map_with_default(atom())) + + assert equal?( + intersection(map_with_default(number()), map_with_default(float())), + map_with_default(float()) + ) end test "list" do @@ -782,552 +817,509 @@ defmodule Module.Types.DescrTest do end end - describe "domain key types" do - # for intersection - test "map domain key types" do - assert subtype?(empty_map(), closed_map([{{:domain_key, :integer}, atom()}])) - - t1 = closed_map([{{:domain_key, :integer}, atom()}]) - t2 = closed_map([{{:domain_key, :integer}, binary()}]) - - assert equal?(intersection(t1, t2), empty_map()) - - t1 = closed_map([{{:domain_key, :integer}, atom()}]) - t2 = closed_map([{{:domain_key, :atom}, term()}]) - - # their intersection is the empty map - refute empty?(intersection(t1, t2)) - assert equal?(intersection(t1, t2), empty_map()) - end - - test "basic map with domain key type and fetch" do - integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) - assert map_fetch(integer_to_atom, :foo) == :badkey - - # the key :a is for sure of type pid and exists in type - # %{atom() => pid()} and not %{:a => not_set()} - t1 = closed_map([{{:domain_key, :atom}, pid()}]) - t2 = closed_map(a: not_set()) - t3 = open_map(a: not_set()) + # TODO: operator t\[t'] + # test "map delete" do + # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) - # Indeed, t2 is equivalent to the empty map - assert map_fetch(difference(t1, t2), :a) == :badkey - assert map_fetch(difference(t1, t3), :a) == {false, pid()} + # assert map_delete(t1, atom([:a])) + # |> equal?(closed_map([{:a, not_set()}, {{:domain_key, :integer}, number()}])) - t4 = closed_map([{{:domain_key, :pid}, atom()}]) - assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()} + # assert map_delete(t1, atom([:a, :b])) + # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) - assert map_fetch(closed_map([{{:domain_key, :atom}, pid()}]), :a) == :badkey - - assert map_fetch(dynamic(closed_map([{{:domain_key, :atom}, pid()}])), :a) == - {true, dynamic(pid())} + # assert map_delete(t1, term()) + # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) + # end +end - assert closed_map([{{:domain_key, :atom}, number()}]) - |> difference(open_map(a: if_set(integer()))) - |> map_fetch(:a) == {false, float()} +describe "projections" do + test "fun_fetch" do + assert fun_fetch(term(), 1) == :error + assert fun_fetch(union(term(), dynamic(fun())), 1) == :error + assert fun_fetch(fun(), 1) == :ok + assert fun_fetch(dynamic(), 1) == :ok + end - assert closed_map([{{:domain_key, :atom}, number()}]) - |> difference(closed_map(b: if_set(integer()))) - |> map_fetch(:a) == :badkey + test "truthness" do + for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do + assert truthness(type) == :undefined + assert truthness(dynamic(type)) == :undefined end - test "map get" do - assert map_get(term(), term()) == :badmap - - map_type = closed_map([{{:domain_key, :tuple}, binary()}]) - assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())} - # assert map_fetch(map_type, :b) == :badkey - - # Type with all domain types - # %{:bar => :ok, integer() => :int, float() => :float, atom() => binary(), binary() => integer(), tuple() => float(), map() => pid(), reference() => port(), pid() => boolean()} - all_domains = - closed_map([ - {:bar, atom([:ok])}, - {{:domain_key, :integer}, atom([:int])}, - {{:domain_key, :float}, atom([:float])}, - {{:domain_key, :atom}, binary()}, - {{:domain_key, :binary}, integer()}, - {{:domain_key, :tuple}, float()}, - {{:domain_key, :map}, pid()}, - {{:domain_key, :reference}, port()}, - {{:domain_key, :pid}, reference()}, - {{:domain_key, :port}, boolean()} - ]) - - assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])} - - assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()} - assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()} - assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])} - assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()} - assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()} - assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())} - assert map_get(all_domains, empty_map()) == {:ok, pid() |> nil_or_type()} - - # Union - assert map_get(all_domains, union(tuple(), empty_map())) == - {:ok, union(float(), pid() |> nil_or_type())} - - # Removing all maps with tuple keys - t_no_tuple = difference(all_domains, closed_map([{{:domain_key, :tuple}, float()}])) - t_really_no_tuple = difference(all_domains, open_map([{{:domain_key, :tuple}, float()}])) - assert subtype?(all_domains, open_map()) - # It's only closed maps, so it should not change - assert map_get(t_no_tuple, tuple()) == {:ok, float() |> nil_or_type()} - # This time we actually removed all tuple to float keys - assert map_get(t_really_no_tuple, tuple()) == {:ok_absent, atom([nil])} - - t1 = closed_map([{{:domain_key, :tuple}, integer()}]) - t2 = closed_map([{{:domain_key, :tuple}, float()}]) - t3 = union(t1, t2) - assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()} + for type <- [atom([false]), atom([nil]), atom([nil, false]), atom([false, nil])] do + assert truthness(type) == :always_false + assert truthness(dynamic(type)) == :always_false end - test "map get with dynamic" do - {_answer, type_selected} = map_get(dynamic(), term()) - assert equal?(type_selected, dynamic() |> nil_or_type()) + for type <- + [negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do + assert truthness(type) == :always_true + assert truthness(dynamic(type)) == :always_true end - - test "more complex map get over atoms" do - map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}]) - assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])} - assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())} - assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()} - - assert map_get(map, atom() |> difference(atom([:a]))) == - {:ok, union(atom([:b]), pid() |> nil_or_type())} - end - - test "subtyping with domain key types" do - t1 = closed_map([{{:domain_key, :integer}, number()}]) - t2 = closed_map([{{:domain_key, :integer}, integer()}]) - - assert subtype?(t2, t1) - - t1_minus_t2 = difference(t1, t2) - refute empty?(t1_minus_t2) - - assert subtype?(map_with_default(number()), open_map()) - t = difference(open_map(), map_with_default(number())) - refute empty?(t) - refute subtype?(open_map(), map_with_default(number())) - assert subtype?(map_with_default(integer()), map_with_default(number())) - refute subtype?(map_with_default(float()), map_with_default(atom())) - - assert equal?( - intersection(map_with_default(number()), map_with_default(float())), - map_with_default(float()) - ) - end - - # TODO: operator t\[t'] - # test "map delete" do - # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) - - # assert map_delete(t1, atom([:a])) - # |> equal?(closed_map([{:a, not_set()}, {{:domain_key, :integer}, number()}])) - - # assert map_delete(t1, atom([:a, :b])) - # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) - - # assert map_delete(t1, term()) - # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) - # end end - describe "projections" do - test "fun_fetch" do - assert fun_fetch(term(), 1) == :error - assert fun_fetch(union(term(), dynamic(fun())), 1) == :error - assert fun_fetch(fun(), 1) == :ok - assert fun_fetch(dynamic(), 1) == :ok - end - - test "truthness" do - for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do - assert truthness(type) == :undefined - assert truthness(dynamic(type)) == :undefined - end + test "atom_fetch" do + assert atom_fetch(term()) == :error + assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error - for type <- [atom([false]), atom([nil]), atom([nil, false]), atom([false, nil])] do - assert truthness(type) == :always_false - assert truthness(dynamic(type)) == :always_false - end + assert atom_fetch(atom()) == {:infinite, []} + assert atom_fetch(dynamic()) == {:infinite, []} - for type <- - [negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do - assert truthness(type) == :always_true - assert truthness(dynamic(type)) == :always_true - end - end + assert atom_fetch(atom([:foo, :bar])) == + {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} - test "atom_fetch" do - assert atom_fetch(term()) == :error - assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error - - assert atom_fetch(atom()) == {:infinite, []} - assert atom_fetch(dynamic()) == {:infinite, []} - - assert atom_fetch(atom([:foo, :bar])) == - {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} - - assert atom_fetch(union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} - assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} - end - - test "list_hd" do - assert list_hd(none()) == :badnonemptylist - assert list_hd(term()) == :badnonemptylist - assert list_hd(list(term())) == :badnonemptylist - assert list_hd(empty_list()) == :badnonemptylist - assert list_hd(non_empty_list(term())) == {false, term()} - assert list_hd(non_empty_list(integer())) == {false, integer()} - assert list_hd(difference(list(number()), list(integer()))) == {false, number()} - - assert list_hd(dynamic()) == {true, dynamic()} - assert list_hd(dynamic(list(integer()))) == {true, dynamic(integer())} - assert list_hd(union(dynamic(), atom())) == :badnonemptylist - assert list_hd(union(dynamic(), list(term()))) == :badnonemptylist + assert atom_fetch(union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} + assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} + end - assert list_hd(difference(list(number()), list(number()))) == :badnonemptylist - assert list_hd(dynamic(difference(list(number()), list(number())))) == :badnonemptylist + test "list_hd" do + assert list_hd(none()) == :badnonemptylist + assert list_hd(term()) == :badnonemptylist + assert list_hd(list(term())) == :badnonemptylist + assert list_hd(empty_list()) == :badnonemptylist + assert list_hd(non_empty_list(term())) == {false, term()} + assert list_hd(non_empty_list(integer())) == {false, integer()} + assert list_hd(difference(list(number()), list(integer()))) == {false, number()} + + assert list_hd(dynamic()) == {true, dynamic()} + assert list_hd(dynamic(list(integer()))) == {true, dynamic(integer())} + assert list_hd(union(dynamic(), atom())) == :badnonemptylist + assert list_hd(union(dynamic(), list(term()))) == :badnonemptylist + + assert list_hd(difference(list(number()), list(number()))) == :badnonemptylist + assert list_hd(dynamic(difference(list(number()), list(number())))) == :badnonemptylist + + assert list_hd(union(dynamic(list(float())), non_empty_list(atom()))) == + {true, union(dynamic(float()), atom())} + + # If term() is in the tail, it means list(term()) is in the tail + # and therefore any term can be returned from hd. + assert list_hd(non_empty_list(atom(), term())) == {false, term()} + assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {false, atom()} + end - assert list_hd(union(dynamic(list(float())), non_empty_list(atom()))) == - {true, union(dynamic(float()), atom())} + test "list_tl" do + assert list_tl(none()) == :badnonemptylist + assert list_tl(term()) == :badnonemptylist + assert list_tl(empty_list()) == :badnonemptylist + assert list_tl(list(integer())) == :badnonemptylist + assert list_tl(difference(list(number()), list(number()))) == :badnonemptylist - # If term() is in the tail, it means list(term()) is in the tail - # and therefore any term can be returned from hd. - assert list_hd(non_empty_list(atom(), term())) == {false, term()} - assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {false, atom()} - end + assert list_tl(non_empty_list(integer())) == {false, list(integer())} - test "list_tl" do - assert list_tl(none()) == :badnonemptylist - assert list_tl(term()) == :badnonemptylist - assert list_tl(empty_list()) == :badnonemptylist - assert list_tl(list(integer())) == :badnonemptylist - assert list_tl(difference(list(number()), list(number()))) == :badnonemptylist + assert list_tl(non_empty_list(integer(), atom())) == + {false, union(atom(), non_empty_list(integer(), atom()))} - assert list_tl(non_empty_list(integer())) == {false, list(integer())} + # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list + # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of + # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. + assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == + {false, + atom() + |> union(float()) + |> union(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())))} - assert list_tl(non_empty_list(integer(), atom())) == - {false, union(atom(), non_empty_list(integer(), atom()))} + assert list_tl(dynamic()) == {true, dynamic()} + assert list_tl(dynamic(list(integer()))) == {true, dynamic(list(integer()))} - # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list - # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of - # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. - assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == - {false, - atom() - |> union(float()) - |> union( - union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())) - )} + assert list_tl(dynamic(list(integer(), atom()))) == + {true, dynamic(union(atom(), list(integer(), atom())))} + end - assert list_tl(dynamic()) == {true, dynamic()} - assert list_tl(dynamic(list(integer()))) == {true, dynamic(list(integer()))} + test "tuple_fetch" do + assert tuple_fetch(term(), 0) == :badtuple + assert tuple_fetch(integer(), 0) == :badtuple - assert list_tl(dynamic(list(integer(), atom()))) == - {true, dynamic(union(atom(), list(integer(), atom())))} - end + 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 - test "tuple_fetch" do - assert tuple_fetch(term(), 0) == :badtuple - assert tuple_fetch(integer(), 0) == :badtuple + assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} + assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} + assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex - 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_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} - assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} - assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex + assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == + {false, atom()} - assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex - assert tuple_fetch(empty_tuple(), 0) == :badindex - assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badindex + assert difference(tuple([union(integer(), atom())]), open_tuple([atom()])) + |> tuple_fetch(0) == {false, integer()} - assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == - {false, atom()} + 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 difference(tuple([union(integer(), atom())]), open_tuple([atom()])) - |> tuple_fetch(0) == {false, integer()} + assert tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0) == + {false, union(integer(), atom())} - 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([integer(), atom(), union(atom(), integer())]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} - assert tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0) == - {false, union(integer(), atom())} + assert tuple([integer(), atom(), union(union(atom(), integer()), list(term()))]) + |> difference(tuple([integer(), term(), atom()])) + |> difference(open_tuple([term(), atom(), list(term())])) + |> tuple_fetch(2) == {false, integer()} - assert tuple([integer(), atom(), union(atom(), integer())]) - |> difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), integer()])) + |> tuple_fetch(1) == :badindex - assert tuple([integer(), atom(), union(union(atom(), integer()), list(term()))]) - |> difference(tuple([integer(), term(), atom()])) - |> difference(open_tuple([term(), atom(), list(term())])) - |> tuple_fetch(2) == {false, integer()} + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} - assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), integer()])) - |> tuple_fetch(1) == :badindex + assert tuple_fetch(tuple(), 0) == :badindex + end - assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + 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(tuple(), 0) == :badindex - end + assert tuple_fetch(dynamic(tuple()), 0) + |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) 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(union(dynamic(), open_tuple([atom()])), 0) == + {true, union(atom(), dynamic())} + end - assert tuple_fetch(dynamic(tuple()), 0) - |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) + test "tuple_delete_at" do + assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badindex + assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex + assert tuple_delete_at(empty_tuple(), 0) == :badindex + assert tuple_delete_at(integer(), 0) == :badtuple + assert tuple_delete_at(term(), 0) == :badtuple + + # Test deleting an element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) == + tuple([integer(), boolean()]) + + # Test deleting the last element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom()]), 1) == + tuple([integer()]) + + # Test deleting from an open tuple + assert tuple_delete_at(open_tuple([integer(), atom(), boolean()]), 1) == + open_tuple([integer(), boolean()]) + + # Test deleting from a dynamic tuple + assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == + dynamic(tuple([integer()])) + + # Test deleting from a union of tuples + assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == + union(tuple([integer()]), tuple([float()])) + + # Test deleting from an intersection of tuples + assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + |> tuple_delete_at(1) == tuple([integer()]) + + # Test deleting from a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_delete_at(1) + |> equal?(tuple([integer(), boolean()])) + + # Test deleting from a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_delete_at(1) + |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + + # Successfully deleting at position `index` in a tuple means that the dynamic + # values that succeed are intersected with tuples of size at least `index` + assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) + assert dynamic(term()) |> tuple_delete_at(0) == dynamic(tuple()) + + assert dynamic(union(tuple(), integer())) + |> tuple_delete_at(1) + |> equal?(dynamic(tuple_of_size_at_least(1))) + end - assert tuple_fetch(union(dynamic(), open_tuple([atom()])), 0) == - {true, union(atom(), dynamic())} - end + test "tuple_insert_at" do + assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badindex + assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex + assert tuple_insert_at(integer(), 0, boolean()) == :badtuple + assert tuple_insert_at(term(), 0, boolean()) == :badtuple + + # Out-of-bounds in a union + assert union(tuple([integer(), atom()]), tuple([float()])) + |> tuple_insert_at(2, boolean()) == :badindex + + # Test inserting into a closed tuple + assert tuple_insert_at(tuple([integer(), atom()]), 1, boolean()) == + tuple([integer(), boolean(), atom()]) + + # Test inserting at the beginning of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 0, boolean()) == + tuple([boolean(), integer(), atom()]) + + # Test inserting at the end of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 2, boolean()) == + tuple([integer(), atom(), boolean()]) + + # Test inserting into an empty tuple + assert tuple_insert_at(empty_tuple(), 0, integer()) == tuple([integer()]) + + # Test inserting into an open tuple + assert tuple_insert_at(open_tuple([integer(), atom()]), 1, boolean()) == + open_tuple([integer(), boolean(), atom()]) + + # Test inserting a dynamic type + assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == + dynamic(tuple([integer(), term(), atom()])) + + # Test inserting into a dynamic tuple + assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == + dynamic(tuple([integer(), boolean(), atom()])) + + # Test inserting into a union of tuples + assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == + union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) + + # Test inserting into a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_insert_at(1, float()) + |> equal?(tuple([integer(), float(), atom(), boolean()])) + + # Test inserting into a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_insert_at(1, boolean()) + |> equal?( + union( + tuple([integer(), boolean(), atom()]), + dynamic(tuple([float(), boolean(), binary()])) + ) + ) - test "tuple_delete_at" do - assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badindex - assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex - assert tuple_delete_at(empty_tuple(), 0) == :badindex - assert tuple_delete_at(integer(), 0) == :badtuple - assert tuple_delete_at(term(), 0) == :badtuple + # If you successfully intersect at position index in a type, then the dynamic values + # that succeed are intersected with tuples of size at least index + assert dynamic(union(tuple(), integer())) + |> tuple_insert_at(1, boolean()) + |> equal?(dynamic(open_tuple([term(), boolean()]))) + end - # Test deleting an element from a closed tuple - assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) == - tuple([integer(), boolean()]) + test "tuple_values" do + assert tuple_values(integer()) == :badtuple + assert tuple_values(tuple([])) == none() + assert tuple_values(tuple()) == term() + assert tuple_values(open_tuple([integer()])) == term() + assert tuple_values(tuple([integer(), atom()])) == union(integer(), atom()) - # Test deleting the last element from a closed tuple - assert tuple_delete_at(tuple([integer(), atom()]), 1) == - tuple([integer()]) + assert tuple_values(union(tuple([float(), pid()]), tuple([reference()]))) == + union(float(), union(pid(), reference())) - # Test deleting from an open tuple - assert tuple_delete_at(open_tuple([integer(), atom(), boolean()]), 1) == - open_tuple([integer(), boolean()]) + assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), term()]))) == + union(integer(), atom()) - # Test deleting from a dynamic tuple - assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == - dynamic(tuple([integer()])) + assert union(tuple([atom([:ok])]), open_tuple([integer()])) + |> difference(open_tuple([term(), term()])) + |> tuple_values() == union(atom([:ok]), integer()) - # Test deleting from a union of tuples - assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == - union(tuple([integer()]), tuple([float()])) + assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), atom([:ok])]))) == + union(number(), atom()) - # Test deleting from an intersection of tuples - assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) - |> tuple_delete_at(1) == tuple([integer()]) + assert tuple_values(dynamic(tuple())) == dynamic() + assert tuple_values(dynamic(tuple([integer()]))) == dynamic(integer()) - # Test deleting from a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) - |> tuple_delete_at(1) - |> equal?(tuple([integer(), boolean()])) + assert tuple_values(union(dynamic(tuple([integer()])), tuple([atom()]))) == + union(dynamic(integer()), atom()) - # Test deleting from a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) - |> tuple_delete_at(1) - |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + assert tuple_values(union(dynamic(tuple()), integer())) == :badtuple + assert tuple_values(dynamic(union(integer(), tuple([atom()])))) == dynamic(atom()) - # Successfully deleting at position `index` in a tuple means that the dynamic - # values that succeed are intersected with tuples of size at least `index` - assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) - assert dynamic(term()) |> tuple_delete_at(0) == dynamic(tuple()) + assert tuple_values(union(dynamic(tuple([integer()])), tuple([integer()]))) + |> equal?(integer()) + end - assert dynamic(union(tuple(), integer())) - |> tuple_delete_at(1) - |> equal?(dynamic(tuple_of_size_at_least(1))) - end + test "map_fetch" do + assert map_fetch(term(), :a) == :badmap + assert map_fetch(union(open_map(), integer()), :a) == :badmap - test "tuple_insert_at" do - assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badindex - assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex - assert tuple_insert_at(integer(), 0, boolean()) == :badtuple - assert tuple_insert_at(term(), 0, boolean()) == :badtuple + assert map_fetch(open_map(), :a) == :badkey + assert map_fetch(open_map(a: not_set()), :a) == :badkey + assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey + assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badkey - # Out-of-bounds in a union - assert union(tuple([integer(), atom()]), tuple([float()])) - |> tuple_insert_at(2, boolean()) == :badindex + assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} - # Test inserting into a closed tuple - assert tuple_insert_at(tuple([integer(), atom()]), 1, boolean()) == - tuple([integer(), boolean(), atom()]) + assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == + {false, union(integer(), atom())} - # Test inserting at the beginning of a tuple - assert tuple_insert_at(tuple([integer(), atom()]), 0, boolean()) == - tuple([boolean(), integer(), atom()]) + {false, value_type} = + open_map(my_map: open_map(foo: integer())) + |> intersection(open_map(my_map: open_map(bar: boolean()))) + |> map_fetch(:my_map) - # Test inserting at the end of a tuple - assert tuple_insert_at(tuple([integer(), atom()]), 2, boolean()) == - tuple([integer(), atom(), boolean()]) + assert equal?(value_type, open_map(foo: integer(), bar: boolean())) - # Test inserting into an empty tuple - assert tuple_insert_at(empty_tuple(), 0, integer()) == tuple([integer()]) + {false, value_type} = + closed_map(a: union(integer(), atom())) + |> difference(open_map(a: integer())) + |> map_fetch(:a) - # Test inserting into an open tuple - assert tuple_insert_at(open_tuple([integer(), atom()]), 1, boolean()) == - open_tuple([integer(), boolean(), atom()]) + assert equal?(value_type, atom()) - # Test inserting a dynamic type - assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == - dynamic(tuple([integer(), term(), atom()])) + {false, value_type} = + closed_map(a: integer(), b: atom()) + |> difference(closed_map(a: integer(), b: atom([:foo]))) + |> map_fetch(:a) - # Test inserting into a dynamic tuple - assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == - dynamic(tuple([integer(), boolean(), atom()])) - - # Test inserting into a union of tuples - assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == - union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) + assert equal?(value_type, integer()) - # Test inserting into a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) - |> tuple_insert_at(1, float()) - |> equal?(tuple([integer(), float(), atom(), boolean()])) - - # Test inserting into a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) - |> tuple_insert_at(1, boolean()) - |> equal?( - union( - tuple([integer(), boolean(), atom()]), - dynamic(tuple([float(), boolean(), binary()])) - ) - ) + {false, value_type} = + closed_map(a: integer()) + |> difference(closed_map(a: atom())) + |> map_fetch(:a) - # If you successfully intersect at position index in a type, then the dynamic values - # that succeed are intersected with tuples of size at least index - assert dynamic(union(tuple(), integer())) - |> tuple_insert_at(1, boolean()) - |> equal?(dynamic(open_tuple([term(), boolean()]))) - end + assert equal?(value_type, integer()) - test "tuple_values" do - assert tuple_values(integer()) == :badtuple - assert tuple_values(tuple([])) == none() - assert tuple_values(tuple()) == term() - assert tuple_values(open_tuple([integer()])) == term() - assert tuple_values(tuple([integer(), atom()])) == union(integer(), atom()) + {false, value_type} = + open_map(a: integer(), b: atom()) + |> union(closed_map(a: tuple())) + |> map_fetch(:a) - assert tuple_values(union(tuple([float(), pid()]), tuple([reference()]))) == - union(float(), union(pid(), reference())) + assert equal?(value_type, union(integer(), tuple())) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), term()]))) == - union(integer(), atom()) + {false, value_type} = + closed_map(a: atom()) + |> difference(closed_map(a: atom([:foo, :bar]))) + |> difference(closed_map(a: atom([:bar]))) + |> map_fetch(:a) - assert union(tuple([atom([:ok])]), open_tuple([integer()])) - |> difference(open_tuple([term(), term()])) - |> tuple_values() == union(atom([:ok]), integer()) + assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), atom([:ok])]))) == - union(number(), atom()) + assert closed_map(a: union(atom(), pid()), b: integer(), c: tuple()) + |> difference(open_map(a: atom(), b: integer())) + |> difference(open_map(a: atom(), c: tuple())) + |> map_fetch(:a) == {false, pid()} - assert tuple_values(dynamic(tuple())) == dynamic() - assert tuple_values(dynamic(tuple([integer()]))) == dynamic(integer()) + assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) + |> difference(open_map(a: atom([:foo]), b: integer())) + |> difference(open_map(a: atom(), c: tuple())) + |> map_fetch(:a) == {false, pid()} - assert tuple_values(union(dynamic(tuple([integer()])), tuple([atom()]))) == - union(dynamic(integer()), atom()) + assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) + |> difference(open_map(a: atom([:foo, :bar]))) + |> difference(open_map(a: atom([:foo, :baz]))) + |> map_fetch(:a) == {false, integer()} + end - assert tuple_values(union(dynamic(tuple()), integer())) == :badtuple - assert tuple_values(dynamic(union(integer(), tuple([atom()])))) == dynamic(atom()) + 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 tuple_values(union(dynamic(tuple([integer()])), tuple([integer()]))) - |> equal?(integer()) - end + assert intersection(dynamic(), open_map(a: integer())) + |> map_fetch(:a) == {false, intersection(integer(), dynamic())} - test "map_fetch" do - assert map_fetch(term(), :a) == :badmap - assert map_fetch(union(open_map(), integer()), :a) == :badmap + {false, type} = union(dynamic(integer()), open_map(a: integer())) |> map_fetch(:a) + assert equal?(type, integer()) - assert map_fetch(open_map(), :a) == :badkey - assert map_fetch(open_map(a: not_set()), :a) == :badkey - assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey - assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badkey + assert union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch(:a) == :badkey - assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} + assert union(dynamic(open_map(a: atom())), open_map(a: integer())) + |> map_fetch(:a) == {false, union(dynamic(atom()), integer())} - assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == - {false, union(integer(), atom())} + # With domains + integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) + assert map_fetch(integer_to_atom, :foo) == :badkey - {false, value_type} = - open_map(my_map: open_map(foo: integer())) - |> intersection(open_map(my_map: open_map(bar: boolean()))) - |> map_fetch(:my_map) + # the key :a is for sure of type pid and exists in type + # %{atom() => pid()} and not %{:a => not_set()} + t1 = closed_map([{{:domain_key, :atom}, pid()}]) + t2 = closed_map(a: not_set()) + t3 = open_map(a: not_set()) - assert equal?(value_type, open_map(foo: integer(), bar: boolean())) + # Indeed, t2 is equivalent to the empty map + assert map_fetch(difference(t1, t2), :a) == :badkey + assert map_fetch(difference(t1, t3), :a) == {false, pid()} - {false, value_type} = - closed_map(a: union(integer(), atom())) - |> difference(open_map(a: integer())) - |> map_fetch(:a) + t4 = closed_map([{{:domain_key, :pid}, atom()}]) + assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()} - assert equal?(value_type, atom()) + assert map_fetch(closed_map([{{:domain_key, :atom}, pid()}]), :a) == :badkey - {false, value_type} = - closed_map(a: integer(), b: atom()) - |> difference(closed_map(a: integer(), b: atom([:foo]))) - |> map_fetch(:a) + assert map_fetch(dynamic(closed_map([{{:domain_key, :atom}, pid()}])), :a) == + {true, dynamic(pid())} - assert equal?(value_type, integer()) + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(open_map(a: if_set(integer()))) + |> map_fetch(:a) == {false, float()} - {false, value_type} = - closed_map(a: integer()) - |> difference(closed_map(a: atom())) - |> map_fetch(:a) + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(closed_map(b: if_set(integer()))) + |> map_fetch(:a) == :badkey + end - assert equal?(value_type, integer()) + test "map_get" do + test "map get" do + assert map_get(term(), term()) == :badmap - {false, value_type} = - open_map(a: integer(), b: atom()) - |> union(closed_map(a: tuple())) - |> map_fetch(:a) + map_type = closed_map([{{:domain_key, :tuple}, binary()}]) + assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())} - assert equal?(value_type, union(integer(), tuple())) + # Type with all domain types + # %{:bar => :ok, integer() => :int, float() => :float, atom() => binary(), binary() => integer(), tuple() => float(), map() => pid(), reference() => port(), pid() => boolean()} + all_domains = + closed_map([ + {:bar, atom([:ok])}, + {{:domain_key, :integer}, atom([:int])}, + {{:domain_key, :float}, atom([:float])}, + {{:domain_key, :atom}, binary()}, + {{:domain_key, :binary}, integer()}, + {{:domain_key, :tuple}, float()}, + {{:domain_key, :map}, pid()}, + {{:domain_key, :reference}, port()}, + {{:domain_key, :pid}, reference()}, + {{:domain_key, :port}, boolean()} + ]) - {false, value_type} = - closed_map(a: atom()) - |> difference(closed_map(a: atom([:foo, :bar]))) - |> difference(closed_map(a: atom([:bar]))) - |> map_fetch(:a) + assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])} - assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) + assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()} + assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()} + assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])} + assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()} + assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()} + assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())} + assert map_get(all_domains, empty_map()) == {:ok, pid() |> nil_or_type()} - assert closed_map(a: union(atom(), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom(), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) - |> map_fetch(:a) == {false, pid()} + # Union + assert map_get(all_domains, union(tuple(), empty_map())) == + {:ok, union(float(), pid() |> nil_or_type())} - assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom([:foo]), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) - |> map_fetch(:a) == {false, pid()} + # Removing all maps with tuple keys + t_no_tuple = difference(all_domains, closed_map([{{:domain_key, :tuple}, float()}])) + t_really_no_tuple = difference(all_domains, open_map([{{:domain_key, :tuple}, float()}])) + assert subtype?(all_domains, open_map()) + # It's only closed maps, so it should not change + assert map_get(t_no_tuple, tuple()) == {:ok, float() |> nil_or_type()} + # This time we actually removed all tuple to float keys + assert map_get(t_really_no_tuple, tuple()) == {:ok_absent, atom([nil])} - assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) - |> difference(open_map(a: atom([:foo, :bar]))) - |> difference(open_map(a: atom([:foo, :baz]))) - |> map_fetch(:a) == {false, integer()} + t1 = closed_map([{{:domain_key, :tuple}, integer()}]) + t2 = closed_map([{{:domain_key, :tuple}, float()}]) + t3 = union(t1, t2) + assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()} end - 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())} - - {false, type} = union(dynamic(integer()), open_map(a: integer())) |> map_fetch(:a) - assert equal?(type, integer()) + test "map get with dynamic" do + {_answer, type_selected} = map_get(dynamic(), term()) + assert equal?(type_selected, dynamic() |> nil_or_type()) + end - assert union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch(:a) == :badkey + test "more complex map get over atoms" do + map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}]) + assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])} + assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())} + assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()} - assert union(dynamic(open_map(a: atom())), open_map(a: integer())) - |> map_fetch(:a) == {false, union(dynamic(atom()), integer())} + assert map_get(map, atom() |> difference(atom([:a]))) == + {:ok, union(atom([:b]), pid() |> nil_or_type())} end test "map_delete" do @@ -1500,26 +1492,26 @@ defmodule Module.Types.DescrTest do test "map put with key type" do # Using a literal key or an expression of that singleton key is the same - assert map_update(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} + assert map_refresh(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} # Several keys - assert map_update(empty_map(), atom([:a, :b]), integer()) == + assert map_refresh(empty_map(), atom([:a, :b]), integer()) == {:ok, closed_map(a: if_set(integer()), b: if_set(integer()))} - assert map_update(empty_map(), integer(), integer()) == + assert map_refresh(empty_map(), integer(), integer()) == {:ok, closed_map([{{:domain_key, :integer}, integer()}])} - assert map_update(closed_map([{{:domain_key, :integer}, integer()}]), integer(), float()) == + assert map_refresh(closed_map([{{:domain_key, :integer}, integer()}]), integer(), float()) == {:ok, closed_map([{{:domain_key, :integer}, number()}])} - assert map_update(open_map(), integer(), integer()) == {:ok, open_map()} + assert map_refresh(open_map(), integer(), integer()) == {:ok, open_map()} - {:ok, type} = map_update(empty_map(), integer(), dynamic()) + {:ok, type} = map_refresh(empty_map(), integer(), dynamic()) assert equal?(type, dynamic(closed_map([{{:domain_key, :integer}, term()}]))) # Adding a key of type float to a dynamic only guarantees that we have a map # as we cannot express "has at least one key of type float => float" - {:ok, type} = map_update(dynamic(), float(), float()) + {:ok, type} = map_refresh(dynamic(), float(), float()) assert equal?(type, dynamic(open_map())) assert closed_map([{{:domain_key, :integer}, integer()}]) @@ -1528,16 +1520,16 @@ defmodule Module.Types.DescrTest do assert closed_map([{{:domain_key, :integer}, integer()}]) |> difference(open_map()) - |> map_update(integer(), float()) == :badmap + |> map_refresh(integer(), float()) == :badmap - assert map_update(empty_map(), number(), float()) == + assert map_refresh(empty_map(), number(), float()) == {:ok, closed_map([{{:domain_key, :integer}, float()}, {{:domain_key, :float}, float()}])} # Tricky cases with atoms: # We add one atom fields that maps to an integer, which is not :a. So we do not touch # :a, add integer to :b, and add a domain field. - assert map_update( + assert map_refresh( closed_map(a: pid(), b: pid()), atom() |> difference(atom([:a])), integer() @@ -1549,7 +1541,7 @@ defmodule Module.Types.DescrTest do {{:domain_key, :atom}, integer()} ])} - assert map_update(empty_map(), term(), integer()) == {:ok, map_with_default(integer())} + assert map_refresh(empty_map(), term(), integer()) == {:ok, map_with_default(integer())} end end From 54d603f32cc12f2059072e685f923875891723aa Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Fri, 9 May 2025 16:11:53 +0200 Subject: [PATCH 5/6] Fix tests --- .../test/elixir/module/types/descr_test.exs | 735 +++++++++--------- 1 file changed, 380 insertions(+), 355 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 318afe00b26..47111ddc70e 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -830,432 +830,432 @@ defmodule Module.Types.DescrTest do # assert map_delete(t1, term()) # |> equal?(closed_map([{:a, if_set(pid())}, {{:domain_key, :integer}, number()}])) # end -end -describe "projections" do - test "fun_fetch" do - assert fun_fetch(term(), 1) == :error - assert fun_fetch(union(term(), dynamic(fun())), 1) == :error - assert fun_fetch(fun(), 1) == :ok - assert fun_fetch(dynamic(), 1) == :ok - end + describe "projections" do + test "fun_fetch" do + assert fun_fetch(term(), 1) == :error + assert fun_fetch(union(term(), dynamic(fun())), 1) == :error + assert fun_fetch(fun(), 1) == :ok + assert fun_fetch(dynamic(), 1) == :ok + end + + test "truthness" do + for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do + assert truthness(type) == :undefined + assert truthness(dynamic(type)) == :undefined + end - test "truthness" do - for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do - assert truthness(type) == :undefined - assert truthness(dynamic(type)) == :undefined + for type <- [atom([false]), atom([nil]), atom([nil, false]), atom([false, nil])] do + assert truthness(type) == :always_false + assert truthness(dynamic(type)) == :always_false + end + + for type <- + [negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do + assert truthness(type) == :always_true + assert truthness(dynamic(type)) == :always_true + end end - for type <- [atom([false]), atom([nil]), atom([nil, false]), atom([false, nil])] do - assert truthness(type) == :always_false - assert truthness(dynamic(type)) == :always_false + test "atom_fetch" do + assert atom_fetch(term()) == :error + assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error + + assert atom_fetch(atom()) == {:infinite, []} + assert atom_fetch(dynamic()) == {:infinite, []} + + assert atom_fetch(atom([:foo, :bar])) == + {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} + + assert atom_fetch(union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} + assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} end - for type <- - [negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do - assert truthness(type) == :always_true - assert truthness(dynamic(type)) == :always_true + test "list_hd" do + assert list_hd(none()) == :badnonemptylist + assert list_hd(term()) == :badnonemptylist + assert list_hd(list(term())) == :badnonemptylist + assert list_hd(empty_list()) == :badnonemptylist + assert list_hd(non_empty_list(term())) == {false, term()} + assert list_hd(non_empty_list(integer())) == {false, integer()} + assert list_hd(difference(list(number()), list(integer()))) == {false, number()} + + assert list_hd(dynamic()) == {true, dynamic()} + assert list_hd(dynamic(list(integer()))) == {true, dynamic(integer())} + assert list_hd(union(dynamic(), atom())) == :badnonemptylist + assert list_hd(union(dynamic(), list(term()))) == :badnonemptylist + + assert list_hd(difference(list(number()), list(number()))) == :badnonemptylist + assert list_hd(dynamic(difference(list(number()), list(number())))) == :badnonemptylist + + assert list_hd(union(dynamic(list(float())), non_empty_list(atom()))) == + {true, union(dynamic(float()), atom())} + + # If term() is in the tail, it means list(term()) is in the tail + # and therefore any term can be returned from hd. + assert list_hd(non_empty_list(atom(), term())) == {false, term()} + assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {false, atom()} end - end - test "atom_fetch" do - assert atom_fetch(term()) == :error - assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error + test "list_tl" do + assert list_tl(none()) == :badnonemptylist + assert list_tl(term()) == :badnonemptylist + assert list_tl(empty_list()) == :badnonemptylist + assert list_tl(list(integer())) == :badnonemptylist + assert list_tl(difference(list(number()), list(number()))) == :badnonemptylist - assert atom_fetch(atom()) == {:infinite, []} - assert atom_fetch(dynamic()) == {:infinite, []} + assert list_tl(non_empty_list(integer())) == {false, list(integer())} - assert atom_fetch(atom([:foo, :bar])) == - {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} + assert list_tl(non_empty_list(integer(), atom())) == + {false, union(atom(), non_empty_list(integer(), atom()))} - assert atom_fetch(union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} - assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} - end + # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list + # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of + # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. + assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == + {false, + atom() + |> union(float()) + |> union( + union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())) + )} - test "list_hd" do - assert list_hd(none()) == :badnonemptylist - assert list_hd(term()) == :badnonemptylist - assert list_hd(list(term())) == :badnonemptylist - assert list_hd(empty_list()) == :badnonemptylist - assert list_hd(non_empty_list(term())) == {false, term()} - assert list_hd(non_empty_list(integer())) == {false, integer()} - assert list_hd(difference(list(number()), list(integer()))) == {false, number()} - - assert list_hd(dynamic()) == {true, dynamic()} - assert list_hd(dynamic(list(integer()))) == {true, dynamic(integer())} - assert list_hd(union(dynamic(), atom())) == :badnonemptylist - assert list_hd(union(dynamic(), list(term()))) == :badnonemptylist - - assert list_hd(difference(list(number()), list(number()))) == :badnonemptylist - assert list_hd(dynamic(difference(list(number()), list(number())))) == :badnonemptylist - - assert list_hd(union(dynamic(list(float())), non_empty_list(atom()))) == - {true, union(dynamic(float()), atom())} - - # If term() is in the tail, it means list(term()) is in the tail - # and therefore any term can be returned from hd. - assert list_hd(non_empty_list(atom(), term())) == {false, term()} - assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {false, atom()} - end + assert list_tl(dynamic()) == {true, dynamic()} + assert list_tl(dynamic(list(integer()))) == {true, dynamic(list(integer()))} + + assert list_tl(dynamic(list(integer(), atom()))) == + {true, dynamic(union(atom(), list(integer(), atom())))} + end - test "list_tl" do - assert list_tl(none()) == :badnonemptylist - assert list_tl(term()) == :badnonemptylist - assert list_tl(empty_list()) == :badnonemptylist - assert list_tl(list(integer())) == :badnonemptylist - assert list_tl(difference(list(number()), list(number()))) == :badnonemptylist + test "tuple_fetch" do + assert tuple_fetch(term(), 0) == :badtuple + assert tuple_fetch(integer(), 0) == :badtuple - assert list_tl(non_empty_list(integer())) == {false, list(integer())} + 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 list_tl(non_empty_list(integer(), atom())) == - {false, union(atom(), non_empty_list(integer(), atom()))} + assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} + assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} + assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex - # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list - # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of - # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. - assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == - {false, - atom() - |> union(float()) - |> union(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())))} + assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex + assert tuple_fetch(empty_tuple(), 0) == :badindex + assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badindex - assert list_tl(dynamic()) == {true, dynamic()} - assert list_tl(dynamic(list(integer()))) == {true, dynamic(list(integer()))} + assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == + {false, atom()} - assert list_tl(dynamic(list(integer(), atom()))) == - {true, dynamic(union(atom(), list(integer(), atom())))} - end + assert difference(tuple([union(integer(), atom())]), open_tuple([atom()])) + |> tuple_fetch(0) == {false, integer()} - test "tuple_fetch" do - assert tuple_fetch(term(), 0) == :badtuple - assert tuple_fetch(integer(), 0) == :badtuple + 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(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(union(tuple([integer()]), tuple([atom()])), 0) == + {false, union(integer(), atom())} - assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} - assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} - assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex + assert tuple([integer(), atom(), union(atom(), integer())]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} - 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([integer(), atom(), union(union(atom(), integer()), list(term()))]) + |> difference(tuple([integer(), term(), atom()])) + |> difference(open_tuple([term(), atom(), list(term())])) + |> tuple_fetch(2) == {false, integer()} - assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == - {false, atom()} + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), integer()])) + |> tuple_fetch(1) == :badindex - assert difference(tuple([union(integer(), atom())]), open_tuple([atom()])) - |> tuple_fetch(0) == {false, integer()} + assert tuple([integer(), atom(), integer()]) + |> difference(tuple([integer(), term(), atom()])) + |> tuple_fetch(2) == {false, integer()} + + assert tuple_fetch(tuple(), 0) == :badindex + end - 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) + 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(union(tuple([integer()]), tuple([atom()])), 0) == - {false, union(integer(), atom())} + assert tuple_fetch(dynamic(tuple()), 0) + |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) - assert tuple([integer(), atom(), union(atom(), integer())]) - |> difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + assert tuple_fetch(union(dynamic(), open_tuple([atom()])), 0) == + {true, union(atom(), dynamic())} + end - assert tuple([integer(), atom(), union(union(atom(), integer()), list(term()))]) - |> difference(tuple([integer(), term(), atom()])) - |> difference(open_tuple([term(), atom(), list(term())])) - |> tuple_fetch(2) == {false, integer()} + test "tuple_delete_at" do + assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badindex + assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex + assert tuple_delete_at(empty_tuple(), 0) == :badindex + assert tuple_delete_at(integer(), 0) == :badtuple + assert tuple_delete_at(term(), 0) == :badtuple - assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), integer()])) - |> tuple_fetch(1) == :badindex + # Test deleting an element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) == + tuple([integer(), boolean()]) - assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + # Test deleting the last element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom()]), 1) == + tuple([integer()]) - assert tuple_fetch(tuple(), 0) == :badindex - end + # Test deleting from an open tuple + assert tuple_delete_at(open_tuple([integer(), atom(), boolean()]), 1) == + open_tuple([integer(), boolean()]) - 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 + # Test deleting from a dynamic tuple + assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == + dynamic(tuple([integer()])) - assert tuple_fetch(dynamic(tuple()), 0) - |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) + # Test deleting from a union of tuples + assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == + union(tuple([integer()]), tuple([float()])) - assert tuple_fetch(union(dynamic(), open_tuple([atom()])), 0) == - {true, union(atom(), dynamic())} - end + # Test deleting from an intersection of tuples + assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + |> tuple_delete_at(1) == tuple([integer()]) - test "tuple_delete_at" do - assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badindex - assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex - assert tuple_delete_at(empty_tuple(), 0) == :badindex - assert tuple_delete_at(integer(), 0) == :badtuple - assert tuple_delete_at(term(), 0) == :badtuple - - # Test deleting an element from a closed tuple - assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) == - tuple([integer(), boolean()]) - - # Test deleting the last element from a closed tuple - assert tuple_delete_at(tuple([integer(), atom()]), 1) == - tuple([integer()]) - - # Test deleting from an open tuple - assert tuple_delete_at(open_tuple([integer(), atom(), boolean()]), 1) == - open_tuple([integer(), boolean()]) - - # Test deleting from a dynamic tuple - assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == - dynamic(tuple([integer()])) - - # Test deleting from a union of tuples - assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == - union(tuple([integer()]), tuple([float()])) - - # Test deleting from an intersection of tuples - assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) - |> tuple_delete_at(1) == tuple([integer()]) - - # Test deleting from a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) - |> tuple_delete_at(1) - |> equal?(tuple([integer(), boolean()])) - - # Test deleting from a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) - |> tuple_delete_at(1) - |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) - - # Successfully deleting at position `index` in a tuple means that the dynamic - # values that succeed are intersected with tuples of size at least `index` - assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) - assert dynamic(term()) |> tuple_delete_at(0) == dynamic(tuple()) - - assert dynamic(union(tuple(), integer())) - |> tuple_delete_at(1) - |> equal?(dynamic(tuple_of_size_at_least(1))) - end + # Test deleting from a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_delete_at(1) + |> equal?(tuple([integer(), boolean()])) + + # Test deleting from a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_delete_at(1) + |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + + # Successfully deleting at position `index` in a tuple means that the dynamic + # values that succeed are intersected with tuples of size at least `index` + assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) + assert dynamic(term()) |> tuple_delete_at(0) == dynamic(tuple()) + + assert dynamic(union(tuple(), integer())) + |> tuple_delete_at(1) + |> equal?(dynamic(tuple_of_size_at_least(1))) + end + + test "tuple_insert_at" do + assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badindex + assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex + assert tuple_insert_at(integer(), 0, boolean()) == :badtuple + assert tuple_insert_at(term(), 0, boolean()) == :badtuple + + # Out-of-bounds in a union + assert union(tuple([integer(), atom()]), tuple([float()])) + |> tuple_insert_at(2, boolean()) == :badindex + + # Test inserting into a closed tuple + assert tuple_insert_at(tuple([integer(), atom()]), 1, boolean()) == + tuple([integer(), boolean(), atom()]) + + # Test inserting at the beginning of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 0, boolean()) == + tuple([boolean(), integer(), atom()]) - test "tuple_insert_at" do - assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badindex - assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex - assert tuple_insert_at(integer(), 0, boolean()) == :badtuple - assert tuple_insert_at(term(), 0, boolean()) == :badtuple - - # Out-of-bounds in a union - assert union(tuple([integer(), atom()]), tuple([float()])) - |> tuple_insert_at(2, boolean()) == :badindex - - # Test inserting into a closed tuple - assert tuple_insert_at(tuple([integer(), atom()]), 1, boolean()) == - tuple([integer(), boolean(), atom()]) - - # Test inserting at the beginning of a tuple - assert tuple_insert_at(tuple([integer(), atom()]), 0, boolean()) == - tuple([boolean(), integer(), atom()]) - - # Test inserting at the end of a tuple - assert tuple_insert_at(tuple([integer(), atom()]), 2, boolean()) == - tuple([integer(), atom(), boolean()]) - - # Test inserting into an empty tuple - assert tuple_insert_at(empty_tuple(), 0, integer()) == tuple([integer()]) - - # Test inserting into an open tuple - assert tuple_insert_at(open_tuple([integer(), atom()]), 1, boolean()) == - open_tuple([integer(), boolean(), atom()]) - - # Test inserting a dynamic type - assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == - dynamic(tuple([integer(), term(), atom()])) - - # Test inserting into a dynamic tuple - assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == - dynamic(tuple([integer(), boolean(), atom()])) - - # Test inserting into a union of tuples - assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == - union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) - - # Test inserting into a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) - |> tuple_insert_at(1, float()) - |> equal?(tuple([integer(), float(), atom(), boolean()])) - - # Test inserting into a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) - |> tuple_insert_at(1, boolean()) - |> equal?( - union( - tuple([integer(), boolean(), atom()]), - dynamic(tuple([float(), boolean(), binary()])) + # Test inserting at the end of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 2, boolean()) == + tuple([integer(), atom(), boolean()]) + + # Test inserting into an empty tuple + assert tuple_insert_at(empty_tuple(), 0, integer()) == tuple([integer()]) + + # Test inserting into an open tuple + assert tuple_insert_at(open_tuple([integer(), atom()]), 1, boolean()) == + open_tuple([integer(), boolean(), atom()]) + + # Test inserting a dynamic type + assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == + dynamic(tuple([integer(), term(), atom()])) + + # Test inserting into a dynamic tuple + assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == + dynamic(tuple([integer(), boolean(), atom()])) + + # Test inserting into a union of tuples + assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == + union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) + + # Test inserting into a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_insert_at(1, float()) + |> equal?(tuple([integer(), float(), atom(), boolean()])) + + # Test inserting into a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_insert_at(1, boolean()) + |> equal?( + union( + tuple([integer(), boolean(), atom()]), + dynamic(tuple([float(), boolean(), binary()])) + ) ) - ) - # If you successfully intersect at position index in a type, then the dynamic values - # that succeed are intersected with tuples of size at least index - assert dynamic(union(tuple(), integer())) - |> tuple_insert_at(1, boolean()) - |> equal?(dynamic(open_tuple([term(), boolean()]))) - end + # If you successfully intersect at position index in a type, then the dynamic values + # that succeed are intersected with tuples of size at least index + assert dynamic(union(tuple(), integer())) + |> tuple_insert_at(1, boolean()) + |> equal?(dynamic(open_tuple([term(), boolean()]))) + end - test "tuple_values" do - assert tuple_values(integer()) == :badtuple - assert tuple_values(tuple([])) == none() - assert tuple_values(tuple()) == term() - assert tuple_values(open_tuple([integer()])) == term() - assert tuple_values(tuple([integer(), atom()])) == union(integer(), atom()) + test "tuple_values" do + assert tuple_values(integer()) == :badtuple + assert tuple_values(tuple([])) == none() + assert tuple_values(tuple()) == term() + assert tuple_values(open_tuple([integer()])) == term() + assert tuple_values(tuple([integer(), atom()])) == union(integer(), atom()) - assert tuple_values(union(tuple([float(), pid()]), tuple([reference()]))) == - union(float(), union(pid(), reference())) + assert tuple_values(union(tuple([float(), pid()]), tuple([reference()]))) == + union(float(), union(pid(), reference())) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), term()]))) == - union(integer(), atom()) + assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), term()]))) == + union(integer(), atom()) - assert union(tuple([atom([:ok])]), open_tuple([integer()])) - |> difference(open_tuple([term(), term()])) - |> tuple_values() == union(atom([:ok]), integer()) + assert union(tuple([atom([:ok])]), open_tuple([integer()])) + |> difference(open_tuple([term(), term()])) + |> tuple_values() == union(atom([:ok]), integer()) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), atom([:ok])]))) == - union(number(), atom()) + assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), atom([:ok])]))) == + union(number(), atom()) - assert tuple_values(dynamic(tuple())) == dynamic() - assert tuple_values(dynamic(tuple([integer()]))) == dynamic(integer()) + assert tuple_values(dynamic(tuple())) == dynamic() + assert tuple_values(dynamic(tuple([integer()]))) == dynamic(integer()) - assert tuple_values(union(dynamic(tuple([integer()])), tuple([atom()]))) == - union(dynamic(integer()), atom()) + assert tuple_values(union(dynamic(tuple([integer()])), tuple([atom()]))) == + union(dynamic(integer()), atom()) - assert tuple_values(union(dynamic(tuple()), integer())) == :badtuple - assert tuple_values(dynamic(union(integer(), tuple([atom()])))) == dynamic(atom()) + assert tuple_values(union(dynamic(tuple()), integer())) == :badtuple + assert tuple_values(dynamic(union(integer(), tuple([atom()])))) == dynamic(atom()) - assert tuple_values(union(dynamic(tuple([integer()])), tuple([integer()]))) - |> equal?(integer()) - end + assert tuple_values(union(dynamic(tuple([integer()])), tuple([integer()]))) + |> equal?(integer()) + end - test "map_fetch" do - assert map_fetch(term(), :a) == :badmap - assert map_fetch(union(open_map(), integer()), :a) == :badmap + test "map_fetch" do + assert map_fetch(term(), :a) == :badmap + assert map_fetch(union(open_map(), integer()), :a) == :badmap - assert map_fetch(open_map(), :a) == :badkey - assert map_fetch(open_map(a: not_set()), :a) == :badkey - assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey - assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badkey + assert map_fetch(open_map(), :a) == :badkey + assert map_fetch(open_map(a: not_set()), :a) == :badkey + assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey + assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badkey - assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} + assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} - assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == - {false, union(integer(), atom())} + assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == + {false, union(integer(), atom())} - {false, value_type} = - open_map(my_map: open_map(foo: integer())) - |> intersection(open_map(my_map: open_map(bar: boolean()))) - |> map_fetch(:my_map) + {false, value_type} = + open_map(my_map: open_map(foo: integer())) + |> intersection(open_map(my_map: open_map(bar: boolean()))) + |> map_fetch(:my_map) - assert equal?(value_type, open_map(foo: integer(), bar: boolean())) + assert equal?(value_type, open_map(foo: integer(), bar: boolean())) - {false, value_type} = - closed_map(a: union(integer(), atom())) - |> difference(open_map(a: integer())) - |> map_fetch(:a) + {false, value_type} = + closed_map(a: union(integer(), atom())) + |> difference(open_map(a: integer())) + |> map_fetch(:a) - assert equal?(value_type, atom()) + assert equal?(value_type, atom()) - {false, value_type} = - closed_map(a: integer(), b: atom()) - |> difference(closed_map(a: integer(), b: atom([:foo]))) - |> map_fetch(:a) + {false, value_type} = + closed_map(a: integer(), b: atom()) + |> difference(closed_map(a: integer(), b: atom([:foo]))) + |> map_fetch(:a) - assert equal?(value_type, integer()) + assert equal?(value_type, integer()) - {false, value_type} = - closed_map(a: integer()) - |> difference(closed_map(a: atom())) - |> map_fetch(:a) + {false, value_type} = + closed_map(a: integer()) + |> difference(closed_map(a: atom())) + |> map_fetch(:a) - assert equal?(value_type, integer()) + assert equal?(value_type, integer()) - {false, value_type} = - open_map(a: integer(), b: atom()) - |> union(closed_map(a: tuple())) - |> map_fetch(:a) + {false, value_type} = + open_map(a: integer(), b: atom()) + |> union(closed_map(a: tuple())) + |> map_fetch(:a) - assert equal?(value_type, union(integer(), tuple())) + assert equal?(value_type, union(integer(), tuple())) - {false, value_type} = - closed_map(a: atom()) - |> difference(closed_map(a: atom([:foo, :bar]))) - |> difference(closed_map(a: atom([:bar]))) - |> map_fetch(:a) + {false, value_type} = + closed_map(a: atom()) + |> difference(closed_map(a: atom([:foo, :bar]))) + |> difference(closed_map(a: atom([:bar]))) + |> map_fetch(:a) - assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) + assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) - assert closed_map(a: union(atom(), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom(), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) - |> map_fetch(:a) == {false, pid()} + assert closed_map(a: union(atom(), pid()), b: integer(), c: tuple()) + |> difference(open_map(a: atom(), b: integer())) + |> difference(open_map(a: atom(), c: tuple())) + |> map_fetch(:a) == {false, pid()} - assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom([:foo]), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) - |> map_fetch(:a) == {false, pid()} + assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) + |> difference(open_map(a: atom([:foo]), b: integer())) + |> difference(open_map(a: atom(), c: tuple())) + |> map_fetch(:a) == {false, pid()} - assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) - |> difference(open_map(a: atom([:foo, :bar]))) - |> difference(open_map(a: atom([:foo, :baz]))) - |> map_fetch(:a) == {false, integer()} - end + assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) + |> difference(open_map(a: atom([:foo, :bar]))) + |> difference(open_map(a: atom([:foo, :baz]))) + |> map_fetch(:a) == {false, integer()} + end - 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 + 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())} + assert intersection(dynamic(), open_map(a: integer())) + |> map_fetch(:a) == {false, intersection(integer(), dynamic())} - {false, type} = union(dynamic(integer()), open_map(a: integer())) |> map_fetch(:a) - assert equal?(type, integer()) + {false, type} = union(dynamic(integer()), open_map(a: integer())) |> map_fetch(:a) + assert equal?(type, integer()) - assert union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch(:a) == :badkey + assert union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch(:a) == :badkey - assert union(dynamic(open_map(a: atom())), open_map(a: integer())) - |> map_fetch(:a) == {false, union(dynamic(atom()), integer())} + assert union(dynamic(open_map(a: atom())), open_map(a: integer())) + |> map_fetch(:a) == {false, union(dynamic(atom()), integer())} - # With domains - integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) - assert map_fetch(integer_to_atom, :foo) == :badkey + # With domains + integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) + assert map_fetch(integer_to_atom, :foo) == :badkey - # the key :a is for sure of type pid and exists in type - # %{atom() => pid()} and not %{:a => not_set()} - t1 = closed_map([{{:domain_key, :atom}, pid()}]) - t2 = closed_map(a: not_set()) - t3 = open_map(a: not_set()) + # the key :a is for sure of type pid and exists in type + # %{atom() => pid()} and not %{:a => not_set()} + t1 = closed_map([{{:domain_key, :atom}, pid()}]) + t2 = closed_map(a: not_set()) + t3 = open_map(a: not_set()) - # Indeed, t2 is equivalent to the empty map - assert map_fetch(difference(t1, t2), :a) == :badkey - assert map_fetch(difference(t1, t3), :a) == {false, pid()} + # Indeed, t2 is equivalent to the empty map + assert map_fetch(difference(t1, t2), :a) == :badkey + assert map_fetch(difference(t1, t3), :a) == {false, pid()} - t4 = closed_map([{{:domain_key, :pid}, atom()}]) - assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()} + t4 = closed_map([{{:domain_key, :pid}, atom()}]) + assert map_fetch(difference(t1, t4) |> difference(t3), :a) == {false, pid()} - assert map_fetch(closed_map([{{:domain_key, :atom}, pid()}]), :a) == :badkey + assert map_fetch(closed_map([{{:domain_key, :atom}, pid()}]), :a) == :badkey - assert map_fetch(dynamic(closed_map([{{:domain_key, :atom}, pid()}])), :a) == - {true, dynamic(pid())} + assert map_fetch(dynamic(closed_map([{{:domain_key, :atom}, pid()}])), :a) == + {true, dynamic(pid())} - assert closed_map([{{:domain_key, :atom}, number()}]) - |> difference(open_map(a: if_set(integer()))) - |> map_fetch(:a) == {false, float()} + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(open_map(a: if_set(integer()))) + |> map_fetch(:a) == {false, float()} - assert closed_map([{{:domain_key, :atom}, number()}]) - |> difference(closed_map(b: if_set(integer()))) - |> map_fetch(:a) == :badkey - end + assert closed_map([{{:domain_key, :atom}, number()}]) + |> difference(closed_map(b: if_set(integer()))) + |> map_fetch(:a) == :badkey + end - test "map_get" do test "map get" do assert map_get(term(), term()) == :badmap @@ -1360,7 +1360,10 @@ describe "projections" do # Deleting from a difference of maps {:ok, type} = - map_delete(difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())), :b) + map_delete( + difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())), + :b + ) assert equal?(type, closed_map(a: integer())) @@ -1460,11 +1463,15 @@ describe "projections" do assert equal?( type, - union(closed_map(a: integer(), c: boolean()), closed_map(b: atom(), c: boolean())) + union( + closed_map(a: integer(), c: boolean()), + closed_map(b: atom(), c: boolean()) + ) ) # Put a key-value pair in a dynamic map - assert map_put(dynamic(open_map()), :a, integer()) == {:ok, dynamic(open_map(a: integer()))} + assert map_put(dynamic(open_map()), :a, integer()) == + {:ok, dynamic(open_map(a: integer()))} # Put a key-value pair in an intersection of maps {:ok, type} = @@ -1524,7 +1531,10 @@ describe "projections" do assert map_refresh(empty_map(), number(), float()) == {:ok, - closed_map([{{:domain_key, :integer}, float()}, {{:domain_key, :float}, float()}])} + closed_map([ + {{:domain_key, :integer}, float()}, + {{:domain_key, :float}, float()} + ])} # Tricky cases with atoms: # We add one atom fields that maps to an integer, which is not :a. So we do not touch @@ -1648,7 +1658,8 @@ describe "projections" do "empty_list() or non_empty_list(float() or integer(), pid())" # Merge last element types - assert union(list(atom([:ok]), integer()), list(atom([:ok]), float())) |> to_quoted_string() == + assert union(list(atom([:ok]), integer()), list(atom([:ok]), float())) + |> to_quoted_string() == "empty_list() or non_empty_list(:ok, float() or integer())" assert union(dynamic(list(integer(), float())), dynamic(list(integer(), pid()))) @@ -1746,7 +1757,12 @@ describe "projections" do ) decimal_int = - closed_map(__struct__: atom([Decimal]), coef: integer(), exp: integer(), sign: integer()) + closed_map( + __struct__: atom([Decimal]), + coef: integer(), + exp: integer(), + sign: integer() + ) assert atom([:error]) |> union( @@ -1828,9 +1844,15 @@ describe "projections" do "%{..., a: float() or integer()}" # Fusing complex nested maps with unions - assert closed_map(status: atom([:ok]), data: closed_map(value: term(), count: empty_list())) + assert closed_map( + status: atom([:ok]), + data: closed_map(value: term(), count: empty_list()) + ) |> union( - closed_map(status: atom([:ok]), data: closed_map(value: term(), count: open_map())) + closed_map( + status: atom([:ok]), + data: closed_map(value: term(), count: open_map()) + ) ) |> union(closed_map(status: atom([:error]), reason: atom([:timeout]))) |> union(closed_map(status: atom([:error]), reason: atom([:crash]))) @@ -1854,7 +1876,10 @@ describe "projections" do "%{data: %{x: float() or integer(), y: atom()}, meta: map()}" # Test complex combinations - assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean())) + assert intersection( + open_map(a: number(), b: atom()), + open_map(a: integer(), c: boolean()) + ) |> union(difference(open_map(x: atom()), open_map(x: boolean()))) |> to_quoted_string() == "%{..., a: integer(), b: atom(), c: boolean()} or %{..., x: atom() and not boolean()}" From 75025dcfee747b327b18d27e11616c2404fdd48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 13 May 2025 12:41:17 +0200 Subject: [PATCH 6/6] Update lib/elixir/lib/module/types/descr.ex --- lib/elixir/lib/module/types/descr.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2100c5120ff..a7dbf36bf15 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -2699,8 +2699,7 @@ defmodule Module.Types.Descr do if((bitmap &&& @bit_float) != 0, do: :float), if((bitmap &&& @bit_pid) != 0, do: :pid), if((bitmap &&& @bit_port) != 0, do: :port), - if((bitmap &&& @bit_reference) != 0, do: :reference), - if((bitmap &&& @bit_fun) != 0, do: :fun) + if((bitmap &&& @bit_reference) != 0, do: :reference) ] |> Enum.reject(&is_nil/1) end