diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index f9a0d48ac8..4581941d2a 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -37,13 +37,15 @@ defmodule Module.Types.Descr do defmacro bdd_leaf(arg1, arg2), do: {arg1, arg2} - # Map fields are stored as orddicts (sorted key-value lists). + # Map fields and domains are stored as orddicts (sorted key-value lists). @fields_new [] defguardp is_fields_empty(fields) when fields == [] defguardp fields_size(fields) when length(fields) - @domain_key_types [:binary, :integer, :float, :pid, :port, :reference] ++ - [:fun, :atom, :tuple, :map, :list] + @domain_key_types :lists.sort( + [:binary, :integer, :float, :pid, :port, :reference] ++ + [:fun, :atom, :tuple, :map, :list] + ) # Remark: those are explicit BDD constructors. The functional constructors are `bdd_new/1` and `bdd_new/3`. @fun_top {:negation, %{}} @@ -2649,14 +2651,14 @@ defmodule Module.Types.Descr do defp domain_key_to_descr(:list), do: @list_top defp map_descr(tag, pairs) do - {fields, domains, dynamic?} = map_descr_pairs(pairs, [], %{}, false) + {fields, domains, dynamic?} = map_descr_pairs(pairs, [], @fields_new, false) map_new = - if domains != %{} do + if not is_fields_empty(domains) do domains = if tag == :open do value = term_or_optional() - Enum.reduce(@domain_key_types, domains, &Map.put_new(&2, &1, value)) + fields_put_all_new(domains, @domain_key_types, value) else domains end @@ -2673,11 +2675,23 @@ defmodule Module.Types.Descr do end defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do - Enum.reduce(domain_keys, domain, fn key, acc when is_atom(key) -> - Map.update(acc, key, if_set(value), &union(&1, value)) - end) + map_put_domain(domain, :lists.usort(domain_keys), if_set(value), value) end + defp map_put_domain([{k1, v1} | t1], [k2 | _] = keys, initial, value) when k1 < k2 do + [{k1, v1} | map_put_domain(t1, keys, initial, value)] + end + + defp map_put_domain([{k1, v1} | t1], [k1 | keys], _initial, value) do + [{k1, union(v1, value)} | map_put_domain(t1, keys, if_set(value), value)] + end + + defp map_put_domain(domain, [k2 | keys], initial, value) do + [{k2, initial} | map_put_domain(domain, keys, initial, value)] + end + + defp map_put_domain(domain, [], _initial, _value), do: domain + defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do case is_atom(key) do true -> map_descr_pairs(rest, [{key, :term} | fields], domain, dynamic?) @@ -2708,7 +2722,7 @@ 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(map = %{}), do: Map.get(map, :atom, 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. @@ -2716,8 +2730,8 @@ defmodule Module.Types.Descr do 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) do - remove_optional(Map.get(domain, key, none())) + defp map_domain_tag_to_type(domain, key) when is_list(domain) do + remove_optional(fields_get(domain, key, none())) end defp map_domain_tag_to_type(domain, _key) do @@ -3071,29 +3085,34 @@ defmodule Module.Types.Descr do defp map_domain_intersection(:open, tag_or_domains), do: tag_or_domains defp map_domain_intersection(tag_or_domains, :open), do: tag_or_domains - defp map_domain_intersection(domains1 = %{}, domains2 = %{}) do - new_domains = - for {domain_key, type1} <- domains1, reduce: %{} do - acc_domains -> - case domains2 do - %{^domain_key => type2} -> - inter = intersection(type1, type2) - - if empty_or_optional?(inter) do - acc_domains - else - Map.put(acc_domains, domain_key, inter) - end - - _ -> - acc_domains - end - end - + defp map_domain_intersection(domains1, domains2) do # If the explicit domains are empty, use simple atom tags - if map_size(new_domains) == 0, do: :closed, else: new_domains + case map_domain_intersection_fields(domains1, domains2) do + [] -> :closed + new_domains -> new_domains + end end + defp map_domain_intersection_fields([{k1, _} | t1], [{k2, _} | _] = l2) when k1 < k2 do + map_domain_intersection_fields(t1, l2) + end + + defp map_domain_intersection_fields([{k1, _} | _] = l1, [{k2, _} | t2]) when k1 > k2 do + map_domain_intersection_fields(l1, t2) + end + + defp map_domain_intersection_fields([{k, type1} | t1], [{_, type2} | t2]) do + inter = intersection(type1, type2) + + if empty_or_optional?(inter) do + map_domain_intersection_fields(t1, t2) + else + [{k, inter} | map_domain_intersection_fields(t1, t2)] + end + end + + defp map_domain_intersection_fields(_, _), do: [] + defp map_literal_intersection_open_closed([{k1, v1} | t1], [{k2, _} | _] = l2) when k1 < k2 do # If the type in the open map is optional, we continue case v1 do @@ -3423,8 +3442,8 @@ defmodule Module.Types.Descr do # which is filtered by `map_bdd_to_dnf_*/1`. throw(:open) - domains = %{} -> - Enum.reduce(domains, acc, fn {domain_key, value}, acc -> + domains when is_list(domains) -> + fields_fold(domains, acc, fn domain_key, value, acc -> value = remove_optional(value) if empty?(value) do @@ -3862,16 +3881,18 @@ defmodule Module.Types.Descr do :open :closed -> - Map.from_keys(domain_keys, if_set(type_fun.(true, none()))) + fields_from_keys(domain_keys, if_set(type_fun.(true, none()))) - domains = %{} -> + # Note: domain_keys may contain duplicates, so we cannot + # do a side-by-side traversal here. + domains when is_list(domains) -> Enum.reduce(domain_keys, domains, fn domain_key, acc -> - case acc do - %{^domain_key => value} -> - %{acc | domain_key => union(value, type_fun.(true, remove_optional(value)))} + case fields_find(domain_key, acc) do + {:ok, value} -> + fields_store(domain_key, union(value, type_fun.(true, remove_optional(value))), acc) - %{} -> - Map.put(acc, domain_key, if_set(type_fun.(true, none()))) + :error -> + fields_store(domain_key, if_set(type_fun.(true, none())), acc) end end) end @@ -4303,25 +4324,27 @@ defmodule Module.Types.Descr do # An open map is a subtype iff the negative domains are all present as term_or_optional() defp map_check_domain_keys?(:open, neg_domains) do - map_size(neg_domains) == length(@domain_key_types) and - Enum.all?(neg_domains, fn {_domain_key, type} -> subtype?(term_or_optional(), type) end) + fields_size(neg_domains) == length(@domain_key_types) and + Enum.all?(fields_to_list(neg_domains), fn {_domain_key, type} -> + subtype?(term_or_optional(), type) + end) end # A positive domains is smaller than a closed map iff all its keys are empty or optional defp map_check_domain_keys?(pos_domains, :closed) do - Enum.all?(pos_domains, fn {_domain_key, type} -> empty_or_optional?(type) end) + Enum.all?(fields_to_list(pos_domains), fn {_domain_key, type} -> empty_or_optional?(type) end) end # Component-wise comparison of domains defp map_check_domain_keys?(pos_domains, neg_domains) do - Enum.all?(pos_domains, fn {domain_key, type} -> - subtype?(type, Map.get(neg_domains, domain_key, not_set())) + Enum.all?(fields_to_list(pos_domains), fn {domain_key, type} -> + subtype?(type, fields_get(neg_domains, domain_key, not_set())) end) end # Pop a domain type, already removing non optional. - defp map_pop_domain_bdd(domains = %{}, fields, domain_key) do - case :maps.take(domain_key, domains) do + 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)} :error -> {false, none(), map_new(domains, fields)} end @@ -4551,26 +4574,26 @@ defmodule Module.Types.Descr do end end - def map_literal_to_quoted({:closed, empty}, _opts) when is_fields_empty(empty) do + defp map_literal_to_quoted({:closed, empty}, _opts) when is_fields_empty(empty) do {:empty_map, [], []} end - def map_literal_to_quoted({:open, empty}, _opts) when is_fields_empty(empty) do + defp map_literal_to_quoted({:open, empty}, _opts) when is_fields_empty(empty) do {:map, [], []} end - def map_literal_to_quoted({domains = %{}, empty}, _opts) - when map_size(domains) == 0 and is_fields_empty(empty) do + defp map_literal_to_quoted({domains, empty}, _opts) + when is_fields_empty(domains) and is_fields_empty(empty) do {:empty_map, [], []} end - def map_literal_to_quoted({:open, [{:__struct__, @not_atom_or_optional}]}, _opts) do + defp map_literal_to_quoted({:open, [{:__struct__, @not_atom_or_optional}]}, _opts) do {:non_struct_map, [], []} end - def map_literal_to_quoted({domains = %{}, fields}, opts) do + defp map_literal_to_quoted({domains, fields}, opts) when is_list(domains) do domain_fields = - for {domain_key, value_type} <- domains do + for {domain_key, value_type} <- fields_to_list(domains) do non_optional = remove_optional_static(value_type) value_quoted = @@ -4587,7 +4610,7 @@ defmodule Module.Types.Descr do {:%{}, [], domain_fields ++ regular_fields_quoted} end - def map_literal_to_quoted({tag, fields}, opts) do + defp map_literal_to_quoted({tag, fields}, opts) do case tag do :closed -> with {:ok, struct_descr} <- fields_find(:__struct__, fields), @@ -4655,12 +4678,13 @@ defmodule Module.Types.Descr do ## Map fields helpers # - # Map fields are stored as orddicts (sorted key-value lists). + # Map fields and domains are stored as orddicts (sorted key-value lists). # These helpers wrap :orddict operations so the representation # can be changed without modifying every call site. @compile {:inline, fields_from_reverse_list: 1, + fields_from_keys: 2, fields_to_list: 1, fields_fold: 3, fields_keys: 1, @@ -4674,13 +4698,18 @@ defmodule Module.Types.Descr do fields_map: 2} defp fields_from_reverse_list(list), do: :lists.ukeysort(1, list) + defp fields_from_keys(keys, value), do: Enum.map(:lists.usort(keys), &{&1, value}) defp fields_to_list(fields), do: fields defp fields_fold(fields, acc, fun), do: :orddict.fold(fun, acc, fields) - defp fields_keys(fields), do: :orddict.fetch_keys(fields) defp fields_store(key, value, fields), do: :orddict.store(key, value, fields) defp fields_find(key, fields), do: :orddict.find(key, fields) defp fields_take(key, fields), do: :orddict.take(key, fields) + defp fields_fetch!(key, fields), do: :orddict.fetch(key, fields) + defp fields_is_key(key, fields), do: :orddict.is_key(key, fields) + + defp fields_merge(fun, fields1, fields2), do: :orddict.merge(fun, fields1, fields2) + defp fields_map(fun, fields), do: :orddict.map(fun, fields) defp fields_get(fields, key, default) do case :orddict.find(key, fields) do @@ -4689,10 +4718,20 @@ defmodule Module.Types.Descr do end end - defp fields_fetch!(key, fields), do: :orddict.fetch(key, fields) - defp fields_is_key(key, fields), do: :orddict.is_key(key, fields) - defp fields_merge(fun, fields1, fields2), do: :orddict.merge(fun, fields1, fields2) - defp fields_map(fun, fields), do: :orddict.map(fun, fields) + defp fields_put_all_new(fields, [], _value), do: fields + defp fields_put_all_new([], keys, value), do: Enum.map(keys, &{&1, value}) + + defp fields_put_all_new([{k1, _} = h | t1], [k2 | _] = keys, value) when k1 < k2 do + [h | fields_put_all_new(t1, keys, value)] + end + + defp fields_put_all_new([{k1, _} | _] = fields, [k2 | keys], value) when k1 > k2 do + [{k2, value} | fields_put_all_new(fields, keys, value)] + end + + defp fields_put_all_new([h | t1], [_ | keys], value) do + [h | fields_put_all_new(t1, keys, value)] + end defp fields_merge_with_defaults([{k1, v1} | rest1] = f1, d1, [{k2, v2} | rest2] = f2, d2, fun) do cond do diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 28e3eda104..5251556a57 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_v6. + elixir_checker_v7. %% debug_info callback diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index ed62906e77..8334928859 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -1793,7 +1793,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_v6, map} = :erlang.binary_to_term(chunk) + assert {:elixir_checker_v7, 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 c8abb4ecb6..a6706d11f8 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_v6, contents} = :erlang.binary_to_term(check_bin) + assert {:elixir_checker_v7, contents} = :erlang.binary_to_term(check_bin) Map.new(contents.exports) end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 5fc952d0c8..20843e9903 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 32 + @manifest_vsn 33 @checkpoint_vsn 4 import Record