Skip to content

Commit 1557d1d

Browse files
committed
Typecheck cond
1 parent 5d626cb commit 1557d1d

File tree

5 files changed

+200
-15
lines changed

5 files changed

+200
-15
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,48 @@ defmodule Module.Types.Descr do
631631
# an empty list of atoms. It is simplified to `0` in set operations, and the key
632632
# is removed from the map.
633633

634+
@false_or_nil_atoms [
635+
:sets.from_list([false, nil], version: 2),
636+
:sets.from_list([nil], version: 2),
637+
:sets.from_list([false], version: 2)
638+
]
639+
640+
@doc """
641+
Compute the truthness of an element.
642+
643+
It is either :undefined, :always_true, or :always_false.
644+
"""
645+
def truthness(:term), do: :undefined
646+
647+
def truthness(%{} = descr) do
648+
descr = Map.get(descr, :dynamic, descr)
649+
650+
case descr do
651+
:term ->
652+
:undefined
653+
654+
%{atom: {:union, set}}
655+
when map_size(descr) == 1 and set in @false_or_nil_atoms ->
656+
:always_false
657+
658+
%{atom: {:union, set}}
659+
when map_size(descr) == 1 and not is_map_key(set, false) and not is_map_key(set, nil) ->
660+
:always_true
661+
662+
%{atom: {:negation, %{nil => _, false => _}}} ->
663+
:always_true
664+
665+
%{atom: _} ->
666+
:undefined
667+
668+
_ when map_size(descr) == 0 ->
669+
:undefined
670+
671+
_ ->
672+
:always_true
673+
end
674+
end
675+
634676
@doc """
635677
Optimized version of `not empty?(intersection(atom([atom]), type))`.
636678
"""

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

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,34 @@ defmodule Module.Types.Expr do
178178
of_expr(post, stack, context)
179179
end
180180

181-
# TODO: cond do pat -> expr end
182181
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
183-
context =
184-
Enum.reduce(clauses, context, fn {:->, _meta, [head, body]}, context ->
185-
{_, context} = of_expr(head, stack, context)
186-
{_, context} = of_expr(body, stack, context)
187-
context
188-
end)
182+
clauses
183+
|> reduce_non_empty({none(), context}, fn
184+
{:->, meta, [[head], body]}, {acc, context}, last? ->
185+
{head_type, context} = of_expr(head, stack, context)
189186

190-
{dynamic(), context}
187+
context =
188+
if stack.mode == :infer do
189+
context
190+
else
191+
case truthness(head_type) do
192+
:always_true when not last? ->
193+
warning = {:badcond, "always match", head_type, head, context}
194+
warn(__MODULE__, warning, meta, stack, context)
195+
196+
:always_false ->
197+
warning = {:badcond, "never match", head_type, head, context}
198+
warn(__MODULE__, warning, meta, stack, context)
199+
200+
_ ->
201+
context
202+
end
203+
end
204+
205+
{body_type, context} = of_expr(body, stack, context)
206+
{union(body_type, acc), context}
207+
end)
208+
|> dynamic_unless_static(stack)
191209
end
192210

193211
# TODO: case expr do pat -> expr end
@@ -459,6 +477,15 @@ defmodule Module.Types.Expr do
459477
{Enum.reduce(returns, &union/2), context}
460478
end
461479

