diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index c6a18b93cf..cfd9db80d5 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -40,7 +40,13 @@ defmodule Module.Types.Apply do fas = list(tuple([atom(), integer()])) struct_info = - list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean()))) + list( + closed_map( + default: {term(), true}, + field: {atom(), false}, + required: {boolean(), true} + ) + ) shared_info = [ attributes: list(tuple([atom(), list(term())])), @@ -254,7 +260,8 @@ defmodule Module.Types.Apply do {Map, :fetch, [{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]}, {Map, :fetch!, [{[open_map(), term()], term()}]}, - {Map, :from_struct, [{[open_map(__struct__: atom())], open_map(__struct__: not_set())}]}, + {Map, :from_struct, + [{[open_map(__struct__: {atom(), false})], open_map(__struct__: {none(), true})}]}, {Map, :get, [{[open_map(), term()], term()}]}, {Map, :get, [{[open_map(), term(), term()], term()}]}, {Map, :get_lazy, [{[open_map(), term(), fun(0)], term()}]}, @@ -990,8 +997,8 @@ defmodule Module.Types.Apply do info = {:strong, [term(), open_map()], [ - {[term(), open_map([{key, term()}])], atom([true])}, - {[term(), open_map([{key, not_set()}])], atom([false])} + {[term(), open_map([{key, {term(), false}}])], atom([true])}, + {[term(), open_map([{key, {none(), true}}])], atom([false])} ]} {info, filter_domain(info, expected, 2), context} @@ -1002,8 +1009,8 @@ defmodule Module.Types.Apply do info = {:strong, [open_map(), term()], [ - {[open_map([{key, term()}]), term()], atom([true])}, - {[open_map([{key, not_set()}]), term()], atom([false])} + {[open_map([{key, {term(), false}}]), term()], atom([true])}, + {[open_map([{key, {none(), true}}]), term()], atom([false])} ]} {info, filter_domain(info, expected, 2), context} @@ -1011,40 +1018,40 @@ defmodule Module.Types.Apply do def remote_domain(:erlang, :map_get, [key, _], expected, _meta, _stack, context) when is_atom(key) do - domain = [term(), open_map([{key, expected}])] + domain = [term(), open_map([{key, {expected, false}}])] {{:strong, nil, [{domain, term()}]}, domain, context} end def remote_domain(Map, :fetch!, [_, key], expected, _meta, _stack, context) when is_atom(key) do - domain = [open_map([{key, expected}]), term()] + domain = [open_map([{key, {expected, false}}]), term()] {{:strong, nil, [{domain, term()}]}, domain, context} end def remote_domain(:maps, :get, [key, _], expected, _meta, _stack, context) when is_atom(key) do - domain = [term(), open_map([{key, expected}])] + domain = [term(), open_map([{key, {expected, false}}])] {{:strong, nil, [{domain, term()}]}, domain, context} end def remote_domain(Map, :replace!, [_, key, _], _expected, _meta, _stack, context) when is_atom(key) do - domain = [open_map([{key, term()}]), term(), term()] + domain = [open_map([{key, {term(), false}}]), term(), term()] {{:strong, nil, [{domain, open_map()}]}, domain, context} end def remote_domain(:maps, :update, [key, _, _], _expected, _meta, _stack, context) when is_atom(key) do - domain = [term(), term(), open_map([{key, term()}])] + domain = [term(), term(), open_map([{key, {term(), false}}])] {{:strong, nil, [{domain, open_map()}]}, domain, context} end def remote_domain(Map, :pop!, [_, key], _expected, _meta, _stack, context) when is_atom(key) do - domain = [open_map([{key, term()}]), term()] + domain = [open_map([{key, {term(), false}}]), term()] {{:strong, nil, [{domain, tuple([term(), open_map()])}]}, domain, context} end def remote_domain(Map, :update!, [_, key, _], _expected, _meta, _stack, context) when is_atom(key) do - domain = [open_map([{key, term()}]), term(), fun(1)] + domain = [open_map([{key, {term(), false}}]), term(), fun(1)] {{:strong, nil, [{domain, open_map()}]}, domain, context} end @@ -1132,7 +1139,7 @@ defmodule Module.Types.Apply do defp remote_apply(Map, :from_struct, info, [map] = args_types, stack) do case remote_apply(info, args_types, stack) do {:ok, _type} -> - case map_update(map, @struct_key, not_set(), false, true) do + case map_update(map, @struct_key, none(), true, false, true) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} @@ -1146,7 +1153,7 @@ defmodule Module.Types.Apply do end defp remote_apply(Map, :delete, _info, [map, key] = args_types, stack) do - case map_update(map, key, not_set(), false, true) do + case map_update(map, key, none(), true, false, true) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} :badmap -> {:error, badremote(Map, :delete, args_types)} {:error, _errors} -> {:ok, map} @@ -1223,7 +1230,7 @@ defmodule Module.Types.Apply do _ -> args_types end - case map_update(map, key, not_set(), true, false) do + case map_update(map, key, none(), true, true, false) do {value, descr, _errors} -> value = opt_union(value, default) {:ok, return(tuple([value, descr]), args_types, stack)} @@ -1239,7 +1246,7 @@ defmodule Module.Types.Apply do defp remote_apply(Map, :pop_lazy, _info, [map, key, fun] = args_types, stack) do case fun_apply(fun, []) do {:ok, default} -> - case map_update(map, key, not_set(), true, false) do + case map_update(map, key, none(), true, true, false) do {value, descr, _errors} -> value = opt_union(value, default) {:ok, return(tuple([value, descr]), args_types, stack)} @@ -1257,7 +1264,7 @@ defmodule Module.Types.Apply do end defp remote_apply(Map, :put, _info, [map, key, value] = args_types, stack) do - case map_update(map, key, value, false, true) do + case map_update(map, key, value, false, false, true) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} :badmap -> {:error, badremote(Map, :put, args_types)} {:error, _errors} -> {:ok, map} @@ -1265,7 +1272,7 @@ defmodule Module.Types.Apply do end defp remote_apply(Map, :pop!, _info, [map, key] = args_types, stack) do - case map_update(map, key, not_set(), true, false) do + case map_update(map, key, none(), true, true, false) do {value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)} :badmap -> {:error, badremote(Map, :pop!, args_types)} {:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}} @@ -1273,7 +1280,7 @@ defmodule Module.Types.Apply do end defp remote_apply(Map, :replace, _info, [map, key, value] = args_types, stack) do - fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end + fun = fn _value, optional? -> {value, optional?} end case map_update_fun(map, key, fun, false, false) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} @@ -1283,7 +1290,7 @@ defmodule Module.Types.Apply do end defp remote_apply(Map, :replace!, _info, [map, key, value] = args_types, stack) do - fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end + fun = fn _value, optional? -> {value, optional?} end case map_update_fun(map, key, fun, false, false) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} @@ -1310,12 +1317,12 @@ defmodule Module.Types.Apply do _ -> map end - fun_apply = fn optional?, arg_type -> + fun_apply = fn arg_type, optional? -> if empty?(arg_type) do - default + {default, false} else case fun_apply(fun, [arg_type]) do - {:ok, res} -> if optional?, do: opt_union(res, default), else: res + {:ok, res} -> {if(optional?, do: opt_union(res, default), else: res), false} reason -> throw({:badapply, reason, [arg_type]}) end end @@ -1359,7 +1366,6 @@ defmodule Module.Types.Apply do if key_type == dynamic() or key_type == term() do {:ok, return(open_map(), args_types, stack)} else - value_type = if_set(value_type) domain_keys = to_domain_keys(key_type) keys = @@ -1368,7 +1374,13 @@ defmodule Module.Types.Apply do _ -> [domain_keys] end - map = closed_map(Enum.map(keys, &{&1, value_type})) + map = + closed_map( + Enum.map(keys, fn + key when is_atom(key) -> {key, {value_type, true}} + key -> {key, value_type} + end) + ) map_and_maybe_empty_map = case empty_list? do @@ -1400,7 +1412,7 @@ defmodule Module.Types.Apply do end defp remote_apply(:maps, :put, _info, [key, value, map] = args_types, stack) do - case map_update(map, key, value, false, true) do + case map_update(map, key, value, false, false, true) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} :badmap -> {:error, badremote(:maps, :put, args_types)} {:error, _errors} -> {:ok, map} @@ -1408,7 +1420,7 @@ defmodule Module.Types.Apply do end defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do - case map_update(map, key, not_set(), false, true) do + case map_update(map, key, none(), true, false, true) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} :badmap -> {:error, badremote(:maps, :remove, args_types)} {:error, _errors} -> {:ok, map} @@ -1416,7 +1428,7 @@ defmodule Module.Types.Apply do end defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do - case map_update(map, key, not_set(), true, false) do + case map_update(map, key, none(), true, true, false) do {value, descr, _errors} -> result = opt_union(tuple([value, descr]), atom([:error])) {:ok, return(result, args_types, stack)} @@ -1437,7 +1449,7 @@ defmodule Module.Types.Apply do end defp remote_apply(:maps, :update, _info, [key, value, map] = args_types, stack) do - fun = fn optional?, _type -> if optional?, do: if_set(value), else: value end + fun = fn _value, optional? -> {value, optional?} end case map_update_fun(map, key, fun, false, false) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} @@ -1808,8 +1820,8 @@ defmodule Module.Types.Apply do defp map_put_new(map, key, value, name, args_types, stack) do fun = fn - true, type -> opt_union(type, value) - false, type -> if empty?(type), do: value, else: type + type, true -> {opt_union(type, value), false} + type, false -> {if(empty?(type), do: value, else: type), false} end case map_update_fun(map, key, fun, false, true) do @@ -1827,9 +1839,9 @@ defmodule Module.Types.Apply do _ -> map end - fun_apply = fn optional?, arg_type -> + fun_apply = fn arg_type, optional? -> case fun_apply(fun, [arg_type]) do - {:ok, res} -> if optional?, do: if_set(res), else: res + {:ok, res} -> {res, optional?} reason -> throw({:badapply, reason, [arg_type]}) end end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2fdb5a25ee..cff1b18db0 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -74,10 +74,7 @@ defmodule Module.Types.Descr do @empty_list %{bitmap: @bit_empty_list} @not_non_empty_list Map.delete(@term, :list) - @not_set %{optional: 1} - @term_or_optional Map.put(@term, :optional, 1) - @term_or_dynamic_optional Map.put(@term, :dynamic, @term_or_optional) - @not_atom_or_optional Map.delete(@term_or_optional, :atom) + @not_atom Map.delete(@term, :atom) @empty_intersection [0, :bdd_bot] @empty_difference [0, :bdd_bot] @@ -127,7 +124,7 @@ defmodule Module.Types.Descr do defp expand_node({_id, state, generator}) do recur = fn name -> - {id, _if_set_id, generator} = Map.fetch!(state, name) + {id, generator} = Map.fetch!(state, name) make_node(id, state, generator) end @@ -144,10 +141,10 @@ defmodule Module.Types.Descr do def recursive(equations) when is_map(equations) do state = Map.new(equations, fn {name, generator} -> - {name, {make_ref(), make_ref(), generator}} + {name, {make_ref(), generator}} end) - Map.new(state, fn {name, {id, _if_set_id, generator}} -> + Map.new(state, fn {name, {id, generator}} -> {name, make_node(id, state, generator)} end) end @@ -334,87 +331,6 @@ defmodule Module.Types.Descr do defp unwrap_domain_tuple(descr, _transform) when descr == %{}, do: [] - ## Optional - - # `not_set()` is a special base type that represents a not_set field in a map. - # E.g., `%{a: integer(), b: not_set(), ...}` represents a map with an integer - # field `a` and an not_set field `b`, and possibly other fields. - # - # The `if_set()` modifier is syntactic sugar for specifying the key as a union - # of the key type and `not_set()`. For example, `%{:foo => if_set(integer())}` - # is equivalent to `%{:foo => integer() or not_set()}`. - # - # `not_set()` has no meaning outside of map types. - def not_set(), do: @not_set - - def if_set({_, _, _} = node) do - node |> unfold() |> if_set() - end - - def if_set(:term), do: term_or_optional() - - # If type contains a :dynamic part, :optional gets added there. - def if_set(type) do - case type do - %{dynamic: :term} when map_size(type) == 1 -> %{dynamic: term_or_optional()} - %{dynamic: :term} -> Map.put(%{type | dynamic: term_or_optional()}, :optional, 1) - %{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, - keep_optional: 1, remove_optional: 1, remove_optional_static: 1, optional_to_term: 1} - defp keep_optional(descr) do - case descr do - %{dynamic: %{optional: 1}, optional: 1} -> %{dynamic: %{optional: 1}, optional: 1} - %{dynamic: %{optional: 1}} -> %{dynamic: %{optional: 1}} - %{optional: 1} -> %{optional: 1} - _ -> @none - end - end - - defp remove_optional(descr) do - case descr do - %{dynamic: %{optional: _} = dynamic} when map_size(dynamic) == 1 -> - descr - |> Map.delete(:dynamic) - |> remove_optional_static() - - %{dynamic: %{optional: _} = dynamic} -> - descr - |> Map.replace!(:dynamic, Map.delete(dynamic, :optional)) - |> remove_optional_static() - - _ -> - remove_optional_static(descr) - end - end - - defp remove_optional_static(%{} = descr), do: Map.delete(descr, :optional) - defp remove_optional_static(descr), do: descr - - defp optional_to_term(descr) do - case descr do - %{dynamic: %{optional: _}} -> @term_or_dynamic_optional - %{optional: _} -> term_or_optional() - _ -> :term - end - end - - defp pop_optional_static(:term), do: {false, :term} - - defp pop_optional_static(%{} = type) do - case :maps.take(:optional, type) do - :error -> {false, type} - {1, type} -> {true, type} - end - end - - defp pop_optional_static(type), do: {false, type} - ## Set operations @doc """ @@ -454,7 +370,7 @@ defmodule Module.Types.Descr do Map.pop(descr, :dynamic, descr) end - defp put_dynamic(:term, dynamic), do: optional_to_term(%{dynamic: dynamic}) + defp put_dynamic(:term, _dynamic), do: :term defp put_dynamic(static, dynamic) when static == dynamic, do: static defp put_dynamic(_static, dynamic) when dynamic == @none, do: @none defp put_dynamic(static, dynamic), do: Map.put(static, :dynamic, dynamic) @@ -470,8 +386,8 @@ defmodule Module.Types.Descr do @doc """ Computes the union of two descrs. """ - def bare_union(:term, other), do: optional_to_term(other) - def bare_union(other, :term), do: optional_to_term(other) + def bare_union(:term, _other), do: :term + def bare_union(_other, :term), do: :term def bare_union(none, other) when none == @none, do: other def bare_union(other, none) when none == @none, do: other @@ -505,15 +421,14 @@ defmodule Module.Types.Descr do defp bare_union(:dynamic, v1, v2), do: dynamic_union(v1, v2, &bare_union/3) defp bare_union(:list, v1, v2), do: list_union(v1, v2) defp bare_union(:map, v1, v2), do: map_union(v1, v2) - defp bare_union(:optional, 1, 1), do: 1 defp bare_union(:tuple, v1, v2), do: tuple_union(v1, v2) defp bare_union(:fun, v1, v2), do: fun_union(v1, v2) @doc """ Computes the intersection of two descrs. """ - def bare_intersection(:term, other), do: remove_optional(other) - def bare_intersection(other, :term), do: remove_optional(other) + def bare_intersection(:term, other), do: other + def bare_intersection(other, :term), do: other def bare_intersection(left, right) do left = unfold(left) @@ -545,7 +460,6 @@ defmodule Module.Types.Descr do defp bare_intersection(:bitmap, v1, v2), do: v1 &&& v2 defp bare_intersection(:list, v1, v2), do: list_intersection(v1, v2) defp bare_intersection(:map, v1, v2), do: map_intersection(v1, v2) - defp bare_intersection(:optional, 1, 1), do: 1 defp bare_intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2) defp bare_intersection(:fun, v1, v2), do: fun_intersection(v1, v2) @@ -557,7 +471,7 @@ defmodule Module.Types.Descr do @doc """ Computes the difference between two types. """ - def bare_difference(left, :term), do: keep_optional(left) + def bare_difference(_left, :term), do: @none def bare_difference(left, none) when none == @none, do: left def bare_difference(left, right) do @@ -586,7 +500,6 @@ defmodule Module.Types.Descr do defp bare_difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2) defp bare_difference(:list, v1, v2), do: list_difference(v1, v2) defp bare_difference(:map, v1, v2), do: map_difference(v1, v2) - defp bare_difference(:optional, 1, 1), do: 0 defp bare_difference(:tuple, v1, v2), do: tuple_difference(v1, v2) defp bare_difference(:fun, v1, v2), do: fun_difference(v1, v2) @@ -633,7 +546,6 @@ defmodule Module.Types.Descr do descr -> not Map.has_key?(descr, :atom) and not Map.has_key?(descr, :bitmap) and - not Map.has_key?(descr, :optional) and (not Map.has_key?(descr, :tuple) or tuple_empty?(descr.tuple, seen)) and (not Map.has_key?(descr, :map) or map_empty?(descr.map, seen)) and (not Map.has_key?(descr, :list) or list_empty?(descr.list, seen)) and @@ -672,7 +584,10 @@ defmodule Module.Types.Descr do defp numberize(:map, bdd) do bdd_map(bdd, fn bdd_leaf(tag, fields) -> - bdd_leaf_new(tag, fields_map(fn _key, value -> numberize(value) end, fields)) + bdd_leaf_new( + tag, + fields_map(fn _key, {value, optional?} -> {numberize(value), optional?} end, fields) + ) end) end @@ -691,7 +606,6 @@ defmodule Module.Types.Descr do def singleton?(descr), do: static_singleton?(Map.get(descr, :dynamic, descr)) defp static_singleton?(:term), do: false - defp static_singleton?(%{optional: _}), do: false defp static_singleton?(%{list: _}), do: false defp static_singleton?(%{fun: _}), do: false defp static_singleton?(descr), do: each_singleton?(descr, [:atom, :bitmap, :map, :tuple], false) @@ -732,7 +646,9 @@ defmodule Module.Types.Descr do :empty [{:closed, fields, _negs}] -> - Enum.all?(fields_to_list(fields), fn {_, v} -> static_singleton?(v) end) + Enum.all?(fields_to_list(fields), fn {_, {v, optional?}} -> + not optional? and static_singleton?(v) + end) _ -> false @@ -949,7 +865,7 @@ defmodule Module.Types.Descr do empty_difference_subtype?(Map.delete(left, :dynamic), Map.delete(right, :dynamic), seen) end - defp empty_difference_subtype?(left, :term, _seen), do: keep_optional(left) == @none + defp empty_difference_subtype?(_left, :term, _seen), do: true defp empty_difference_subtype?(left, right, seen) do left = unfold(left) @@ -986,10 +902,10 @@ defmodule Module.Types.Descr do This reimplements intersection/2 but aborts as it finds a disjoint part. """ - def disjoint?(:term, other), do: empty?(remove_optional(other)) - def disjoint?(other, :term), do: empty?(remove_optional(other)) - def disjoint?(%{dynamic: :term}, other), do: empty?(remove_optional(other)) - def disjoint?(other, %{dynamic: :term}), do: empty?(remove_optional(other)) + def disjoint?(:term, other), do: empty?(other) + def disjoint?(other, :term), do: empty?(other) + def disjoint?(%{dynamic: :term}, other), do: empty?(other) + def disjoint?(other, %{dynamic: :term}), do: empty?(other) # Two gradual types are disjoint if their upper bounds are disjoint. def disjoint?(left, right) do @@ -1045,7 +961,7 @@ defmodule Module.Types.Descr do if empty?(other) do {:error, other} else - {:ok, remove_optional(other)} + {:ok, other} end end @@ -2749,23 +2665,23 @@ defmodule Module.Types.Descr do # on top of the static type. Though, the latter may be used for printing purposes. # # There are two ways for a descr to represent a static type: either the - # `:dynamic` field is not_set, or it contains a type equal to the static component + # `:dynamic` field is absent, or it contains a type equal to the static component # (that is, there are no extra dynamic values). - defp dynamic_union(:term, other, _fun), do: optional_to_term(other) - defp dynamic_union(other, :term, _fun), do: optional_to_term(other) + defp dynamic_union(:term, _other, _fun), do: :term + defp dynamic_union(_other, :term, _fun), do: :term defp dynamic_union(left, right, fun), do: symmetrical_merge(unfold(left), unfold(right), fun) - defp dynamic_intersection(:term, other, _fun), do: remove_optional_static(other) - defp dynamic_intersection(other, :term, _fun), do: remove_optional_static(other) + defp dynamic_intersection(:term, other, _fun), do: other + defp dynamic_intersection(other, :term, _fun), do: other defp dynamic_intersection(left, right, fun), do: symmetrical_intersection(unfold(left), unfold(right), fun) defp dynamic_difference(left, descr, _fun) when descr == %{}, do: left - defp dynamic_difference(left, :term, _fun), do: keep_optional(left) + defp dynamic_difference(_left, :term, _fun), do: @none defp dynamic_difference(left, right, fun) do iterator_dynamic_difference(:maps.next(:maps.iterator(unfold(right))), unfold(left), fun) @@ -2831,13 +2747,13 @@ defmodule Module.Types.Descr do # (@domain_key_types) to types, and `fields` is a map of atom keys (:foo, :bar, ...) # to types. # - # For instance, the type `%{atom() => if_set(integer())}` is the type of maps where atom 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 map literal - # `{%{atom: if_set(integer())}, %{}}`, with no defined keys. + # `{%{atom: integer()}, %{}}`, with no defined keys. Domain values are implicitly optional. # # 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()`. + # atom to `integer()`, and every other domain key to `term()`. @doc """ Converts a type into domain keys. @@ -2915,7 +2831,7 @@ defmodule Module.Types.Descr do end defp map_descr_static(:open, fields, domains) do - value = term_or_optional() + value = term() domains = fields_put_all_new(domains, @domain_key_types, value) %{map: map_new(domains, fields)} end @@ -2925,7 +2841,7 @@ defmodule Module.Types.Descr do end defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do - map_put_domain(domain, :lists.usort(domain_keys), map_domain_if_set(value), value) + map_put_domain(domain, :lists.usort(domain_keys), value, value) end defp map_put_domain([{k1, v1} | t1], [k2 | _] = keys, initial, value) when k1 < k2 do @@ -2933,7 +2849,7 @@ defmodule Module.Types.Descr do end defp map_put_domain([{k1, v1} | t1], [k1 | keys], _initial, value) do - [{k1, bare_union(v1, value)} | map_put_domain(t1, keys, map_domain_if_set(value), value)] + [{k1, bare_union(v1, value)} | map_put_domain(t1, keys, value, value)] end defp map_put_domain(domain, [k2 | keys], initial, value) do @@ -2942,75 +2858,68 @@ defmodule Module.Types.Descr do defp map_put_domain(domain, [], _initial, _value), do: domain - # Map domains store if_set(value). For recursive nodes, keep that application lazy - # so constructing the descriptor does not unfold the same map domain forever. - defp map_domain_if_set({id, state, _generator} = node) do - case if_set_node_id(id, state) do - :already_if_set -> node - if_set_id -> make_node(if_set_id, state, fn _recur -> node |> unfold() |> if_set() end) - end - end - - defp map_domain_if_set(value), do: if_set(value) - - defp if_set_node_id(id, state) do - Enum.find_value(state, fn - {_name, {^id, if_set_id, _generator}} -> if_set_id - {_name, {_id, ^id, _generator}} -> :already_if_set - _ -> nil - end) - end - defp map_descr_pairs(pairs) do {fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_possible?} = - Enum.reduce(pairs, {[], @fields_new, [], @fields_new, false, false}, fn {key, value}, acc -> - map_descr_pair(key, value, acc) + Enum.reduce(pairs, {[], @fields_new, [], @fields_new, false, false}, fn + {key, {value, optional?}}, acc when is_atom(key) and is_boolean(optional?) -> + map_descr_pair(key, value, optional?, acc) + + {key, value}, acc when is_list(key) -> + map_descr_domain(key, value, acc) end) {fields_from_reverse_list(fields), domains, fields_from_reverse_list(dynamic_fields), dynamic_domains, dynamic?, static_possible?} end - defp map_descr_pair( + defp map_descr_domain( key, value, {fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_empty?} ) do {dynamic_value, static_value, value_dynamic?} = split_dynamic(value) dynamic? = dynamic? or value_dynamic? + static_empty? = static_empty? or static_value == @none - if is_atom(key) do - {[{key, static_value} | fields], domains, [{key, dynamic_value} | dynamic_fields], - dynamic_domains, dynamic?, static_empty?} - else - {fields, map_put_domain(domains, key, static_value), dynamic_fields, - map_put_domain(dynamic_domains, key, dynamic_value), dynamic?, static_empty?} - end + {fields, map_put_domain(domains, key, static_value), dynamic_fields, + map_put_domain(dynamic_domains, key, dynamic_value), dynamic?, static_empty?} + end + + defp map_descr_pair( + key, + value, + optional?, + {fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_empty?} + ) do + {dynamic_value, static_value, value_dynamic?} = split_dynamic(value) + dynamic? = dynamic? or value_dynamic? + static_empty? = static_empty? or (not optional? and static_value == @none) + + {[{key, {static_value, optional?}} | fields], domains, + [{key, {dynamic_value, optional?}} | dynamic_fields], dynamic_domains, dynamic?, + static_empty?} end # 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(domains), do: fields_get(domains, :atom, not_set()) + defp map_key_tag_to_field(:open), do: {term(), true} + defp map_key_tag_to_field(:closed), do: {none(), true} + defp map_key_tag_to_field(domains), do: {fields_get(domains, :atom, none()), true} - # Gets the domain type association to a map. - # In this case, we already remove the optional to simplify upstream. + # Gets the domain type association to a map. Domain values are stored without + # an optional marker, so no conversion is necessary here. @compile {:inline, map_domain_tag_to_type: 1} defp map_domain_tag_to_type(:open), do: term() defp map_domain_tag_to_type(:closed), do: none() defp map_domain_tag_to_type(domain, key) when is_list(domain) do - remove_optional(fields_get(domain, key, none())) + fields_get(domain, key, none()) end defp map_domain_tag_to_type(domain, _key) do map_domain_tag_to_type(domain) end - defguardp is_optional_static(map) - when is_map(map) and is_map_key(map, :optional) - defp map_new(tag, fields), do: bdd_leaf_new(tag, fields) defp map_only?(descr), do: empty?(Map.delete(descr, :map)) @@ -3042,7 +2951,12 @@ defmodule Module.Types.Descr do defp map_literal_intersection(:open, map1, :open, map2, intersection_fun, seen) do new_fields = fields_merge( - fn _, type1, type2 -> non_empty_intersection!(type1, type2, intersection_fun, seen) end, + fn _key, {type1, optional1?}, {type2, optional2?} -> + optional? = optional1? and optional2? + type = intersection_fun.(type1, type2) + if not optional? and empty_seen?(type, seen), do: throw(:empty) + {type, optional?} + end, map1, map2 ) @@ -3076,8 +2990,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 = map_key_tag_to_type(tag_or_domains1) - default2 = map_key_tag_to_type(tag_or_domains2) + default1 = map_key_tag_to_field(tag_or_domains1) + default2 = map_key_tag_to_field(tag_or_domains2) # Compute the new domain tag_or_domains = map_domain_intersection(tag_or_domains1, tag_or_domains2, seen) @@ -3089,8 +3003,11 @@ defmodule Module.Types.Descr do # 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_or_domains, - fields_merge_with_defaults(map1, default1, map2, default2, fn _key, v1, v2 -> - non_empty_intersection!(v1, v2, intersection_fun, seen) + fields_merge_with_defaults(map1, default1, map2, default2, fn _key, {v1, o1}, {v2, o2} -> + optional? = o1 and o2 + type = intersection_fun.(v1, v2) + if not optional? and empty_seen?(type, seen), do: throw(:empty) + {type, optional?} end)} end @@ -3123,7 +3040,7 @@ defmodule Module.Types.Descr do defp map_domain_intersection_fields([{k, type1} | t1], [{_, type2} | t2], seen) do inter = bare_intersection(type1, type2) - if empty_seen?(remove_optional(inter), seen) do + if empty_seen?(inter, seen) do map_domain_intersection_fields(t1, t2, seen) else [{k, inter} | map_domain_intersection_fields(t1, t2, seen)] @@ -3133,44 +3050,51 @@ defmodule Module.Types.Descr do defp map_domain_intersection_fields(_, _, _seen), do: [] defp map_literal_intersection_open_closed( - [{k1, v1} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, intersection_fun, seen ) when k1 < k2 do + {_, optional1?} = f1 + # If the type in the open map is optional, we continue - case v1 do - %{optional: 1} -> map_literal_intersection_open_closed(t1, l2, intersection_fun, seen) - _ -> throw(:empty) - end + if optional1?, + do: map_literal_intersection_open_closed(t1, l2, intersection_fun, seen), + else: throw(:empty) end defp map_literal_intersection_open_closed( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, f2} | t2], intersection_fun, seen ) when k1 > k2 do # Anything in the closed map not in open is preserved - [{k2, v2} | map_literal_intersection_open_closed(l1, t2, intersection_fun, seen)] + [{k2, f2} | map_literal_intersection_open_closed(l1, t2, intersection_fun, seen)] end defp map_literal_intersection_open_closed( - [{key, v1} | t1], - [{_, v2} | t2], + [{key, f1} | t1], + [{_, f2} | t2], intersection_fun, seen ) do + {v1, optional1?} = f1 + {v2, optional2?} = f2 + optional? = optional1? and optional2? + type = intersection_fun.(v1, v2) + if not optional? and empty_seen?(type, seen), do: throw(:empty) + [ - {key, non_empty_intersection!(v1, v2, intersection_fun, seen)} + {key, {type, optional?}} | map_literal_intersection_open_closed(t1, t2, intersection_fun, seen) ] end defp map_literal_intersection_open_closed(t1, t2, _intersection_fun, _seen) do - if Enum.all?(t1, fn {_, v} -> match?(%{optional: 1}, v) end) do + if Enum.all?(t1, fn {_, {_, optional?}} -> optional? end) do t2 else throw(:empty) @@ -3178,13 +3102,15 @@ defmodule Module.Types.Descr do end defp map_literal_intersection_closed( - [{k1, v1} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, intersection_fun, seen ) when k1 < k2 do - if is_optional_static(v1) do + {_, optional1?} = f1 + + if optional1? do map_literal_intersection_closed(t1, l2, intersection_fun, seen) else throw(:empty) @@ -3193,28 +3119,32 @@ defmodule Module.Types.Descr do defp map_literal_intersection_closed( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, f2} | t2], intersection_fun, seen ) when k1 > k2 do - if is_optional_static(v2) do + {_, optional2?} = f2 + + if optional2? do map_literal_intersection_closed(l1, t2, intersection_fun, seen) else throw(:empty) end end - defp map_literal_intersection_closed([{key, v1} | t1], [{_, v2} | t2], intersection_fun, seen) do - [ - {key, non_empty_intersection!(v1, v2, intersection_fun, seen)} - | map_literal_intersection_closed(t1, t2, intersection_fun, seen) - ] + defp map_literal_intersection_closed([{key, f1} | t1], [{_, f2} | t2], intersection_fun, seen) do + {v1, optional1?} = f1 + {v2, optional2?} = f2 + optional? = optional1? and optional2? + type = intersection_fun.(v1, v2) + if not optional? and empty_seen?(type, seen), do: throw(:empty) + [{key, {type, optional?}} | map_literal_intersection_closed(t1, t2, intersection_fun, seen)] end defp map_literal_intersection_closed(t1, t2, _intersection_fun, _seen) do - if Enum.any?(t1, fn {_, v} -> not is_optional_static(v) end) or - Enum.any?(t2, fn {_, v} -> not is_optional_static(v) end) do + if Enum.any?(t1, fn {_, {_, optional?}} -> not optional? end) or + Enum.any?(t2, fn {_, {_, optional?}} -> not optional? end) do throw(:empty) end @@ -3226,11 +3156,6 @@ defmodule Module.Types.Descr do if empty?(type), do: throw(:empty), else: type end - defp non_empty_intersection!(type1, type2, intersection_fun, seen) do - type = intersection_fun.(type1, type2) - if empty_seen?(type, seen), do: throw(:empty), else: type - end - defp map_bdd_to_dnf_remove_empty(bdd) do bdd_to_dnf(bdd) |> Enum.reduce([], fn {pos, negs}, acc -> @@ -3278,7 +3203,7 @@ defmodule Module.Types.Descr do case :maps.take(:dynamic, descr) do :error -> if descr_key?(descr, :map) and non_empty_map_only?(descr) do - {static_optional?, static_type} = map_fetch_key_static(descr, key) + {static_type, static_optional?} = map_fetch_key_static(descr, key) if static_optional? or empty?(static_type) do :badkey @@ -3291,8 +3216,8 @@ defmodule Module.Types.Descr do {dynamic, static} -> if descr_key?(dynamic, :map) and map_only?(static) do - {dynamic_optional?, dynamic_type} = map_fetch_key_static(dynamic, key) - {static_optional?, static_type} = map_fetch_key_static(static, key) + {dynamic_type, dynamic_optional?} = map_fetch_key_static(dynamic, key) + {static_type, static_optional?} = map_fetch_key_static(static, key) if static_optional? or empty?(dynamic_type) do :badkey @@ -3308,10 +3233,8 @@ defmodule Module.Types.Descr do # Optimization for bdd leafs defp map_fetch_key_static(%{map: bdd_leaf(tag, fields)}, key) do case fields_find(key, fields) do - {:ok, value} -> pop_optional_static(value) - :error when tag == :open -> {true, term()} - :error when tag == :closed -> {true, none()} - :error -> tag |> map_key_tag_to_type() |> pop_optional_static() + {:ok, field} -> field + :error -> map_key_tag_to_field(tag) end end @@ -3319,46 +3242,48 @@ defmodule Module.Types.Descr do bdd |> map_bdd_to_dnf_with_empty() |> map_dnf_fetch_static(key) end - defp map_fetch_key_static(%{}, _key), do: {false, none()} - defp map_fetch_key_static(:term, _key), do: {true, term()} + defp map_fetch_key_static(%{}, _key), do: {none(), false} + defp map_fetch_key_static(:term, _key), do: {term(), true} - # Takes a map DNF and returns the union of types it can take for a given key. - # If the key may be undefined, it will contain the `not_set()` type. + # Takes a map DNF and returns the union of present-value types a key can take + # and whether the key is optional. defp map_dnf_fetch_static(dnf, key) do - Enum.reduce(dnf, none(), fn + Enum.reduce(dnf, {none(), false}, fn # Optimization: if there are no negatives {tag, fields, []}, acc -> - case fields_find(key, fields) do - {:ok, value} -> opt_union(value, acc) - :error when tag == :open -> throw(:open) - :error -> map_key_tag_to_type(tag) |> opt_union(acc) - end + field = + case fields_find(key, fields) do + {:ok, field} -> field + :error when tag == :open -> throw(:open) + :error -> map_key_tag_to_field(tag) + end + + field_opt_union(field, acc, %{}) {tag, fields, negs}, acc -> - {value, bdd} = map_pop_key_bdd(tag, fields, key) + {field, bdd} = map_pop_key_bdd(tag, fields, key) case map_split_negative_pairs_key(negs, key) do :empty -> acc negative -> - value = + field = if map_pair_projection_keeps_full_fst?(negative, bdd) do - value + field else negs - |> map_split_negative_key(key, value, bdd) - |> Enum.reduce(none(), fn {value, _}, acc -> opt_union(value, acc) end) + |> map_split_negative_key(key, field, bdd) + |> Enum.reduce({none(), false}, fn {field, _}, acc -> + field_opt_union(field, acc, %{}) + end) end - opt_union(value, acc) + field_opt_union(field, acc, %{}) end end) catch - :open -> {true, term()} - else - value -> - pop_optional_static(value) + :open -> {term(), true} end defp map_split_negative_pairs_key(negs, key) do @@ -3385,20 +3310,23 @@ defmodule Module.Types.Descr do not map_empty?(map_difference(bdd, neg_bdd), %{}) end - defp map_pair_projection_keeps_full_snd?(negative, value) do - neg_values = - Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc -> - bare_union(neg_value, acc) + defp map_pair_projection_keeps_full_snd?(negative, field) do + neg_field = + Enum.reduce(negative, {none(), false}, fn {neg_field, _neg_bdd}, acc -> + field_union(neg_field, acc) end) - not empty?(bare_difference(value, neg_values)) + not field_empty?(field_difference(field, neg_field)) end defp map_split_negative_key(negs, key, value, bdd) do map_split_negative(negs, value, bdd, fn neg_tag, neg_fields -> case fields_take(key, neg_fields) do - {neg_value, neg_fields} -> {true, neg_value, map_new(neg_tag, neg_fields)} - :error -> {false, map_key_tag_to_type(neg_tag), map_new(neg_tag, neg_fields)} + {neg_field, neg_fields} -> + {true, neg_field, map_new(neg_tag, neg_fields)} + + :error -> + {false, map_key_tag_to_field(neg_tag), map_new(neg_tag, neg_fields)} end end) end @@ -3411,39 +3339,39 @@ defmodule Module.Types.Descr do throw(:empty) bdd_leaf(neg_tag, neg_fields), acc -> - {found?, neg_value, neg_bdd} = take_fun.(neg_tag, neg_fields) + {found?, neg_field, neg_bdd} = take_fun.(neg_tag, neg_fields) if not found? and neg_tag == :open do # In case the map is open, t \ t₁ is always empty, # t ∩ t₁ is always t, so we just need to deal with the bdd. - Enum.reduce(acc, [], fn {value, bdd}, acc -> + Enum.reduce(acc, [], fn {field, bdd}, acc -> diff_bdd = map_difference(bdd, neg_bdd) if map_empty?(diff_bdd, %{}) do acc else - [{value, diff_bdd} | acc] + [{field, diff_bdd} | acc] end end) else - Enum.reduce(acc, [], fn {value, bdd}, acc -> + Enum.reduce(acc, [], fn {field, bdd}, acc -> # If the negative tag is closed, then they are likely disjoint, # so we can drastically cut down the amount of operations. if neg_tag == :closed and map_empty?(map_intersection(bdd, neg_bdd), %{}) do - [{value, bdd} | acc] + [{field, bdd} | acc] else - intersection_value = bare_intersection(value, neg_value) + intersection_field = field_intersection(field, neg_field) - if empty?(intersection_value) do - [{value, bdd} | acc] + if field_empty?(intersection_field) do + [{field, bdd} | acc] else diff_bdd = map_difference(bdd, neg_bdd) if map_empty?(diff_bdd, %{}) do - prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) + prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc) else - acc = [{intersection_value, diff_bdd} | acc] - prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) + acc = [{intersection_field, diff_bdd} | acc] + prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc) end end end @@ -3456,14 +3384,14 @@ defmodule Module.Types.Descr do defp map_pop_key_bdd(tag, fields, key) do case fields_take(key, fields) do - {value, fields} -> {value, map_new(tag, fields)} - :error -> {map_key_tag_to_type(tag), map_new(tag, fields)} + {field, fields} -> {field, map_new(tag, fields)} + :error -> {map_key_tag_to_field(tag), map_new(tag, fields)} end end - defp prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) do - diff_value = bare_difference(value, neg_value) - if empty?(diff_value), do: acc, else: [{diff_value, bdd} | acc] + defp prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc) do + diff_field = field_difference(field, neg_field) + if field_empty?(diff_field), do: acc, else: [{diff_field, bdd} | acc] end @doc """ @@ -3529,9 +3457,9 @@ defmodule Module.Types.Descr do defp has_empty_map?(dnf) do Enum.any?(dnf, fn {_, fields, negs} -> - Enum.all?(fields_to_list(fields), fn {_key, value} -> is_optional_static(value) end) and + Enum.all?(fields_to_list(fields), fn {_key, {_value, optional?}} -> optional? end) and Enum.all?(negs, fn bdd_leaf(_, fields) -> - not Enum.all?(fields_to_list(fields), fn {_key, value} -> is_optional_static(value) end) + not Enum.all?(fields_to_list(fields), fn {_key, {_value, optional?}} -> optional? end) end) end) end @@ -3549,8 +3477,6 @@ defmodule Module.Types.Descr do domains when is_list(domains) -> fields_fold(domains, acc, fn domain_key, value, acc -> - value = remove_optional(value) - if empty?(value) do acc else @@ -3568,11 +3494,11 @@ defmodule Module.Types.Descr do domain_keys_type -> {_seen, acc} = bdd_reduce(bdd, {%{}, domain_keys_type}, fn bdd_leaf(_tag, fields), seen_acc -> - fields_fold(fields, seen_acc, fn key, _type, {seen, acc} -> + fields_fold(fields, seen_acc, fn key, {_type, _optional?}, {seen, acc} -> if Map.has_key?(seen, key) do {seen, acc} else - {_, value} = map_dnf_fetch_static(dnf, key) + {value, _optional?} = map_dnf_fetch_static(dnf, key) seen = Map.put(seen, key, []) if empty?(value) do @@ -3599,19 +3525,24 @@ defmodule Module.Types.Descr do The `return_type?` flag is used for optimizations purposes. If set to false, the returned `type` should not be used, as it will be imprecise. """ - def map_update(descr, key_descr, type, return_type? \\ true, force? \\ false) - - def map_update(descr, key_descr, :term, return_type?, force?), - do: map_update_unchecked(descr, key_descr, fn _ -> :term end, return_type?, force?) - - def map_update(descr, key_descr, type, return_type?, force?) do + def map_update(descr, key_descr, type, optional?, return_type? \\ true, force? \\ false) + when is_boolean(optional?) do case type do + :term -> + map_update_unchecked( + descr, + key_descr, + fn _, _ -> {:term, optional?} end, + return_type?, + force? + ) + %{dynamic: dynamic} -> - fun = fn _, _ -> dynamic end + fun = fn _, _ -> {dynamic, optional?} end map_update_unchecked(dynamic(descr), key_descr, fun, return_type?, force?) %{} -> - fun = fn _, _ -> type end + fun = fn _, _ -> {type, optional?} end map_update_unchecked(descr, key_descr, fun, return_type?, force?) end end @@ -3622,28 +3553,22 @@ defmodule Module.Types.Descr do `key_descr` is split into optional and required keys and tracked accordingly. The gradual aspect of `key_descr` does not impact the return type. - This is a more general version of `map_update/5` and has the same return values. + This is a more general version of `map_update/6` and has the same return values. However, the third argument is an anonymous function that receives the current - value and returns `type_fun`. Note the value returned by `type_fun` cannot hold - dynamic. Any dynamic conversion must happen before invoking this function. + value and whether it is optional. Note the value returned by `type_fun` cannot + hold dynamic. Any dynamic conversion must happen before invoking this function. """ def map_update_fun(descr, key_descr, type_fun, return_type? \\ true, force? \\ false) do gradual? = gradual?(descr) - type_fun = fn optional?, value -> - if is_function(type_fun, 1) do - case type_fun.(if gradual?, do: dynamic(value), else: value) do - %{dynamic: dynamic} -> dynamic - descr -> descr - end - else - value = if gradual?, do: dynamic(value), else: value + type_fun = fn value, optional? -> + value = if gradual?, do: dynamic(value), else: value + {new_value, new_optional?} = type_fun.(value, optional?) - case type_fun.(optional?, value) do - %{dynamic: dynamic} -> dynamic - descr -> descr - end - end + new_value = + if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value + + {new_value, new_optional?} end map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) @@ -3743,9 +3668,9 @@ defmodule Module.Types.Descr do # initial return type. `map_update_keys_static` will then union into the # computed type below, using the original bdd/dnf, not the one with updated domains. descr = map_update_put_domains(bdd, domains, type_fun, force?) - {remove_optional(value), descr, errors, true} + {value, descr, errors, true} else - {remove_optional(value), none(), errors, false} + {value, none(), errors, false} end map_update_keys_static(dnf, required_keys, optional_keys, type_fun, force?, static?, acc) @@ -3779,20 +3704,18 @@ defmodule Module.Types.Descr do defp map_update_keys(dnf, keys, type_fun, required_key?, force?, static?, acc) do Enum.reduce(keys, acc, fn key, {acc_value, acc_descr, acc_errors, acc_found?} -> - {{optional?, value}, descr} = + {{value, optional?}, descr} = case dnf do # Optimization: avoid creating term types when updating open maps [{:open, fields, []}] -> if fields_is_key(key, fields) do - {value, descr} = map_dnf_pop_key_static(dnf, key, none()) - {pop_optional_static(value), descr} + map_dnf_pop_key_static(dnf, key, {none(), false}) else - {{true, term()}, %{map: map_new(:open, fields)}} + {{term(), true}, %{map: map_new(:open, fields)}} end _ -> - {value, descr} = map_dnf_pop_key_static(dnf, key, none()) - {pop_optional_static(value), descr} + map_dnf_pop_key_static(dnf, key, {none(), false}) end if not force? and empty?(value) do @@ -3801,8 +3724,10 @@ defmodule Module.Types.Descr do else acc_value = opt_union(value, acc_value) + {new_value, new_optional?} = type_fun.(value, optional?) + acc_descr = - opt_union(map_put_key_static(descr, key, type_fun.(optional?, value)), acc_descr) + opt_union(map_put_key_static(descr, key, new_value, new_optional?), acc_descr) # The field will be missing if we are not forcing, # we are in static mode and the value is optional. @@ -3818,17 +3743,20 @@ defmodule Module.Types.Descr do end # Directly inserts a key of a given type into every positive and negative map. - defp map_put_key_static(%{map: bdd} = descr, key, type) do + defp map_put_key_static(%{map: bdd} = descr, key, value, optional?) do bdd = bdd_map(bdd, fn - bdd_leaf(:closed, fields) when type == @not_set -> bdd_leaf_new(:closed, fields) - bdd_leaf(tag, fields) -> bdd_leaf_new(tag, fields_store(key, type, fields)) + bdd_leaf(:closed, fields) when optional? and value == @none -> + bdd_leaf_new(:closed, fields) + + bdd_leaf(tag, fields) -> + bdd_leaf_new(tag, fields_store(key, {value, optional?}, fields)) end) %{descr | map: bdd} end - defp map_put_key_static(descr, _key, _type), do: descr + defp map_put_key_static(descr, _key, _value, _optional?), do: descr # Removes a key from a map type and return its type. # @@ -3843,23 +3771,23 @@ defmodule Module.Types.Descr do # Note: if initial is nil, it means the value is not required. # So we don't compute it for performance. defp map_dnf_pop_key_static(dnf, key, initial) do - {value, bdd} = + {field, bdd} = Enum.reduce(dnf, {initial, :bdd_bot}, fn # Optimization: if there are no negatives, we can directly remove the key. - {tag, fields, []}, {value, bdd} -> + {tag, fields, []}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) - {maybe_opt_union(value, fn -> fst end), opt_map_union(bdd, snd, %{})} + {maybe_field_opt_union(field, fn -> fst end), opt_map_union(bdd, snd, %{})} - {tag, fields, negs}, {value, bdd} -> + {tag, fields, negs}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) case map_split_negative_pairs_key(negs, key) do :empty -> - {value, bdd} + {field, bdd} negative -> keep_fst? = - value == nil or map_pair_projection_keeps_full_fst?(negative, snd) + field == nil or map_pair_projection_keeps_full_fst?(negative, snd) keep_snd? = map_pair_projection_keeps_full_snd?(negative, fst) @@ -3868,11 +3796,11 @@ defmodule Module.Types.Descr do do: [], else: map_split_negative_key(negs, key, fst, snd) - {maybe_opt_union(value, fn -> + {maybe_field_opt_union(field, fn -> if keep_fst? do fst else - Enum.reduce(pairs, none(), &opt_union(elem(&1, 0), &2)) + Enum.reduce(pairs, {none(), false}, &field_opt_union(elem(&1, 0), &2, %{})) end end), if keep_snd? do @@ -3884,20 +3812,20 @@ defmodule Module.Types.Descr do end) if bdd == :bdd_bot do - {value, %{}} + {field, %{}} else - {value, %{map: bdd}} + {field, %{map: bdd}} end end defp map_update_merge_atom_key(bdd, dnf) do {_seen, acc} = bdd_reduce(bdd, {%{}, none()}, fn bdd_leaf(_tag, fields), seen_acc -> - fields_fold(fields, seen_acc, fn key, _type, {seen, acc} -> + fields_fold(fields, seen_acc, fn key, {_type, _optional?}, {seen, acc} -> if Map.has_key?(seen, key) do {seen, acc} else - {_, value} = map_dnf_fetch_static(dnf, key) + {value, _optional?} = map_dnf_fetch_static(dnf, key) {Map.put(seen, key, []), opt_union(acc, value)} end end) @@ -3908,11 +3836,11 @@ defmodule Module.Types.Descr do defp map_update_any_atom_key?(bdd, dnf) do bdd_reduce(bdd, %{}, fn bdd_leaf(_tag, fields), acc -> - fields_fold(fields, acc, fn key, _type, acc -> + fields_fold(fields, acc, fn key, {_type, _optional?}, acc -> if Map.has_key?(acc, key) do acc else - {_, value} = map_dnf_fetch_static(dnf, key) + {value, _optional?} = map_dnf_fetch_static(dnf, key) not empty?(value) and throw(:found_key) Map.put(acc, key, []) end @@ -3928,8 +3856,7 @@ defmodule Module.Types.Descr do # where found? tracks whether at least one domain key was matched in the map. defp map_update_get_domains(dnf, domain_keys, acc, require_type?, any_atom_key) do Enum.reduce(domain_keys, {false, [], [], acc}, fn domain_key, {found?, valid, invalid, acc} -> - # Get the value type for this domain key, excluding optional entries - value = map_get_domain_no_optional(dnf, domain_key, none()) + value = map_get_domain(dnf, domain_key, none()) cond do # Atom domains are special: we also check for individually named atom keys @@ -4008,7 +3935,7 @@ defmodule Module.Types.Descr do # the callback may itself typecheck a function application, and # applying it to `none()` will raise undue warnings. if force?, - do: fields_from_keys(domain_keys, if_set(type_fun.(true, none()))), + do: fields_from_keys(domain_keys, elem(type_fun.(none(), true), 0)), else: :closed # Note: domain_keys may contain duplicates, so we cannot @@ -4019,14 +3946,14 @@ defmodule Module.Types.Descr do {:ok, value} -> fields_store( domain_key, - opt_union(value, type_fun.(true, remove_optional(value))), + opt_union(value, elem(type_fun.(value, true), 0)), acc ) :error -> # Likewise, only forced updates may synthesize missing domain keys. if force?, - do: fields_store(domain_key, if_set(type_fun.(true, none())), acc), + do: fields_store(domain_key, elem(type_fun.(none(), true), 0), acc), else: acc end end) @@ -4099,7 +4026,7 @@ defmodule Module.Types.Descr do ((map_keys_from_negated_set(maybe_negated_set, bdd) -- optional_keys) -- required_keys) ++ optional_keys - type_fun = fn _, _ -> type end + type_fun = fn _, _ -> {type, false} end descr = case required_domains ++ optional_domains do @@ -4108,7 +4035,7 @@ defmodule Module.Types.Descr do end dnf = map_bdd_to_dnf_with_empty(bdd) - map_put_keys_static(dnf, required_keys ++ optional_keys, type, descr) + map_put_keys_static(dnf, required_keys ++ optional_keys, type, false, descr) end defp map_put_static(%{}, _split_keys, _type) do @@ -4126,14 +4053,14 @@ defmodule Module.Types.Descr do open_map() else dnf = map_bdd_to_dnf_with_empty(@map_top) - map_put_keys_static(dnf, required_keys ++ optional_keys, type, none()) + map_put_keys_static(dnf, required_keys ++ optional_keys, type, false, none()) end end - defp map_put_keys_static(dnf, keys, type, acc) do + defp map_put_keys_static(dnf, keys, value, optional?, acc) do Enum.reduce(keys, acc, fn key, acc -> {nil, descr} = map_dnf_pop_key_static(dnf, key, nil) - opt_union(map_put_key_static(descr, key, type), acc) + opt_union(map_put_key_static(descr, key, value, optional?), acc) end) end @@ -4191,9 +4118,9 @@ defmodule Module.Types.Descr do acc = map_get_keys(dnf, required_keys, acc) acc = map_get_keys(dnf, optional_keys, acc) acc = map_get_keys(dnf, map_keys_from_negated_set(maybe_negated_set, bdd), acc) - acc = Enum.reduce(required_domains, acc, &map_get_domain_no_optional(dnf, &1, &2)) - acc = Enum.reduce(optional_domains, acc, &map_get_domain_no_optional(dnf, &1, &2)) - remove_optional(acc) + acc = Enum.reduce(required_domains, acc, &map_get_domain(dnf, &1, &2)) + acc = Enum.reduce(optional_domains, acc, &map_get_domain(dnf, &1, &2)) + acc end defp map_get_static(%{}, _split_keys), do: none() @@ -4201,14 +4128,13 @@ defmodule Module.Types.Descr do defp map_get_keys(dnf, keys, acc) do Enum.reduce(keys, acc, fn atom, acc -> - {_, value} = map_dnf_fetch_static(dnf, atom) + {value, _optional?} = map_dnf_fetch_static(dnf, atom) opt_union(value, acc) end) end - # Take a map bdd and return the union of types for the given key domain. - # Notice this already removes the optional field from the domain. - defp map_get_domain_no_optional(dnf, domain_key, acc) when is_atom(domain_key) do + # Take a map BDD and return the union of present-value types for the given key domain. + defp map_get_domain(dnf, domain_key, acc) when is_atom(domain_key) do Enum.reduce(dnf, acc, fn # Optimization: if there are no negatives, get the domain tag directly {tag, _fields, []}, acc -> @@ -4229,7 +4155,7 @@ defmodule Module.Types.Descr do defp map_keys_from_negated_set(set, bdd) do bdd |> bdd_reduce(%{}, fn bdd_leaf(_, fields), acc -> - fields_fold(fields, acc, fn atom, _, acc -> + fields_fold(fields, acc, fn atom, {_type, _optional?}, acc -> if :sets.is_element(atom, set), do: acc, else: Map.put(acc, atom, true) end) end) @@ -4329,7 +4255,9 @@ defmodule Module.Types.Descr do defp init_map_line_empty?(tag, fields, negs), do: init_map_line_empty?(tag, fields, negs, %{}) defp init_map_line_empty?(tag, fields, negs, seen) do - Enum.any?(fields_to_list(fields), fn {_key, type} -> empty_seen?(type, seen) end) or + Enum.any?(fields_to_list(fields), fn {_key, {type, optional?}} -> + not optional? and empty_seen?(type, seen) + end) or map_line_empty?(tag, fields, negs, seen) end @@ -4359,7 +4287,7 @@ defmodule Module.Types.Descr do end defp map_line_meet_empty?( - [{k1, v1} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4368,25 +4296,27 @@ defmodule Module.Types.Descr do seen ) when k1 < k2 do + {_, optional1?} = f1 + cond do # The key is only in the positive map, which means the difference # with a negative open tag (all possible types) tag will surely be empty. neg_tag == :open -> - map_line_meet_empty?(t1, l2, tag, neg_tag, [{k1, v1} | acc_meet], negs, seen) + map_line_meet_empty?(t1, l2, tag, neg_tag, [{k1, f1} | acc_meet], negs, seen) # In this case the difference will never be empty, so we can skip ahead. - neg_tag == :closed and not is_optional_static(v1) -> + neg_tag == :closed and not optional1? -> throw(:closed) true -> - v2 = map_key_tag_to_type(neg_tag) - map_line_meet_empty?(k1, v1, v2, t1, l2, tag, neg_tag, acc_meet, negs, seen) + f2 = map_key_tag_to_field(neg_tag) + map_line_meet_empty?(k1, f1, f2, t1, l2, tag, neg_tag, acc_meet, negs, seen) end end defp map_line_meet_empty?( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, f2} | t2], tag, neg_tag, acc_meet, @@ -4397,61 +4327,72 @@ defmodule Module.Types.Descr do # The keys is only in the negative map and the positive map is closed, # in that case, this field is not_set(), and its difference with the # negative map type is empty iff the negative type is optional. - if tag == :closed and not is_optional_static(v2) do + {_, optional2?} = f2 + + if tag == :closed and not optional2? do throw(:closed) else - v1 = map_key_tag_to_type(tag) - map_line_meet_empty?(k2, v1, v2, l1, t2, tag, neg_tag, acc_meet, negs, seen) + f1 = map_key_tag_to_field(tag) + map_line_meet_empty?(k2, f1, f2, l1, t2, tag, neg_tag, acc_meet, negs, seen) end end - defp map_line_meet_empty?( - [{k, v1} | t1], - [{_, v2} | t2], - tag, - neg_tag, - acc_meet, - negs, - seen - ) do - map_line_meet_empty?(k, v1, v2, t1, t2, tag, neg_tag, acc_meet, negs, seen) + defp map_line_meet_empty?([{k, f1} | t1], [{_, f2} | t2], tag, neg_tag, acc_meet, negs, seen) do + map_line_meet_empty?(k, f1, f2, t1, t2, tag, neg_tag, acc_meet, negs, seen) end - defp map_line_meet_empty?([{k1, v1} | t1], [], tag, neg_tag, acc_meet, negs, seen) do - v2 = map_key_tag_to_type(neg_tag) - map_line_meet_empty?(k1, v1, v2, t1, [], tag, neg_tag, acc_meet, negs, seen) + defp map_line_meet_empty?([{k1, f1} | t1], [], tag, neg_tag, acc_meet, negs, seen) do + f2 = map_key_tag_to_field(neg_tag) + map_line_meet_empty?(k1, f1, f2, t1, [], tag, neg_tag, acc_meet, negs, seen) end - defp map_line_meet_empty?([], [{k2, v2} | t2], tag, neg_tag, acc_meet, negs, seen) do - v1 = map_key_tag_to_type(tag) - map_line_meet_empty?(k2, v1, v2, [], t2, tag, neg_tag, acc_meet, negs, seen) + defp map_line_meet_empty?([], [{k2, f2} | t2], tag, neg_tag, acc_meet, negs, seen) do + f1 = map_key_tag_to_field(tag) + map_line_meet_empty?(k2, f1, f2, [], t2, tag, neg_tag, acc_meet, negs, seen) end defp map_line_meet_empty?([], [], _tag, _neg_tag, _acc_meet, _negs, _seen) do true end - defp map_line_meet_empty?(key, type, neg_type, t1, t2, tag, neg_tag, acc_meet, negs, seen) do + defp map_line_meet_empty?( + key, + {type, optional?}, + {neg_type, neg_optional?}, + t1, + t2, + tag, + neg_tag, + acc_meet, + negs, + seen + ) do meet = bare_intersection(type, neg_type) + meet_optional? = optional? and neg_optional? - if empty_seen?(meet, seen) do + if field_empty_seen?(meet, meet_optional?, seen) do # This negative map is disjoint from the current line at this field. - map_line_empty?(tag, Enum.reverse(acc_meet, [{key, type} | t1]), negs, seen) + reverse_meet = Enum.reverse(acc_meet, [{key, {type, optional?}} | t1]) + map_line_empty?(tag, reverse_meet, negs, seen) else diff = bare_difference(type, neg_type) + diff_optional? = optional? and not neg_optional? + new_acc_meet = [{key, {meet, meet_optional?}} | acc_meet] - if empty_seen?(diff, seen) do + if field_empty_seen?(diff, diff_optional?, seen) do # The field is a subtype of the negative field, so their intersection is type. - map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, type} | acc_meet], negs, seen) + map_line_meet_empty?(t1, t2, tag, neg_tag, new_acc_meet, negs, seen) else - map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs, seen) and - map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs, seen) + reverse_meet = Enum.reverse(acc_meet, [{key, {diff, diff_optional?}} | t1]) + + map_line_empty?(tag, reverse_meet, negs, seen) and + map_line_meet_empty?(t1, t2, tag, neg_tag, new_acc_meet, negs, seen) end end end defp map_line_fields_empty?( - [{k1, v1} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4460,6 +4401,8 @@ defmodule Module.Types.Descr do seen ) when k1 < k2 do + {_, optional1?} = f1 + cond do # The key is only in the positive map, which means the difference # with a negative open tag (all possible types) tag will surely be empty. @@ -4467,26 +4410,20 @@ defmodule Module.Types.Descr do map_line_fields_empty?(t1, l2, tag, neg_tag, fields, negs, seen) # In this case the difference will never be empty, so we can skip ahead. - neg_tag == :closed and not is_optional_static(v1) -> + neg_tag == :closed and not optional1? -> throw(:closed) true -> - map_line_fields_empty_recur?( - k1, - v1, - map_key_tag_to_type(neg_tag), - tag, - fields, - negs, - seen - ) and + f2 = map_key_tag_to_field(neg_tag) + + map_line_fields_empty_recur?(k1, f1, f2, tag, fields, negs, seen) and map_line_fields_empty?(t1, l2, tag, neg_tag, fields, negs, seen) end end defp map_line_fields_empty?( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, f2} | t2], tag, neg_tag, fields, @@ -4497,43 +4434,52 @@ defmodule Module.Types.Descr do # The keys is only in the negative map and the positive map is closed, # in that case, this field is not_set(), and its difference with the # negative map type is empty iff the negative type is optional. + {_, optional2?} = f2 + if tag == :closed do - if is_optional_static(v2) do + if optional2? do map_line_fields_empty?(l1, t2, tag, neg_tag, fields, negs, seen) else throw(:closed) end else - map_line_fields_empty_recur?(k2, map_key_tag_to_type(tag), v2, tag, fields, negs, seen) and + f1 = map_key_tag_to_field(tag) + + map_line_fields_empty_recur?(k2, f1, f2, tag, fields, negs, seen) and map_line_fields_empty?(l1, t2, tag, neg_tag, fields, negs, seen) end end - defp map_line_fields_empty?( - [{key, v1} | t1], - [{_, v2} | t2], - tag, - neg_tag, - fields, - negs, - seen - ) do - map_line_fields_empty_recur?(key, v1, v2, tag, fields, negs, seen) and + defp map_line_fields_empty?([{key, f1} | t1], [{_, f2} | t2], tag, neg_tag, fields, negs, seen) do + map_line_fields_empty_recur?(key, f1, f2, tag, fields, negs, seen) and map_line_fields_empty?(t1, t2, tag, neg_tag, fields, negs, seen) end defp map_line_fields_empty?(t1, t2, tag, neg_tag, fields, negs, seen) do - Enum.all?(t1, fn {key, v1} -> - map_line_fields_empty_recur?(key, v1, map_key_tag_to_type(neg_tag), tag, fields, negs, seen) + Enum.all?(t1, fn {key, f1} -> + f2 = map_key_tag_to_field(neg_tag) + map_line_fields_empty_recur?(key, f1, f2, tag, fields, negs, seen) end) and - Enum.all?(t2, fn {key, v2} -> - map_line_fields_empty_recur?(key, map_key_tag_to_type(tag), v2, tag, fields, negs, seen) + Enum.all?(t2, fn {key, f2} -> + f1 = map_key_tag_to_field(tag) + map_line_fields_empty_recur?(key, f1, f2, tag, fields, negs, seen) end) end - defp map_line_fields_empty_recur?(key, v1, v2, tag, fields, negs, seen) do + defp map_line_fields_empty_recur?( + key, + {v1, optional1?}, + {v2, optional2?}, + tag, + fields, + negs, + seen + ) do diff = bare_difference(v1, v2) - empty_seen?(diff, seen) or map_line_empty?(tag, fields_store(key, diff, fields), negs, seen) + optional? = optional1? and not optional2? + + field_empty_seen?(diff, optional?, seen) or + map_line_empty?(tag, fields_store(key, {diff, optional?}, fields), negs, seen) end # Verify the domain condition from equation (22) in paper ICFP'23 https://www.irif.fr/~gc/papers/icfp23.pdf @@ -4542,29 +4488,29 @@ defmodule Module.Types.Descr do defp map_check_domain_keys?(:closed, _, _seen), do: true defp map_check_domain_keys?(_, :open, _seen), do: true - # An open map is a subtype iff the negative domains are all present as term_or_optional() + # An open map is a subtype iff the negative domains all accept term(). defp map_check_domain_keys?(:open, neg_domains, seen) do fields_size(neg_domains) == length(@domain_key_types) and Enum.all?(fields_to_list(neg_domains), fn {_domain_key, type} -> - subtype_seen?(term_or_optional(), type, seen) + subtype_seen?(term(), type, seen) end) end - # A positive domains is smaller than a closed map iff all its keys are empty or optional + # A positive domain is smaller than a closed map iff all its value types are empty. defp map_check_domain_keys?(pos_domains, :closed, seen) do Enum.all?(fields_to_list(pos_domains), fn {_domain_key, type} -> - empty_seen?(remove_optional(type), seen) + empty_seen?(type, seen) end) end # Component-wise comparison of domains defp map_check_domain_keys?(pos_domains, neg_domains, seen) do Enum.all?(fields_to_list(pos_domains), fn {domain_key, type} -> - subtype_seen?(type, fields_get(neg_domains, domain_key, not_set()), seen) + subtype_seen?(type, fields_get(neg_domains, domain_key, none()), seen) end) end - # Pop a domain type, already removing non optional. + # Pop a domain's present-value type. defp map_pop_domain_bdd(domains, fields, domain_key) when is_list(domains) do case fields_take(domain_key, domains) do {value, domains} -> {true, value, map_new(domains, fields)} @@ -4616,7 +4562,7 @@ defmodule Module.Types.Descr do {:empty_map, [], []} end - defp map_literal_to_quoted({:open, [{:__struct__, @not_atom_or_optional}]}, _opts) do + defp map_literal_to_quoted({:open, [{:__struct__, {@not_atom, true}}]}, _opts) do {:non_struct_map, [], []} end @@ -4629,7 +4575,7 @@ defmodule Module.Types.Descr do defp map_literal_to_quoted({tag, fields}, opts) do case tag do :closed -> - with {:ok, struct_descr} <- fields_find(:__struct__, fields), + with {:ok, {struct_descr, false}} <- fields_find(:__struct__, fields), {:finite, [struct]} <- atom_fetch(struct_descr), info when is_list(info) <- maybe_struct(struct), true <- fields_size(fields) == length(info) + 1, @@ -4638,10 +4584,10 @@ defmodule Module.Types.Descr do fields = for %{field: field} <- info, - type = fields_fetch!(field, fields), + {type, optional?} = fields_fetch!(field, fields), # TODO: This should consider the struct default type - not collapse? or type != term(), - do: {field, type} + not collapse? or optional? or type != term(), + do: {field, {type, optional?}} {:%, [], [ @@ -4680,13 +4626,11 @@ defmodule Module.Types.Descr do end defp map_domain_field_to_quoted(domain_key, value_type, opts) do - non_optional = remove_optional_static(value_type) - value_quoted = - if empty?(non_optional) do + if empty?(value_type) do {:not_set, [], []} else - map_value_to_quoted(non_optional, opts) + to_quoted(value_type, opts) end key_quoted = @@ -4712,8 +4656,8 @@ defmodule Module.Types.Descr do defp map_fields_to_quoted(tag, sorted, opts) do keyword? = Inspect.List.keyword?(sorted) - for {key, type} <- sorted, - not (tag == :open and is_optional_static(type) and term_type?(type)) do + for {key, {type, optional?}} <- sorted, + not (tag == :open and optional? and term_type?(type)) do key = if keyword? do {:__block__, [format: :keyword], [key]} @@ -4721,13 +4665,11 @@ defmodule Module.Types.Descr do literal_to_quoted(key) end - {key, map_value_to_quoted(type, opts)} + {key, map_value_to_quoted(type, optional?, opts)} end end - defp map_value_to_quoted(type, opts) do - {optional?, type} = pop_optional_static(type) - + defp map_value_to_quoted(type, optional?, opts) do cond do not optional? -> to_quoted(type, opts) empty?(type) -> {:not_set, [], []} @@ -4811,6 +4753,47 @@ defmodule Module.Types.Descr do defp fields_merge_with_defaults(f1, _d1, [], d2, fun), do: Enum.map(f1, fn {k, v1} -> {k, fun.(k, v1, d2)} end) + defp field_union({value1, optional1?}, {value2, optional2?}) do + {bare_union(value1, value2), optional1? or optional2?} + end + + defp maybe_field_opt_union(nil, _fun), do: nil + defp maybe_field_opt_union(field, fun), do: field_opt_union(field, fun.(), %{}) + + defp field_intersection({value1, optional1?}, {value2, optional2?}) do + {bare_intersection(value1, value2), optional1? and optional2?} + end + + defp field_difference({value1, optional1?}, {value2, optional2?}) do + {bare_difference(value1, value2), optional1? and not optional2?} + end + + defp field_opt_union({value1, optional1?}, {value2, optional2?}, seen) do + {opt_union(value1, value2, seen), optional1? or optional2?} + end + + defp field_opt_intersection({value1, optional1?}, {value2, optional2?}, seen) do + {opt_intersection(value1, value2, seen), optional1? and optional2?} + end + + defp field_opt_difference({value1, optional1?}, {value2, optional2?}, seen) do + {opt_difference(value1, value2, seen), optional1? and not optional2?} + end + + defp field_subtype?({value1, optional1?}, {value2, optional2?}) do + (not optional1? or optional2?) and subtype?(value1, value2) + end + + defp field_disjoint?({value1, optional1?}, {value2, optional2?}) do + (not optional1? or not optional2?) and disjoint?(value1, value2) + end + + defp field_empty?({_value, true}), do: false + defp field_empty?({value, false}), do: empty?(value) + + defp field_empty_seen?(_value, true, _seen), do: false + defp field_empty_seen?(value, false, seen), do: empty_seen?(value, seen) + ## Tuple # Represents tuple types as a BDD with nodes of the following forms: @@ -5241,7 +5224,7 @@ defmodule Module.Types.Descr do case :maps.take(:dynamic, descr) do :error -> if descr_key?(descr, :tuple) and non_empty_tuple_only?(descr) do - {static_optional?, static_type} = tuple_fetch_static(descr, key) + {static_type, static_optional?} = tuple_fetch_static(descr, key) # If I access a static tuple at a "open position", we have two options: # @@ -5268,8 +5251,8 @@ defmodule Module.Types.Descr do {dynamic, static} -> if descr_key?(dynamic, :tuple) and tuple_only?(static) do - {dynamic_optional?, dynamic_type} = tuple_fetch_static(dynamic, key) - {static_optional?, static_type} = tuple_fetch_static(static, key) + {dynamic_type, dynamic_optional?} = tuple_fetch_static(dynamic, key) + {static_type, static_optional?} = tuple_fetch_static(static, key) if empty?(dynamic_type) do :badindex @@ -5293,28 +5276,28 @@ defmodule Module.Types.Descr do defp tuple_fetch_static(descr, index) when is_integer(index) do case descr do - :term -> {true, term()} + :term -> {term(), true} %{tuple: bdd_leaf(tag, elements)} -> tuple_fetch_element(elements, index, tag) %{tuple: bdd} -> tuple_bdd_fetch_static(bdd, index) - %{} -> {false, none()} + %{} -> {none(), false} end end defp tuple_bdd_fetch_static(bdd, index) do bdd |> tuple_bdd_to_dnf_with_negations() - |> Enum.reduce({false, none()}, fn + |> Enum.reduce({none(), false}, fn # Optimization: if there are no negatives - {tag, elements, []}, {acc_optional?, acc_descr} -> - {optional?, descr} = tuple_fetch_element(elements, index, tag) - {optional? or acc_optional?, opt_union(descr, acc_descr)} + {tag, elements, []}, {acc_descr, acc_optional?} -> + {descr, optional?} = tuple_fetch_element(elements, index, tag) + {opt_union(descr, acc_descr), optional? or acc_optional?} - {tag, elements, negs}, {acc_optional?, acc_descr} -> + {tag, elements, negs}, {acc_descr, acc_optional?} -> {_, value, bdd} = tuple_take_element(elements, index, tag) case tuple_split_negative_pairs_index(negs, index) do :empty -> - {acc_optional?, acc_descr} + {acc_descr, acc_optional?} negative -> value = @@ -5323,15 +5306,17 @@ defmodule Module.Types.Descr do else negs |> tuple_split_negative(index, value, bdd) - |> Enum.reduce(none(), fn {value, _}, acc -> opt_union(value, acc) end) + |> Enum.reduce({none(), false}, fn {field, _}, acc -> + field_opt_union(field, acc, %{}) + end) end - {optional?, descr} = pop_optional_static(value) - {optional? or acc_optional?, opt_union(descr, acc_descr)} + {descr, optional?} = value + {opt_union(descr, acc_descr), optional? or acc_optional?} end end) catch - :open -> {true, term()} + :open -> {term(), true} end # Remove negatives: @@ -5347,34 +5332,34 @@ defmodule Module.Types.Descr do if not found? and neg_tag == :open do # In case the tuple is open, t \ t₁ is always empty, # t ∩ t₁ is always t, so we just need to deal with the bdd. - Enum.reduce(acc, [], fn {value, bdd}, acc -> + Enum.reduce(acc, [], fn {field, bdd}, acc -> diff_bdd = tuple_difference(bdd, neg_bdd) if tuple_empty?(diff_bdd, %{}) do acc else - [{value, diff_bdd} | acc] + [{field, diff_bdd} | acc] end end) else - Enum.reduce(acc, [], fn {value, bdd}, acc -> + Enum.reduce(acc, [], fn {field, bdd}, acc -> # If the negative tag is closed, then they are likely disjoint, # so we can drastically cut down the amount of operations. if neg_tag == :closed and tuple_empty?(tuple_intersection(bdd, neg_bdd), %{}) do - [{value, bdd} | acc] + [{field, bdd} | acc] else - intersection_value = bare_intersection(value, neg_value) + intersection_field = field_intersection(field, neg_value) - if empty?(intersection_value) do - [{value, bdd} | acc] + if field_empty?(intersection_field) do + [{field, bdd} | acc] else diff_bdd = tuple_difference(bdd, neg_bdd) if tuple_empty?(diff_bdd, %{}) do - prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) + prepend_map_pair_unless_empty_diff(field, neg_value, bdd, acc) else - acc = [{intersection_value, diff_bdd} | acc] - prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) + acc = [{intersection_field, diff_bdd} | acc] + prepend_map_pair_unless_empty_diff(field, neg_value, bdd, acc) end end end @@ -5412,22 +5397,22 @@ defmodule Module.Types.Descr do defp tuple_pair_projection_keeps_full_snd?(negative, value) do neg_values = - Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc -> - bare_union(neg_value, acc) + Enum.reduce(negative, {none(), false}, fn {neg_value, _neg_bdd}, acc -> + field_union(neg_value, acc) end) - not empty?(bare_difference(value, neg_values)) + not field_empty?(field_difference(value, neg_values)) end - defp tuple_fetch_element([], _, :open), do: {true, term()} - defp tuple_fetch_element([], _, :closed), do: {true, none()} - defp tuple_fetch_element([h | _], 0, _tag), do: {false, h} + defp tuple_fetch_element([], _, :open), do: {term(), true} + defp tuple_fetch_element([], _, :closed), do: {none(), true} + defp tuple_fetch_element([h | _], 0, _tag), do: {h, false} defp tuple_fetch_element([_ | t], i, tag), do: tuple_fetch_element(t, i - 1, tag) defp tuple_take_element(elements, index, tag) do case do_tuple_take_element(elements, index, []) do - :error -> {false, tuple_tag_to_type(tag), tuple_new(tag, elements)} - {value, elements} -> {true, value, tuple_new(tag, elements)} + :error -> {false, tuple_tag_to_field(tag), tuple_new(tag, elements)} + {value, elements} -> {true, {value, false}, tuple_new(tag, elements)} end end @@ -5435,8 +5420,8 @@ defmodule Module.Types.Descr do defp do_tuple_take_element([h | t], 0, acc), do: {h, Enum.reverse(acc, t)} defp do_tuple_take_element([h | t], i, acc), do: do_tuple_take_element(t, i - 1, [h | acc]) - defp tuple_tag_to_type(:open), do: term_or_optional() - defp tuple_tag_to_type(:closed), do: none() + defp tuple_tag_to_field(:open), do: {term(), true} + defp tuple_tag_to_field(:closed), do: {none(), false} @doc """ Returns all of the values that are part of a tuple. @@ -6367,8 +6352,8 @@ defmodule Module.Types.Descr do """ def opt_union(left, right), do: opt_union(left, right, %{}) - defp opt_union(:term, other, _seen), do: optional_to_term(other) - defp opt_union(other, :term, _seen), do: optional_to_term(other) + defp opt_union(:term, _other, _seen), do: :term + defp opt_union(_other, :term, _seen), do: :term defp opt_union(none, other, _seen) when none == @none, do: other defp opt_union(other, none, _seen) when none == @none, do: other @@ -6405,7 +6390,6 @@ defmodule Module.Types.Descr do defp opt_union(:list, v1, v2, _seen), do: list_union(v1, v2) defp opt_union(:map, v1, v2, seen), do: opt_map_union(v1, v2, seen) - defp opt_union(:optional, 1, 1, _seen), do: 1 defp opt_union(:tuple, v1, v2, _seen), do: opt_tuple_union(v1, v2) defp opt_union(:fun, v1, v2, _seen), do: fun_union(v1, v2) @@ -6414,8 +6398,8 @@ defmodule Module.Types.Descr do """ def opt_intersection(left, right), do: opt_intersection(left, right, %{}) - defp opt_intersection(:term, other, _seen), do: remove_optional(other) - defp opt_intersection(other, :term, _seen), do: remove_optional(other) + defp opt_intersection(:term, other, _seen), do: other + defp opt_intersection(other, :term, _seen), do: other defp opt_intersection(left, right, seen) do left = unfold(left) @@ -6446,7 +6430,6 @@ defmodule Module.Types.Descr do defp opt_intersection(:bitmap, v1, v2, _seen), do: v1 &&& v2 defp opt_intersection(:list, v1, v2, seen), do: opt_list_intersection(v1, v2, seen) defp opt_intersection(:map, v1, v2, seen), do: opt_map_intersection(v1, v2, seen) - defp opt_intersection(:optional, 1, 1, _seen), do: 1 defp opt_intersection(:tuple, v1, v2, seen), do: opt_tuple_intersection(v1, v2, seen) defp opt_intersection(:fun, v1, v2, _seen), do: fun_intersection(v1, v2) @@ -6460,7 +6443,7 @@ defmodule Module.Types.Descr do """ def opt_difference(left, right), do: opt_difference(left, right, %{}) - defp opt_difference(left, :term, _seen), do: keep_optional(left) + defp opt_difference(_left, :term, _seen), do: @none defp opt_difference(left, none, _seen) when none == @none, do: left defp opt_difference(left, right, seen) do @@ -6489,7 +6472,6 @@ defmodule Module.Types.Descr do defp opt_difference(:bitmap, v1, v2, _seen), do: v1 - (v1 &&& v2) defp opt_difference(:list, v1, v2, seen), do: opt_list_difference(v1, v2, seen) defp opt_difference(:map, v1, v2, seen), do: opt_map_difference(v1, v2, seen) - defp opt_difference(:optional, 1, 1, _seen), do: 0 defp opt_difference(:tuple, v1, v2, _seen), do: opt_tuple_difference(v1, v2) defp opt_difference(:fun, v1, v2, _seen), do: fun_difference(v1, v2) @@ -6503,10 +6485,6 @@ defmodule Module.Types.Descr do opt_difference(term(), descr) end - @compile {:inline, maybe_opt_union: 2} - defp maybe_opt_union(nil, _fun), do: nil - defp maybe_opt_union(descr, fun), do: opt_union(descr, fun.()) - defp opt_list_intersection(bdd_leaf(:term, :term), bdd, _seen), do: bdd defp opt_list_intersection(bdd, bdd_leaf(:term, :term), _seen), do: bdd @@ -6598,7 +6576,7 @@ defmodule Module.Types.Descr do {tag2, pos2} {:one_key_difference, key, v1, v2} -> - {tag1, fields_store(key, opt_union(v1, v2, seen), pos1)} + {tag1, fields_store(key, field_opt_union(v1, v2, seen), pos1)} :left_subtype_of_right -> {tag2, pos2} @@ -6625,7 +6603,7 @@ defmodule Module.Types.Descr do opt_map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), + if field_subtype?(p1, p2), do: opt_map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), else: :none @@ -6649,7 +6627,7 @@ defmodule Module.Types.Descr do opt_map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left) {:one_key_difference, _, p1, p2} -> - if subtype?(p2, p1), + if field_subtype?(p2, p1), do: opt_map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left), else: :none @@ -6675,10 +6653,10 @@ defmodule Module.Types.Descr do k1 != :__struct__ and tag1 == tag2 -> opt_map_union_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) - subtype?(v1, v2) -> + field_subtype?(v1, v2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - subtype?(v2, v1) -> + field_subtype?(v2, v1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -6686,17 +6664,21 @@ defmodule Module.Types.Descr do end :left_subtype_of_right -> - if subtype?(v1, v2), do: opt_map_union_strategy(t1, t2, tag1, tag2, status), else: :none + if field_subtype?(v1, v2), + do: opt_map_union_strategy(t1, t2, tag1, tag2, status), + else: :none :right_subtype_of_left -> - if subtype?(v2, v1), do: opt_map_union_strategy(t1, t2, tag1, tag2, status), else: :none + if field_subtype?(v2, v1), + do: opt_map_union_strategy(t1, t2, tag1, tag2, status), + else: :none {:one_key_difference, _key, p1, p2} -> cond do - subtype?(p1, p2) and subtype?(v1, v2) -> + field_subtype?(p1, p2) and field_subtype?(v1, v2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - subtype?(p2, p1) and subtype?(v2, v1) -> + field_subtype?(p2, p1) and field_subtype?(v2, v1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -6729,8 +6711,8 @@ defmodule Module.Types.Descr do {:one_key_difference, _, p1, p2} -> cond do - lhs? and subtype?(p1, p2) -> :left_subtype_of_right - rhs? and subtype?(p2, p1) -> :right_subtype_of_left + lhs? and field_subtype?(p1, p2) -> :left_subtype_of_right + rhs? and field_subtype?(p2, p1) -> :right_subtype_of_left true -> :none end @@ -6809,28 +6791,35 @@ defmodule Module.Types.Descr do # Therefore, this clause is only used when `type` is `:intersection` or `:none`. # `:union` falls through to the general clause below. The reason we have # this long comment is because this was a regression in the past. - defp opt_map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type, seen) + defp opt_map_leaf_difference( + bdd_leaf(tag, fields), + bdd_leaf(:open, [{key, field2}]), + type, + seen + ) when type != :union do - {found?, v1} = + {_, optional2?} = field2 + + {found?, field1} = case fields_find(key, fields) do - {:ok, value} -> {true, value} - :error -> {false, map_key_tag_to_type(tag)} + {:ok, field} -> {true, field} + :error -> {false, map_key_tag_to_field(tag)} end cond do tag == :closed and not found? -> - if is_optional_static(v2), do: :subtype, else: :disjoint + if optional2?, do: :subtype, else: :disjoint # In case the left-side is open, we will only be adding new keys # to the open map, which makes future eliminations harder. tag == :open and not found? and fields != [] -> :none - disjoint?(v1, v2) -> + field_disjoint?(field1, field2) -> :disjoint true -> - opt_map_leaf_one_key_difference(tag, fields, key, v1, v2, type, seen) + opt_map_leaf_one_key_difference(tag, fields, key, field1, field2, type, seen) end end @@ -6850,13 +6839,13 @@ defmodule Module.Types.Descr do end end - defp opt_map_leaf_one_key_difference(tag, fields, key, v1, v2, type, seen) do - v_diff = opt_difference(v1, v2, seen) + defp opt_map_leaf_one_key_difference(tag, fields, key, field1, field2, type, seen) do + field_diff = field_opt_difference(field1, field2, seen) - if empty?(v_diff) do + if field_empty?(field_diff) do :subtype else - a_diff = bdd_leaf_new(tag, fields_store(key, v_diff, fields)) + a_diff = bdd_leaf_new(tag, fields_store(key, field_diff, fields)) a_type = case type do @@ -6864,14 +6853,14 @@ defmodule Module.Types.Descr do :bdd_bot :union -> - bdd_leaf_new(tag, fields_store(key, opt_union(v1, v2, seen), fields)) + bdd_leaf_new(tag, fields_store(key, field_opt_union(field1, field2, seen), fields)) :intersection -> - v_int = opt_intersection(v1, v2, seen) + field_int = field_opt_intersection(field1, field2, seen) - if empty?(v_int), + if field_empty?(field_int), do: :bdd_bot, - else: bdd_leaf_new(tag, fields_store(key, v_int, fields)) + else: bdd_leaf_new(tag, fields_store(key, field_int, fields)) end {:one_key_difference, a_diff, a_type} @@ -6887,14 +6876,16 @@ defmodule Module.Types.Descr do end end - defp opt_map_difference_strategy([{k1, value} | t1], [{k2, _} | _] = l2, tag1, tag2, status) + defp opt_map_difference_strategy([{k1, field} | t1], [{k2, _} | _] = l2, tag1, tag2, status) when k1 < k2 do + {_, optional?} = field + # Left side has a key the right side does not have, # left can only be a subtype if the right side is open. # If the right side is closed and the key is not optional, they are disjoint. case status do _ when tag2 == :closed -> - if not is_optional_static(value) do + if not optional? do :disjoint else opt_map_difference_strategy(t1, l2, tag1, tag2, :none) @@ -6904,7 +6895,7 @@ defmodule Module.Types.Descr do opt_map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), + if field_subtype?(p1, p2), do: opt_map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), else: :none @@ -6916,11 +6907,13 @@ defmodule Module.Types.Descr do end end - defp opt_map_difference_strategy([{k1, _} | _] = l1, [{k2, value} | t2], tag1, tag2, _status) + defp opt_map_difference_strategy([{k1, _} | _] = l1, [{k2, field} | t2], tag1, tag2, _status) when k1 > k2 do + {_, optional?} = field + # Right side has a key the left side does not have, # if left-side is closed, they are disjoint. - if tag1 == :closed and not is_optional_static(value) do + if tag1 == :closed and not optional? do :disjoint else opt_map_difference_strategy(l1, t2, tag1, tag2, :none) @@ -6934,7 +6927,7 @@ defmodule Module.Types.Descr do defp opt_map_difference_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do # They have the same key but different values - if disjoint?(v1, v2) do + if field_disjoint?(v1, v2) do :disjoint else case status do @@ -6942,16 +6935,17 @@ defmodule Module.Types.Descr do opt_map_difference_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) {:one_key_difference, _key, p1, p2} -> - if subtype?(p1, p2) and subtype?(v1, v2) do + if field_subtype?(p1, p2) and field_subtype?(v1, v2) do opt_map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) else :none end _ -> - if status in [:all_equal, :left_subtype_of_right] and subtype?(v1, v2), - do: opt_map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right), - else: opt_map_difference_strategy(t1, t2, tag1, tag2, :none) + if status in [:all_equal, :left_subtype_of_right] and + field_subtype?(v1, v2), + do: opt_map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right), + else: opt_map_difference_strategy(t1, t2, tag1, tag2, :none) end end end @@ -6968,7 +6962,7 @@ defmodule Module.Types.Descr do :left_subtype_of_right {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), do: :left_subtype_of_right, else: :none + if field_subtype?(p1, p2), do: :left_subtype_of_right, else: :none :left_subtype_of_right -> :left_subtype_of_right @@ -6977,10 +6971,10 @@ defmodule Module.Types.Descr do :none end - tag1 == :closed and l2 != [] and Enum.all?(l2, fn {_, v} -> not is_optional_static(v) end) -> + tag1 == :closed and l2 != [] and Enum.all?(l2, fn {_, {_, optional?}} -> not optional? end) -> :disjoint - tag2 == :closed and l1 != [] and Enum.all?(l1, fn {_, v} -> not is_optional_static(v) end) -> + tag2 == :closed and l1 != [] and Enum.all?(l1, fn {_, {_, optional?}} -> not optional? end) -> :disjoint true -> @@ -7023,7 +7017,7 @@ defmodule Module.Types.Descr do else case opt_map_difference_strategy(acc_fields, neg_fields, tag, neg_tag) do {:one_key_difference, key, v1, v2} -> - {fields_store(key, opt_difference(v1, v2), acc_fields), acc_negs} + {fields_store(key, field_opt_difference(v1, v2, %{}), acc_fields), acc_negs} :disjoint -> {acc_fields, acc_negs} diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 225f50309b..bf7a114a53 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -17,26 +17,26 @@ defmodule Module.Types.Expr do @try_catch atom([:error, :exit, :throw]) @caller closed_map( - __struct__: atom([Macro.Env]), - aliases: aliases, - context: atom([:match, :guard, nil]), - context_modules: list_of_modules, - file: binary(), - function: opt_union(tuple([atom(), integer()]), atom([nil])), - functions: functions_and_macros, - lexical_tracker: opt_union(pid(), atom([nil])), - line: integer(), - macro_aliases: aliases, - macros: functions_and_macros, - module: atom(), - requires: list_of_modules, - tracers: list_of_modules, - versioned_vars: open_map() + __struct__: {atom([Macro.Env]), false}, + aliases: {aliases, false}, + context: {atom([:match, :guard, nil]), false}, + context_modules: {list_of_modules, false}, + file: {binary(), false}, + function: {opt_union(tuple([atom(), integer()]), atom([nil])), false}, + functions: {functions_and_macros, false}, + lexical_tracker: {opt_union(pid(), atom([nil])), false}, + line: {integer(), false}, + macro_aliases: {aliases, false}, + macros: {functions_and_macros, false}, + module: {atom(), false}, + requires: {list_of_modules, false}, + tracers: {list_of_modules, false}, + versioned_vars: {open_map(), false} ) # We do not make exception dynamic on purpose. If you do a blank rescue, # then we will assume you need to statically handle all possible exceptions. - @exception open_map(__struct__: atom(), __exception__: term()) + @exception open_map(__struct__: {atom(), false}, __exception__: {term(), false}) args_or_arity = opt_union(list(term()), integer()) @@ -182,7 +182,7 @@ defmodule Module.Types.Expr do expected_pairs = Enum.flat_map(pairs_types, fn {key_type, _value_type} -> case atom_fetch(key_type) do - {:finite, [key]} -> [{key, term()}] + {:finite, [key]} -> [{key, {term(), false}}] _ -> [] end end) @@ -691,7 +691,9 @@ defmodule Module.Types.Expr do def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context) when not is_atom(callee) and is_atom(key_or_fun) do if Keyword.get(meta, :no_parens, false) do - {type, context} = of_expr(callee, open_map([{key_or_fun, expected}]), expr, stack, context) + {type, context} = + of_expr(callee, open_map([{key_or_fun, {expected, false}}]), expr, stack, context) + Of.map_fetch(call, type, key_or_fun, stack, context) else {type, context} = of_expr(callee, atom(), call, stack, context) @@ -752,7 +754,7 @@ defmodule Module.Types.Expr do defp of_tuple([elem | elems], index, acc, expected, expr, stack, context) do expr_expected = case tuple_fetch(expected, index) do - {_, type} -> type + {_optional?, type} -> type _ -> term() end @@ -767,7 +769,7 @@ defmodule Module.Types.Expr do ## Try defp of_rescue(var, exceptions, expr, info, meta, stack, context) do - args = [__exception__: term()] + args = [__exception__: {term(), false}] {structs, context} = Enum.map_reduce(exceptions, context, fn exception, context -> @@ -1046,7 +1048,7 @@ defmodule Module.Types.Expr do do: [{args, return}] defp literal_map_update(descr, key_descr, value_descr) do - case map_update(descr, key_descr, value_descr, false, false) do + case map_update(descr, key_descr, value_descr, false, false, false) do {_type, descr, []} -> {:ok, descr} {_, _, [error | _]} -> error :badmap -> :badmap diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index ec8794382b..c6662a99ba 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -265,7 +265,7 @@ defmodule Module.Types.Of do {Function, fun()}, {Integer, integer()}, {List, opt_union(empty_list(), non_empty_list(term(), term()))}, - {Map, open_map(__struct__: if_set(Module.Types.Descr.opt_negation(atom())))}, + {Map, open_map(__struct__: {Module.Types.Descr.opt_negation(atom()), true})}, {Port, port()}, {PID, pid()}, {Reference, reference()}, @@ -294,7 +294,7 @@ defmodule Module.Types.Of do if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do struct_type(struct, info) else - open_map(__struct__: atom([struct])) + open_map(__struct__: {atom([struct]), false}) end end @@ -365,7 +365,7 @@ defmodule Module.Types.Of do # Because a multiple key may override single keys, we can only # collect single keys while there are no multiples. [key] when multiple == [] -> - {dynamic?, domain, [{key, value_type} | single], multiple} + {dynamic?, domain, [{key, {value_type, false}} | single], multiple} _ -> {dynamic?, domain, single, [{pos, value_type} | multiple]} @@ -382,7 +382,7 @@ defmodule Module.Types.Of do [{keys, type} | tail] -> for key <- keys, t <- cartesian_map(tail) do - closed_map(non_multiple ++ [{key, type} | t]) + closed_map(non_multiple ++ [{key, {type, false}} | t]) end |> Enum.reduce(&opt_union/2) end @@ -391,7 +391,11 @@ defmodule Module.Types.Of do end defp union_negated([], new_type, single, multiple) do - single = Enum.map(single, fn {key, old_type} -> {key, opt_union(old_type, new_type)} end) + single = + Enum.map(single, fn + {key, {old_type, optional?}} -> + {key, {opt_union(old_type, new_type), optional?}} + end) multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end) @@ -401,12 +405,13 @@ defmodule Module.Types.Of do defp union_negated(negated, new_type, single, multiple) do {single, matched} = - Enum.map_reduce(single, [], fn {key, old_type}, matched -> - if key in negated do - {{key, old_type}, [key | matched]} - else - {{key, opt_union(old_type, new_type)}, matched} - end + Enum.map_reduce(single, [], fn + {key, {old_type, optional?}}, matched -> + if key in negated do + {{key, {old_type, optional?}}, [key | matched]} + else + {{key, {opt_union(old_type, new_type), optional?}}, matched} + end end) multiple = @@ -414,7 +419,7 @@ defmodule Module.Types.Of do {keys, opt_union(old_type, new_type)} end) - {Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple} + {Enum.map(negated -- matched, fn key -> {key, {none(), true}} end) ++ single, multiple} end defp pairs(pairs, expected, stack, context, of_fun) do @@ -423,7 +428,7 @@ defmodule Module.Types.Of do expected_value_type = with {[key], [], []} <- pos_neg_domain, - {_, expected_value_type} <- map_fetch_key(expected, key) do + {_optional?, expected_value_type} <- map_fetch_key(expected, key) do expected_value_type else _ -> term() @@ -458,7 +463,7 @@ defmodule Module.Types.Of do [[]] [{keys, type} | tail] -> - for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t] + for key <- keys, t <- cartesian_map(tail), do: [{key, {type, false}} | t] end end @@ -480,15 +485,15 @@ defmodule Module.Types.Of do Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> value_type = case map_fetch_key(expected, key) do - {_, expected_value_type} -> expected_value_type + {_optional?, expected_value_type} -> expected_value_type _ -> term() end {type, context} = of_fun.(value, value_type, stack, context) - {{key, type}, context} + {{key, {type, false}}, context} end) - {closed_map([{:__struct__, atom([struct])} | args_types]), context} + {closed_map([__struct__: {atom([struct]), false}] ++ args_types), context} end end @@ -527,9 +532,16 @@ defmodule Module.Types.Of do # we introduce typed structs. They are only used by exceptions. def struct_type(struct, info, args_types \\ []) do term = dynamic() - pairs = for %{field: field} <- info, do: {field, term} - pairs = [{:__struct__, atom([struct])} | pairs] - pairs = if args_types == [], do: pairs, else: pairs ++ args_types + pairs = for %{field: field} <- info, do: {field, {term, false}} + pairs = [{:__struct__, {atom([struct]), false}} | pairs] + + pairs = + if args_types == [] do + pairs + else + pairs ++ args_types + end + closed_map(pairs) end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index c96de6f504..0431920264 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -534,14 +534,18 @@ defmodule Module.Types.Pattern do defp of_pattern_tree({:open_map, static, dynamic}, stack, context) do dynamic = - Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end) + Enum.map(dynamic, fn {key, value} -> + {key, {of_pattern_tree(value, stack, context), false}} + end) open_map(static ++ dynamic) end defp of_pattern_tree({:closed_map, static, dynamic}, stack, context) do dynamic = - Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, stack, context)} end) + Enum.map(dynamic, fn {key, value} -> + {key, {of_pattern_tree(value, stack, context), false}} + end) closed_map(static ++ dynamic) end @@ -721,20 +725,20 @@ defmodule Module.Types.Pattern do pairs = Map.new(pairs) term = term() - static = [__struct__: atom([struct])] + static = [__struct__: {atom([struct]), false}] dynamic = [] {static, dynamic} = Enum.reduce(info, {static, dynamic}, fn %{field: field}, {static, dynamic} -> case pairs do %{^field => value_type} when is_descr(value_type) -> - {[{field, value_type} | static], dynamic} + {[{field, {value_type, false}} | static], dynamic} %{^field => value_type} -> {static, [{field, value_type} | dynamic]} _ -> - {[{field, term} | static], dynamic} + {[{field, {term, false}} | static], dynamic} end end) @@ -772,7 +776,8 @@ defmodule Module.Types.Pattern do {refined, context} = of_match_var(var, atom(), expr, stack, context) if compatible?(refined, atom()) do - of_open_map(args, singleton?(refined), [__struct__: refined], [], path, stack, context) + fields = [__struct__: {refined, false}] + of_open_map(args, singleton?(refined), fields, [], path, stack, context) else error = {:badstruct, refined, expr, context} {error_type(), false, error(__MODULE__, error, meta, stack, context)} @@ -861,7 +866,7 @@ defmodule Module.Types.Pattern do precise? = precise? and value_precise? if is_descr(value_type) do - {precise?, [{key, value_type} | static], dynamic, context} + {precise?, [{key, {value_type, false}} | static], dynamic, context} else {precise?, static, [{key, value_type} | dynamic], context} end @@ -1165,7 +1170,7 @@ defmodule Module.Types.Pattern do # var.field def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, expected, expr, stack, context) when not is_atom(callee) do - {type, context} = of_guard(callee, open_map([{key, expected}]), expr, stack, context) + {type, context} = of_guard(callee, open_map([{key, {expected, false}}]), expr, stack, context) Of.map_fetch(map_fetch, type, key, stack, context) end diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 4156d0c62d..3a4147ace5 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -691,7 +691,7 @@ defmodule Protocol do structs_domain = case structs do [] -> Descr.none() - _ -> Descr.open_map(__struct__: Descr.atom(structs)) + _ -> Descr.open_map(__struct__: {Descr.atom(structs), false}) end domain = diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 97ab8788f6..ca29a307e2 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -11,7 +11,7 @@ -define(typespecs, 'Elixir.Kernel.Typespec'). checker_version() -> - elixir_checker_v9. + elixir_checker_v10. %% debug_info callback diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 8a3bfefb5e..5bdb9bac31 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -29,12 +29,12 @@ defmodule Module.Types.DescrTest do defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}]) defp projected_negative_map(size) do - Enum.reduce(1..size, open_map(k: open_map(), x: term()), fn index, acc -> + Enum.reduce(1..size, open_map(k: {open_map(), false}, x: {term(), false}), fn index, acc -> opt_difference( acc, open_map([ - {:k, open_map([{String.to_unsafe_atom("value#{index}"), integer()}])}, - {String.to_unsafe_atom("field#{index}"), integer()} + {:k, {open_map([{String.to_unsafe_atom("value#{index}"), {integer(), false}}]), false}}, + {String.to_unsafe_atom("field#{index}"), {integer(), false}} ]) ) end) @@ -60,7 +60,6 @@ defmodule Module.Types.DescrTest do test "term" do assert opt_union(term(), float()) == term() assert opt_union(term(), binary()) == term() - assert opt_union(term(), if_set(binary())) == if_set(term()) end test "none" do @@ -103,24 +102,6 @@ defmodule Module.Types.DescrTest do assert equal?(opt_union(dynamic(atom()), atom()), atom()) refute equal?(opt_union(dynamic(atom()), atom()), dynamic(atom())) - - assert equal?( - opt_union(term(), dynamic(if_set(integer()))), - opt_union(term(), dynamic(not_set())) - ) - - refute equal?( - opt_union(term(), dynamic(if_set(integer()))), - dynamic(opt_union(term(), not_set())) - ) - end - - test "optional" do - # Test that union preserves optional properties - t = opt_union(term(), dynamic(not_set())) - assert subtype?(term(), t) - assert subtype?(dynamic(), t) - assert subtype?(dynamic(integer()), t) end test "tuple" do @@ -156,18 +137,21 @@ defmodule Module.Types.DescrTest do test "map" do assert equal?(opt_union(open_map(), open_map()), open_map()) - assert equal?(opt_union(closed_map(a: integer()), open_map()), open_map()) + assert equal?(opt_union(closed_map(a: {integer(), false}), open_map()), open_map()) assert equal?( - opt_union(closed_map(a: integer()), opt_negation(closed_map(a: integer()))), + opt_union( + closed_map(a: {integer(), false}), + opt_negation(closed_map(a: {integer(), false})) + ), term() ) - a_integer_open = open_map(a: integer()) - assert equal?(opt_union(closed_map(a: integer()), a_integer_open), a_integer_open) + a_integer_open = open_map(a: {integer(), false}) + assert equal?(opt_union(closed_map(a: {integer(), false}), a_integer_open), a_integer_open) - closed = closed_map(a: integer(), b: atom()) - open = open_map(a: integer(), b: boolean()) + closed = closed_map(a: {integer(), false}, b: {atom(), false}) + open = open_map(a: {integer(), false}, b: {boolean(), false}) assert subtype?(closed, opt_union(closed, open)) assert subtype?(open, opt_union(closed, open)) @@ -196,23 +180,23 @@ defmodule Module.Types.DescrTest do # We assert using == on purpose as we want to return open maps assert opt_union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map() - assert opt_union(open_map(), opt_difference(open_map(), closed_map(foo: atom()))) == + assert opt_union(open_map(), opt_difference(open_map(), closed_map(foo: {atom(), false}))) == open_map() - assert opt_union(opt_difference(open_map(), closed_map(foo: atom())), open_map()) == + assert opt_union(opt_difference(open_map(), closed_map(foo: {atom(), false})), open_map()) == open_map() # Ensure no duplicate, no matter the order assert opt_union( - open_map(a: integer()), - open_map(a: number(), b: binary()) + open_map(a: {integer(), false}), + open_map(a: {number(), false}, b: {binary(), false}) ) - |> opt_union(open_map(a: integer())) == + |> opt_union(open_map(a: {integer(), false})) == opt_union( - open_map(a: number(), b: binary()), - open_map(a: integer()) + open_map(a: {number(), false}, b: {binary(), false}), + open_map(a: {integer(), false}) ) - |> opt_union(open_map(a: integer())) + |> opt_union(open_map(a: {integer(), false})) end test "list" do @@ -239,36 +223,36 @@ defmodule Module.Types.DescrTest do # Optimization one: same tags, all but one key are structurally equal assert opt_union( - open_map(a: float(), b: atom()), - open_map(a: integer(), b: atom()) + open_map(a: {float(), false}, b: {atom(), false}), + open_map(a: {integer(), false}, b: {atom(), false}) ) - |> equal?(open_map(a: opt_union(float(), integer()), b: atom())) + |> equal?(open_map(a: {opt_union(float(), integer()), false}, b: {atom(), false})) assert opt_union( - closed_map(a: float(), b: atom()), - closed_map(a: integer(), b: atom()) - ) == closed_map(a: opt_union(float(), integer()), b: atom()) + closed_map(a: {float(), false}, b: {atom(), false}), + closed_map(a: {integer(), false}, b: {atom(), false}) + ) == closed_map(a: {opt_union(float(), integer()), false}, b: {atom(), false}) # Optimization two: we can tell that one map is a subtype of the other: assert opt_union( - closed_map(a: term(), b: term()), - closed_map(a: float(), b: binary()) - ) == closed_map(a: term(), b: term()) + closed_map(a: {term(), false}, b: {term(), false}), + closed_map(a: {float(), false}, b: {binary(), false}) + ) == closed_map(a: {term(), false}, b: {term(), false}) assert opt_union( - open_map(a: term()), - closed_map(a: float(), b: binary()) - ) == open_map(a: term()) + open_map(a: {term(), false}), + closed_map(a: {float(), false}, b: {binary(), false}) + ) == open_map(a: {term(), false}) assert opt_union( - closed_map(a: float(), b: binary()), - open_map(a: term()) - ) == open_map(a: term()) + closed_map(a: {float(), false}, b: {binary(), false}), + open_map(a: {term(), false}) + ) == open_map(a: {term(), false}) assert opt_union( - closed_map(a: term(), b: tuple([term(), term()])), - closed_map(a: float(), b: tuple([atom(), binary()])) - ) == closed_map(a: term(), b: tuple([term(), term()])) + closed_map(a: {term(), false}, b: {tuple([term(), term()]), false}), + closed_map(a: {float(), false}, b: {tuple([atom(), binary()]), false}) + ) == closed_map(a: {term(), false}, b: {tuple([term(), term()]), false}) end test "optimizations (tuples)" do @@ -301,19 +285,12 @@ defmodule Module.Types.DescrTest do end end - describe "if_set" do - test "unfolds recursive nodes" do - %{X: node} = recursive(%{X: fn _recur -> integer() end}) - - assert if_set(node) == if_set(integer()) - refute is_map_key(if_set(node), :dynamic) - end - - test "preserves static parts alongside dynamic term" do - type = opt_union(atom([:value]), dynamic()) |> if_set() - - assert equal?(type, opt_union(if_set(atom([:value])), dynamic(if_set(term())))) - refute equal?(type, dynamic(if_set(term()))) + describe "map fields" do + test "map fields accept an explicit optional flag" do + assert closed_map(key: {integer(), false}) == closed_map(key: {integer(), false}) + refute subtype?(closed_map(key: {integer(), true}), closed_map(key: {integer(), false})) + assert subtype?(closed_map(key: {integer(), false}), closed_map(key: {integer(), true})) + assert empty?(open_map(key: {none(), false})) end end @@ -347,18 +324,6 @@ defmodule Module.Types.DescrTest do assert equal?(opt_intersection(term(), dynamic()), dynamic()) assert empty?(opt_intersection(dynamic(), none())) assert empty?(opt_intersection(opt_intersection(dynamic(), atom()), integer())) - - assert empty?(opt_intersection(dynamic(not_set()), term())) - refute empty?(opt_intersection(dynamic(if_set(integer())), term())) - - # Check for structural equivalence - assert opt_intersection(dynamic(not_set()), term()) == none() - assert equal?(opt_intersection(if_set(dynamic(integer())), term()), dynamic(integer())) - - assert equal?( - opt_intersection(if_set(opt_union(atom(), dynamic())), term()), - opt_union(atom(), dynamic()) - ) end test "tuple" do @@ -371,8 +336,8 @@ defmodule Module.Types.DescrTest do |> equal?(tuple([atom(), integer()])) empty_field = - closed_map(key: atom([:value])) - |> opt_difference(open_map(key: atom(), optional: if_set(atom()))) + closed_map(key: {atom([:value]), false}) + |> opt_difference(open_map(key: {atom(), false}, optional: {atom(), true})) assert empty?(empty_field) refute empty_field == none() @@ -390,76 +355,118 @@ defmodule Module.Types.DescrTest do assert opt_intersection(open_map(), open_map()) == open_map() assert equal?( - opt_intersection(closed_map(a: integer()), open_map()), - closed_map(a: integer()) + opt_intersection(closed_map(a: {integer(), false}), open_map()), + closed_map(a: {integer(), false}) ) assert equal?( - opt_intersection(closed_map(a: integer()), open_map(a: integer())), - closed_map(a: integer()) + opt_intersection( + closed_map(a: {integer(), false}), + open_map(a: {integer(), false}) + ), + closed_map(a: {integer(), false}) ) - assert opt_intersection(closed_map(a: integer()), open_map(b: not_set())) - |> equal?(closed_map(a: integer())) + assert opt_intersection(closed_map(a: {integer(), false}), open_map(b: {none(), true})) + |> equal?(closed_map(a: {integer(), false})) - assert opt_intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) - |> equal?(closed_map(a: integer())) + assert opt_intersection(closed_map(a: {integer(), false}), open_map(b: {integer(), true})) + |> equal?(closed_map(a: {integer(), false})) assert equal?( - opt_intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))), - closed_map(a: integer()) + opt_intersection( + closed_map(a: {integer(), false}), + closed_map(a: {integer(), true}) + ), + closed_map(a: {integer(), false}) ) - assert empty?(opt_intersection(closed_map(a: integer()), closed_map(a: atom()))) + assert empty?( + opt_intersection(closed_map(a: {integer(), false}), closed_map(a: {atom(), false})) + ) # Maps leaves are actually optimized, so some of the code branches # can only be tested through negations. This is the intersection between # open_map(a: integer()) and open_map(b: integer()) a_and_b = opt_negation( - opt_union(opt_negation(open_map(a: integer())), opt_negation(open_map(b: integer()))) + opt_union( + opt_negation(open_map(a: {integer(), false})), + opt_negation(open_map(b: {integer(), false})) + ) ) assert equal?( # The additional parts we are intersecting are empty opt_intersection( - opt_union(a_and_b, closed_map(c: float())), - opt_union(a_and_b, closed_map(d: float())) + opt_union(a_and_b, closed_map(c: {float(), false})), + opt_union(a_and_b, closed_map(d: {float(), false})) ), a_and_b ) # This is a regression triggered by an optimization assert opt_intersection( - closed_map(tag: atom([true]), halted: atom([true]), assigns: term()), + closed_map( + tag: {atom([true]), false}, + halted: {atom([true]), false}, + assigns: {term(), false} + ), opt_union( - closed_map(tag: atom([true]), halted: atom([true]), assigns: term()), - closed_map(tag: atom([true]), halted: term(), assigns: open_map()) + closed_map( + tag: {atom([true]), false}, + halted: {atom([true]), false}, + assigns: {term(), false} + ), + closed_map( + tag: {atom([true]), false}, + halted: {term(), false}, + assigns: {open_map(), false} + ) + ) + ) + |> equal?( + closed_map( + tag: {atom([true]), false}, + halted: {atom([true]), false}, + assigns: {term(), false} ) ) - |> equal?(closed_map(tag: atom([true]), halted: atom([true]), assigns: term())) end # Closed maps with not set keys should have no impact test "map closed with not set keys" do - assert opt_intersection(closed_map(a: integer()), closed_map(a: integer(), b: not_set())) == - closed_map(a: integer()) + assert opt_intersection( + closed_map(a: {integer(), false}), + closed_map(a: {integer(), false}, b: {none(), true}) + ) == + closed_map(a: {integer(), false}) - assert opt_intersection(closed_map(a: integer(), b: not_set()), closed_map(a: integer())) == - closed_map(a: integer()) + assert opt_intersection( + closed_map(a: {integer(), false}, b: {none(), true}), + closed_map(a: {integer(), false}) + ) == + closed_map(a: {integer(), false}) - assert opt_intersection(closed_map(a: integer()), closed_map(a: not_set())) == + assert opt_intersection(closed_map(a: {integer(), false}), closed_map(a: {none(), true})) == none() assert opt_intersection( - closed_map(a: integer(), b: not_set()), - closed_map(a: integer(), c: not_set()) + closed_map(a: {integer(), false}, b: {none(), true}), + closed_map(a: {integer(), false}, c: {none(), true}) ) == - closed_map(a: integer()) + closed_map(a: {integer(), false}) + + assert opt_intersection(empty_map(), closed_map(a: {integer(), true})) == empty_map() + assert opt_intersection(closed_map(a: {integer(), true}), empty_map()) == empty_map() - assert opt_intersection(empty_map(), closed_map(a: if_set(integer()))) == empty_map() - assert opt_intersection(closed_map(a: if_set(integer())), empty_map()) == empty_map() - refute disjoint?(empty_map(), closed_map(a: if_set(integer()))) + assert opt_intersection( + open_map(a: {integer(), true}), + closed_map(a: {atom(), true}) + ) + |> equal?(empty_map()) + + refute disjoint?(empty_map(), closed_map(a: {integer(), true})) end test "map with domain keys" do @@ -637,17 +644,6 @@ defmodule Module.Types.DescrTest do assert empty?(opt_difference(dynamic(), term())) assert empty?(opt_difference(none(), dynamic())) assert opt_difference(dynamic(integer()), integer()) == none() - - # Covers assembling a result with static :term and dynamic unfolded term. - assert opt_difference(term(), %{dynamic: none(), optional: 1}) == term() - end - - test "optional" do - # Test that difference preserves optional properties - x = if_set(dynamic(integer())) - term_map = opt_union(integer(), opt_difference(term(), integer())) - assert equal?(term_map, term()) - assert equal?(opt_difference(x, term()), opt_difference(x, term_map)) end test "tuple" do @@ -712,44 +708,52 @@ defmodule Module.Types.DescrTest do assert opt_difference(open_map(), term()) == none() assert opt_difference(open_map(), none()) == open_map() - assert empty?(opt_difference(closed_map(a: integer()), open_map())) + assert empty?(opt_difference(closed_map(a: {integer(), false}), open_map())) - assert opt_difference(closed_map(a: integer(), b: if_set(atom())), closed_map(a: integer())) - |> opt_difference(closed_map(a: integer(), b: atom())) + assert opt_difference( + closed_map(a: {integer(), false}, b: {atom(), true}), + closed_map(a: {integer(), false}) + ) + |> opt_difference(closed_map(a: {integer(), false}, b: {atom(), false})) |> empty?() refute empty?(opt_difference(open_map(), empty_map())) # Difference with single field closed map on rhs - assert opt_difference(closed_map(a: integer()), closed_map(a: integer())) == none() + assert opt_difference(closed_map(a: {integer(), false}), closed_map(a: {integer(), false})) == + none() - assert opt_difference(open_map(a: atom()), closed_map(b: integer())) - |> equal?(open_map(a: atom())) + assert opt_difference(open_map(a: {atom(), false}), closed_map(b: {integer(), false})) + |> equal?(open_map(a: {atom(), false})) - assert opt_difference(open_map(a: integer()), closed_map(b: boolean())) - |> equal?(open_map(a: integer())) + assert opt_difference(open_map(a: {integer(), false}), closed_map(b: {boolean(), false})) + |> equal?(open_map(a: {integer(), false})) # Difference with single field open map on rhs (they are optimized) - assert opt_difference(closed_map(a: integer()), open_map(a: integer())) == none() - assert opt_difference(closed_map(a: integer()), open_map(a: if_set(integer()))) == none() + assert opt_difference(closed_map(a: {integer(), false}), open_map(a: {integer(), false})) == + none() + + assert opt_difference(closed_map(a: {integer(), false}), open_map(a: {integer(), true})) == + none() - assert opt_difference(closed_map(a: integer()), open_map(b: integer())) == - closed_map(a: integer()) + assert opt_difference(closed_map(a: {integer(), false}), open_map(b: {integer(), false})) == + closed_map(a: {integer(), false}) - assert opt_difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none() + assert opt_difference(closed_map(a: {integer(), false}), open_map(b: {integer(), true})) == + none() end test "map difference of subtype" do for {k1, k2} <- [{:a, :b}, {:b, :a}, {:b, :c}, {:c, :a}, {:c, :b}, {:b, :d}] do - a = closed_map([{k1, atom([:x, :y])}, {k2, pid()}]) + a = closed_map([{k1, {atom([:x, :y]), false}}, {k2, {pid(), false}}]) b = closed_map([ - {k1, if_set(opt_union(atom([:y]), float()))}, - {k2, opt_union(pid(), opt_union(binary(), integer()))} + {k1, {opt_union(atom([:y]), float()), true}}, + {k2, {opt_union(pid(), opt_union(binary(), integer())), false}} ]) - c = open_map([{k2, if_set(opt_union(pid(), binary()))}]) + c = open_map([{k2, {opt_union(pid(), binary()), true}}]) # a <= c, while b is unrelated to both a and c. # Therefore (a \ b) <= a <= c and (a \ b) \ c is empty. @@ -766,8 +770,8 @@ defmodule Module.Types.DescrTest do test "map double negation with redundant empty map" do type = - closed_map(a: atom()) - |> opt_union(open_map(a: if_set(integer()))) + closed_map(a: {atom(), false}) + |> opt_union(open_map(a: {integer(), true})) |> opt_union(empty_map()) assert opt_negation(opt_negation(type)) |> equal?(type) @@ -778,32 +782,50 @@ defmodule Module.Types.DescrTest do atom_foo = atom([:foo]) atom_bar = atom([:bar]) - assert opt_difference(open_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == - open_map(__struct__: atom_foo) + assert opt_difference( + open_map(__struct__: {atom_foo, false}), + open_map(__struct__: {atom_bar, false}) + ) == + open_map(__struct__: {atom_foo, false}) - assert opt_difference(closed_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == - closed_map(__struct__: atom_foo) + assert opt_difference( + closed_map(__struct__: {atom_foo, false}), + open_map(__struct__: {atom_bar, false}) + ) == + closed_map(__struct__: {atom_foo, false}) - assert opt_difference(open_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == - open_map(__struct__: atom_foo) + assert opt_difference( + open_map(__struct__: {atom_foo, false}), + closed_map(__struct__: {atom_bar, false}) + ) == + open_map(__struct__: {atom_foo, false}) - assert opt_difference(closed_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == - closed_map(__struct__: atom_foo) + assert opt_difference( + closed_map(__struct__: {atom_foo, false}), + closed_map(__struct__: {atom_bar, false}) + ) == + closed_map(__struct__: {atom_foo, false}) - assert opt_difference(closed_map(__struct__: atom_foo), closed_map(__struct__: term())) == + assert opt_difference( + closed_map(__struct__: {atom_foo, false}), + closed_map(__struct__: {term(), false}) + ) == none() - assert opt_difference(closed_map(__struct__: atom()), closed_map(__struct__: atom_bar)) == - closed_map(__struct__: opt_difference(atom(), atom_bar)) + assert opt_difference( + closed_map(__struct__: {atom(), false}), + closed_map(__struct__: {atom_bar, false}) + ) == + closed_map(__struct__: {opt_difference(atom(), atom_bar), false}) # Explicitly assert we keep it as cascading differences assert %{map: {_, {_, :closed, _}, :bdd_bot, :bdd_bot, _}} = opt_difference( opt_difference( - open_map(value: term()), - closed_map(__struct__: atom_foo, value: term()) + open_map(value: {term(), false}), + closed_map(__struct__: {atom_foo, false}, value: {term(), false}) ), - closed_map(__struct__: atom_bar, name: term()) + closed_map(__struct__: {atom_bar, false}, name: {term(), false}) ) end @@ -825,12 +847,12 @@ defmodule Module.Types.DescrTest do # Difference with a non-domain key map t5 = closed_map([{domain_key(:integer), opt_union(atom(), integer())}]) - t6 = closed_map(a: atom()) + t6 = closed_map(a: {atom(), false}) assert equal?(opt_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()}]) + a_number = closed_map(a: {number(), false}) + a_number_and_pids = closed_map([{:a, {number(), false}}, {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()}]) @@ -853,7 +875,7 @@ defmodule Module.Types.DescrTest do assert empty?(opt_intersection(atom_to_pid, a_number)) # (%{:a => number} and not %{:a => float}) is %{:a => integer} - assert equal?(opt_difference(a_number, atom_to_float), closed_map(a: integer())) + assert equal?(opt_difference(a_number, atom_to_float), closed_map(a: {integer(), false})) end test "list" do @@ -959,23 +981,23 @@ defmodule Module.Types.DescrTest do describe "creation" do test "map hoists dynamic" do - assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer())) + assert dynamic(open_map(a: {integer(), false})) == open_map(a: {dynamic(integer()), false}) assert opt_union( - open_map(a: binary()), - dynamic(open_map(a: opt_union(integer(), binary()))) + open_map(a: {binary(), false}), + dynamic(open_map(a: {opt_union(integer(), binary()), false})) ) == - open_map(a: dynamic(integer()) |> opt_union(binary())) + open_map(a: {dynamic(integer()) |> opt_union(binary()), false}) # For domains too 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 - t1 = dynamic(open_map(a: if_set(integer()))) - t2 = open_map(a: if_set(dynamic(integer()))) - assert opt_union(open_map(a: not_set()), t1) == t2 + # Optional dynamic fields also must work + t1 = dynamic(open_map(a: {integer(), true})) + t2 = open_map(a: {dynamic(integer()), true}) + assert opt_union(open_map(a: {none(), true}), t1) == t2 end test "structural types preserve static part of gradual elements" do @@ -988,8 +1010,10 @@ defmodule Module.Types.DescrTest do for {descr, static_descr, dynamic_descr} <- [ {tuple([gradual, x]), tuple([static, x]), tuple([upper_bound, x])}, {open_tuple([gradual]), open_tuple([static]), open_tuple([upper_bound])}, - {closed_map(a: gradual), closed_map(a: static), closed_map(a: upper_bound)}, - {open_map(a: gradual), open_map(a: static), open_map(a: upper_bound)}, + {closed_map(a: {gradual, false}), closed_map(a: {static, false}), + closed_map(a: {upper_bound, false})}, + {open_map(a: {gradual, false}), open_map(a: {static, false}), + open_map(a: {upper_bound, false})}, {closed_map([{domain_key(:integer), gradual}]), closed_map([{domain_key(:integer), static}]), closed_map([{domain_key(:integer), upper_bound}])}, @@ -1046,17 +1070,25 @@ defmodule Module.Types.DescrTest do test "map" do assert subtype?(open_map(), term()) - assert subtype?(closed_map(a: integer()), open_map()) - assert subtype?(closed_map(a: integer()), closed_map(a: integer())) - assert subtype?(closed_map(a: integer()), open_map(a: integer())) - assert subtype?(closed_map(a: integer(), b: atom()), open_map(a: integer())) - assert subtype?(closed_map(a: integer()), closed_map(a: opt_union(integer(), atom()))) + assert subtype?(closed_map(a: {integer(), false}), open_map()) + assert subtype?(closed_map(a: {integer(), false}), closed_map(a: {integer(), false})) + assert subtype?(closed_map(a: {integer(), false}), open_map(a: {integer(), false})) + + assert subtype?( + closed_map(a: {integer(), false}, b: {atom(), false}), + open_map(a: {integer(), false}) + ) + + assert subtype?( + closed_map(a: {integer(), false}), + closed_map(a: {opt_union(integer(), atom()), false}) + ) # optional - refute subtype?(closed_map(a: if_set(integer())), closed_map(a: integer())) - 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()))) + refute subtype?(closed_map(a: {integer(), true}), closed_map(a: {integer(), false})) + assert subtype?(closed_map(a: {integer(), false}), closed_map(a: {integer(), true})) + refute subtype?(closed_map(a: {term(), true}), closed_map(a: {term(), false})) + assert subtype?(closed_map(a: {term(), false}), closed_map(a: {term(), true})) # With domains t1 = closed_map([{domain_key(:integer), number()}]) @@ -1087,13 +1119,6 @@ defmodule Module.Types.DescrTest do ) end - test "optional" do - refute subtype?(if_set(none()), term()) - refute subtype?(if_set(term()), term()) - assert subtype?(if_set(term()), if_set(term())) - refute subtype?(if_set(term()), if_set(dynamic(term()))) - end - test "list" do refute subtype?(non_empty_list(integer()), opt_difference(list(number()), list(integer()))) assert subtype?(list(term(), boolean()), list(term(), atom())) @@ -1189,8 +1214,12 @@ defmodule Module.Types.DescrTest do end test "map" do - assert compatible?(closed_map(a: integer()), open_map()) - assert compatible?(opt_intersection(dynamic(), open_map()), closed_map(a: integer())) + assert compatible?(closed_map(a: {integer(), false}), open_map()) + + assert compatible?( + opt_intersection(dynamic(), open_map()), + closed_map(a: {integer(), false}) + ) end test "list" do @@ -1249,9 +1278,11 @@ defmodule Module.Types.DescrTest do end test "map" do - assert open_map(a: none()) |> empty?() - assert closed_map(a: integer(), b: none()) |> empty?() - assert opt_intersection(closed_map(b: atom()), open_map(a: integer())) |> empty?() + assert open_map(a: {none(), false}) |> empty?() + assert closed_map(a: {integer(), false}, b: {none(), false}) |> empty?() + + assert opt_intersection(closed_map(b: {atom(), false}), open_map(a: {integer(), false})) + |> empty?() end test "fun" do @@ -1665,8 +1696,10 @@ defmodule Module.Types.DescrTest do refute singleton?(non_empty_list(atom([:foo]))) end - @disguised_empty_map closed_map(key: atom([:value])) - |> opt_difference(open_map(key: atom(), optional: if_set(atom()))) + @disguised_empty_map closed_map(key: {atom([:value]), false}) + |> opt_difference( + open_map(key: {atom(), false}, optional: {atom(), true}) + ) test "atoms" do assert singleton?(atom([:foo])) @@ -1684,16 +1717,24 @@ defmodule Module.Types.DescrTest do test "maps" do assert singleton?(empty_map()) - assert singleton?(closed_map(key: atom([:value]))) - assert singleton?(closed_map(key: atom([:value])) |> opt_union(@disguised_empty_map)) - refute singleton?(closed_map(key: binary())) - refute singleton?(closed_map(key: if_set(atom([:value])))) - refute singleton?(closed_map(__struct__: :term)) + assert singleton?(closed_map(key: {atom([:value]), false})) + + assert singleton?( + closed_map(key: {atom([:value]), false}) + |> opt_union(@disguised_empty_map) + ) + + refute singleton?(closed_map(key: {binary(), false})) + refute singleton?(closed_map(key: {atom([:value]), true})) + refute singleton?(closed_map(__struct__: {:term, false})) refute singleton?(open_map()) - refute singleton?(open_map(key: atom([:value]))) + refute singleton?(open_map(key: {atom([:value]), false})) refute singleton?( - opt_union(closed_map(key: atom([:value])), closed_map(other: atom([:value]))) + opt_union( + closed_map(key: {atom([:value]), false}), + closed_map(other: {atom([:value]), false}) + ) ) end @@ -1703,7 +1744,10 @@ defmodule Module.Types.DescrTest do refute singleton?(tuple([binary()])) refute singleton?(open_tuple([])) refute singleton?(opt_union(tuple([atom([:value])]), tuple([atom([:other_value])]))) - refute singleton?(opt_union(tuple([atom([:value])]), closed_map(other: atom([:value])))) + + refute singleton?( + opt_union(tuple([atom([:value])]), closed_map(other: {atom([:value]), false})) + ) # Both BDD lines produce the same singleton tuple, so the tuple DNF must not duplicate it. a = tuple([opt_union(integer(), atom([:ok])), atom([:x])]) @@ -2249,26 +2293,28 @@ defmodule Module.Types.DescrTest do # A non existent map type is refused assert open_map() - |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) |> map_to_list() == :badmap assert map_to_list(empty_map()) == {:ok, empty_list()} assert map_to_list(open_map()) == {:ok, list(tuple([term(), term()]))} - assert map_to_list(closed_map(a: integer())) == + assert map_to_list(closed_map(a: {integer(), false})) == {:ok, non_empty_list(tuple([atom([:a]), integer()]))} - assert map_to_list(closed_map(a: term())) == + assert map_to_list(closed_map(a: {term(), false})) == {:ok, non_empty_list(tuple([atom([:a]), term()]))} - assert map_to_list(closed_map(a: integer(), b: atom())) == + assert map_to_list(closed_map(a: {integer(), false}, b: {atom(), false})) == {:ok, non_empty_list( tuple([atom([:a]), integer()]) |> opt_union(tuple([atom([:b]), atom()])) )} - assert map_to_list(opt_union(closed_map(a: float()), closed_map(b: pid()))) == + assert map_to_list( + opt_union(closed_map(a: {float(), false}), closed_map(b: {pid(), false})) + ) == {:ok, non_empty_list( tuple([atom([:a]), float()]) @@ -2276,7 +2322,7 @@ defmodule Module.Types.DescrTest do )} # Test with struct-like descrs - assert map_to_list(closed_map(__struct__: term())) == + assert map_to_list(closed_map(__struct__: {term(), false})) == {:ok, non_empty_list(tuple([atom([:__struct__]), term()]))} # Test with domain keys @@ -2289,8 +2335,8 @@ defmodule Module.Types.DescrTest do # Test with both atom keys and domain keys map_with_both = closed_map([ - {:a, atom([:ok])}, - {:b, float()}, + {:a, {atom([:ok]), false}}, + {:b, {float(), false}}, {domain_key(:integer), binary()}, {domain_key(:tuple), pid()} ]) @@ -2306,7 +2352,9 @@ defmodule Module.Types.DescrTest do # Test open maps - should return list of key-value tuples assert map_to_list(open_map()) == {:ok, list(tuple([term(), term()]))} - assert map_to_list(open_map(a: integer())) == {:ok, non_empty_list(tuple([term(), term()]))} + + assert map_to_list(open_map(a: {integer(), false})) == + {:ok, non_empty_list(tuple([term(), term()]))} {:ok, list} = map_to_list(open_map([{domain_key(:integer), binary()}])) @@ -2344,10 +2392,15 @@ defmodule Module.Types.DescrTest do assert map_to_list(dynamic(open_map())) == {:ok, dynamic(list(tuple([term(), term()])))} - assert map_to_list(dynamic(closed_map(a: integer()))) == + assert map_to_list(dynamic(closed_map(a: {integer(), false}))) == {:ok, dynamic(non_empty_list(tuple([atom([:a]), integer()])))} - assert map_to_list(opt_union(dynamic(closed_map(a: integer())), closed_map(b: atom()))) == + assert map_to_list( + opt_union( + dynamic(closed_map(a: {integer(), false})), + closed_map(b: {atom(), false}) + ) + ) == {:ok, opt_union( non_empty_list(tuple([atom([:b]), atom()])), @@ -2366,7 +2419,10 @@ defmodule Module.Types.DescrTest do # Test with negations assert map_to_list( - opt_difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())) + opt_difference( + closed_map(a: {integer(), false}, b: {atom(), false}), + closed_map(a: {integer(), false}) + ) ) == {:ok, non_empty_list( @@ -2375,8 +2431,8 @@ defmodule Module.Types.DescrTest do )} # If a key is removed entirely by a negation, it should not appear in the result - assert closed_map(a: if_set(integer()), b: atom()) - |> opt_difference(closed_map(a: integer(), b: term())) + assert closed_map(a: {integer(), true}, b: {atom(), false}) + |> opt_difference(closed_map(a: {integer(), false}, b: {term(), false})) |> map_to_list() == {:ok, non_empty_list(tuple([atom([:b]), atom()]))} end @@ -2406,76 +2462,93 @@ defmodule Module.Types.DescrTest do assert map_fetch_key(opt_union(open_map(), integer()), :a) == :badmap assert map_fetch_key(opt_difference(open_map(), open_map()), :a) == :badmap - assert map_fetch_key(opt_difference(closed_map(a: integer()), closed_map(a: term())), :a) == + assert map_fetch_key( + opt_difference(closed_map(a: {integer(), false}), closed_map(a: {term(), false})), + :a + ) == :badmap assert map_fetch_key(open_map(), :a) == :badkey - assert map_fetch_key(open_map(a: not_set()), :a) == :badkey + assert map_fetch_key(open_map(a: {none(), true}), :a) == :badkey - assert map_fetch_key(opt_union(closed_map(a: integer()), closed_map(b: atom())), :a) == + assert map_fetch_key( + opt_union(closed_map(a: {integer(), false}), closed_map(b: {atom(), false})), + :a + ) == :badkey - assert map_fetch_key(closed_map(a: integer()), :a) == {false, integer()} + assert map_fetch_key(closed_map(a: {integer(), false}), :a) == {false, integer()} - assert map_fetch_key(opt_union(closed_map(a: integer()), closed_map(a: atom())), :a) == + assert map_fetch_key( + opt_union(closed_map(a: {integer(), false}), closed_map(a: {atom(), false})), + :a + ) == {false, opt_union(integer(), atom())} {false, value_type} = - open_map(my_map: open_map(foo: integer())) - |> opt_intersection(open_map(my_map: open_map(bar: boolean()))) + open_map(my_map: {open_map(foo: {integer(), false}), false}) + |> opt_intersection(open_map(my_map: {open_map(bar: {boolean(), false}), false})) |> map_fetch_key(:my_map) - assert equal?(value_type, open_map(foo: integer(), bar: boolean())) + assert equal?(value_type, open_map(foo: {integer(), false}, bar: {boolean(), false})) {false, value_type} = - closed_map(a: opt_union(integer(), atom())) - |> opt_difference(open_map(a: integer())) + closed_map(a: {opt_union(integer(), atom()), false}) + |> opt_difference(open_map(a: {integer(), false})) |> map_fetch_key(:a) assert equal?(value_type, atom()) {false, value_type} = - closed_map(a: integer(), b: atom()) - |> opt_difference(closed_map(a: integer(), b: atom([:foo]))) + closed_map(a: {integer(), false}, b: {atom(), false}) + |> opt_difference(closed_map(a: {integer(), false}, b: {atom([:foo]), false})) |> map_fetch_key(:a) assert equal?(value_type, integer()) {false, value_type} = - closed_map(a: integer()) - |> opt_difference(closed_map(a: atom())) + closed_map(a: {integer(), false}) + |> opt_difference(closed_map(a: {atom(), false})) |> map_fetch_key(:a) assert equal?(value_type, integer()) {false, value_type} = - open_map(a: integer(), b: atom()) - |> opt_union(closed_map(a: tuple())) + open_map(a: {integer(), false}, b: {atom(), false}) + |> opt_union(closed_map(a: {tuple(), false})) |> map_fetch_key(:a) assert equal?(value_type, opt_union(integer(), tuple())) {false, value_type} = - closed_map(a: atom()) - |> opt_difference(closed_map(a: atom([:foo, :bar]))) - |> opt_difference(closed_map(a: atom([:bar]))) + closed_map(a: {atom(), false}) + |> opt_difference(closed_map(a: {atom([:foo, :bar]), false})) + |> opt_difference(closed_map(a: {atom([:bar]), false})) |> map_fetch_key(:a) assert equal?(value_type, opt_intersection(atom(), opt_negation(atom([:foo, :bar])))) - assert closed_map(a: opt_union(atom([:ok]), pid()), b: integer(), c: tuple()) - |> opt_difference(open_map(a: atom([:ok]), b: integer())) - |> opt_difference(open_map(a: atom(), c: tuple())) + assert closed_map( + a: {opt_union(atom([:ok]), pid()), false}, + b: {integer(), false}, + c: {tuple(), false} + ) + |> opt_difference(open_map(a: {atom([:ok]), false}, b: {integer(), false})) + |> opt_difference(open_map(a: {atom(), false}, c: {tuple(), false})) |> map_fetch_key(:a) == {false, pid()} - assert closed_map(a: opt_union(atom([:foo]), pid()), b: integer(), c: tuple()) - |> opt_difference(open_map(a: atom([:foo]), b: integer())) - |> opt_difference(open_map(a: atom(), c: tuple())) + assert closed_map( + a: {opt_union(atom([:foo]), pid()), false}, + b: {integer(), false}, + c: {tuple(), false} + ) + |> opt_difference(open_map(a: {atom([:foo]), false}, b: {integer(), false})) + |> opt_difference(open_map(a: {atom(), false}, c: {tuple(), false})) |> map_fetch_key(:a) == {false, pid()} - assert closed_map(a: opt_union(atom([:foo, :bar, :baz]), integer())) - |> opt_difference(open_map(a: atom([:foo, :bar]))) - |> opt_difference(open_map(a: atom([:foo, :baz]))) + assert closed_map(a: {opt_union(atom([:foo, :bar, :baz]), integer()), false}) + |> opt_difference(open_map(a: {atom([:foo, :bar]), false})) + |> opt_difference(open_map(a: {atom([:foo, :baz]), false})) |> map_fetch_key(:a) == {false, integer()} end @@ -2487,19 +2560,25 @@ defmodule Module.Types.DescrTest do test "map_fetch_key with dynamic" do assert map_fetch_key(dynamic(), :a) == {true, dynamic()} assert map_fetch_key(opt_union(dynamic(), integer()), :a) == :badmap - assert map_fetch_key(opt_union(dynamic(open_map(a: integer())), integer()), :a) == :badmap + + assert map_fetch_key(opt_union(dynamic(open_map(a: {integer(), false})), integer()), :a) == + :badmap + assert map_fetch_key(opt_union(dynamic(integer()), integer()), :a) == :badmap - assert opt_intersection(dynamic(), open_map(a: integer())) + assert opt_intersection(dynamic(), open_map(a: {integer(), false})) |> map_fetch_key(:a) == {false, opt_intersection(integer(), dynamic())} - {false, type} = opt_union(dynamic(integer()), open_map(a: integer())) |> map_fetch_key(:a) + {false, type} = + opt_union(dynamic(integer()), open_map(a: {integer(), false})) |> map_fetch_key(:a) + assert equal?(type, integer()) - assert opt_union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch_key(:a) == + assert opt_union(dynamic(integer()), open_map(a: {integer(), true})) + |> map_fetch_key(:a) == :badkey - assert opt_union(dynamic(open_map(a: atom())), open_map(a: integer())) + assert opt_union(dynamic(open_map(a: {atom(), false})), open_map(a: {integer(), false})) |> map_fetch_key(:a) == {false, opt_union(dynamic(atom()), integer())} end @@ -2510,8 +2589,8 @@ defmodule Module.Types.DescrTest do # 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()) + t2 = closed_map(a: {none(), true}) + t3 = open_map(a: {none(), true}) # Indeed, t2 is equivalent to the empty map assert map_fetch_key(opt_difference(t1, t2), :a) == :badkey @@ -2526,11 +2605,11 @@ defmodule Module.Types.DescrTest do {true, dynamic(pid())} assert closed_map([{domain_key(:atom), number()}]) - |> opt_difference(open_map(a: if_set(integer()))) + |> opt_difference(open_map(a: {integer(), true})) |> map_fetch_key(:a) == {false, float()} assert closed_map([{domain_key(:atom), number()}]) - |> opt_difference(closed_map(b: if_set(integer()))) + |> opt_difference(closed_map(b: {integer(), true})) |> map_fetch_key(:a) == :badkey end end @@ -2539,11 +2618,14 @@ defmodule Module.Types.DescrTest do test "with static" do assert numberize(term()) == term() - assert open_map(list: list(integer(), atom()), tuple: tuple([float(), binary(), integer()])) + assert open_map( + list: {list(integer(), atom()), false}, + tuple: {tuple([float(), binary(), integer()]), false} + ) |> numberize() == open_map( - list: list(number(), atom()), - tuple: tuple([number(), binary(), number()]) + list: {list(number(), atom()), false}, + tuple: {tuple([number(), binary(), number()]), false} ) end @@ -2566,7 +2648,7 @@ defmodule Module.Types.DescrTest do # %{: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])}, + {:bar, {atom([:ok]), false}}, {domain_key(:integer), atom([:int])}, {domain_key(:float), atom([:float])}, {domain_key(:atom), binary()}, @@ -2613,7 +2695,12 @@ defmodule Module.Types.DescrTest do end test "with atom fall back" do - map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {domain_key(:atom), pid()}]) + map = + closed_map([ + {:a, {atom([:a]), false}}, + {:b, {atom([:b]), false}}, + {domain_key(:atom), pid()} + ]) assert map_get(map, atom([:a, :b])) == {:ok, atom([:a, :b])} @@ -2627,22 +2714,28 @@ defmodule Module.Types.DescrTest do assert map_get(map, atom() |> opt_difference(atom([:a]))) == {:ok, opt_union(atom([:b]), pid())} - assert map_get(closed_map(a: atom([:a]), b: atom([:b])), atom()) == + assert map_get(closed_map(a: {atom([:a]), false}, b: {atom([:b]), false}), atom()) == {:ok, atom([:a, :b])} assert map_get(closed_map([{domain_key(:atom), integer()}]), atom([:a, :b])) == {:ok, integer()} # Have one of the keys be a __struct__ - map = closed_map([{:a, atom([:a])}, {:__struct__, term()}, {domain_key(:atom), pid()}]) + map = + closed_map([ + {:a, {atom([:a]), false}}, + {:__struct__, {term(), false}}, + {domain_key(:atom), pid()} + ]) + {:ok, term} = map_get(map, atom() |> opt_difference(atom([:a]))) assert equal?(term, term()) base = open_map([{domain_key(:atom), term()}]) - bad = open_map(a: if_set(opt_negation(integer()))) + bad = open_map(a: {opt_negation(integer()), true}) map = opt_negation(opt_union(opt_negation(base), bad)) - assert equal?(map, open_map(a: integer())) + assert equal?(map, open_map(a: {integer(), false})) {:ok, type} = map_get(map, atom()) assert equal?(type, term()) @@ -2650,7 +2743,7 @@ defmodule Module.Types.DescrTest do {:ok, type} = map_get(map, atom([:a])) assert equal?(type, integer()) - map = closed_map([{:a, term()}, {domain_key(:atom), integer()}]) + map = closed_map([{:a, {term(), false}}, {domain_key(:atom), integer()}]) {:ok, type} = map_get(map, atom()) assert equal?(type, term()) @@ -2662,8 +2755,8 @@ defmodule Module.Types.DescrTest do assert equal?(type, integer()) map = - closed_map([{:a, term()}, {domain_key(:atom), integer()}]) - |> opt_difference(open_map(a: opt_negation(pid()))) + closed_map([{:a, {term(), false}}, {domain_key(:atom), integer()}]) + |> opt_difference(open_map(a: {opt_negation(pid()), false})) {:ok, type} = map_get(map, atom()) assert equal?(type, opt_union(integer(), pid())) @@ -2675,8 +2768,12 @@ defmodule Module.Types.DescrTest do assert equal?(type, integer()) map = - closed_map([{:a, term()}, {:b, binary()}, {domain_key(:atom), integer()}]) - |> opt_difference(open_map(a: opt_negation(pid()))) + closed_map([ + {:a, {term(), false}}, + {:b, {binary(), false}}, + {domain_key(:atom), integer()} + ]) + |> opt_difference(open_map(a: {opt_negation(pid()), false})) {:ok, type} = map_get(map, atom()) assert equal?(type, opt_union(opt_union(integer(), pid()), binary())) @@ -2723,91 +2820,128 @@ defmodule Module.Types.DescrTest do describe "map_update" do test "with static atom keys" do - assert map_update(open_map(key: binary()), atom([:key]), integer()) == - {binary(), open_map(key: integer()), []} + assert map_update(open_map(key: {binary(), false}), atom([:key]), integer(), false) == + {binary(), open_map(key: {integer(), false}), []} - assert map_update(dynamic(open_map(key: binary())), atom([:key]), integer()) == - {dynamic(binary()), dynamic(open_map(key: integer())), []} + assert map_update(dynamic(open_map(key: {binary(), false})), atom([:key]), integer(), false) == + {dynamic(binary()), dynamic(open_map(key: {integer(), false})), []} # Optional fail for static maps - assert map_update(open_map(key: if_set(atom([:value]))), atom([:key]), integer()) == + assert map_update(open_map(key: {atom([:value]), true}), atom([:key]), integer(), false) == {:error, [badkey: :key]} # ...unless forcing assert map_update( - open_map(key: if_set(atom([:value]))), + open_map(key: {atom([:value]), true}), atom([:key]), integer(), + false, true, true ) == - {atom([:value]), open_map(key: integer()), []} + {atom([:value]), open_map(key: {integer(), false}), []} # But optional does not fail for dynamic ones - assert map_update(dynamic(open_map(key: if_set(atom([:value])))), atom([:key]), integer()) == - {dynamic(atom([:value])), dynamic(open_map(key: integer())), []} + assert map_update( + dynamic(open_map(key: {atom([:value]), true})), + atom([:key]), + integer(), + false + ) == + {dynamic(atom([:value])), dynamic(open_map(key: {integer(), false})), []} - assert map_update(dynamic(), atom([:key]), integer()) == - {dynamic(), dynamic(open_map(key: integer())), []} + assert map_update(dynamic(), atom([:key]), integer(), false) == + {dynamic(), dynamic(open_map(key: {integer(), false})), []} # Empty value fails for static maps - assert map_update(closed_map(key: not_set()), atom([:key]), integer()) == + assert map_update(closed_map(key: {none(), true}), atom([:key]), integer(), false) == {:error, [badkey: :key]} # ...unless forcing - assert map_update(closed_map(key: not_set()), atom([:key]), integer(), true, true) == - {none(), closed_map(key: integer()), []} + assert map_update( + closed_map(key: {none(), true}), + atom([:key]), + integer(), + false, + true, + true + ) == + {none(), closed_map(key: {integer(), false}), []} # When putting multiple keys, we don't know which one will be set - assert map_update(open_map(key1: atom(), key2: binary()), atom([:key1, :key2]), integer()) == + assert map_update( + open_map(key1: {atom(), false}, key2: {binary(), false}), + atom([:key1, :key2]), + integer(), + false + ) == {opt_union(atom(), binary()), opt_union( - open_map(key1: atom(), key2: integer()), - open_map(key1: integer(), key2: binary()) + open_map(key1: {atom(), false}, key2: {integer(), false}), + open_map(key1: {integer(), false}, key2: {binary(), false}) ), []} # When putting multiple keys, all have to be set - assert map_update(open_map(key1: atom(), key2: binary()), atom([:key1, :key3]), integer()) == + assert map_update( + open_map(key1: {atom(), false}, key2: {binary(), false}), + atom([:key1, :key3]), + integer(), + false + ) == {term(), opt_union( - open_map(key1: integer(), key2: binary()), - open_map(key1: atom(), key2: binary(), key3: integer()) + open_map(key1: {integer(), false}, key2: {binary(), false}), + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ) ), [badkey: :key3]} # ...unless forcing assert map_update( - open_map(key1: atom(), key2: binary()), + open_map(key1: {atom(), false}, key2: {binary(), false}), atom([:key1, :key3]), integer(), + false, true, true ) == {term(), opt_union( - open_map(key1: integer(), key2: binary()), - open_map(key1: atom(), key2: binary(), key3: integer()) + open_map(key1: {integer(), false}, key2: {binary(), false}), + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ) ), []} # ...unless dynamic - assert map_update(dynamic(open_map()), atom([:key1, :key2]), integer()) == + assert map_update(dynamic(open_map()), atom([:key1, :key2]), integer(), false) == {dynamic(), - dynamic(opt_union(open_map(key1: integer()), open_map(key2: integer()))), []} + dynamic( + opt_union( + open_map(key1: {integer(), false}), + open_map(key2: {integer(), false}) + ) + ), []} # A "none" map assert open_map() - |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) - |> map_update(atom([:b]), integer()) == {:error, [badkey: :b]} + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) + |> map_update(atom([:b]), integer(), false) == {:error, [badkey: :b]} # ... even when forcing assert open_map() - |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) - |> map_update(atom([:b]), integer(), true, true) == {none(), none(), []} + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) + |> map_update(atom([:b]), integer(), false, true, true) == {none(), none(), []} end # Times out without a projection-aware map_update path test "with projected negative maps" do - assert map_update(projected_negative_map(100), atom([:k]), binary()) == - {open_map(), open_map(k: binary(), x: term()), []} + assert map_update(projected_negative_map(100), atom([:k]), binary(), false) == + {open_map(), open_map(k: {binary(), false}, x: {term(), false}), []} end test "with non-empty open maps does not call the callback with none from absent branches" do @@ -2816,9 +2950,9 @@ defmodule Module.Types.DescrTest do # due to the key being absent in the map. type = dynamic(opt_difference(open_map(), empty_map())) - fun = fn _optional?, value -> + fun = fn value, _optional? -> send(self(), :callback_invoked) - value + {value, false} end assert map_update_fun(type, binary(), fun, false, false) == {dynamic(none()), type, []} @@ -2827,75 +2961,103 @@ defmodule Module.Types.DescrTest do test "with dynamic atom keys" do assert {type, descr, errors} = - map_update(closed_map(key: atom([:value])), dynamic(), atom([:new_value])) + map_update( + closed_map(key: {atom([:value]), false}), + dynamic(), + atom([:new_value]), + false + ) assert equal?(type, atom([:value])) - assert equal?(descr, closed_map(key: atom([:value, :new_value]))) + assert equal?(descr, closed_map(key: {atom([:value, :new_value]), false})) assert errors == [] assert {type, descr, errors} = map_update( - dynamic(closed_map(key: atom([:value]))), + dynamic(closed_map(key: {atom([:value]), false})), dynamic(), - atom([:new_value]) + atom([:new_value]), + false ) assert equal?(type, dynamic(atom([:value]))) - assert equal?(descr, dynamic(closed_map(key: atom([:value, :new_value])))) + assert equal?(descr, dynamic(closed_map(key: {atom([:value, :new_value]), false}))) assert errors == [] # Check struct fields assert {type, descr, errors} = - map_update(open_map(__struct__: term()), dynamic(atom()), integer()) + map_update( + open_map(__struct__: {term(), false}), + dynamic(atom()), + integer(), + false + ) assert type == term() - assert equal?(descr, open_map(__struct__: term())) + assert equal?(descr, open_map(__struct__: {term(), false})) assert errors == [] # When precise dynamic keys are given, at least one must succeed assert map_update( - closed_map(key1: atom(), key2: binary()), + closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key1, :key3])), - integer() + integer(), + false ) == - {atom(), closed_map(key1: integer(), key2: binary()), []} + {atom(), closed_map(key1: {integer(), false}, key2: {binary(), false}), []} assert map_update( - closed_map(key1: atom(), key2: binary()), + closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), - integer() + integer(), + false ) == {:error, []} # ...unless forcing assert map_update( - closed_map(key1: atom(), key2: binary()), + closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), integer(), + false, true, true ) == {none(), opt_union( - closed_map(key1: atom(), key2: binary(), key3: integer()), - closed_map(key1: atom(), key2: binary(), key4: integer()) + closed_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ), + closed_map( + key1: {atom(), false}, + key2: {binary(), false}, + key4: {integer(), false} + ) ), []} # ...unless open assert map_update( - open_map(key1: atom(), key2: binary()), + open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key1, :key3])), - integer() + integer(), + false ) == {term(), opt_union( - open_map(key1: integer(), key2: binary()), - open_map(key1: atom(), key2: binary(), key3: integer()) + open_map(key1: {integer(), false}, key2: {binary(), false}), + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ) ), []} assert map_update( - open_map(key1: atom(), key2: binary()), + open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), - integer() + integer(), + false ) == {:error, []} end @@ -2907,10 +3069,10 @@ defmodule Module.Types.DescrTest do {domain_key(:port), binary()} ]) - assert map_update(map, none(), integer()) == + assert map_update(map, none(), integer(), false) == {:error, []} - assert map_update(map, integer(), integer()) == + assert map_update(map, integer(), integer(), false) == {binary(), closed_map([ {domain_key(:integer), opt_union(integer(), binary())}, @@ -2918,7 +3080,7 @@ defmodule Module.Types.DescrTest do {domain_key(:port), binary()} ]), []} - assert map_update(map, opt_union(pid(), integer()), integer()) == + assert map_update(map, opt_union(pid(), integer()), integer(), false) == {binary(), closed_map([ {domain_key(:integer), opt_union(integer(), binary())}, @@ -2926,7 +3088,7 @@ defmodule Module.Types.DescrTest do {domain_key(:port), binary()} ]), []} - assert map_update(map, opt_union(pid(), reference()), integer()) == + assert map_update(map, opt_union(pid(), reference()), integer(), false) == {binary(), closed_map([ {domain_key(:integer), binary()}, @@ -2937,7 +3099,8 @@ defmodule Module.Types.DescrTest do assert map_update( map, opt_union(pid(), dynamic(opt_union(reference(), integer()))), - integer() + integer(), + false ) == {binary(), closed_map([ @@ -2949,7 +3112,8 @@ defmodule Module.Types.DescrTest do assert map_update( map, opt_union(pid(), dynamic(opt_union(reference(), binary()))), - integer() + integer(), + false ) == {binary(), closed_map([ @@ -2958,11 +3122,18 @@ defmodule Module.Types.DescrTest do {domain_key(:port), binary()} ]), []} - assert map_update(map, dynamic(opt_union(reference(), binary())), integer()) == + assert map_update(map, dynamic(opt_union(reference(), binary())), integer(), false) == {:error, []} # ... unless forcing - assert map_update(map, dynamic(opt_union(reference(), binary())), integer(), true, true) == + assert map_update( + map, + dynamic(opt_union(reference(), binary())), + integer(), + false, + true, + true + ) == {none(), closed_map([ {domain_key(:integer), binary()}, @@ -2974,40 +3145,61 @@ defmodule Module.Types.DescrTest do # Putting dynamic atom over record keys assert {type, descr, errors} = - map_update(closed_map(key1: binary(), key2: pid()), atom(), integer()) + map_update( + closed_map(key1: {binary(), false}, key2: {pid(), false}), + atom(), + integer(), + false + ) assert equal?(type, opt_union(binary(), pid())) assert equal?( descr, opt_union( - closed_map(key1: binary(), key2: integer()), - closed_map(key1: opt_union(integer(), binary()), key2: pid()) + closed_map(key1: {binary(), false}, key2: {integer(), false}), + closed_map(key1: {opt_union(integer(), binary()), false}, key2: {pid(), false}) ) ) assert errors == [baddomain: atom()] # ... unless forcing - assert map_update(closed_map(key1: binary(), key2: pid()), atom(), integer(), true, true) == + assert map_update( + closed_map(key1: {binary(), false}, key2: {pid(), false}), + atom(), + integer(), + false, + true, + true + ) == {opt_union(binary(), pid()), [ - closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: pid()]), - closed_map(key1: integer(), key2: pid()), - closed_map(key1: binary(), key2: integer()) + closed_map([ + {domain_key(:atom), integer()}, + key1: {binary(), false}, + key2: {pid(), false} + ]), + closed_map(key1: {integer(), false}, key2: {pid(), false}), + closed_map(key1: {binary(), false}, key2: {integer(), false}) ] |> Enum.reduce(&opt_union/2), [baddomain: atom()]} assert {type, descr, errors} = - map_update(closed_map(key1: binary(), key2: pid()), dynamic(atom()), integer()) + map_update( + closed_map(key1: {binary(), false}, key2: {pid(), false}), + dynamic(atom()), + integer(), + false + ) assert equal?(type, opt_union(binary(), pid())) assert equal?( descr, opt_union( - closed_map(key1: binary(), key2: integer()), - closed_map(key1: opt_union(integer(), binary()), key2: pid()) + closed_map(key1: {binary(), false}, key2: {integer(), false}), + closed_map(key1: {opt_union(integer(), binary()), false}, key2: {pid(), false}) ) ) @@ -3015,14 +3207,14 @@ defmodule Module.Types.DescrTest do # A "none()" map assert open_map() - |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) - |> map_update(binary(), integer()) == {:error, [baddomain: binary()]} + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) + |> map_update(binary(), integer(), false) == {:error, [baddomain: binary()]} # ... even when forcing {type, descr, errors} = open_map() - |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) - |> map_update(binary(), integer(), true, true) + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) + |> map_update(binary(), integer(), false, true, true) assert empty?(type) assert empty?(descr) @@ -3030,20 +3222,21 @@ defmodule Module.Types.DescrTest do end test "with mixed keys" do - assert map_update(dynamic(), opt_union(atom([:key]), binary()), integer()) == + assert map_update(dynamic(), opt_union(atom([:key]), binary()), integer(), false) == {dynamic(), dynamic(open_map()), []} # When precise dynamic keys are given, at least one must succeed assert map_update( - closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:key]), integer())), - integer() + integer(), + false ) == {opt_union(atom(), binary()), opt_union( - closed_map([{:key, integer()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {integer(), false}}, {domain_key(:integer), binary()}]), closed_map([ - {:key, atom()}, + {:key, {atom(), false}}, {domain_key(:integer), opt_union(binary(), integer())} ]) ), []} @@ -3051,148 +3244,194 @@ defmodule Module.Types.DescrTest do # Negated keys assert {type, descr, errors} = map_update( - closed_map(key1: binary(), key2: binary()), + closed_map(key1: {binary(), false}, key2: {binary(), false}), opt_difference(atom(), atom([:key1])), - integer() + integer(), + false ) assert equal?(type, binary()) - assert equal?(descr, closed_map(key1: binary(), key2: opt_union(integer(), binary()))) + + assert equal?( + descr, + closed_map(key1: {binary(), false}, key2: {opt_union(integer(), binary()), false}) + ) + assert errors == [baddomain: atom()] assert map_update( - closed_map([key1: binary(), key2: binary()] ++ [{domain_key(:atom), pid()}]), + closed_map( + [key1: {binary(), false}, key2: {binary(), false}] ++ + [{domain_key(:atom), pid()}] + ), opt_difference(atom(), atom([:key1])), - integer() + integer(), + false ) == {opt_union(binary(), pid()), opt_union( - closed_map([{domain_key(:atom), pid()}, key1: binary(), key2: integer()]), + closed_map([ + {domain_key(:atom), pid()}, + key1: {binary(), false}, + key2: {integer(), false} + ]), closed_map([ {domain_key(:atom), opt_union(pid(), integer())}, - key1: binary(), - key2: binary() + key1: {binary(), false}, + key2: {binary(), false} ]) ), []} # Missing keys assert map_update( - closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:other_key]), pid())), - integer() + integer(), + false ) == {:error, []} # ...unless forcing assert map_update( - closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:other_key]), pid())), integer(), + false, true, true ) == {none(), opt_union( closed_map([ - {:key, atom()}, + {:key, {atom(), false}}, {domain_key(:integer), binary()}, {domain_key(:pid), integer()} ]), closed_map([ - {:key, atom()}, - {:other_key, integer()}, + {:key, {atom(), false}}, + {:other_key, {integer(), false}}, {domain_key(:integer), binary()} ]) ), []} # Popping dynamic keys - non_struct_map = opt_difference(open_map(), open_map(__struct__: atom())) - {type, descr, []} = map_update(non_struct_map, dynamic(), not_set(), true, true) + non_struct_map = opt_difference(open_map(), open_map(__struct__: {atom(), false})) + {type, descr, []} = map_update(non_struct_map, dynamic(), none(), true, true, true) assert type == term() - assert equal?(descr, open_map(__struct__: if_set(opt_negation(atom())))) + assert equal?(descr, open_map(__struct__: {opt_negation(atom()), true})) end end describe "map_put" do test "with static atom keys" do - assert map_put(open_map(key: binary()), atom([:key]), integer()) == - {:ok, open_map(key: integer())} + assert map_put(open_map(key: {binary(), false}), atom([:key]), integer()) == + {:ok, open_map(key: {integer(), false})} - assert map_put(dynamic(open_map(key: binary())), atom([:key]), integer()) == - {:ok, dynamic(open_map(key: integer()))} + assert map_put(dynamic(open_map(key: {binary(), false})), atom([:key]), integer()) == + {:ok, dynamic(open_map(key: {integer(), false}))} # Optional does not fail on put keys - assert map_put(open_map(key: if_set(atom([:value]))), atom([:key]), integer()) == - {:ok, open_map(key: integer())} + assert map_put(open_map(key: {atom([:value]), true}), atom([:key]), integer()) == + {:ok, open_map(key: {integer(), false})} # But optional does not fail for dynamic ones - assert map_put(dynamic(open_map(key: if_set(atom([:value])))), atom([:key]), integer()) == - {:ok, dynamic(open_map(key: integer()))} + assert map_put(dynamic(open_map(key: {atom([:value]), true})), atom([:key]), integer()) == + {:ok, dynamic(open_map(key: {integer(), false}))} assert map_put(dynamic(), atom([:key]), integer()) == - {:ok, dynamic(open_map(key: integer()))} + {:ok, dynamic(open_map(key: {integer(), false}))} # Empty value does not fail for put - assert map_put(closed_map(key: not_set()), atom([:key]), integer()) == - {:ok, closed_map(key: integer())} + assert map_put(closed_map(key: {none(), true}), atom([:key]), integer()) == + {:ok, closed_map(key: {integer(), false})} # When putting multiple keys, we don't know which one will be set - assert map_put(open_map(key1: atom(), key2: binary()), atom([:key1, :key2]), integer()) == + assert map_put( + open_map(key1: {atom(), false}, key2: {binary(), false}), + atom([:key1, :key2]), + integer() + ) == {:ok, opt_union( - open_map(key1: atom(), key2: integer()), - open_map(key1: integer(), key2: binary()) + open_map(key1: {atom(), false}, key2: {integer(), false}), + open_map(key1: {integer(), false}, key2: {binary(), false}) )} # When putting multiple keys, set even missing keys - assert map_put(open_map(key1: atom(), key2: binary()), atom([:key1, :key3]), integer()) == + assert map_put( + open_map(key1: {atom(), false}, key2: {binary(), false}), + atom([:key1, :key3]), + integer() + ) == {:ok, opt_union( - open_map(key1: atom(), key2: binary(), key3: integer()), - open_map(key1: integer(), key2: binary()) + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ), + open_map(key1: {integer(), false}, key2: {binary(), false}) )} assert map_put(dynamic(open_map()), atom([:key1, :key2]), integer()) == - {:ok, dynamic(opt_union(open_map(key1: integer()), open_map(key2: integer())))} + {:ok, + dynamic( + opt_union( + open_map(key1: {integer(), false}), + open_map(key2: {integer(), false}) + ) + )} end test "with dynamic/term as key-value" do - assert map_put(closed_map(key: atom([:value])), dynamic(), dynamic()) == + assert map_put(closed_map(key: {atom([:value]), false}), dynamic(), dynamic()) == {:ok, dynamic(open_map())} - assert map_put(closed_map(key: atom([:value])), dynamic(), term()) == + assert map_put(closed_map(key: {atom([:value]), false}), dynamic(), term()) == {:ok, open_map()} - assert map_put(closed_map(key: atom([:value])), term(), dynamic()) == + assert map_put(closed_map(key: {atom([:value]), false}), term(), dynamic()) == {:ok, dynamic(open_map())} - assert map_put(closed_map(key: atom([:value])), term(), term()) == + assert map_put(closed_map(key: {atom([:value]), false}), term(), term()) == {:ok, open_map()} - assert map_put(dynamic(closed_map(key: atom([:value]))), term(), term()) == + assert map_put(dynamic(closed_map(key: {atom([:value]), false})), term(), term()) == {:ok, dynamic(open_map())} end test "with dynamic atom keys" do assert map_put( - open_map(key1: atom(), key2: binary()), + open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key1, :key3])), integer() ) == {:ok, opt_union( - open_map(key1: atom(), key2: binary(), key3: integer()), - open_map(key1: integer(), key2: binary()) + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ), + open_map(key1: {integer(), false}, key2: {binary(), false}) )} assert map_put( - open_map(key1: atom(), key2: binary()), + open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), integer() ) == {:ok, opt_union( - open_map(key1: atom(), key2: binary(), key3: integer()), - open_map(key1: atom(), key2: binary(), key4: integer()) + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key3: {integer(), false} + ), + open_map( + key1: {atom(), false}, + key2: {binary(), false}, + key4: {integer(), false} + ) )} end @@ -3249,12 +3488,20 @@ defmodule Module.Types.DescrTest do ])} # Putting dynamic atom over record keys - assert map_put(closed_map(key1: binary(), key2: binary()), atom(), integer()) == + assert map_put( + closed_map(key1: {binary(), false}, key2: {binary(), false}), + atom(), + integer() + ) == {:ok, [ - closed_map(key1: binary(), key2: integer()), - closed_map(key1: integer(), key2: binary()), - closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: binary()]) + closed_map(key1: {binary(), false}, key2: {integer(), false}), + closed_map(key1: {integer(), false}, key2: {binary(), false}), + closed_map([ + {domain_key(:atom), integer()}, + key1: {binary(), false}, + key2: {binary(), false} + ]) ] |> Enum.reduce(&opt_union/2)} end @@ -3265,33 +3512,33 @@ defmodule Module.Types.DescrTest do # When precise dynamic keys are given, at least one must succeed assert map_put( - closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:key]), integer())), integer() ) == {:ok, opt_union( - closed_map([{:key, integer()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {integer(), false}}, {domain_key(:integer), binary()}]), closed_map([ - {:key, atom()}, + {:key, {atom(), false}}, {domain_key(:integer), opt_union(binary(), integer())} ]) )} assert map_put( - closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), + closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:other_key]), pid())), integer() ) == {:ok, opt_union( closed_map([ - {:key, atom()}, - {:other_key, integer()}, + {:key, {atom(), false}}, + {:other_key, {integer(), false}}, {domain_key(:integer), binary()} ]), closed_map([ - {:key, atom()}, + {:key, {atom(), false}}, {domain_key(:integer), binary()}, {domain_key(:pid), integer()} ]) @@ -3299,28 +3546,39 @@ defmodule Module.Types.DescrTest do # Negated keys assert map_put( - closed_map(key1: binary(), key2: binary()), + closed_map(key1: {binary(), false}, key2: {binary(), false}), opt_difference(atom(), atom([:key1])), integer() ) == {:ok, opt_union( - closed_map(key1: binary(), key2: integer()), - closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: binary()]) + closed_map(key1: {binary(), false}, key2: {integer(), false}), + closed_map([ + {domain_key(:atom), integer()}, + key1: {binary(), false}, + key2: {binary(), false} + ]) )} assert map_put( - closed_map([key1: binary(), key2: binary()] ++ [{domain_key(:atom), pid()}]), + closed_map( + [key1: {binary(), false}, key2: {binary(), false}] ++ + [{domain_key(:atom), pid()}] + ), opt_difference(atom(), atom([:key1])), integer() ) == {:ok, opt_union( - closed_map([{domain_key(:atom), pid()}, key1: binary(), key2: integer()]), + closed_map([ + {domain_key(:atom), pid()}, + key1: {binary(), false}, + key2: {integer(), false} + ]), closed_map([ {domain_key(:atom), opt_union(integer(), pid())}, - key1: binary(), - key2: binary() + key1: {binary(), false}, + key2: {binary(), false} ]) )} end @@ -3329,18 +3587,22 @@ defmodule Module.Types.DescrTest do test "with projected negative maps" do map = projected_negative_map(100) - assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} + assert map_put(map, atom([:k]), binary()) == + {:ok, open_map(k: {binary(), false}, x: {term(), false})} map = - opt_difference(open_map(k: integer(), x: term()), open_map(k: integer(), a: integer())) + opt_difference( + open_map(k: {integer(), false}, x: {term(), false}), + open_map(k: {integer(), false}, a: {integer(), false}) + ) {:ok, type} = map_put(map, atom([:k]), binary()) assert equal?( type, opt_difference( - open_map(k: binary(), x: term()), - open_map(k: binary(), a: integer()) + open_map(k: {binary(), false}, x: {term(), false}), + open_map(k: {binary(), false}, a: {integer(), false}) ) ) end @@ -3349,20 +3611,16 @@ defmodule Module.Types.DescrTest do # map_put/3 passes nil as the popped value accumulator because it only needs the map side. map = projected_negative_map(100) - |> opt_difference(open_map(k: atom(), x: term())) + |> opt_difference(open_map(k: {atom(), false}, x: {term(), false})) - assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} + assert map_put(map, atom([:k]), binary()) == + {:ok, open_map(k: {binary(), false}, x: {term(), false})} end end describe "disjoint" do - test "optional" do - assert disjoint?(term(), if_set(none())) - assert disjoint?(term(), if_set(none()) |> opt_union(non_empty_list(none()))) - end - test "map" do - refute disjoint?(open_map(), open_map(a: integer())) + refute disjoint?(open_map(), open_map(a: {integer(), false})) end end @@ -3443,7 +3701,7 @@ defmodule Module.Types.DescrTest do assert opt_union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == "dynamic() or :bar or :foo" - assert opt_intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == + assert opt_intersection(dynamic(), closed_map(a: {integer(), false})) |> to_quoted_string() == "dynamic(%{a: integer()})" end @@ -3547,7 +3805,7 @@ defmodule Module.Types.DescrTest do |> to_quoted_string() == "{integer(), atom()}" - assert tuple([closed_map(a: integer()), open_map()]) |> to_quoted_string() == + assert tuple([closed_map(a: {integer(), false}), open_map()]) |> to_quoted_string() == "{%{a: integer()}, map()}" assert opt_union(tuple([integer(), atom()]), tuple([integer(), atom()])) @@ -3579,14 +3837,18 @@ defmodule Module.Types.DescrTest do |> opt_union(tuple([atom([:ok]), term()])) |> to_quoted_string() == "{:ok, term()}" - assert tuple([closed_map(a: integer(), b: atom()), open_map()]) - |> opt_union(tuple([closed_map(a: integer(), b: atom()), open_map()])) + assert tuple([closed_map(a: {integer(), false}, b: {atom(), false}), open_map()]) + |> opt_union( + tuple([closed_map(a: {integer(), false}, b: {atom(), false}), open_map()]) + ) |> to_quoted_string() == "{%{a: integer(), b: atom()}, map()}" # Nested fusion - assert tuple([closed_map(a: integer(), b: atom()), open_map()]) - |> opt_union(tuple([closed_map(a: float(), b: atom()), open_map()])) + assert tuple([closed_map(a: {integer(), false}, b: {atom(), false}), open_map()]) + |> opt_union( + tuple([closed_map(a: {float(), false}, b: {atom(), false}), open_map()]) + ) |> to_quoted_string() == "{%{a: float() or integer(), b: atom()}, map()}" @@ -3604,26 +3866,26 @@ defmodule Module.Types.DescrTest do # ``` decimal_inf = closed_map( - __struct__: atom([Decimal]), - coef: atom([:inf]), - exp: integer(), - sign: integer() + __struct__: {atom([Decimal]), false}, + coef: {atom([:inf]), false}, + exp: {integer(), false}, + sign: {integer(), false} ) decimal_nan = closed_map( - __struct__: atom([Decimal]), - coef: atom([:NaN]), - exp: integer(), - sign: integer() + __struct__: {atom([Decimal]), false}, + coef: {atom([:NaN]), false}, + exp: {integer(), false}, + sign: {integer(), false} ) decimal_int = closed_map( - __struct__: atom([Decimal]), - coef: integer(), - exp: integer(), - sign: integer() + __struct__: {atom([Decimal]), false}, + coef: {integer(), false}, + exp: {integer(), false}, + sign: {integer(), false} ) assert atom([:error]) @@ -3770,122 +4032,174 @@ defmodule Module.Types.DescrTest do assert empty_map() |> to_quoted_string() == "empty_map()" assert open_map() |> to_quoted_string() == "map()" - assert closed_map(a: integer()) |> to_quoted_string() == "%{a: integer()}" - assert open_map(a: float()) |> to_quoted_string() == "%{..., a: float()}" + assert closed_map(a: {integer(), false}) |> to_quoted_string() == "%{a: integer()}" + assert open_map(a: {float(), false}) |> to_quoted_string() == "%{..., a: float()}" - assert closed_map("Elixir.Foo.Bar": integer()) |> to_quoted_string() == + assert closed_map([{Foo.Bar, {integer(), false}}]) |> to_quoted_string() == "%{Foo.Bar => integer()}" - assert open_map("Elixir.Foo.Bar": float()) |> to_quoted_string() == + assert open_map([{Foo.Bar, {float(), false}}]) |> to_quoted_string() == "%{..., Foo.Bar => float()}" - assert opt_difference(open_map(), open_map(a: term())) |> to_quoted_string() == + assert opt_difference(open_map(), open_map(a: {term(), false})) |> to_quoted_string() == "%{..., a: not_set()}" - assert closed_map(a: integer(), b: atom()) |> to_quoted_string() == + assert closed_map(a: {integer(), false}, b: {atom(), false}) |> to_quoted_string() == "%{a: integer(), b: atom()}" - assert open_map(a: float()) - |> opt_difference(closed_map(a: float())) + assert open_map(a: {float(), false}) + |> opt_difference(closed_map(a: {float(), false})) |> to_quoted_string() == "%{..., a: float()} and not %{a: float()}" assert opt_difference(open_map(), empty_map()) |> to_quoted_string() == "map() and not empty_map()" - assert closed_map(foo: opt_union(integer(), not_set())) |> to_quoted_string() == + assert closed_map(foo: {integer(), true}) |> to_quoted_string() == "%{foo: if_set(integer())}" # Test normalization - assert open_map(a: integer(), b: atom()) - |> opt_difference(open_map(b: atom())) - |> opt_union(open_map(a: integer())) + assert open_map(a: {integer(), false}, b: {atom(), false}) + |> opt_difference(open_map(b: {atom(), false})) + |> opt_union(open_map(a: {integer(), false})) |> to_quoted_string() == "%{..., a: integer()}" - assert opt_union(open_map(a: integer()), open_map(a: integer())) |> to_quoted_string() == + assert opt_union(open_map(a: {integer(), false}), open_map(a: {integer(), false})) + |> to_quoted_string() == "%{..., a: integer()}" - assert opt_difference(open_map(a: number(), b: atom()), open_map(a: integer())) + assert opt_difference( + open_map(a: {number(), false}, b: {atom(), false}), + open_map(a: {integer(), false}) + ) |> to_quoted_string() == "%{..., a: float(), b: atom()}" # Basic map fusion - assert opt_union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() == + assert opt_union(closed_map(a: {integer(), false}), closed_map(a: {integer(), false})) + |> to_quoted_string() == "%{a: integer()}" - assert opt_union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() == + assert opt_union(closed_map(a: {integer(), false}), closed_map(a: {float(), false})) + |> to_quoted_string() == "%{a: float() or integer()}" # Nested fusion - assert opt_union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom())) - |> opt_union(closed_map(x: pid(), y: pid(), z: port())) - |> opt_union(closed_map(x: pid(), y: pid(), z: atom())) + assert opt_union( + closed_map(a: {integer(), false}, b: {atom(), false}), + closed_map(a: {float(), false}, b: {atom(), false}) + ) + |> opt_union(closed_map(x: {pid(), false}, y: {pid(), false}, z: {port(), false})) + |> opt_union(closed_map(x: {pid(), false}, y: {pid(), false}, z: {atom(), false})) |> to_quoted_string() == "%{a: float() or integer(), b: atom()} or %{x: pid(), y: pid(), z: atom() or port()}" # Open map fusion - assert opt_union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() == + assert opt_union(open_map(a: {integer(), false}), open_map(a: {float(), false})) + |> to_quoted_string() == "%{..., a: float() or integer()}" # Fusing complex nested maps with unions assert closed_map( - status: atom([:ok]), - data: closed_map(value: term(), count: empty_list()) + status: {atom([:ok]), false}, + data: {closed_map(value: {term(), false}, count: {empty_list(), false}), false} ) |> opt_union( closed_map( - status: atom([:ok]), - data: closed_map(value: term(), count: open_map()) + status: {atom([:ok]), false}, + data: {closed_map(value: {term(), false}, count: {open_map(), false}), false} ) ) - |> opt_union(closed_map(status: atom([:error]), reason: atom([:timeout]))) - |> opt_union(closed_map(status: atom([:error]), reason: atom([:crash]))) + |> opt_union( + closed_map(status: {atom([:error]), false}, reason: {atom([:timeout]), false}) + ) + |> opt_union( + closed_map(status: {atom([:error]), false}, reason: {atom([:crash]), false}) + ) |> to_quoted_string() == "%{data: %{count: empty_list() or map(), value: term()}, status: :ok} or\n %{reason: :crash or :timeout, status: :error}" # Difference and union tests - assert closed_map(status: atom([:ok]), value: term()) - |> opt_difference(closed_map(status: atom([:ok]), value: float())) + assert closed_map(status: {atom([:ok]), false}, value: {term(), false}) + |> opt_difference(closed_map(status: {atom([:ok]), false}, value: {float(), false})) |> opt_union( - closed_map(status: atom([:ok]), value: term()) - |> opt_difference(closed_map(status: atom([:ok]), value: integer())) + closed_map(status: {atom([:ok]), false}, value: {term(), false}) + |> opt_difference( + closed_map(status: {atom([:ok]), false}, value: {integer(), false}) + ) ) |> to_quoted_string() == "%{status: :ok, value: term()}" # Nested map fusion - assert closed_map(data: closed_map(x: integer(), y: atom()), meta: open_map()) - |> opt_union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map())) + assert closed_map( + data: {closed_map(x: {integer(), false}, y: {atom(), false}), false}, + meta: {open_map(), false} + ) + |> opt_union( + closed_map( + data: {closed_map(x: {float(), false}, y: {atom(), false}), false}, + meta: {open_map(), false} + ) + ) |> to_quoted_string() == "%{data: %{x: float() or integer(), y: atom()}, meta: map()}" # Test complex combinations assert opt_intersection( - open_map(a: number(), b: atom()), - open_map(a: integer(), c: boolean()) + open_map(a: {number(), false}, b: {atom(), false}), + open_map(a: {integer(), false}, c: {boolean(), false}) + ) + |> opt_union( + opt_difference(open_map(x: {atom(), false}), open_map(x: {boolean(), false})) ) - |> opt_union(opt_difference(open_map(x: atom()), open_map(x: boolean()))) |> to_quoted_string() == "%{..., a: integer(), b: atom(), c: boolean()} or %{..., x: atom() and not boolean()}" - assert closed_map(a: number(), b: atom(), c: pid()) - |> opt_difference(closed_map(a: integer(), b: atom(), c: pid())) + assert closed_map(a: {number(), false}, b: {atom(), false}, c: {pid(), false}) + |> opt_difference( + closed_map(a: {integer(), false}, b: {atom(), false}, c: {pid(), false}) + ) |> to_quoted_string() == "%{a: float(), b: atom(), c: pid()}" # No simplification compared to above, as it is an open map - assert open_map(a: number(), b: atom()) - |> opt_difference(closed_map(a: integer(), b: atom())) + assert open_map(a: {number(), false}, b: {atom(), false}) + |> opt_difference(closed_map(a: {integer(), false}, b: {atom(), false})) |> to_quoted_string() == "%{..., a: float() or integer(), b: atom()} and not %{a: integer(), b: atom()}" # Remark: this simplification is order dependent. Having the first difference # after the second gives a different result. - assert open_map(a: number(), b: atom(), c: opt_union(pid(), port())) - |> opt_difference(open_map(a: integer(), b: atom(), c: opt_union(pid(), port()))) - |> opt_difference(open_map(a: float(), b: atom(), c: pid())) + assert open_map( + a: {number(), false}, + b: {atom(), false}, + c: {opt_union(pid(), port()), false} + ) + |> opt_difference( + open_map( + a: {integer(), false}, + b: {atom(), false}, + c: {opt_union(pid(), port()), false} + ) + ) + |> opt_difference( + open_map(a: {float(), false}, b: {atom(), false}, c: {pid(), false}) + ) |> to_quoted_string() == "%{..., a: float(), b: atom(), c: port()}" - assert open_map(a: number(), b: atom(), c: opt_union(pid(), port())) - |> opt_difference(open_map(a: float(), b: atom(), c: pid())) - |> opt_difference(open_map(a: integer(), b: atom(), c: opt_union(pid(), port()))) + assert open_map( + a: {number(), false}, + b: {atom(), false}, + c: {opt_union(pid(), port()), false} + ) + |> opt_difference( + open_map(a: {float(), false}, b: {atom(), false}, c: {pid(), false}) + ) + |> opt_difference( + open_map( + a: {integer(), false}, + b: {atom(), false}, + c: {opt_union(pid(), port()), false} + ) + ) |> to_quoted_string() == "%{..., a: float(), b: atom(), c: port()}" end @@ -3902,59 +4216,77 @@ defmodule Module.Types.DescrTest do ]) |> to_quoted_string() == "%{bitstring() => integer()}" - assert closed_map([{domain_key(:integer), not_set()}, {:float, float()}]) + assert closed_map([{domain_key(:integer), none()}, float: {float(), false}]) |> to_quoted_string() == "%{integer() => not_set(), float: float()}" end test "map (negation)" do - assert open_map(a: integer()) |> opt_negation() |> to_quoted_string() == + assert open_map(a: {integer(), false}) |> opt_negation() |> to_quoted_string() == "not (map() and not %{..., a: if_set(not integer())})" - assert open_map(a: opt_negation(integer())) |> to_quoted_string() == + assert open_map(a: {opt_negation(integer()), false}) |> to_quoted_string() == "%{..., a: not integer()}" - assert closed_map(a: integer()) |> opt_negation() |> to_quoted_string() == + assert closed_map(a: {integer(), false}) |> opt_negation() |> to_quoted_string() == "not %{a: integer()}" - assert closed_map(a: opt_negation(integer())) |> to_quoted_string() == + assert closed_map(a: {opt_negation(integer()), false}) |> to_quoted_string() == "%{a: not integer()}" end test "structs" do - assert open_map(__struct__: term()) |> to_quoted_string() == + assert open_map(__struct__: {term(), false}) |> to_quoted_string() == "%{..., __struct__: term()}" - assert open_map(__struct__: atom([URI])) |> to_quoted_string() == + assert open_map(__struct__: {atom([URI]), false}) |> to_quoted_string() == "%{..., __struct__: URI}" - assert closed_map(__struct__: atom([URI])) |> to_quoted_string() == + assert closed_map(__struct__: {atom([URI]), false}) |> to_quoted_string() == "%{__struct__: URI}" - assert closed_map(__struct__: atom([NoFieldsStruct])) |> to_quoted_string() == + assert closed_map(__struct__: {atom([NoFieldsStruct]), false}) |> to_quoted_string() == "%NoFieldsStruct{}" - assert closed_map(__struct__: atom([URI, Another])) |> to_quoted_string() == + assert closed_map(__struct__: {atom([URI, Another]), false}) |> to_quoted_string() == "%{__struct__: Another or URI}" - assert closed_map(__struct__: atom([Decimal]), coef: term(), exp: term(), sign: term()) + assert closed_map( + __struct__: {atom([Decimal]), false}, + coef: {term(), false}, + exp: {term(), false}, + sign: {term(), false} + ) |> to_quoted_string(collapse_structs: false) == "%Decimal{sign: term(), coef: term(), exp: term()}" - assert closed_map(__struct__: atom([Decimal]), coef: term(), exp: term(), sign: term()) + assert closed_map( + __struct__: {atom([Decimal]), false}, + coef: {term(), false}, + exp: {term(), false}, + sign: {term(), false} + ) |> to_quoted_string() == "%Decimal{}" - assert closed_map(__struct__: atom([Decimal]), coef: term(), exp: term(), sign: integer()) + assert closed_map( + __struct__: {atom([Decimal]), false}, + coef: {term(), false}, + exp: {term(), false}, + sign: {integer(), false} + ) |> to_quoted_string() == "%Decimal{sign: integer()}" # Does not fuse structs - assert opt_union(closed_map(__struct__: atom([Foo])), closed_map(__struct__: atom([Bar]))) + assert opt_union( + closed_map(__struct__: {atom([Foo]), false}), + closed_map(__struct__: {atom([Bar]), false}) + ) |> to_quoted_string() == "%{__struct__: Bar} or %{__struct__: Foo}" # Properly format non_struct_map - assert open_map(__struct__: if_set(opt_negation(atom()))) |> to_quoted_string() == + assert open_map(__struct__: {opt_negation(atom()), true}) |> to_quoted_string() == "non_struct_map()" end end @@ -3992,47 +4324,48 @@ defmodule Module.Types.DescrTest do test "map difference" do # Create a large map with various types map1 = - open_map([ - {:id, integer()}, - {:name, binary()}, - {:age, opt_union(integer(), atom())}, - {:email, binary()}, - {:active, boolean()}, - {:tags, list(atom())} - ]) + open_map( + id: {integer(), false}, + name: {binary(), false}, + age: {opt_union(integer(), atom()), false}, + email: {binary(), false}, + active: {boolean(), false}, + tags: {list(atom()), false} + ) # Create another large map with some differences and many more entries map2 = open_map( [ - {:id, integer()}, - {:name, binary()}, - {:age, integer()}, - {:email, binary()}, - {:active, boolean()}, - {:tags, non_empty_list(atom())}, - {:meta, - open_map([ - {:created_at, binary()}, - {:updated_at, binary()}, - {:status, atom()} - ])}, - {:permissions, tuple([atom(), integer(), atom()])}, - {:profile, - open_map([ - {:bio, binary()}, - {:interests, non_empty_list(binary())}, - {:social_media, - open_map([ - {:twitter, binary()}, - {:instagram, binary()}, - {:linkedin, binary()} - ])} - ])}, - {:notifications, boolean()} + id: {integer(), false}, + name: {binary(), false}, + age: {integer(), false}, + email: {binary(), false}, + active: {boolean(), false}, + tags: {non_empty_list(atom()), false}, + meta: + {open_map( + created_at: {binary(), false}, + updated_at: {binary(), false}, + status: {atom(), false} + ), false}, + permissions: {tuple([atom(), integer(), atom()]), false}, + profile: + {open_map( + bio: {binary(), false}, + interests: {non_empty_list(binary()), false}, + social_media: + {open_map( + twitter: {binary(), false}, + instagram: {binary(), false}, + linkedin: {binary(), false} + ), false} + ), false}, + notifications: {boolean(), false} ] ++ Enum.map(1..50, fn i -> - {String.to_unsafe_atom("field_#{i}"), atom([String.to_unsafe_atom("value_#{i}")])} + {String.to_unsafe_atom("field_#{i}"), + {atom([String.to_unsafe_atom("value_#{i}")]), false}} end) ) @@ -4041,12 +4374,12 @@ defmodule Module.Types.DescrTest do end test "map intersection and then difference" do - actual = open_map(__struct__: atom(), __exception__: atom([true])) + actual = open_map(__struct__: {atom(), false}, __exception__: {atom([true]), false}) expected = for i <- 1..50 do name = String.to_unsafe_atom("name_#{i}") - closed_map([__struct__: atom([name])] ++ [{name, binary()}]) + closed_map([__struct__: {atom([name]), false}] ++ [{name, {binary(), false}}]) end |> Enum.reduce(&opt_union/2) @@ -4057,90 +4390,112 @@ defmodule Module.Types.DescrTest do test "struct difference" do entries = [ - closed_map(__struct__: atom([MapSet]), map: term()), - closed_map(__struct__: atom([Jason.OrderedObject]), values: term()), - closed_map(__struct__: atom([GenEvent.Stream]), timeout: term(), manager: term()), - closed_map(__struct__: atom([HashDict]), size: term(), root: term()), - closed_map(__struct__: atom([HashSet]), size: term(), root: term()), + closed_map(__struct__: {atom([MapSet]), false}, map: {term(), false}), + closed_map(__struct__: {atom([Jason.OrderedObject]), false}, values: {term(), false}), + closed_map( + __struct__: {atom([GenEvent.Stream]), false}, + timeout: {term(), false}, + manager: {term(), false} + ), closed_map( - __struct__: atom([IO.Stream]), - raw: term(), - device: term(), - line_or_bytes: term() + __struct__: {atom([HashDict]), false}, + size: {term(), false}, + root: {term(), false} ), - closed_map(__struct__: atom([Range]), first: term(), last: term(), step: term()), closed_map( - __struct__: atom([Stream]), - enum: term(), - done: term(), - funs: term(), - accs: term() + __struct__: {atom([HashSet]), false}, + size: {term(), false}, + root: {term(), false} ), closed_map( - __struct__: atom([Req.Response.Async]), - pid: term(), - ref: term(), - stream_fun: term(), - cancel_fun: term() + __struct__: {atom([IO.Stream]), false}, + raw: {term(), false}, + device: {term(), false}, + line_or_bytes: {term(), false} ), closed_map( - __struct__: atom([Postgrex.Stream]), - options: term(), - params: term(), - query: term(), - conn: term() + __struct__: {atom([Range]), false}, + first: {term(), false}, + last: {term(), false}, + step: {term(), false} ), closed_map( - __struct__: atom([DBConnection.PrepareStream]), - opts: term(), - params: term(), - query: term(), - conn: term() + __struct__: {atom([Stream]), false}, + enum: {term(), false}, + done: {term(), false}, + funs: {term(), false}, + accs: {term(), false} ), closed_map( - __struct__: atom([DBConnection.Stream]), - opts: term(), - params: term(), - query: term(), - conn: term() + __struct__: {atom([Req.Response.Async]), false}, + pid: {term(), false}, + ref: {term(), false}, + stream_fun: {term(), false}, + cancel_fun: {term(), false} ), closed_map( - __struct__: atom([Ecto.Adapters.SQL.Stream]), - meta: term(), - opts: term(), - params: term(), - statement: term() + __struct__: {atom([Postgrex.Stream]), false}, + options: {term(), false}, + params: {term(), false}, + query: {term(), false}, + conn: {term(), false} ), closed_map( - __struct__: atom([Date.Range]), - first: term(), - last: term(), - step: term(), - first_in_iso_days: term(), - last_in_iso_days: term() + __struct__: {atom([DBConnection.PrepareStream]), false}, + opts: {term(), false}, + params: {term(), false}, + query: {term(), false}, + conn: {term(), false} ), closed_map( - __struct__: atom([File.Stream]), - node: term(), - raw: term(), - path: term(), - modes: term(), - line_or_bytes: term() + __struct__: {atom([DBConnection.Stream]), false}, + opts: {term(), false}, + params: {term(), false}, + query: {term(), false}, + conn: {term(), false} ), closed_map( - __struct__: atom([Phoenix.LiveView.LiveStream]), - name: term(), - ref: term(), - inserts: term(), - deletes: term(), - reset?: term(), - dom_id: term(), - consumable?: term() + __struct__: {atom([Ecto.Adapters.SQL.Stream]), false}, + meta: {term(), false}, + opts: {term(), false}, + params: {term(), false}, + statement: {term(), false} + ), + closed_map( + __struct__: {atom([Date.Range]), false}, + first: {term(), false}, + last: {term(), false}, + step: {term(), false}, + first_in_iso_days: {term(), false}, + last_in_iso_days: {term(), false} + ), + closed_map( + __struct__: {atom([File.Stream]), false}, + node: {term(), false}, + raw: {term(), false}, + path: {term(), false}, + modes: {term(), false}, + line_or_bytes: {term(), false} + ), + closed_map( + __struct__: {atom([Phoenix.LiveView.LiveStream]), false}, + name: {term(), false}, + ref: {term(), false}, + inserts: {term(), false}, + deletes: {term(), false}, + reset?: {term(), false}, + dom_id: {term(), false}, + consumable?: {term(), false} ) ] range = - closed_map(__struct__: atom([Range]), first: integer(), last: integer(), step: integer()) + closed_map( + __struct__: {atom([Range]), false}, + first: {integer(), false}, + last: {integer(), false}, + step: {integer(), false} + ) assert subtype?(range, Enum.reduce(entries, &opt_union/2)) end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 46759aabea..0de7ebf4c7 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -431,7 +431,7 @@ defmodule Module.Types.ExprTest do 123 = x.baz_bat x ) - ) == dynamic(open_map(foo_bar: atom([:foo]), baz_bat: integer())) + ) == dynamic(open_map(foo_bar: {atom([:foo]), false}, baz_bat: {integer(), false})) end test "infers args" do @@ -1059,8 +1059,8 @@ defmodule Module.Types.ExprTest do describe "maps" do test "creating maps as records" do - assert typecheck!(%{foo: :bar}) == closed_map(foo: atom([:bar])) - assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: term())) + assert typecheck!(%{foo: :bar}) == closed_map(foo: {atom([:bar]), false}) + assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: {term(), false})) end test "creating maps as records with dynamic keys" do @@ -1069,7 +1069,7 @@ defmodule Module.Types.ExprTest do foo = :foo %{foo => :first, foo => :second} ) - ) == closed_map(foo: atom([:second])) + ) == closed_map(foo: {atom([:second]), false}) assert typecheck!( ( @@ -1083,10 +1083,14 @@ defmodule Module.Types.ExprTest do ) ) |> equal?( - closed_map(foo: atom([:second])) - |> opt_union(closed_map(bar: atom([:second]))) - |> opt_union(closed_map(foo: atom([:first]), bar: atom([:second]))) - |> opt_union(closed_map(bar: atom([:first]), foo: atom([:second]))) + closed_map(foo: {atom([:second]), false}) + |> opt_union(closed_map(bar: {atom([:second]), false})) + |> opt_union( + closed_map(foo: {atom([:first]), false}, bar: {atom([:second]), false}) + ) + |> opt_union( + closed_map(bar: {atom([:first]), false}, foo: {atom([:second]), false}) + ) ) end @@ -1096,10 +1100,7 @@ defmodule Module.Types.ExprTest do # Since key cannot override :foo based on position, we preserve it assert typecheck!([key], %{key => 456, foo: :bar}) == dynamic( - closed_map([ - {to_domain_keys(:term), integer()}, - {:foo, atom([:bar])} - ]) + closed_map([{to_domain_keys(:term), integer()}, foo: {atom([:bar]), false}]) ) # Since key can override :foo based on position, we union it @@ -1107,7 +1108,7 @@ defmodule Module.Types.ExprTest do dynamic( closed_map([ {to_domain_keys(:term), atom([:baz])}, - {:foo, atom([:bar, :baz])} + foo: {atom([:bar, :baz]), false} ]) ) @@ -1119,10 +1120,7 @@ defmodule Module.Types.ExprTest do %{:foo => :bar, key => :baz} ) ) == - closed_map([ - {domain_key(:integer), atom([:baz])}, - {:foo, atom([:bar])} - ]) + closed_map([{domain_key(:integer), atom([:baz])}, foo: {atom([:bar]), false}]) # Multiple keys are fully overridden for simplicity assert typecheck!( @@ -1134,23 +1132,17 @@ defmodule Module.Types.ExprTest do ) ) == opt_union( - closed_map([ - {domain_key(:integer), atom([:new])}, - {:foo, atom([:old])} - ]), - closed_map([ - {domain_key(:integer), atom([:new])}, - {:bar, atom([:old])} - ]) + closed_map([{domain_key(:integer), atom([:new])}, foo: {atom([:old]), false}]), + closed_map([{domain_key(:integer), atom([:new])}, bar: {atom([:old]), false}]) ) end test "updating to maps as records" do assert typecheck!([x], %{x | x: :zero}) == - dynamic(open_map(x: atom([:zero]))) + dynamic(open_map(x: {atom([:zero]), false})) assert typecheck!([x], %{%{x | x: :zero} | y: :one}) == - dynamic(open_map(x: atom([:zero]), y: atom([:one]))) + dynamic(open_map(x: {atom([:zero]), false}, y: {atom([:one]), false})) assert typecheck!( ( @@ -1165,10 +1157,14 @@ defmodule Module.Types.ExprTest do ) ) |> equal?( - closed_map(key1: atom([:one]), key2: atom([:two!])) - |> opt_union(closed_map(key1: atom([:two!]), key2: atom([:one!]))) - |> opt_union(closed_map(key1: atom([:one!]), key2: atom([:two!]))) - |> opt_union(closed_map(key1: atom([:two!]), key2: atom([:two]))) + closed_map(key1: {atom([:one]), false}, key2: {atom([:two!]), false}) + |> opt_union( + closed_map(key1: {atom([:two!]), false}, key2: {atom([:one!]), false}) + ) + |> opt_union( + closed_map(key1: {atom([:one!]), false}, key2: {atom([:two!]), false}) + ) + |> opt_union(closed_map(key1: {atom([:two!]), false}, key2: {atom([:two]), false})) ) assert typeerror!([x = :foo], %{x | x: :zero}) == ~l""" @@ -1283,7 +1279,7 @@ defmodule Module.Types.ExprTest do x = %{foo: :bar} %{x | key => :baz} ) - ) == closed_map(foo: atom([:bar, :baz])) + ) == closed_map(foo: {atom([:bar, :baz]), false}) # Override based on position assert typecheck!( @@ -1292,7 +1288,7 @@ defmodule Module.Types.ExprTest do x = %{foo: :bar, baz: :bat} %{x | key => :old, foo: :new} ) - ) == closed_map(foo: atom([:new]), baz: atom([:old, :bat])) + ) == closed_map(foo: {atom([:new]), false}, baz: {atom([:old, :bat]), false}) assert typeerror!( [key], @@ -1377,18 +1373,18 @@ defmodule Module.Types.ExprTest do test "creating structs" do assert typecheck!(%Point{}) == closed_map( - __struct__: atom([Point]), - x: atom([nil]), - y: atom([nil]), - z: integer() + __struct__: {atom([Point]), false}, + x: {atom([nil]), false}, + y: {atom([nil]), false}, + z: {integer(), false} ) assert typecheck!(%Point{x: :zero}) == closed_map( - __struct__: atom([Point]), - x: atom([:zero]), - y: atom([nil]), - z: integer() + __struct__: {atom([Point]), false}, + x: {atom([:zero]), false}, + y: {atom([nil]), false}, + z: {integer(), false} ) end @@ -1427,11 +1423,11 @@ defmodule Module.Types.ExprTest do integer_date_type = dynamic( closed_map( - __struct__: atom([Date]), - day: integer(), - calendar: atom(), - month: term(), - year: term() + __struct__: {atom([Date]), false}, + day: {integer(), false}, + calendar: {atom(), false}, + month: {term(), false}, + year: {term(), false} ) ) @@ -2185,7 +2181,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(open_map(foo: term())) + ) == dynamic(open_map(foo: {term(), false})) assert typecheck!( [x, key], @@ -2874,14 +2870,14 @@ defmodule Module.Types.ExprTest do dynamic( opt_union( closed_map( - __struct__: atom([ArgumentError]), - __exception__: term(), - message: term() + __struct__: {atom([ArgumentError]), false}, + __exception__: {term(), false}, + message: {term(), false} ), closed_map( - __struct__: atom([RuntimeError]), - __exception__: term(), - message: term() + __struct__: {atom([RuntimeError]), false}, + __exception__: {term(), false}, + message: {term(), false} ) ) ) @@ -2896,8 +2892,8 @@ defmodule Module.Types.ExprTest do end ) == open_map( - __struct__: atom(), - __exception__: term() + __struct__: {atom(), false}, + __exception__: {term(), false} ) end @@ -3501,12 +3497,20 @@ defmodule Module.Types.ExprTest do assert typecheck!(URI.__info__(:struct)) == list( - closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())) + closed_map( + default: {term(), true}, + field: {atom(), false}, + required: {boolean(), true} + ) ) assert typecheck!([x], x.__info__(:struct)) == list( - closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())) + closed_map( + default: {term(), true}, + field: {atom(), false}, + required: {boolean(), true} + ) ) |> opt_union(atom([nil])) end diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index c3e69a39f9..0bddc4f5cd 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -39,8 +39,8 @@ defmodule Module.Types.InferTest do end args = [ - open_map(__struct__: atom([Point])), - open_map(__struct__: atom([Point])), + open_map(__struct__: {atom([Point]), false}), + open_map(__struct__: {atom([Point]), false}), atom([Point]), atom([Point]) ] @@ -141,10 +141,15 @@ defmodule Module.Types.InferTest do number = opt_union(integer(), float()) assert types[{:fun, 1}] == - {:infer, nil, [{[open_map(foo: number, bar: number)], dynamic(number)}]} + {:infer, nil, + [{[open_map(foo: {number, false}, bar: {number, false})], dynamic(number)}]} assert types[{:fun_nested, 1}] == - {:infer, nil, [{[open_map(x: open_map(foo: number, bar: number))], dynamic(number)}]} + {:infer, nil, + [ + {[open_map(x: {open_map(foo: {number, false}, bar: {number, false}), false})], + dynamic(number)} + ]} end test "from Elixir built-in", config do @@ -234,10 +239,10 @@ defmodule Module.Types.InferTest do tuple([ binary(), closed_map( - __struct__: atom([module]), - x: binary(), - y: atom([nil]), - z: atom([nil]) + __struct__: {atom([module]), false}, + x: {binary(), false}, + y: {atom([nil]), false}, + z: {atom([nil]), false} ) ]) ) @@ -250,10 +255,10 @@ defmodule Module.Types.InferTest do assert return == dynamic( closed_map( - __struct__: atom([module]), - x: binary(), - y: atom([nil]), - z: term() + __struct__: {atom([module]), false}, + x: {binary(), false}, + y: {atom([nil]), false}, + z: {term(), false} ) ) @@ -262,10 +267,10 @@ defmodule Module.Types.InferTest do assert return == dynamic( closed_map( - __struct__: atom([module]), - x: binary(), - y: atom([nil]), - z: term() + __struct__: {atom([module]), false}, + x: {binary(), false}, + y: {atom([nil]), false}, + z: {term(), false} ) ) end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index ecdd0f664e..d759577f6f 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -141,7 +141,7 @@ defmodule Module.Types.IntegrationTest do dynamic(opt_union(empty_list(), non_empty_list(term(), term()))) assert itself_arg.(Itself.Map) == - dynamic(open_map(__struct__: if_set(opt_negation(atom())))) + dynamic(open_map(__struct__: {opt_negation(atom()), true})) assert itself_arg.(Itself.Port) == dynamic(port()) assert itself_arg.(Itself.PID) == dynamic(pid()) @@ -151,10 +151,16 @@ defmodule Module.Types.IntegrationTest do assert itself_arg.(Itself.Range) == dynamic( - closed_map(__struct__: atom([Range]), first: term(), last: term(), step: term()) + closed_map( + __struct__: {atom([Range]), false}, + first: {term(), false}, + last: {term(), false}, + step: {term(), false} + ) ) - assert itself_arg.(Itself.Unknown) == dynamic(open_map(__struct__: atom([Unknown]))) + assert itself_arg.(Itself.Unknown) == + dynamic(open_map(__struct__: {atom([Unknown]), false})) end test "ignores additional callbacks on implementations" do @@ -1963,7 +1969,7 @@ defmodule Module.Types.IntegrationTest do defp read_chunk(binary) do assert {:ok, {_module, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"]) - assert {:elixir_checker_v9, map} = :erlang.binary_to_term(chunk) + assert {:elixir_checker_v10, map} = :erlang.binary_to_term(chunk) map end diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index 24b0df12d7..f656f8ae64 100644 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ b/lib/elixir/test/elixir/module/types/map_test.exs @@ -29,7 +29,7 @@ defmodule Module.Types.MapTest do assert typecheck!([x], :maps.take(:key, x)) == opt_union( - dynamic(tuple([term(), open_map(key: not_set())])), + dynamic(tuple([term(), open_map(key: {none(), true})])), atom([:error]) ) @@ -39,8 +39,8 @@ defmodule Module.Types.MapTest do tuple([ term(), opt_union( - open_map(foo: not_set()), - open_map(bar: not_set()) + open_map(foo: {none(), true}), + open_map(bar: {none(), true}) ) ]) ), @@ -98,7 +98,7 @@ defmodule Module.Types.MapTest do empty_map() assert typecheck!([x], Map.delete(x, :key)) == - dynamic(open_map(key: not_set())) + dynamic(open_map(key: {none(), true})) # If one of them succeeds, we are still fine! assert typecheck!( @@ -107,7 +107,7 @@ defmodule Module.Types.MapTest do ) == opt_union( empty_map(), - closed_map(foo: integer()) + closed_map(foo: {integer(), false}) ) assert typecheck!([x], Map.delete(x, 123)) == dynamic(open_map()) @@ -130,10 +130,10 @@ defmodule Module.Types.MapTest do test "combined with put" do assert typecheck!([x], x |> Map.delete(:key) |> Map.put(:key, "123")) == - dynamic(open_map(key: binary())) + dynamic(open_map(key: {binary(), false})) assert typecheck!([x, y], x |> Map.delete(:key) |> Map.put(String.to_unsafe_atom(y), "123")) == - dynamic(open_map(key: if_set(binary()))) + dynamic(open_map(key: {binary(), true})) end end @@ -212,7 +212,7 @@ defmodule Module.Types.MapTest do y = Integer.to_string(Map.fetch!(x, :key)) {x, y} ) - ) == dynamic(tuple([open_map(key: integer()), binary()])) + ) == dynamic(tuple([open_map(key: {integer(), false}), binary()])) end test "errors" do @@ -278,7 +278,7 @@ defmodule Module.Types.MapTest do Map.from_keys(x, 123) ) ) == - closed_map(key1: if_set(integer()), key2: if_set(integer())) + closed_map(key1: {integer(), true}, key2: {integer(), true}) |> opt_difference(empty_map()) assert typecheck!( @@ -288,18 +288,7 @@ defmodule Module.Types.MapTest do Map.from_keys(x, 123) ) ) == - closed_map([ - {domain_key(:integer), if_set(integer())}, - {domain_key(:binary), if_set(integer())} - ]) - end - - test "optionalizing an inferred gradual value preserves its static part" do - value = typecheck!([condition?, x], if(condition?, do: :value, else: x)) - optional = if_set(value) - - assert equal?(optional, opt_union(if_set(atom([:value])), dynamic(if_set(term())))) - refute equal?(optional, dynamic(if_set(term()))) + closed_map([{domain_key(:integer), integer()}, {domain_key(:binary), integer()}]) end test "reports errors from static part of optionalized gradual value" do @@ -331,22 +320,22 @@ defmodule Module.Types.MapTest do describe "Map.from_struct/1" do test "checking" do assert typecheck!(Map.from_struct(%{__struct__: URI, port: 433})) == - closed_map(port: integer()) + closed_map(port: {integer(), false}) assert typecheck!(Map.from_struct(%URI{})) == closed_map( - authority: atom([nil]), - fragment: atom([nil]), - host: atom([nil]), - path: atom([nil]), - port: atom([nil]), - query: atom([nil]), - scheme: atom([nil]), - userinfo: atom([nil]) + authority: {atom([nil]), false}, + fragment: {atom([nil]), false}, + host: {atom([nil]), false}, + path: {atom([nil]), false}, + port: {atom([nil]), false}, + query: {atom([nil]), false}, + scheme: {atom([nil]), false}, + userinfo: {atom([nil]), false} ) assert typecheck!([x], Map.from_struct(x)) == - dynamic(open_map(__struct__: not_set())) + dynamic(open_map(__struct__: {none(), true})) end test "inference" do @@ -356,7 +345,7 @@ defmodule Module.Types.MapTest do _ = Map.from_struct(x) x ) - ) == dynamic(open_map(__struct__: atom())) + ) == dynamic(open_map(__struct__: {atom(), false})) end test "errors" do @@ -579,15 +568,15 @@ defmodule Module.Types.MapTest do tuple([opt_union(integer(), atom([nil])), empty_map()]) assert typecheck!([x], Map.pop(x, :key)) == - dynamic(tuple([term(), open_map(key: not_set())])) + dynamic(tuple([term(), open_map(key: {none(), true})])) assert typecheck!([condition?, x], Map.pop(x, if(condition?, do: :foo, else: :bar))) == dynamic( tuple([ term(), opt_union( - open_map(foo: not_set()), - open_map(bar: not_set()) + open_map(foo: {none(), true}), + open_map(bar: {none(), true}) ) ]) ) @@ -645,7 +634,7 @@ defmodule Module.Types.MapTest do ) assert typecheck!([x], Map.pop_lazy(x, :key, fn -> :error end)) == - dynamic(tuple([term(), open_map(key: not_set())])) + dynamic(tuple([term(), open_map(key: {none(), true})])) assert typecheck!( [condition?, x], @@ -655,8 +644,8 @@ defmodule Module.Types.MapTest do tuple([ term(), opt_union( - open_map(foo: not_set()), - open_map(bar: not_set()) + open_map(foo: {none(), true}), + open_map(bar: {none(), true}) ) ]) ) @@ -726,15 +715,15 @@ defmodule Module.Types.MapTest do tuple([opt_union(integer(), atom([:error])), empty_map()]) assert typecheck!([x], Map.pop(x, :key, :error)) == - dynamic(tuple([term(), open_map(key: not_set())])) + dynamic(tuple([term(), open_map(key: {none(), true})])) assert typecheck!([condition?, x], Map.pop(x, if(condition?, do: :foo, else: :bar), :error)) == dynamic( tuple([ term(), opt_union( - open_map(foo: not_set()), - open_map(bar: not_set()) + open_map(foo: {none(), true}), + open_map(bar: {none(), true}) ) ]) ) @@ -789,15 +778,15 @@ defmodule Module.Types.MapTest do tuple([integer(), empty_map()]) assert typecheck!([x], Map.pop!(x, :key)) == - dynamic(tuple([term(), open_map(key: not_set())])) + dynamic(tuple([term(), open_map(key: {none(), true})])) assert typecheck!([condition?, x], Map.pop!(x, if(condition?, do: :foo, else: :bar))) == dynamic( tuple([ term(), opt_union( - open_map(foo: not_set()), - open_map(bar: not_set()) + open_map(foo: {none(), true}), + open_map(bar: {none(), true}) ) ]) ) @@ -813,7 +802,7 @@ defmodule Module.Types.MapTest do _ = Map.pop!(x, :key) x ) - ) == dynamic(open_map(key: term())) + ) == dynamic(open_map(key: {term(), false})) end test "errors" do @@ -841,16 +830,16 @@ defmodule Module.Types.MapTest do describe "Map.put/3" do test "checking" do assert typecheck!(Map.put(%{}, :key, :value)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!(:maps.put(:key, :value, %{})) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!(Map.put(%{key: 123}, :key, :value)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!([x], Map.put(x, :key, :value)) == - dynamic(open_map(key: atom([:value]))) + dynamic(open_map(key: {atom([:value]), false})) # If one of them succeeds, we are still fine! assert typecheck!( @@ -858,8 +847,8 @@ defmodule Module.Types.MapTest do Map.put(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") ) == opt_union( - closed_map(foo: binary()), - closed_map(foo: integer(), bar: binary()) + closed_map(foo: {binary(), false}), + closed_map(foo: {integer(), false}, bar: {binary(), false}) ) assert typecheck!([x], Map.put(x, 123, 456)) == dynamic(open_map()) @@ -900,19 +889,23 @@ defmodule Module.Types.MapTest do describe "Map.put_new_lazy/3" do test "checking" do assert typecheck!(Map.put_new_lazy(%{}, :key, fn -> :value end)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!(Map.put_new_lazy(%{key: 123}, :key, fn -> :value end)) == - closed_map(key: integer()) + closed_map(key: {integer(), false}) assert typecheck!([x], Map.put_new_lazy(x, :key, fn -> :value end)) == - dynamic(open_map(key: term())) + dynamic(open_map(key: {term(), false})) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.put_new_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn -> "123" end) - ) == opt_union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) + ) == + opt_union( + closed_map(foo: {integer(), false}), + closed_map(foo: {integer(), false}, bar: {binary(), false}) + ) assert typecheck!([], Map.put_new_lazy(%{789 => "binary"}, 123, fn -> 456 end)) == closed_map([{domain_key(:integer), opt_union(binary(), integer())}]) @@ -962,19 +955,23 @@ defmodule Module.Types.MapTest do describe "Map.put_new/3" do test "checking" do assert typecheck!(Map.put_new(%{}, :key, :value)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!(Map.put_new(%{key: 123}, :key, :value)) == - closed_map(key: integer()) + closed_map(key: {integer(), false}) assert typecheck!([x], Map.put_new(x, :key, :value)) == - dynamic(open_map(key: term())) + dynamic(open_map(key: {term(), false})) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.put_new(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") - ) == opt_union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) + ) == + opt_union( + closed_map(foo: {integer(), false}), + closed_map(foo: {integer(), false}, bar: {binary(), false}) + ) assert typecheck!([], Map.put_new(%{789 => "binary"}, 123, 456)) == closed_map([{domain_key(:integer), opt_union(binary(), integer())}]) @@ -1013,16 +1010,16 @@ defmodule Module.Types.MapTest do describe "Map.replace/3" do test "checking" do assert typecheck!(Map.replace(%{key: 123}, :key, :value)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!([x], Map.replace(x, :key, :value)) == - dynamic(open_map(key: if_set(atom([:value])))) + dynamic(open_map(key: {atom([:value]), true})) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.replace(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") - ) == closed_map(foo: binary()) + ) == closed_map(foo: {binary(), false}) assert typecheck!([x], Map.replace(x, 123, 456)) == dynamic(open_map()) end @@ -1060,10 +1057,10 @@ defmodule Module.Types.MapTest do describe "Map.replace_lazy/3" do test "checking" do assert typecheck!(Map.replace_lazy(%{key: 123}, :key, fn _ -> :value end)) == - dynamic(closed_map(key: atom([:value]))) + dynamic(closed_map(key: {atom([:value]), false})) assert typecheck!([x], Map.replace_lazy(x, :key, fn _ -> :value end)) == - dynamic(open_map(key: if_set(atom([:value])))) + dynamic(open_map(key: {atom([:value]), true})) # If one of them succeeds, we are still fine! assert typecheck!( @@ -1071,7 +1068,7 @@ defmodule Module.Types.MapTest do Map.replace_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn _ -> "123" end) - ) == dynamic(closed_map(foo: binary())) + ) == dynamic(closed_map(foo: {binary(), false})) # Both succeed but different clauses assert typecheck!( @@ -1087,8 +1084,8 @@ defmodule Module.Types.MapTest do ) == dynamic( opt_union( - closed_map(key1: atom([:foo]), key2: float()), - closed_map(key1: integer(), key2: atom([:bar])) + closed_map(key1: {atom([:foo]), false}, key2: {float(), false}), + closed_map(key1: {integer(), false}, key2: {atom([:bar]), false}) ) ) @@ -1147,19 +1144,19 @@ defmodule Module.Types.MapTest do describe "Map.replace!/3" do test "checking" do assert typecheck!(Map.replace!(%{key: 123}, :key, :value)) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!(:maps.update(:key, :value, %{key: 123})) == - closed_map(key: atom([:value])) + closed_map(key: {atom([:value]), false}) assert typecheck!([x], Map.replace!(x, :key, :value)) == - dynamic(open_map(key: atom([:value]))) + dynamic(open_map(key: {atom([:value]), false})) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.replace!(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") - ) == closed_map(foo: binary()) + ) == closed_map(foo: {binary(), false}) assert typecheck!([x], Map.replace!(x, 123, 456)) == dynamic(open_map()) end @@ -1171,7 +1168,7 @@ defmodule Module.Types.MapTest do _ = Map.replace!(x, :key, :value) x ) - ) == dynamic(open_map(key: term())) + ) == dynamic(open_map(key: {term(), false})) end test "errors" do @@ -1254,13 +1251,13 @@ defmodule Module.Types.MapTest do describe "Map.update/4" do test "checking" do assert typecheck!(Map.update(%{}, :key, :default, fn _ -> :value end)) == - dynamic(closed_map(key: atom([:default]))) + dynamic(closed_map(key: {atom([:default]), false})) assert typecheck!(Map.update(%{key: 123}, :key, :default, fn _ -> :value end)) == - dynamic(closed_map(key: atom([:value]))) + dynamic(closed_map(key: {atom([:value]), false})) assert typecheck!([x], Map.update(x, :key, :default, fn _ -> :value end)) == - dynamic(open_map(key: atom([:value, :default]))) + dynamic(open_map(key: {atom([:value, :default]), false})) # If one of them succeeds, we are still fine! assert typecheck!( @@ -1271,8 +1268,8 @@ defmodule Module.Types.MapTest do ) == dynamic( opt_union( - closed_map(foo: binary()), - closed_map(foo: integer(), bar: atom([:default])) + closed_map(foo: {binary(), false}), + closed_map(foo: {integer(), false}, bar: {atom([:default]), false}) ) ) @@ -1291,8 +1288,8 @@ defmodule Module.Types.MapTest do ) == dynamic( opt_union( - closed_map(key1: atom([:foo]), key2: float()), - closed_map(key1: integer(), key2: atom([:bar])) + closed_map(key1: {atom([:foo]), false}, key2: {float(), false}), + closed_map(key1: {integer(), false}, key2: {atom([:bar]), false}) ) ) @@ -1344,16 +1341,16 @@ defmodule Module.Types.MapTest do describe "Map.update!/3" do test "checking" do assert typecheck!(Map.update!(%{key: 123}, :key, fn _ -> :value end)) == - dynamic(closed_map(key: atom([:value]))) + dynamic(closed_map(key: {atom([:value]), false})) assert typecheck!([x], Map.update!(x, :key, fn _ -> :value end)) == - dynamic(open_map(key: atom([:value]))) + dynamic(open_map(key: {atom([:value]), false})) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.update!(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn _ -> "123" end) - ) == dynamic(closed_map(foo: binary())) + ) == dynamic(closed_map(foo: {binary(), false})) # Both succeed but different clauses assert typecheck!( @@ -1365,8 +1362,8 @@ defmodule Module.Types.MapTest do ) == dynamic( opt_union( - closed_map(key1: atom([:foo]), key2: float()), - closed_map(key1: integer(), key2: atom([:bar])) + closed_map(key1: {atom([:foo]), false}, key2: {float(), false}), + closed_map(key1: {integer(), false}, key2: {atom([:bar]), false}) ) ) @@ -1386,7 +1383,7 @@ defmodule Module.Types.MapTest do _ = Map.update!(x, :key, fn _ -> :value end) x ) - ) == dynamic(open_map(key: term())) + ) == dynamic(open_map(key: {term(), false})) end test "errors" do @@ -1425,7 +1422,7 @@ defmodule Module.Types.MapTest do test "with unknown function type" do assert typecheck!([x], Map.update!(x, :body, &:zlib.gunzip/1)) == - dynamic(open_map(body: term())) + dynamic(open_map(body: {term(), false})) end end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index b79fc1d267..2263491401 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -196,13 +196,13 @@ defmodule Module.Types.PatternTest do test "variable name fields" do assert typecheck!([x = %_{}], x.__struct__) == dynamic(atom()) - assert typecheck!([x = %_{}], x) == dynamic(open_map(__struct__: atom())) + assert typecheck!([x = %_{}], x) == dynamic(open_map(__struct__: {atom(), false})) assert typecheck!([x = %m{}, m = Point], x) == - dynamic(open_map(__struct__: atom([Point]))) + dynamic(open_map(__struct__: {atom([Point]), false})) assert typecheck!([m = Point, x = %m{}], x) == - dynamic(open_map(__struct__: atom([Point]))) + dynamic(open_map(__struct__: {atom([Point]), false})) assert typeerror!([m = 123], %^m{} = %Point{}) == ~l""" @@ -225,9 +225,12 @@ defmodule Module.Types.PatternTest do describe "maps" do test "atom keys in patterns" do - assert typecheck!([x = %{foo: :bar}], x) == dynamic(open_map(foo: atom([:bar]))) + assert typecheck!([x = %{foo: :bar}], x) == dynamic(open_map(foo: {atom([:bar]), false})) assert typecheck!([x = %{123 => 456}], x) == dynamic(open_map()) - assert typecheck!([x = %{123 => 456, foo: :bar}], x) == dynamic(open_map(foo: atom([:bar]))) + + assert typecheck!([x = %{123 => 456, foo: :bar}], x) == + dynamic(open_map(foo: {atom([:bar]), false})) + assert typecheck!([%{foo: :bar = x}], x) == dynamic(atom([:bar])) assert typecheck!( @@ -239,8 +242,10 @@ defmodule Module.Types.PatternTest do ) == dynamic( tuple([ - open_map(assigns: open_map(app: open_map(slug: term()))), - open_map(slug: term()), + open_map( + assigns: {open_map(app: {open_map(slug: {term(), false}), false}), false} + ), + open_map(slug: {term(), false}), term() ]) ) @@ -254,8 +259,10 @@ defmodule Module.Types.PatternTest do ) == dynamic( tuple([ - open_map(assigns: open_map(app: open_map(slug: term()))), - open_map(slug: term()), + open_map( + assigns: {open_map(app: {open_map(slug: {term(), false}), false}), false} + ), + open_map(slug: {term(), false}), term() ]) ) @@ -263,7 +270,10 @@ defmodule Module.Types.PatternTest do test "domain keys in patterns" do assert typecheck!([x = %{123 => 456}], x) == dynamic(open_map()) - assert typecheck!([x = %{123 => 456, foo: :bar}], x) == dynamic(open_map(foo: atom([:bar]))) + + assert typecheck!([x = %{123 => 456, foo: :bar}], x) == + dynamic(open_map(foo: {atom([:bar]), false})) + assert typecheck!([%{"123" => :bar = x}], x) == dynamic(atom([:bar])) end @@ -601,7 +611,7 @@ defmodule Module.Types.PatternTest do test "not" do assert typecheck!([x], not x, x) == dynamic(atom([false])) - assert typecheck!([x], not x.foo, x) == dynamic(open_map(foo: atom([false]))) + assert typecheck!([x], not x.foo, x) == dynamic(open_map(foo: {atom([false]), false})) assert typeerror!([x], not length(x), x) |> strip_ansi() == ~l""" incompatible types given to Kernel.not/1: @@ -654,11 +664,13 @@ defmodule Module.Types.PatternTest do end test "is_struct/1" do - assert typecheck!([x], is_struct(x), x) == dynamic(open_map(__struct__: atom())) - assert typecheck!([x], is_struct(x, URI), x) == dynamic(open_map(__struct__: atom([URI]))) + assert typecheck!([x], is_struct(x), x) == dynamic(open_map(__struct__: {atom(), false})) + + assert typecheck!([x], is_struct(x, URI), x) == + dynamic(open_map(__struct__: {atom([URI]), false})) assert typecheck!([x], not is_struct(x), x) - |> equal?(dynamic(opt_negation(open_map(__struct__: atom())))) + |> equal?(dynamic(opt_negation(open_map(__struct__: {atom(), false})))) assert typecheck!([x], not is_struct(x, URI), x) == dynamic() end @@ -677,12 +689,16 @@ defmodule Module.Types.PatternTest do end test "is_map_key/2" do - assert typecheck!([x], is_map_key(x, :foo), x) == dynamic(open_map(foo: term())) - assert typecheck!([x], :erlang.is_map_key(:foo, x), x) == dynamic(open_map(foo: term())) - assert typecheck!([x], not is_map_key(x, :foo), x) == dynamic(open_map(foo: not_set())) + assert typecheck!([x], is_map_key(x, :foo), x) == dynamic(open_map(foo: {term(), false})) + + assert typecheck!([x], :erlang.is_map_key(:foo, x), x) == + dynamic(open_map(foo: {term(), false})) + + assert typecheck!([x], not is_map_key(x, :foo), x) == + dynamic(open_map(foo: {none(), true})) assert typecheck!([x], not :erlang.is_map_key(:foo, x), x) == - dynamic(open_map(foo: not_set())) + dynamic(open_map(foo: {none(), true})) end test "elem" do @@ -704,10 +720,10 @@ defmodule Module.Types.PatternTest do test "map.field" do assert typecheck!([x = %{foo: :bar}], x.bar, x) == - dynamic(open_map(foo: atom([:bar]), bar: atom([true]))) + dynamic(open_map(foo: {atom([:bar]), false}, bar: {atom([true]), false})) assert typecheck!([x = %{foo: :bar}], not x.bar, x) == - dynamic(open_map(foo: atom([:bar]), bar: atom([false]))) + dynamic(open_map(foo: {atom([:bar]), false}, bar: {atom([false]), false})) assert typeerror!([x = %Point{}], x.foo_bar, :ok) == ~l""" the following pattern will never match: @@ -839,7 +855,13 @@ defmodule Module.Types.PatternTest do dynamic(opt_union(integer(), float())) assert typecheck!([m], elem(m.pair, max(m.x, m.y)) > 0, m) == - dynamic(open_map(pair: open_tuple([]), x: integer(), y: integer())) + dynamic( + open_map( + pair: {open_tuple([]), false}, + x: {integer(), false}, + y: {integer(), false} + ) + ) assert typeerror!( [x, y], @@ -1053,7 +1075,7 @@ defmodule Module.Types.PatternTest do end test "with singleton literals and composite types" do - assert typecheck!([x], x.key == :ok, x) == dynamic(open_map(key: atom([:ok]))) + assert typecheck!([x], x.key == :ok, x) == dynamic(open_map(key: {atom([:ok]), false})) assert typecheck!([x], hd(x) == :ok, x) == dynamic(non_empty_list(term(), term())) assert typecheck!([x], elem(x, 0) == :ok, x) == dynamic(open_tuple([atom([:ok])])) end @@ -1239,7 +1261,7 @@ defmodule Module.Types.PatternTest do [x], is_map(x) and map_size(x) != 1 and is_map_key(x, :a) and is_map_key(x, :b), x - ) == dynamic(open_map(a: term(), b: term())) + ) == dynamic(open_map(a: {term(), false}, b: {term(), false})) end test "map_size ordered" do diff --git a/lib/elixir/test/elixir/module/types/recursive_test.exs b/lib/elixir/test/elixir/module/types/recursive_test.exs index 7017d2da53..5914d12d70 100644 --- a/lib/elixir/test/elixir/module/types/recursive_test.exs +++ b/lib/elixir/test/elixir/module/types/recursive_test.exs @@ -89,11 +89,11 @@ defmodule Module.Types.RecursiveTest do assert equal?(from_nodes, from_descrs) n = recursive_node(integer()) - result = closed_map(a: n) + result = closed_map(a: {n, false}) refute empty?(result) n = recursive_node(atom()) - result = open_map(b: n) + result = open_map(b: {n, false}) refute empty?(result) end @@ -387,11 +387,12 @@ defmodule Module.Types.RecursiveTest do %{X: nx, Y: ny} = recursive(%{ X: fn recur -> - closed_map(outer: closed_map(inner: recur.(:X))) |> bare_union(atom([nil])) + closed_map(outer: {closed_map(inner: {recur.(:X), false}), false}) + |> bare_union(atom([nil])) end, Y: fn recur -> - closed_map(outer: closed_map(inner: recur.(:Y))) - |> bare_union(closed_map(outer: closed_map(inner: integer()))) + closed_map(outer: {closed_map(inner: {recur.(:Y), false}), false}) + |> bare_union(closed_map(outer: {closed_map(inner: {integer(), false}), false})) |> bare_union(atom([nil])) end }) @@ -441,12 +442,12 @@ defmodule Module.Types.RecursiveTest do %{X: nx, Y: ny} = recursive(%{ X: fn recur -> - closed_map(a: closed_map(a: recur.(:X))) - |> bare_union(closed_map(a: closed_map(a: atom()))) + closed_map(a: {closed_map(a: {recur.(:X), false}), false}) + |> bare_union(closed_map(a: {closed_map(a: {atom(), false}), false})) end, Y: fn recur -> - closed_map(a: closed_map(a: recur.(:Y))) - |> bare_union(closed_map(a: closed_map(a: atom()))) + closed_map(a: {closed_map(a: {recur.(:Y), false}), false}) + |> bare_union(closed_map(a: {closed_map(a: {atom(), false}), false})) end }) @@ -510,8 +511,8 @@ defmodule Module.Types.RecursiveTest do %{X: nx} = recursive(%{ X: fn recur -> - closed_map(a: integer(), b: recur.(:X)) - |> bare_union(closed_map(a: integer(), b: atom())) + closed_map(a: {integer(), false}, b: {recur.(:X), false}) + |> bare_union(closed_map(a: {integer(), false}, b: {atom(), false})) end }) @@ -576,7 +577,9 @@ defmodule Module.Types.RecursiveTest do # M = %{a: M} | nil, as a node defp rec_map_node() do - recursive(%{M: fn recur -> closed_map(a: recur.(:M)) |> bare_union(atom([nil])) end}) + recursive(%{ + M: fn recur -> closed_map(a: {recur.(:M), false}) |> bare_union(atom([nil])) end + }) |> Map.fetch!(:M) end @@ -585,7 +588,9 @@ defmodule Module.Types.RecursiveTest do # M = %{a: M, b: descr} | nil defp rec_map_with(descr) do recursive(%{ - M: fn recur -> closed_map(a: recur.(:M), b: descr) |> bare_union(atom([nil])) end + M: fn recur -> + closed_map(a: {recur.(:M), false}, b: {descr, false}) |> bare_union(atom([nil])) + end }) |> Map.fetch!(:M) |> unfold() @@ -752,8 +757,16 @@ defmodule Module.Types.RecursiveTest do # one differing non-recursive key next to a shared recursive node node = rec_map_node() - union = opt_union(closed_map(a: node, b: integer()), closed_map(a: node, b: float())) - assert equal?(union, closed_map(a: node, b: bare_union(integer(), float()))) + union = + opt_union( + closed_map(a: {node, false}, b: {integer(), false}), + closed_map(a: {node, false}, b: {float(), false}) + ) + + assert equal?( + union, + closed_map(a: {node, false}, b: {bare_union(integer(), float()), false}) + ) # tuple union with one differing index holding recursive nodes union = opt_union(tuple([integer(), node]), tuple([float(), node])) @@ -797,10 +810,10 @@ defmodule Module.Types.RecursiveTest do node_a = rec_map_node() node_b = rec_map_node() - m_at = closed_map(a: node_a, t: atom()) - m_az = closed_map(a: node_a, t: atom([:z])) - m_bt = closed_map(a: node_b, t: atom()) - m_bz = closed_map(a: node_b, t: atom([:z])) + m_at = closed_map(a: {node_a, false}, t: {atom(), false}) + m_az = closed_map(a: {node_a, false}, t: {atom([:z]), false}) + m_bt = closed_map(a: {node_b, false}, t: {atom(), false}) + m_bz = closed_map(a: {node_b, false}, t: {atom([:z]), false}) # a left BDD with union/negation structure makes the leaf difference # request the union of the recursive field values (type: :union) @@ -875,7 +888,7 @@ defmodule Module.Types.RecursiveTest do # map <-> tuple mutual recursion: P = %{items: Q} | nil, Q = {integer(), P} | nil build_pq = fn -> recursive(%{ - P: fn recur -> closed_map(items: recur.(:Q)) |> bare_union(atom([nil])) end, + P: fn recur -> closed_map(items: {recur.(:Q), false}) |> bare_union(atom([nil])) end, Q: fn recur -> tuple([integer(), recur.(:P)]) |> bare_union(atom([nil])) end }) |> Map.fetch!(:P) @@ -892,7 +905,7 @@ defmodule Module.Types.RecursiveTest do # recursion through a list of maps: E = list(%{next: E} | nil) build_e = fn -> recursive(%{ - E: fn recur -> list(bare_union(closed_map(next: recur.(:E)), atom([nil]))) end + E: fn recur -> list(bare_union(closed_map(next: {recur.(:E), false}), atom([nil]))) end }) |> Map.fetch!(:E) |> unfold() diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index 8664f4eb6c..7540f1739c 100644 --- a/lib/elixir/test/elixir/protocol/consolidation_test.exs +++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs @@ -165,7 +165,7 @@ defmodule Protocol.ConsolidationTest do defp exports(binary) do {:ok, {_, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(binary, [~c"ExCk"]) - assert {:elixir_checker_v9, contents} = :erlang.binary_to_term(check_bin) + assert {:elixir_checker_v10, contents} = :erlang.binary_to_term(check_bin) Map.new(contents.exports) end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 597934cffe..c1536ce8ee 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -5,7 +5,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 36 + @manifest_vsn 37 @checkpoint_vsn 5 import Record