diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 90e49b3772d..0400ad8c5a0 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1603,7 +1603,7 @@ defmodule Module.Types.Descr do defp fun_normalize(%{fun: {:union, bdds}}, arity) do case :maps.take(arity, bdds) do - {bdd, _rest} -> + {bdd, rest} -> {domain, arrows} = Enum.reduce(fun_bdd_to_pos_dnf(arity, bdd), {term(), []}, fn pos_funs, {domain, arrows} -> @@ -1611,7 +1611,13 @@ defmodule Module.Types.Descr do end) if arrows == [] do - {:badarity, :maps.keys(bdds)} + # The function is empty at the requested arity. Report the *other* + # arities (never the called one, which would be self-contradictory), + # or :badfun when there are none, i.e. the function is empty. + case :maps.keys(rest) do + [] -> :badfun + other -> {:badarity, other} + end else {:ok, domain, arrows} end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 78c8f252eb3..a0f6f0ec1e3 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1378,6 +1378,28 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun_mixed, [integer()]) == {:badarity, [1, 2]} assert fun_apply(fun_mixed, [integer(), atom()]) == {:badarity, [2, 1]} + # A function that is empty at the called arity is :badfun, not a + # self-contradictory {:badarity, [called_arity]}. + empty_fun = + opt_difference(fun([opt_union(integer(), float())], atom()), fun([integer()], atom())) + + assert empty?(empty_fun) + assert fun_apply(empty_fun, [integer()]) == :badfun + + # When the function is empty at the called arity but usable at another, the + # badarity must list only the *other* arity, never the (empty) called one. + # `empty_fun` keeps a structured empty arity-1 entry, which union preserves, + # so `usable_at_2` is equal? to a plain 2-arity function yet carries it. + usable_at_2 = opt_union(empty_fun, fun([integer(), atom()], boolean())) + + refute empty?(usable_at_2) + assert equal?(usable_at_2, fun([integer(), atom()], boolean())) + + # Congruent with the normalized form, and excludes the empty arity 1. + assert fun_apply(usable_at_2, [integer()]) == {:badarity, [2]} + assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == {:badarity, [2]} + assert fun_apply(usable_at_2, [integer(), atom()]) == {:ok, boolean()} + # Function intersection tests (no overlap) fun0 = opt_intersection(fun([integer()], atom()), fun([float()], binary())) assert fun_apply(fun0, [integer()]) == {:ok, atom()}