Skip to content

Commit b29c83a

Browse files
authored
Apply further fn optimizations and fixes (#14619)
Closes #14598
1 parent d7bdea5 commit b29c83a

File tree

3 files changed

+64
-68
lines changed

3 files changed

+64
-68
lines changed

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

Lines changed: 49 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ defmodule Module.Types.Descr do
4646
@not_non_empty_list Map.delete(@term, :list)
4747
@not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list)
4848

49-
@empty_intersection [0, @none, [], :fun_bottom]
50-
@empty_difference [0, [], :fun_bottom]
49+
@empty_intersection [0, []]
50+
@empty_difference [0, []]
5151

5252
defguard is_descr(descr) when is_map(descr) or descr == :term
5353

@@ -398,12 +398,20 @@ defmodule Module.Types.Descr do
398398
# Returning 0 from the callback is taken as none() for that subtype.
399399
defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2)
400400
defp intersection(:bitmap, v1, v2), do: v1 &&& v2
401-
defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2)
402401
defp intersection(:list, v1, v2), do: list_intersection(v1, v2)
403402
defp intersection(:map, v1, v2), do: map_intersection(v1, v2)
404403
defp intersection(:optional, 1, 1), do: 1
405404
defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2)
406-
defp intersection(:fun, v1, v2), do: fun_intersection(v1, v2)
405+
406+
defp intersection(:fun, v1, v2) do
407+
bdd = fun_intersection(v1, v2)
408+
if bdd == :fun_bottom, do: 0, else: bdd
409+
end
410+
411+
defp intersection(:dynamic, v1, v2) do
412+
descr = dynamic_intersection(v1, v2)
413+
if descr == @none, do: 0, else: descr
414+
end
407415

408416
@doc """
409417
Computes the difference between two types.
@@ -490,7 +498,11 @@ defmodule Module.Types.Descr do
490498
defp difference(:map, v1, v2), do: map_difference(v1, v2)
491499
defp difference(:optional, 1, 1), do: 0
492500
defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2)
493-
defp difference(:fun, v1, v2), do: fun_difference(v1, v2)
501+
502+
defp difference(:fun, v1, v2) do
503+
bdd = fun_difference(v1, v2)
504+
if bdd == :fun_bottom, do: 0, else: bdd
505+
end
494506

495507
@doc """
496508
Compute the negation of a type.
@@ -1159,7 +1171,7 @@ defmodule Module.Types.Descr do
11591171
with {:ok, domain, static_arrows, dynamic_arrows} <-
11601172
fun_normalize_both(fun_static, fun_dynamic, arity) do
11611173
cond do
1162-
empty?(args_domain) ->
1174+
Enum.any?(arguments, &empty?/1) ->
11631175
{:badarg, domain_to_flat_args(domain, arity)}
11641176

11651177
not subtype?(args_domain, domain) ->
@@ -1170,26 +1182,21 @@ defmodule Module.Types.Descr do
11701182
end
11711183

11721184
static? ->
1173-
{:ok, fun_apply_static(arguments, static_arrows, false)}
1185+
{:ok, fun_apply_static(arguments, static_arrows)}
11741186

11751187
static_arrows == [] ->
11761188
# TODO: We need to validate this within the theory
11771189
arguments = Enum.map(arguments, &upper_bound/1)
1178-
{:ok, dynamic(fun_apply_static(arguments, dynamic_arrows, false))}
1190+
{:ok, dynamic(fun_apply_static(arguments, dynamic_arrows))}
11791191

11801192
true ->
11811193
# For dynamic cases, combine static and dynamic results
1182-
{static_args, dynamic_args, maybe_empty?} =
1183-
if args_dynamic? do
1184-
{Enum.map(arguments, &upper_bound/1), Enum.map(arguments, &lower_bound/1), true}
1185-
else
1186-
{arguments, arguments, false}
1187-
end
1194+
arguments = Enum.map(arguments, &upper_bound/1)
11881195

11891196
{:ok,
11901197
union(
1191-
fun_apply_static(static_args, static_arrows, false),
1192-
dynamic(fun_apply_static(dynamic_args, dynamic_arrows, maybe_empty?))
1198+
fun_apply_static(arguments, static_arrows),
1199+
dynamic(fun_apply_static(arguments, dynamic_arrows))
11931200
)}
11941201
end
11951202
end
@@ -1289,26 +1296,12 @@ defmodule Module.Types.Descr do
12891296
:badfun
12901297
end
12911298

1292-
defp fun_apply_static(arguments, arrows, maybe_empty?) do
1299+
defp fun_apply_static(arguments, arrows) do
12931300
type_args = args_to_domain(arguments)
12941301

1295-
# Optimization: short-circuits when inner loop is none() or outer loop is term()
1296-
if maybe_empty? and empty?(type_args) do
1297-
Enum.reduce_while(arrows, none(), fn intersection_of_arrows, acc ->
1298-
Enum.reduce_while(intersection_of_arrows, term(), fn
1299-
{_dom, _ret}, acc when acc == @none -> {:halt, acc}
1300-
{_dom, ret}, acc -> {:cont, intersection(acc, ret)}
1301-
end)
1302-
|> case do
1303-
:term -> {:halt, :term}
1304-
inner -> {:cont, union(inner, acc)}
1305-
end
1306-
end)
1307-
else
1308-
Enum.reduce(arrows, none(), fn intersection_of_arrows, acc ->
1309-
aux_apply(acc, type_args, term(), intersection_of_arrows)
1310-
end)
1311-
end
1302+
Enum.reduce(arrows, none(), fn intersection_of_arrows, acc ->
1303+
aux_apply(acc, type_args, term(), intersection_of_arrows)
1304+
end)
13121305
end
13131306

