Skip to content

Commit 0fc582f

Browse files
committed
Enhance list type handling and add list_proper? function
- Refactor list_bdd_to_pos_dnf to improve handling of negations and last types. - Introduce list_proper? function to check if a list type is a proper list. - Update list_hd and list_tl to fix wrong cases.
1 parent 34004a2 commit 0fc582f

File tree

2 files changed

+152
-68
lines changed

2 files changed

+152
-68
lines changed

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

Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,13 +1795,12 @@ defmodule Module.Types.Descr do
17951795
#
17961796
# A negation only matters when the negated list type is a supertype of the
17971797
# corresponding positive list type; in that case we subtract the negated
1798-
# variant from the positive one.
1798+
# variant from the positive one. This is done in list_bdd_to_pos_dnf/1.
17991799
{list_type, last_type} =
18001800
list_bdd_to_pos_dnf(bdd)
18011801
|> Enum.reduce({list_type, last_type_no_list}, fn
1802-
{head, tail}, {acc_head, acc_tail} ->
1803-
tail = list_tail_unfold(tail)
1804-
{union(head, acc_head), union(tail, acc_tail)}
1802+
{list, last, _negs}, {acc_list, acc_last} ->
1803+
{union(list, acc_list), union(last, acc_last)}
18051804
end)
18061805

18071806
list_new(list_type, last_type)
@@ -1829,40 +1828,50 @@ defmodule Module.Types.Descr do
18291828
end
18301829
end
18311830

