Skip to content

Commit b4915b5

Browse files
committed
Properly track or and list precision
1 parent 0ef544c commit b4915b5

2 files changed

Lines changed: 122 additions & 51 deletions

File tree

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

Lines changed: 118 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -784,18 +784,40 @@ defmodule Module.Types.Pattern do
784784
of_pattern(suffix, path, stack, context)
785785
end
786786

787+
# [prefix | suffix]
788+
defp of_list([prefix], suffix, path, stack, context) when is_var(prefix) and is_var(suffix) do
789+
{suffix_type, suffix_precise?, context} =
790+
of_pattern(suffix, [:tail | path], stack, context)
791+
792+
context = annotate_list_subpattern(suffix, context)
793+
794+
{prefix_type, prefix_precise?, context} =
795+
of_subpattern(prefix, [:head | path], stack, context)
796+
797+
context = annotate_list_subpattern(prefix, context)
798+
799+
type =
800+
if is_descr(prefix_type) and is_descr(suffix_type) do
801+
non_empty_list(prefix_type, suffix_type)
802+
else
803+
{:non_empty_list, [prefix_type], suffix_type}
804+
end
805+
806+
{type, prefix_precise? and suffix_precise?, context}
807+
end
808+
787809
# [prefix1, prefix2, prefix3], [prefix1, prefix2 | suffix]
788810
defp of_list(prefix, suffix, path, stack, context) do
789-
{suffix_type, tail_precise?, context} = of_pattern(suffix, [:tail | path], stack, context)
811+
{suffix_type, _precise?, context} = of_pattern(suffix, [:tail | path], stack, context)
790812

