Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
28 changes: 28 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand Down
Loading