Skip to content

Commit 6fd161d

Browse files
committed
Optimize previous handling by using nested unions instead of nested differences
Partially addresses #15129.
1 parent 576f123 commit 6fd161d

5 files changed

Lines changed: 112 additions & 62 deletions

File tree

lib/elixir/lib/module/types.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ defmodule Module.Types do
322322
base_info = {:def, kind, fun, expected}
323323

324324
{_, _, _, mapping, clauses_types, clauses_context} =
325-
Enum.reduce(clauses, {0, 0, [], [], [], context}, fn
325+
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], context}, fn
326326
{meta, args, guards, body}, {index, total, previous, mapping, inferred, acc_context} ->
327327
fresh_context = fresh_context(acc_context)
328328
info = {base_info, args, guards}

lib/elixir/lib/module/types/expr.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ defmodule Module.Types.Expr do
728728
%{failed: failed?} = original
729729

730730
{result, _previous, context} =
731-
Enum.reduce(clauses, {acc, [], original}, fn
731+
Enum.reduce(clauses, {acc, Pattern.init_previous(), original}, fn
732732
{:->, meta, [head, body]}, {acc, previous, context} ->
733733
{failed?, context} = reset_failed(context, failed?)
734734
{patterns, guards} = extract_head(head)

lib/elixir/lib/module/types/pattern.ex

Lines changed: 66 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,63 @@ defmodule Module.Types.Pattern do
88
alias Module.Types.{Apply, Of}
99
import Module.Types.{Helpers, Descr}
1010

11+
@doc """
12+
Returns the initial value for previous clause information.
13+
"""
14+
def init_previous do
15+
{[], none()}
16+
end
17+
18+
defp empty_previous?({list, _descr}), do: list == []
19+
20+
defp previous_subtype?(_, {[], _}), do: false
21+
defp previous_subtype?([], _), do: true
22+
defp previous_subtype?([type], {_, descr}), do: subtype?(type, descr)
23+
defp previous_subtype?(args, {_, descr}), do: subtype?(args_to_domain(args), descr)
24+
25+
defp concat_previous([], previous),
26+
do: previous
27+
28+
defp concat_previous([type], {list, descr}),
29+
do: {[[type] | list], union(type, descr)}
30+
31+
defp concat_previous(types, {list, descr}),
32+
do: {[types | list], union(args_to_domain(types), descr)}
33+
34+
defp of_pattern_previous(types, {[], _}, _trees, _pattern_info, _tag, _stack, _context) do
35+
{:ok, types}
36+
end
37+
38+
defp of_pattern_previous(types, {_, descr}, trees, pattern_info, tag, stack, context) do
39+
types =
40+
case types do
41+
[type] ->
42+
[difference(type, descr)]
43+
44+
[_ | _] ->
45+
types
46+
|> args_to_domain()
47+
|> difference(descr)
48+
|> domain_to_flat_args(types)
49+
end
50+
51+
if index = Enum.find_index(types, &empty?/1) do
52+
{_, _, pattern} = Enum.fetch!(trees, index)
53+
context = badpattern_error(pattern, index, tag, stack, context)
54+
{:error, error_vars(pattern_info, context)}
55+
else
56+
{:ok, types}
57+
end
58+
end
59+
60+
defp previous_to_string({previous, _}) do
61+
Enum.map_join(previous, "\n ", fn types ->
62+
types
63+
|> Enum.map_join(", ", &to_quoted_string/1)
64+
|> indent(4)
65+
end)
66+
end
67+
1168
@doc """
1269
Refine the dependencies of variables represented by version.
1370
"""
@@ -129,16 +186,17 @@ defmodule Module.Types.Pattern do
129186
Every step we deduct previous clauses from the current ones and,
130187
in case of failures, we try to break down the root cause.
131188
"""
132-
def of_head(patterns, guards, expected, previous, tag, meta, stack, original_context) do
189+
def of_head(patterns, guards, expected, previous, tag, meta, stack, original) do
133190
stack = %{stack | meta: meta}
134191

135192
{trees, precise?, context} =
136-
of_precise_head(patterns, guards, expected, previous, tag, stack, original_context)
193+
of_precise_head(patterns, guards, expected, previous, tag, stack, original)
137194

138-
if context.failed and previous != [] and Keyword.get(meta, :generated, false) != true do
195+
if context.failed and not empty_previous?(previous) and
196+
Keyword.get(meta, :generated, false) != true do
139197
# If it failed, let's try to break it down to a better error message.
140198
# First we check if it fails without previous, if it doesn't, check if it is redundant.
141-
case of_precise_head(patterns, guards, expected, [], tag, stack, original_context) do
199+
case of_precise_head(patterns, guards, expected, init_previous(), tag, stack, original) do
142200
{other_trees, _, %{failed: true} = other_context} ->
143201
{other_trees, previous, other_context}
144202

@@ -150,7 +208,7 @@ defmodule Module.Types.Pattern do
150208
|> upper_bound()
151209
end)
152210

153-
if args_subtype?(args_types, previous) do
211+
if previous_subtype?(args_types, previous) do
154212
warning = {:redundant, tag, expected, args_types, previous, other_context}
155213
{other_trees, previous, warn(__MODULE__, warning, meta, stack, other_context)}
156214
else
@@ -166,12 +224,12 @@ defmodule Module.Types.Pattern do
166224
end)
167225

168226
cond do
169-
args_subtype?(args_types, previous) ->
227+
previous_subtype?(args_types, previous) ->
170228
warning = {:redundant, tag, expected, args_types, previous, context}
171229
{trees, previous, warn(__MODULE__, warning, meta, stack, context)}
172230

173231
precise? ->
174-
{trees, [args_types | previous], context}
232+
{trees, concat_previous(args_types, previous), context}
175233

176234
true ->
177235
{trees, previous, context}
@@ -192,22 +250,6 @@ defmodule Module.Types.Pattern do
192250
end
193251
end
194252

195-
defp args_subtype?(_, []),
196-
do: false
197-
198-
defp args_subtype?([], _),
199-
do: true
200-
201-
defp args_subtype?([type], previous),
202-
do: subtype?(type, Enum.reduce(previous, none(), &union(&2, hd(&1))))
203-
204-
defp args_subtype?(args, previous) do
205-
subtype?(
206-
args_to_domain(args),
207-
Enum.reduce(previous, none(), &union(&2, args_to_domain(&1)))
208-
)
209-
end
210-
211253
@doc """
212254
Computes the domain from the pattern tree and expected types.
213255
@@ -335,31 +377,6 @@ defmodule Module.Types.Pattern do
335377
{:ok, Enum.reverse(acc)}
336378
end
337379

338-
defp of_pattern_previous(types, [], _trees, _pattern_info, _tag, _stack, _context) do
339-
{:ok, types}
340-
end
341-
342-
defp of_pattern_previous(types, previous, trees, pattern_info, tag, stack, context) do
343-
types =
344-
case types do
345-
[type] ->
346-
[Enum.reduce(previous, type, &difference(&2, hd(&1)))]
347-
348-
[_ | _] ->
349-
previous
350-
|> Enum.reduce(args_to_domain(types), &difference(&2, args_to_domain(&1)))
351-
|> domain_to_flat_args(types)
352-
end
353-
354-
if index = Enum.find_index(types, &empty?/1) do
355-
{_, _, pattern} = Enum.fetch!(trees, index)
356-
context = badpattern_error(pattern, index, tag, stack, context)
357-
{:error, error_vars(pattern_info, context)}
358-
else
359-
{:ok, types}
360-
end
361-
end
362-
363380
defp of_pattern_refine(types, pattern_info, tag, stack, context) do
364381
pattern_info
365382
|> Enum.reverse()
@@ -1490,7 +1507,7 @@ defmodule Module.Types.Pattern do
14901507
else
14911508
_ ->
14921509
with {_op, _meta, expr, type} <- info,
1493-
true <- args_subtype?(expected, previous) do
1510+
true <- previous_subtype?(expected, previous) do
14941511
"""
14951512
the following clause cannot match because the previous clauses already matched all possible values:
14961513
@@ -1690,12 +1707,4 @@ defmodule Module.Types.Pattern do
16901707
|> Enum.map_join(", ", &to_quoted_string/1)
16911708
|> indent(4)
16921709
end
1693-
1694-
defp previous_to_string(previous) do
1695-
Enum.map_join(previous, "\n ", fn types ->
1696-
types
1697-
|> Enum.map_join(", ", &to_quoted_string/1)
1698-
|> indent(4)
1699-
end)
1700-
end
17011710
end

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,43 @@ defmodule Module.Types.IntegrationTest do
850850
end
851851

852852
describe "performance regressions" do
853+
test "redundant clause checking on structs with many fields" do
854+
files = %{
855+
"big_struct.ex" => """
856+
defmodule BigStruct do
857+
defstruct [:f1, :f2, :f3, :f4, :f5, :f6, :f7, :f8, :f9, :f10,
858+
:f11, :f12, :f13, :f14, :f15, :f16, :f17, :f18, :f19, :f20,
859+
:f21, :f22, :f23, :f24, :f25, :f26, :f27, :f28, :f29, :f30,
860+
:f31, :f32, :f33, :value, :schema]
861+
def cast(%__MODULE__{value: nil, schema: %{k1: _}}), do: :ok
862+
def cast(%__MODULE__{value: nil, schema: %{k2: _}}), do: :ok
863+
def cast(%__MODULE__{value: nil, schema: %{k3: _}}), do: :ok
864+
def cast(%__MODULE__{value: nil, schema: %{k4: _}}), do: :ok
865+
def cast(%__MODULE__{value: nil, schema: %{k5: _}}), do: :ok
866+
def cast(%__MODULE__{value: nil, schema: %{k6: _}}), do: :ok
867+
def cast(%__MODULE__{value: nil, schema: %{k7: _}}), do: :ok
868+
def cast(%__MODULE__{value: nil, schema: %{k8: _}}), do: :ok
869+
def cast(%__MODULE__{value: nil, schema: %{k9: _}}), do: :ok
870+
def cast(%__MODULE__{value: nil, schema: %{k10: _}}), do: :ok
871+
def cast(%__MODULE__{value: nil, schema: %{k11: _}}), do: :ok
872+
def cast(%__MODULE__{value: nil, schema: %{k12: _}}), do: :ok
873+
def cast(%__MODULE__{value: nil, schema: %{k13: _}}), do: :ok
874+
def cast(%__MODULE__{value: nil, schema: %{k14: _}}), do: :ok
875+
def cast(%__MODULE__{value: nil, schema: %{k15: _}}), do: :ok
876+
def cast(%__MODULE__{value: nil, schema: %{k16: _}}), do: :ok
877+
def cast(%__MODULE__{value: nil, schema: %{k17: _}}), do: :ok
878+
def cast(%__MODULE__{value: nil, schema: %{k18: _}}), do: :ok
879+
def cast(%__MODULE__{value: nil, schema: %{k19: _}}), do: :ok
880+
def cast(%__MODULE__{value: nil, schema: %{k20: _}}), do: :ok
881+
# This different clause avoids optimizations from kick in many cases
882+
def cast(%__MODULE__{schema: %{target_key: x}}), do: x
883+
end
884+
"""
885+
}
886+
887+
assert_no_warnings(files)
888+
end
889+
853890
test "unions and intersections of open maps" do
854891
files = %{
855892
"large_head.ex" => """

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,19 +153,23 @@ defmodule TypeHelper do
153153
def __precise__?(patterns, guards) do
154154
stack = new_stack(:static)
155155
expected = Enum.map(patterns, fn _ -> Descr.dynamic() end)
156+
previous = Pattern.init_previous()
157+
tag = {:fn, patterns}
156158

157-
{_trees, previous, _context} =
158-
Pattern.of_head(patterns, guards, expected, [], {:fn, patterns}, [], stack, new_context())
159+
{_trees, {previous, _}, _context} =
160+
Pattern.of_head(patterns, guards, expected, previous, tag, [], stack, new_context())
159161

160162
previous != []
161163
end
162164

163165
def __typecheck__(mode, patterns, guards, body) do
164166
stack = new_stack(mode)
165167
expected = Enum.map(patterns, fn _ -> Descr.dynamic() end)
168+
previous = Pattern.init_previous()
169+
tag = {:fn, patterns}
166170

167171
{_trees, _precise?, context} =
168-
Pattern.of_head(patterns, guards, expected, [], {:fn, patterns}, [], stack, new_context())
172+
Pattern.of_head(patterns, guards, expected, previous, tag, [], stack, new_context())
169173

170174
Expr.of_expr(body, Descr.term(), :ok, stack, context)
171175
end

0 commit comments

Comments
 (0)