Skip to content

Commit efb50de

Browse files
committed
Type check generators in comprehensions
1 parent 4787116 commit efb50de

File tree

7 files changed

+133
-41
lines changed

7 files changed

+133
-41
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -811,14 +811,12 @@ defmodule Module.Types.Apply do
811811
end
812812

813813
def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do
814-
traces = collect_traces(expr, context)
815814
{mod, fun, arity, converter} = mfac
816815
meta = elem(expr, 1)
817816

818-
# Protocol errors can be very verbose, so we collapse structs
819-
{banner, hints, opts} =
820-
cond do
821-
meta[:from_interpolation] ->
817+
{banner, hints, traces} =
818+
case Keyword.get(meta, :type_check) do
819+
:interpolation ->
822820
{_, _, [arg]} = expr
823821

824822
{"""
@@ -827,34 +825,50 @@ defmodule Module.Types.Apply do
827825
#{expr_to_string(arg) |> indent(4)}
828826
829827
it has type:
830-
""", [:interpolation], [collapse_structs: true]}
828+
""", [:interpolation], collect_traces(expr, context)}
831829

832-
Code.ensure_loaded?(mod) and
833-
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
834-
{nil, [{:protocol, mod}], [collapse_structs: true]}
830+
:generator ->
831+
{:<-, _, [_, arg]} = expr
835832

836-
true ->
837-
{nil, [], []}
838-
end
833+
{"""
834+
incompatible value given to for-comprehension:
839835
840-
explanation =
841-
empty_arg_reason(converter.(args_types)) ||
842-
"""
843-
but expected one of:
844-
#{clauses_args_to_quoted_string(clauses, converter, opts)}
845-
"""
836+
#{expr_to_string(expr) |> indent(4)}
846837
847-
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
838+
it has type:
839+
""", [:generator], collect_traces(arg, context)}
848840

849-
banner =
850-
banner ||
851-
"""
852-
incompatible types given to #{mfa_or_fa}:
841+
_ ->
842+
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
853843

