diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 9270b4d4113..8e0f5935c5f 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -5001,3 +5001,69 @@ defimpl Enumerable, for: Function do description: "only anonymous functions of arity 2 are enumerable" end end + +defimpl Enumerable, for: Range do + def reduce(first..last//step, acc, fun) do + reduce(first, last, acc, fun, step) + end + + # TODO: Remove me on v2.0 + def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do + step = if first <= last, do: 1, else: -1 + reduce(Map.put(range, :step, step), acc, fun) + end + + defp reduce(_first, _last, {:halt, acc}, _fun, _step) do + {:halted, acc} + end + + defp reduce(first, last, {:suspend, acc}, fun, step) do + {:suspended, acc, &reduce(first, last, &1, fun, step)} + end + + defp reduce(first, last, {:cont, acc}, fun, step) + when step > 0 and first <= last + when step < 0 and first >= last do + reduce(first + step, last, fun.(first, acc), fun, step) + end + + defp reduce(_, _, {:cont, acc}, _fun, _up) do + {:done, acc} + end + + def member?(first..last//step, value) when is_integer(value) do + if step > 0 do + {:ok, first <= value and value <= last and rem(value - first, step) == 0} + else + {:ok, last <= value and value <= first and rem(value - first, step) == 0} + end + end + + # TODO: Remove me on v2.0 + def member?(%{__struct__: Range, first: first, last: last} = range, value) + when is_integer(value) do + step = if first <= last, do: 1, else: -1 + member?(Map.put(range, :step, step), value) + end + + def member?(_, _value) do + {:ok, false} + end + + def count(range) do + {:ok, Range.size(range)} + end + + def slice(first.._//step = range) do + {:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)} + end + + # TODO: Remove me on v2.0 + def slice(%{__struct__: Range, first: first, last: last} = range) do + step = if first <= last, do: 1, else: -1 + slice(Map.put(range, :step, step)) + end + + defp slice(_current, _step, 0), do: [] + defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)] +end diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 7950607ad32..1fdc6f36104 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -2003,7 +2003,7 @@ defmodule Protocol.UndefinedError do # Indent only lines with contents on them |> String.replace(~r/^(?=.+)/m, " ") - "protocol #{inspect(protocol)} not implemented for type " <> + "protocol #{inspect(protocol)} not implemented for " <> value_type(value) <> maybe_description(description) <> maybe_available(protocol) <> @@ -2038,7 +2038,7 @@ defmodule Protocol.UndefinedError do ". There are no implementations for this protocol." {:consolidated, types} -> - ". This protocol is implemented for the following type(s): " <> + ". This protocol is implemented for: " <> Enum.map_join(types, ", ", &inspect/1) :not_consolidated -> diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index ee0639eca3b..b111e1fa0a3 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -200,7 +200,13 @@ defprotocol Inspect do do: var!(info) var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts)) + + unquote(inspect_module).inspect_as_struct( + var!(struct), + var!(name), + var!(infos), + var!(opts) + ) end end end @@ -390,6 +396,10 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do + inspect_as_map(map, opts) + end + + def inspect_as_map(map, opts) do list = if Keyword.get(opts.custom_options, :sort_maps) do map |> Map.to_list() |> :lists.sort() @@ -408,7 +418,7 @@ defimpl Inspect, for: Map do map_container_doc(list, "", opts, fun) end - def inspect(map, name, infos, opts) do + def inspect_as_struct(map, name, infos, opts) do fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end map_container_doc(infos, name, opts, fun) end @@ -599,25 +609,36 @@ end defimpl Inspect, for: Any do def inspect(%module{} = struct, opts) do try do - {module.__struct__(), module.__info__(:struct)} + module.__info__(:struct) rescue - _ -> Inspect.Map.inspect(struct, opts) + _ -> Inspect.Map.inspect_as_map(struct, opts) else - {dunder, fields} -> - if Map.keys(dunder) == Map.keys(struct) do - infos = - for %{field: field} = info <- fields, - field not in [:__struct__, :__exception__], - do: info - - Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts) + info -> + if valid_struct?(info, struct) do + info = + for %{field: field} = map <- info, + field != :__exception__, + do: map + + Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) else - Inspect.Map.inspect(struct, opts) + Inspect.Map.inspect_as_map(struct, opts) end end end - def inspect(map, name, infos, opts) do + defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1) + + defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field), + do: valid_struct?(info, struct, count - 1) + + defp valid_struct?([], _struct, 0), + do: true + + defp valid_struct?(_fields, _struct, _count), + do: false + + def inspect_as_struct(map, name, infos, opts) do open = color_doc("#" <> name <> "<", :map, opts) sep = color_doc(",", :map, opts) close = color_doc(">", :map, opts) @@ -631,6 +652,25 @@ defimpl Inspect, for: Any do end end +defimpl Inspect, for: Range do + import Inspect.Algebra + import Kernel, except: [inspect: 2] + + def inspect(first..last//1, opts) when last >= first do + concat([to_doc(first, opts), "..", to_doc(last, opts)]) + end + + def inspect(first..last//step, opts) do + concat([to_doc(first, opts), "..", to_doc(last, opts), "//", to_doc(step, opts)]) + end + + # TODO: Remove me on v2.0 + def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do + step = if first <= last, do: 1, else: -1 + inspect(Map.put(range, :step, step), opts) + end +end + require Protocol Protocol.derive( diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index ec1de4d0ea9..4d661c8eb92 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -355,14 +355,14 @@ defmodule Inspect.Algebra do # we won't try to render any failed instruct when building # the error message. if Process.get(:inspect_trap) do - Inspect.Map.inspect(struct, opts) + Inspect.Map.inspect_as_map(struct, opts) else try do Process.put(:inspect_trap, true) inspected_struct = struct - |> Inspect.Map.inspect(%{ + |> Inspect.Map.inspect_as_map(%{ opts | syntax_colors: [], inspect_fun: Inspect.Opts.default_inspect_fun() @@ -389,7 +389,7 @@ defmodule Inspect.Algebra do end end else - Inspect.Map.inspect(struct, opts) + Inspect.Map.inspect_as_map(struct, opts) end end diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 90483af1095..df7621e3149 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3409,7 +3409,7 @@ defmodule Kernel do """ defmacro to_charlist(term) do - quote(do: List.Chars.to_charlist(unquote(term))) + quote(do: :"Elixir.List.Chars".to_charlist(unquote(term))) end @doc """ @@ -4064,7 +4064,7 @@ defmodule Kernel do -1 end - {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} + {:%, [], [Elixir.Range, {:%{}, [], [first: first, last: last, step: step]}]} end defp stepless_range(nil, first, last, _caller) do @@ -4090,7 +4090,7 @@ defmodule Kernel do Macro.Env.stacktrace(caller) ) - {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} + {:%, [], [Elixir.Range, {:%{}, [], [first: first, last: last, step: step]}]} end defp stepless_range(:match, first, last, caller) do @@ -4103,7 +4103,7 @@ defmodule Kernel do Macro.Env.stacktrace(caller) ) - {:%{}, [], [__struct__: Elixir.Range, first: first, last: last]} + {:%, [], [Elixir.Range, {:%{}, [], [first: first, last: last]}]} end @doc """ @@ -4142,14 +4142,14 @@ defmodule Kernel do range(__CALLER__.context, first, last, step) false -> - range(__CALLER__.context, first, last, step) + {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} end end defp range(context, first, last, step) when is_integer(first) and is_integer(last) and is_integer(step) when context != nil do - {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} + {:%, [], [Elixir.Range, {:%{}, [], [first: first, last: last, step: step]}]} end defp range(nil, first, last, step) do @@ -4553,11 +4553,10 @@ defmodule Kernel do raise ArgumentError, "found unescaped value on the right side of in/2: #{inspect(right)}" right -> - with {:%{}, _meta, fields} <- right, - [__struct__: Elixir.Range, first: first, last: last, step: step] <- - :lists.usort(fields) do - in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step))) - else + case range_fields(right) do + [first: first, last: last, step: step] -> + in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last), expand.(step))) + _ when in_body? -> quote(do: Elixir.Enum.member?(unquote(right), unquote(left))) @@ -4567,6 +4566,10 @@ defmodule Kernel do end end + defp range_fields({:%, _, [Elixir.Range, {:%{}, _, fields}]}), do: :lists.usort(fields) + defp range_fields({:%{}, _, [__struct__: Elixir.Range] ++ fields}), do: :lists.usort(fields) + defp range_fields(_), do: [] + defp raise_on_invalid_args_in_2(right) do raise ArgumentError, << "invalid right argument for operator \"in\", it expects a compile-time proper list ", @@ -5379,7 +5382,7 @@ defmodule Kernel do john = %User{name: "John"} MyProtocol.call(john) - ** (Protocol.UndefinedError) protocol MyProtocol not implemented for %User{...} + ** (Protocol.UndefinedError) protocol MyProtocol not implemented for User (a struct) `defstruct/1`, however, allows protocol implementations to be *derived*. This can be done by defining a `@derive` attribute as a diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 3f77c61a4ed..4374e4c0e21 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -145,7 +145,7 @@ defmodule Module.Types do defp default_domain({_, arity} = fun_arity, impl) do with {for, callbacks} <- impl, true <- fun_arity in callbacks do - [Module.Types.Of.impl(for) | List.duplicate(Descr.dynamic(), arity - 1)] + [Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)] else _ -> List.duplicate(Descr.dynamic(), arity) end @@ -282,7 +282,7 @@ defmodule Module.Types do try do {args_types, context} = - Pattern.of_head(args, guards, expected, :default, meta, stack, context) + Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context) {return_type, context} = Expr.of_expr(body, stack, context) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index ed510e5e634..335c3278655 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -566,11 +566,21 @@ defmodule Module.Types.Apply do is_map_key(builtin_modules(), module) end + @builtin_protocols [ + Collectable, + Enumerable, + IEx.Info, + Inspect, + JSON.Encoder, + List.Chars, + String.Chars + ] + defp builtin_modules do case :persistent_term.get(__MODULE__, nil) do nil -> {:ok, mods} = :application.get_key(:elixir, :modules) - mods = Map.from_keys(mods, []) + mods = Map.from_keys(mods -- @builtin_protocols, []) :persistent_term.put(__MODULE__, mods) mods @@ -791,7 +801,7 @@ defmodule Module.Types.Apply do empty_arg_reason(args_types) || """ but expected one of: - #{clauses_args_to_quoted_string(clauses, converter)} + #{clauses_args_to_quoted_string(clauses, converter, [])} """ %{ @@ -817,33 +827,62 @@ defmodule Module.Types.Apply do def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do traces = collect_traces(expr, context) {mod, fun, arity, converter} = mfac + meta = elem(expr, 1) + + # Protocol errors can be very verbose, so we collapse structs + {banner, hints, opts} = + cond do + meta[:from_interpolation] -> + {_, _, [arg]} = expr + + {""" + incompatible value given to string interpolation: + + #{expr_to_string(arg) |> indent(4)} + + it has type: + """, [:interpolation], [collapse_structs: true]} + + Code.ensure_loaded?(mod) and + Keyword.has_key?(mod.module_info(:attributes), :__protocol__) -> + {nil, [{:protocol, mod}], [collapse_structs: true]} + + true -> + {nil, [], []} + end explanation = empty_arg_reason(converter.(args_types)) || """ but expected one of: - #{clauses_args_to_quoted_string(clauses, converter)} + #{clauses_args_to_quoted_string(clauses, converter, opts)} """ - mfa_or_fa = - if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}" + mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}" + + banner = + banner || + """ + incompatible types given to #{mfa_or_fa}: + + #{expr_to_string(expr) |> indent(4)} + + given types: + """ %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ + banner, """ - incompatible types given to #{mfa_or_fa}: - - #{expr_to_string(expr) |> indent(4)} - - given types: #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} """, explanation, - format_traces(traces) + format_traces(traces), + format_hints(hints) ]) } end @@ -998,25 +1037,25 @@ defmodule Module.Types.Apply do |> IO.iodata_to_binary() end - defp clauses_args_to_quoted_string([{args, _return}], converter) do - "\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4)) + defp clauses_args_to_quoted_string([{args, _return}], converter, opts) do + "\n " <> (clause_args_to_quoted_string(args, converter, opts) |> indent(4)) end - defp clauses_args_to_quoted_string(clauses, converter) do + defp clauses_args_to_quoted_string(clauses, converter, opts) do clauses |> Enum.with_index(fn {args, _return}, index -> """ ##{index + 1} - #{clause_args_to_quoted_string(args, converter)}\ + #{clause_args_to_quoted_string(args, converter, opts)}\ """ |> indent(4) end) |> Enum.join("\n") end - defp clause_args_to_quoted_string(args, converter) do - docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra())) + defp clause_args_to_quoted_string(args, converter, opts) do + docs = Enum.map(args, &(&1 |> to_quoted(opts) |> Code.Formatter.to_algebra())) args_docs_to_quoted_string(converter.(docs)) end @@ -1054,13 +1093,4 @@ defmodule Module.Types.Apply do single_line -> binary_slice(single_line, 1..-2//1) end end - - defp integer_to_ordinal(i) do - case rem(i, 10) do - 1 when rem(i, 100) != 11 -> "#{i}st" - 2 when rem(i, 100) != 12 -> "#{i}nd" - 3 when rem(i, 100) != 13 -> "#{i}rd" - _ -> "#{i}th" - end - end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index a76260e97c9..19b4553e397 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -47,6 +47,18 @@ defmodule Module.Types.Helpers do ## Warnings + @doc """ + Converts an itneger into ordinal. + """ + def integer_to_ordinal(i) do + case rem(i, 10) do + 1 when rem(i, 100) != 11 -> "#{i}st" + 2 when rem(i, 100) != 12 -> "#{i}nd" + 3 when rem(i, 100) != 13 -> "#{i}rd" + _ -> "#{i}th" + end + end + @doc """ Formatted hints in typing errors. """ @@ -70,6 +82,21 @@ defmodule Module.Types.Helpers do "var.fun()" (with parentheses) means "var" is an atom() """ + :interpolation -> + """ + + #{hint()} string interpolation in Elixir uses the String.Chars protocol to \ + convert a data structure into a string. Either convert the data type into a \ + string upfront or implement the protocol accordingly + """ + + {:protocol, protocol} -> + """ + + #{hint()} #{inspect(protocol)} is a protocol in Elixir. Either make sure you \ + give valid data types as arguments or implement the protocol accordingly + """ + :anonymous_rescue -> """ @@ -203,12 +230,34 @@ defmodule Module.Types.Helpers do @doc """ Converts the given expression to a string, translating inlined Erlang calls back to Elixir. + + We also undo some macro expresions done by the Kernel module. """ def expr_to_string(expr) do expr |> Macro.prewalk(fn - {{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta) - other -> other + {:%, _, [Range, {:%{}, _, fields}]} = node -> + case :lists.usort(fields) do + [first: first, last: last, step: step] -> + quote do + unquote(first)..unquote(last)//unquote(step) + end + + _ -> + node + end + + {{:., _, [Elixir.String.Chars, :to_string]}, meta, [arg]} -> + {:to_string, meta, [arg]} + + {{:., _, [Elixir.List.Chars, :to_charlist]}, meta, [arg]} -> + {:to_charlist, meta, [arg]} + + {{:., _, [mod, fun]}, meta, args} -> + erl_to_ex(mod, fun, args, meta) + + other -> + other end) |> Macro.to_string() end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index cb10c78042b..7c5d25e7300 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -89,9 +89,6 @@ defmodule Module.Types.Of do ## Implementations - # Right now we are still defaulting all implementations to their dynamic variations. - # TODO: What should the default types be once we have typed protocols? - impls = [ {Atom, atom()}, {BitString, binary()}, @@ -99,7 +96,7 @@ defmodule Module.Types.Of do {Function, fun()}, {Integer, integer()}, {List, list(term())}, - {Map, open_map()}, + {Map, open_map(__struct__: not_set())}, {Port, port()}, {PID, pid()}, {Reference, reference()}, @@ -108,16 +105,16 @@ defmodule Module.Types.Of do ] for {for, type} <- impls do - def impl(unquote(for)), do: unquote(Macro.escape(dynamic(type))) + def impl(unquote(for)), do: unquote(Macro.escape(type)) end def impl(struct) do # Elixir did not strictly require the implementation to be available, so we need a fallback. # TODO: Assume implementation is available on Elixir v2.0. if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do - dynamic(struct_type(struct, info)) + struct_type(struct, info) else - dynamic(open_map(__struct__: atom([struct]))) + open_map(__struct__: atom([struct])) end end @@ -330,6 +327,7 @@ defmodule Module.Types.Of do Module.Types.Pattern.of_guard(left, type, expr, stack, context) :expr -> + left = annotate_interpolation(left, right) {actual, context} = Module.Types.Expr.of_expr(left, stack, context) intersect(actual, type, expr, stack, context) end @@ -337,6 +335,17 @@ defmodule Module.Types.Of do specifier_size(kind, right, stack, context) end + defp annotate_interpolation( + {{:., _, [String.Chars, :to_string]} = dot, meta, [arg]}, + {:binary, _, nil} + ) do + {dot, [from_interpolation: true] ++ meta, [arg]} + end + + defp annotate_interpolation(left, _right) do + left + end + defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left) defp specifier_type(:match, {:utf8, _, _}), do: @integer defp specifier_type(:match, {:utf16, _, _}), do: @integer diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 77f51a16d7c..1b428ac8bda 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -72,7 +72,8 @@ defmodule Module.Types.Pattern do stack, context ) do - with {:ok, type, context} <- of_pattern_intersect(tree, type, pattern, tag, stack, context) do + with {:ok, type, context} <- + of_pattern_intersect(tree, type, pattern, index, tag, stack, context) do acc = [type | acc] of_pattern_args_tree(tail, expected_types, changed, index + 1, acc, tag, stack, context) end @@ -115,7 +116,7 @@ defmodule Module.Types.Pattern do {[type], context} = of_pattern_recur([expected], tag, stack, context, fn [type], [0], context -> with {:ok, type, context} <- - of_pattern_intersect(tree, type, expr, tag, stack, context) do + of_pattern_intersect(tree, type, expr, 0, tag, stack, context) do {:ok, [type], context} end end) @@ -177,7 +178,7 @@ defmodule Module.Types.Pattern do end :error -> - throw({types, badpattern_error(expr, tag, stack, context)}) + throw({types, badpattern_error(expr, index, tag, stack, context)}) end end) @@ -216,7 +217,7 @@ defmodule Module.Types.Pattern do end) end - defp badpattern_error(expr, tag, stack, context) do + defp badpattern_error(expr, index, tag, stack, context) do meta = if meta = get_meta(expr) do meta ++ Keyword.take(stack.meta, [:generated, :line]) @@ -224,15 +225,15 @@ defmodule Module.Types.Pattern do stack.meta end - error(__MODULE__, {:badpattern, expr, tag, context}, meta, stack, context) + error(__MODULE__, {:badpattern, expr, index, tag, context}, meta, stack, context) end - defp of_pattern_intersect(tree, expected, expr, tag, stack, context) do + defp of_pattern_intersect(tree, expected, expr, index, tag, stack, context) do actual = of_pattern_tree(tree, context) type = intersection(actual, expected) if empty?(type) do - {:error, badpattern_error(expr, tag, stack, context)} + {:error, badpattern_error(expr, index, tag, stack, context)} else {:ok, type, context} end @@ -750,8 +751,8 @@ defmodule Module.Types.Pattern do # # The match pattern ones have the whole expression instead # of a single pattern. - def format_diagnostic({:badpattern, pattern_or_expr, tag, context}) do - {to_trace, message} = badpattern(tag, pattern_or_expr) + def format_diagnostic({:badpattern, pattern_or_expr, index, tag, context}) do + {to_trace, message} = badpattern(tag, pattern_or_expr, index) traces = collect_traces(to_trace, context) %{ @@ -760,7 +761,7 @@ defmodule Module.Types.Pattern do } end - defp badpattern({:try_else, type}, pattern) do + defp badpattern({:try_else, type}, pattern, _) do {pattern, """ the following clause will never match: @@ -773,7 +774,7 @@ defmodule Module.Types.Pattern do """} end - defp badpattern({:case, meta, type, expr}, pattern) do + defp badpattern({:case, meta, type, expr}, pattern, _) do if meta[:type_check] == :expr do {expr, """ @@ -799,7 +800,7 @@ defmodule Module.Types.Pattern do end end - defp badpattern({:match, type}, expr) do + defp badpattern({:match, type}, expr, _) do {expr, """ the following pattern will never match: @@ -812,7 +813,32 @@ defmodule Module.Types.Pattern do """} end - defp badpattern(_tag, pattern_or_expr) do + defp badpattern({:infer, types}, pattern_or_expr, index) do + type = Enum.fetch!(types, index) + + if type == dynamic() do + {pattern_or_expr, + """ + the #{integer_to_ordinal(index + 1)} pattern in clause will never match: + + #{expr_to_string(pattern_or_expr) |> indent(4)} + """} + else + # This can only happen in protocol implementations + {pattern_or_expr, + """ + the #{integer_to_ordinal(index + 1)} pattern in clause will never match: + + #{expr_to_string(pattern_or_expr) |> indent(4)} + + because it is expected to receive type: + + #{to_quoted_string(type) |> indent(4)} + """} + end + end + + defp badpattern(:default, pattern_or_expr, _) do {pattern_or_expr, """ the following pattern will never match: diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index c492a7041f4..4cc5b28c447 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -560,25 +560,26 @@ defmodule Protocol do # Ensure the types are sorted so the compiled beam is deterministic types = Enum.sort(types) - with {:ok, ast_info, specs, compile_info} <- beam_protocol(protocol), - {:ok, definitions} <- change_debug_info(protocol, ast_info, types), - do: compile(definitions, specs, compile_info) + with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol), + {:ok, definitions, signatures} <- + consolidate(protocol, any, definitions, signatures, types), + do: compile(definitions, signatures, compile_info) end defp beam_protocol(protocol) do - chunk_ids = [:debug_info, [?D, ?o, ?c, ?s], [?E, ?x, ?C, ?k]] + chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]] opts = [:allow_missing_chunks] case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do {:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} -> - {:debug_info_v1, _backend, {:elixir_v1, info, specs}} = debug_info - %{attributes: attributes, definitions: definitions} = info + {:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info + %{attributes: attributes, definitions: definitions, signatures: signatures} = module_map chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks) chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks) case attributes[:__protocol__] do [fallback_to_any: any] -> - {:ok, {any, definitions}, specs, {info, chunks}} + {:ok, any, definitions, signatures, {module_map, specs, chunks}} _ -> {:error, :not_a_protocol} @@ -596,29 +597,81 @@ defmodule Protocol do end end - # Change the debug information to the optimized - # impl_for/1 dispatch version. - defp change_debug_info(protocol, {any, definitions}, types) do - types = if any, do: types, else: List.delete(types, Any) - all = [Any] ++ for {mod, _guard} <- built_in(), do: mod - structs = types -- all - + # Consolidate the protocol for faster implementations and fine-grained type information. + defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do case List.keytake(definitions, {:__protocol__, 1}, 0) do {protocol_def, definitions} -> + types = if fallback_to_any?, do: types, else: List.delete(types, Any) + built_in_plus_any = [Any] ++ for {mod, _guard} <- built_in(), do: mod + structs = types -- built_in_plus_any + {impl_for, definitions} = List.keytake(definitions, {:impl_for, 1}, 0) + {impl_for!, definitions} = List.keytake(definitions, {:impl_for!, 1}, 0) {struct_impl_for, definitions} = List.keytake(definitions, {:struct_impl_for, 1}, 0) protocol_def = change_protocol(protocol_def, types) impl_for = change_impl_for(impl_for, protocol, types) struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs) + new_signatures = new_signatures(definitions, protocol, types) - {:ok, [protocol_def, impl_for, struct_impl_for] ++ definitions} + definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions + signatures = Enum.into(new_signatures, signatures) + {:ok, definitions, signatures} nil -> {:error, :not_a_protocol} end end + defp new_signatures(definitions, protocol, types) do + alias Module.Types.Descr + + clauses = + types + |> List.delete(Any) + |> Enum.map(fn impl -> + {[Module.Types.Of.impl(impl)], Descr.atom([Module.concat(protocol, impl)])} + end) + + {domain, impl_for, impl_for!} = + case clauses do + [] -> + if Any in types do + clauses = [{[Descr.term()], Descr.atom([Module.concat(protocol, Any)])}] + {Descr.none(), clauses, clauses} + else + {Descr.none(), [{[Descr.term()], Descr.atom([nil])}], + [{[Descr.none()], Descr.none()}]} + end + + _ -> + domain = + clauses + |> Enum.map(fn {[domain], _} -> domain end) + |> Enum.reduce(&Descr.union/2) + + not_domain = Descr.negation(domain) + + if Any in types do + clauses = clauses ++ [{[not_domain], Descr.atom([Module.concat(protocol, Any)])}] + {Descr.term(), clauses, clauses} + else + {domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses} + end + end + + new_signatures = + for {{fun, arity}, :def, _, _} <- definitions do + rest = List.duplicate(Descr.term(), arity - 1) + {{fun, arity}, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}} + end + + [ + {{:impl_for, 1}, {:strong, [Descr.term()], impl_for}}, + {{:impl_for!, 1}, {:strong, [domain], impl_for!}} + ] ++ new_signatures + end + defp change_protocol({_name, _kind, meta, clauses}, types) do clauses = Enum.map(clauses, fn @@ -682,9 +735,9 @@ defmodule Protocol do end # Finally compile the module and emit its bytecode. - defp compile(definitions, specs, {info, chunks}) do - info = %{info | definitions: definitions} - {:ok, :elixir_erl.consolidate(info, specs, chunks)} + defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do + module_map = %{module_map | definitions: definitions, signatures: signatures} + {:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)} end ## Definition callbacks diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index b876ffb21d2..9fa0939a443 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -526,88 +526,3 @@ defmodule Range do def range?(_), do: false end - -defimpl Enumerable, for: Range do - def reduce(first..last//step, acc, fun) do - reduce(first, last, acc, fun, step) - end - - # TODO: Remove me on v2.0 - def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do - step = if first <= last, do: 1, else: -1 - reduce(Map.put(range, :step, step), acc, fun) - end - - defp reduce(_first, _last, {:halt, acc}, _fun, _step) do - {:halted, acc} - end - - defp reduce(first, last, {:suspend, acc}, fun, step) do - {:suspended, acc, &reduce(first, last, &1, fun, step)} - end - - defp reduce(first, last, {:cont, acc}, fun, step) - when step > 0 and first <= last - when step < 0 and first >= last do - reduce(first + step, last, fun.(first, acc), fun, step) - end - - defp reduce(_, _, {:cont, acc}, _fun, _up) do - {:done, acc} - end - - def member?(first..last//step, value) when is_integer(value) do - if step > 0 do - {:ok, first <= value and value <= last and rem(value - first, step) == 0} - else - {:ok, last <= value and value <= first and rem(value - first, step) == 0} - end - end - - # TODO: Remove me on v2.0 - def member?(%{__struct__: Range, first: first, last: last} = range, value) - when is_integer(value) do - step = if first <= last, do: 1, else: -1 - member?(Map.put(range, :step, step), value) - end - - def member?(_, _value) do - {:ok, false} - end - - def count(range) do - {:ok, Range.size(range)} - end - - def slice(first.._//step = range) do - {:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)} - end - - # TODO: Remove me on v2.0 - def slice(%{__struct__: Range, first: first, last: last} = range) do - step = if first <= last, do: 1, else: -1 - slice(Map.put(range, :step, step)) - end - - defp slice(_current, _step, 0), do: [] - defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)] -end - -defimpl Inspect, for: Range do - import Inspect.Algebra - import Kernel, except: [inspect: 2] - - def inspect(first..last//1, opts) when last >= first do - concat([to_doc(first, opts), "..", to_doc(last, opts)]) - end - - def inspect(first..last//step, opts) do - concat([to_doc(first, opts), "..", to_doc(last, opts), "//", to_doc(step, opts)]) - end - - # TODO: Remove me on v2.0 - def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do - step = if first <= last, do: 1, else: -1 - inspect(Map.put(range, :step, step), opts) - end -end diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 8ba322b88fd..64eb05bee55 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -171,6 +171,7 @@ bootstrap_files() -> [ <<"kernel/utils.ex">>, <<"macro/env.ex">>, + <<"range.ex">>, <<"keyword.ex">>, <<"module.ex">>, <<"list.ex">>, @@ -203,7 +204,6 @@ bootstrap_files() -> <<"path.ex">>, <<"file.ex">>, <<"map.ex">>, - <<"range.ex">>, <<"access.ex">>, <<"io.ex">>, <<"system.ex">>, diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 17ed4d9f0ef..791657eca78 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -118,9 +118,10 @@ scope(_Meta, ExpandCaptures) -> %% Static compilation hook, used in protocol consolidation -consolidate(Map, TypeSpecs, Chunks) -> - {Prefix, Forms, _Def, _Defmacro, _Macros} = dynamic_form(Map), - load_form(Map, Prefix, Forms, TypeSpecs, Chunks). +consolidate(Map, TypeSpecs, DocsChunk) -> + {Prefix, Forms, Def, _Defmacro, _Macros} = dynamic_form(Map), + CheckerChunk = checker_chunk(Map, Def, chunk_opts(Map)), + load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk). %% Dynamic compilation hook, used in regular compiler @@ -143,16 +144,17 @@ compile(#{module := Module, anno := Anno} = BaseMap) -> {Prefix, Forms, Def, Defmacro, Macros} = dynamic_form(Map), {Types, Callbacks, TypeSpecs} = typespecs_form(Map, TranslatedTypespecs, Macros), - ChunkOpts = - case lists:member(deterministic, ?key(Map, compile_opts)) of - true -> [deterministic]; - false -> [] - end, - + ChunkOpts = chunk_opts(Map), DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts), CheckerChunk = checker_chunk(Map, Def, ChunkOpts), load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk). +chunk_opts(Map) -> + case lists:member(deterministic, ?key(Map, compile_opts)) of + true -> [deterministic]; + false -> [] + end. + dynamic_form(#{module := Module, relative_file := RelativeFile, attributes := Attributes, definitions := Definitions, unreachable := Unreachable, deprecated := Deprecated, compile_opts := Opts} = Map) -> diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index c8943cbe6af..18c61406f85 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -459,18 +459,16 @@ indent_n([H | T], Count, Indent) -> [Indent, H | indent_n(T, Count - 1, Indent)] prefix(warning) -> highlight(<<"warning:">>, warning); prefix(error) -> highlight(<<"error:">>, error); -prefix(hint) -> highlight(<<"hint:">>, hint). +prefix(hint) -> <<"hint:">>. highlight(Message, Severity) -> case {Severity, application:get_env(elixir, ansi_enabled, false)} of {warning, true} -> yellow(Message); {error, true} -> red(Message); - {hint, true} -> blue(Message); _ -> Message end. yellow(Msg) -> ["\e[33m", Msg, "\e[0m"]. -blue(Msg) -> ["\e[34m", Msg, "\e[0m"]. red(Msg) -> ["\e[31m", Msg, "\e[0m"]. env_format(Meta, #{file := EnvFile} = E) -> diff --git a/lib/elixir/test/elixir/fixtures/consolidation/no_impl.ex b/lib/elixir/test/elixir/fixtures/consolidation/no_impl.ex new file mode 100644 index 00000000000..7cd9f90db4c --- /dev/null +++ b/lib/elixir/test/elixir/fixtures/consolidation/no_impl.ex @@ -0,0 +1,3 @@ +defprotocol Protocol.ConsolidationTest.NoImpl do + def ok(term) +end diff --git a/lib/elixir/test/elixir/fixtures/consolidation/with_any.ex b/lib/elixir/test/elixir/fixtures/consolidation/with_any.ex index 42912e5633c..513257ec5d2 100644 --- a/lib/elixir/test/elixir/fixtures/consolidation/with_any.ex +++ b/lib/elixir/test/elixir/fixtures/consolidation/with_any.ex @@ -1,5 +1,5 @@ defprotocol Protocol.ConsolidationTest.WithAny do @fallback_to_any true @doc "Ok" - def ok(term) + def ok(term, opts) end diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index d7f2ffca477..4f273360da1 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -526,7 +526,7 @@ defmodule Inspect.MapTest do # Inspect.Error is raised here when we tried to print the error message # called by another exception (Protocol.UndefinedError in this case) exception_message = ~s''' - protocol Enumerable not implemented for type Inspect.MapTest.Failing (a struct) + protocol Enumerable not implemented for Inspect.MapTest.Failing (a struct) Got value: @@ -930,7 +930,7 @@ defmodule Inspect.CustomProtocolTest do got Protocol.UndefinedError with message: """ - protocol Inspect.CustomProtocolTest.CustomInspect not implemented for type Inspect.CustomProtocolTest.MissingImplementation (a struct) + protocol Inspect.CustomProtocolTest.CustomInspect not implemented for Inspect.CustomProtocolTest.MissingImplementation (a struct) Got value: @@ -961,7 +961,7 @@ defmodule Inspect.CustomProtocolTest do got Protocol.UndefinedError with message: """ - protocol Inspect.CustomProtocolTest.CustomInspect not implemented for type Inspect.CustomProtocolTest.MissingImplementation (a struct) + protocol Inspect.CustomProtocolTest.CustomInspect not implemented for Inspect.CustomProtocolTest.MissingImplementation (a struct) Got value: diff --git a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs index c4e10309073..31e172489ca 100644 --- a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs +++ b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs @@ -453,7 +453,7 @@ defmodule Kernel.LexicalTrackerTest do Code.eval_string(""" defmodule Kernel.LexicalTrackerTest.PatternGuardsCompile do %URI{} = URI.parse("/") - case 1..3 do + case Range.new(1, 3) do range when is_struct(range, Range) -> :ok end Kernel.LexicalTracker.references(__ENV__.lexical_tracker) diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 386ad37cb57..e449f9e170a 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -6,6 +6,33 @@ defmodule Module.Types.IntegrationTest do import ExUnit.CaptureIO import Module.Types.Descr + defp builtin_protocols do + [ + Collectable, + Enumerable, + IEx.Info, + Inspect, + JSON.Encoder, + List.Chars, + String.Chars + ] + end + + test "built-in protocols" do + builtin_protocols = + for app <- ~w[eex elixir ex_unit iex logger mix]a, + Application.ensure_loaded(app), + module <- Application.spec(app, :modules), + Code.ensure_loaded(module), + function_exported?(module, :__protocol__, 1), + do: module + + # If this test fails, update: + # * lib/elixir/lib/module/types/apply.ex + # * lib/elixir/scripts/elixir_docs.ex + assert Enum.sort(builtin_protocols) == builtin_protocols() + end + setup_all do previous = Application.get_env(:elixir, :ansi_enabled, false) Application.put_env(:elixir, :ansi_enabled, false) @@ -53,7 +80,7 @@ defmodule Module.Types.IntegrationTest do ] end - test "writes exports for inferred protocols and implementations" do + test "writes exports for implementations" do files = %{ "pi.ex" => """ defprotocol Itself do @@ -104,7 +131,7 @@ defmodule Module.Types.IntegrationTest do assert itself_arg.(Itself.Function) == dynamic(fun()) assert itself_arg.(Itself.Integer) == dynamic(integer()) assert itself_arg.(Itself.List) == dynamic(list(term())) - assert itself_arg.(Itself.Map) == dynamic(open_map()) + assert itself_arg.(Itself.Map) == dynamic(open_map(__struct__: not_set())) assert itself_arg.(Itself.Port) == dynamic(port()) assert itself_arg.(Itself.PID) == dynamic(pid()) assert itself_arg.(Itself.Reference) == dynamic(reference()) @@ -311,6 +338,102 @@ defmodule Module.Types.IntegrationTest do assert_no_warnings(files) end + test "mismatched impl" do + files = %{ + "a.ex" => """ + defprotocol Itself do + def itself(data) + end + + defimpl Itself, for: Range do + def itself(nil), do: nil + def itself(range), do: range + end + """ + } + + warnings = [ + """ + warning: the 1st pattern in clause will never match: + + nil + + because it is expected to receive type: + + dynamic(%Range{first: term(), last: term(), step: term()}) + + typing violation found at: + │ + 6 │ def itself(nil), do: nil + │ ~~~~~~~~~~~~~~~~~~~~~~~~ + │ + └─ a.ex:6: Itself.Range.itself/1 + """ + ] + + assert_warnings(files, warnings) + end + + test "protocol dispatch" do + files = %{ + "a.ex" => """ + defmodule FooBar do + def example1(_.._//_ = data), do: to_string(data) + def example2(_.._//_ = data), do: "hello \#{data} world" + end + """ + } + + warnings = [ + """ + warning: incompatible value given to string interpolation: + + data + + it has type: + + -dynamic(%Range{first: term(), last: term(), step: term()})- + + but expected one of: + + %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or + %Version.Requirement{} or atom() or binary() or float() or integer() or list(term()) + + where "data" was given the type: + + # type: dynamic(%Range{}) + # from: a.ex:3:24 + _.._//_ = data + + hint: string interpolation in Elixir uses the String.Chars protocol to convert a data structure into a string. Either convert the data type into a string upfront or implement the protocol accordingly + """, + """ + warning: incompatible types given to String.Chars.to_string/1: + + to_string(data) + + given types: + + -dynamic(%Range{first: term(), last: term(), step: term()})- + + but expected one of: + + %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or + %Version.Requirement{} or atom() or binary() or float() or integer() or list(term()) + + where "data" was given the type: + + # type: dynamic(%Range{}) + # from: a.ex:2:24 + _.._//_ = data + + hint: String.Chars is a protocol in Elixir. Either make sure you give valid data types as arguments or implement the protocol accordingly + """ + ] + + assert_warnings(files, warnings, consolidate_protocols: true) + end + test "returns diagnostics with source and file" do files = %{ "a.ex" => """ @@ -338,8 +461,7 @@ defmodule Module.Types.IntegrationTest do assert String.ends_with?(file, "generated.ex") assert Path.type(file) == :absolute after - :code.delete(A) - :code.purge(A) + purge(A) end end @@ -1059,12 +1181,14 @@ defmodule Module.Types.IntegrationTest do end end - defp assert_warnings(files, expected) when is_binary(expected) do - assert capture_compile_warnings(files) == expected + defp assert_warnings(files, expected, opts \\ []) + + defp assert_warnings(files, expected, opts) when is_binary(expected) do + assert capture_compile_warnings(files, opts) == expected end - defp assert_warnings(files, expecteds) when is_list(expecteds) do - output = capture_compile_warnings(files) + defp assert_warnings(files, expecteds, opts) when is_list(expecteds) do + output = capture_compile_warnings(files, opts) Enum.each(expecteds, fn expected -> assert output =~ expected @@ -1072,27 +1196,27 @@ defmodule Module.Types.IntegrationTest do end defp assert_no_warnings(files) do - assert capture_compile_warnings(files) == "" + assert capture_compile_warnings(files, []) == "" end - defp capture_compile_warnings(files) do + defp capture_compile_warnings(files, opts) do in_tmp(fn -> paths = generate_files(files) - capture_io(:stderr, fn -> compile_to_path(paths) end) + capture_io(:stderr, fn -> compile_to_path(paths, opts) end) end) end defp with_compile_warnings(files) do in_tmp(fn -> paths = generate_files(files) - with_io(:stderr, fn -> compile_to_path(paths) end) |> elem(0) + with_io(:stderr, fn -> compile_to_path(paths, []) end) |> elem(0) end) end defp compile_modules(files) do in_tmp(fn -> paths = generate_files(files) - {modules, _warnings} = compile_to_path(paths) + {modules, _warnings} = compile_to_path(paths, []) Map.new(modules, fn module -> {^module, binary, _filename} = :code.get_object_code(module) @@ -1101,13 +1225,43 @@ defmodule Module.Types.IntegrationTest do end) end - defp compile_to_path(paths) do + defp compile_to_path(paths, opts) do + if opts[:consolidate_protocols] do + Code.prepend_path(".") + + result = + compile_to_path_with_after_compile(paths, fn -> + if Keyword.get(opts, :consolidate_protocols, false) do + paths = [".", Application.app_dir(:elixir, "ebin")] + protocols = Protocol.extract_protocols(paths) + + for protocol <- protocols do + impls = Protocol.extract_impls(protocol, paths) + {:ok, binary} = Protocol.consolidate(protocol, impls) + File.write!(Atom.to_string(protocol) <> ".beam", binary) + purge(protocol) + end + end + end) + + Code.delete_path(".") + Enum.each(builtin_protocols(), &purge/1) + + result + else + compile_to_path_with_after_compile(paths, fn -> :ok end) + end + end + + defp compile_to_path_with_after_compile(paths, callback) do {:ok, modules, warnings} = - Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true) + Kernel.ParallelCompiler.compile_to_path(paths, ".", + return_diagnostics: true, + after_compile: callback + ) for module <- modules do - :code.delete(module) - :code.purge(module) + purge(module) end {modules, warnings} @@ -1126,6 +1280,11 @@ defmodule Module.Types.IntegrationTest do map end + defp purge(mod) do + :code.delete(mod) + :code.purge(mod) + end + defp in_tmp(fun) do path = PathHelpers.tmp_path("checker") diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index a482f801d14..64f35f88e6a 100644 --- a/lib/elixir/test/elixir/protocol/consolidation_test.exs +++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs @@ -8,16 +8,16 @@ Kernel.ParallelCompiler.compile_to_path(files, path, return_diagnostics: true) defmodule Protocol.ConsolidationTest do use ExUnit.Case, async: true - alias Protocol.ConsolidationTest.{Sample, WithAny} + alias Protocol.ConsolidationTest.{Sample, WithAny, NoImpl} defimpl WithAny, for: Map do - def ok(map) do + def ok(map, _opts) do {:ok, map} end end defimpl WithAny, for: Any do - def ok(any) do + def ok(any, _opts) do {:ok, any} end end @@ -47,7 +47,7 @@ defmodule Protocol.ConsolidationTest do {:ok, binary} = Protocol.consolidate(Sample, [Any, ImplStruct]) :code.load_binary(Sample, ~c"protocol_test.exs", binary) - @sample_binary binary + defp sample_binary, do: unquote(binary) # Any should be moved to the end :code.purge(WithAny) @@ -55,6 +55,16 @@ defmodule Protocol.ConsolidationTest do {:ok, binary} = Protocol.consolidate(WithAny, [Any, ImplStruct, Map]) :code.load_binary(WithAny, ~c"protocol_test.exs", binary) + defp with_any_binary, do: unquote(binary) + + # No Any + :code.purge(NoImpl) + :code.delete(NoImpl) + {:ok, binary} = Protocol.consolidate(NoImpl, []) + :code.load_binary(NoImpl, ~c"protocol_test.exs", binary) + + defp no_impl_binary, do: unquote(binary) + test "consolidated?/1" do assert Protocol.consolidated?(WithAny) refute Protocol.consolidated?(Enumerable) @@ -64,7 +74,7 @@ defmodule Protocol.ConsolidationTest do output = ExUnit.CaptureIO.capture_io(:stderr, fn -> defimpl WithAny, for: Integer do - def ok(_any), do: :ok + def ok(_any, _opts), do: :ok end end) @@ -86,7 +96,7 @@ defmodule Protocol.ConsolidationTest do :code.delete(WithAny.Integer) end - test "consolidated implementations without any" do + test "consolidated implementations without fallback to any" do assert is_nil(Sample.impl_for(:foo)) assert is_nil(Sample.impl_for(fn x -> x end)) assert is_nil(Sample.impl_for(1)) @@ -106,8 +116,9 @@ defmodule Protocol.ConsolidationTest do assert Sample.impl_for(%NoImplStruct{}) == nil end - test "consolidated implementations with any and tuple fallback" do + test "consolidated implementations with fallback to any" do assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any + # Derived assert WithAny.impl_for(%ImplStruct{}) == Protocol.ConsolidationTest.WithAny.Protocol.ConsolidationTest.ImplStruct @@ -118,72 +129,126 @@ defmodule Protocol.ConsolidationTest do end test "consolidation keeps docs" do - {:ok, {Sample, [{~c"Docs", docs_bin}]}} = :beam_lib.chunks(@sample_binary, [~c"Docs"]) + {:ok, {Sample, [{~c"Docs", docs_bin}]}} = :beam_lib.chunks(sample_binary(), [~c"Docs"]) {:docs_v1, _, _, _, _, _, docs} = :erlang.binary_to_term(docs_bin) ok_doc = List.keyfind(docs, {:function, :ok, 1}, 0) assert {{:function, :ok, 1}, _, ["ok(term)"], %{"en" => "Ok"}, _} = ok_doc end - test "consolidation keeps chunks" do - deprecated = [{{:ok, 1}, "Reason"}] - assert deprecated == Sample.__info__(:deprecated) - - {:ok, {Sample, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(@sample_binary, [~c"ExCk"]) - assert {:elixir_checker_v1, contents} = :erlang.binary_to_term(check_bin) - assert %{{:ok, 1} => %{deprecated: "Reason", sig: _}} = Map.new(contents.exports) - end - @tag :requires_source test "consolidation keeps source" do assert Sample.__info__(:compile)[:source] end test "consolidated keeps callbacks" do - {:ok, callbacks} = Code.Typespec.fetch_callbacks(@sample_binary) + {:ok, callbacks} = Code.Typespec.fetch_callbacks(sample_binary()) assert callbacks != [] end - test "consolidation errors on missing BEAM files" do - defprotocol NoBeam do - def example(arg) - end - - assert Protocol.consolidate(String, []) == {:error, :not_a_protocol} - assert Protocol.consolidate(NoBeam, []) == {:error, :no_beam_info} - end - test "consolidation updates attributes" do assert Sample.__protocol__(:consolidated?) assert Sample.__protocol__(:impls) == {:consolidated, [ImplStruct]} assert WithAny.__protocol__(:consolidated?) assert WithAny.__protocol__(:impls) == {:consolidated, [Any, Map, ImplStruct]} + assert NoImpl.__protocol__(:consolidated?) + assert NoImpl.__protocol__(:impls) == {:consolidated, []} end - test "consolidation extracts protocols" do - protos = Protocol.extract_protocols([Application.app_dir(:elixir, "ebin")]) - assert Enumerable in protos - assert Inspect in protos - end + describe "exports" do + import Module.Types.Descr + alias Module.Types.Of + + defp exports(binary) do + {:ok, {_, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(binary, [~c"ExCk"]) + assert {:elixir_checker_v1, contents} = :erlang.binary_to_term(check_bin) + Map.new(contents.exports) + end + + test "keeps deprecations" do + deprecated = [{{:ok, 1}, "Reason"}] + assert deprecated == Sample.__info__(:deprecated) + + assert %{{:ok, 1} => %{deprecated: "Reason", sig: _}} = exports(sample_binary()) + end + + test "defines signatures without fallback to any" do + exports = exports(sample_binary()) + + assert %{{:impl_for, 1} => %{sig: {:strong, domain, clauses}}} = exports + assert domain == [term()] + + assert clauses == [ + {[Of.impl(ImplStruct)], atom([Sample.Protocol.ConsolidationTest.ImplStruct])}, + {[negation(Of.impl(ImplStruct))], atom([nil])} + ] - test "consolidation extracts implementations with charlist path" do - protos = - Protocol.extract_impls(Enumerable, [to_charlist(Application.app_dir(:elixir, "ebin"))]) + assert %{{:impl_for!, 1} => %{sig: {:strong, domain, clauses}}} = exports + assert domain == [Of.impl(ImplStruct)] - assert List in protos - assert Function in protos + assert clauses == [ + {[Of.impl(ImplStruct)], atom([Sample.Protocol.ConsolidationTest.ImplStruct])} + ] + + assert %{{:ok, 1} => %{sig: {:strong, nil, clauses}}} = exports + + assert clauses == [ + {[Of.impl(ImplStruct)], dynamic()} + ] + end + + test "defines signatures with fallback to any" do + exports = exports(with_any_binary()) + + assert %{ + {:impl_for, 1} => %{sig: {:strong, domain, clauses}}, + {:impl_for!, 1} => %{sig: {:strong, domain, clauses}} + } = exports + + assert domain == [term()] + + assert clauses == [ + {[Of.impl(Map)], atom([WithAny.Map])}, + {[Of.impl(ImplStruct)], atom([WithAny.Protocol.ConsolidationTest.ImplStruct])}, + {[negation(union(Of.impl(ImplStruct), Of.impl(Map)))], atom([WithAny.Any])} + ] + + assert %{{:ok, 2} => %{sig: {:strong, nil, clauses}}} = exports + + assert clauses == [ + {[term(), term()], dynamic()} + ] + end + + test "defines signatures without implementation" do + exports = exports(no_impl_binary()) + + assert %{{:impl_for, 1} => %{sig: {:strong, domain, clauses}}} = exports + assert domain == [term()] + assert clauses == [{[term()], atom([nil])}] + + assert %{{:impl_for!, 1} => %{sig: {:strong, domain, clauses}}} = exports + assert domain == [none()] + assert clauses == [{[none()], none()}] + + assert %{{:ok, 1} => %{sig: {:strong, nil, clauses}}} = exports + assert clauses == [{[none()], dynamic()}] + end end - test "consolidation extracts implementations with binary path" do - protos = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")]) - assert List in protos - assert Function in protos + test "consolidation errors on missing BEAM files" do + defprotocol NoBeam do + def example(arg) + end + + assert Protocol.consolidate(String, []) == {:error, :not_a_protocol} + assert Protocol.consolidate(NoBeam, []) == {:error, :no_beam_info} end test "protocol not implemented" do message = - "protocol Protocol.ConsolidationTest.Sample not implemented for type Atom. " <> - "This protocol is implemented for the following type(s): Protocol.ConsolidationTest.ImplStruct" <> + "protocol Protocol.ConsolidationTest.Sample not implemented for Atom. " <> + "This protocol is implemented for: Protocol.ConsolidationTest.ImplStruct" <> "\n\nGot value:\n\n :foo\n" assert_raise Protocol.UndefinedError, message, fn -> @@ -191,4 +256,26 @@ defmodule Protocol.ConsolidationTest do sample.ok(:foo) end end + + describe "extraction" do + test "protocols" do + protos = Protocol.extract_protocols([Application.app_dir(:elixir, "ebin")]) + assert Enumerable in protos + assert Inspect in protos + end + + test "implementations with charlist path" do + protos = + Protocol.extract_impls(Enumerable, [to_charlist(Application.app_dir(:elixir, "ebin"))]) + + assert List in protos + assert Function in protos + end + + test "implementations with binary path" do + protos = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")]) + assert List in protos + assert Function in protos + end + end end diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index 210f41b7088..118210c8eab 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -120,7 +120,7 @@ defmodule ProtocolTest do test "protocol not implemented" do message = """ - protocol ProtocolTest.Sample not implemented for type Atom + protocol ProtocolTest.Sample not implemented for Atom Got value: @@ -289,7 +289,7 @@ defmodule ProtocolTest do assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}, []} assert_raise Protocol.UndefinedError, - ~r"protocol ProtocolTest.Derivable not implemented for type ProtocolTest.NoImplStruct \(a struct\), you should try harder", + ~r"protocol ProtocolTest.Derivable not implemented for ProtocolTest.NoImplStruct \(a struct\), you should try harder", fn -> struct = %NoImplStruct{a: 1, b: 1} Derivable.ok(struct) diff --git a/lib/elixir/test/elixir/string/chars_test.exs b/lib/elixir/test/elixir/string/chars_test.exs index dd3cf61a02e..18669d8ec90 100644 --- a/lib/elixir/test/elixir/string/chars_test.exs +++ b/lib/elixir/test/elixir/string/chars_test.exs @@ -106,7 +106,7 @@ defmodule String.Chars.ErrorsTest do test "bitstring" do message = """ - protocol String.Chars not implemented for type BitString, cannot convert a bitstring to a string + protocol String.Chars not implemented for BitString, cannot convert a bitstring to a string Got value: @@ -120,7 +120,7 @@ defmodule String.Chars.ErrorsTest do test "tuple" do message = """ - protocol String.Chars not implemented for type Tuple + protocol String.Chars not implemented for Tuple Got value: @@ -134,7 +134,7 @@ defmodule String.Chars.ErrorsTest do test "PID" do message = - ~r"^protocol String\.Chars not implemented for type PID\n\nGot value:\n\n #PID<.+?>$" + ~r"^protocol String\.Chars not implemented for PID\n\nGot value:\n\n #PID<.+?>$" assert_raise Protocol.UndefinedError, message, fn -> to_string(self()) @@ -143,7 +143,7 @@ defmodule String.Chars.ErrorsTest do test "ref" do message = - ~r"^protocol String\.Chars not implemented for type Reference\n\nGot value:\n\n #Reference<.+?>$" + ~r"^protocol String\.Chars not implemented for Reference\n\nGot value:\n\n #Reference<.+?>$" assert_raise Protocol.UndefinedError, message, fn -> to_string(make_ref()) == "" @@ -152,7 +152,7 @@ defmodule String.Chars.ErrorsTest do test "function" do message = - ~r"^protocol String\.Chars not implemented for type Function\n\nGot value:\n\n #Function<.+?>$" + ~r"^protocol String\.Chars not implemented for Function\n\nGot value:\n\n #Function<.+?>$" assert_raise Protocol.UndefinedError, message, fn -> to_string(fn -> nil end) @@ -163,7 +163,7 @@ defmodule String.Chars.ErrorsTest do [port | _] = Port.list() message = - ~r"^protocol String\.Chars not implemented for type Port\n\nGot value:\n\n #Port<.+?>$" + ~r"^protocol String\.Chars not implemented for Port\n\nGot value:\n\n #Port<.+?>$" assert_raise Protocol.UndefinedError, message, fn -> to_string(port) @@ -172,7 +172,7 @@ defmodule String.Chars.ErrorsTest do test "user-defined struct" do message = - "protocol String\.Chars not implemented for type String.Chars.ErrorsTest.Foo (a struct)\n\nGot value:\n\n %String.Chars.ErrorsTest.Foo{foo: \"bar\"}\n" + "protocol String\.Chars not implemented for String.Chars.ErrorsTest.Foo (a struct)\n\nGot value:\n\n %String.Chars.ErrorsTest.Foo{foo: \"bar\"}\n" assert_raise Protocol.UndefinedError, message, fn -> to_string(%Foo{}) diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index a1e5506c1f6..b49fd0cc883 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -507,8 +507,8 @@ defmodule ExUnit.FormatterTest do defstruct key: 0 defimpl Inspect do - def inspect(struct, opts) when is_atom(opts) do - struct.unknown + def inspect(_struct, opts) when is_atom(opts) do + raise "the clause above should never match" end end end