Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the rendered type is not precise here. The type is purely dynamic with upper bound (term() -> atom()) \ (integer() -> none()) and empty static part. The pretty printer drops arrow negations which, if I understand it, is by design


# (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())"
Expand Down
Loading