854-
#{expr_to_string(expr) |> indent(4)}
844+
{"""
845+
incompatible types given to #{mfa_or_fa}:
855846
856-
given types:
857-
"""
847+
#{expr_to_string(expr) |> indent(4)}
848+
849+
given types:
850+
""", [], collect_traces(expr, context)}
851+
end
852+
853+
explanation =
854+
cond do
855+
reason = empty_arg_reason(converter.(args_types)) ->
856+
reason
857+
858+
Code.ensure_loaded?(mod) and
859+
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
860+
# Protocol errors can be very verbose, so we collapse structs
861+
"""
862+
but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
863+
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
864+
"""
865+
866+
true ->
867+
"""
868+
but expected one of:
869+
#{clauses_args_to_quoted_string(clauses, converter, [])}
870+
"""
871+
end
858872

859873
%{
860874
details: %{typing_traces: traces},

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ defmodule Module.Types.Descr do
3131
@map_empty [{:closed, %{}, []}]
3232

3333
@none %{}
34-
@empty_list %{bitmap: @bit_empty_list}
35-
@not_non_empty_list %{bitmap: @bit_top, atom: @atom_top, tuple: @tuple_top, map: @map_top}
3634
@term %{
3735
bitmap: @bit_top,
3836
atom: @atom_top,
3937
tuple: @tuple_top,
4038
map: @map_top,
4139
list: @non_empty_list_top
4240
}
41+
@empty_list %{bitmap: @bit_empty_list}
42+
@not_non_empty_list Map.delete(@term, :list)
4343

4444
@empty_intersection [0, @none]
4545
@empty_difference [0, []]
@@ -98,6 +98,7 @@ defmodule Module.Types.Descr do
9898
@not_set %{optional: 1}
9999
@term_or_optional Map.put(@term, :optional, 1)
100100
@term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})
101+
@not_atom_or_optional Map.delete(@term_or_optional, :atom)
101102

102103
def not_set(), do: @not_set
103104
def if_set(:term), do: term_or_optional()
@@ -1751,10 +1752,32 @@ defmodule Module.Types.Descr do
17511752
end
17521753

17531754
# Two maps are fusible if they differ in at most one element.
1755+
defp non_fusible_maps?({_, fields1, []}, {_, fields2, []})
1756+
when map_size(fields1) > map_size(fields2) do
1757+
not fusible_maps?(Map.to_list(fields2), fields1, 0)
1758+
end
1759+
17541760
defp non_fusible_maps?({_, fields1, []}, {_, fields2, []}) do
1755-
Enum.count_until(fields1, fn {key, value} -> Map.fetch!(fields2, key) != value end, 2) > 1
1761+
not fusible_maps?(Map.to_list(fields1), fields2, 0)
1762+
end
1763+
1764+
defp fusible_maps?([{:__struct__, value} | rest], fields, count) do
1765+
case Map.fetch!(fields, :__struct__) do
1766+
^value -> fusible_maps?(rest, fields, count)
1767+
_ -> false
1768+
end
17561769
end
17571770

1771+
defp fusible_maps?([{key, value} | rest], fields, count) do
1772+
case Map.fetch!(fields, key) do
1773+
^value -> fusible_maps?(rest, fields, count)
1774+
_ when count == 1 -> false
1775+
_ when count == 0 -> fusible_maps?(rest, fields, count + 1)
1776+
end
1777+
end
1778+
1779+
defp fusible_maps?([], _fields, _count), do: true
1780+
17581781
defp map_non_negated_fuse_pair({tag, fields1, []}, {_, fields2, []}) do
17591782
fields =
17601783
symmetrical_merge(fields1, fields2, fn _k, v1, v2 ->
@@ -1818,6 +1841,11 @@ defmodule Module.Types.Descr do
18181841
{:empty_map, [], []}
18191842
end
18201843

1844+
def map_literal_to_quoted({:open, %{__struct__: @not_atom_or_optional} = fields}, _opts)
1845+
when map_size(fields) == 1 do
1846+
{:non_struct_map, [], []}
1847+
end
1848+
18211849
def map_literal_to_quoted({tag, fields}, opts) do
18221850
case tag do
18231851
:closed ->

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,13 +471,17 @@ defmodule Module.Types.Expr do
471471

472472
## Comprehensions
473473

474-
defp for_clause({:<-, _, [left, right]} = expr, stack, context) do
474+
defp for_clause({:<-, meta, [left, right]}, stack, context) do
475+
expr = {:<-, [type_check: :generator] ++ meta, [left, right]}
475476
{pattern, guards} = extract_head([left])
476-
{_, context} = of_expr(right, stack, context)
477+
{type, context} = of_expr(right, stack, context)
477478

478479
{_type, context} =
479480
Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context)
480481

482+
{_type, context} =
483+
Apply.remote(Enumerable, :count, [right], [type], expr, stack, context)
484+
481485
context
482486
end
483487

@@ -511,6 +515,7 @@ defmodule Module.Types.Expr do
511515
end
512516

513517
defp for_option({:uniq, _}, _stack, context) do
518+
# This option is verified to be a boolean at compile-time
514519
context
515520
end
516521

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ defmodule Module.Types.Helpers do
9090
string upfront or implement the protocol accordingly
9191
"""
9292

93-
{:protocol, protocol} ->
93+
:generator ->
9494
"""
9595
96-
#{hint()} #{inspect(protocol)} is a protocol in Elixir. Either make sure you \
97-
give valid data types as arguments or implement the protocol accordingly
96+
#{hint()} for-comprehensions in Elixir use the Enumerable protocol to traverse \
97+
data structures. Either convert the data type into a list (or another Enumerable) \
98+
or implement the protocol accordingly
9899
"""
99100

100101
:anonymous_rescue ->

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ defmodule Module.Types.Of do
9696
{Function, fun()},
9797
{Integer, integer()},
9898
{List, list(term())},
99-
{Map, open_map(__struct__: not_set())},
99+
{Map, open_map(__struct__: if_set(negation(atom())))},
100100
{Port, port()},
101101
{PID, pid()},
102102
{Reference, reference()},
@@ -339,7 +339,7 @@ defmodule Module.Types.Of do
339339
{{:., _, [String.Chars, :to_string]} = dot, meta, [arg]},
340340
{:binary, _, nil}
341341
) do
342-
{dot, [from_interpolation: true] ++ meta, [arg]}
342+
{dot, [type_check: :interpolation] ++ meta, [arg]}
343343
end
344344

