Skip to content

Commit 30b59ed

Browse files
authored
Track types across case clauses (#15080)
1 parent ff13244 commit 30b59ed

File tree

9 files changed

+490
-210
lines changed

9 files changed

+490
-210
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,15 +2069,20 @@ defmodule Kernel do
20692069
end
20702070

20712071
defp build_boolean_check(operator, check, true_clause, false_clause) do
2072-
annotate_case(
2073-
[optimize_boolean: true, type_check: :expr],
2072+
bools =
20742073
quote do
2075-
case unquote(check) do
2076-
false -> unquote(false_clause)
2077-
true -> unquote(true_clause)
2078-
other -> :erlang.error({:badbool, unquote(operator), other})
2079-
end
2074+
false -> unquote(false_clause)
2075+
true -> unquote(true_clause)
20802076
end
2077+
2078+
error =
2079+
quote generated: true do
2080+
other -> :erlang.error({:badbool, unquote(operator), other})
2081+
end
2082+
2083+
annotate_case(
2084+
[optimize_boolean: true, type_check: :expr],
2085+
{:case, [], [check, [do: bools ++ error]]}
20812086
)
20822087
end
20832088

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ defmodule Module.Types.Descr do
478478
Computes the difference between two types.
479479
"""
480480
def difference(left, :term), do: keep_optional(left)
481+
def difference(left, none) when none == @none, do: left
481482

482483
def difference(left, right) do
483484
if gradual?(left) or gradual?(right) do

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

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ defmodule Module.Types.Expr do
295295

296296
def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, expr, stack, context) do
297297
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
298-
info = {:case, meta, case_type, case_expr}
299298

300299
added_meta =
301300
if Macro.quoted_literal?(case_expr) do
@@ -312,7 +311,7 @@ defmodule Module.Types.Expr do
312311
else
313312
clauses
314313
end
315-
|> of_clauses([case_type], expected, expr, info, stack, context, none())
314+
|> of_case_clauses(case_type, expected, meta, case_expr, expr, stack, context)
316315
|> dynamic_unless_static(stack)
317316
end
318317

@@ -726,6 +725,63 @@ defmodule Module.Types.Expr do
726725
end)
727726
end
728727

728+
defp of_case_clauses(clauses, domain, expected, case_meta, case_expr, expr, stack, original) do
729+
%{failed: failed?} = original
730+
731+
{result, _previous, context} =
732+
Enum.reduce(clauses, {none(), none(), original}, fn
733+
{:->, meta, [[clause] = head, body]}, {acc, previous, context} ->
734+
{failed?, context} = reset_failed(context, failed?)
735+
{patterns, guards} = extract_head(head)
736+
737+
clause_domain = difference(domain, previous)
738+
info = {:case, case_meta, clause_domain, case_expr, previous}
739+
740+
{trees, precise?, context} =
741+
Pattern.of_head(patterns, guards, [clause_domain], info, meta, stack, context)
742+
743+
# It failed, let's try to detect if it was due a previous or current clause.
744+
# The current clause will be easier to understand, so we prefer that
745+
{[{clause_tree, _, _}], precise?, context} =
746+
if context.failed and previous != none() do
747+
info = {:case, case_meta, domain, case_expr, nil}
748+
749+
case Pattern.of_head(patterns, guards, [domain], info, meta, stack, context) do
750+
{_, _, %{failed: true}} = result -> result
751+
_ -> {trees, precise?, context}
752+
end
753+
else
754+
{trees, precise?, context}
755+
end
756+
757+
{previous, context} =
758+
if context.failed do
759+
{previous, context}
760+
else
761+
clause_type = Pattern.of_pattern_tree(clause_tree, stack, context) |> upper_bound()
762+
763+
cond do
764+
stack.mode != :infer and previous != none() and subtype?(clause_type, previous) ->
765+
stack = %{stack | meta: meta}
766+
{previous, Pattern.badpattern_error(clause, 0, info, stack, context)}
767+
768+
precise? ->
769+
{union(previous, clause_type), context}
770+
771+
true ->
772+
{previous, context}
773+
end
774+
end
775+
776+
{result, context} = of_expr(body, expected, expr || body, stack, context)
777+
778+
{union(result, acc), previous,
779+
context |> set_failed(failed?) |> Of.reset_vars(original)}
780+
end)
781+
782+
{result, context}
783+
end
784+
729785
defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}}
730786
defp reset_failed(context, _), do: {false, context}
731787

@@ -773,6 +829,25 @@ defmodule Module.Types.Expr do
773829

774830
## Warning formatting
775831

832+
def format_diagnostic({:badclause, {:case, meta, type, expr}, [head], context}) do
833+
{expr, message} =
834+
if meta[:type_check] == :expr do
835+
{expr,
836+
"""
837+
the following conditional expression will always evaluate to #{to_quoted_string(type)}:
838+
839+
#{expr_to_string(expr) |> indent(4)}
840+
"""}
841+
else
842+
{head, "the following clause has no effect because a previous clause will always match\n"}
843+
end
844+
845+
%{
846+
details: %{typing_traces: collect_traces(expr, context)},
847+
message: message
848+
}
849+
end
850+
776851
def format_diagnostic({:badupdate, type, expr, context}) do
777852
{:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
778853
traces = collect_traces(map, context)

lib/elixir/lib/module/types/helpers.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ defmodule Module.Types.Helpers do
143143
"""
144144
def collect_traces(expr, %{vars: vars}) do
145145
{_, versions} =
146-
Macro.prewalk(expr, %{}, fn
147-
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
146+
Macro.prewalk(expr, %{}, fn node, versions ->
147+
with {var_name, meta, var_context} when is_atom(var_name) and is_atom(var_context) <- node,
148+
false <- String.starts_with?(Atom.to_string(var_name), "_") do
148149
version = meta[:version]
149150

150151
case vars do
@@ -160,9 +161,9 @@ defmodule Module.Types.Helpers do
160161
_ ->
161162
{:ok, versions}
162163
end
163-
164-
node, versions ->
165-
{node, versions}
164+
else
165+
_ -> {node, versions}
166+
end
166167
end)
167168

168169
versions

lib/elixir/lib/module/types/of.ex

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,13 @@ defmodule Module.Types.Of do
8686
or if we are doing a guard analysis or occurrence typing.
8787
Returns `true` if there was a refinement, `false` otherwise.
8888
"""
89-
def refine_body_var({_, meta, _}, type, expr, stack, context) do
90-
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
89+
def refine_body_var(var_or_version, type, expr, stack, context, allow_empty? \\ false)
90+
91+
def refine_body_var({_, meta, _}, type, expr, stack, context, allow_empty?) do
92+
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context, allow_empty?)
9193
end
9294

93-
def refine_body_var(version, type, expr, stack, context)
95+
def refine_body_var(version, type, expr, stack, context, allow_empty?)
9496
when is_integer(version) or is_reference(version) do
9597
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
9698

@@ -105,6 +107,15 @@ defmodule Module.Types.Of do
105107

106108
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
107109
case compatible_intersection(old_type, type) do
110+
{:error, _} when allow_empty? ->
111+
data = %{
112+
data
113+
| type: none(),
114+
off_traces: new_trace(expr, none(), stack, off_traces)
115+
}
116+
117+
{none(), %{context | vars: %{vars | version => data}}}
118+
108119
{:ok, new_type} when new_type != old_type ->
109120
data = %{
110121
data

0 commit comments

Comments
 (0)