Skip to content

Commit c35f651

Browse files
committed
Analyze conditional orelse and when
1 parent f1bbb2c commit c35f651

5 files changed

Lines changed: 163 additions & 63 deletions

File tree

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,22 @@ defmodule Module.Types.Of do
7474
Returns `true` if there was a refinement, `false` otherwise.
7575
"""
7676
def refine_body_var({_, meta, _}, type, expr, stack, context) do
77-
version = Keyword.fetch!(meta, :version)
77+
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
78+
end
79+
80+
def refine_body_var(version, type, expr, stack, context)
81+
when is_integer(version) or is_reference(version) do
7882
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
7983

84+
context =
85+
case context.conditional_vars do
86+
%{} = conditional_vars ->
87+
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
88+
89+
nil ->
90+
context
91+
end
92+
8093
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
8194
case compatible_intersection(old_type, type) do
8295
{:ok, new_type} when new_type != old_type ->
@@ -104,8 +117,11 @@ defmodule Module.Types.Of do
104117
use compatibility.
105118
"""
106119
def refine_head_var({_, meta, _}, type, expr, stack, context) do
107-
version = Keyword.fetch!(meta, :version)
120+
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
121+
end
108122

123+
def refine_head_var(version, type, expr, stack, context)
124+
when is_integer(version) or is_reference(version) do
109125
case context.vars do
110126
%{^version => %{errored: true}} ->
111127
{:ok, error_type(), context}

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

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,8 @@ defmodule Module.Types.Pattern do
420420
421421
They behave like guards, so we need to take into account their scope.
422422
"""
423-
def of_size(:match, arg, expr, stack, %{pattern_info: pattern_info} = context) do
424-
context = init_guard_info(context)
425-
{type, context} = of_guard(arg, integer(), expr, stack, context)
426-
{type, %{context | pattern_info: pattern_info}}
423+
def of_size(:match, arg, expr, stack, context) do
424+
of_guard(arg, integer(), expr, stack, context)
427425
end
428426

429427
def of_size(:guard, arg, expr, stack, context) do
@@ -755,34 +753,35 @@ defmodule Module.Types.Pattern do
755753
context
756754
end
757755

758-
defp of_guards(guards, stack, context) do
759-
# TODO: This match? is temporary until we support multiple guards
760-
single? = match?([_], guards)
761-
context = init_guard_info(context, single?)
762-
return = if single?, do: @atom_true, else: term()
756+
defp of_guards([guard], stack, context) do
757+
{type, context} = of_guard(guard, stack, context)
758+
maybe_badguard(type, guard, stack, context)
759+
end
763760

764-
context =
765-
Enum.reduce(guards, context, fn guard, context ->
766-
{type, context} = of_guard(guard, return, guard, stack, context)
761+
defp of_guards(guards, stack, context) do
762+
cond_context = %{context | conditional_vars: %{}}
767763

768-
if never_true?(type) do
769-
error = {:badguard, type, guard, context}
770-
error(__MODULE__, error, error_meta(guard, stack), stack, context)
771-
else
772-
context
773-
end
764+
{vars_conds, context} =
765+
Enum.map_reduce(guards, context, fn guard, context ->
766+
{type, %{vars: vars, conditional_vars: cond_vars}} = of_guard(guard, stack, cond_context)
767+
{{vars, cond_vars}, maybe_badguard(type, guard, stack, context)}
774768
end)
775769

776-
{_, context} = pop_guard_info(context)
777-
context
770+
when_expr = Enum.reduce(guards, {:_, [], []}, &{:when, [], [&2, &1]})
771+
of_cond_vars(vars_conds, when_expr, stack, context)
778772
end
779773

780-
defp init_guard_info(context, check_domain? \\ true) do
781-
%{context | pattern_info: {check_domain?}}
774+
defp maybe_badguard(type, guard, stack, context) do
775+
if never_true?(type) do
776+
error = {:badguard, type, guard, context}
777+
error(__MODULE__, error, error_meta(guard, stack), stack, context)
778+
else
779+
context
780+
end
782781
end
783782

784-
defp pop_guard_info(%{pattern_info: pattern_info} = context) do
785-
{pattern_info, %{context | pattern_info: nil}}
783+
defp of_guard(guard, stack, context) do
784+
of_guard(guard, @atom_true, guard, stack, context)
786785
end
787786

788787
# :atom
@@ -871,10 +870,7 @@ defmodule Module.Types.Pattern do
871870

872871
# var
873872
def of_guard(var, expected, expr, stack, context) when is_var(var) do
874-
case context.pattern_info do
875-
{true} -> Of.refine_body_var(var, expected, expr, stack, context)
876-
{false} -> {Of.var(var, context), context}
877-
end
873+
Of.refine_body_var(var, expected, expr, stack, context)
878874
end
879875

880876
defp of_remote(fun, _meta, [left, right], call, expected, stack, context)
@@ -888,29 +884,10 @@ defmodule Module.Types.Pattern do
888884
# For example, if the expected type is true for andalso, then it can
889885
# only be true if both clauses are executed, so we know the first
890886
# argument has to be true and the second has to be expected.
891-
{left_domain, right_domain, surely_rhs?} =
892-
if subtype?(expected, both_domain) do
893-
{both_domain, expected, true}
894-
else
895-
{boolean(), term(), false}
896-
end
897-
898-
{left_type, context} = of_guard(left, left_domain, call, stack, context)
899-
900-
{right_type, context} =
901-
if surely_rhs? do
902-
of_guard(right, right_domain, call, stack, context)
903-
else
904-
%{pattern_info: pattern_info} = context
905-
context = %{context | pattern_info: {false}}
906-
{type, context} = of_guard(right, right_domain, call, stack, context)
907-
{type, %{context | pattern_info: pattern_info}}
908-
end
909-
910-
if compatible?(left_type, abort_domain) do
911-
{union(abort_domain, right_type), context}
887+
if subtype?(expected, both_domain) do
888+
of_logical_both(left, both_domain, right, expected, abort_domain, call, stack, context)
912889
else
913-
{right_type, context}
890+
of_logical_cond(left, right, expected, abort_domain, call, stack, context)
914891
end
915892
end
916893

@@ -924,6 +901,58 @@ defmodule Module.Types.Pattern do
924901
Apply.remote_apply(info, :erlang, fun, args_types, call, stack, context)
925902
end
926903

904+
defp of_logical_both(left, left_domain, right, right_domain, to_abort, call, stack, context) do
905+
{left_type, context} = of_guard(left, left_domain, call, stack, context)
906+
{right_type, context} = of_guard(right, right_domain, call, stack, context)
907+
908+
if disjoint?(left_type, to_abort) do
909+
{right_type, context}
910+
else
911+
{union(to_abort, right_type), context}
912+
end
913+
end
914+
915+
defp of_logical_cond(left, right, expected, to_abort, call, stack, context) do
916+
cond_context = %{context | conditional_vars: %{}}
917+
918+
# First we do pass to find the surely types, which are stored directly in the context
919+
{_left_type, context} = of_guard(left, boolean(), call, stack, context)
920+
921+
# Now we find the conditional ones
922+
{left_type, left_context} = of_guard(left, expected, call, stack, cond_context)
923+
{right_type, right_context} = of_guard(right, expected, call, stack, cond_context)
924+
925+
%{vars: left_vars, conditional_vars: left_cond} = left_context
926+
%{vars: right_vars, conditional_vars: right_cond} = right_context
927+
vars_conds = [{left_vars, left_cond}, {right_vars, right_cond}]
928+
context = of_cond_vars(vars_conds, call, stack, context)
929+
930+
if disjoint?(left_type, to_abort) do
931+
{right_type, context}
932+
else
933+
{union(to_abort, right_type), context}
934+
end
935+
end
936+
937+
defp of_cond_vars([{vars, cond} | vars_conds], expr, stack, context) do
938+
Enum.reduce(Map.keys(cond), context, fn version, context ->
939+
if Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
940+
%{^version => %{type: type}} = vars
941+
942+
type =
943+
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
944+
%{^version => %{type: type}} = vars
945+
union(acc, type)
946+
end)
947+
948+
{_, context} = Of.refine_body_var(version, type, expr, stack, context)
949+
context
950+
else
951+
context
952+
end
953+
end)
954+
end
955+
927956
## Helpers
928957

929958
def format_diagnostic({:badguard, type, expr, context}) do

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,67 @@ defmodule Module.Types.PatternTest do
473473
"""
474474
end
475475

