Skip to content

Commit 981ef72

Browse files
committed
domain_to_args and args_to_domain
1 parent 684acb9 commit 981ef72

File tree

3 files changed

+129
-136
lines changed

3 files changed

+129
-136
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -820,17 +820,27 @@ defmodule Module.Types.Apply do
820820

821821
def format_diagnostic({{:badapply, reason}, args_types, fun_type, expr, context}) do
822822
traces =
823-
if reason == :badarg do
824-
collect_traces(expr, context)
825-
else
826-
# In case there the type itself is invalid,
827-
# we limit the trace.
828-
collect_traces(elem(expr, 0), context)
823+
case reason do
824+
# Include arguments in traces in case of badarg
825+
{:badarg, _} -> collect_traces(expr, context)
826+
# Otherwise just the fun
827+
_ -> collect_traces(elem(expr, 0), context)
829828
end
830829

831830
message =
832831
case reason do
833832
# TODO: Return the domain here
833+
{:badarg, _} ->
834+
"""
835+
expected a #{length(args_types)}-arity function on call:
836+
837+
#{expr_to_string(expr) |> indent(4)}
838+
839+
but got type:
840+
841+
#{to_quoted_string(fun_type) |> indent(4)}
842+
"""
843+
834844
:badarg ->
835845
"""
836846
expected a #{length(args_types)}-arity function on call:

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

Lines changed: 102 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,50 @@ defmodule Module.Types.Descr do
133133
end
134134

135135
@doc """
136+
Converts a list of arguments into a domain.
137+
136138
Tuples represent function domains, using unions to combine parameters.
137139
138-
Example: for functions (integer, float ->:ok) and (float, integer -> :error)
139-
domain isn't {integer|float,integer|float} as that would incorrectly accept {float,float}
140-
Instead, it is {integer,float} or {float,integer}
140+
Example: for functions (integer(), float() -> :ok) and (float(), integer() -> :error)
141+
domain isn't `{integer() or float(), integer() or float()}` as that would incorrectly
142+
accept `{float(), float()}`, instead it is `{integer(), float()} or {float(), integer()}`.
143+
"""
144+
def args_to_domain(types) when is_list(types), do: tuple(types)
145+
146+
@doc """
147+
Converts the domain to arguments.
148+
149+
The domain is expected to be closed tuples. They may have complex negations
150+
which are then simplified to a union of positive tuple literals only.
141151
142-
Made public for testing.
152+
* For static tuple types: eliminates all negations from the DNF representation.
153+
154+
* For gradual tuple types: processes both dynamic and static components separately,
155+
then combines them.
156+
157+
Internally it uses `tuple_reduce/4` with concatenation as the join function
158+
and a transform that is simply the identity.
143159
"""
144-
def domain_descr(types) when is_list(types), do: tuple(types)
160+
def domain_to_args(descr) do
161+
case :maps.take(:dynamic, descr) do
162+
:error ->
163+
{tuple_elim_negations_static(descr), []}
164+
165+
{dynamic, static} ->
166+
{tuple_elim_negations_static(static), tuple_elim_negations_static(dynamic)}
167+
end
168+
end
169+
170+
# Call tuple_reduce to build the simple union of tuples that come from each map literal.
171+
# Thus, initial is `[]`, join is concatenation, and the transform of a map literal
172+
# with no negations is just to keep the map literal as is.
173+
defp tuple_elim_negations_static(%{tuple: dnf} = descr) when map_size(descr) == 1 do
174+
tuple_reduce(dnf, [], &Kernel.++/2, fn :closed, elements ->
175+
[elements]
176+
end)
177+
end
178+
179+
defp tuple_elim_negations_static(descr) when descr == %{}, do: []
145180

146181
## Optional
147182

@@ -988,80 +1023,71 @@ defmodule Module.Types.Descr do
9881023
end
9891024

9901025
def fun_apply(fun, arguments) do
991-
if empty?(domain_descr(arguments)) do
992-
:badarg
993-
else
994-
case :maps.take(:dynamic, fun) do
995-
:error ->
996-
if fun_only?(fun) do
997-
fun_apply_with_strategy(fun, nil, arguments)
998-
else
999-
:badfun
1000-
end
1026+
case :maps.take(:dynamic, fun) do
1027+
:error ->
1028+
if fun_only?(fun) do
1029+
fun_apply_with_strategy(fun, fun, nil, arguments)
1030+
else
1031+
:badfun
1032+
end
10011033

1002-
# Optimize the cases where dynamic closes over all function types
1003-
{:term, fun_static} when fun_static == %{} ->
1004-
{:ok, dynamic()}
1005-
1006-
{%{fun: @fun_top}, fun_static} when fun_static == %{} ->
1007-
{:ok, dynamic()}
1008-
1009-
{fun_dynamic, fun_static} ->
1010-
if fun_only?(fun_static) do
1011-
with :badarg <- fun_apply_with_strategy(fun_static, fun_dynamic, arguments) do
1012-
if compatible?(fun, fun(arguments, term())) do
1013-
{:ok, dynamic()}
1014-
else
1015-
:badarg
1016-
end
1017-
end
1018-
else
1019-
:badfun
1020-
end
1021-
end
1034+
# Optimize the cases where dynamic closes over all function types
1035+
{:term, fun_static} when fun_static == %{} ->
1036+
{:ok, dynamic()}
1037+
1038+
{%{fun: @fun_top}, fun_static} when fun_static == %{} ->
1039+
{:ok, dynamic()}
1040+
1041+
{fun_dynamic, fun_static} ->
1042+
if fun_only?(fun_static) do
1043+
fun_apply_with_strategy(fun, fun_static, fun_dynamic, arguments)
1044+
else
1045+
:badfun
1046+
end
10221047
end
10231048
end
10241049

10251050
defp fun_only?(descr), do: empty?(Map.delete(descr, :fun))
10261051

1027-
defp fun_apply_with_strategy(fun_static, fun_dynamic, arguments) do
1052+
defp fun_apply_with_strategy(fun, fun_static, fun_dynamic, arguments) do
10281053
args_dynamic? = any_dynamic?(arguments)
1054+
args_domain = args_to_domain(arguments)
1055+
static? = fun_dynamic == nil and not args_dynamic?
10291056
arity = length(arguments)
10301057

1031-
# For non-dynamic function and arguments, just return the static result
1032-
if fun_dynamic == nil and not args_dynamic? do
1033-
with {:ok, static_domain, static_arrows} <- fun_normalize(fun_static, arity, :static) do
1034-
if subtype?(domain_descr(arguments), static_domain) do
1058+
with {:ok, domain, static_arrows, dynamic_arrows} <-
1059+
fun_normalize_both(fun_static, fun_dynamic, arity) do
1060+
cond do
1061+
empty?(args_domain) ->
1062+
{:badarg, domain}
1063+
1064+
not subtype?(args_domain, domain) ->
1065+
if static? or not compatible?(fun, fun(arguments, term())) do
1066+
{:badarg, domain}
1067+
else
1068+
{:ok, dynamic()}
1069+
end
1070+
1071+
static? ->
10351072
{:ok, fun_apply_static(arguments, static_arrows, false)}
1036-
else
1037-
:badarg
1038-
end
1039-
end
1040-
else
1041-
with {:ok, domain, static_arrows, dynamic_arrows} <-
1042-
fun_normalize_both(fun_static, fun_dynamic, arity) do
1043-
cond do
1044-
not subtype?(domain_descr(arguments), domain) ->
1045-
:badarg
10461073

1047-
static_arrows == [] ->
1048-
{:ok, dynamic(fun_apply_static(arguments, dynamic_arrows, false))}
1074+
static_arrows == [] ->
1075+
{:ok, dynamic(fun_apply_static(arguments, dynamic_arrows, false))}
10491076

1050-
true ->
1051-
# For dynamic cases, combine static and dynamic results
1052-
{static_args, dynamic_args, maybe_empty?} =
1053-
if args_dynamic? do
1054-
{Enum.map(arguments, &upper_bound/1), Enum.map(arguments, &lower_bound/1), true}
1055-
else
1056-
{arguments, arguments, false}
1057-
end
1058-
1059-
{:ok,
1060-
union(
1061-
fun_apply_static(static_args, static_arrows, false),
1062-
dynamic(fun_apply_static(dynamic_args, dynamic_arrows, maybe_empty?))
1063-
)}
1064-
end
1077+
true ->
1078+
# For dynamic cases, combine static and dynamic results
1079+
{static_args, dynamic_args, maybe_empty?} =
1080+
if args_dynamic? do
1081+
{Enum.map(arguments, &upper_bound/1), Enum.map(arguments, &lower_bound/1), true}
1082+
else
1083+
{arguments, arguments, false}
1084+
end
1085+
1086+
{:ok,
1087+
union(
1088+
fun_apply_static(static_args, static_arrows, false),
1089+
dynamic(fun_apply_static(dynamic_args, dynamic_arrows, maybe_empty?))
1090+
)}
10651091
end
10661092
end
10671093
end
@@ -1137,7 +1163,7 @@ defmodule Module.Types.Descr do
11371163
# Calculate domain from all positive functions
11381164
path_domain =
11391165
Enum.reduce(pos_funs, none(), fn {args, _}, acc ->
1140-
union(acc, domain_descr(args))
1166+
union(acc, args_to_domain(args))
11411167
end)
11421168

11431169
{intersection(domain, path_domain), [pos_funs | arrows], bad_arities}
@@ -1161,7 +1187,7 @@ defmodule Module.Types.Descr do
11611187
end
11621188

11631189
defp fun_apply_static(arguments, arrows, maybe_empty?) do
1164-
type_args = domain_descr(arguments)
1190+
type_args = args_to_domain(arguments)
11651191

11661192
# Optimization: short-circuits when inner loop is none() or outer loop is term()
11671193
if maybe_empty? and empty?(type_args) do
@@ -1202,7 +1228,7 @@ defmodule Module.Types.Descr do
12021228

12031229
defp aux_apply(result, input, returns_reached, [{dom, ret} | arrow_intersections]) do
12041230
# Calculate the part of the input not covered by this arrow's domain
1205-
dom_subtract = difference(input, domain_descr(dom))
1231+
dom_subtract = difference(input, args_to_domain(dom))
12061232

12071233
# Refine the return type by intersecting with this arrow's return type
12081234
ret_refine = intersection(returns_reached, ret)
@@ -1298,7 +1324,7 @@ defmodule Module.Types.Descr do
12981324
# function's domain is a supertype of the positive domain and if the phi function
12991325
# determines emptiness.
13001326
length(neg_arguments) == positive_arity and
1301-
subtype?(domain_descr(neg_arguments), positive_domain) and
1327+
subtype?(args_to_domain(neg_arguments), positive_domain) and
13021328
phi_starter(neg_arguments, negation(neg_return), positives)
13031329
end)
13041330
end
@@ -1311,10 +1337,10 @@ defmodule Module.Types.Descr do
13111337
positives
13121338
|> Enum.reduce_while({:empty, none()}, fn
13131339
{args, _}, {:empty, _} ->
1314-
{:cont, {length(args), domain_descr(args)}}
1340+
{:cont, {length(args), args_to_domain(args)}}
13151341

13161342
{args, _}, {arity, dom} when length(args) == arity ->
1317-
{:cont, {arity, union(dom, domain_descr(args))}}
1343+
{:cont, {arity, union(dom, args_to_domain(args))}}
13181344

13191345
{_args, _}, {_arity, _} ->
13201346
{:halt, {:empty, none()}}
@@ -3091,6 +3117,9 @@ defmodule Module.Types.Descr do
30913117
end)
30923118
end
30933119

3120+
@doc """
3121+
Returns all of the values that are part of a tuple.
3122+
"""
30943123
def tuple_values(descr) do
30953124
case :maps.take(:dynamic, descr) do
30963125
:error ->
@@ -3182,46 +3211,6 @@ defmodule Module.Types.Descr do
31823211
)
31833212
end
31843213

3185-
@doc """
3186-
Converts a tuple type to a simple union by eliminating negations.
3187-
3188-
Takes a tuple type with complex negations and simplifies it to a union of
3189-
positive tuple literals only.
3190-
3191-
For static tuple types: eliminates all negations from the DNF representation.
3192-
For gradual tuple types: processes both dynamic and static components separately,
3193-
then combines them.
3194-
3195-
Uses `tuple_reduce/4` with concatenation as the join function and a transform
3196-
that is simply the identity.
3197-
3198-
Returns the descriptor unchanged for non-tuple types, or a descriptor with
3199-
simplified tuple DNF containing only positive literals. If simplification
3200-
results in an empty tuple list, removes the `:tuple` key entirely.
3201-
"""
3202-
def tuple_elim_negations(descr) do
3203-
case :maps.take(:dynamic, descr) do
3204-
:error ->
3205-
tuple_elim_negations_static(descr)
3206-
3207-
{dynamic, static} ->
3208-
tuple_elim_negations_static(static)
3209-
|> union(dynamic(tuple_elim_negations_static(dynamic)))
3210-
end
3211-
end
3212-
3213-
# Call tuple_reduce to build the simple union of tuples that come from each map literal.
3214-
# Thus, initial is `[]`, join is concatenation, and the transform of a map literal
3215-
# with no negations is just to keep the map literal as is.
3216-
defp tuple_elim_negations_static(%{tuple: dnf} = descr) do
3217-
case tuple_reduce(dnf, [], &Kernel.++/2, fn tag, elements -> [{tag, elements, []}] end) do
3218-
[] -> Map.delete(descr, :tuple)
3219-
new_dnf -> %{descr | tuple: new_dnf}
3220-
end
3221-
end
3222-
3223-
defp tuple_elim_negations_static(descr), do: descr
3224-
32253214
defp tuple_pop_index(tag, elements, index) do
32263215
case List.pop_at(elements, index) do
32273216
{nil, _} -> {tag_to_type(tag), %{tuple: [{tag, elements, []}]}}

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@ defmodule Module.Types.DescrTest do
12421242
|> equal?(integer())
12431243
end
12441244

1245-
test "tuple_elim_negations" do
1245+
test "domain_to_args" do
12461246
# take complex tuples, normalize them, and check if they are still equal
12471247
complex_tuples = [
12481248
tuple([term(), atom(), number()])
@@ -1251,24 +1251,18 @@ defmodule Module.Types.DescrTest do
12511251
difference(
12521252
tuple([union(atom(), pid()), union(integer(), float())]),
12531253
tuple([union(atom(), pid()), float()])
1254-
),
1255-
# open_tuple case with union in elements
1256-
difference(
1257-
open_tuple([union(boolean(), pid()), union(atom(), integer())]),
1258-
open_tuple([pid(), integer()])
1259-
),
1260-
open_tuple([term(), term(), term()])
1261-
|> difference(open_tuple([term(), integer(), atom(), atom()]))
1262-
|> difference(tuple([float(), float(), float(), float(), float()]))
1263-
|> difference(tuple([term(), term(), term(), term(), term(), term()]))
1254+
)
12641255
]
12651256

1266-
Enum.each(complex_tuples, fn orig ->
1267-
norm = tuple_elim_negations(orig)
1268-
# should split into multiple simple tuples
1269-
assert equal?(norm, orig)
1270-
assert Enum.all?(norm.tuple, fn {_, _, neg} -> neg == [] end)
1271-
assert not Enum.all?(orig.tuple, fn {_, _, neg} -> neg == [] end)
1257+
multi_args_to_domain = fn args ->
1258+
Enum.reduce(args, none(), &union(args_to_domain(&1), &2))
1259+
end
1260+
1261+
Enum.each(complex_tuples, fn domain ->
1262+
{static, dynamic} = domain_to_args(domain)
1263+
1264+
assert union(multi_args_to_domain.(static), dynamic(multi_args_to_domain.(dynamic)))
1265+
|> equal?(domain)
12721266
end)
12731267
end
12741268

0 commit comments

Comments
 (0)