480+
defp reduce_non_empty([last], acc, fun),
481+
do: fun.(last, acc, true)
482+
483+
defp reduce_non_empty([head | tail], acc, fun),
484+
do: reduce_non_empty(tail, fun.(head, acc, false), fun)
485+
486+
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
487+
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
488+
462489
defp of_clauses(clauses, stack, context) do
463490
Enum.reduce(clauses, context, fn {:->, meta, [head, body]}, context ->
464491
{patterns, guards} = extract_head(head)
@@ -480,13 +507,8 @@ defmodule Module.Types.Expr do
480507
{other, []}
481508
end
482509

483-
defp flatten_when({:when, _meta, [left, right]}) do
484-
[left | flatten_when(right)]
485-
end
486-
487-
defp flatten_when(other) do
488-
[other]
489-
end
510+
defp flatten_when({:when, _meta, [left, right]}), do: [left | flatten_when(right)]
511+
defp flatten_when(other), do: [other]
490512

491513
defp of_expr_context(expr, stack, context) do
492514
{_type, context} = of_expr(expr, stack, context)
@@ -582,4 +604,25 @@ defmodule Module.Types.Expr do
582604
])
583605
}
584606
end
607+
608+
def format_diagnostic({:badcond, explain, type, expr, context}) do
609+
traces = collect_traces(expr, context)
610+
611+
%{
612+
details: %{typing_traces: traces},
613+
message:
614+
IO.iodata_to_binary([
615+
"""
616+
this clause in cond will #{explain}:
617+
618+
#{expr_to_string(expr) |> indent(4)}
619+
620+
since it has type:
621+
622+
#{to_quoted_string(type) |> indent(4)}
623+
""",
624+
format_traces(traces)
625+
])
626+
}
627+
end
585628
end

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,9 @@ defmodule Module.Types.Of do
582582
when name in [:>=, :"=<", :>, :<, :min, :max] do
583583
context =
584584
cond do
585+
stack.mode == :infer ->
586+
context
587+
585588
match?({false, _}, map_fetch(left, :__struct__)) or
586589
match?({false, _}, map_fetch(right, :__struct__)) ->
587590
warning = {:struct_comparison, expr, context}
@@ -609,6 +612,9 @@ defmodule Module.Types.Of do
609612
when name in [:==, :"/=", :"=:=", :"=/="] do
610613
context =
611614
cond do
615+
stack.mode == :infer ->
616+
context
617+
612618
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
613619
context
614620

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,24 @@ defmodule Module.Types.DescrTest do
594594
assert fun_fetch(dynamic(), 1) == :ok
595595
end
596596

597+
test "truthness" do
598+
for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do
599+
assert truthness(type) == :undefined
600+
assert truthness(dynamic(type)) == :undefined
601+
end
602+
603+
for type <- [atom([false]), atom([nil]), atom([nil, false]), atom([false, nil])] do
604+
assert truthness(type) == :always_false
605+
assert truthness(dynamic(type)) == :always_false
606+
end
607+
608+
for type <-
609+
[negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do
610+
assert truthness(type) == :always_true
611+
assert truthness(dynamic(type)) == :always_true
612+
end
613+
end
614+
597615
test "atom_fetch" do
598616
assert atom_fetch(term()) == :error
599617
assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,82 @@ defmodule Module.Types.ExprTest do
888888
end
889889
end
890890

891+
describe "cond" do
892+
test "always true" do
893+
assert typecheck!(
894+
cond do
895+
true -> :ok
896+
end
897+
) == atom([:ok])
898+
899+
assert typecheck!(
900+
[x, y],
901+
cond do
902+
y -> :y
903+
x -> :x
904+
end
905+
) == atom([:x, :y])
906+
907+
assert typedyn!(
908+
[x, y],
909+
cond do
910+
y -> :y
911+
x -> :x
912+
end
913+
) == dynamic(atom([:x, :y]))
914+
915+
assert typewarn!(
916+
[x, y = {:foo, :bar}],
917+
cond do
918+
y -> :y
919+
x -> :x
920+
end
921+
) ==
922+
{atom([:x, :y]),
923+
~l"""
924+
this clause in cond will always match:
925+
926+
y
927+
928+
since it has type:
929+
930+
dynamic({:foo, :bar})
931+
932+
where "y" was given the type:
933+
934+
# type: dynamic({:foo, :bar})
935+
# from: types_test.ex:LINE-7
936+
y = {:foo, :bar}
937+
"""}
938+
end
939+
940+
test "always false" do
941+
assert typewarn!(
942+
[x, y = false],
943+
cond do
944+
y -> :y
945+
x -> :x
946+
end
947+
) ==
948+
{atom([:x, :y]),
949+
~l"""
950+
this clause in cond will never match:
951+
952+
y
953+
954+
since it has type:
955+
956+
dynamic(false)
957+
958+
where "y" was given the type:
959+
960+
# type: dynamic(false)
961+
# from: types_test.ex:LINE-7
962+
y = false
963+
"""}
964+
end
965+
end
966+
891967
describe "comprehensions" do
892968
test "binary generators" do
893969
assert typeerror!([<<x>>], for(<<y <- x>>, do: y)) ==

0 commit comments

Comments
 (0)