Skip to content

Commit f1e4f10

Browse files
committed
Type check protocol dispatch
1 parent 30764fe commit f1e4f10

File tree

8 files changed

+142
-52
lines changed

8 files changed

+142
-52
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3409,7 +3409,7 @@ defmodule Kernel do
34093409
34103410
"""
34113411
defmacro to_charlist(term) do
3412-
quote(do: List.Chars.to_charlist(unquote(term)))
3412+
quote(do: :"Elixir.List.Chars".to_charlist(unquote(term)))
34133413
end
34143414

34153415
@doc """
@@ -4149,7 +4149,7 @@ defmodule Kernel do
41494149
defp range(context, first, last, step)
41504150
when is_integer(first) and is_integer(last) and is_integer(step)
41514151
when context != nil do
4152-
{:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]}
4152+
{:%, [], [Elixir.Range, {:%{}, [], [first: first, last: last, step: step]}]}
41534153
end
41544154

41554155
defp range(nil, first, last, step) do

lib/elixir/lib/module/types.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ defmodule Module.Types do
145145
defp default_domain({_, arity} = fun_arity, impl) do
146146
with {for, callbacks} <- impl,
147147
true <- fun_arity in callbacks do
148-
[Module.Types.Of.impl(for) | List.duplicate(Descr.dynamic(), arity - 1)]
148+
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
149149
else
150150
_ -> List.duplicate(Descr.dynamic(), arity)
151151
end

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,21 @@ defmodule Module.Types.Apply do
566566
is_map_key(builtin_modules(), module)
567567
end
568568

569+
@builtin_protocols [
570+
Collectable,
571+
Enumerable,
572+
IEx.Info,
573+
Inspect,
574+
JSON.Encoder,
575+
List.Chars,
576+
String.Chars
577+
]
578+
569579
defp builtin_modules do
570580
case :persistent_term.get(__MODULE__, nil) do
571581
nil ->
572582
{:ok, mods} = :application.get_key(:elixir, :modules)
573-
mods = Map.from_keys(mods, [])
583+
mods = Map.from_keys(mods -- @builtin_protocols, [])
574584
:persistent_term.put(__MODULE__, mods)
575585
mods
576586

@@ -791,7 +801,7 @@ defmodule Module.Types.Apply do
791801
empty_arg_reason(args_types) ||
792802
"""
793803
but expected one of:
794-
#{clauses_args_to_quoted_string(clauses, converter)}
804+
#{clauses_args_to_quoted_string(clauses, converter, [])}
795805
"""
796806

797807
%{
@@ -817,23 +827,37 @@ defmodule Module.Types.Apply do
817827
def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do
818828
traces = collect_traces(expr, context)
819829
{mod, fun, arity, converter} = mfac
830+
meta = elem(expr, 1)
831+
832+
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
833+
834+
# Protocol errors can be very verbose, so we collapse structs
835+
{caller, hints, opts} =
836+
cond do
837+
meta[:from_interpolation] ->
838+
{"string interpolation", [:interpolation], [collapse_structs: true]}
839+
840+
Code.ensure_loaded?(mod) and
841+
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
842+
{mfa_or_fa, [{:protocol, mod}], [collapse_structs: true]}
843+
844+
true ->
845+
{mfa_or_fa, [], []}
846+
end
820847

821848
explanation =
822849
empty_arg_reason(converter.(args_types)) ||
823850
"""
824851
but expected one of:
825-
#{clauses_args_to_quoted_string(clauses, converter)}
852+
#{clauses_args_to_quoted_string(clauses, converter, opts)}
826853
"""
827854

828-
mfa_or_fa =
829-
if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
830-
831855
%{
832856
details: %{typing_traces: traces},
833857
message:
834858
IO.iodata_to_binary([
835859
"""
836-
incompatible types given to #{mfa_or_fa}:
860+
incompatible types given to #{caller}:
837861
838862
#{expr_to_string(expr) |> indent(4)}
839863
@@ -843,7 +867,8 @@ defmodule Module.Types.Apply do
843867
844868
""",
845869
explanation,
846-
format_traces(traces)
870+
format_traces(traces),
871+
format_hints(hints)
847872
])
848873
}
849874
end
@@ -998,25 +1023,25 @@ defmodule Module.Types.Apply do
9981023
|> IO.iodata_to_binary()
9991024
end
10001025

1001-
defp clauses_args_to_quoted_string([{args, _return}], converter) do
1002-
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
1026+
defp clauses_args_to_quoted_string([{args, _return}], converter, opts) do
1027+
"\n " <> (clause_args_to_quoted_string(args, converter, opts) |> indent(4))
10031028
end
10041029

1005-
defp clauses_args_to_quoted_string(clauses, converter) do
1030+
defp clauses_args_to_quoted_string(clauses, converter, opts) do
10061031
clauses
10071032
|> Enum.with_index(fn {args, _return}, index ->
10081033
"""
10091034
10101035
##{index + 1}
1011-
#{clause_args_to_quoted_string(args, converter)}\
1036+
#{clause_args_to_quoted_string(args, converter, opts)}\
10121037
"""
10131038
|> indent(4)
10141039
end)
10151040
|> Enum.join("\n")
10161041
end
10171042

1018-
defp clause_args_to_quoted_string(args, converter) do
1019-
docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra()))
1043+
defp clause_args_to_quoted_string(args, converter, opts) do
1044+
docs = Enum.map(args, &(&1 |> to_quoted(opts) |> Code.Formatter.to_algebra()))
10201045
args_docs_to_quoted_string(converter.(docs))
10211046
end
10221047

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ defmodule Module.Types.Helpers do
7070
"var.fun()" (with parentheses) means "var" is an atom()
7171
"""
7272

73+
:interpolation ->
74+
"""
75+
76+
#{hint()} string interpolation in Elixir use the String.Chars protocol to \
77+
convert a data structure into a string. Either convert the data type into a \
78+
string upfront or implement the protocol accordingly
79+
"""
80+
81+
{:protocol, protocol} ->
82+
"""
83+
84+
#{hint()} #{inspect(protocol)} is a protocol in Elixir. Either make sure you \
85+
give valid data types as argument or implement the protocol accordingly
86+
"""
87+
7388
:anonymous_rescue ->
7489
"""
7590

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,14 @@ defmodule Module.Types.Of do
8989

