Skip to content

Commit e126dab

Browse files
committed
Optimize protocol consolidation
* Allow directory listings to be pre-computed * Do not require struct modules to be loaded The current implementation would require us to recompute protocols whenever a struct changed, something that is not currently available. Furthermore, the fact we had to load modules naturally made consolidation more expensive. Therefore we need a lazy type resolution, and until we have such feature, we will only track struct names.
1 parent 3854728 commit e126dab

File tree

5 files changed

+92
-34
lines changed

5 files changed

+92
-34
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,17 @@ defmodule Module.Types.Apply do
10671067
"""
10681068
end
10691069

1070+
# The protocol has, at the moment, simplified clauses, so we build the complete one
1071+
clauses =
1072+
case mod.__protocol__(:impls) do
1073+
{:consolidated, mods} ->
1074+
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
1075+
[{[domain], dynamic()}]
1076+
1077+
_ ->
1078+
clauses
1079+
end
1080+
10701081
{"""
10711082
but expected a type that implements the #{inspect(mod)} protocol.
10721083
#{fix}\

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,25 @@ defmodule Module.Types.Of do
141141
{Any, term()}
142142
]
143143

144+
@doc """
145+
Currently, for protocol implementations, we only store
146+
the open struct definition. This is because we don't want
147+
to reconsolidate whenever the struct changes, but at the
148+
moment we can't store references either. Ideally struct
149+
types on protocol dispatches would be lazily resolved.
150+
"""
151+
def impl(for, mode \\ :closed)
152+
144153
for {for, type} <- impls do
145-
def impl(unquote(for)), do: unquote(Macro.escape(type))
154+
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
146155
end
147156

148-
def impl(struct) do
149-
# Elixir did not strictly require the implementation to be available, so we need a fallback.
157+
def impl(struct, mode) do
158+
# Elixir did not strictly require the implementation to be available,
159+
# so we need to deal with such cases accordingly.
150160
# TODO: Assume implementation is available on Elixir v2.0.
151-
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
161+
# A warning is emitted since v1.19+.
162+
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
152163
struct_type(struct, info)
153164
else
154165
open_map(__struct__: atom([struct]))

lib/elixir/lib/protocol.ex

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ defmodule Protocol do
453453
true
454454
455455
"""
456-
@spec extract_protocols([charlist | String.t()]) :: [atom]
456+
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
457457
def extract_protocols(paths) do
458458
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
459459
case attributes[:__protocol__] do
@@ -482,7 +482,7 @@ defmodule Protocol do
482482
true
483483
484484
"""
485-
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
485+
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
486486
def extract_impls(protocol, paths) when is_atom(protocol) do
487487
prefix = Atom.to_charlist(protocol) ++ [?.]
488488

@@ -496,17 +496,25 @@ defmodule Protocol do
496496

497497
defp extract_matching_by_attribute(paths, prefix, callback) do
498498
for path <- paths,
499-
# Do not use protocols as they may be consolidating
500-
path = if(is_list(path), do: path, else: String.to_charlist(path)),
501-
file <- list_dir(path),
499+
{path, files} = list_dir(path),
500+
file <- files,
502501
mod = extract_from_file(path, file, prefix, callback),
503502
do: mod
504503
end
505504

505+
# Do not use protocols as they may be consolidating
506+
defp list_dir({path, files}) when is_list(path) and is_list(files) do
507+
{path, files}
508+
end
509+
510+
defp list_dir(path) when is_binary(path) do
511+
list_dir(String.to_charlist(path))
512+
end
513+
506514
defp list_dir(path) when is_list(path) do
507515
case :file.list_dir(path) do
508-
{:ok, files} -> files
509-
_ -> []
516+
{:ok, files} -> {path, files}
517+
_ -> {path, []}
510518
end
511519
end
512520

