Skip to content

Commit 615751d

Browse files
authored
Implement optimization of tuple unions (#14228)
1 parent a43741f commit 615751d

File tree

2 files changed

+147
-29
lines changed

2 files changed

+147
-29
lines changed

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

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ defmodule Module.Types.Descr do
343343
def empty?(:term), do: false
344344

345345
def empty?(%{} = descr) do
346-
case Map.get(descr, :dynamic, descr) do
346+
case :maps.get(:dynamic, descr, _default = descr) do
347347
:term ->
348348
false
349349

@@ -1897,18 +1897,18 @@ defmodule Module.Types.Descr do
18971897

18981898
defp map_non_negated_fuse(maps) do
18991899
Enum.reduce(maps, [], fn map, acc ->
1900-
fuse_with_first_fusible(map, acc)
1900+
map_fuse_with_first_fusible(map, acc)
19011901
end)
19021902
end
19031903

1904-
defp fuse_with_first_fusible(map, []), do: [map]
1904+
defp map_fuse_with_first_fusible(map, []), do: [map]
19051905

1906-
defp fuse_with_first_fusible(map, [candidate | rest]) do
1906+
defp map_fuse_with_first_fusible(map, [candidate | rest]) do
19071907
if fused = maybe_optimize_map_union(map, candidate) do
19081908
# we found a fusible candidate, we're done
19091909
[fused | rest]
19101910
else
1911-
[candidate | fuse_with_first_fusible(map, rest)]
1911+
[candidate | map_fuse_with_first_fusible(map, rest)]
19121912
end
19131913
end
19141914

@@ -2157,9 +2157,106 @@ defmodule Module.Types.Descr do
21572157
end
21582158
end
21592159

2160-
# Removes duplicates in union, which should trickle to other operations.
2161-
# This is a cheap optimization that relies on structural equality.
2162-
defp tuple_union(left, right), do: left ++ (right -- left)
2160+
defp tuple_union(dnf1, dnf2) do
2161+
# Union is just concatenation, but we rely on some optimization strategies to
2162+
# avoid the list to grow when possible
2163+
2164+
# first pass trying to identify patterns where two maps can be fused as one
2165+
with [tuple1] <- dnf1,
2166+
[tuple2] <- dnf2,
2167+
optimized when optimized != nil <- maybe_optimize_tuple_union(tuple1, tuple2) do
2168+
[optimized]
2169+
else
2170+
# otherwise we just concatenate and remove structural duplicates
2171+
_ -> dnf1 ++ (dnf2 -- dnf1)
2172+
end
2173+
end
2174+
2175+
defp maybe_optimize_tuple_union({tag1, pos1, []} = tuple1, {tag2, pos2, []} = tuple2) do
2176+
case tuple_union_optimization_strategy(tag1, pos1, tag2, pos2) do
2177+
:all_equal ->
2178+
tuple1
2179+
2180+
{:one_index_difference, index, v1, v2} ->
2181+
new_pos = List.replace_at(pos1, index, union(v1, v2))
2182+
{tag1, new_pos, []}
2183+
2184+
:left_subtype_of_right ->
2185+
tuple2
2186+
2187+
:right_subtype_of_left ->
2188+
tuple1
2189+
2190+
nil ->
2191+
nil
2192+
end
2193+
end
2194+
2195+
defp maybe_optimize_tuple_union(_, _), do: nil
2196+
2197+
defp tuple_union_optimization_strategy(tag1, pos1, tag2, pos2)
2198+
defp tuple_union_optimization_strategy(tag, pos, tag, pos), do: :all_equal
2199+
2200+
# might be one extra loop but cheap and avoids doing deep subtype comparisons
2201+
defp tuple_union_optimization_strategy(:closed, pos1, :closed, pos2)
2202+
when length(pos1) != length(pos2),
2203+
do: nil
2204+
2205+
defp tuple_union_optimization_strategy(tag1, pos1, tag2, pos2) do
2206+
status =
2207+
case {tag1, tag2} do
2208+
{:open, :closed} -> :right_subtype_of_left
2209+
{:closed, :open} -> :left_subtype_of_right
2210+
{same, same} -> :all_equal
2211+
end
2212+
2213+
do_tuple_union_optimization_strategy(tag1, pos1, tag2, pos2, 0, status)
2214+
end
2215+
2216+
defp do_tuple_union_optimization_strategy(_tag1, [], _tag2, [], _i, status), do: status
2217+
2218+
defp do_tuple_union_optimization_strategy(:open, [], _tag2, _pos2, _i, status)
2219+
when status in [:all_equal, :right_subtype_of_left],
2220+
do: :right_subtype_of_left
2221+
2222+
defp do_tuple_union_optimization_strategy(_tag1, _pos1, :open, [], _i, status)
2223+
when status in [:all_equal, :left_subtype_of_right],
2224+
do: :left_subtype_of_right
2225+
2226+
defp do_tuple_union_optimization_strategy(tag1, [v1 | pos1], tag2, [v2 | pos2], i, status) do
2227+
if next_status = tuple_union_next_strategy(i, v1, v2, status) do
2228+
do_tuple_union_optimization_strategy(tag1, pos1, tag2, pos2, i + 1, next_status)
2229+
end
2230+
end
2231+
2232+
defp do_tuple_union_optimization_strategy(_tag1, _pos1, _tag2, _pos2, _i, _status), do: nil
2233+
2234+
defp tuple_union_next_strategy(index, v1, v2, status)
2235+
2236+
# structurally equal values do not impact the ongoing strategy
2237+
defp tuple_union_next_strategy(_index, same, same, status), do: status
2238+
2239+
defp tuple_union_next_strategy(index, v1, v2, :all_equal) do
2240+
{:one_index_difference, index, v1, v2}
2241+
end
2242+
2243+
defp tuple_union_next_strategy(_index, v1, v2, {:one_index_difference, _, d1, d2}) do
2244+
# we have at least two differences now, we switch strategy
2245+
# if both are subtypes in one direction, keep checking
2246+
cond do
2247+
subtype?(d1, d2) and subtype?(v1, v2) -> :left_subtype_of_right
2248+
subtype?(d2, d1) and subtype?(v2, v1) -> :right_subtype_of_left
2249+
true -> nil
2250+
end
2251+
end
2252+
2253+
defp tuple_union_next_strategy(_index, v1, v2, :left_subtype_of_right) do
2254+
if subtype?(v1, v2), do: :left_subtype_of_right
2255+
end
2256+
2257+
defp tuple_union_next_strategy(_index, v1, v2, :right_subtype_of_left) do
2258+
if subtype?(v2, v1), do: :right_subtype_of_left
2259+
end
21632260

21642261
defp tuple_to_quoted(dnf, opts) do
21652262
dnf
@@ -2189,27 +2286,19 @@ defmodule Module.Types.Descr do
21892286

21902287
defp tuple_non_negated_fuse(tuples) do
21912288
Enum.reduce(tuples, [], fn tuple, acc ->
2192-
case Enum.split_while(acc, &non_fusible_tuples?(tuple, &1)) do
2193-
{_, []} ->
2194-
[tuple | acc]
2195-
2196-
{others, [match | rest]} ->
2197-
fused = tuple_non_negated_fuse_pair(tuple, match)
2198-
others ++ [fused | rest]
2199-
end
2289+
tuple_fuse_with_first_fusible(tuple, acc)
22002290
end)
22012291
end
22022292

2203-
# Two tuples are fusible if they have no negations and differ in at most one element.
2204-
defp non_fusible_tuples?({_, elems1, []}, {_, elems2, []}) do
2205-
Enum.zip(elems1, elems2) |> Enum.count_until(fn {a, b} -> a != b end, 2) > 1
2206-
end
2207-
2208-
defp tuple_non_negated_fuse_pair({tag, elems1, []}, {_, elems2, []}) do
2209-
fused_elements =
2210-
Enum.zip_with(elems1, elems2, fn a, b -> if a == b, do: a, else: union(a, b) end)
2293+
defp tuple_fuse_with_first_fusible(tuple, []), do: [tuple]
22112294

2212-
{tag, fused_elements, []}
2295+
defp tuple_fuse_with_first_fusible(tuple, [candidate | rest]) do
2296+
if fused = maybe_optimize_tuple_union(tuple, candidate) do
2297+
# we found a fusible candidate, we're done
2298+
[fused | rest]
2299+
else
2300+
[candidate | tuple_fuse_with_first_fusible(tuple, rest)]
2301+
end
22132302
end
22142303

22152304
defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ defmodule Module.Types.DescrTest do
106106
|> equal?(list(term()))
107107
end
108108

109-
test "optimizations" do
109+
test "optimizations (maps)" do
110110
# The tests are checking the actual implementation, not the semantics.
111111
# This is why we are using structural comparisons.
112112
# It's fine to remove these if the implementation changes, but breaking
@@ -123,7 +123,7 @@ defmodule Module.Types.DescrTest do
123123
closed_map(a: integer(), b: atom())
124124
) == closed_map(a: union(float(), integer()), b: atom())
125125