791-
{head_precise?, static, dynamic, context} =
792-
Enum.reduce(prefix, {true, [], [], context}, fn arg, {_, static, dynamic, context} ->
793-
{type, precise?, context} = of_subpattern(arg, [:head | path], stack, context)
813+
{static, dynamic, context} =
814+
Enum.reduce(prefix, {[], [], context}, fn arg, {static, dynamic, context} ->
815+
{type, _precise?, context} = of_subpattern(arg, [:head | path], stack, context)
794816

795817
if is_descr(type) do
796-
{precise?, [type | static], dynamic, context}
818+
{[type | static], dynamic, context}
797819
else
798-
{precise?, static, [type | dynamic], context}
820+
{static, [type | dynamic], context}
799821
end
800822
end)
801823

@@ -811,11 +833,19 @@ defmodule Module.Types.Pattern do
811833
{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type}
812834
end
813835

814-
precise? =
815-
head_precise? and tail_precise? and is_var(suffix) and
816-
match?([head] when is_var(head), prefix)
836+
{type, false, context}
837+
end
817838

818-
{type, precise?, context}
839+
defp list_subpattern?(version, context) do
840+
is_map_key(context.subpatterns, {:list, version})
841+
end
842+
843+
defp annotate_list_subpattern({name, meta, _}, context) do
844+
if name != :_ do
845+
put_in(context.subpatterns[{:list, Keyword.fetch!(meta, :version)}], true)
846+
else
847+
context
848+
end
819849
end
820850

821851
# These cases don't need to store information because they have no intersection
@@ -861,45 +891,43 @@ defmodule Module.Types.Pattern do
861891
{true, of_changed(changed, stack, context)}
862892
end
863893

864-
defp of_guards([guard], changed, vars, stack, context) do
865-
context = init_pattern_info(context, {false, nil, true, vars, Map.from_keys(changed, [])})
894+
defp of_guards(guards, changed, vars, stack, context) do
895+
context =
896+
init_pattern_info(context, %{
897+
allow_empty?: false,
898+
parent_version: nil,
899+
precise?: true,
900+
vars: vars,
901+
changed: Map.from_keys(changed, [])
902+
})
903+
904+
{guard_precise?, context} = of_guards(guards, stack, context)
905+
{%{precise?: precise?, changed: changed}, context} = pop_pattern_info(context)
906+
{precise? and guard_precise?, of_changed(Map.keys(changed), stack, context)}
907+
end
908+
909+
defp of_guards([guard], stack, context) do
866910
{type, context} = of_guard(guard, stack, context)
867-
{precise?, context} = maybe_badguard(type, guard, stack, context)
868-
{{_, _, pattern_vars?, _, changed_map}, context} = pop_pattern_info(context)
869-
{precise? and pattern_vars?, of_changed(Map.keys(changed_map), stack, context)}
911+
maybe_badguard(type, guard, stack, context)
870912
end
871913

872-
defp of_guards(guards, changed, vars, stack, context) do
873-
context = init_pattern_info(context, {false, nil, true, vars, Map.from_keys(changed, [])})
914+
defp of_guards(guards, stack, context) do
874915
expr = Enum.reduce(guards, {:_, [], []}, &{:when, [], [&2, &1]})
875916

876-
{precise?, context} =
877-
Of.with_conditional_vars(guards, true, expr, stack, context, fn guard, precise?, context ->
878-
{type, context} = of_guard(guard, stack, context)
879-
{guard_precise?, context} = maybe_badguard(type, guard, stack, context)
880-
{guard_precise? and precise?, context}
881-
end)
882-
883-
{{_, _, pattern_vars?, _, changed_map}, context} = pop_pattern_info(context)
884-
{precise? and pattern_vars?, of_changed(Map.keys(changed_map), stack, context)}
917+
Of.with_conditional_vars(guards, true, expr, stack, context, fn guard, precise?, context ->
918+
{type, context} = of_guard(guard, stack, context)
919+
{guard_precise?, context} = maybe_badguard(type, guard, stack, context)
920+
{guard_precise? and precise?, context}
921+
end)
885922
end
886923

887-
defp update_parent_version(
888-
parent_version,
889-
%{pattern_info: {fail_mode?, old_version, pattern_vars?, vars, changed_map}} = context
890-
) do
891-
{old_version,
892-
%{context | pattern_info: {fail_mode?, parent_version, pattern_vars?, vars, changed_map}}}
924+
defp update_parent_version(parent_version, %{pattern_info: pattern_info} = context) do
925+
{pattern_info.parent_version,
926+
%{context | pattern_info: %{pattern_info | parent_version: parent_version}}}
893927
end
894928

895-
defp enable_conditional_mode(
896-
%{pattern_info: {_fail_mode?, version, pattern_vars?, vars, changed_map}} = context
897-
) do
898-
%{
899-
context
900-
| pattern_info: {true, version, pattern_vars?, vars, changed_map},
901-
conditional_vars: %{}
902-
}
929+
defp enable_conditional_mode(%{pattern_info: pattern_info} = context) do
930+
%{context | pattern_info: %{pattern_info | allow_empty?: true}, conditional_vars: %{}}
903931
end
904932

905933
defp maybe_badguard(type, guard, stack, context) do
@@ -1011,18 +1039,26 @@ defmodule Module.Types.Pattern do
10111039
# and also when vars change, so we need to deal with all possibilities
10121040
# for pattern_info.
10131041
case context.pattern_info do
1014-
{fail_mode?, dep_version, pattern_vars?, vars, changed} when is_map(changed) ->
1015-
pattern_vars? = pattern_vars? and not is_map_key(vars, version)
1042+
%{
1043+
allow_empty?: allow_empty?,
1044+
precise?: precise?,
1045+
vars: vars,
1046+
parent_version: parent_version,
1047+
changed: changed
1048+
} = pattern_info ->
1049+
precise? =
1050+
precise? and not is_map_key(vars, version) and not list_subpattern?(version, context)
1051+
10161052
changed = Map.put(changed, version, [])
1017-
pattern_info = {fail_mode?, dep_version, pattern_vars?, vars, changed}
1053+
pattern_info = %{pattern_info | precise?: precise?, changed: changed}
10181054
context = %{context | pattern_info: pattern_info}
10191055

10201056
context =
1021-
if dep_version != nil,
1022-
do: Of.track_var(version, [dep_version], [], context),
1057+
if parent_version != nil,
1058+
do: Of.track_var(version, [parent_version], [], context),
10231059
else: context
10241060

1025-
Of.refine_body_var(version, expected, expr, stack, context, fail_mode?)
1061+
Of.refine_body_var(version, expected, expr, stack, context, allow_empty?)
10261062

10271063
list when is_list(list) ->
10281064
node = path_node(expected, var, expr, [])
@@ -1054,12 +1090,14 @@ defmodule Module.Types.Pattern do
10541090
of_guard(other_side, term(), call, stack, context)
10551091
end
10561092

1093+
@comp_op [:==, :"/=", :"=:=", :"=/="]
1094+
10571095
defp of_remote(fun, [left, right] = args, call, expected, stack, context)
1058-
when fun in [:==, :"/=", :"=:=", :"=/="] do
1096+
when fun in @comp_op do
10591097
with false <- Macro.quoted_literal?(left) or Macro.quoted_literal?(right),
10601098
true <- is_var(left) or is_var(right),
10611099
{boolean, _maybe_or_always} <- booleaness(expected),
1062-
%{pattern_info: {_, _, _, _, _}} <- context do
1100+
%{pattern_info: %{}} <- context do
10631101
polarity =
10641102
case boolean do
10651103
true -> fun in [:==, :"=:="]
@@ -1089,6 +1127,27 @@ defmodule Module.Types.Pattern do
10891127
# building nested conditional environments.
10901128
[left | right] = unpack_op(call, fun, [])
10911129

1130+
# In case the right side is a sequence of comparisons with imprecise literals, collapse them.
1131+
right =
1132+
with {{:., _, [:erlang, comp_op]}, _, [{var_name, _, var_ctx}, literal]}
1133+
when comp_op in @comp_op and is_atom(var_name) and is_atom(var_ctx) and
1134+
(is_number(literal) or is_binary(literal)) <- left,
1135+
true <-
1136+
Enum.all?(right, fn
1137+
{{:., _, [:erlang, ^comp_op]}, _, [{^var_name, _, ^var_ctx}, other_literal]}
1138+
when is_integer(literal) and is_integer(other_literal)
1139+
when is_float(literal) and is_float(other_literal)
1140+
when is_binary(literal) and is_binary(other_literal) ->
1141+
true
1142+
1143+
_ ->
1144+
false
1145+
end) do
1146+
[]
1147+
else
1148+
_ -> right
1149+
end
1150+
10921151
# For example, if the expected type is true for andalso, then it can
10931152
# only be true if both clauses are executed, so we know the first
10941153
# argument has to be true and the second has to be expected.
@@ -1116,6 +1175,17 @@ defmodule Module.Types.Pattern do
11161175
{type, vars_conds} =
11171176
of_logical_cond([left | right], true, expected, abort_domain, stack, cond_context, [])
11181177

1178+
# We will be precise if all branches changed the same variable
1179+
context =
1180+
update_in(context.pattern_info.precise?, fn
1181+
false ->
1182+
false
1183+
1184+
true ->
1185+
[{_, cond} | tail] = vars_conds
1186+
Enum.all?(tail, fn {_, tail_cond} -> cond == tail_cond end)
1187+
end)
1188+
11191189
{type, Of.reduce_conditional_vars(vars_conds, call, stack, context)}
11201190
end
11211191
end

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,7 @@ defmodule Module.Types.PatternTest do
855855
assert typecheck!([x], x === "foo", x) == dynamic(binary())
856856
assert typecheck!([x], not (x == "foo"), x) == dynamic()
857857
assert typecheck!([x], not (x === "foo"), x) == dynamic()
858+
assert typecheck!([x], x in ["foo", "bar", "baz"], x) == dynamic(binary())
858859

859860
assert typecheck!([x], x != "foo", x) == dynamic()
860861
assert typecheck!([x], x !== "foo", x) == dynamic()
@@ -1024,7 +1025,7 @@ defmodule Module.Types.PatternTest do
10241025
end
10251026
end
10261027

1027-
describe "comparison in guards" do
1028+
describe "size comparison in guards" do
10281029
test "length equality" do
10291030
assert typecheck!([x], length(x) != 0, x) == dynamic(non_empty_list(term()))
10301031
assert typecheck!([x], not (length(x) != 0), x) == dynamic(empty_list())
@@ -1224,6 +1225,7 @@ defmodule Module.Types.PatternTest do
12241225
assert precise?([[_ | _]])
12251226
assert precise?([x, [y | z]])
12261227

1228+
refute precise?([[x | y]], is_integer(x))
12271229
refute precise?([[x | x]])
12281230
refute precise?([x, [x | y]])
12291231

@@ -1244,7 +1246,6 @@ defmodule Module.Types.PatternTest do
12441246
assert precise?([x], x.key != :ok)
12451247
assert precise?([x], not (x.key != :ok))
12461248
assert precise?([x, y], x == :ok and y == :error)
1247-
assert precise?([x, y], x == :ok or y == :error)
12481249

12491250
refute precise?([x, y], x == y)
12501251
refute precise?([x], x == 123)
@@ -1254,7 +1255,7 @@ defmodule Module.Types.PatternTest do
12541255
refute precise?([x, y], x == hd(y))
12551256
refute precise?([x], hd(x) == :ok)
12561257
refute precise?([x, y], x == :ok and y == 123)
1257-
refute precise?([x, y], x == :ok or y == 123)
1258+
refute precise?([x, y], x == :ok or y == :error)
12581259
end
12591260

12601261
test "sized guards" do

0 commit comments

Comments
 (0)