Skip to content

Commit 8b733ea

Browse files
committed
Add denormalization when pretty printing functions
1 parent 207e51b commit 8b733ea

File tree

2 files changed

+157
-26
lines changed

2 files changed

+157
-26
lines changed

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

Lines changed: 124 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,117 @@ 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)
14941504

1495-
for {positives, negatives} <- arrows, not fun_empty?(positives, negatives) do
1496-
fun_intersection_to_quoted(positives, opts)
1505+
if static_pos != [] and dynamic_pos != [] do
1506+
{dynamic_pos, static_pos} = fun_denormalize_pos(dynamic_pos, static_pos)
1507+
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 args_subtype?(left_args, right_args) do
1572+
Enum.zip_reduce(left_args, right_args, true, fn left, right, acc ->
1573+
acc and subtype?(left, right)
1574+
end)
1575+
end
1576+
1577+
defp pivot([head | tail], acc, fun) do
1578+
case fun.(head) do
1579+
{:ok, value} -> {:ok, acc ++ [value | tail]}
1580+
:error -> pivot(tail, [head | acc], fun)
1581+
end
1582+
end
1583+
1584+
defp pivot([], _acc, _fun), do: :error
1585+
1586+
# Converts a function BDD (Binary Decision Diagram) to its quoted representation
1587+
defp fun_to_quoted(bdd, opts) do
1588+
case fun_get_pos(bdd) do
14991589
[] -> []
1500-
multiple -> [Enum.reduce(multiple, &{:or, [], [&2, &1]})]
1590+
pos -> [fun_pos_to_quoted(pos, opts)]
15011591
end
15021592
end
15031593

1594+
defp fun_get_pos(bdd) do
1595+
for {pos, negs} <- fun_get(bdd), not fun_empty?(pos, negs), do: pos
1596+
end
1597+
1598+
defp fun_pos_to_quoted([_ | _] = pos, opts) do
1599+
pos
1600+
|> Enum.sort()
1601+
|> Enum.map(&fun_intersection_to_quoted(&1, opts))
1602+
|> Enum.reduce(&{:or, [], [&2, &1]})
1603+
end
1604+
15041605
defp fun_intersection_to_quoted(intersection, opts) do
15051606
intersection
1607+
|> Enum.sort()
15061608
|> Enum.map(fn {args, ret} ->
15071609
{:__block__, [],
15081610
[[{:->, [], [Enum.map(args, &to_quoted(&1, opts)), to_quoted(ret, opts)]}]]}
@@ -1904,6 +2006,9 @@ defmodule Module.Types.Descr do
19042006

19052007
defp dynamic_to_quoted(descr, opts) do
19062008
cond do
2009+
descr == %{} ->
2010+
[]
2011+
19072012
term_type?(descr) ->
19082013
[{:dynamic, [], []}]
19092014

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,26 +1803,52 @@ 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+
# We cannot optimize across intersections
1844+
assert intersection(domain_part, codomain_part) |> to_quoted_string() ==
1845+
"""
1846+
dynamic((pid(), float() -> integer())) or
1847+
((dynamic(atom()) or integer(), binary() -> float()) and
1848+
(pid(), float() -> dynamic(atom()) or integer()))\
1849+
"""
1850+
end
1851+
18261852
test "map" do
18271853
assert empty_map() |> to_quoted_string() == "empty_map()"
18281854
assert open_map() |> to_quoted_string() == "map()"

0 commit comments

Comments
 (0)