diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 90e49b3772..571a391ecb 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1929,25 +1929,33 @@ defmodule Module.Types.Descr do # representation. The goal here is to do the opposite of fun_descr # and put static and dynamic parts back together to improve # pretty printing. - defp fun_denormalize(%{fun: {:union, static_repr}}, %{fun: {:union, dynamic_repr}}, opts) do - # Denormalize each arity - for {arity, static_bdd} <- static_repr, - {^arity, dynamic_bdd} <- dynamic_repr, - reduce: {static_repr, dynamic_repr, []} do - {statics, dynamics, acc} -> - with {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do - {Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]} - else - _ -> {statics, dynamics, acc} - end - end + defp fun_denormalize( + %{fun: {:union, static_repr}} = static, + %{fun: {:union, dynamic_repr}} = dynamic, + opts + ) do + {static_repr, dynamic_repr, acc} = + Enum.reduce(static_repr, {static_repr, dynamic_repr, []}, fn + {arity, static_bdd}, {statics, dynamics, acc} -> + with %{^arity => dynamic_bdd} <- dynamics, + {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do + {Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]} + else + _ -> {statics, dynamics, acc} + end + end) + + {fun_replace_arities(static, static_repr), fun_replace_arities(dynamic, dynamic_repr), acc} end # If not unions of functions, do not try to denormalize. - defp fun_denormalize(static_repr, dynamic_repr, _opts) do - {static_repr, dynamic_repr, []} + defp fun_denormalize(static, dynamic, _opts) do + {static, dynamic, []} end + defp fun_replace_arities(descr, arities) when arities == %{}, do: Map.delete(descr, :fun) + defp fun_replace_arities(descr, arities), do: %{descr | fun: {:union, arities}} + defp fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do static_pos = fun_bdd_to_pos_dnf(arity, static_bdd) dynamic_pos = fun_bdd_to_pos_dnf(arity, dynamic_bdd) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 78c8f252eb..cfad46c819 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -3689,6 +3689,34 @@ defmodule Module.Types.DescrTest do """ end + test "fun union of static and dynamic keeps non-fun components" do + # Denormalizing a static/dynamic fun union must not drop the other parts of + # the type, nor crash when an arity fails to denormalize. + + # (a) must not raise when denormalization fails for an arity + assert opt_difference(fun([term()], atom()), fun([integer()], dynamic(atom()))) + |> to_quoted_string() == + "dynamic((term() -> atom()) and (integer() -> atom()))" + + # (b) non-fun components must be preserved alongside the denormalized fun + assert opt_union( + integer(), + opt_union(fun([integer()], atom()), dynamic(fun([integer()], atom()))) + ) + |> to_quoted_string() == "(integer() -> atom()) or integer()" + + # several components, with a leftover non-denormalized dynamic arity + assert opt_union( + atom([:tag]), + opt_union( + fun([integer()], atom()), + dynamic(opt_union(fun([integer()], atom()), fun([integer(), integer()], atom()))) + ) + ) + |> to_quoted_string() == + "dynamic((integer(), integer() -> atom())) or :tag or (integer() -> atom())" + end + test "fun (negation)" do assert fun([integer()], atom()) |> opt_negation() |> to_quoted_string() == "not (integer() -> atom())"