From a246611dd5a16643ed7c77dc9404657ef771bbea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 20 Sep 2026 20:01:07 +0200 Subject: [PATCH] Do not report empty function arities as badarity Fixes #15921 --- lib/elixir/lib/module/types/descr.ex | 31 ++++++++++++------- .../test/elixir/module/types/descr_test.exs | 28 +++++++++++++++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 04544d9357..e0f4174161 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1556,18 +1556,23 @@ defmodule Module.Types.Descr do defp fun_other_non_empty_arities(%{fun: {:union, bdds}}, arity) do case :maps.take(arity, bdds) do - {_bdd, rest} -> - for {a, b} <- rest, - not Enum.all?(bdd_to_dnf(b), fn {pos, neg} -> fun_line_empty?(pos, neg) end), - do: a - - :error -> - [] + {_bdd, rest} -> bdds_non_empty_arities(rest) + :error -> [] end end defp fun_other_non_empty_arities(_, _), do: [] + # Returns the arities of the given BDD map whose function type is not empty. + # An empty bucket carries no value, so it must never be reported as a + # supported arity (otherwise we would return :badarity for a function type + # that is actually empty, or reject a live arity in favour of a dead one). + defp bdds_non_empty_arities(bdds) do + for {arity, bdd} <- bdds, + not Enum.all?(bdd_to_dnf(bdd), fn {pos, neg} -> fun_line_empty?(pos, neg) end), + do: arity + end + # Transforms a binary decision diagram (BDD) into the canonical `domain-arrows` pair: # # 1. **domain**: The union of all domains from positive functions in the BDD @@ -1602,9 +1607,10 @@ defmodule Module.Types.Descr do if arrows == [] do # 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 + # non-empty arities (never the called one, which would be + # self-contradictory), or :badfun when there are none, i.e. the + # function is empty. + case bdds_non_empty_arities(rest) do [] -> :badfun other -> {:badarity, other} end @@ -1613,7 +1619,10 @@ defmodule Module.Types.Descr do end :error -> - {:badarity, :maps.keys(bdds)} + case bdds_non_empty_arities(bdds) do + [] -> :badfun + other -> {:badarity, other} + end end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index a39af87d92..1c73a3a8ae 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1459,6 +1459,34 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == {:badarity, [2]} assert fun_apply(usable_at_2, [integer(), atom()]) == {:ok, boolean()} + # A function that is empty at *every* arity is :badfun at any arity, even + # when other (also empty) arity entries are structurally present. An empty + # entry must never be reported as a supported arity. + empty_at_1 = opt_difference(fun([integer()], atom()), fun([integer()], term())) + + empty_at_2 = + opt_difference(fun([integer(), atom()], atom()), fun([integer(), atom()], term())) + + empty_both = opt_union(empty_at_1, empty_at_2) + + assert empty?(empty_both) + assert fun_apply(empty_both, [integer()]) == :badfun + assert fun_apply(empty_both, [integer(), atom()]) == :badfun + + # Same when the called arity has no entry at all and the only entries + # present are empty ones. + assert fun_apply(empty_at_2, [integer()]) == :badfun + assert fun_apply(dynamic(empty_at_2), [integer()]) == :badfun + + # An empty static arity must not shadow a live dynamic one: `live_at_2` is + # semantically equal to its dynamic arity-2 part, so applying it to two + # arguments must succeed instead of reporting the dead arity 1. + live_at_2 = opt_union(empty_at_1, dynamic(fun([integer(), atom()], boolean()))) + + assert equal?(live_at_2, dynamic(fun([integer(), atom()], boolean()))) + assert fun_apply(live_at_2, [integer(), atom()]) == {:ok, dynamic(boolean())} + assert fun_apply(live_at_2, [integer()]) == {:badarity, [2]} + # Function intersection tests (no overlap) fun0 = opt_intersection(fun([integer()], atom()), fun([float()], binary())) assert fun_apply(fun0, [integer()]) == {:ok, atom()}