@@ -633,7 +641,7 @@ defmodule Protocol do
633641
checker =
634642
if checker do
635643
update_in(checker.exports, fn exports ->
636-
signatures = new_signatures(definitions, protocol_funs, protocol, types)
644+
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
637645

638646
for {fun, info} <- exports do
639647
if sig = Map.get(signatures, fun) do
@@ -652,14 +660,13 @@ defmodule Protocol do
652660
end
653661
end
654662

655-
defp new_signatures(definitions, protocol_funs, protocol, types) do
663+
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
656664
alias Module.Types.Descr
665+
types_minus_any = List.delete(types, Any)
657666

658667
clauses =
659-
types
660-
|> List.delete(Any)
661-
|> Enum.map(fn impl ->
662-
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
668+
Enum.map(types_minus_any, fn impl ->
669+
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
663670
end)
664671

665672
{domain, impl_for, impl_for!} =
@@ -674,10 +681,16 @@ defmodule Protocol do
674681
end
675682

676683
_ ->
684+
structs_domain =
685+
case structs do
686+
[] -> Descr.none()
687+
_ -> Descr.open_map(__struct__: Descr.atom(structs))
688+
end
689+
677690
domain =
678-
clauses
679-
|> Enum.map(fn {[domain], _} -> domain end)
680-
|> Enum.reduce(&Descr.union/2)
691+
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
692+
Descr.union(Module.Types.Of.impl(impl, :open), acc)
693+
end)
681694

682695
not_domain = Descr.negation(domain)
683696

lib/elixir/test/elixir/protocol/consolidation_test.exs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,11 @@ defmodule Protocol.ConsolidationTest do
187187
assert domain == [term()]
188188

189189
assert clauses == [
190-
{[Of.impl(Map)], atom([WithAny.Map])},
191-
{[Of.impl(ImplStruct)], atom([WithAny.Protocol.ConsolidationTest.ImplStruct])},
192-
{[negation(union(Of.impl(ImplStruct), Of.impl(Map)))], atom([WithAny.Any])}
190+
{[Of.impl(Map, :open)], atom([WithAny.Map])},
191+
{[Of.impl(ImplStruct, :open)],
192+
atom([WithAny.Protocol.ConsolidationTest.ImplStruct])},
193+
{[negation(union(Of.impl(ImplStruct, :open), Of.impl(Map, :open)))],
194+
atom([WithAny.Any])}
193195
]
194196

195197
assert %{{:ok, 2} => %{sig: {:strong, nil, clauses}}} = exports
@@ -256,18 +258,34 @@ defmodule Protocol.ConsolidationTest do
256258
assert Inspect in protos
257259
end
258260

261+
test "protocols with expanded path" do
262+
path = to_charlist(Application.app_dir(:elixir, "ebin"))
263+
{:ok, mods} = :file.list_dir(path)
264+
protos = Protocol.extract_protocols([{path, mods}])
265+
assert Enumerable in protos
266+
assert Inspect in protos
267+
end
268+
259269
test "implementations with charlist path" do
260-
protos =
270+
impls =
261271
Protocol.extract_impls(Enumerable, [to_charlist(Application.app_dir(:elixir, "ebin"))])
262272

263-
assert List in protos
264-
assert Function in protos
273+
assert List in impls
274+
assert Function in impls
265275
end
266276

267277
test "implementations with binary path" do
268-
protos = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")])
269-
assert List in protos
270-
assert Function in protos
278+
impls = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")])
279+
assert List in impls
280+
assert Function in impls
281+
end
282+
283+
test "implementations with expanded path" do
284+
path = to_charlist(Application.app_dir(:elixir, "ebin"))
285+
{:ok, mods} = :file.list_dir(path)
286+
impls = Protocol.extract_impls(Enumerable, [{path, mods}])
287+
assert List in impls
288+
assert Function in impls
271289
end
272290
end
273291
end

lib/mix/lib/mix/compilers/protocol.ex

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,16 @@ defmodule Mix.Compilers.Protocol do
126126
end
127127

128128
defp consolidation_paths do
129-
filter_otp(:code.get_path(), :code.lib_dir())
130-
end
131-
132-
defp filter_otp(paths, otp) do
133-
Enum.filter(paths, &(not :lists.prefix(otp, &1)))
129+
otp = :code.lib_dir()
130+
131+
# Pre-list the paths in each directory for performance
132+
for path <- :code.get_path(), not :lists.prefix(otp, path) do
133+
{path,
134+
case :file.list_dir(path) do
135+
{:ok, files} -> files
136+
_ -> []
137+
end}
138+
end
134139
end
135140

136141
defp consolidate([], _paths, output, _opts) do

0 commit comments

Comments
 (0)