From eee8786a56f33312e2969def04ad028e01f0daaf Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 6 May 2025 14:33:54 +0200 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 From ce69a9fe9b33f0dcdb9b777d40edb0163f5ced22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Jun 2025 20:49:34 +0200 Subject: [PATCH 07/17] Update lib/elixir/test/elixir/module/types/descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index c5f547fe22c..39a6a7cf503 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1593,8 +1593,9 @@ defmodule Module.Types.DescrTest do assert union(dynamic(open_map(a: atom())), open_map(a: integer())) |> map_fetch(:a) == {false, union(dynamic(atom()), integer())} + end - # With domains + test "map_fetch with domain keys" do integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) assert map_fetch(integer_to_atom, :foo) == :badkey From 520f6c70886fb8cc325076ee73bb518452f12b6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Jun 2025 20:54:37 +0200 Subject: [PATCH 08/17] Apply suggestions from code review --- lib/elixir/test/elixir/module/types/descr_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 39a6a7cf503..099f39e40c5 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -283,7 +283,7 @@ defmodule Module.Types.DescrTest do assert empty?(intersection(closed_map(a: integer()), closed_map(a: atom()))) end - test "map with domain key types" do + test "map with domain keys" 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()}]) @@ -533,7 +533,7 @@ defmodule Module.Types.DescrTest do |> equal?(open_map(a: integer())) end - test "map with domain key types" do + test "map with domain keys" do # Non-overlapping domain keys t1 = closed_map([{{:domain_key, :integer}, atom()}]) t2 = closed_map([{{:domain_key, :atom}, binary()}]) @@ -1881,7 +1881,7 @@ defmodule Module.Types.DescrTest do assert equal?(type, atom()) end - test "map put with key type" do + test "map_put with domain keys" do # Using a literal key or an expression of that singleton key is the same assert map_refresh(empty_map(), atom([:a]), integer()) == {:ok, closed_map(a: integer())} From 82789facadab1970d684787ecfea773f6bc7af4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Jun 2025 21:02:27 +0200 Subject: [PATCH 09/17] Update lib/elixir/test/elixir/module/types/descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 099f39e40c5..71da63ba5d7 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1739,8 +1739,9 @@ 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())) + end - ## Delete from maps with domain + test "map_delete with atom fallback" do 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()}])} From 251b5247a3fc30c8876f1e02ce11cfaa861a45af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Jun 2025 21:03:51 +0200 Subject: [PATCH 10/17] Update lib/elixir/test/elixir/module/types/descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 71da63ba5d7..f798bef760a 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1746,7 +1746,6 @@ defmodule Module.Types.DescrTest do |> map_delete(:a) == {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {{:domain_key, :atom}, pid()}])} end - # TODO: operator t\[t'] # test "map_delete with domain keys" do # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) From 31d243a487754c28e12910302d52638b1fa9b6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Jun 2025 21:50:41 +0200 Subject: [PATCH 11/17] Update lib/elixir/test/elixir/module/types/descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index f798bef760a..a55701fef3d 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1746,6 +1746,7 @@ defmodule Module.Types.DescrTest do |> map_delete(:a) == {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {{:domain_key, :atom}, pid()}])} end + # TODO: operator t\[t'] # test "map_delete with domain keys" do # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) From 586288d3ebf6f6627411cc10de9892f29d7c294b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Jun 2025 14:11:22 -0700 Subject: [PATCH 12/17] Use macro for domain_key encapsulation --- lib/elixir/lib/module/types/descr.ex | 66 ++++---- .../test/elixir/module/types/descr_test.exs | 156 +++++++++--------- 2 files changed, 113 insertions(+), 109 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 6ae982954dd..87e1ad39664 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -26,6 +26,8 @@ defmodule Module.Types.Descr do @bit_top (1 <<< 7) - 1 @bit_number @bit_integer ||| @bit_float + defmacrop domain_key(key), do: {:domain_key, key} + @domain_key_types [ :binary, :empty_list, @@ -109,7 +111,7 @@ defmodule Module.Types.Descr do :closed, [], Enum.map(@domain_key_types, fn key_type -> - {{:domain_key, key_type}, if_set(default)} + {domain_key(key_type), if_set(default)} end) ) end @@ -2284,8 +2286,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({: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()) + 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) @@ -2520,15 +2522,15 @@ defmodule Module.Types.Descr do 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) + 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) + Map.put(acc_domains, domain_key(domain_key), inter) end end @@ -2853,17 +2855,17 @@ defmodule Module.Types.Descr do :open :closed -> - {:closed, %{{:domain_key, domain} => if_set(type)}} + {: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))} + 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))} + {:closed, Map.update(domains, domain_key(domain), if_set(type), &union(&1, type))} end end @@ -3024,7 +3026,7 @@ defmodule Module.Types.Descr do 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) + Map.get(domains, domain_key(key_type), tag_to_type(tag)) |> union(acc) end) end @@ -3116,7 +3118,7 @@ defmodule Module.Types.Descr do 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 -> + {{_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. @@ -3323,9 +3325,9 @@ defmodule Module.Types.Descr do true {{:closed, pos_domains}, {:closed, neg_domains}} -> - Enum.all?(pos_domains, fn {{:domain_key, key}, type} -> + Enum.all?(pos_domains, fn {domain_key(key), type} -> subtype?(type, not_set()) || - case Map.get(neg_domains, {:domain_key, key}) do + case Map.get(neg_domains, domain_key(key)) do nil -> false neg_type -> subtype?(type, neg_type) end @@ -3333,8 +3335,8 @@ defmodule Module.Types.Descr do # 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 + 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) @@ -3345,8 +3347,8 @@ defmodule Module.Types.Descr do {{: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 + 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) @@ -3356,20 +3358,20 @@ defmodule Module.Types.Descr do # 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 = 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}))) + 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 + 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 @@ -3378,7 +3380,7 @@ defmodule Module.Types.Descr do # 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} -> + Enum.all?(neg_domains, fn {domain_key(_), type} -> subtype?(term_or_optional(), type) end) @@ -3386,7 +3388,7 @@ defmodule Module.Types.Descr do {: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 + case Map.get(neg_domains, domain_key(domain_key)) do # Not all domain keys are present nil -> false type -> subtype?(term_or_optional(), type) @@ -3396,7 +3398,7 @@ defmodule Module.Types.Descr do # 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} -> + Enum.all?(pos_domains, fn {domain_key(_), type} -> subtype?(type, not_set()) end) @@ -3404,7 +3406,7 @@ defmodule Module.Types.Descr do {{: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 + case Map.get(pos_domains, domain_key(domain_key)) do # Not all domain keys are present nil -> false type -> subtype?(type, not_set()) @@ -3424,7 +3426,7 @@ defmodule Module.Types.Descr do # 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 + 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 @@ -3583,7 +3585,7 @@ defmodule Module.Types.Descr do def map_literal_to_quoted({{:closed, domains}, fields}, opts) do domain_fields = - for {{:domain_key, domain_type}, value_type} <- domains do + for {domain_key(domain_type), value_type} <- domains do key = {:string, [], ["#{domain_type}() => "]} {key, to_quoted(value_type, opts)} end @@ -3594,7 +3596,7 @@ defmodule Module.Types.Descr do def map_literal_to_quoted({{:open, domains}, fields}, opts) do domain_fields = - for {{:domain_key, domain_type}, value_type} <- domains do + for {domain_key(domain_type), value_type} <- domains do key = {:string, [], ["#{domain_type}() => "]} {key, to_quoted(value_type, opts)} end @@ -4612,14 +4614,14 @@ defmodule Module.Types.Descr do # Helpers for domain key validation defp split_domain_key_pairs(pairs) do Enum.split_with(pairs, fn - {{:domain_key, _}, _} -> false + {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) + 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" diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index a55701fef3d..a951ece5bf2 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -14,6 +14,7 @@ end defmodule Module.Types.DescrTest do use ExUnit.Case, async: true + defmacrop domain_key(key), do: {:domain_key, key} import Module.Types.Descr, except: [fun: 1] describe "union" do @@ -102,27 +103,27 @@ defmodule Module.Types.DescrTest do assert equal?(union(closed_map(a: integer()), a_integer_open), a_integer_open) # Domain key types - atom_to_atom = open_map([{{:domain_key, :atom}, atom()}]) - atom_to_integer = open_map([{{:domain_key, :atom}, integer()}]) + 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())}]) + 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())}])) + |> 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(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() + 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() + assert union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map() end test "list" do @@ -286,56 +287,56 @@ defmodule Module.Types.DescrTest do test "map with domain keys" 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()}]) + 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()}]) + 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()}]) + 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()} + {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()}]) + 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()} + {domain_key(:integer), intersection(integer(), float())}, + {domain_key(:binary), none()} ]) ) - assert subtype?(empty_map(), closed_map([{{:domain_key, :integer}, atom()}])) + assert subtype?(empty_map(), closed_map([{domain_key(:integer), atom()}])) - t1 = closed_map([{{:domain_key, :integer}, atom()}]) - t2 = closed_map([{{:domain_key, :integer}, binary()}]) + 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()}]) + 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)) @@ -535,30 +536,30 @@ defmodule Module.Types.DescrTest do test "map with domain keys" do # Non-overlapping domain keys - t1 = closed_map([{{:domain_key, :integer}, atom()}]) - t2 = closed_map([{{:domain_key, :atom}, binary()}]) + 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])}]) + 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]))}])) + |> 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())}]) + 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()}]) + 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. @@ -687,8 +688,8 @@ defmodule Module.Types.DescrTest do 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())}]) + t1 = dynamic(open_map([{domain_key(:integer), integer()}])) + t2 = open_map([{domain_key(:integer), dynamic(integer())}]) assert t1 == t2 # if_set on dynamic fields also must work @@ -753,8 +754,8 @@ defmodule Module.Types.DescrTest do 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()}]) + t1 = closed_map([{domain_key(:integer), number()}]) + t2 = closed_map([{domain_key(:integer), integer()}]) assert subtype?(t2, t1) @@ -1596,12 +1597,12 @@ defmodule Module.Types.DescrTest do end test "map_fetch with domain keys" do - integer_to_atom = open_map([{{:domain_key, :integer}, atom()}]) + 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()}]) + t1 = closed_map([{domain_key(:atom), pid()}]) t2 = closed_map(a: not_set()) t3 = open_map(a: not_set()) @@ -1609,19 +1610,19 @@ defmodule Module.Types.DescrTest do assert map_fetch(difference(t1, t2), :a) == :badkey assert map_fetch(difference(t1, t3), :a) == {false, pid()} - t4 = closed_map([{{:domain_key, :pid}, atom()}]) + 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) == + assert map_fetch(dynamic(closed_map([{domain_key(:atom), pid()}])), :a) == {true, dynamic(pid())} - assert closed_map([{{:domain_key, :atom}, number()}]) + 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()}]) + assert closed_map([{domain_key(:atom), number()}]) |> difference(closed_map(b: if_set(integer()))) |> map_fetch(:a) == :badkey end @@ -1629,7 +1630,7 @@ defmodule Module.Types.DescrTest do test "map_get with domain keys" do assert map_get(term(), term()) == :badmap - map_type = closed_map([{{:domain_key, :tuple}, binary()}]) + map_type = closed_map([{domain_key(:tuple), binary()}]) assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())} # Type with all domain types @@ -1637,15 +1638,15 @@ defmodule Module.Types.DescrTest do 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()} + {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])} @@ -1663,16 +1664,16 @@ defmodule Module.Types.DescrTest do {: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()}])) + 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()}]) + 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 @@ -1683,7 +1684,7 @@ defmodule Module.Types.DescrTest do end test "map_get with atom fall back" do - map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {{:domain_key, :atom}, pid()}]) + 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()} @@ -1742,23 +1743,23 @@ defmodule Module.Types.DescrTest do end test "map_delete with atom fallback" do - assert closed_map([{:a, integer()}, {:b, atom()}, {{:domain_key, :atom}, pid()}]) + 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()}])} + {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {domain_key(:atom), pid()}])} end # TODO: operator t\[t'] # test "map_delete with domain keys" do - # t1 = closed_map([{:a, pid()}, {{:domain_key, :integer}, number()}]) + # 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()}])) + # |> 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()}])) + # |> 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()}])) + # |> equal?(closed_map([{:a, if_set(pid())}, {domain_key(:integer), number()}])) # end test "map_take" do @@ -1891,34 +1892,35 @@ defmodule Module.Types.DescrTest do {:ok, closed_map(a: if_set(integer()), b: if_set(integer()))} assert map_refresh(empty_map(), integer(), integer()) == - {:ok, closed_map([{{:domain_key, :integer}, integer()}])} + {:ok, closed_map([{domain_key(:integer), integer()}])} - assert map_refresh(closed_map([{{:domain_key, :integer}, integer()}]), integer(), float()) == - {:ok, closed_map([{{:domain_key, :integer}, number()}])} + assert map_refresh(closed_map([{domain_key(:integer), integer()}]), integer(), float()) == + {:ok, closed_map([{domain_key(:integer), number()}])} assert map_refresh(open_map(), integer(), integer()) == {:ok, open_map()} - {:ok, type} = map_refresh(empty_map(), integer(), dynamic()) - assert equal?(type, dynamic(closed_map([{{:domain_key, :integer}, term()}]))) + # TODO: Revisit this + # {: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_refresh(dynamic(), float(), float()) assert equal?(type, dynamic(open_map())) - assert closed_map([{{:domain_key, :integer}, integer()}]) + assert closed_map([{domain_key(:integer), integer()}]) |> difference(open_map()) |> empty?() - assert closed_map([{{:domain_key, :integer}, integer()}]) + assert closed_map([{domain_key(:integer), integer()}]) |> difference(open_map()) |> map_refresh(integer(), float()) == :badmap assert map_refresh(empty_map(), number(), float()) == {:ok, closed_map([ - {{:domain_key, :integer}, float()}, - {{:domain_key, :float}, float()} + {domain_key(:integer), float()}, + {domain_key(:float), float()} ])} # Tricky cases with atoms: @@ -1933,7 +1935,7 @@ defmodule Module.Types.DescrTest do closed_map([ {:a, pid()}, {:b, union(pid(), integer())}, - {{:domain_key, :atom}, integer()} + {domain_key(:atom), integer()} ])} assert map_refresh(empty_map(), term(), integer()) == {:ok, map_with_default(integer())} From 85be15caae044723ef09be7e591539a5ef11c593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Jun 2025 14:59:43 -0700 Subject: [PATCH 13/17] Optimize intersection of domain and key types --- lib/elixir/lib/module/types/descr.ex | 170 ++++++++++++++++----------- 1 file changed, 100 insertions(+), 70 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 87e1ad39664..01c5bf2b143 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -2284,10 +2284,46 @@ defmodule Module.Types.Descr do {acc, dynamic?} end + # TODO: Rename this to tuple_tag_to_type defp tag_to_type(:open), do: term_or_optional() defp tag_to_type(:closed), do: not_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()) + + # Rename this to map_key_tag_to_type + defp map_key_tag_to_type(:open), do: term_or_optional() + defp map_key_tag_to_type(:closed), do: not_set() + defp map_key_tag_to_type({:closed, domain}), do: Map.get(domain, domain_key(:atom), not_set()) + + defp map_key_tag_to_type({:open, domain}), + do: Map.get(domain, domain_key(:atom), term_or_optional()) + + # Helpers for domain key validation + # TODO: Merge this and the next clause into one + 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 defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :optional) @@ -2474,8 +2510,8 @@ defmodule Module.Types.Descr 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) + default1 = map_key_tag_to_type(tag1) + default2 = map_key_tag_to_type(tag2) # Compute the new domain tag = map_domain_intersection(tag1, tag2) @@ -2486,27 +2522,10 @@ defmodule Module.Types.Descr do # 3. If key is only in map2, compute non empty intersection with atom1 # 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) - - # 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} + {tag, + symmetrical_merge(map1, default1, map2, default2, fn _key, v1, v2 -> + non_empty_intersection!(v1, v2) + end)} end # Compute the intersection of two tags or tag-domain pairs. @@ -2516,8 +2535,8 @@ defmodule Module.Types.Descr do 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) + default1 = map_key_tag_to_type(tag1) + default2 = map_key_tag_to_type(tag2) new_domains = for domain_key <- @domain_key_types, reduce: %{} do @@ -2568,7 +2587,7 @@ defmodule Module.Types.Descr do {:open, fields2, []}, dnf1 when map_size(fields2) == 1 -> Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc -> {key, value, _rest} = :maps.next(:maps.iterator(fields2)) - t_diff = difference(Map.get(fields1, key, tag_to_type(tag1)), value) + t_diff = difference(Map.get(fields1, key, map_key_tag_to_type(tag1)), value) if empty?(t_diff) do acc @@ -2641,7 +2660,11 @@ 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 - tag_to_type(tag) |> pop_optional_static() + case tag do + :open -> {true, term()} + :closed -> {true, none()} + other -> map_key_tag_to_type(other) |> pop_optional_static() + end end # Takes a map dnf and returns the union of types it can take for a given key. @@ -2655,7 +2678,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives and the key does not exist, return the default one. {tag, %{}, []}, acc -> - tag_to_type(tag) |> union(acc) + map_key_tag_to_type(tag) |> union(acc) {tag, fields, negs}, acc -> {fst, snd} = map_pop_key(tag, fields, key) @@ -3026,7 +3049,7 @@ defmodule Module.Types.Descr do 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) + Map.get(domains, domain_key(key_type), map_key_tag_to_type(tag)) |> union(acc) end) end @@ -3115,7 +3138,7 @@ defmodule Module.Types.Descr do dnf |> Enum.reduce(none(), fn {tag, _fields, []}, acc when is_atom(tag) -> - tag_to_type(tag) |> union(acc) + map_key_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 -> @@ -3123,7 +3146,7 @@ defmodule Module.Types.Descr do # 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) + map_key_tag_to_type(tag) |> union(acc) {tag, fields, negs}, acc -> {fst, snd} = map_pop_domain(tag, fields, key_domain) @@ -3252,8 +3275,8 @@ defmodule Module.Types.Descr do defp map_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do if map_check_domain_keys(tag, neg_tag) do - atom_default = tag_to_type(tag) - neg_atom_default = tag_to_type(neg_tag) + atom_default = map_key_tag_to_type(tag) + neg_atom_default = map_key_tag_to_type(neg_tag) (Enum.all?(neg_fields, fn {neg_key, neg_type} -> cond do @@ -3418,7 +3441,7 @@ defmodule Module.Types.Descr do defp map_pop_key(tag, fields, key) do case :maps.take(key, fields) do {value, fields} -> {value, %{map: map_new(tag, fields)}} - :error -> {tag_to_type(tag), %{map: map_new(tag, fields)}} + :error -> {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}} end end @@ -3428,12 +3451,12 @@ defmodule Module.Types.Descr do 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)}} + :error -> {map_key_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)}} + do: {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}} defp map_split_negative(negs, key) do Enum.reduce_while(negs, [], fn @@ -4532,9 +4555,9 @@ defmodule Module.Types.Descr do ## Map helpers + # Erlang maps:merge_with/3 has to preserve the order in combiner. + # We don't care about the order, so we have a faster implementation. defp symmetrical_merge(left, right, fun) do - # Erlang maps:merge_with/3 has to preserve the order in combiner. - # We don't care about the order, so we have a faster implementation. if map_size(left) > map_size(right) do iterator_merge(:maps.next(:maps.iterator(right)), left, fun) else @@ -4554,9 +4577,44 @@ defmodule Module.Types.Descr do defp iterator_merge(:none, map, _fun), do: map + # Perform a symmetrical merge with default values + defp symmetrical_merge(left, left_default, right, right_default, fun) do + iterator = :maps.next(:maps.iterator(left)) + iterator_merge_left(iterator, left_default, right, right_default, %{}, fun) + end + + defp iterator_merge_left({key, v1, iterator}, v1_default, map, v2_default, acc, fun) do + value = + case map do + %{^key => v2} -> fun.(key, v1, v2) + %{} -> fun.(key, v1, v2_default) + end + + acc = Map.put(acc, key, value) + iterator_merge_left(:maps.next(iterator), v1_default, map, v2_default, acc, fun) + end + + defp iterator_merge_left(:none, v1_default, map, _v2_default, acc, fun) do + iterator_merge_right(:maps.next(:maps.iterator(map)), v1_default, acc, fun) + end + + defp iterator_merge_right({key, v2, iterator}, v1_default, acc, fun) do + acc = + case acc do + %{^key => _} -> acc + %{} -> Map.put(acc, key, fun.(key, v1_default, v2)) + end + + iterator_merge_right(:maps.next(iterator), v1_default, acc, fun) + end + + defp iterator_merge_right(:none, _v1_default, acc, _fun) do + acc + end + + # Erlang maps:intersect_with/3 has to preserve the order in combiner. + # We don't care about the order, so we have a faster implementation. defp symmetrical_intersection(left, right, fun) do - # Erlang maps:intersect_with/3 has to preserve the order in combiner. - # We don't care about the order, so we have a faster implementation. if map_size(left) > map_size(right) do iterator_intersection(:maps.next(:maps.iterator(right)), left, [], fun) else @@ -4610,32 +4668,4 @@ 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 From 00f593b9b1e16f3ed0c32b3eddb3f4a66fe66394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 22 Jun 2025 17:41:12 -0700 Subject: [PATCH 14/17] Initial work on removing tag from domain handling --- lib/elixir/lib/module/types/descr.ex | 53 ++++++++----------- .../test/elixir/module/types/descr_test.exs | 18 +++---- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 01c5bf2b143..c5dde65a5c1 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -63,6 +63,11 @@ defmodule Module.Types.Descr do @not_non_empty_list Map.delete(@term, :list) @not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list) + @not_set %{optional: 1} + @term_or_optional Map.put(@term, :optional, 1) + @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1}) + @not_atom_or_optional Map.delete(@term_or_optional, :atom) + @empty_intersection [0, @none] @empty_difference [0, []] @@ -88,12 +93,11 @@ defmodule Module.Types.Descr do 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) + else: map_descr(domain_pairs, regular_pairs) end def empty_list(), do: %{bitmap: @bit_empty_list} @@ -104,26 +108,20 @@ defmodule Module.Types.Descr do 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: open_map(pairs, @term_or_optional, false) + def open_map(pairs, default), do: open_map(pairs, if_set(default), true) - @doc "A map (closed or open is the same) with a default type %{term() => default}" - def map_with_default(default) do - map_descr( - :closed, - [], - Enum.map(@domain_key_types, fn key_type -> - {domain_key(key_type), if_set(default)} - end) - ) - end - - def open_map(pairs) do + defp open_map(pairs, default, force?) 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) + if domain_pairs != [] or force?, + do: + Map.new(@domain_key_types, fn key_type -> {domain_key(key_type), default} end) + |> Map.merge(Map.new(domain_pairs)) + |> Map.to_list() + |> map_descr(regular_pairs), + else: map_descr(:open, regular_pairs) end def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements) @@ -289,16 +287,11 @@ defmodule Module.Types.Descr do # is equivalent to `%{:foo => integer() or not_set()}`. # # `not_set()` has no meaning outside of map types. - - @not_set %{optional: 1} - @term_or_optional Map.put(@term, :optional, 1) - @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1}) - @not_atom_or_optional Map.delete(@term_or_optional, :atom) - def not_set(), do: @not_set + def if_set(:term), do: term_or_optional() - # actually, if type contains a :dynamic part, :optional gets added there because - # the dynamic + + # If type contains a :dynamic part, :optional gets added there. def if_set(type) do case type do %{dynamic: dyn} -> Map.put(%{type | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1) @@ -2245,7 +2238,7 @@ defmodule Module.Types.Descr do # 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 + defp map_descr(tag, fields) when is_atom(tag) do case map_descr_pairs(fields, [], false) do {fields, true} -> %{dynamic: %{map: map_new(tag, fields |> Enum.reverse() |> :maps.from_list())}} @@ -2255,7 +2248,7 @@ defmodule Module.Types.Descr do end end - def map_descr(tag, fields, domains) do + defp map_descr(domains, fields) do {fields, fields_dynamic?} = map_descr_pairs(fields, [], false) {domains, domains_dynamic?} = map_descr_pairs(domains, [], false) @@ -2263,9 +2256,9 @@ defmodule Module.Types.Descr do 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)}} + %{dynamic: %{map: map_new(:closed, fields_map, domains_map)}} else - %{map: map_new(tag, fields_map, domains_map)} + %{map: map_new(:closed, fields_map, domains_map)} 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 a951ece5bf2..4329f44adb4 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -14,9 +14,17 @@ end defmodule Module.Types.DescrTest do use ExUnit.Case, async: true - defmacrop domain_key(key), do: {:domain_key, key} import Module.Types.Descr, except: [fun: 1] + defmacrop domain_key(key), do: {:domain_key, key} + + defp number(), do: union(integer(), float()) + defp empty_tuple(), do: tuple([]) + defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n)) + defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n)) + defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) + defp map_with_default(descr), do: open_map([], if_set(descr)) + describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer()) @@ -343,8 +351,6 @@ defmodule Module.Types.DescrTest do assert equal?(intersection(t1, t2), empty_map()) end - defp number(), do: union(integer(), float()) - test "list" do assert intersection(list(term()), list(term())) == list(term()) assert intersection(list(integer()), list(integer())) == list(integer()) @@ -456,10 +462,6 @@ defmodule Module.Types.DescrTest do assert empty?(difference(dynamic(integer()), integer())) end - defp empty_tuple(), do: tuple([]) - defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n)) - defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n)) - test "tuple" do assert empty?(difference(open_tuple([atom()]), open_tuple([term()]))) refute empty?(difference(tuple(), empty_tuple())) @@ -581,8 +583,6 @@ defmodule Module.Types.DescrTest do 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)) - test "list" do # Basic list type differences assert difference(list(term()), empty_list()) == non_empty_list(term()) From d76694fdc717077ad6924f6fde239c1db36bc8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Jun 2025 09:59:52 +0200 Subject: [PATCH 15/17] Provide already mapped domain keys --- lib/elixir/lib/module/types/descr.ex | 218 +++++++++++---------------- 1 file changed, 89 insertions(+), 129 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index c5dde65a5c1..69cb22a024e 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -29,18 +29,18 @@ defmodule Module.Types.Descr do defmacrop domain_key(key), do: {:domain_key, key} @domain_key_types [ - :binary, - :empty_list, - :integer, - :float, - :pid, - :port, - :reference, - :fun, - :atom, - :tuple, - :map, - :list + {:domain_key, :binary}, + {:domain_key, :empty_list}, + {:domain_key, :integer}, + {:domain_key, :float}, + {:domain_key, :pid}, + {:domain_key, :port}, + {:domain_key, :reference}, + {:domain_key, :fun}, + {:domain_key, :atom}, + {:domain_key, :tuple}, + {:domain_key, :map}, + {:domain_key, :list} ] @fun_top :fun_top @@ -90,40 +90,16 @@ 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 - {regular_pairs, domain_pairs} = split_domain_key_pairs(pairs) - domain_pairs = validate_domain_keys(domain_pairs) - - if domain_pairs == [], - do: map_descr(:closed, regular_pairs), - else: map_descr(domain_pairs, regular_pairs) - end - + def closed_map(pairs), do: map_descr(:closed, pairs, @term_or_optional, false) def empty_list(), do: %{bitmap: @bit_empty_list} def empty_map(), do: %{map: @map_empty} def integer(), do: %{bitmap: @bit_integer} def float(), do: %{bitmap: @bit_float} 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: open_map(pairs, @term_or_optional, false) - def open_map(pairs, default), do: open_map(pairs, if_set(default), true) - - defp open_map(pairs, default, force?) do - {regular_pairs, domain_pairs} = split_domain_key_pairs(pairs) - domain_pairs = validate_domain_keys(domain_pairs) - - if domain_pairs != [] or force?, - do: - Map.new(@domain_key_types, fn key_type -> {domain_key(key_type), default} end) - |> Map.merge(Map.new(domain_pairs)) - |> Map.to_list() - |> map_descr(regular_pairs), - else: map_descr(:open, regular_pairs) - end - + def open_map(pairs), do: map_descr(:open, pairs, @term_or_optional, false) + def open_map(pairs, default), do: map_descr(:open, pairs, if_set(default), true) def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements) def pid(), do: %{bitmap: @bit_pid} def port(), do: %{bitmap: @bit_port} @@ -2238,43 +2214,56 @@ defmodule Module.Types.Descr do # 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) when is_atom(tag) do - case map_descr_pairs(fields, [], false) do - {fields, true} -> - %{dynamic: %{map: map_new(tag, fields |> Enum.reverse() |> :maps.from_list())}} + defp map_descr(tag, pairs, default, force?) do + {fields, domains, dynamic?} = map_descr_pairs(pairs, [], %{}, false) - {_, false} -> - %{map: map_new(tag, :maps.from_list(fields))} + map_new = + if domains != %{} or force? do + domains = + if tag == :open do + Enum.reduce(@domain_key_types, domains, &Map.put_new(&2, &1, default)) + else + domains + end + + map_new({tag, domains}, fields) + else + map_new(tag, fields) + end + + case dynamic? do + true -> %{dynamic: %{map: map_new}} + false -> %{map: map_new} end end - defp map_descr(domains, fields) 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) + # TOD: Double check if we indeed want the union here + defp map_put_domain(domain, key, value) do + Map.update(domain, key, if_set(value), &union(&1, value)) + end - if fields_dynamic? or domains_dynamic? do - %{dynamic: %{map: map_new(:closed, fields_map, domains_map)}} - else - %{map: map_new(:closed, fields_map, domains_map)} + defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do + case is_atom(key) do + true -> map_descr_pairs(rest, [{key, :term} | fields], domain, dynamic?) + false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, :term), dynamic?) end end - defp map_descr_pairs([{key, :term} | rest], acc, dynamic?) do - map_descr_pairs(rest, [{key, :term} | acc], dynamic?) - end + defp map_descr_pairs([{key, value} | rest], fields, domain, dynamic?) do + {value, dynamic?} = + case :maps.take(:dynamic, value) do + :error -> {value, dynamic?} + {dynamic, _static} -> {dynamic, true} + end - defp map_descr_pairs([{key, value} | rest], acc, dynamic?) do - case :maps.take(:dynamic, value) do - :error -> map_descr_pairs(rest, [{key, value} | acc], dynamic?) - {dynamic, _static} -> map_descr_pairs(rest, [{key, dynamic} | acc], true) + case is_atom(key) do + true -> map_descr_pairs(rest, [{key, value} | fields], domain, dynamic?) + false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, value), dynamic?) end end - defp map_descr_pairs([], acc, dynamic?) do - {acc, dynamic?} + defp map_descr_pairs([], fields, domain, dynamic?) do + {fields |> Enum.reverse() |> :maps.from_list(), domain, dynamic?} end # TODO: Rename this to tuple_tag_to_type @@ -2289,40 +2278,10 @@ defmodule Module.Types.Descr do defp map_key_tag_to_type({:open, domain}), do: Map.get(domain, domain_key(:atom), term_or_optional()) - # Helpers for domain key validation - # TODO: Merge this and the next clause into one - 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 - 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)) @@ -2534,15 +2493,15 @@ defmodule Module.Types.Descr do 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) + type1 = Map.get(domains1, domain_key, default1) + type2 = Map.get(domains2, domain_key, default2) inter = intersection(type1, type2) if empty?(inter) do acc_domains else - Map.put(acc_domains, domain_key(domain_key), inter) + Map.put(acc_domains, domain_key, inter) end end @@ -2781,8 +2740,8 @@ defmodule Module.Types.Descr do {:atom, atom_key}, acc -> map_refresh_atom(acc, atom_key, type) - key, acc -> - map_refresh_domain(acc, key, type) + domain_key, acc -> + map_refresh_domain(acc, domain_key, type) end) {:ok, new_descr} @@ -2861,27 +2820,27 @@ defmodule Module.Types.Descr do considered_keys |> :sets.to_list() |> Enum.reduce(descr, fn key, acc -> map_refresh_key(acc, key, type) end) - |> map_refresh_domain(:atom, type) + |> map_refresh_domain(domain_key(:atom), type) end end - def map_refresh_tag(tag, domain, type) do + def map_refresh_tag(tag, domain_key, type) do case tag do :open -> :open :closed -> - {:closed, %{domain_key(domain) => if_set(type)}} + {:closed, %{domain_key => if_set(type)}} {:open, domains} -> - if Map.has_key?(domains, domain_key(domain)) do - {:open, Map.update!(domains, domain_key(domain), &union(&1, type))} + if Map.has_key?(domains, domain_key) do + {:open, Map.update!(domains, domain_key, &union(&1, type))} else {:open, domains} end {:closed, domains} -> - {:closed, Map.update(domains, domain_key(domain), if_set(type), &union(&1, type))} + {:closed, Map.update(domains, domain_key, if_set(type), &union(&1, type))} end end @@ -3007,21 +2966,22 @@ defmodule Module.Types.Descr do 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] + not empty?(%{type_kind => type}) -> [domain_key(type_kind) | acc] true -> acc end end end + # TODO: Optimize this 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_binary) != 0, do: domain_key(:binary)), + if((bitmap &&& @bit_empty_list) != 0, do: domain_key(:empty_list)), + if((bitmap &&& @bit_integer) != 0, do: domain_key(:integer)), + if((bitmap &&& @bit_float) != 0, do: domain_key(:float)), + if((bitmap &&& @bit_pid) != 0, do: domain_key(:pid)), + if((bitmap &&& @bit_port) != 0, do: domain_key(:port)), + if((bitmap &&& @bit_reference) != 0, do: domain_key(:reference)) ] |> Enum.reject(&is_nil/1) end @@ -3042,7 +3002,7 @@ defmodule Module.Types.Descr do key_type, acc -> # Note: we could stop if we reach term()_or_optional() - Map.get(domains, domain_key(key_type), map_key_tag_to_type(tag)) |> union(acc) + Map.get(domains, key_type, map_key_tag_to_type(tag)) |> union(acc) end) end @@ -3053,8 +3013,8 @@ defmodule Module.Types.Descr do {:atom, atom_type}, acc -> map_get_atom(dnf, atom_type) |> union(acc) - key_type, acc -> - map_get_domain(dnf, key_type) |> union(acc) + domain_key, acc -> + map_get_domain(dnf, domain_key) |> union(acc) end) end @@ -3107,7 +3067,7 @@ defmodule Module.Types.Descr do union(type, acc) end end) - |> union(map_get_domain(dnf, :atom)) + |> union(map_get_domain(dnf, domain_key(:atom))) end end @@ -3127,14 +3087,14 @@ defmodule Module.Types.Descr do end # Take a map dnf and return the union of types for the given key domain. - def map_get_domain(dnf, key_domain) when is_atom(key_domain) do + def map_get_domain(dnf, domain_key(_) = domain_key) do dnf |> Enum.reduce(none(), fn {tag, _fields, []}, acc when is_atom(tag) -> map_key_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 -> + {{_tag, %{^domain_key => value}}, _fields, []}, acc -> value |> union(acc) # Optimization: if there are no negatives and the key does not exist, return the default type. @@ -3142,9 +3102,9 @@ defmodule Module.Types.Descr do map_key_tag_to_type(tag) |> union(acc) {tag, fields, negs}, acc -> - {fst, snd} = map_pop_domain(tag, fields, key_domain) + {fst, snd} = map_pop_domain(tag, fields, domain_key) - case map_split_negative_domain(negs, key_domain) do + case map_split_negative_domain(negs, domain_key) do :empty -> acc @@ -3374,12 +3334,12 @@ defmodule Module.Types.Descr do # 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 = Map.has_key?(neg_domains, domain_key) + pos_has_key = Map.has_key?(pos_domains, domain_key) domain_key_present && (pos_has_key || - subtype?(term_or_optional(), Map.get(neg_domains, domain_key(domain_key)))) + subtype?(term_or_optional(), Map.get(neg_domains, domain_key))) end) positive_check && negative_check @@ -3404,7 +3364,7 @@ defmodule Module.Types.Descr do {: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 + case Map.get(neg_domains, domain_key) do # Not all domain keys are present nil -> false type -> subtype?(term_or_optional(), type) @@ -3422,7 +3382,7 @@ defmodule Module.Types.Descr do {{: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 + case Map.get(pos_domains, domain_key) do # Not all domain keys are present nil -> false type -> subtype?(type, not_set()) @@ -3442,9 +3402,9 @@ defmodule Module.Types.Descr do # 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 -> {map_key_tag_to_type(tag), %{map: map_new(tag, fields, domains)}} + case :maps.take(domain_key, domains) do + {value, domains} -> {value, %{map: map_new({tag, domains}, fields)}} + :error -> {map_key_tag_to_type(tag), %{map: map_new({tag, domains}, fields)}} end end From d593537ed485de7bdd453bea62725d1f23fd7d79 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 1 Jul 2025 19:25:46 +0200 Subject: [PATCH 16/17] Remove tags for domains --- lib/elixir/lib/module/types/descr.ex | 294 ++++++------------ .../test/elixir/module/types/descr_test.exs | 1 + 2 files changed, 88 insertions(+), 207 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 69cb22a024e..4341fa676fb 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -555,6 +555,8 @@ defmodule Module.Types.Descr do end end + defp empty_or_optional?(type), do: empty?(remove_optional(type)) + # For atom, bitmap, and optional, if the key is present, # then they are not empty, defp empty_key?(:fun, value), do: fun_empty?(value) @@ -2203,16 +2205,17 @@ defmodule Module.Types.Descr do # `%{..., 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 + # Instead of a tag :open or :closed, we can also use a map od 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. + # For instance, the type `%{atom() => if_set(integer())}` is the type of maps where atom keys + # map to integers, without any non-atom keys. It is represented using the map literal + # `{%{atom: if_set(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()}}, %{}, []}. + # The type `%{..., atom() => integer()}` represents maps with atom keys bound to integers, + # and other keys bound to any type. It will be represented using a map domain that maps + # atom to `if_set(integer())`, and every other domain key to `term_or_optional()`. defp map_descr(tag, pairs, default, force?) do {fields, domains, dynamic?} = map_descr_pairs(pairs, [], %{}, false) @@ -2226,7 +2229,7 @@ defmodule Module.Types.Descr do domains end - map_new({tag, domains}, fields) + map_new(domains, fields) else map_new(tag, fields) end @@ -2266,17 +2269,13 @@ defmodule Module.Types.Descr do {fields |> Enum.reverse() |> :maps.from_list(), domain, dynamic?} end - # TODO: Rename this to tuple_tag_to_type - defp tag_to_type(:open), do: term_or_optional() - defp tag_to_type(:closed), do: not_set() + defp tuple_tag_to_type(:open), do: term_or_optional() + defp tuple_tag_to_type(:closed), do: not_set() - # Rename this to map_key_tag_to_type + # Gets the default type associated to atom keys in a map. defp map_key_tag_to_type(:open), do: term_or_optional() defp map_key_tag_to_type(:closed), do: not_set() - defp map_key_tag_to_type({:closed, domain}), do: Map.get(domain, domain_key(:atom), not_set()) - - defp map_key_tag_to_type({:open, domain}), - do: Map.get(domain, domain_key(:atom), term_or_optional()) + defp map_key_tag_to_type(domains = %{}), do: Map.get(domains, domain_key(:atom), not_set()) defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :optional) @@ -2458,15 +2457,15 @@ defmodule Module.Types.Descr do end # At least one tag is a tag-domain pair. - defp map_literal_intersection(tag1, map1, tag2, map2) do + defp map_literal_intersection(tag_or_domains1, map1, tag_or_domains2, 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 = map_key_tag_to_type(tag1) - default2 = map_key_tag_to_type(tag2) + default1 = map_key_tag_to_type(tag_or_domains1) + default2 = map_key_tag_to_type(tag_or_domains2) # Compute the new domain - tag = map_domain_intersection(tag1, tag2) + tag_or_domains = map_domain_intersection(tag_or_domains1, tag_or_domains2) # 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) @@ -2474,7 +2473,7 @@ defmodule Module.Types.Descr do # 3. If key is only in map2, compute non empty intersection with atom1 # We do that by computing intersection on all key labels in both map1 and map2, # using default values when a key is not present. - {tag, + {tag_or_domains, symmetrical_merge(map1, default1, map2, default2, fn _key, v1, v2 -> non_empty_intersection!(v1, v2) end)} @@ -2483,32 +2482,30 @@ defmodule Module.Types.Descr do # 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 - defp map_domain_intersection(tag, :open), do: tag - - defp map_domain_intersection({tag1, domains1}, {tag2, domains2}) do - default1 = map_key_tag_to_type(tag1) - default2 = map_key_tag_to_type(tag2) + defp map_domain_intersection(:open, tag_or_domains), do: tag_or_domains + defp map_domain_intersection(tag_or_domains, :open), do: tag_or_domains + defp map_domain_intersection(domains1 = %{}, domains2 = %{}) do new_domains = - for domain_key <- @domain_key_types, reduce: %{} do + for {domain_key(_) = domain_key, type1} <- domains1, reduce: %{} do acc_domains -> - type1 = Map.get(domains1, domain_key, default1) - type2 = Map.get(domains2, domain_key, default2) - - inter = intersection(type1, type2) - - if empty?(inter) do - acc_domains - else - Map.put(acc_domains, domain_key, inter) + case domains2 do + %{^domain_key => type2} -> + inter = intersection(type1, type2) + + if empty_or_optional?(inter) do + acc_domains + else + Map.put(acc_domains, domain_key, inter) + end + + _ -> + acc_domains 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} + if map_size(new_domains) == 0, do: :closed, else: new_domains end defp map_literal_intersection_loop(:none, acc), do: {:closed, acc} @@ -2611,12 +2608,9 @@ 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()} - other -> map_key_tag_to_type(other) |> pop_optional_static() - end + defp map_fetch_static(%{map: [{tag_or_domains, fields, []}]}, key) + when not is_map_key(fields, key) do + map_key_tag_to_type(tag_or_domains) |> pop_optional_static() end # Takes a map dnf and returns the union of types it can take for a given key. @@ -2824,23 +2818,11 @@ defmodule Module.Types.Descr do end end - def map_refresh_tag(tag, domain_key, type) do - case tag do - :open -> - :open - - :closed -> - {:closed, %{domain_key => if_set(type)}} - - {:open, domains} -> - if Map.has_key?(domains, domain_key) do - {:open, Map.update!(domains, domain_key, &union(&1, type))} - else - {:open, domains} - end - - {:closed, domains} -> - {:closed, Map.update(domains, domain_key, if_set(type), &union(&1, type))} + def map_refresh_tag(tag_or_domains, domain_key, type) do + case tag_or_domains do + :open -> :open + :closed -> %{domain_key => if_set(type)} + domains = %{} -> Map.update(domains, domain_key, if_set(type), &union(&1, type)) end end @@ -2988,21 +2970,23 @@ defmodule Module.Types.Descr do 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 + defp unfold_domains(:closed), do: %{} + + defp unfold_domains(:open), + do: Map.new(@domain_key_types, fn domain_key -> {domain_key, @term_or_optional} end) - def map_get_static(%{map: [{{tag, domains}, fields, []}]}, key_descr) do + defp unfold_domains(domains = %{}), do: domains + + def map_get_static(%{map: [{tag_or_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. + domains = unfold_domains(tag_or_domains) + key_descr |> covered_key_types() |> Enum.reduce(none(), fn - {:atom, atom_type}, acc -> - map_get_atom([{{tag, domains}, fields, []}], atom_type) |> union(acc) - - key_type, acc -> - # Note: we could stop if we reach term()_or_optional() - Map.get(domains, key_type, map_key_tag_to_type(tag)) |> union(acc) + # Note: we could stop if we reach term_or_optional() + {:atom, atom_type}, acc -> map_get_atom([{domains, fields, []}], atom_type) |> union(acc) + key_type, acc -> Map.get(domains, key_type, not_set()) |> union(acc) end) end @@ -3094,15 +3078,15 @@ defmodule Module.Types.Descr do map_key_tag_to_type(tag) |> union(acc) # Optimization: if there are no negatives and domains exists, return its value - {{_tag, %{^domain_key => value}}, _fields, []}, acc -> + {%{^domain_key => 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 -> - map_key_tag_to_type(tag) |> union(acc) + {domains = %{}, _fields, []}, acc -> + map_key_tag_to_type(domains) |> union(acc) - {tag, fields, negs}, acc -> - {fst, snd} = map_pop_domain(tag, fields, domain_key) + {tag_or_domains, fields, negs}, acc -> + {fst, snd} = map_pop_domain(tag_or_domains, fields, domain_key) case map_split_negative_domain(negs, domain_key) do :empty -> @@ -3281,114 +3265,25 @@ defmodule Module.Types.Descr do # 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) - pos_has_key = Map.has_key?(pos_domains, domain_key) + defp map_check_domain_keys(:closed, _), do: true + defp map_check_domain_keys(_, :open), do: true - domain_key_present && - (pos_has_key || - subtype?(term_or_optional(), Map.get(neg_domains, 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) do - # Not all domain keys are present - nil -> false - type -> subtype?(term_or_optional(), type) - end - end) + # An open map is a subtype iff the negative domains are all present as term_or_optional() + defp map_check_domain_keys(:open, neg_domains) do + map_size(neg_domains) == length(@domain_key_types) and + Enum.all?(neg_domains, fn {domain_key(_), 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) + # A positive domains is smaller than a closed map iff all its keys are empty or optional + defp map_check_domain_keys(pos_domains, :closed) do + Enum.all?(pos_domains, fn {domain_key(_), type} -> empty_or_optional?(type) end) + 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) do - # Not all domain keys are present - nil -> false - type -> subtype?(type, not_set()) - end - end) - end + # Component-wise comparison of domains + defp map_check_domain_keys(pos_domains, neg_domains) do + Enum.all?(pos_domains, fn {domain_key(_) = domain_key, type} -> + subtype?(type, Map.get(neg_domains, domain_key, not_set())) + end) end defp map_pop_key(tag, fields, key) do @@ -3398,16 +3293,17 @@ 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())}} + # Pop a domain type, e.g. popping integers from %{integer() => binary()} + # returns {if_set(binary()), %{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 + defp map_pop_domain(domains = %{}, fields, domain_key) do case :maps.take(domain_key, domains) do - {value, domains} -> {value, %{map: map_new({tag, domains}, fields)}} - :error -> {map_key_tag_to_type(tag), %{map: map_new({tag, domains}, fields)}} + {value, domains} -> {if_set(value), %{map: map_new(domains, fields)}} + :error -> {map_key_tag_to_type(domains), %{map: map_new(domains, fields)}} end end + # Atom case defp map_pop_domain(tag, fields, _domain_key), do: {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}} @@ -3544,22 +3440,17 @@ defmodule Module.Types.Descr do {:map, [], []} end - def map_literal_to_quoted({{:closed, domains}, fields}, _opts) + def map_literal_to_quoted({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 + def map_literal_to_quoted({domains = %{}, fields}, opts) do domain_fields = for {domain_key(domain_type), value_type} <- domains do key = {:string, [], ["#{domain_type}() => "]} @@ -3570,17 +3461,6 @@ defmodule Module.Types.Descr do {:%{}, [], 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 -> @@ -4086,7 +3966,7 @@ defmodule Module.Types.Descr do Enum.reduce(dnf, none(), fn # Optimization: if there are no negatives, just return the type at that index. {tag, elements, []}, acc -> - Enum.at(elements, index, tag_to_type(tag)) |> union(acc) + Enum.at(elements, index, tuple_tag_to_type(tag)) |> union(acc) {tag, elements, negs}, acc -> {fst, snd} = tuple_pop_index(tag, elements, index) @@ -4200,7 +4080,7 @@ defmodule Module.Types.Descr do defp tuple_pop_index(tag, elements, index) do case List.pop_at(elements, index) do - {nil, _} -> {tag_to_type(tag), %{tuple: [{tag, elements, []}]}} + {nil, _} -> {tuple_tag_to_type(tag), %{tuple: [{tag, elements, []}]}} {type, rest} -> {type, %{tuple: [{tag, rest, []}]}} 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 4329f44adb4..d487ecd3aae 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1653,6 +1653,7 @@ defmodule Module.Types.DescrTest do 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()} From 48a0e5c9da38b7768ccb4e1bcdef6330d98a0844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 3 Jul 2025 14:44:45 +0200 Subject: [PATCH 17/17] Update lib/elixir/test/elixir/module/types/descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index f650cad6a98..a30367cdc60 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1753,20 +1753,6 @@ defmodule Module.Types.DescrTest do {:ok, closed_map([{:a, not_set()}, {:b, atom()}, {domain_key(:atom), pid()}])} end - # TODO: operator t\[t'] - # test "map_delete with domain keys" 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_take" do assert map_take(term(), :a) == :badmap assert map_take(integer(), :a) == :badmap