1832-
# Takes all the lines from the root to the leaves finishing with a 1,
1833-
# and compile into tuples of positive and negative nodes. Positive nodes are
1834-
# those followed by a left path, negative nodes are those followed by a right path.
1835-
defp list_bdd_to_dnf(bdd) do
1836-
bdd_to_dnf(bdd)
1837-
|> Enum.reduce([], fn {pos_list, neg_list}, acc ->
1838-
case non_empty_list_literals_intersection(pos_list) do
1839-
:empty ->
1840-
acc
1841-
1842-
{list, last} ->
1843-
if list_line_empty?(list, last, neg_list),
1844-
do: acc,
1845-
else: [{{list, last}, neg_list} | acc]
1846-
end
1847-
end)
1848-
end
1849-
18501831
# Takes all the lines from the root to the leaves finishing with a 1,
18511832
# and compile into tuples of positive and negative nodes. Keep only the non-empty positives,
18521833
# and include the impact of negations on the last type.
18531834
# To see if a negation changes the last type or the list type, we just need to check
18541835
# if the negative list type is a supertype of the positive list type. In that case,
18551836
# we can remove the negative last type from the positive one.
1837+
18561838
# (If this subtracted type was empty, the whole type would be empty)
18571839
defp list_bdd_to_pos_dnf(bdd) do
18581840
bdd_to_dnf(bdd)
1859-
|> Enum.reduce([], fn {pos_list, neg_list}, acc ->
1841+
|> Enum.reduce([], fn {pos_list, negs}, acc ->
18601842
case non_empty_list_literals_intersection(pos_list) do
18611843
:empty ->
18621844
acc
18631845

18641846
{list, last} ->
1865-
if list_line_empty?(list, last, neg_list), do: acc, else: [{list, last} | acc]
1847+
if empty?(list) or empty?(last) do
1848+
acc
1849+
else
1850+
Enum.reduce_while(negs, {list_tail_unfold(last), []}, fn {neg_type, neg_last},
1851+
{acc_last, acc_negs} ->
1852+
if subtype?(list, neg_type) do
1853+
difference = difference(acc_last, neg_last)
1854+
if empty?(difference), do: {:halt, nil}, else: {:cont, {difference, acc_negs}}
1855+
else
1856+
{:cont, {acc_last, [{neg_type, neg_last} | acc_negs]}}
1857+
end
1858+
end)
1859+
|> case do
1860+
{:halt, nil} -> acc
1861+
{last, negs} -> [{list, last, Enum.reverse(negs)} | acc]
1862+
end
1863+
end
1864+
end
1865+
end)
1866+
end
1867+
1868+
# Compute the head of a list (faster because we discard computations on the last type).
1869+
defp list_bdd_to_hd(bdd) do
1870+
bdd_to_dnf(bdd)
1871+
|> Enum.reduce(none(), fn {pos_list, neg_list}, acc ->
1872+
case non_empty_list_literals_intersection(pos_list) do
1873+
:empty -> acc
1874+
{list, last} -> if list_line_empty?(list, last, neg_list), do: acc, else: union(acc, list)
18661875
end
18671876
end)
18681877
end
@@ -1885,6 +1894,47 @@ defmodule Module.Types.Descr do
18851894
defp list_top?(bdd_leaf(:term, :term)), do: true
18861895
defp list_top?(_), do: false
18871896

1897+
@doc """
1898+
Checks if a list type is a proper list (terminated by empty list).
1899+
"""
1900+
def list_proper?(:term), do: false
1901+
1902+
def list_proper?(%{} = descr) do
1903+
case :maps.take(:dynamic, descr) do
1904+
:error ->
1905+
list_proper_static?(descr)
1906+
1907+
{dynamic, static} ->
1908+
list_proper_static?(static) and (list_proper_static?(dynamic) or empty?(dynamic))
1909+
end
1910+
end
1911+
1912+
defp list_proper_static?(:term), do: false
1913+
1914+
defp list_proper_static?(%{} = descr) do
1915+
# A list is proper if it's either the empty list alone, or all non-empty
1916+
# list types have tails that are subtypes of empty list
1917+
case descr do
1918+
%{bitmap: bitmap, list: bdd} ->
1919+
(bitmap &&& @bit_empty_list) != 0 and empty?(Map.drop(descr, [:list, :bitmap])) and
1920+
list_bdd_proper?(bdd)
1921+
1922+
%{bitmap: bitmap} ->
1923+
(bitmap &&& @bit_empty_list) != 0 and empty?(Map.delete(descr, :bitmap))
1924+
1925+
%{list: bdd} ->
1926+
empty?(Map.delete(descr, :list)) and list_bdd_proper?(bdd)
1927+
1928+
%{} ->
1929+
false
1930+
end
1931+
end
1932+
1933+
defp list_bdd_proper?(bdd) do
1934+
list_bdd_to_pos_dnf(bdd)
1935+
|> Enum.all?(fn {_list, last, _negs} -> subtype?(last, @empty_list) end)
1936+
end
1937+
18881938
defp list_intersection(bdd_leaf(list1, last1), bdd_leaf(list2, last2)) do
18891939
try do
18901940
list = non_empty_intersection!(list1, list2)
@@ -1914,10 +1964,16 @@ defmodule Module.Types.Descr do
19141964
# The result may be larger than the initial bdd1, which is maintained in the accumulator.
19151965
defp list_difference(bdd_leaf(list1, last1) = bdd1, bdd_leaf(list2, last2) = bdd2) do
19161966
cond do
1917-
disjoint?(list1, list2) or disjoint?(last1, last2) -> bdd_leaf(list1, last1)
1918-
subtype?(list1, list2) and subtype?(last1, last2) -> :bdd_bot
1919-
equal?(list1, list2) -> bdd_leaf(list1, difference(last1, last2))
1920-
true -> bdd_difference(bdd1, bdd2)
1967+
disjoint?(list1, list2) or disjoint?(last1, last2) ->
1968+
bdd_leaf(list1, last1)
1969+
1970+
subtype?(list1, list2) ->
1971+
if subtype?(last1, last2),
1972+
do: :bdd_bot,
1973+
else: bdd_leaf(list1, difference(last1, last2))
1974+
1975+
true ->
1976+
bdd_difference(bdd1, bdd2)
19211977
end
19221978
end
19231979

@@ -1989,22 +2045,20 @@ defmodule Module.Types.Descr do
19892045

19902046
defp list_hd_static(:term), do: :term
19912047

1992-
defp list_hd_static(%{list: bdd}) do
1993-
list_bdd_to_pos_dnf(bdd)
1994-
|> Enum.reduce(none(), fn {list, _}, acc ->
1995-
union(list, acc)
1996-
end)
1997-
end
2048+
defp list_hd_static(%{list: bdd}), do: list_bdd_to_hd(bdd)
19982049

19992050
defp list_hd_static(%{}), do: none()
20002051

20012052
@doc """
2002-
Returns the tail of a list.
2053+
Computes the type of the tail of a non-empty list type.
20032054
2004-
Errors on a possibly empty list.
2005-
On a non empty list of integers, it returns a (possibly empty) list of integers.
2006-
On a non empty list of integers, with an atom tail element, it returns either an atom,
2007-
or a (possibly empty) list of integers with an atom tail element.
2055+
Returns `{dynamic?, type}` on success, where `dynamic?` indicates whether
2056+
the result contains a dynamic component. Returns `:badnonemptylist` if the
2057+
input type is not guaranteed to be a non-empty list.
2058+
2059+
For a `non_empty_list(t)`, the tail type is `list(t)` (possibly empty).
2060+
For an improper list `non_empty_list(t, s)`, the tail type is
2061+
`list(t, s) or s` (either the rest of the list or the terminator).
20082062
"""
20092063
def list_tl(:term), do: :badnonemptylist
20102064

@@ -2042,7 +2096,9 @@ defmodule Module.Types.Descr do
20422096
%{list: bdd}
20432097
end
20442098

2045-
list_bdd_to_pos_dnf(bdd) |> Enum.reduce(initial, fn {_, tail}, acc -> union(tail, acc) end)
2099+
list_bdd_to_pos_dnf(bdd)
2100+
|> Enum.reduce(none(), fn {_list, last, _negs}, acc -> union(acc, last) end)
2101+
|> union(initial)
20462102
end
20472103

20482104
defp list_tl_static(%{}), do: none()
@@ -2053,13 +2109,31 @@ defmodule Module.Types.Descr do
20532109
{unions, list_rendered?} =
20542110
dnf
20552111
|> Enum.reduce({[], false}, fn {list_type, last_type, negs}, {acc, list_rendered?} ->
2112+
# <<<<<<< HEAD
2113+
# if subtype?(last_type, @empty_list) do
2114+
# name = if empty?, do: :list, else: :non_empty_list
2115+
# {name, [to_quoted(list_type, opts)], empty?}
2116+
# else
2117+
# args = [to_quoted(list_type, opts), to_quoted(last_type, opts)]
2118+
# {:non_empty_list, args, list_rendered?}
2119+
# =======
20562120
{name, arguments, list_rendered?} =
2057-
if subtype?(last_type, @empty_list) do
2058-
name = if empty?, do: :list, else: :non_empty_list
2059-
{name, [to_quoted(list_type, opts)], empty?}
2060-
else
2061-
args = [to_quoted(list_type, opts), to_quoted(last_type, opts)]
2062-
{:non_empty_list, args, list_rendered?}
2121+
cond do
2122+
# list_type == term() and list_improper_static?(last_type) ->
2123+
# {:improper_list, [], list_rendered?}
2124+
2125+
subtype?(last_type, @empty_list) ->
2126+
name = if empty?, do: :list, else: :non_empty_list
2127+
{name, [to_quoted(list_type, opts)], empty?}
2128+
2129+
subtype?(@not_non_empty_list, last_type) ->
2130+
args = [to_quoted(list_type, opts), {:term, [], []}]
2131+
{:non_empty_list, args, list_rendered?}
2132+
2133+
true ->
2134+
args = [to_quoted(list_type, opts), to_quoted(last_type, opts)]
2135+
{:non_empty_list, args, list_rendered?}
2136+
# >>>>>>> 4e5179dd6 (Enhance list type handling and add list_proper? function)
20632137
end
20642138

20652139
acc =
@@ -2096,27 +2170,19 @@ defmodule Module.Types.Descr do
20962170
end
20972171

20982172
# Eliminate empty lists from the union, and redundant types (that are subtypes of others,
2099-
# or that can be merged with others).
2173+
# or that can be merged with others). List_bdd_to_pos_dnf already takes into account the
2174+
# impact of negations on the last type.
21002175
defp list_normalize(bdd) do
2101-
list_bdd_to_dnf(bdd)
2102-
|> Enum.reduce([], fn {{list, last}, negs}, acc ->
2103-
# First, try to eliminate the negations from the existing type.
2104-
{list, last, negs} =
2176+
list_bdd_to_pos_dnf(bdd)
2177+
|> Enum.reduce([], fn {list, last, negs}, acc ->
2178+
# Prune negations from those with empty intersections.
2179+
negs =
21052180
Enum.uniq(negs)
2106-
|> Enum.reduce({list, last, []}, fn {nlist, nlast}, {acc_list, acc_last, acc_negs} ->
2107-
last = list_tail_unfold(last)
2108-
new_list = intersection(list, nlist)
2109-
new_last = intersection(last, nlast)
2110-
2111-
cond do
2112-
# No intersection between the list and the negative
2113-
empty?(new_list) or empty?(new_last) -> {acc_list, acc_last, acc_negs}
2114-
subtype?(list, nlist) -> {acc_list, difference(acc_last, nlast), acc_negs}
2115-
true -> {acc_list, acc_last, [{nlist, nlast} | acc_negs]}
2116-
end
2181+
|> Enum.filter(fn {nlist, nlast} ->
2182+
not empty?(intersection(list, nlist)) and not empty?(intersection(last, nlast))
21172183
end)
21182184

2119-
add_to_list_normalize(acc, list, last, negs |> Enum.reverse())
2185+
add_to_list_normalize(acc, list, last, negs)
21202186
end)
21212187
end
21222188

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,7 @@ defmodule Module.Types.DescrTest do
12491249
assert list_hd(non_empty_list(term())) == {:ok, term()}
12501250
assert list_hd(non_empty_list(integer())) == {:ok, integer()}
12511251
assert list_hd(difference(list(number()), list(integer()))) == {:ok, number()}
1252+
assert list_hd(non_empty_list(atom(), float())) == {:ok, atom()}
12521253

12531254
assert list_hd(dynamic()) == {:ok, dynamic()}
12541255
assert list_hd(dynamic(list(integer()))) == {:ok, dynamic(integer())}
@@ -1267,6 +1268,17 @@ defmodule Module.Types.DescrTest do
12671268
assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {:ok, atom()}
12681269
end
12691270

1271+
test "list_proper?" do
1272+
assert list_proper?(term()) == false
1273+
assert list_proper?(none()) == false
1274+
assert list_proper?(empty_list()) == true
1275+
assert list_proper?(non_empty_list(integer())) == true
1276+
assert list_proper?(non_empty_list(integer(), atom())) == false
1277+
assert list_proper?(non_empty_list(integer(), term())) == false
1278+
assert list_proper?(non_empty_list(integer(), list(term()))) == true
1279+
assert list_proper?(list(integer()) |> union(list(integer(), integer()))) == false
1280+
end
1281+
12701282
test "list_tl" do
12711283
assert list_tl(none()) == :badnonemptylist
12721284
assert list_tl(term()) == :badnonemptylist
@@ -1284,11 +1296,9 @@ defmodule Module.Types.DescrTest do
12841296
# integers with an atom tail, or a (possibly empty) list of tuples with a float tail.
12851297
assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) ==
12861298
{:ok,
1287-
atom()
1288-
|> union(float())
1289-
|> union(
1290-
union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))
1291-
)}
1299+
union(atom(), float())
1300+
|> union(non_empty_list(integer(), atom()))
1301+
|> union(non_empty_list(tuple(), float()))}
12921302

12931303
assert list_tl(dynamic()) == {:ok, dynamic()}
12941304
assert list_tl(dynamic(list(integer()))) == {:ok, dynamic(list(integer()))}
@@ -2391,6 +2401,14 @@ defmodule Module.Types.DescrTest do
23912401
assert union(dynamic(list(integer(), float())), dynamic(list(integer(), pid())))
23922402
|> to_quoted_string() ==
23932403
"dynamic(empty_list() or non_empty_list(integer(), float() or pid()))"
2404+
2405+
list_with_tail =
2406+
non_empty_list(atom(), union(integer(), empty_list()))
2407+
|> difference(non_empty_list(atom([:ok]), integer()))
2408+
|> difference(non_empty_list(atom(), integer()))
2409+
2410+
# Check that simplifications occur.
2411+
assert to_quoted_string(list_with_tail) == "non_empty_list(atom())"
23942412
end
23952413

23962414
test "tuples" do

0 commit comments

Comments
 (0)