476+
test "when checks" do
477+
assert typecheck!([x], is_binary(x) when is_atom(x), x) == dynamic(union(binary(), atom()))
478+
479+
assert typecheck!([x], is_binary(x) when map_size(x) >= 0, x) ==
480+
dynamic(union(binary(), open_map()))
481+
482+
assert typecheck!([x], tuple_size(x) >= 0 when map_size(x) >= 0, x) ==
483+
dynamic(union(tuple(), open_map()))
484+
485+
assert typecheck!([x, y], is_binary(x) when is_atom(y), {x, y}) ==
486+
dynamic(tuple([term(), term()]))
487+
end
488+
489+
test "conditional checks" do
490+
assert typecheck!([x], is_binary(x) or is_atom(x), x) == dynamic(union(binary(), atom()))
491+
492+
assert typecheck!([x], is_binary(x) or map_size(x) >= 0, x) ==
493+
dynamic(union(binary(), open_map()))
494+
495+
assert typecheck!([x, y], is_binary(x) or is_atom(y), {x, y}) ==
496+
dynamic(tuple([term(), term()]))
497+
498+
assert typecheck!([x], not (is_pid(x) and is_atom(x)), x) |> equal?(dynamic(term()))
499+
500+
assert typecheck!([x, y], not (is_pid(x) and is_atom(y)), {x, y}) ==
501+
dynamic(tuple([term(), term()]))
502+
503+
# Error
504+
assert typeerror!([x], is_pid(x) and is_atom(x), x) == ~l"""
505+
this guard will never succeed:
506+
507+
is_pid(x) and is_atom(x)
508+
509+
because it returns type:
510+
511+
false
512+
513+
where "x" was given the type:
514+
515+
# type: pid()
516+
# from: types_test.ex:LINE
517+
is_pid(x)
518+
"""
519+
520+
assert typeerror!([x], (is_binary(x) or is_atom(x)) and is_pid(x), x) == ~l"""
521+
this guard will never succeed:
522+
523+
(is_binary(x) or is_atom(x)) and is_pid(x)
524+
525+
because it returns type:
526+
527+
false
528+
529+
where "x" was given the type:
530+
531+
# type: dynamic(atom() or binary())
532+
# from: types_test.ex:LINE
533+
is_binary(x) or is_atom(x)
534+
"""
535+
end
536+
476537
test "domain checks" do
477538
# Regular domain check
478539
assert typecheck!([x], length(x) == 3, x) == dynamic(list(term()))

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,13 @@ defmodule TypeHelper do
136136

