From 5175c5b1e87f4fc327e449e323e8939d4bb6c25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 16 Jul 2026 20:48:34 +0200 Subject: [PATCH 1/9] Do not store optional in domain types --- lib/elixir/lib/module/types/descr.ex | 101 +++++++++++---------------- 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2fdb5a25ee9..8fe9d4dbdba 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -347,19 +347,22 @@ defmodule Module.Types.Descr do # `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) + case unfold(type) do + %{dynamic: :term} = descr when map_size(descr) == 1 -> + %{dynamic: term_or_optional()} + + %{dynamic: :term} = descr -> + Map.put(%{descr | dynamic: term_or_optional()}, :optional, 1) + + %{dynamic: dyn} = descr -> + Map.put(%{descr | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1) + + descr -> + Map.put(descr, :optional, 1) end end @@ -2831,13 +2834,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 +2918,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 +2928,8 @@ 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) + value = remove_optional(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 +2937,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,25 +2946,6 @@ 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 -> @@ -2992,16 +2977,18 @@ defmodule Module.Types.Descr do # 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()) - # Gets the domain type association to a map. - # In this case, we already remove the optional to simplify upstream. + defp map_key_tag_to_type(domains), + do: domains |> fields_get(:atom, none()) |> if_set() + + # 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 @@ -3123,7 +3110,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)] @@ -3928,8 +3915,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 +3994,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, remove_optional(type_fun.(true, none()))), else: :closed # Note: domain_keys may contain duplicates, so we cannot @@ -4019,14 +4005,14 @@ defmodule Module.Types.Descr do {:ok, value} -> fields_store( domain_key, - opt_union(value, type_fun.(true, remove_optional(value))), + opt_union(value, remove_optional(type_fun.(true, value))), 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, remove_optional(type_fun.(true, none())), acc), else: acc end end) @@ -4191,8 +4177,8 @@ 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)) + acc = Enum.reduce(required_domains, acc, &map_get_domain(dnf, &1, &2)) + acc = Enum.reduce(optional_domains, acc, &map_get_domain(dnf, &1, &2)) remove_optional(acc) end @@ -4206,9 +4192,8 @@ defmodule Module.Types.Descr do 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 -> @@ -4542,29 +4527,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)} @@ -4680,13 +4665,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 = From 9e8dc5a990448e4b5d79c60bc4a6df78ad075254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 16 Jul 2026 23:34:20 +0200 Subject: [PATCH 2/9] Remove optional field from descr --- lib/elixir/lib/module/types/apply.ex | 72 +- lib/elixir/lib/module/types/descr.ex | 1017 +++++++----- lib/elixir/lib/module/types/expr.ex | 38 +- lib/elixir/lib/module/types/of.ex | 48 +- lib/elixir/lib/module/types/pattern.ex | 28 +- lib/elixir/lib/protocol.ex | 2 +- .../test/elixir/module/types/descr_test.exs | 1456 ++++++++++------- .../test/elixir/module/types/expr_test.exs | 118 +- .../test/elixir/module/types/infer_test.exs | 37 +- .../elixir/module/types/integration_test.exs | 12 +- .../test/elixir/module/types/map_test.exs | 169 +- .../test/elixir/module/types/pattern_test.exs | 68 +- .../elixir/module/types/recursive_test.exs | 55 +- 13 files changed, 1861 insertions(+), 1259 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index c6a18b93cf3..680a98f8edd 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)} @@ -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 optional?, _type -> {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 optional?, _type -> {value, optional?} end case map_update_fun(map, key, fun, false, false) do {_value, descr, _errors} -> {:ok, return(descr, args_types, stack)} @@ -1312,10 +1319,10 @@ defmodule Module.Types.Apply do fun_apply = fn optional?, arg_type -> 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 @@ -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 optional?, _type -> {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 + true, type -> {opt_union(type, value), false} + false, type -> {if(empty?(type), do: value, else: type), false} end case map_update_fun(map, key, fun, false, true) do @@ -1829,7 +1841,7 @@ defmodule Module.Types.Apply do fun_apply = fn optional?, arg_type -> 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 8fe9d4dbdba..6d7e6f4b6ba 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,90 +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(:term), do: term_or_optional() - - # If type contains a :dynamic part, :optional gets added there. - def if_set(type) do - case unfold(type) do - %{dynamic: :term} = descr when map_size(descr) == 1 -> - %{dynamic: term_or_optional()} - - %{dynamic: :term} = descr -> - Map.put(%{descr | dynamic: term_or_optional()}, :optional, 1) - - %{dynamic: dyn} = descr -> - Map.put(%{descr | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1) - - descr -> - Map.put(descr, :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 """ @@ -457,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) @@ -473,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 @@ -508,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) @@ -548,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) @@ -560,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 @@ -589,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) @@ -636,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 @@ -675,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 @@ -694,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) @@ -735,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 @@ -952,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) @@ -989,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 @@ -1048,7 +961,7 @@ defmodule Module.Types.Descr do if empty?(other) do {:error, other} else - {:ok, remove_optional(other)} + {:ok, other} end end @@ -2752,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) @@ -2928,7 +2841,6 @@ defmodule Module.Types.Descr do end defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do - value = remove_optional(value) map_put_domain(domain, :lists.usort(domain_keys), value, value) end @@ -2948,38 +2860,51 @@ defmodule Module.Types.Descr do 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 - # 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_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 - defp map_key_tag_to_type(domains), - do: domains |> fields_get(:atom, none()) |> if_set() + # Gets the default type associated to atom keys in a map. + 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. Domain values are stored without # an optional marker, so no conversion is necessary here. @@ -2995,9 +2920,6 @@ defmodule Module.Types.Descr 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)) @@ -3029,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 ) @@ -3063,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) @@ -3076,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 @@ -3120,44 +3050,50 @@ defmodule Module.Types.Descr do defp map_domain_intersection_fields(_, _, _seen), do: [] defp map_literal_intersection_open_closed( - [{k1, v1} | t1], + [{k1, {_v1, optional1?}} | t1], [{k2, _} | _] = l2, intersection_fun, seen ) when k1 < k2 do # 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, {v2, optional2?}} | 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, {v2, optional2?}} + | map_literal_intersection_open_closed(l1, t2, intersection_fun, seen) + ] end defp map_literal_intersection_open_closed( - [{key, v1} | t1], - [{_, v2} | t2], + [{key, {v1, optional1?}} | t1], + [{_, {v2, optional2?}} | t2], intersection_fun, seen ) do + 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) @@ -3165,13 +3101,13 @@ defmodule Module.Types.Descr do end defp map_literal_intersection_closed( - [{k1, v1} | t1], + [{k1, {_v1, optional1?}} | t1], [{k2, _} | _] = l2, intersection_fun, seen ) when k1 < k2 do - if is_optional_static(v1) do + if optional1? do map_literal_intersection_closed(t1, l2, intersection_fun, seen) else throw(:empty) @@ -3180,28 +3116,33 @@ defmodule Module.Types.Descr do defp map_literal_intersection_closed( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, {_v2, optional2?}} | t2], intersection_fun, seen ) when k1 > k2 do - if is_optional_static(v2) do + 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, {v1, optional1?}} | t1], + [{_, {v2, optional2?}} | t2], + intersection_fun, + seen + ) do + 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 @@ -3213,11 +3154,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 -> @@ -3295,10 +3231,12 @@ 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, {value, optional?}} -> + {optional?, value} + + :error -> + {value, optional?} = map_key_tag_to_field(tag) + {optional?, value} end end @@ -3309,43 +3247,45 @@ defmodule Module.Types.Descr do defp map_fetch_key_static(%{}, _key), do: {false, none()} defp map_fetch_key_static(:term, _key), do: {true, term()} - # 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 whether the key is optional and the union of + # present-value types it can take. defp map_dnf_fetch_static(dnf, key) do - Enum.reduce(dnf, none(), 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 + {value, optional?} = + Enum.reduce(dnf, {none(), false}, fn + # Optimization: if there are no negatives + {tag, fields, []}, acc -> + field = + case fields_find(key, fields) do + {:ok, field} -> field + :error -> map_key_tag_to_field(tag) + end - {tag, fields, negs}, acc -> - {value, bdd} = map_pop_key_bdd(tag, fields, key) + field_union(field, acc) - case map_split_negative_pairs_key(negs, key) do - :empty -> - acc + {tag, fields, negs}, acc -> + {field, bdd} = map_pop_key_bdd(tag, fields, key) - negative -> - value = - if map_pair_projection_keeps_full_fst?(negative, bdd) do - value - else - negs - |> map_split_negative_key(key, value, bdd) - |> Enum.reduce(none(), fn {value, _}, acc -> opt_union(value, acc) end) - end + case map_split_negative_pairs_key(negs, key) do + :empty -> + acc - opt_union(value, acc) - end - end) - catch - :open -> {true, term()} - else - value -> - pop_optional_static(value) + negative -> + field = + if map_pair_projection_keeps_full_fst?(negative, bdd) do + field + else + negs + |> map_split_negative_key(key, field, bdd) + |> Enum.reduce({none(), false}, fn {field, _}, acc -> + field_union(field, acc) + end) + end + + field_union(field, acc) + end + end) + + {optional?, value} end defp map_split_negative_pairs_key(negs, key) do @@ -3372,20 +3312,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 @@ -3398,39 +3341,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 @@ -3443,14 +3386,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 """ @@ -3516,9 +3459,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 @@ -3536,8 +3479,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 @@ -3555,7 +3496,7 @@ 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 @@ -3588,17 +3529,31 @@ defmodule Module.Types.Descr do """ 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: map_update_optional(descr, key_descr, type, false, return_type?, force?) - def map_update(descr, key_descr, type, return_type?, force?) do + def map_update(descr, key_descr, type, optional?, return_type?, force?) + when is_boolean(optional?), + do: map_update_optional(descr, key_descr, type, optional?, return_type?, force?) + + defp map_update_optional(descr, key_descr, :term, optional?, return_type?, force?) do + map_update_unchecked( + descr, + key_descr, + fn _, _ -> {:term, optional?} end, + return_type?, + force? + ) + end + + defp map_update_optional(descr, key_descr, type, optional?, return_type?, force?) do case type do %{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 @@ -3619,23 +3574,29 @@ defmodule Module.Types.Descr do 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 + value = if gradual?, do: dynamic(value), else: value + {new_value, new_optional?} = map_update_field(type_fun.(value)) + + new_value = + if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value + + {new_value, optional? or new_optional?} else value = if gradual?, do: dynamic(value), else: value + {new_value, new_optional?} = map_update_field(type_fun.(optional?, value)) - case type_fun.(optional?, value) do - %{dynamic: dynamic} -> dynamic - descr -> descr - end + new_value = + if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value + + {new_value, new_optional?} end end map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) end + defp map_update_field({value, optional?}) when is_boolean(optional?), do: {value, optional?} + def map_update_unchecked(:term, _key_descr, _type_fun, _return_type?, _force?), do: :badmap def map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) do @@ -3730,9 +3691,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) @@ -3771,15 +3732,17 @@ defmodule Module.Types.Descr 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} + {{value, optional?}, descr} = + map_dnf_pop_key_static(dnf, key, {none(), false}) + + {{optional?, value}, descr} else {{true, term()}, %{map: map_new(:open, fields)}} end _ -> - {value, descr} = map_dnf_pop_key_static(dnf, key, none()) - {pop_optional_static(value), descr} + {{value, optional?}, descr} = map_dnf_pop_key_static(dnf, key, {none(), false}) + {{optional?, value}, descr} end if not force? and empty?(value) do @@ -3788,8 +3751,10 @@ defmodule Module.Types.Descr do else acc_value = opt_union(value, acc_value) + {new_value, new_optional?} = type_fun.(optional?, value) + 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. @@ -3805,17 +3770,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. # @@ -3830,23 +3798,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_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) @@ -3855,11 +3823,11 @@ defmodule Module.Types.Descr do do: [], else: map_split_negative_key(negs, key, fst, snd) - {maybe_opt_union(value, fn -> + {maybe_field_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_union(elem(&1, 0), &2)) end end), if keep_snd? do @@ -3871,16 +3839,16 @@ 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 @@ -3895,7 +3863,7 @@ 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 @@ -3994,7 +3962,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, remove_optional(type_fun.(true, none()))), + do: fields_from_keys(domain_keys, elem(type_fun.(true, none()), 0)), else: :closed # Note: domain_keys may contain duplicates, so we cannot @@ -4005,14 +3973,14 @@ defmodule Module.Types.Descr do {:ok, value} -> fields_store( domain_key, - opt_union(value, remove_optional(type_fun.(true, value))), + opt_union(value, elem(type_fun.(true, value), 0)), acc ) :error -> # Likewise, only forced updates may synthesize missing domain keys. if force?, - do: fields_store(domain_key, remove_optional(type_fun.(true, none())), acc), + do: fields_store(domain_key, elem(type_fun.(true, none()), 0), acc), else: acc end end) @@ -4085,7 +4053,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 @@ -4094,7 +4062,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 @@ -4112,14 +4080,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 @@ -4179,7 +4147,7 @@ defmodule Module.Types.Descr do acc = map_get_keys(dnf, map_keys_from_negated_set(maybe_negated_set, bdd), 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)) - remove_optional(acc) + acc end defp map_get_static(%{}, _split_keys), do: none() @@ -4214,7 +4182,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) @@ -4314,7 +4282,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 @@ -4344,7 +4314,7 @@ defmodule Module.Types.Descr do end defp map_line_meet_empty?( - [{k1, v1} | t1], + [{k1, {v1, optional1?}} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4357,21 +4327,41 @@ defmodule Module.Types.Descr 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, {v1, optional1?}} | 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) + v2 = map_key_tag_to_field(neg_tag) + + map_line_meet_empty?( + k1, + {v1, optional1?}, + v2, + t1, + l2, + tag, + neg_tag, + acc_meet, + negs, + seen + ) end end defp map_line_meet_empty?( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, {v2, optional2?}} | t2], tag, neg_tag, acc_meet, @@ -4382,61 +4372,139 @@ 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 + 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) + v1 = map_key_tag_to_field(tag) + + map_line_meet_empty?( + k2, + v1, + {v2, optional2?}, + l1, + t2, + tag, + neg_tag, + acc_meet, + negs, + seen + ) end end defp map_line_meet_empty?( - [{k, v1} | t1], - [{_, v2} | t2], + [{k, {v1, optional1?}} | t1], + [{_, {v2, optional2?}} | 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) + map_line_meet_empty?( + k, + {v1, optional1?}, + {v2, optional2?}, + 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, {v1, optional1?}} | t1], + [], + tag, + neg_tag, + acc_meet, + negs, + seen + ) do + v2 = map_key_tag_to_field(neg_tag) + map_line_meet_empty?(k1, {v1, optional1?}, v2, 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, {v2, optional2?}} | t2], + tag, + neg_tag, + acc_meet, + negs, + seen + ) do + v1 = map_key_tag_to_field(tag) + map_line_meet_empty?(k2, v1, {v2, optional2?}, [], 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) + map_line_empty?( + tag, + Enum.reverse(acc_meet, [{key, {type, optional?}} | t1]), + negs, + seen + ) else diff = bare_difference(type, neg_type) + diff_optional? = optional? and not neg_optional? - 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, + [{key, {meet, meet_optional?}} | 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) + map_line_empty?( + tag, + Enum.reverse(acc_meet, [{key, {diff, diff_optional?}} | t1]), + negs, + seen + ) and + map_line_meet_empty?( + t1, + t2, + tag, + neg_tag, + [{key, {meet, meet_optional?}} | acc_meet], + negs, + seen + ) end end end defp map_line_fields_empty?( - [{k1, v1} | t1], + [{k1, {v1, optional1?}} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4452,14 +4520,14 @@ 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), + {v1, optional1?}, + map_key_tag_to_field(neg_tag), tag, fields, negs, @@ -4471,7 +4539,7 @@ defmodule Module.Types.Descr do defp map_line_fields_empty?( [{k1, _} | _] = l1, - [{k2, v2} | t2], + [{k2, {v2, optional2?}} | t2], tag, neg_tag, fields, @@ -4483,42 +4551,85 @@ defmodule Module.Types.Descr do # 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 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 + map_line_fields_empty_recur?( + k2, + map_key_tag_to_field(tag), + {v2, optional2?}, + 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], + [{key, {v1, optional1?}} | t1], + [{_, {v2, optional2?}} | t2], tag, neg_tag, fields, negs, seen ) do - map_line_fields_empty_recur?(key, v1, v2, tag, fields, negs, seen) and + map_line_fields_empty_recur?( + key, + {v1, optional1?}, + {v2, optional2?}, + 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, {v1, optional1?}} -> + map_line_fields_empty_recur?( + key, + {v1, optional1?}, + map_key_tag_to_field(neg_tag), + 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, {v2, optional2?}} -> + map_line_fields_empty_recur?( + key, + map_key_tag_to_field(tag), + {v2, optional2?}, + 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 @@ -4601,7 +4712,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 @@ -4614,7 +4725,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, @@ -4623,10 +4734,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?}} {:%, [], [ @@ -4693,10 +4804,10 @@ defmodule Module.Types.Descr do end defp map_fields_to_quoted(tag, sorted, opts) do - keyword? = Inspect.List.keyword?(sorted) + keyword? = Inspect.List.keyword?(Enum.map(sorted, fn {key, _} -> {key, nil} end)) - 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]} @@ -4704,13 +4815,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, [], []} @@ -4794,6 +4903,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 + {opt_union(value1, value2), optional1? or optional2?} + end + + defp maybe_field_union(nil, _fun), do: nil + defp maybe_field_union(field, fun), do: field_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: @@ -5306,10 +5456,10 @@ 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_union(field, acc) end) end - {optional?, descr} = pop_optional_static(value) + {descr, optional?} = value {optional? or acc_optional?, opt_union(descr, acc_descr)} end end) @@ -5330,34 +5480,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 @@ -5395,11 +5545,11 @@ 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()} @@ -5409,8 +5559,8 @@ defmodule Module.Types.Descr do 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 @@ -5418,8 +5568,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(), true} @doc """ Returns all of the values that are part of a tuple. @@ -6350,8 +6500,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 @@ -6388,7 +6538,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) @@ -6397,8 +6546,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) @@ -6429,7 +6578,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) @@ -6443,7 +6591,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 @@ -6472,7 +6620,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) @@ -6486,10 +6633,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 @@ -6581,7 +6724,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} @@ -6608,7 +6751,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 @@ -6632,7 +6775,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 @@ -6644,24 +6787,42 @@ defmodule Module.Types.Descr do end end - defp opt_map_union_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do + defp opt_map_union_strategy( + [{_, field} | t1], + [{_, field} | t2], + tag1, + tag2, + status + ) do # Same key and same value, nothing changes opt_map_union_strategy(t1, t2, tag1, tag2, status) end - defp opt_map_union_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do + defp opt_map_union_strategy( + [{k1, field1} | t1], + [{_, field2} | t2], + tag1, + tag2, + status + ) do # They have the same key but different values case status do :all_equal -> cond do # Don't do difference on struct keys k1 != :__struct__ and tag1 == tag2 -> - opt_map_union_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) + opt_map_union_strategy( + t1, + t2, + tag1, + tag2, + {:one_key_difference, k1, field1, field2} + ) - subtype?(v1, v2) -> + field_subtype?(field1, field2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - subtype?(v2, v1) -> + field_subtype?(field2, field1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -6669,17 +6830,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?(field1, field2), + 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?(field2, field1), + 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?(field1, field2) -> 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?(field2, field1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -6712,8 +6877,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 @@ -6792,28 +6957,33 @@ 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 = {_value2, optional2?}}]), + type, + seen + ) when type != :union do - {found?, v1} = + {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 @@ -6833,13 +7003,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 @@ -6847,14 +7017,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} @@ -6870,14 +7040,20 @@ 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, {_value, optional?}} | t1], + [{k2, _} | _] = l2, + tag1, + tag2, + status + ) when k1 < k2 do # 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) @@ -6887,7 +7063,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 @@ -6899,42 +7075,67 @@ 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, {_value, optional?}} | t2], + tag1, + tag2, + _status + ) when k1 > k2 do # 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) end end - defp opt_map_difference_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do + defp opt_map_difference_strategy( + [{_, field} | t1], + [{_, field} | t2], + tag1, + tag2, + status + ) do # Same key and same value, nothing changes opt_map_difference_strategy(t1, t2, tag1, tag2, status) end - defp opt_map_difference_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do + defp opt_map_difference_strategy( + [{k1, field1} | t1], + [{_, field2} | t2], + tag1, + tag2, + status + ) do # They have the same key but different values - if disjoint?(v1, v2) do + if field_disjoint?(field1, field2) do :disjoint else case status do :all_equal when tag1 == tag2 -> - opt_map_difference_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) + opt_map_difference_strategy( + t1, + t2, + tag1, + tag2, + {:one_key_difference, k1, field1, field2} + ) {:one_key_difference, _key, p1, p2} -> - if subtype?(p1, p2) and subtype?(v1, v2) do + if field_subtype?(p1, p2) and field_subtype?(field1, field2) 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?(field1, field2), + 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 @@ -6951,7 +7152,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 @@ -6960,10 +7161,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 -> @@ -7006,7 +7207,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 225f50309ba..5885d465359 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) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index ec8794382b0..0ea1a9502d4 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 @@ -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 @@ -485,10 +490,10 @@ defmodule Module.Types.Of do 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 ++ Enum.map(args_types, fn {key, value} -> {key, {value, false}} end) + end + closed_map(pairs) end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index c96de6f504b..0e488d41e68 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,15 @@ 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) + of_open_map( + args, + singleton?(refined), + [__struct__: {refined, false}], + [], + path, + stack, + context + ) else error = {:badstruct, refined, expr, context} {error_type(), false, error(__MODULE__, error, meta, stack, context)} @@ -861,7 +873,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 +1177,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 4156d0c62de..3a4147ace52 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/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 8a3bfefb5e2..c2d0777ab1e 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,60 +2820,82 @@ 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()) == + {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()) == + {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()) == {:error, [badkey: :key]} # ...unless forcing assert map_update( - open_map(key: if_set(atom([:value]))), + open_map(key: {atom([:value]), true}), atom([:key]), integer(), 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() + ) == + {dynamic(atom([:value])), dynamic(open_map(key: {integer(), false})), []} assert map_update(dynamic(), atom([:key]), integer()) == - {dynamic(), dynamic(open_map(key: integer())), []} + {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()) == {: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(), + 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() + ) == {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() + ) == {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(), true, @@ -2784,30 +2903,39 @@ defmodule Module.Types.DescrTest do ) == {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()) == {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()))) + |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) |> map_update(atom([:b]), integer()) == {:error, [badkey: :b]} # ... even when forcing 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_update(atom([:b]), integer(), 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()), []} + {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 @@ -2818,7 +2946,7 @@ defmodule Module.Types.DescrTest do fun = fn _optional?, value -> send(self(), :callback_invoked) - value + {value, false} end assert map_update_fun(type, binary(), fun, false, false) == {dynamic(none()), type, []} @@ -2827,48 +2955,48 @@ 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])) 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]) ) 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()) 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() ) == - {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() ) == {: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(), true, @@ -2876,24 +3004,36 @@ defmodule Module.Types.DescrTest do ) == {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() ) == {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() ) == {:error, []} @@ -2974,40 +3114,58 @@ 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() + ) 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(), + 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() + ) 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,13 +3173,13 @@ defmodule Module.Types.DescrTest do # A "none()" map 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_update(binary(), integer()) == {:error, [baddomain: binary()]} # ... even when forcing {type, descr, errors} = 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_update(binary(), integer(), true, true) assert empty?(type) @@ -3035,15 +3193,15 @@ defmodule Module.Types.DescrTest do # 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() ) == {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,40 +3209,52 @@ 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() ) 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() ) == {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() ) == {: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(), true, @@ -3093,106 +3263,136 @@ defmodule Module.Types.DescrTest do {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 +3449,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 +3473,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 +3507,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 +3548,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 +3572,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 +3662,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 +3766,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 +3798,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 +3827,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 +3993,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 +4177,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 +4285,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 +4335,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 +4351,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 46759aabeae..0de7ebf4c7e 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 c3e69a39f96..0bddc4f5cd7 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 ecdd0f664ef..ad2a3ec7d67 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 diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index 24b0df12d74..f656f8ae649 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 b79fc1d2675..2263491401c 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 7017d2da53b..5914d12d706 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() From 70502265ecf0c4a9d63b619552adc61f549eb85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 00:01:09 +0200 Subject: [PATCH 3/9] Fix field_ dispatches --- lib/elixir/lib/module/types/descr.ex | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 6d7e6f4b6ba..be20008a55d 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3260,7 +3260,7 @@ defmodule Module.Types.Descr do :error -> map_key_tag_to_field(tag) end - field_union(field, acc) + field_opt_union(field, acc, %{}) {tag, fields, negs}, acc -> {field, bdd} = map_pop_key_bdd(tag, fields, key) @@ -3277,11 +3277,11 @@ defmodule Module.Types.Descr do negs |> map_split_negative_key(key, field, bdd) |> Enum.reduce({none(), false}, fn {field, _}, acc -> - field_union(field, acc) + field_opt_union(field, acc, %{}) end) end - field_union(field, acc) + field_opt_union(field, acc, %{}) end end) @@ -3803,7 +3803,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives, we can directly remove the key. {tag, fields, []}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) - {maybe_field_union(field, fn -> fst end), opt_map_union(bdd, snd, %{})} + {maybe_field_opt_union(field, fn -> fst end, %{}), opt_map_union(bdd, snd, %{})} {tag, fields, negs}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) @@ -3823,13 +3823,17 @@ defmodule Module.Types.Descr do do: [], else: map_split_negative_key(negs, key, fst, snd) - {maybe_field_union(field, fn -> - if keep_fst? do - fst - else - Enum.reduce(pairs, {none(), false}, &field_union(elem(&1, 0), &2)) - end - end), + {maybe_field_opt_union( + field, + fn -> + if keep_fst? do + fst + else + Enum.reduce(pairs, {none(), false}, &field_opt_union(elem(&1, 0), &2, %{})) + end + end, + %{} + ), if keep_snd? do opt_map_union(bdd, snd, %{}) else @@ -4904,11 +4908,11 @@ defmodule Module.Types.Descr do do: Enum.map(f1, fn {k, v1} -> {k, fun.(k, v1, d2)} end) defp field_union({value1, optional1?}, {value2, optional2?}) do - {opt_union(value1, value2), optional1? or optional2?} + {bare_union(value1, value2), optional1? or optional2?} end - defp maybe_field_union(nil, _fun), do: nil - defp maybe_field_union(field, fun), do: field_union(field, fun.()) + defp maybe_field_opt_union(nil, _fun, _seen), do: nil + defp maybe_field_opt_union(field, fun, seen), do: field_opt_union(field, fun.(), seen) defp field_intersection({value1, optional1?}, {value2, optional2?}) do {bare_intersection(value1, value2), optional1? and optional2?} @@ -5456,7 +5460,9 @@ defmodule Module.Types.Descr do else negs |> tuple_split_negative(index, value, bdd) - |> Enum.reduce({none(), false}, fn {field, _}, acc -> field_union(field, acc) end) + |> Enum.reduce({none(), false}, fn {field, _}, acc -> + field_opt_union(field, acc, %{}) + end) end {descr, optional?} = value From 10630a93b3b7ae53ac330732a453ed3e4d6a8496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 00:19:34 +0200 Subject: [PATCH 4/9] More reorg --- lib/elixir/lib/module/types/apply.ex | 8 +- lib/elixir/lib/module/types/descr.ex | 158 +++++++----------- lib/elixir/lib/module/types/expr.ex | 4 +- lib/elixir/lib/module/types/of.ex | 6 +- lib/elixir/lib/module/types/pattern.ex | 4 +- lib/elixir/src/elixir_erl.erl | 2 +- .../test/elixir/module/types/descr_test.exs | 76 ++++----- .../elixir/module/types/recursive_test.exs | 14 +- lib/mix/lib/mix/compilers/elixir.ex | 2 +- 9 files changed, 120 insertions(+), 154 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 680a98f8edd..2a09a9d102c 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -471,7 +471,7 @@ defmodule Module.Types.Apply do remote_error({:negindex, index - 1}, :erlang, :element, 2, expr, stack, context) else case tuple_fetch(tuple_type, index - 1) do - {_optional?, value_type} -> + {value_type, _optional?} -> {return(value_type, [tuple_type], stack), context} :badtuple -> @@ -570,7 +570,7 @@ defmodule Module.Types.Apply do remote_error({:negindex, index}, Kernel, :elem, 2, expr, stack, context) else case tuple_fetch(tuple_type, index) do - {_optional?, value_type} -> + {value_type, _optional?} -> {return(value_type, [tuple_type], stack), context} :badtuple -> @@ -883,8 +883,8 @@ defmodule Module.Types.Apply do disjoint?(left_type, right_type) -> {:mismatched_comparison, left_type, right_type} - match?({false, _}, map_fetch_key(dynamic(left_type), :__struct__)) and - match?({false, _}, map_fetch_key(dynamic(right_type), :__struct__)) -> + match?({_, false}, map_fetch_key(dynamic(left_type), :__struct__)) and + match?({_, false}, map_fetch_key(dynamic(right_type), :__struct__)) -> {:struct_comparison, left_type, right_type} true -> diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index be20008a55d..3eda42c1695 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3187,9 +3187,9 @@ defmodule Module.Types.Descr do Fetches the type of the value returned by accessing `key` on `map` with the assumption that the descr is exclusively a map (or dynamic). - It returns a two element tuple or `:error`. The first element says - if the type is dynamically optional or not, the second element is - the type. In static mode, optional keys are not allowed. + It returns a two element tuple or `:error`. The first element is the + type and the second element says if the type is dynamically optional + or not. In static mode, optional keys are not allowed. Being dynamically optional means that the field may be present (while statically optional means we need to consider the field as @@ -3201,12 +3201,12 @@ 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 else - {false, static_type} + {static_type, false} end else :badmap @@ -3214,13 +3214,13 @@ 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 else - {dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} + {opt_union(dynamic(dynamic_type), static_type), dynamic_optional?} end else :badmap @@ -3231,12 +3231,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, optional?}} -> - {optional?, value} - - :error -> - {value, optional?} = map_key_tag_to_field(tag) - {optional?, value} + {:ok, field} -> field + :error -> map_key_tag_to_field(tag) end end @@ -3244,8 +3240,8 @@ 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 whether the key is optional and the union of # present-value types it can take. @@ -3285,7 +3281,7 @@ defmodule Module.Types.Descr do end end) - {optional?, value} + {value, optional?} end defp map_split_negative_pairs_key(negs, key) do @@ -3500,7 +3496,7 @@ defmodule Module.Types.Descr do 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 @@ -3575,7 +3571,7 @@ defmodule Module.Types.Descr do type_fun = fn optional?, value -> if is_function(type_fun, 1) do value = if gradual?, do: dynamic(value), else: value - {new_value, new_optional?} = map_update_field(type_fun.(value)) + {new_value, new_optional?} = type_fun.(value) new_value = if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value @@ -3583,7 +3579,7 @@ defmodule Module.Types.Descr do {new_value, optional? or new_optional?} else value = if gradual?, do: dynamic(value), else: value - {new_value, new_optional?} = map_update_field(type_fun.(optional?, value)) + {new_value, new_optional?} = type_fun.(optional?, value) new_value = if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value @@ -3595,8 +3591,6 @@ defmodule Module.Types.Descr do map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) end - defp map_update_field({value, optional?}) when is_boolean(optional?), do: {value, optional?} - def map_update_unchecked(:term, _key_descr, _type_fun, _return_type?, _force?), do: :badmap def map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) do @@ -3727,22 +3721,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, optional?}, descr} = - map_dnf_pop_key_static(dnf, key, {none(), false}) - - {{optional?, 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, optional?}, descr} = map_dnf_pop_key_static(dnf, key, {none(), false}) - {{optional?, value}, descr} + map_dnf_pop_key_static(dnf, key, {none(), false}) end if not force? and empty?(value) do @@ -3856,7 +3846,7 @@ defmodule Module.Types.Descr do 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) @@ -3871,7 +3861,7 @@ defmodule Module.Types.Descr do 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 @@ -4159,7 +4149,7 @@ 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 @@ -4808,7 +4798,7 @@ defmodule Module.Types.Descr do end defp map_fields_to_quoted(tag, sorted, opts) do - keyword? = Inspect.List.keyword?(Enum.map(sorted, fn {key, _} -> {key, nil} end)) + keyword? = Inspect.List.keyword?(sorted) for {key, {type, optional?}} <- sorted, not (tag == :open and optional? and term_type?(type)) do @@ -5351,18 +5341,18 @@ defmodule Module.Types.Descr do Returns one of: - - `{false, type}` if the element is always accessible and has the given `type`. - - `{true, type}` if the element is dynamically optional and has the given `type`. + - `{type, false}` if the element is always accessible and has the given `type`. + - `{type, true}` if the element is dynamically optional and has the given `type`. - `:badindex` if the index is never accessible in the tuple type. - `:badtuple` if the descr is not a tuple type. ## Examples iex> tuple_fetch(tuple([integer(), atom()]), 0) - {false, integer()} + {integer(), false} iex> tuple_fetch(dynamic(), 0) - {true, dynamic()} + {dynamic(), true} iex> tuple_fetch(bare_union(tuple([integer()]), tuple([integer(), atom()])), 1) :badindex @@ -5378,7 +5368,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: # @@ -5397,7 +5387,7 @@ defmodule Module.Types.Descr do if static_optional? or empty?(static_type) do :badindex else - {false, static_type} + {static_type, false} end else :badtuple @@ -5405,13 +5395,13 @@ 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 else - {static_optional? or dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} + {opt_union(dynamic(dynamic_type), static_type), static_optional? or dynamic_optional?} end else :badtuple @@ -5430,28 +5420,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 = @@ -5466,11 +5456,11 @@ defmodule Module.Types.Descr do end {descr, optional?} = value - {optional? or acc_optional?, opt_union(descr, acc_descr)} + {opt_union(descr, acc_descr), optional? or acc_optional?} end end) catch - :open -> {true, term()} + :open -> {term(), true} end # Remove negatives: @@ -5558,9 +5548,9 @@ defmodule Module.Types.Descr do 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 @@ -5575,7 +5565,7 @@ defmodule Module.Types.Descr do defp do_tuple_take_element([h | t], i, acc), do: do_tuple_take_element(t, i - 1, [h | acc]) defp tuple_tag_to_field(:open), do: {term(), true} - defp tuple_tag_to_field(:closed), do: {none(), true} + defp tuple_tag_to_field(:closed), do: {none(), false} @doc """ Returns all of the values that are part of a tuple. @@ -6793,24 +6783,12 @@ defmodule Module.Types.Descr do end end - defp opt_map_union_strategy( - [{_, field} | t1], - [{_, field} | t2], - tag1, - tag2, - status - ) do + defp opt_map_union_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do # Same key and same value, nothing changes opt_map_union_strategy(t1, t2, tag1, tag2, status) end - defp opt_map_union_strategy( - [{k1, field1} | t1], - [{_, field2} | t2], - tag1, - tag2, - status - ) do + defp opt_map_union_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do # They have the same key but different values case status do :all_equal -> @@ -6822,13 +6800,13 @@ defmodule Module.Types.Descr do t2, tag1, tag2, - {:one_key_difference, k1, field1, field2} + {:one_key_difference, k1, v1, v2} ) - field_subtype?(field1, field2) -> + field_subtype?(v1, v2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - field_subtype?(field2, field1) -> + field_subtype?(v2, v1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -6836,21 +6814,21 @@ defmodule Module.Types.Descr do end :left_subtype_of_right -> - if field_subtype?(field1, field2), + if field_subtype?(v1, v2), do: opt_map_union_strategy(t1, t2, tag1, tag2, status), else: :none :right_subtype_of_left -> - if field_subtype?(field2, field1), + 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 - field_subtype?(p1, p2) and field_subtype?(field1, field2) -> + field_subtype?(p1, p2) and field_subtype?(v1, v2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - field_subtype?(p2, p1) and field_subtype?(field2, field1) -> + field_subtype?(p2, p1) and field_subtype?(v2, v1) -> opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) true -> @@ -7098,26 +7076,14 @@ defmodule Module.Types.Descr do end end - defp opt_map_difference_strategy( - [{_, field} | t1], - [{_, field} | t2], - tag1, - tag2, - status - ) do + defp opt_map_difference_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do # Same key and same value, nothing changes opt_map_difference_strategy(t1, t2, tag1, tag2, status) end - defp opt_map_difference_strategy( - [{k1, field1} | t1], - [{_, field2} | t2], - tag1, - tag2, - status - ) do + defp opt_map_difference_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do # They have the same key but different values - if field_disjoint?(field1, field2) do + if field_disjoint?(v1, v2) do :disjoint else case status do @@ -7127,11 +7093,11 @@ defmodule Module.Types.Descr do t2, tag1, tag2, - {:one_key_difference, k1, field1, field2} + {:one_key_difference, k1, v1, v2} ) {:one_key_difference, _key, p1, p2} -> - if field_subtype?(p1, p2) and field_subtype?(field1, field2) 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 @@ -7139,7 +7105,7 @@ defmodule Module.Types.Descr do _ -> if status in [:all_equal, :left_subtype_of_right] and - field_subtype?(field1, field2), + 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 diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 5885d465359..40d47a43213 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -222,7 +222,7 @@ defmodule Module.Types.Expr do {map_type, context} = of_expr(map, term(), struct, stack, context) context = - with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__), + with {struct_key_type, false} <- map_fetch_key(map_type, :__struct__), {:finite, [^module]} <- atom_fetch(struct_key_type) do context else @@ -754,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 + {type, _optional?} -> type _ -> term() end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 0ea1a9502d4..1516d71074f 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -305,7 +305,7 @@ defmodule Module.Types.Of do """ def map_fetch(expr, type, field, stack, context) when is_atom(field) do case map_fetch_key(type, field) do - {_optional?, value_type} -> + {value_type, _optional?} -> {value_type, context} reason -> @@ -428,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 + {expected_value_type, _optional?} <- map_fetch_key(expected, key) do expected_value_type else _ -> term() @@ -485,7 +485,7 @@ 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 + {expected_value_type, _optional?} -> expected_value_type _ -> term() end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 0e488d41e68..bdf641df8c1 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -469,7 +469,7 @@ defmodule Module.Types.Pattern do defp of_pattern_var([{:elem, index} | rest], type, context) when is_integer(index) do case tuple_fetch(type, index) do - {_optional?, type} -> of_pattern_var(rest, type, context) + {type, _optional?} -> of_pattern_var(rest, type, context) _reason -> :error end end @@ -477,7 +477,7 @@ defmodule Module.Types.Pattern do defp of_pattern_var([{:key, field} | rest], type, context) when is_atom(field) do case map_fetch_key(type, field) do - {_optional?, type} -> of_pattern_var(rest, type, context) + {type, _optional?} -> of_pattern_var(rest, type, context) _reason -> :error end end diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 97ab8788f6b..ca29a307e2b 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 c2d0777ab1e..dff79a5b98d 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -859,10 +859,10 @@ defmodule Module.Types.DescrTest do t_diff = opt_difference(a_number, atom_to_float) # Removing atom keys that map to float, make the :a key point to integer only. - assert map_fetch_key(t_diff, :a) == {false, integer()} + assert map_fetch_key(t_diff, :a) == {integer(), false} # %{a => number, atom => pid} and not %{atom => float} gives numbers on :a assert map_fetch_key(opt_difference(a_number_and_pids, atom_to_float), :a) == - {false, number()} + {number(), false} assert map_fetch_key(t_diff, :foo) == :badkey @@ -1932,12 +1932,12 @@ defmodule Module.Types.DescrTest do assert tuple_fetch(tuple([none(), atom()]), 1) == :badtuple assert tuple_fetch(tuple([none()]), 0) == :badtuple - assert tuple_fetch(tuple([integer(), atom()]), 0) == {false, integer()} - assert tuple_fetch(tuple([integer(), atom()]), 1) == {false, atom()} + assert tuple_fetch(tuple([integer(), atom()]), 0) == {integer(), false} + assert tuple_fetch(tuple([integer(), atom()]), 1) == {atom(), false} assert tuple_fetch(tuple([integer(), atom()]), 2) == :badindex - assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} - assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} + assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {integer(), false} + assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {atom(), false} assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex @@ -1945,25 +1945,25 @@ defmodule Module.Types.DescrTest do assert opt_difference(tuple(), tuple()) |> tuple_fetch(0) == :badtuple assert tuple([atom()]) |> opt_difference(empty_tuple()) |> tuple_fetch(0) == - {false, atom()} + {atom(), false} assert opt_difference(tuple([opt_union(integer(), atom())]), open_tuple([atom()])) - |> tuple_fetch(0) == {false, integer()} + |> tuple_fetch(0) == {integer(), false} assert tuple_fetch(opt_union(tuple([integer(), atom()]), dynamic(open_tuple([atom()]))), 1) - |> Kernel.then(fn {opt, ty} -> opt and equal?(ty, opt_union(atom(), dynamic())) end) + |> Kernel.then(fn {ty, opt} -> opt and equal?(ty, opt_union(atom(), dynamic())) end) assert tuple_fetch(opt_union(tuple([integer()]), tuple([atom()])), 0) == - {false, opt_union(integer(), atom())} + {opt_union(integer(), atom()), false} assert tuple([integer(), atom(), opt_union(atom(), integer())]) |> opt_difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + |> tuple_fetch(2) == {integer(), false} assert tuple([integer(), atom(), opt_union(opt_union(atom(), integer()), list(term()))]) |> opt_difference(tuple([integer(), term(), atom()])) |> opt_difference(open_tuple([term(), atom(), list(term())])) - |> tuple_fetch(2) == {false, integer()} + |> tuple_fetch(2) == {integer(), false} assert tuple([integer(), atom(), integer()]) |> opt_difference(tuple([integer(), term(), integer()])) @@ -1971,25 +1971,25 @@ defmodule Module.Types.DescrTest do assert tuple([integer(), atom(), integer()]) |> opt_difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {false, integer()} + |> tuple_fetch(2) == {integer(), false} assert tuple_fetch(tuple(), 0) == :badindex - assert tuple_fetch(projected_negative_tuple(200), 1) == {false, term()} + assert tuple_fetch(projected_negative_tuple(200), 1) == {term(), false} end test "tuple_fetch with dynamic" do - assert tuple_fetch(dynamic(), 0) == {true, dynamic()} + assert tuple_fetch(dynamic(), 0) == {dynamic(), true} assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex assert tuple_fetch(opt_union(dynamic(), integer()), 0) == :badtuple assert tuple_fetch(tuple([none()]), 0) == :badtuple assert tuple_fetch(dynamic(tuple()), 0) - |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) + |> Kernel.then(fn {type, opt} -> opt and equal?(type, dynamic()) end) assert tuple_fetch(opt_union(dynamic(), open_tuple([atom()])), 0) == - {true, opt_union(atom(), dynamic())} + {opt_union(atom(), dynamic()), true} end test "tuple_delete_at" do @@ -2114,7 +2114,7 @@ defmodule Module.Types.DescrTest do |> equal?(open_tuple([term(), boolean()])) inserted = tuple_insert_at(projected_negative_tuple(200), 1, atom([:inserted])) - assert tuple_fetch(inserted, 1) == {false, atom([:inserted])} + assert tuple_fetch(inserted, 1) == {atom([:inserted]), false} # Test inserting into a difference of tuples assert opt_difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) @@ -2477,50 +2477,50 @@ defmodule Module.Types.DescrTest do ) == :badkey - assert map_fetch_key(closed_map(a: {integer(), false}), :a) == {false, integer()} + assert map_fetch_key(closed_map(a: {integer(), false}), :a) == {integer(), false} assert map_fetch_key( opt_union(closed_map(a: {integer(), false}), closed_map(a: {atom(), false})), :a ) == - {false, opt_union(integer(), atom())} + {opt_union(integer(), atom()), false} - {false, value_type} = + {value_type, false} = 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(), false}, bar: {boolean(), false})) - {false, value_type} = + {value_type, false} = 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} = + {value_type, false} = 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} = + {value_type, false} = closed_map(a: {integer(), false}) |> opt_difference(closed_map(a: {atom(), false})) |> map_fetch_key(:a) assert equal?(value_type, integer()) - {false, value_type} = + {value_type, false} = 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} = + {value_type, false} = closed_map(a: {atom(), false}) |> opt_difference(closed_map(a: {atom([:foo, :bar]), false})) |> opt_difference(closed_map(a: {atom([:bar]), false})) @@ -2535,7 +2535,7 @@ defmodule Module.Types.DescrTest do ) |> 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()} + |> map_fetch_key(:a) == {pid(), false} assert closed_map( a: {opt_union(atom([:foo]), pid()), false}, @@ -2544,21 +2544,21 @@ defmodule Module.Types.DescrTest do ) |> 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()} + |> map_fetch_key(:a) == {pid(), false} 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()} + |> map_fetch_key(:a) == {integer(), false} end # Times out without a projection-only map_fetch_key path test "map_fetch_key with projected negative maps" do - assert map_fetch_key(projected_negative_map(100), :k) == {false, open_map()} + assert map_fetch_key(projected_negative_map(100), :k) == {open_map(), false} end test "map_fetch_key with dynamic" do - assert map_fetch_key(dynamic(), :a) == {true, dynamic()} + assert map_fetch_key(dynamic(), :a) == {dynamic(), true} assert map_fetch_key(opt_union(dynamic(), integer()), :a) == :badmap assert map_fetch_key(opt_union(dynamic(open_map(a: {integer(), false})), integer()), :a) == @@ -2567,9 +2567,9 @@ defmodule Module.Types.DescrTest do assert map_fetch_key(opt_union(dynamic(integer()), integer()), :a) == :badmap assert opt_intersection(dynamic(), open_map(a: {integer(), false})) - |> map_fetch_key(:a) == {false, opt_intersection(integer(), dynamic())} + |> map_fetch_key(:a) == {opt_intersection(integer(), dynamic()), false} - {false, type} = + {type, false} = opt_union(dynamic(integer()), open_map(a: {integer(), false})) |> map_fetch_key(:a) assert equal?(type, integer()) @@ -2579,7 +2579,7 @@ defmodule Module.Types.DescrTest do :badkey assert opt_union(dynamic(open_map(a: {atom(), false})), open_map(a: {integer(), false})) - |> map_fetch_key(:a) == {false, opt_union(dynamic(atom()), integer())} + |> map_fetch_key(:a) == {opt_union(dynamic(atom()), integer()), false} end test "map_fetch_key with domain keys" do @@ -2594,19 +2594,19 @@ defmodule Module.Types.DescrTest do # Indeed, t2 is equivalent to the empty map assert map_fetch_key(opt_difference(t1, t2), :a) == :badkey - assert map_fetch_key(opt_difference(t1, t3), :a) == {false, pid()} + assert map_fetch_key(opt_difference(t1, t3), :a) == {pid(), false} t4 = closed_map([{domain_key(:pid), atom()}]) - assert map_fetch_key(opt_difference(t1, t4) |> opt_difference(t3), :a) == {false, pid()} + assert map_fetch_key(opt_difference(t1, t4) |> opt_difference(t3), :a) == {pid(), false} assert map_fetch_key(closed_map([{domain_key(:atom), pid()}]), :a) == :badkey assert map_fetch_key(dynamic(closed_map([{domain_key(:atom), pid()}])), :a) == - {true, dynamic(pid())} + {dynamic(pid()), true} assert closed_map([{domain_key(:atom), number()}]) |> opt_difference(open_map(a: {integer(), true})) - |> map_fetch_key(:a) == {false, float()} + |> map_fetch_key(:a) == {float(), false} assert closed_map([{domain_key(:atom), number()}]) |> opt_difference(closed_map(b: {integer(), true})) diff --git a/lib/elixir/test/elixir/module/types/recursive_test.exs b/lib/elixir/test/elixir/module/types/recursive_test.exs index 5914d12d706..704a8f8d5de 100644 --- a/lib/elixir/test/elixir/module/types/recursive_test.exs +++ b/lib/elixir/test/elixir/module/types/recursive_test.exs @@ -483,18 +483,18 @@ defmodule Module.Types.RecursiveTest do t = unfold(nx) - assert {false, type} = tuple_fetch(t, 0) + assert {type, false} = tuple_fetch(t, 0) assert equal?(type, integer()) - assert {false, _type} = tuple_fetch(t, 1) + assert {_type, false} = tuple_fetch(t, 1) result = tuple_values(t) assert subtype?(integer(), result) result = tuple_delete_at(t, 0) - assert {false, _type} = tuple_fetch(result, 0) + assert {_type, false} = tuple_fetch(result, 0) result = tuple_insert_at(t, 0, boolean()) - assert {false, type} = tuple_fetch(result, 0) + assert {type, false} = tuple_fetch(result, 0) assert equal?(type, boolean()) # X = {X} | {atom()} @@ -503,7 +503,7 @@ defmodule Module.Types.RecursiveTest do tx = unfold(nx) t = opt_difference(tx, tuple([atom()])) - assert {false, type} = tuple_fetch(t, 0) + assert {type, false} = tuple_fetch(t, 0) assert equal?(type, tx) ## map_fetch_key on descr with recursive node value @@ -516,9 +516,9 @@ defmodule Module.Types.RecursiveTest do end }) - assert {false, type} = map_fetch_key(unfold(nx), :a) + assert {type, false} = map_fetch_key(unfold(nx), :a) assert equal?(type, integer()) - assert {false, _type} = map_fetch_key(unfold(nx), :b) + assert {_type, false} = map_fetch_key(unfold(nx), :b) ## list_hd and list_tl on descr with recursive node tail # X = non_empty_list(integer(), X) | non_empty_list(integer(), []) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 597934cffe1..c1536ce8eed 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 From 7bbefe39b58b5793c4fa872c543b5955fd3a3c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 00:30:13 +0200 Subject: [PATCH 5/9] More --- lib/elixir/lib/module/types/apply.ex | 14 +- lib/elixir/lib/module/types/descr.ex | 279 +++++------------- .../test/elixir/module/types/descr_test.exs | 2 +- .../elixir/module/types/integration_test.exs | 2 +- .../elixir/protocol/consolidation_test.exs | 2 +- 5 files changed, 92 insertions(+), 207 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 2a09a9d102c..976aa181504 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -1280,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 -> {value, optional?} 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)} @@ -1290,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 -> {value, optional?} 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)} @@ -1317,7 +1317,7 @@ 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, false} else @@ -1449,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 -> {value, optional?} 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)} @@ -1820,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} - false, type -> {if(empty?(type), do: value, else: type), false} + 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 @@ -1839,7 +1839,7 @@ 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} -> {res, optional?} reason -> throw({:badapply, reason, [arg_type]}) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 3eda42c1695..622c630b3a2 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3050,12 +3050,14 @@ defmodule Module.Types.Descr do defp map_domain_intersection_fields(_, _, _seen), do: [] defp map_literal_intersection_open_closed( - [{k1, {_v1, optional1?}} | 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 if optional1?, do: map_literal_intersection_open_closed(t1, l2, intersection_fun, seen), @@ -3064,24 +3066,23 @@ defmodule Module.Types.Descr do defp map_literal_intersection_open_closed( [{k1, _} | _] = l1, - [{k2, {v2, optional2?}} | t2], + [{k2, f2} | t2], intersection_fun, seen ) when k1 > k2 do # Anything in the closed map not in open is preserved - [ - {k2, {v2, optional2?}} - | 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, optional1?}} | t1], - [{_, {v2, optional2?}} | 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) @@ -3101,12 +3102,14 @@ defmodule Module.Types.Descr do end defp map_literal_intersection_closed( - [{k1, {_v1, optional1?}} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, intersection_fun, seen ) when k1 < k2 do + {_, optional1?} = f1 + if optional1? do map_literal_intersection_closed(t1, l2, intersection_fun, seen) else @@ -3116,11 +3119,13 @@ defmodule Module.Types.Descr do defp map_literal_intersection_closed( [{k1, _} | _] = l1, - [{k2, {_v2, optional2?}} | t2], + [{k2, f2} | t2], intersection_fun, seen ) when k1 > k2 do + {_, optional2?} = f2 + if optional2? do map_literal_intersection_closed(l1, t2, intersection_fun, seen) else @@ -3128,12 +3133,9 @@ defmodule Module.Types.Descr do end end - defp map_literal_intersection_closed( - [{key, {v1, optional1?}} | t1], - [{_, {v2, optional2?}} | t2], - intersection_fun, - seen - ) do + 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) @@ -3562,13 +3564,13 @@ defmodule Module.Types.Descr do This is a more general version of `map_update/5` 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 -> + type_fun = fn value, optional? -> if is_function(type_fun, 1) do value = if gradual?, do: dynamic(value), else: value {new_value, new_optional?} = type_fun.(value) @@ -3579,7 +3581,7 @@ defmodule Module.Types.Descr do {new_value, optional? or new_optional?} else value = if gradual?, do: dynamic(value), else: value - {new_value, new_optional?} = type_fun.(optional?, value) + {new_value, new_optional?} = type_fun.(value, optional?) new_value = if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value @@ -3741,7 +3743,7 @@ defmodule Module.Types.Descr do else acc_value = opt_union(value, acc_value) - {new_value, new_optional?} = type_fun.(optional?, value) + {new_value, new_optional?} = type_fun.(value, optional?) acc_descr = opt_union(map_put_key_static(descr, key, new_value, new_optional?), acc_descr) @@ -3956,7 +3958,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, elem(type_fun.(true, none()), 0)), + do: fields_from_keys(domain_keys, elem(type_fun.(none(), true), 0)), else: :closed # Note: domain_keys may contain duplicates, so we cannot @@ -3967,14 +3969,14 @@ defmodule Module.Types.Descr do {:ok, value} -> fields_store( domain_key, - opt_union(value, elem(type_fun.(true, value), 0)), + 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, elem(type_fun.(true, none()), 0), acc), + do: fields_store(domain_key, elem(type_fun.(none(), true), 0), acc), else: acc end end) @@ -4308,7 +4310,7 @@ defmodule Module.Types.Descr do end defp map_line_meet_empty?( - [{k1, {v1, optional1?}} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4317,45 +4319,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, optional1?}} | 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 optional1? -> throw(:closed) true -> - v2 = map_key_tag_to_field(neg_tag) - - map_line_meet_empty?( - k1, - {v1, optional1?}, - 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, optional2?}} | t2], + [{k2, f2} | t2], tag, neg_tag, acc_meet, @@ -4366,73 +4350,28 @@ 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 and not optional2? do throw(:closed) else - v1 = map_key_tag_to_field(tag) - - map_line_meet_empty?( - k2, - v1, - {v2, optional2?}, - 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, optional1?}} | t1], - [{_, {v2, optional2?}} | t2], - tag, - neg_tag, - acc_meet, - negs, - seen - ) do - map_line_meet_empty?( - k, - {v1, optional1?}, - {v2, optional2?}, - 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, optional1?}} | t1], - [], - tag, - neg_tag, - acc_meet, - negs, - seen - ) do - v2 = map_key_tag_to_field(neg_tag) - map_line_meet_empty?(k1, {v1, optional1?}, 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, optional2?}} | t2], - tag, - neg_tag, - acc_meet, - negs, - seen - ) do - v1 = map_key_tag_to_field(tag) - map_line_meet_empty?(k2, v1, {v2, optional2?}, [], 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 @@ -4456,49 +4395,27 @@ defmodule Module.Types.Descr 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, optional?}} | 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? + acc_meet = [{key, {meet, meet_optional?}} | acc_meet] 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, {meet, meet_optional?}} | acc_meet], - negs, - seen - ) + map_line_meet_empty?(t1, t2, tag, neg_tag, acc_meet, negs, seen) else - map_line_empty?( - tag, - Enum.reverse(acc_meet, [{key, {diff, diff_optional?}} | t1]), - negs, - seen - ) and - map_line_meet_empty?( - t1, - t2, - tag, - neg_tag, - [{key, {meet, meet_optional?}} | 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, acc_meet, negs, seen) end end end defp map_line_fields_empty?( - [{k1, {v1, optional1?}} | t1], + [{k1, f1} | t1], [{k2, _} | _] = l2, tag, neg_tag, @@ -4507,6 +4424,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. @@ -4518,22 +4437,16 @@ defmodule Module.Types.Descr do throw(:closed) true -> - map_line_fields_empty_recur?( - k1, - {v1, optional1?}, - map_key_tag_to_field(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, optional2?}} | t2], + [{k2, f2} | t2], tag, neg_tag, fields, @@ -4544,6 +4457,8 @@ 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 optional2? do map_line_fields_empty?(l1, t2, tag, neg_tag, fields, negs, seen) @@ -4551,62 +4466,26 @@ defmodule Module.Types.Descr do throw(:closed) end else - map_line_fields_empty_recur?( - k2, - map_key_tag_to_field(tag), - {v2, optional2?}, - 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, optional1?}} | t1], - [{_, {v2, optional2?}} | t2], - tag, - neg_tag, - fields, - negs, - seen - ) do - map_line_fields_empty_recur?( - key, - {v1, optional1?}, - {v2, optional2?}, - 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, optional1?}} -> - map_line_fields_empty_recur?( - key, - {v1, optional1?}, - map_key_tag_to_field(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, optional2?}} -> - map_line_fields_empty_recur?( - key, - map_key_tag_to_field(tag), - {v2, optional2?}, - 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 @@ -6943,11 +6822,13 @@ defmodule Module.Types.Descr do # 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, field2 = {_value2, optional2?}}]), + bdd_leaf(:open, [{key, field2}]), type, seen ) when type != :union do + {_, optional2?} = field2 + {found?, field1} = case fields_find(key, fields) do {:ok, field} -> {true, field} @@ -7025,13 +6906,15 @@ defmodule Module.Types.Descr do end defp opt_map_difference_strategy( - [{k1, {_value, optional?}} | t1], + [{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. @@ -7061,12 +6944,14 @@ defmodule Module.Types.Descr do defp opt_map_difference_strategy( [{k1, _} | _] = l1, - [{k2, {_value, optional?}} | t2], + [{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 optional? do diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index dff79a5b98d..c053e059785 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -2944,7 +2944,7 @@ 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, 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 ad2a3ec7d67..d759577f6f3 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -1969,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/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index 8664f4eb6c6..7540f1739c3 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 From 35605294584eeb53a9caafe35d09602493979e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 00:37:10 +0200 Subject: [PATCH 6/9] Avoid clash --- lib/elixir/lib/module/types/descr.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 622c630b3a2..128ab9d086c 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -4400,16 +4400,16 @@ defmodule Module.Types.Descr do else diff = bare_difference(type, neg_type) diff_optional? = optional? and not neg_optional? - acc_meet = [{key, {meet, meet_optional?}} | acc_meet] + new_acc_meet = [{key, {meet, meet_optional?}} | acc_meet] 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, acc_meet, negs, seen) + map_line_meet_empty?(t1, t2, tag, neg_tag, new_acc_meet, negs, seen) else 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, acc_meet, negs, seen) + map_line_meet_empty?(t1, t2, tag, neg_tag, new_acc_meet, negs, seen) end end end From 36ed4a1d560e8a09ccc0d13cf1a303d5bd80a0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 00:49:47 +0200 Subject: [PATCH 7/9] More nits --- lib/elixir/lib/module/types/apply.ex | 4 +- lib/elixir/lib/module/types/descr.ex | 175 +++++++----------- lib/elixir/lib/module/types/expr.ex | 2 +- .../test/elixir/module/types/descr_test.exs | 111 +++++++---- 4 files changed, 142 insertions(+), 150 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 976aa181504..7a05ad97be1 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -1264,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} @@ -1412,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} diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 128ab9d086c..80991b2e75e 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3245,45 +3245,45 @@ defmodule Module.Types.Descr do 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 whether the key is optional and the union of - # present-value types it can take. + # 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 - {value, optional?} = - Enum.reduce(dnf, {none(), false}, fn - # Optimization: if there are no negatives - {tag, fields, []}, acc -> - field = - case fields_find(key, fields) do - {:ok, field} -> field - :error -> map_key_tag_to_field(tag) - end - - field_opt_union(field, acc, %{}) + Enum.reduce(dnf, {none(), false}, fn + # Optimization: if there are no negatives + {tag, fields, []}, acc -> + field = + case fields_find(key, fields) do + {:ok, field} -> field + :error when tag == :open -> throw(:open) + :error -> map_key_tag_to_field(tag) + end - {tag, fields, negs}, acc -> - {field, bdd} = map_pop_key_bdd(tag, fields, key) + field_opt_union(field, acc, %{}) - case map_split_negative_pairs_key(negs, key) do - :empty -> - acc + {tag, fields, negs}, acc -> + {field, bdd} = map_pop_key_bdd(tag, fields, key) - negative -> - field = - if map_pair_projection_keeps_full_fst?(negative, bdd) do - field - else - negs - |> map_split_negative_key(key, field, bdd) - |> Enum.reduce({none(), false}, fn {field, _}, acc -> - field_opt_union(field, acc, %{}) - end) - end + case map_split_negative_pairs_key(negs, key) do + :empty -> + acc - field_opt_union(field, acc, %{}) - end - end) + negative -> + field = + if map_pair_projection_keeps_full_fst?(negative, bdd) do + field + else + negs + |> map_split_negative_key(key, field, bdd) + |> Enum.reduce({none(), false}, fn {field, _}, acc -> + field_opt_union(field, acc, %{}) + end) + end - {value, optional?} + field_opt_union(field, acc, %{}) + end + end) + catch + :open -> {term(), true} end defp map_split_negative_pairs_key(negs, key) do @@ -3525,27 +3525,18 @@ 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, type, return_type?, force?), - do: map_update_optional(descr, key_descr, type, false, return_type?, force?) - - def map_update(descr, key_descr, type, optional?, return_type?, force?) - when is_boolean(optional?), - do: map_update_optional(descr, key_descr, type, optional?, return_type?, force?) - - defp map_update_optional(descr, key_descr, :term, optional?, return_type?, force?) do - map_update_unchecked( - descr, - key_descr, - fn _, _ -> {:term, optional?} end, - return_type?, - force? - ) - end - - defp map_update_optional(descr, key_descr, type, optional?, 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, optional?} end map_update_unchecked(dynamic(descr), key_descr, fun, return_type?, force?) @@ -3562,7 +3553,7 @@ 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 whether it is optional. Note the value returned by `type_fun` cannot hold dynamic. Any dynamic conversion must happen before invoking this function. @@ -3571,23 +3562,13 @@ defmodule Module.Types.Descr do gradual? = gradual?(descr) type_fun = fn value, optional? -> - if is_function(type_fun, 1) do - value = if gradual?, do: dynamic(value), else: value - {new_value, new_optional?} = type_fun.(value) - - new_value = - if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value - - {new_value, optional? or new_optional?} - else - value = if gradual?, do: dynamic(value), else: value - {new_value, new_optional?} = type_fun.(value, optional?) + value = if gradual?, do: dynamic(value), else: value + {new_value, new_optional?} = type_fun.(value, optional?) - new_value = - if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value + new_value = + if is_map(new_value), do: Map.get(new_value, :dynamic, new_value), else: new_value - {new_value, new_optional?} - end + {new_value, new_optional?} end map_update_unchecked(descr, key_descr, type_fun, return_type?, force?) @@ -3795,7 +3776,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives, we can directly remove the key. {tag, fields, []}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) - {maybe_field_opt_union(field, fn -> fst end, %{}), opt_map_union(bdd, snd, %{})} + {maybe_field_opt_union(field, fn -> fst end), opt_map_union(bdd, snd, %{})} {tag, fields, negs}, {field, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) @@ -3815,17 +3796,13 @@ defmodule Module.Types.Descr do do: [], else: map_split_negative_key(negs, key, fst, snd) - {maybe_field_opt_union( - field, - fn -> - if keep_fst? do - fst - else - Enum.reduce(pairs, {none(), false}, &field_opt_union(elem(&1, 0), &2, %{})) - end - end, - %{} - ), + {maybe_field_opt_union(field, fn -> + if keep_fst? do + fst + else + Enum.reduce(pairs, {none(), false}, &field_opt_union(elem(&1, 0), &2, %{})) + end + end), if keep_snd? do opt_map_union(bdd, snd, %{}) else @@ -4780,8 +4757,8 @@ defmodule Module.Types.Descr do {bare_union(value1, value2), optional1? or optional2?} end - defp maybe_field_opt_union(nil, _fun, _seen), do: nil - defp maybe_field_opt_union(field, fun, seen), do: field_opt_union(field, fun.(), seen) + 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?} @@ -6674,13 +6651,7 @@ defmodule Module.Types.Descr do cond do # Don't do difference on struct keys k1 != :__struct__ and tag1 == tag2 -> - opt_map_union_strategy( - t1, - t2, - tag1, - tag2, - {:one_key_difference, k1, v1, v2} - ) + opt_map_union_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) field_subtype?(v1, v2) -> opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) @@ -6905,13 +6876,7 @@ defmodule Module.Types.Descr do end end - defp opt_map_difference_strategy( - [{k1, field} | 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 @@ -6942,13 +6907,7 @@ defmodule Module.Types.Descr do end end - defp opt_map_difference_strategy( - [{k1, _} | _] = l1, - [{k2, field} | t2], - tag1, - tag2, - _status - ) + defp opt_map_difference_strategy([{k1, _} | _] = l1, [{k2, field} | t2], tag1, tag2, _status) when k1 > k2 do {_, optional?} = field @@ -6973,13 +6932,7 @@ defmodule Module.Types.Descr do else case status do :all_equal when tag1 == tag2 -> - opt_map_difference_strategy( - t1, - t2, - tag1, - tag2, - {:one_key_difference, k1, v1, v2} - ) + opt_map_difference_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) {:one_key_difference, _key, p1, p2} -> if field_subtype?(p1, p2) and field_subtype?(v1, v2) do diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 40d47a43213..b0abf3ab597 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -1048,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/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index c053e059785..14dec981e34 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -2820,14 +2820,14 @@ defmodule Module.Types.DescrTest do describe "map_update" do test "with static atom keys" do - assert map_update(open_map(key: {binary(), false}), atom([: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(), false})), atom([: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: {atom([:value]), true}), atom([:key]), integer()) == + assert map_update(open_map(key: {atom([:value]), true}), atom([:key]), integer(), false) == {:error, [badkey: :key]} # ...unless forcing @@ -2835,6 +2835,7 @@ defmodule Module.Types.DescrTest do open_map(key: {atom([:value]), true}), atom([:key]), integer(), + false, true, true ) == @@ -2844,15 +2845,16 @@ defmodule Module.Types.DescrTest do assert map_update( dynamic(open_map(key: {atom([:value]), true})), atom([:key]), - integer() + integer(), + false ) == {dynamic(atom([:value])), dynamic(open_map(key: {integer(), false})), []} - assert map_update(dynamic(), atom([: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: {none(), true}), atom([:key]), integer()) == + assert map_update(closed_map(key: {none(), true}), atom([:key]), integer(), false) == {:error, [badkey: :key]} # ...unless forcing @@ -2860,6 +2862,7 @@ defmodule Module.Types.DescrTest do closed_map(key: {none(), true}), atom([:key]), integer(), + false, true, true ) == @@ -2869,7 +2872,8 @@ defmodule Module.Types.DescrTest do assert map_update( open_map(key1: {atom(), false}, key2: {binary(), false}), atom([:key1, :key2]), - integer() + integer(), + false ) == {opt_union(atom(), binary()), opt_union( @@ -2881,7 +2885,8 @@ defmodule Module.Types.DescrTest do assert map_update( open_map(key1: {atom(), false}, key2: {binary(), false}), atom([:key1, :key3]), - integer() + integer(), + false ) == {term(), opt_union( @@ -2898,6 +2903,7 @@ defmodule Module.Types.DescrTest do open_map(key1: {atom(), false}, key2: {binary(), false}), atom([:key1, :key3]), integer(), + false, true, true ) == @@ -2912,7 +2918,7 @@ defmodule Module.Types.DescrTest do ), []} # ...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( @@ -2924,17 +2930,17 @@ defmodule Module.Types.DescrTest do # A "none" map assert open_map() |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) - |> map_update(atom([:b]), integer()) == {:error, [badkey: :b]} + |> map_update(atom([:b]), integer(), false) == {:error, [badkey: :b]} # ... even when forcing assert open_map() |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) - |> map_update(atom([:b]), integer(), true, true) == {none(), none(), []} + |> 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()) == + assert map_update(projected_negative_map(100), atom([:k]), binary(), false) == {open_map(), open_map(k: {binary(), false}, x: {term(), false}), []} end @@ -2955,7 +2961,12 @@ defmodule Module.Types.DescrTest do test "with dynamic atom keys" do assert {type, descr, errors} = - map_update(closed_map(key: {atom([:value]), false}), 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]), false})) @@ -2965,7 +2976,8 @@ defmodule Module.Types.DescrTest do map_update( dynamic(closed_map(key: {atom([:value]), false})), dynamic(), - atom([:new_value]) + atom([:new_value]), + false ) assert equal?(type, dynamic(atom([:value]))) @@ -2974,7 +2986,12 @@ defmodule Module.Types.DescrTest do # Check struct fields assert {type, descr, errors} = - map_update(open_map(__struct__: {term(), false}), dynamic(atom()), integer()) + map_update( + open_map(__struct__: {term(), false}), + dynamic(atom()), + integer(), + false + ) assert type == term() assert equal?(descr, open_map(__struct__: {term(), false})) @@ -2984,14 +3001,16 @@ defmodule Module.Types.DescrTest do assert map_update( closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key1, :key3])), - integer() + integer(), + false ) == {atom(), closed_map(key1: {integer(), false}, key2: {binary(), false}), []} assert map_update( closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), - integer() + integer(), + false ) == {:error, []} # ...unless forcing @@ -2999,6 +3018,7 @@ defmodule Module.Types.DescrTest do closed_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), integer(), + false, true, true ) == @@ -3020,7 +3040,8 @@ defmodule Module.Types.DescrTest do assert map_update( open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key1, :key3])), - integer() + integer(), + false ) == {term(), opt_union( @@ -3035,7 +3056,8 @@ defmodule Module.Types.DescrTest do assert map_update( open_map(key1: {atom(), false}, key2: {binary(), false}), dynamic(atom([:key3, :key4])), - integer() + integer(), + false ) == {:error, []} end @@ -3047,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())}, @@ -3058,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())}, @@ -3066,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()}, @@ -3077,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([ @@ -3089,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([ @@ -3098,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()}, @@ -3117,7 +3148,8 @@ defmodule Module.Types.DescrTest do map_update( closed_map(key1: {binary(), false}, key2: {pid(), false}), atom(), - integer() + integer(), + false ) assert equal?(type, opt_union(binary(), pid())) @@ -3137,6 +3169,7 @@ defmodule Module.Types.DescrTest do closed_map(key1: {binary(), false}, key2: {pid(), false}), atom(), integer(), + false, true, true ) == @@ -3156,7 +3189,8 @@ defmodule Module.Types.DescrTest do map_update( closed_map(key1: {binary(), false}, key2: {pid(), false}), dynamic(atom()), - integer() + integer(), + false ) assert equal?(type, opt_union(binary(), pid())) @@ -3174,13 +3208,13 @@ defmodule Module.Types.DescrTest do # A "none()" map assert open_map() |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) - |> map_update(binary(), integer()) == {:error, [baddomain: binary()]} + |> map_update(binary(), integer(), false) == {:error, [baddomain: binary()]} # ... even when forcing {type, descr, errors} = open_map() |> opt_difference(open_map(a: {term(), true}, c: {term(), true})) - |> map_update(binary(), integer(), true, true) + |> map_update(binary(), integer(), false, true, true) assert empty?(type) assert empty?(descr) @@ -3188,14 +3222,15 @@ 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(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:key]), integer())), - integer() + integer(), + false ) == {opt_union(atom(), binary()), opt_union( @@ -3211,7 +3246,8 @@ defmodule Module.Types.DescrTest do map_update( closed_map(key1: {binary(), false}, key2: {binary(), false}), opt_difference(atom(), atom([:key1])), - integer() + integer(), + false ) assert equal?(type, binary()) @@ -3229,7 +3265,8 @@ defmodule Module.Types.DescrTest do [{domain_key(:atom), pid()}] ), opt_difference(atom(), atom([:key1])), - integer() + integer(), + false ) == {opt_union(binary(), pid()), opt_union( @@ -3249,7 +3286,8 @@ defmodule Module.Types.DescrTest do assert map_update( closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:other_key]), pid())), - integer() + integer(), + false ) == {:error, []} # ...unless forcing @@ -3257,6 +3295,7 @@ defmodule Module.Types.DescrTest do closed_map([{:key, {atom(), false}}, {domain_key(:integer), binary()}]), dynamic(opt_union(atom([:other_key]), pid())), integer(), + false, true, true ) == From 4ab367b48bd3d562f62fbb6d91de8713e8badc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 01:05:33 +0200 Subject: [PATCH 8/9] Reduce changes --- lib/elixir/lib/module/types/apply.ex | 8 +- lib/elixir/lib/module/types/descr.ex | 22 +++--- lib/elixir/lib/module/types/expr.ex | 4 +- lib/elixir/lib/module/types/of.ex | 6 +- lib/elixir/lib/module/types/pattern.ex | 4 +- .../test/elixir/module/types/descr_test.exs | 76 +++++++++---------- .../elixir/module/types/recursive_test.exs | 14 ++-- 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 7a05ad97be1..cfd9db80d59 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -471,7 +471,7 @@ defmodule Module.Types.Apply do remote_error({:negindex, index - 1}, :erlang, :element, 2, expr, stack, context) else case tuple_fetch(tuple_type, index - 1) do - {value_type, _optional?} -> + {_optional?, value_type} -> {return(value_type, [tuple_type], stack), context} :badtuple -> @@ -570,7 +570,7 @@ defmodule Module.Types.Apply do remote_error({:negindex, index}, Kernel, :elem, 2, expr, stack, context) else case tuple_fetch(tuple_type, index) do - {value_type, _optional?} -> + {_optional?, value_type} -> {return(value_type, [tuple_type], stack), context} :badtuple -> @@ -883,8 +883,8 @@ defmodule Module.Types.Apply do disjoint?(left_type, right_type) -> {:mismatched_comparison, left_type, right_type} - match?({_, false}, map_fetch_key(dynamic(left_type), :__struct__)) and - match?({_, false}, map_fetch_key(dynamic(right_type), :__struct__)) -> + match?({false, _}, map_fetch_key(dynamic(left_type), :__struct__)) and + match?({false, _}, map_fetch_key(dynamic(right_type), :__struct__)) -> {:struct_comparison, left_type, right_type} true -> diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 80991b2e75e..cff1b18db0b 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3189,9 +3189,9 @@ defmodule Module.Types.Descr do Fetches the type of the value returned by accessing `key` on `map` with the assumption that the descr is exclusively a map (or dynamic). - It returns a two element tuple or `:error`. The first element is the - type and the second element says if the type is dynamically optional - or not. In static mode, optional keys are not allowed. + It returns a two element tuple or `:error`. The first element says + if the type is dynamically optional or not, the second element is + the type. In static mode, optional keys are not allowed. Being dynamically optional means that the field may be present (while statically optional means we need to consider the field as @@ -3208,7 +3208,7 @@ defmodule Module.Types.Descr do if static_optional? or empty?(static_type) do :badkey else - {static_type, false} + {false, static_type} end else :badmap @@ -3222,7 +3222,7 @@ defmodule Module.Types.Descr do if static_optional? or empty?(dynamic_type) do :badkey else - {opt_union(dynamic(dynamic_type), static_type), dynamic_optional?} + {dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} end else :badmap @@ -5197,18 +5197,18 @@ defmodule Module.Types.Descr do Returns one of: - - `{type, false}` if the element is always accessible and has the given `type`. - - `{type, true}` if the element is dynamically optional and has the given `type`. + - `{false, type}` if the element is always accessible and has the given `type`. + - `{true, type}` if the element is dynamically optional and has the given `type`. - `:badindex` if the index is never accessible in the tuple type. - `:badtuple` if the descr is not a tuple type. ## Examples iex> tuple_fetch(tuple([integer(), atom()]), 0) - {integer(), false} + {false, integer()} iex> tuple_fetch(dynamic(), 0) - {dynamic(), true} + {true, dynamic()} iex> tuple_fetch(bare_union(tuple([integer()]), tuple([integer(), atom()])), 1) :badindex @@ -5243,7 +5243,7 @@ defmodule Module.Types.Descr do if static_optional? or empty?(static_type) do :badindex else - {static_type, false} + {false, static_type} end else :badtuple @@ -5257,7 +5257,7 @@ defmodule Module.Types.Descr do if empty?(dynamic_type) do :badindex else - {opt_union(dynamic(dynamic_type), static_type), static_optional? or dynamic_optional?} + {static_optional? or dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} end else :badtuple diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index b0abf3ab597..1a38b288988 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -222,7 +222,7 @@ defmodule Module.Types.Expr do {map_type, context} = of_expr(map, term(), struct, stack, context) context = - with {struct_key_type, false} <- map_fetch_key(map_type, :__struct__), + with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__), {:finite, [^module]} <- atom_fetch(struct_key_type) do context else @@ -754,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, _optional?} -> type + {_optional?, type} -> type _ -> term() end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 1516d71074f..2a9bbdca224 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -305,7 +305,7 @@ defmodule Module.Types.Of do """ def map_fetch(expr, type, field, stack, context) when is_atom(field) do case map_fetch_key(type, field) do - {value_type, _optional?} -> + {_optional?, value_type} -> {value_type, context} reason -> @@ -428,7 +428,7 @@ defmodule Module.Types.Of do expected_value_type = with {[key], [], []} <- pos_neg_domain, - {expected_value_type, _optional?} <- map_fetch_key(expected, key) do + {_optional?, expected_value_type} <- map_fetch_key(expected, key) do expected_value_type else _ -> term() @@ -485,7 +485,7 @@ 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, _optional?} -> expected_value_type + {_optional?, expected_value_type} -> expected_value_type _ -> term() end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index bdf641df8c1..0e488d41e68 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -469,7 +469,7 @@ defmodule Module.Types.Pattern do defp of_pattern_var([{:elem, index} | rest], type, context) when is_integer(index) do case tuple_fetch(type, index) do - {type, _optional?} -> of_pattern_var(rest, type, context) + {_optional?, type} -> of_pattern_var(rest, type, context) _reason -> :error end end @@ -477,7 +477,7 @@ defmodule Module.Types.Pattern do defp of_pattern_var([{:key, field} | rest], type, context) when is_atom(field) do case map_fetch_key(type, field) do - {type, _optional?} -> of_pattern_var(rest, type, context) + {_optional?, type} -> of_pattern_var(rest, type, context) _reason -> :error end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 14dec981e34..5bdb9bac31b 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -859,10 +859,10 @@ defmodule Module.Types.DescrTest do t_diff = opt_difference(a_number, atom_to_float) # Removing atom keys that map to float, make the :a key point to integer only. - assert map_fetch_key(t_diff, :a) == {integer(), false} + assert map_fetch_key(t_diff, :a) == {false, integer()} # %{a => number, atom => pid} and not %{atom => float} gives numbers on :a assert map_fetch_key(opt_difference(a_number_and_pids, atom_to_float), :a) == - {number(), false} + {false, number()} assert map_fetch_key(t_diff, :foo) == :badkey @@ -1932,12 +1932,12 @@ defmodule Module.Types.DescrTest do assert tuple_fetch(tuple([none(), atom()]), 1) == :badtuple assert tuple_fetch(tuple([none()]), 0) == :badtuple - assert tuple_fetch(tuple([integer(), atom()]), 0) == {integer(), false} - assert tuple_fetch(tuple([integer(), atom()]), 1) == {atom(), false} + assert tuple_fetch(tuple([integer(), atom()]), 0) == {false, integer()} + assert tuple_fetch(tuple([integer(), atom()]), 1) == {false, atom()} assert tuple_fetch(tuple([integer(), atom()]), 2) == :badindex - assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {integer(), false} - assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {atom(), false} + assert tuple_fetch(open_tuple([integer(), atom()]), 0) == {false, integer()} + assert tuple_fetch(open_tuple([integer(), atom()]), 1) == {false, atom()} assert tuple_fetch(open_tuple([integer(), atom()]), 2) == :badindex assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex @@ -1945,25 +1945,25 @@ defmodule Module.Types.DescrTest do assert opt_difference(tuple(), tuple()) |> tuple_fetch(0) == :badtuple assert tuple([atom()]) |> opt_difference(empty_tuple()) |> tuple_fetch(0) == - {atom(), false} + {false, atom()} assert opt_difference(tuple([opt_union(integer(), atom())]), open_tuple([atom()])) - |> tuple_fetch(0) == {integer(), false} + |> tuple_fetch(0) == {false, integer()} assert tuple_fetch(opt_union(tuple([integer(), atom()]), dynamic(open_tuple([atom()]))), 1) - |> Kernel.then(fn {ty, opt} -> opt and equal?(ty, opt_union(atom(), dynamic())) end) + |> Kernel.then(fn {opt, ty} -> opt and equal?(ty, opt_union(atom(), dynamic())) end) assert tuple_fetch(opt_union(tuple([integer()]), tuple([atom()])), 0) == - {opt_union(integer(), atom()), false} + {false, opt_union(integer(), atom())} assert tuple([integer(), atom(), opt_union(atom(), integer())]) |> opt_difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {integer(), false} + |> tuple_fetch(2) == {false, integer()} assert tuple([integer(), atom(), opt_union(opt_union(atom(), integer()), list(term()))]) |> opt_difference(tuple([integer(), term(), atom()])) |> opt_difference(open_tuple([term(), atom(), list(term())])) - |> tuple_fetch(2) == {integer(), false} + |> tuple_fetch(2) == {false, integer()} assert tuple([integer(), atom(), integer()]) |> opt_difference(tuple([integer(), term(), integer()])) @@ -1971,25 +1971,25 @@ defmodule Module.Types.DescrTest do assert tuple([integer(), atom(), integer()]) |> opt_difference(tuple([integer(), term(), atom()])) - |> tuple_fetch(2) == {integer(), false} + |> tuple_fetch(2) == {false, integer()} assert tuple_fetch(tuple(), 0) == :badindex - assert tuple_fetch(projected_negative_tuple(200), 1) == {term(), false} + assert tuple_fetch(projected_negative_tuple(200), 1) == {false, term()} end test "tuple_fetch with dynamic" do - assert tuple_fetch(dynamic(), 0) == {dynamic(), true} + assert tuple_fetch(dynamic(), 0) == {true, dynamic()} assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex assert tuple_fetch(opt_union(dynamic(), integer()), 0) == :badtuple assert tuple_fetch(tuple([none()]), 0) == :badtuple assert tuple_fetch(dynamic(tuple()), 0) - |> Kernel.then(fn {type, opt} -> opt and equal?(type, dynamic()) end) + |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) assert tuple_fetch(opt_union(dynamic(), open_tuple([atom()])), 0) == - {opt_union(atom(), dynamic()), true} + {true, opt_union(atom(), dynamic())} end test "tuple_delete_at" do @@ -2114,7 +2114,7 @@ defmodule Module.Types.DescrTest do |> equal?(open_tuple([term(), boolean()])) inserted = tuple_insert_at(projected_negative_tuple(200), 1, atom([:inserted])) - assert tuple_fetch(inserted, 1) == {atom([:inserted]), false} + assert tuple_fetch(inserted, 1) == {false, atom([:inserted])} # Test inserting into a difference of tuples assert opt_difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) @@ -2477,50 +2477,50 @@ defmodule Module.Types.DescrTest do ) == :badkey - assert map_fetch_key(closed_map(a: {integer(), false}), :a) == {integer(), false} + assert map_fetch_key(closed_map(a: {integer(), false}), :a) == {false, integer()} assert map_fetch_key( opt_union(closed_map(a: {integer(), false}), closed_map(a: {atom(), false})), :a ) == - {opt_union(integer(), atom()), false} + {false, opt_union(integer(), atom())} - {value_type, false} = + {false, value_type} = 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(), false}, bar: {boolean(), false})) - {value_type, false} = + {false, value_type} = closed_map(a: {opt_union(integer(), atom()), false}) |> opt_difference(open_map(a: {integer(), false})) |> map_fetch_key(:a) assert equal?(value_type, atom()) - {value_type, false} = + {false, value_type} = 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()) - {value_type, false} = + {false, value_type} = closed_map(a: {integer(), false}) |> opt_difference(closed_map(a: {atom(), false})) |> map_fetch_key(:a) assert equal?(value_type, integer()) - {value_type, false} = + {false, value_type} = 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())) - {value_type, false} = + {false, value_type} = closed_map(a: {atom(), false}) |> opt_difference(closed_map(a: {atom([:foo, :bar]), false})) |> opt_difference(closed_map(a: {atom([:bar]), false})) @@ -2535,7 +2535,7 @@ defmodule Module.Types.DescrTest do ) |> 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) == {pid(), false} + |> map_fetch_key(:a) == {false, pid()} assert closed_map( a: {opt_union(atom([:foo]), pid()), false}, @@ -2544,21 +2544,21 @@ defmodule Module.Types.DescrTest do ) |> 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) == {pid(), false} + |> map_fetch_key(:a) == {false, pid()} 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) == {integer(), false} + |> map_fetch_key(:a) == {false, integer()} end # Times out without a projection-only map_fetch_key path test "map_fetch_key with projected negative maps" do - assert map_fetch_key(projected_negative_map(100), :k) == {open_map(), false} + assert map_fetch_key(projected_negative_map(100), :k) == {false, open_map()} end test "map_fetch_key with dynamic" do - assert map_fetch_key(dynamic(), :a) == {dynamic(), true} + 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(), false})), integer()), :a) == @@ -2567,9 +2567,9 @@ defmodule Module.Types.DescrTest do assert map_fetch_key(opt_union(dynamic(integer()), integer()), :a) == :badmap assert opt_intersection(dynamic(), open_map(a: {integer(), false})) - |> map_fetch_key(:a) == {opt_intersection(integer(), dynamic()), false} + |> map_fetch_key(:a) == {false, opt_intersection(integer(), dynamic())} - {type, false} = + {false, type} = opt_union(dynamic(integer()), open_map(a: {integer(), false})) |> map_fetch_key(:a) assert equal?(type, integer()) @@ -2579,7 +2579,7 @@ defmodule Module.Types.DescrTest do :badkey assert opt_union(dynamic(open_map(a: {atom(), false})), open_map(a: {integer(), false})) - |> map_fetch_key(:a) == {opt_union(dynamic(atom()), integer()), false} + |> map_fetch_key(:a) == {false, opt_union(dynamic(atom()), integer())} end test "map_fetch_key with domain keys" do @@ -2594,19 +2594,19 @@ defmodule Module.Types.DescrTest do # Indeed, t2 is equivalent to the empty map assert map_fetch_key(opt_difference(t1, t2), :a) == :badkey - assert map_fetch_key(opt_difference(t1, t3), :a) == {pid(), false} + assert map_fetch_key(opt_difference(t1, t3), :a) == {false, pid()} t4 = closed_map([{domain_key(:pid), atom()}]) - assert map_fetch_key(opt_difference(t1, t4) |> opt_difference(t3), :a) == {pid(), false} + assert map_fetch_key(opt_difference(t1, t4) |> opt_difference(t3), :a) == {false, pid()} assert map_fetch_key(closed_map([{domain_key(:atom), pid()}]), :a) == :badkey assert map_fetch_key(dynamic(closed_map([{domain_key(:atom), pid()}])), :a) == - {dynamic(pid()), true} + {true, dynamic(pid())} assert closed_map([{domain_key(:atom), number()}]) |> opt_difference(open_map(a: {integer(), true})) - |> map_fetch_key(:a) == {float(), false} + |> map_fetch_key(:a) == {false, float()} assert closed_map([{domain_key(:atom), number()}]) |> opt_difference(closed_map(b: {integer(), true})) diff --git a/lib/elixir/test/elixir/module/types/recursive_test.exs b/lib/elixir/test/elixir/module/types/recursive_test.exs index 704a8f8d5de..5914d12d706 100644 --- a/lib/elixir/test/elixir/module/types/recursive_test.exs +++ b/lib/elixir/test/elixir/module/types/recursive_test.exs @@ -483,18 +483,18 @@ defmodule Module.Types.RecursiveTest do t = unfold(nx) - assert {type, false} = tuple_fetch(t, 0) + assert {false, type} = tuple_fetch(t, 0) assert equal?(type, integer()) - assert {_type, false} = tuple_fetch(t, 1) + assert {false, _type} = tuple_fetch(t, 1) result = tuple_values(t) assert subtype?(integer(), result) result = tuple_delete_at(t, 0) - assert {_type, false} = tuple_fetch(result, 0) + assert {false, _type} = tuple_fetch(result, 0) result = tuple_insert_at(t, 0, boolean()) - assert {type, false} = tuple_fetch(result, 0) + assert {false, type} = tuple_fetch(result, 0) assert equal?(type, boolean()) # X = {X} | {atom()} @@ -503,7 +503,7 @@ defmodule Module.Types.RecursiveTest do tx = unfold(nx) t = opt_difference(tx, tuple([atom()])) - assert {type, false} = tuple_fetch(t, 0) + assert {false, type} = tuple_fetch(t, 0) assert equal?(type, tx) ## map_fetch_key on descr with recursive node value @@ -516,9 +516,9 @@ defmodule Module.Types.RecursiveTest do end }) - assert {type, false} = map_fetch_key(unfold(nx), :a) + assert {false, type} = map_fetch_key(unfold(nx), :a) assert equal?(type, integer()) - assert {_type, false} = map_fetch_key(unfold(nx), :b) + assert {false, _type} = map_fetch_key(unfold(nx), :b) ## list_hd and list_tl on descr with recursive node tail # X = non_empty_list(integer(), X) | non_empty_list(integer(), []) From 4487a57413c7f747ae042812097101fbb7954514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2026 01:10:13 +0200 Subject: [PATCH 9/9] Compact --- lib/elixir/lib/module/types/expr.ex | 2 +- lib/elixir/lib/module/types/of.ex | 2 +- lib/elixir/lib/module/types/pattern.ex | 11 ++--------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 1a38b288988..bf7a114a532 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -769,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 -> diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 2a9bbdca224..c6662a99bac 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -539,7 +539,7 @@ defmodule Module.Types.Of do if args_types == [] do pairs else - pairs ++ Enum.map(args_types, fn {key, value} -> {key, {value, false}} end) + pairs ++ args_types end closed_map(pairs) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 0e488d41e68..04319202642 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -776,15 +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, false}], - [], - 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)}