345345
defp annotate_interpolation(left, _right) do

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,15 @@ defmodule Module.Types.DescrTest do
14941494
assert closed_map(__struct__: atom([Decimal]), coef: term(), exp: term(), sign: integer())
14951495
|> to_quoted_string(collapse_structs: true) ==
14961496
"%Decimal{sign: integer()}"
1497+
1498+
# Does not fuse structs
1499+
assert union(closed_map(__struct__: atom([Foo])), closed_map(__struct__: atom([Bar])))
1500+
|> to_quoted_string() ==
1501+
"%{__struct__: Bar} or %{__struct__: Foo}"
1502+
1503+
# Properly format non_struct_map
1504+
assert open_map(__struct__: if_set(negation(atom()))) |> to_quoted_string() ==
1505+
"non_struct_map()"
14971506
end
14981507
end
14991508

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ defmodule Module.Types.IntegrationTest do
131131
assert itself_arg.(Itself.Function) == dynamic(fun())
132132
assert itself_arg.(Itself.Integer) == dynamic(integer())
133133
assert itself_arg.(Itself.List) == dynamic(list(term()))
134-
assert itself_arg.(Itself.Map) == dynamic(open_map(__struct__: not_set()))
134+
assert itself_arg.(Itself.Map) == dynamic(open_map(__struct__: if_set(negation(atom()))))
135135
assert itself_arg.(Itself.Port) == dynamic(port())
136136
assert itself_arg.(Itself.PID) == dynamic(pid())
137137
assert itself_arg.(Itself.Reference) == dynamic(reference())
@@ -374,7 +374,7 @@ defmodule Module.Types.IntegrationTest do
374374
assert_warnings(files, warnings)
375375
end
376376

377-
test "protocol dispatch" do
377+
test "String.Chars protocol dispatch" do
378378
files = %{
379379
"a.ex" => """
380380
defmodule FooBar do
@@ -394,7 +394,7 @@ defmodule Module.Types.IntegrationTest do
394394
395395
-dynamic(%Range{first: term(), last: term(), step: term()})-
396396
397-
but expected one of:
397+
but expected a type that implements the String.Chars protocol, it must be one of:
398398
399399
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
400400
%Version.Requirement{} or atom() or binary() or float() or integer() or list(term())
@@ -416,7 +416,7 @@ defmodule Module.Types.IntegrationTest do
416416
417417
-dynamic(%Range{first: term(), last: term(), step: term()})-
418418
419-
but expected one of:
419+
but expected a type that implements the String.Chars protocol, it must be one of:
420420
421421
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
422422
%Version.Requirement{} or atom() or binary() or float() or integer() or list(term())
@@ -426,8 +426,43 @@ defmodule Module.Types.IntegrationTest do
426426
# type: dynamic(%Range{})
427427
# from: a.ex:2:24
428428
_.._//_ = data
429+
"""
430+
]
431+
432+
assert_warnings(files, warnings, consolidate_protocols: true)
433+
end
434+
435+
test "Enumerable protocol dispatch" do
436+
files = %{
437+
"a.ex" => """
438+
defmodule FooBar do
439+
def example1(%Date{} = date), do: for(x <- date, do: x)
440+
end
441+
"""
442+
}
443+
444+
warnings = [
445+
"""
446+
warning: incompatible value given to for-comprehension:
447+
448+
x <- date
449+
450+
it has type:
451+
452+
-dynamic(%Date{year: term(), month: term(), day: term(), calendar: term()})-
453+
454+
but expected a type that implements the Enumerable protocol, it must be one of:
455+
456+
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
457+
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{} or fun() or list(term()) or non_struct_map()
458+
459+
where "date" was given the type:
460+
461+
# type: dynamic(%Date{})
462+
# from: a.ex:2:24
463+
%Date{} = date
429464
430-
hint: String.Chars is a protocol in Elixir. Either make sure you give valid data types as arguments or implement the protocol accordingly
465+
hint: for-comprehensions in Elixir use the Enumerable protocol to traverse data structures. Either convert the data type into a list (or another Enumerable) or implement the protocol accordingly
431466
"""
432467
]
433468

0 commit comments

Comments
 (0)