126-
# Optimization two: we can tell that one map is a trivial subtype of the other:
126+
# Optimization two: we can tell that one map is a subtype of the other:
127127

128128
assert union(
129129
closed_map(a: term(), b: term()),
@@ -145,6 +145,36 @@ defmodule Module.Types.DescrTest do
145145
closed_map(a: float(), b: tuple([atom(), binary()]))
146146
) == closed_map(a: term(), b: tuple([term(), term()]))
147147
end
148+
149+
test "optimizations (tuples)" do
150+
# Optimization one: same tags, all but one key are structurally equal
151+
assert union(
152+
open_tuple([float(), atom()]),
153+
open_tuple([integer(), atom()])
154+
) == open_tuple([union(float(), integer()), atom()])
155+
156+
assert union(
157+
tuple([float(), atom()]),
158+
tuple([integer(), atom()])
159+
) == tuple([union(float(), integer()), atom()])
160+
161+
# Optimization two: we can tell that one tuple is a subtype of the other:
162+
163+
assert union(
164+
tuple([term(), term()]),
165+
tuple([float(), binary()])
166+
) == tuple([term(), term()])
167+
168+
assert union(
169+
open_tuple([term()]),
170+
tuple([float(), binary()])
171+
) == open_tuple([term()])
172+
173+
assert union(
174+
tuple([float(), binary()]),
175+
open_tuple([term()])
176+
) == open_tuple([term()])
177+
end
148178
end
149179

150180
describe "intersection" do
@@ -1402,8 +1432,7 @@ defmodule Module.Types.DescrTest do
14021432
|> to_quoted_string() ==
14031433
"""
14041434
dynamic(
1405-
:error or {%Decimal{sign: integer(), coef: :NaN or :inf, exp: integer()}, binary()} or
1406-
{%Decimal{sign: integer(), coef: :NaN or :inf or integer(), exp: integer()}, term()}
1435+
:error or {%Decimal{sign: integer(), coef: :NaN or :inf or integer(), exp: integer()}, term()}
14071436
)\
14081437
"""
14091438
end

0 commit comments

Comments
 (0)