13141307
# Helper function for function application that handles the application of
@@ -1471,7 +1464,7 @@ defmodule Module.Types.Descr do
14711464
# This avoids the expensive recursive phi computation by checking only that applying the
14721465
# input to the positive intersection yields a subtype of the return
14731466
if all_non_empty_domains?([{arguments, return} | positives]) do
1474-
fun_apply_static(arguments, [positives], false)
1467+
fun_apply_static(arguments, [positives])
14751468
|> subtype?(return)
14761469
else
14771470
n = length(arguments)
@@ -1496,45 +1489,44 @@ defmodule Module.Types.Descr do
14961489
# Create cache key from function arguments
14971490
cache_key = {args, {b, ret}, [{arguments, return} | rest_positive]}
14981491

1499-
case Map.get(cache, cache_key) do
1500-
nil ->
1492+
case cache do
1493+
%{^cache_key => value} ->
1494+
value
1495+
1496+
%{} ->
15011497
# Compute result and cache it
15021498
{result1, cache} = phi(args, {true, intersection(ret, return)}, rest_positive, cache)
15031499

15041500
if not result1 do
1505-
# Store false result in cache
15061501
cache = Map.put(cache, cache_key, false)
15071502
{false, cache}
15081503
else
1509-
# This doesn't stop if one intermediate result is false?
1510-
{result2, cache} =
1511-
Enum.with_index(arguments)
1512-
|> Enum.reduce_while({true, cache}, fn {type, index}, {acc_result, acc_cache} ->
1513-
{new_result, new_cache} =
1514-
List.update_at(args, index, fn {_, arg} -> {true, difference(arg, type)} end)
1515-
|> phi({b, ret}, rest_positive, acc_cache)
1516-
1517-
if new_result do
1518-
{:cont, {acc_result and new_result, new_cache}}
1519-
else
1520-
{:halt, {false, new_cache}}
1521-
end
1504+
{_index, result2, cache} =
1505+
Enum.reduce_while(arguments, {0, true, cache}, fn
1506+
type, {index, acc_result, acc_cache} ->
1507+
{new_result, new_cache} =
1508+
args
1509+
|> List.update_at(index, fn {_, arg} -> {true, difference(arg, type)} end)
1510+
|> phi({b, ret}, rest_positive, acc_cache)
1511+
1512+
if new_result do
1513+
{:cont, {index + 1, acc_result and new_result, new_cache}}
1514+
else
1515+
{:halt, {index + 1, false, new_cache}}
1516+
end
15221517
end)
15231518

15241519
result = result1 and result2
1525-
# Store result in cache
15261520
cache = Map.put(cache, cache_key, result)
15271521
{result, cache}
15281522
end
1529-
1530-
cached_result ->
1531-
# Return cached result
1532-
{cached_result, cache}
15331523
end
15341524
end
15351525

15361526
defp all_non_empty_domains?(positives) do
1537-
Enum.all?(positives, fn {args, _ret} -> not empty?(args_to_domain(args)) end)
1527+
Enum.all?(positives, fn {args, _ret} ->
1528+
Enum.all?(args, fn arg -> not empty?(arg) end)
1529+
end)
15381530
end
15391531

15401532
defp fun_union(bdd1, bdd2) do
@@ -2392,10 +2384,6 @@ defmodule Module.Types.Descr do
23922384
:empty -> acc
23932385
end
23942386
end
2395-
|> case do
2396-
[] -> 0
2397-
acc -> acc
2398-
end
23992387
end
24002388

24012389
# Intersects two map literals; throws if their intersection is empty.

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -919,16 +919,14 @@ defmodule Module.Types.DescrTest do
919919
assert fun_apply(fun([dynamic()], term()), [dynamic()]) == {:ok, term()}
920920
assert fun_apply(fun([integer()], dynamic()), [integer()]) == {:ok, dynamic()}
921921

922-
assert fun_apply(fun([dynamic()], integer()), [dynamic()])
923-
|> elem(1)
924-
|> equal?(integer())
922+
assert fun_apply(fun([dynamic()], integer()), [dynamic()]) ==
923+
{:ok, union(integer(), dynamic())}
925924

926-
assert fun_apply(fun([dynamic(), atom()], float()), [dynamic(), atom()])
927-
|> elem(1)
928-
|> equal?(float())
925+
assert fun_apply(fun([dynamic(), atom()], float()), [dynamic(), atom()]) ==
926+
{:ok, union(float(), dynamic())}
929927

930928
fun = fun([dynamic(integer())], atom())
931-
assert fun_apply(fun, [dynamic(integer())]) |> elem(1) |> equal?(atom())
929+
assert fun_apply(fun, [dynamic(integer())]) == {:ok, union(atom(), dynamic())}
932930
assert fun_apply(fun, [dynamic(number())]) == {:ok, dynamic()}
933931
assert fun_apply(fun, [integer()]) == {:ok, dynamic()}
934932
assert fun_apply(fun, [float()]) == {:badarg, [dynamic(integer())]}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ defmodule Module.Types.ExprTest do
169169
data -> data
170170
end).(map)
171171
) == dynamic()
172+
173+
assert typecheck!(
174+
[],
175+
[true, false]
176+
|> Enum.random()
177+
|> then(fn
178+
true -> :ok
179+
_ -> :error
180+
end)
181+
) == dynamic(atom([:ok, :error]))
172182
end
173183

174184
test "bad function" do

0 commit comments

Comments
 (0)