9090
## Implementations
9191

92-
# Right now we are still defaulting all implementations to their dynamic variations.
93-
# TODO: What should the default types be once we have typed protocols?
94-
9592
impls = [
9693
{Atom, atom()},
9794
{BitString, binary()},
9895
{Float, float()},
9996
{Function, fun()},
10097
{Integer, integer()},
10198
{List, list(term())},
102-
{Map, open_map()},
99+
{Map, open_map(__struct__: not_set())},
103100
{Port, port()},
104101
{PID, pid()},
105102
{Reference, reference()},
@@ -108,16 +105,16 @@ defmodule Module.Types.Of do
108105
]
109106

110107
for {for, type} <- impls do
111-
def impl(unquote(for)), do: unquote(Macro.escape(dynamic(type)))
108+
def impl(unquote(for)), do: unquote(Macro.escape(type))
112109
end
113110

114111
def impl(struct) do
115112
# Elixir did not strictly require the implementation to be available, so we need a fallback.
116113
# TODO: Assume implementation is available on Elixir v2.0.
117114
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
118-
dynamic(struct_type(struct, info))
115+
struct_type(struct, info)
119116
else
120-
dynamic(open_map(__struct__: atom([struct])))
117+
open_map(__struct__: atom([struct]))
121118
end
122119
end
123120

@@ -330,13 +327,25 @@ defmodule Module.Types.Of do
330327
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
331328

332329
:expr ->
330+
left = annotate_interpolation(left, right)
333331
{actual, context} = Module.Types.Expr.of_expr(left, stack, context)
334332
intersect(actual, type, expr, stack, context)
335333
end
336334

337335
specifier_size(kind, right, stack, context)
338336
end
339337

338+
defp annotate_interpolation(
339+
{{:., _, [String.Chars, :to_string]} = dot, meta, [arg]},
340+
{:binary, _, nil}
341+
) do
342+
{dot, [from_interpolation: true] ++ meta, [arg]}
343+
end
344+
345+
defp annotate_interpolation(left, _right) do
346+
left
347+
end
348+
340349
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
341350
defp specifier_type(:match, {:utf8, _, _}), do: @integer
342351
defp specifier_type(:match, {:utf16, _, _}), do: @integer

lib/elixir/lib/protocol.ex

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -560,25 +560,26 @@ defmodule Protocol do
560560
# Ensure the types are sorted so the compiled beam is deterministic
561561
types = Enum.sort(types)
562562

563-
with {:ok, ast_info, specs, compile_info} <- beam_protocol(protocol),
564-
{:ok, definitions} <- change_debug_info(protocol, ast_info, types),
565-
do: compile(definitions, specs, compile_info)
563+
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
564+
{:ok, definitions, signatures} <-
565+
consolidate(protocol, any, definitions, signatures, types),
566+
do: compile(definitions, signatures, compile_info)
566567
end
567568

568569
defp beam_protocol(protocol) do
569-
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s], [?E, ?x, ?C, ?k]]
570+
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
570571
opts = [:allow_missing_chunks]
571572

572573
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
573574
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
574-
{:debug_info_v1, _backend, {:elixir_v1, info, specs}} = debug_info
575-
%{attributes: attributes, definitions: definitions} = info
575+
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
576+
%{attributes: attributes, definitions: definitions, signatures: signatures} = module_map
576577
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
577578
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
578579

579580
case attributes[:__protocol__] do
580581
[fallback_to_any: any] ->
581-
{:ok, {any, definitions}, specs, {info, chunks}}
582+
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
582583

583584
_ ->
584585
{:error, :not_a_protocol}
@@ -596,29 +597,67 @@ defmodule Protocol do
596597
end
597598
end
598599

