Skip to content

Commit d8c921f

Browse files
committed
Refine hd/tl in guards, closes #15198
1 parent 9c00059 commit d8c921f

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ defmodule Module.Types.Of do
4848
@doc """
4949
Declares a variable.
5050
"""
51-
def declare_var(var, context) do
51+
def declare_var(var, type \\ term(), context) do
5252
{var_name, meta, var_context} = var
5353
version = Keyword.fetch!(meta, :version)
5454

@@ -58,7 +58,7 @@ defmodule Module.Types.Of do
5858

5959
vars ->
6060
data = %{
61-
type: term(),
61+
type: type,
6262
name: var_name,
6363
context: var_context,
6464
off_traces: [],

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

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ defmodule Module.Types.Pattern do
408408
end)
409409
end
410410

411+
defp match_var do
412+
version = make_ref()
413+
{version, {:match, [version: version], __MODULE__}}
414+
end
415+
411416
defp match_error?({:match, _, __MODULE__}, _type), do: true
412417
defp match_error?(_var, type), do: empty?(type)
413418

@@ -651,8 +656,8 @@ defmodule Module.Types.Pattern do
651656
|> Enum.split_while(&(not is_versioned_var(&1)))
652657
|> case do
653658
{matches, []} ->
654-
version = make_ref()
655-
{true, matches, version, {:match, [version: version], __MODULE__}}
659+
{version, var} = match_var()
660+
{true, matches, version, var}
656661

657662
{pre, [{_, meta, _} = var | post]} ->
658663
version = Keyword.fetch!(meta, :version)
@@ -1018,7 +1023,8 @@ defmodule Module.Types.Pattern do
10181023
guard_context: :andalso,
10191024
parent_version: nil,
10201025
vars: vars,
1021-
changed: %{}
1026+
changed: %{},
1027+
subpatterns: %{}
10221028
})
10231029

10241030
{precise?, context} = of_guards(guards, stack, context)
@@ -1064,7 +1070,7 @@ defmodule Module.Types.Pattern do
10641070
{false, context}
10651071

10661072
{true, maybe_or_always} ->
1067-
{maybe_or_always == :always, context}
1073+
{maybe_or_always == :always and context.pattern_info.subpatterns == %{}, context}
10681074

10691075
_false_tuple_or_none ->
10701076
error = {:badguard, type, guard, context}
@@ -1315,6 +1321,42 @@ defmodule Module.Types.Pattern do
13151321
end
13161322
end
13171323

1324+
# We cannot track precision for lists, so we assign match variables
1325+
# to hd/tl and refine them accordingly.
1326+
defp of_remote(
1327+
fun,
1328+
[arg],
1329+
call,
1330+
expected,
1331+
stack,
1332+
%{pattern_info: %{subpatterns: subpatterns}} = context
1333+
)
1334+
when fun in [:hd, :tl] do
1335+
arg_key =
1336+
Macro.prewalk(arg, fn
1337+
{left, meta, right} -> {left, Keyword.take(meta, [:version]), right}
1338+
node -> node
1339+
end)
1340+
1341+
subpattern_key = {fun, arg_key}
1342+
1343+
{var, context} =
1344+
case subpatterns do
1345+
%{^subpattern_key => var} ->
1346+
{var, context}
1347+
1348+
%{} ->
1349+
{type, context} =
1350+
Apply.remote(:erlang, fun, [arg], expected, call, stack, context, &of_guard/5)
1351+
1352+
{_, var} = match_var()
1353+
context = Of.declare_var(var, type, context)
1354+
{var, put_in(context.pattern_info.subpatterns[subpattern_key], var)}
1355+
end
1356+
1357+
of_guard(var, expected, call, stack, context)
1358+
end
1359+
13181360
defp of_remote(fun, args, call, expected, stack, context) do
13191361
Apply.remote(:erlang, fun, args, expected, call, stack, context, &of_guard/5)
13201362
end

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,31 @@ defmodule Module.Types.PatternTest do
642642
"""
643643
end
644644

645+
test "hd/tl" do
646+
assert typecheck!([x], is_list(x) and is_map(hd(x)) and is_map_key(hd(x), :tag), x) ==
647+
dynamic(non_empty_list(term(), term()))
648+
649+
assert typeerror!([x], is_list(x) and is_map(hd(x)) and is_binary(hd(x)), x) =~ ~l"""
650+
this guard will never succeed:
651+
652+
is_list(x) and is_map(hd(x)) and is_binary(hd(x))
653+
654+
because it returns type:
655+
656+
false
657+
658+
where "x" was given the types:
659+
660+
# type: empty_list() or non_empty_list(term(), term())
661+
# from: types_test.ex:649
662+
is_list(x)
663+
664+
# type: non_empty_list(term(), term())
665+
# from: types_test.ex:649
666+
hd(x)
667+
"""
668+
end
669+
645670
test "is_struct/1" do
646671
assert typecheck!([x], is_struct(x), x) == dynamic(open_map(__struct__: atom()))
647672
assert typecheck!([x], is_struct(x, URI), x) == dynamic(open_map(__struct__: atom([URI])))

0 commit comments

Comments
 (0)