diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 9255c2299f..f8e3301879 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -224,12 +224,13 @@ defmodule Module.Types.Apply do {:erlang, :++, [ {[empty_list(), term()], dynamic(term())}, - {[non_empty_list(term()), term()], dynamic(non_empty_list(term(), term()))} + {[non_empty_list(term()), term()], + dynamic(non_empty_maybe_improper_list(term(), term()))} ]}, {:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]}, {:erlang, :andalso, [{[boolean(), term()], dynamic()}]}, {:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]}, - {:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]}, + {:erlang, :hd, [{[non_empty_maybe_improper_list(term(), term())], dynamic()}]}, {:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]}, {:erlang, :insert_element, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]}, @@ -239,7 +240,7 @@ defmodule Module.Types.Apply do {:erlang, :orelse, [{[boolean(), term()], dynamic()}]}, {:erlang, :send, [{[send_destination, term()], dynamic()}]}, {:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]}, - {:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]}, + {:erlang, :tl, [{[non_empty_maybe_improper_list(term(), term())], dynamic()}]}, {:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]} ] do [arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq() @@ -314,11 +315,11 @@ defmodule Module.Types.Apply do end def remote_domain(:erlang, :hd, [_list], expected, _meta, _stack, context) do - {:hd, [non_empty_list(expected, term())], context} + {:hd, [non_empty_maybe_improper_list(expected, term())], context} end def remote_domain(:erlang, :tl, [_list], _expected, _meta, _stack, context) do - {:tl, [non_empty_list(term(), term())], context} + {:tl, [non_empty_maybe_improper_list(term(), term())], context} end def remote_domain(:erlang, name, [_left, _right], _expected, _meta, stack, context) @@ -1151,7 +1152,7 @@ defmodule Module.Types.Apply do args_docs_to_quoted_string(converter.(docs)) end - @composite_types non_empty_list(term(), term()) + @composite_types non_empty_maybe_improper_list(term(), term()) |> union(tuple()) |> union(open_map()) |> union(fun()) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b4bd5dc257..f4235ceedc 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -43,7 +43,6 @@ defmodule Module.Types.Descr do } @empty_list %{bitmap: @bit_empty_list} @not_non_empty_list Map.delete(@term, :list) - @not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list) @empty_intersection [0, @none] @empty_difference [0, []] @@ -74,7 +73,8 @@ defmodule Module.Types.Descr do def float(), do: %{bitmap: @bit_float} def fun(), do: %{bitmap: @bit_fun} def list(type), do: list_descr(type, @empty_list, true) - def non_empty_list(type, tail \\ @empty_list), do: list_descr(type, tail, false) + def non_empty_list(type), do: list_descr(type, @empty_list, false) + def non_empty_maybe_improper_list(type, tail), do: list_descr(type, tail, false) def open_map(), do: %{map: @map_top} def open_map(pairs), do: map_descr(:open, pairs) def open_tuple(elements, _fallback \\ term()), do: tuple_descr(:open, elements) @@ -1078,27 +1078,26 @@ defmodule Module.Types.Descr do defp list_tl_static(%{}), do: none() - defp list_improper_static?(:term), do: false - defp list_improper_static?(%{bitmap: bitmap}) when (bitmap &&& @bit_empty_list) != 0, do: false - defp list_improper_static?(term), do: equal?(term, @not_list) + defp list_improper_term?(:term), do: true + defp list_improper_term?(term), do: equal?(term, @not_non_empty_list) defp list_to_quoted(dnf, empty?, opts) do dnf = list_normalize(dnf) - {unions, list_rendered?} = - Enum.reduce(dnf, {[], false}, fn {list_type, last_type, negs}, {acc, list_rendered?} -> - {name, arguments, list_rendered?} = - cond do - list_type == term() and list_improper_static?(last_type) -> - {:improper_list, [], list_rendered?} + unions = + Enum.reduce(dnf, [], fn {list_type, last_type, negs}, acc -> + {name, arguments} = + if subtype?(last_type, @empty_list) do + name = if empty?, do: :list, else: :non_empty_list + {name, [to_quoted(list_type, opts)]} + else + name = if empty?, do: :maybe_improper_list, else: :non_empty_maybe_improper_list - subtype?(last_type, @empty_list) -> - name = if empty?, do: :list, else: :non_empty_list - {name, [to_quoted(list_type, opts)], empty?} + # mark non_empty_maybe_improper_list(term(), term()) as such rather than: + # non_empty_maybe_improper_list(term(), atom() or binary() or float() or ...) + rendered_last_type = if list_improper_term?(last_type), do: :term, else: last_type - true -> - args = [to_quoted(list_type, opts), to_quoted(last_type, opts)] - {:non_empty_list, args, list_rendered?} + {name, [to_quoted(list_type, opts), to_quoted(rendered_last_type, opts)]} end acc = @@ -1124,10 +1123,10 @@ defmodule Module.Types.Descr do ) end - {acc, list_rendered?} + acc end) - if empty? and not list_rendered? do + if empty? and dnf == [] do [{:empty_list, [], []} | unions] else unions diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index cb56929ec3..66535e4341 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -109,7 +109,7 @@ defmodule Module.Types.Expr do of_expr(suffix, tl_type, expr, stack, context) end - {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} + {non_empty_maybe_improper_list(Enum.reduce(prefix, &union/2), suffix), context} end end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 270d29c2e9..646f6c28e5 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -132,7 +132,7 @@ defmodule Module.Types.Of do {Float, float()}, {Function, fun()}, {Integer, integer()}, - {List, union(empty_list(), non_empty_list(term(), term()))}, + {List, union(empty_list(), non_empty_maybe_improper_list(term(), term()))}, {Map, open_map(__struct__: if_set(negation(atom())))}, {Port, port()}, {PID, pid()}, diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 23ff918bd5..07536dde7c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -345,7 +345,7 @@ defmodule Module.Types.Pattern do def of_pattern_tree({:non_empty_list, [head | tail], suffix}, context) do tail |> Enum.reduce(of_pattern_tree(head, context), &union(of_pattern_tree(&1, context), &2)) - |> non_empty_list(of_pattern_tree(suffix, context)) + |> non_empty_maybe_improper_list(of_pattern_tree(suffix, context)) end def of_pattern_tree({:intersection, entries}, context) do @@ -639,7 +639,7 @@ defmodule Module.Types.Pattern do case {static, dynamic} do {static, []} when is_descr(suffix) -> - {non_empty_list(Enum.reduce(static, &union/2), suffix), context} + {non_empty_maybe_improper_list(Enum.reduce(static, &union/2), suffix), context} {[], dynamic} -> {{:non_empty_list, dynamic, suffix}, context} @@ -685,7 +685,7 @@ defmodule Module.Types.Pattern do Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2)) {suffix, context} = of_guard(suffix, term(), expr, stack, context) - {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} + {non_empty_maybe_improper_list(Enum.reduce(prefix, &union/2), suffix), context} end # {left, right} diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 90285845a7..fc6c278af4 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -44,7 +44,7 @@ defmodule Module.Types.DescrTest do float(), binary(), open_map(), - non_empty_list(term(), term()), + non_empty_maybe_improper_list(term(), term()), empty_list(), tuple(), fun(), @@ -435,7 +435,8 @@ defmodule Module.Types.DescrTest do refute empty?(difference(open_map(), empty_map())) end - defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) + defp list(elem_type, tail_type), + do: union(empty_list(), non_empty_maybe_improper_list(elem_type, tail_type)) test "list" do # Basic list type differences @@ -471,26 +472,32 @@ defmodule Module.Types.DescrTest do |> equal?(list(atom())) assert difference(list(integer(), float()), list(number(), integer())) - |> equal?(non_empty_list(integer(), difference(float(), integer()))) + |> equal?(non_empty_maybe_improper_list(integer(), difference(float(), integer()))) # Empty list with last element assert difference(empty_list(), list(integer(), atom())) == none() assert difference(list(integer(), atom()), empty_list()) == - non_empty_list(integer(), atom()) + non_empty_maybe_improper_list(integer(), atom()) # List with any type and specific last element assert difference(list(term(), term()), list(term(), integer())) |> equal?( - non_empty_list(term(), negation(union(integer(), non_empty_list(term(), term())))) + non_empty_maybe_improper_list( + term(), + negation(union(integer(), non_empty_maybe_improper_list(term(), term()))) + ) ) # Nested lists with last element assert difference(list(list(integer()), atom()), list(list(number()), boolean())) |> equal?( union( - non_empty_list(list(integer()), difference(atom(), boolean())), - non_empty_list(difference(list(integer()), list(number())), atom()) + non_empty_maybe_improper_list(list(integer()), difference(atom(), boolean())), + non_empty_maybe_improper_list( + difference(list(integer()), list(number())), + atom() + ) ) ) @@ -498,8 +505,11 @@ defmodule Module.Types.DescrTest do assert difference(list(integer(), union(atom(), binary())), list(number(), atom())) |> equal?( union( - non_empty_list(integer(), binary()), - non_empty_list(difference(integer(), number()), union(atom(), binary())) + non_empty_maybe_improper_list(integer(), binary()), + non_empty_maybe_improper_list( + difference(integer(), number()), + union(atom(), binary()) + ) ) ) @@ -511,7 +521,7 @@ defmodule Module.Types.DescrTest do # Difference with proper list assert difference(list(integer(), atom()), list(integer())) == - non_empty_list(integer(), atom()) + non_empty_maybe_improper_list(integer(), atom()) end end @@ -719,8 +729,10 @@ defmodule Module.Types.DescrTest do # If term() is in the tail, it means list(term()) is in the tail # and therefore any term can be returned from hd. - assert list_hd(non_empty_list(atom(), term())) == {false, term()} - assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {false, atom()} + assert list_hd(non_empty_maybe_improper_list(atom(), term())) == {false, term()} + + assert list_hd(non_empty_maybe_improper_list(atom(), negation(list(term(), term())))) == + {false, atom()} end test "list_tl" do @@ -732,18 +744,26 @@ defmodule Module.Types.DescrTest do assert list_tl(non_empty_list(integer())) == {false, list(integer())} - assert list_tl(non_empty_list(integer(), atom())) == - {false, union(atom(), non_empty_list(integer(), atom()))} + assert list_tl(non_empty_maybe_improper_list(integer(), atom())) == + {false, union(atom(), non_empty_maybe_improper_list(integer(), atom()))} # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. - assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == + assert list_tl( + union( + non_empty_maybe_improper_list(integer(), atom()), + non_empty_maybe_improper_list(tuple(), float()) + ) + ) == {false, atom() |> union(float()) |> union( - union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())) + union( + non_empty_maybe_improper_list(integer(), atom()), + non_empty_maybe_improper_list(tuple(), float()) + ) )} assert list_tl(dynamic()) == {true, dynamic()} @@ -1298,17 +1318,50 @@ defmodule Module.Types.DescrTest do |> to_quoted_string() == "non_empty_list(term()) and not (non_empty_list(atom()) or non_empty_list(integer()))" + assert list(integer()) |> union(list(atom())) |> to_quoted_string() == + "list(atom()) or list(integer())" + + assert list(integer()) + |> union(list(atom())) + |> difference(empty_list()) + |> to_quoted_string() == + "non_empty_list(atom()) or non_empty_list(integer())" + assert list(term(), integer()) |> to_quoted_string() == - "empty_list() or non_empty_list(term(), integer())" + "maybe_improper_list(term(), integer())" assert difference(list(term(), atom()), list(term(), boolean())) |> to_quoted_string() == - "non_empty_list(term(), atom() and not boolean())" + "non_empty_maybe_improper_list(term(), atom() and not boolean())" assert list(term(), term()) |> to_quoted_string() == - "empty_list() or non_empty_list(term(), term())" + "maybe_improper_list(term(), term())" - assert non_empty_list(term(), difference(term(), list(term()))) |> to_quoted_string() == - "improper_list()" + assert non_empty_maybe_improper_list(term(), term()) |> to_quoted_string() == + "non_empty_maybe_improper_list(term(), term())" + + # TODO this feels wrong, figure this out + # seems to be due to how list_descr discards negations, not an issue with to_quoted_string + assert non_empty_maybe_improper_list(term(), difference(term(), empty_list())) + |> to_quoted_string() == + "non_empty_maybe_improper_list(term(), term())" + + assert list(integer(), integer()) |> to_quoted_string() == + "maybe_improper_list(integer(), integer())" + + assert non_empty_maybe_improper_list(integer(), integer()) |> to_quoted_string() == + "non_empty_maybe_improper_list(integer(), integer())" + + assert union(non_empty_list(integer()), non_empty_maybe_improper_list(integer(), integer())) + |> to_quoted_string() == + "non_empty_maybe_improper_list(integer(), empty_list() or integer())" + + assert union(list(integer()), non_empty_maybe_improper_list(integer(), integer())) + |> to_quoted_string() == + "maybe_improper_list(integer(), empty_list() or integer())" + + assert union(list(term()), non_empty_maybe_improper_list(term(), term())) + |> to_quoted_string() == + "maybe_improper_list(term(), term())" # Test normalization @@ -1317,15 +1370,15 @@ defmodule Module.Types.DescrTest do # Merge subtypes assert union(list(float(), pid()), list(number(), pid())) |> to_quoted_string() == - "empty_list() or non_empty_list(float() or integer(), pid())" + "maybe_improper_list(float() or integer(), pid())" # Merge last element types assert union(list(atom([:ok]), integer()), list(atom([:ok]), float())) |> to_quoted_string() == - "empty_list() or non_empty_list(:ok, float() or integer())" + "maybe_improper_list(:ok, float() or integer())" assert union(dynamic(list(integer(), float())), dynamic(list(integer(), pid()))) |> to_quoted_string() == - "dynamic(empty_list() or non_empty_list(integer(), float() or pid()))" + "dynamic(maybe_improper_list(integer(), float() or pid()))" end test "tuples" do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 53b2373362..a01170fa36 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -36,13 +36,13 @@ defmodule Module.Types.ExprTest do describe "lists" do test "creating lists" do assert typecheck!([1, 2]) == non_empty_list(integer()) - assert typecheck!([1, 2 | 3]) == non_empty_list(integer(), integer()) + assert typecheck!([1, 2 | 3]) == non_empty_maybe_improper_list(integer(), integer()) assert typecheck!([1, 2 | [3, 4]]) == non_empty_list(integer()) assert typecheck!([:ok, 123]) == non_empty_list(union(atom([:ok]), integer())) - assert typecheck!([:ok | 123]) == non_empty_list(atom([:ok]), integer()) + assert typecheck!([:ok | 123]) == non_empty_maybe_improper_list(atom([:ok]), integer()) assert typecheck!([x], [:ok, x]) == dynamic(non_empty_list(term())) - assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term())) + assert typecheck!([x], [:ok | x]) == dynamic(non_empty_maybe_improper_list(term(), term())) end test "inference" do @@ -71,7 +71,7 @@ defmodule Module.Types.ExprTest do but expected one of: - non_empty_list(term(), term()) + non_empty_maybe_improper_list(term(), term()) """ assert typeerror!(hd(123)) |> strip_ansi() == @@ -86,7 +86,7 @@ defmodule Module.Types.ExprTest do but expected one of: - non_empty_list(term(), term()) + non_empty_maybe_improper_list(term(), term()) """ end @@ -94,7 +94,9 @@ defmodule Module.Types.ExprTest do assert typecheck!([x = [123, :foo]], tl(x)) == dynamic(list(union(atom([:foo]), integer()))) assert typecheck!([x = [123 | :foo]], tl(x)) == - dynamic(union(atom([:foo]), non_empty_list(integer(), atom([:foo])))) + dynamic( + union(atom([:foo]), non_empty_maybe_improper_list(integer(), atom([:foo]))) + ) assert typeerror!(tl([])) |> strip_ansi() == ~l""" @@ -108,7 +110,7 @@ defmodule Module.Types.ExprTest do but expected one of: - non_empty_list(term(), term()) + non_empty_maybe_improper_list(term(), term()) """ assert typeerror!(tl(123)) |> strip_ansi() == @@ -123,7 +125,7 @@ defmodule Module.Types.ExprTest do but expected one of: - non_empty_list(term(), term()) + non_empty_maybe_improper_list(term(), term()) """ end end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 4daf956d8c..4c40c7b465 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -208,7 +208,7 @@ defmodule Module.Types.IntegrationTest do assert itself_arg.(Itself.Integer) == dynamic(integer()) assert itself_arg.(Itself.List) == - dynamic(union(empty_list(), non_empty_list(term(), term()))) + dynamic(union(empty_list(), non_empty_maybe_improper_list(term(), term()))) assert itself_arg.(Itself.Map) == dynamic(open_map(__struct__: if_set(negation(atom())))) assert itself_arg.(Itself.Port) == dynamic(port()) @@ -487,7 +487,7 @@ defmodule Module.Types.IntegrationTest do dynamic( %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or %Version.Requirement{} - ) or atom() or binary() or empty_list() or float() or integer() or non_empty_list(term(), term()) + ) or atom() or binary() or float() or integer() or maybe_improper_list(term(), term()) where "data" was given the type: @@ -511,7 +511,7 @@ defmodule Module.Types.IntegrationTest do dynamic( %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or %Version.Requirement{} - ) or atom() or binary() or empty_list() or float() or integer() or non_empty_list(term(), term()) + ) or atom() or binary() or float() or integer() or maybe_improper_list(term(), term()) where "data" was given the type: @@ -551,7 +551,7 @@ defmodule Module.Types.IntegrationTest do dynamic( %Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{} or %Range{} or %Stream{} - ) or empty_list() or fun() or non_empty_list(term(), term()) or non_struct_map() + ) or fun() or maybe_improper_list(term(), term()) or non_struct_map() where "date" was given the type: @@ -580,7 +580,7 @@ defmodule Module.Types.IntegrationTest do but expected a type that implements the Collectable protocol, it must be one of: dynamic(%File.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{}) or binary() or - empty_list() or non_empty_list(term(), term()) or non_struct_map() + maybe_improper_list(term(), term()) or non_struct_map() hint: the :into option in for-comprehensions use the Collectable protocol to build its result. Either pass a valid data type or implement the protocol accordingly """ diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 626c4c9147..18585a6d9c 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -41,7 +41,7 @@ defmodule Module.Types.PatternTest do but expected one of: - non_empty_list(term(), term()) + non_empty_maybe_improper_list(term(), term()) where "name" was given the type: @@ -213,16 +213,16 @@ defmodule Module.Types.PatternTest do dynamic(non_empty_list(integer())) assert typecheck!([x = [1, 2, 3 | y], y = :foo], x) == - dynamic(non_empty_list(integer(), atom([:foo]))) + dynamic(non_empty_maybe_improper_list(integer(), atom([:foo]))) assert typecheck!([x = [1, 2, 3 | y], y = [1.0, 2.0, 3.0]], x) == dynamic(non_empty_list(union(integer(), float()))) assert typecheck!([x = [:ok | z]], {x, z}) == - dynamic(tuple([non_empty_list(term(), term()), term()])) + dynamic(tuple([non_empty_maybe_improper_list(term(), term()), term()])) assert typecheck!([x = [y | z]], {x, y, z}) == - dynamic(tuple([non_empty_list(term(), term()), term(), term()])) + dynamic(tuple([non_empty_maybe_improper_list(term(), term()), term(), term()])) end test "in patterns through ++" do @@ -232,7 +232,7 @@ defmodule Module.Types.PatternTest do dynamic(atom([:foo])) assert typecheck!([x = [1, 2, 3] ++ y, y = :foo], x) == - dynamic(non_empty_list(integer(), atom([:foo]))) + dynamic(non_empty_maybe_improper_list(integer(), atom([:foo]))) assert typecheck!([x = [1, 2, 3] ++ y, y = [1.0, 2.0, 3.0]], x) == dynamic(non_empty_list(union(integer(), float())))