Skip to content

Commit 5125ac8

Browse files
committed
Add denormalization when pretty printing functions
1 parent 207e51b commit 5125ac8

File tree

2 files changed

+178
-26
lines changed

2 files changed

+178
-26
lines changed

lib/elixir/lib/module/types/descr.ex

Lines changed: 147 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -527,28 +527,34 @@ defmodule Module.Types.Descr do
527527
{:term, [], []}
528528
else
529529
# Dynamic always come first for visibility
530-
{dynamic, descr} =
530+
{dynamic, static} =
531531
case :maps.take(:dynamic, descr) do
532-
:error -> {[], descr}
533-
{:term, descr} -> {to_quoted(:dynamic, :term, opts), descr}
534-
{dynamic, descr} -> {to_quoted(:dynamic, difference(dynamic, descr), opts), descr}
532+
:error -> {%{}, descr}
533+
{:term, static} -> {:term, static}
534+
{dynamic, static} -> {difference(dynamic, static), static}
535535
end
536536

537+
{static, dynamic, extra} = fun_denormalize(static, dynamic, opts)
538+
537539
# Merge empty list and list together if they both exist
538-
{extra, descr} =
539-
case descr do
540+
{extra, static} =
541+
case static do
540542
%{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
541-
descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list)
542-
{list_to_quoted(list, true, opts), descr}
543+
static =
544+
static
545+
|> Map.delete(:list)
546+
|> Map.replace!(:bitmap, bitmap - @bit_empty_list)
547+
548+
{list_to_quoted(list, true, opts) ++ extra, static}
543549

544550
%{} ->
545-
{[], descr}
551+
{extra, static}
546552
end
547553

548554
unions =
549-
dynamic ++
555+
to_quoted(:dynamic, dynamic, opts) ++
550556
Enum.sort(
551-
extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end)
557+
extra ++ Enum.flat_map(static, fn {key, value} -> to_quoted(key, value, opts) end)
552558
)
553559

554560
case unions do
@@ -564,7 +570,7 @@ defmodule Module.Types.Descr do
564570
defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
565571
defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, false, opts)
566572
defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)
567-
defp to_quoted(:fun, dnf, opts), do: fun_to_quoted(dnf, opts)
573+
defp to_quoted(:fun, bdd, opts), do: fun_to_quoted(bdd, opts)
568574