599-
# Change the debug information to the optimized
600-
# impl_for/1 dispatch version.
601-
defp change_debug_info(protocol, {any, definitions}, types) do
602-
types = if any, do: types, else: List.delete(types, Any)
603-
all = [Any] ++ for {mod, _guard} <- built_in(), do: mod
604-
structs = types -- all
605-
600+
# Consolidate the protocol for faster implementations and fine-grained type information.
601+
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
606602
case List.keytake(definitions, {:__protocol__, 1}, 0) do
607603
{protocol_def, definitions} ->
604+
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
605+
built_in_plus_any = [Any] ++ for {mod, _guard} <- built_in(), do: mod
606+
structs = types -- built_in_plus_any
607+
608608
{impl_for, definitions} = List.keytake(definitions, {:impl_for, 1}, 0)
609+
{impl_for!, definitions} = List.keytake(definitions, {:impl_for!, 1}, 0)
609610
{struct_impl_for, definitions} = List.keytake(definitions, {:struct_impl_for, 1}, 0)
610611

611612
protocol_def = change_protocol(protocol_def, types)
612613
impl_for = change_impl_for(impl_for, protocol, types)
613614
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
615+
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
614616

615-
{:ok, [protocol_def, impl_for, struct_impl_for] ++ definitions}
617+
new_signatures = new_signatures(definitions, protocol, types, structs)
618+
signatures = Enum.into(new_signatures, signatures)
619+
{:ok, definitions, signatures}
616620

617621
nil ->
618622
{:error, :not_a_protocol}
619623
end
620624
end
621625

626+
defp new_signatures(definitions, protocol, types, structs) do
627+
alias Module.Types.Descr
628+
629+
clauses =
630+
Enum.map(structs ++ List.delete(types, Any), fn impl ->
631+
{[Module.Types.Of.impl(impl)], Descr.atom([Module.concat(protocol, impl)])}
632+
end)
633+
634+
domain =
635+
clauses
636+
|> Enum.map(fn {[domain], _} -> domain end)
637+
|> Enum.reduce(&Descr.union/2)
638+
639+
not_domain = Descr.negation(domain)
640+
641+
{domain, impl_for, impl_for!} =
642+
if Any in types do
643+
clauses = clauses ++ [{[not_domain], Descr.atom([Module.concat(protocol, Any)])}]
644+
{Descr.term(), clauses, clauses}
645+
else
646+
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
647+
end
648+
649+
new_signatures =
650+
for {{fun, arity}, :def, _, _} <- definitions do
651+
rest = List.duplicate(Descr.term(), arity - 1)
652+
{{fun, arity}, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
653+
end
654+
655+
[
656+
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
657+
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
658+
] ++ new_signatures
659+
end
660+
622661
defp change_protocol({_name, _kind, meta, clauses}, types) do
623662
clauses =
624663
Enum.map(clauses, fn
@@ -682,9 +721,9 @@ defmodule Protocol do
682721
end
683722

684723
# Finally compile the module and emit its bytecode.
685-
defp compile(definitions, specs, {info, chunks}) do
686-
info = %{info | definitions: definitions}
687-
{:ok, :elixir_erl.consolidate(info, specs, chunks)}
724+
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
725+
module_map = %{module_map | definitions: definitions, signatures: signatures}
726+
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
688727
end
689728

690729
## Definition callbacks

lib/elixir/src/elixir_erl.erl

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ scope(_Meta, ExpandCaptures) ->
118118

119119
%% Static compilation hook, used in protocol consolidation
120120

121-
consolidate(Map, TypeSpecs, Chunks) ->
122-
{Prefix, Forms, _Def, _Defmacro, _Macros} = dynamic_form(Map),
123-
load_form(Map, Prefix, Forms, TypeSpecs, Chunks).
121+
consolidate(Map, TypeSpecs, DocsChunk) ->
122+
{Prefix, Forms, Def, _Defmacro, _Macros} = dynamic_form(Map),
123+
CheckerChunk = checker_chunk(Map, Def, chunk_opts(Map)),
124+
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
124125

125126
%% Dynamic compilation hook, used in regular compiler
126127

@@ -143,16 +144,17 @@ compile(#{module := Module, anno := Anno} = BaseMap) ->
143144
{Prefix, Forms, Def, Defmacro, Macros} = dynamic_form(Map),
144145
{Types, Callbacks, TypeSpecs} = typespecs_form(Map, TranslatedTypespecs, Macros),
145146

146-
ChunkOpts =
147-
case lists:member(deterministic, ?key(Map, compile_opts)) of
148-
true -> [deterministic];
149-
false -> []
150-
end,
151-
147+
ChunkOpts = chunk_opts(Map),
152148
DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts),
153149
CheckerChunk = checker_chunk(Map, Def, ChunkOpts),
154150
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
155151

152+
chunk_opts(Map) ->
153+
case lists:member(deterministic, ?key(Map, compile_opts)) of
154+
true -> [deterministic];
155+
false -> []
156+
end.
157+
156158
dynamic_form(#{module := Module, relative_file := RelativeFile,
157159
attributes := Attributes, definitions := Definitions, unreachable := Unreachable,
158160
deprecated := Deprecated, compile_opts := Opts} = Map) ->

0 commit comments

Comments
 (0)