137137
{ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(env), env)
138138
{:fn, _, [{:->, _, [[{:when, _, args}], body]}]} = ast
139-
{patterns, guards} = Enum.split(args, -1)
140-
{patterns, guards, body}
139+
{patterns, [guards]} = Enum.split(args, -1)
140+
{patterns, flatten_when(guards), body}
141141
end
142142

143+
defp flatten_when({:when, _meta, [left, right]}), do: [left | flatten_when(right)]
144+
defp flatten_when(other), do: [other]
145+
143146
defp new_stack(mode) do
144147
cache =
145148
if mode == :infer do

lib/ex_unit/test/ex_unit/assertions_test.exs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,15 +1034,6 @@ defmodule ExUnit.AssertionsTest do
10341034
"This should raise an error" = error.message
10351035
end
10361036

1037-
test "flunk with wrong argument type" do
1038-
flunk(["flunk takes a binary, not a list"])
1039-
flunk("This should never be tested")
1040-
rescue
1041-
error ->
1042-
"no function clause matching in ExUnit.Assertions.flunk/1" =
1043-
FunctionClauseError.message(error)
1044-
end
1045-
10461037
test "AssertionError.message/1 is nicely formatted" do
10471038
assert :a = :b
10481039
rescue

0 commit comments

Comments
 (0)