569575
@doc """
570576
Converts a descr to its quoted string representation.
@@ -1488,21 +1494,140 @@ defmodule Module.Types.Descr do
14881494
end
14891495
end
14901496

1491-
# Converts a function BDD (Binary Decision Diagram) to its quoted representation.
1492-
defp fun_to_quoted(bdd, opts) do
1493-
arrows = fun_get(bdd)
1497+
# Converts the static and dynamic parts of descr to its quoted
1498+
# representation. The goal here is to the opposite of fun_descr
1499+
# and put static and dynamic parts back together to improve
1500+
# pretty printing.
1501+
defp fun_denormalize(%{fun: static_bdd} = static, %{fun: dynamic_bdd} = dynamic, opts) do
1502+
static_pos = fun_get_pos(static_bdd)
1503+
dynamic_pos = fun_get_pos(dynamic_bdd)
1504+
1505+
if static_pos != [] and dynamic_pos != [] do
1506+
{dynamic_pos, static_pos} = fun_denormalize_pos(dynamic_pos, static_pos)
14941507

1495-
for {positives, negatives} <- arrows, not fun_empty?(positives, negatives) do
1496-
fun_intersection_to_quoted(positives, opts)
1508+
quoted =
1509+
if dynamic_pos == [] do
1510+
fun_pos_to_quoted(static_pos, opts)
1511+
else
1512+
{:or, [],
1513+
[
1514+
{:dynamic, [], [fun_pos_to_quoted(dynamic_pos, opts)]},
1515+
fun_pos_to_quoted(static_pos, opts)
1516+
]}
1517+
end
1518+
1519+
{Map.delete(static, :fun), Map.delete(dynamic, :fun), [quoted]}
1520+
else
1521+
{static, dynamic, []}
14971522
end
1498-
|> case do
1523+
end
1524+
1525+
defp fun_denormalize(static, dynamic, _opts) do
1526+
{static, dynamic, []}
1527+
end
1528+
1529+
defp fun_denormalize_pos(dynamic_unions, static_unions) do
1530+
Enum.reduce(dynamic_unions, {[], static_unions}, fn
1531+
# Handle fun() types accordingly
1532+
[], {dynamic_unions, static_unions} ->
1533+
{[[] | dynamic_unions], static_unions}
1534+
1535+
dynamic_intersections, {dynamic_unions, static_unions} ->
1536+
{dynamic_intersections, static_unions} =
1537+
Enum.reduce(dynamic_intersections, {[], static_unions}, fn
1538+
{args, return}, {acc, static_unions} ->
1539+
case fun_denormalize_arrow(args, return, static_unions) do
1540+
{:ok, static_unions} -> {acc, static_unions}
1541+
:error -> {[{args, return} | acc], static_unions}
1542+
end
1543+
end)
1544+
1545+
if dynamic_intersections == [] do
1546+
{dynamic_unions, static_unions}
1547+
else
1548+
{[dynamic_intersections | dynamic_unions], static_unions}
1549+
end
1550+
end)
1551+
end
1552+
1553+
defp fun_denormalize_arrow(dynamic_args, dynamic_return, static_unions) do
1554+
pivot(static_unions, [], fn static_intersections ->
1555+
pivot(static_intersections, [], fn {static_args, static_return} ->
1556+
if subtype?(static_return, dynamic_return) and args_subtype?(dynamic_args, static_args) do
1557+
args =
1558+
Enum.zip_with(static_args, dynamic_args, fn static_arg, dynamic_arg ->
1559+
union(dynamic(difference(static_arg, dynamic_arg)), dynamic_arg)
1560+
end)
1561+
1562+
return = union(dynamic(difference(dynamic_return, static_return)), static_return)
1563+
{:ok, {args, return}}
1564+
else
1565+
:error
1566+
end
1567+
end)
1568+
end)
1569+
end
1570+
1571+
defp arrow_subtype?(left_args, left_return, right_args, right_return) do
1572+
subtype?(right_return, left_return) and args_subtype?(left_args, right_args)
1573+
end
1574+
1575+
defp args_subtype?(left_args, right_args) do
1576+
Enum.zip_reduce(left_args, right_args, true, fn left, right, acc ->
1577+
acc and subtype?(left, right)
1578+
end)
1579+
end
1580+
1581+
defp pivot([head | tail], acc, fun) do
1582+
case fun.(head) do
1583+
{:ok, value} -> {:ok, acc ++ [value | tail]}
1584+
:error -> pivot(tail, [head | acc], fun)
1585+
end
1586+
end
1587+
1588+
defp pivot([], _acc, _fun), do: :error
1589+
1590+
# Converts a function BDD (Binary Decision Diagram) to its quoted representation
1591+
defp fun_to_quoted(bdd, opts) do
1592+
case fun_get_pos(bdd) do
14991593
[] -> []
1500-
multiple -> [Enum.reduce(multiple, &{:or, [], [&2, &1]})]
1594+
pos -> [fun_pos_to_quoted(pos, opts)]
15011595
end
15021596
end
15031597

1598+
defp fun_get_pos(bdd) do
1599+
for {pos, negs} <- fun_get(bdd), not fun_empty?(pos, negs) do
1600+
fun_filter_subset(pos, [])
1601+
end
1602+
end
1603+
1604+
# If one arrow is the subset of another arrow in the intersection,
1605+
# we just remove it.
1606+
defp fun_filter_subset([], acc), do: acc
1607+
1608+
defp fun_filter_subset([{args, return} | tail], acc) do
1609+
if Enum.any?(tail, fn {other_args, other_return} ->
1610+
arrow_subtype?(other_args, other_return, args, return)
1611+
end) or
1612+
Enum.any?(acc, fn {other_args, other_return} ->
1613+
arrow_subtype?(other_args, other_return, args, return)
1614+
end) do
1615+
fun_filter_subset(tail, acc)
1616+
else
1617+
fun_filter_subset(tail, [{args, return} | acc])
1618+
end
1619+
end
1620+
1621+
defp fun_pos_to_quoted([_ | _] = pos, opts) do
1622+
pos
1623+
|> Enum.sort()
1624+
|> Enum.map(&fun_intersection_to_quoted(&1, opts))
1625+
|> Enum.reduce(&{:or, [], [&2, &1]})
1626+
end
1627+
15041628
defp fun_intersection_to_quoted(intersection, opts) do
15051629
intersection
1630+
|> Enum.sort()
15061631
|> Enum.map(fn {args, ret} ->
15071632
{:__block__, [],
15081633
[[{:->, [], [Enum.map(args, &to_quoted(&1, opts)), to_quoted(ret, opts)]}]]}
@@ -1904,6 +2029,9 @@ defmodule Module.Types.Descr do
19042029

19052030
defp dynamic_to_quoted(descr, opts) do
19062031
cond do
2032+
descr == %{} ->
2033+
[]
2034+
19072035
term_type?(descr) ->
19082036
[{:dynamic, [], []}]
19092037

lib/elixir/test/elixir/module/types/descr_test.exs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,26 +1803,50 @@ defmodule Module.Types.DescrTest do
18031803
assert fun() |> to_quoted_string() == "fun()"
18041804
assert none_fun(1) |> to_quoted_string() == "(none() -> term())"
18051805

1806-
assert fun([dynamic(integer())], float()) |> to_quoted_string() ==
1807-
"dynamic((none() -> float())) or (integer() -> float())"
1808-
1809-
assert fun([integer(), float()], dynamic()) |> to_quoted_string() ==
1810-
"dynamic((integer(), float() -> term())) or (integer(), float() -> none())"
1811-
18121806
assert fun([integer(), float()], boolean()) |> to_quoted_string() ==
18131807
"(integer(), float() -> boolean())"
18141808

18151809
assert fun([integer()], boolean())
18161810
|> union(fun([float()], boolean()))
18171811
|> to_quoted_string() ==
1818-
"(float() -> boolean()) or (integer() -> boolean())"
1812+
"(integer() -> boolean()) or (float() -> boolean())"
18191813

18201814
assert fun([integer()], boolean())
18211815
|> intersection(fun([float()], boolean()))
18221816
|> to_quoted_string() ==
18231817
"(integer() -> boolean()) and (float() -> boolean())"
18241818
end
18251819

1820+
test "function with dynamic signatures" do
1821+
assert fun([dynamic(atom())], float()) |> to_quoted_string() ==
1822+
"(dynamic(atom()) -> float())"
1823+
1824+
assert fun([integer(), float()], dynamic(atom())) |> to_quoted_string() ==
1825+
"(integer(), float() -> dynamic(atom()))"
1826+
1827+
domain_part = fun([dynamic(atom()) |> union(integer()), binary()], float())
1828+
1829+
assert domain_part |> to_quoted_string() ==
1830+
"(dynamic(atom()) or integer(), binary() -> float())"
1831+
1832+
codomain_part = fun([pid(), float()], dynamic(atom()) |> union(integer()))
1833+
1834+
assert codomain_part |> to_quoted_string() ==
1835+
"(pid(), float() -> dynamic(atom()) or integer())"
1836+
1837+
assert union(domain_part, codomain_part) |> to_quoted_string() ==
1838+
"""
1839+
(dynamic(atom()) or integer(), binary() -> float()) or
1840+
(pid(), float() -> dynamic(atom()) or integer())\
1841+
"""
1842+
1843+
assert intersection(domain_part, codomain_part) |> to_quoted_string() ==
1844+
"""
1845+
(dynamic(atom()) or integer(), binary() -> float()) and
1846+
(pid(), float() -> dynamic(atom()) or integer())\
1847+
"""
1848+
end
1849+
18261850
test "map" do
18271851
assert empty_map() |> to_quoted_string() == "empty_map()"
18281852
assert open_map() |> to_quoted_string() == "map()"

0 commit comments

Comments
 (0)