Skip to content

Commit ab7c019

Browse files
committed
Topsort Erlang modules before compilation, closes #15100
1 parent f2e61f6 commit ab7c019

7 files changed

Lines changed: 89 additions & 101 deletions

File tree

lib/mix/lib/mix/dep/converger.ex

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,17 @@ defmodule Mix.Dep.Converger do
3434
Enum.find(deps, fn %Mix.Dep{app: other_app} -> app == other_app end)
3535
end)
3636
else
37-
find_cycle(graph)
37+
Mix.raise(
38+
"Could not sort dependencies. " <>
39+
"The following dependencies form a cycle: " <>
40+
Enum.join(Mix.Utils.find_cycle!(graph), ", ")
41+
)
3842
end
3943
after
4044
:digraph.delete(graph)
4145
end
4246
end
4347

44-
@doc """
45-
A Depth-First Search to find where is the dependency graph cycle
46-
and then display the cyclic dependencies back to the developer.
47-
"""
48-
def find_cycle(graph), do: find_cycle(graph, :digraph.vertices(graph), MapSet.new())
49-
50-
defp find_cycle(_, [], _visited), do: nil
51-
52-
defp find_cycle(graph, [v | vs], visited) do
53-
if v in visited, do: cycle_found(visited)
54-
55-
find_cycle(graph, :digraph.out_neighbours(graph, v), MapSet.put(visited, v))
56-
find_cycle(graph, vs, visited)
57-
end
58-
59-
defp cycle_found(visited) do
60-
Mix.raise(
61-
"Could not sort dependencies. " <>
62-
"The following dependencies form a cycle: " <>
63-
Enum.join(visited, ", ")
64-
)
65-
end
66-
6748
@doc """
6849
Converge without lock and accumulator updates.
6950

lib/mix/lib/mix/tasks/compile.erlang.ex

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,12 @@ defmodule Mix.Tasks.Compile.Erlang do
8383
end)
8484

8585
compile_path = Path.relative_to(compile_path, File.cwd!())
86+
erls = scan_sources(files, include_path, source_paths, compile_path, opts)
8687

87-
{erls, tuples} =
88-
Enum.unzip(scan_sources(files, include_path, source_paths, compile_path, opts))
88+
{sorted, parallel} = topsort_and_parallelize(erls, compile_path)
89+
opts = [parallel: MapSet.new(parallel)] ++ opts
8990

90-
opts = [parallel: MapSet.new(find_parallel(erls))] ++ opts
91-
92-
Erlang.compile_entries(manifest(), tuples, :erl, :beam, opts, fn input, _output ->
91+
Erlang.compile_entries(manifest(), sorted, :erl, :beam, opts, fn input, _output ->
9392
# We're purging the module because a previous compiler (for example, Phoenix)
9493
# might have already loaded the previous version of it.
9594
module = input |> Path.basename(".erl") |> String.to_atom()
@@ -147,7 +146,7 @@ defmodule Mix.Tasks.Compile.Erlang do
147146
ordered: false
148147
)
149148
|> Enum.flat_map(fn
150-
{:ok, {:ok, erl_file, target_tuple}} -> [{erl_file, target_tuple}]
149+
{:ok, {:ok, erl_file}} -> [erl_file]
151150
{:ok, :error} -> []
152151
end)
153152
end
@@ -156,17 +155,15 @@ defmodule Mix.Tasks.Compile.Erlang do
156155
erl_file = %{
157156
file: file,
158157
module: module_from_artifact(file),
159-
behaviours: [],
160-
compile: [],
158+
deps: [],
161159
includes: [],
162-
invalid: false
160+
status: :ok
163161
}
164162

165163
case :epp.parse_file(Erlang.to_erl_file(file), include_paths, []) do
166164
{:ok, forms} ->
167165
erl_file = List.foldl(tl(forms), erl_file, &do_form(file, &1, &2))
168-
target_tuple = annotate_target(erl_file, compile_path, opts[:force])
169-
{:ok, erl_file, target_tuple}
166+
{:ok, maybe_stale(erl_file, compile_path, opts[:force])}
170167

171168
{:error, _error} ->
172169
:error
@@ -183,49 +180,71 @@ defmodule Mix.Tasks.Compile.Erlang do
183180
end
184181

185182
{:attribute, _, :behaviour, behaviour} ->
186-
%{erl | behaviours: [behaviour | erl.behaviours]}
183+
%{erl | deps: [behaviour | erl.deps]}
187184

188185
{:attribute, _, :behavior, behaviour} ->
189-
%{erl | behaviours: [behaviour | erl.behaviours]}
186+
%{erl | deps: [behaviour | erl.deps]}
190187

191188
{:attribute, _, :compile, value} when is_list(value) ->
192-
%{erl | compile: value ++ erl.compile}
189+
%{erl | deps: Enum.reduce(value, erl.deps, &add_parse_transforms/2)}
193190

194191
{:attribute, _, :compile, value} ->
195-
%{erl | compile: [value | erl.compile]}
192+
%{erl | deps: add_parse_transforms(value, erl.deps)}
196193

197194
_ ->
198195
erl
199196
end
200197
end
201198

202-
defp find_parallel(erls) do
203-
serial = MapSet.new(find_dependencies(erls))
199+
defp topsort_and_parallelize(erls, compile_path) do
200+
graph = :digraph.new()
204201

205-
erls
206-
|> Enum.reject(&(&1.module in serial))
207-
|> Enum.map(& &1.file)
208-
end
202+
for %{module: module, status: status, file: file} <- erls do
203+
:digraph.add_vertex(graph, module, {file, status})
204+
end
205+
206+
for %{module: module, deps: deps} <- erls, dep <- deps do
207+
# It may error if the behaviour/parse transform is not in the project,
208+
# which is expected
209+
:digraph.add_edge(graph, module, dep)
210+
end
209211

210-
defp find_dependencies(erls) do
211-
Enum.flat_map(erls, fn erl ->
212-
transforms =
213-
Enum.flat_map(erl.compile, fn
214-
{:parse_transform, transform} -> [transform]
215-
_ -> []
212+
if vertices = :digraph_utils.topsort(graph) do
213+
sorted =
214+
Enum.reduce(vertices, [], fn module, acc ->
215+
{_, {file, status}} = :digraph.vertex(graph, module)
216+
[{status, file, Path.join(compile_path, "#{module}.beam")} | acc]
216217
end)
217218

218-
transforms ++ erl.behaviours
219+
parallel =
220+
for %{file: file, module: module} <- erls,
221+
:digraph.in_neighbours(graph, module) == [],
222+
do: file
223+
224+
{sorted, parallel}
225+
else
226+
Mix.raise(
227+
"Could not compile Erlang. " <>
228+
"The following modules form a cycle: " <>
229+
Enum.join(Mix.Utils.find_cycle!(graph), ", ")
230+
)
231+
end
232+
end
233+
234+
defp add_parse_transforms(compile, deps) do
235+
Enum.reduce(compile, deps, fn
236+
{:parse_transform, transform}, deps -> [transform | deps]
237+
_, deps -> deps
219238
end)
220239
end
221240

222-
defp annotate_target(erl, compile_path, force) do
241+
defp maybe_stale(erl, compile_path, force) do
223242
beam = Path.join(compile_path, "#{erl.module}.beam")
224243

225244
if force || Mix.Utils.stale?([erl.file | erl.includes], [beam]) do
226-
{:stale, erl.file, beam}
245+
%{erl | status: :stale}
227246
else
228-
{:ok, erl.file, beam}
247+
erl
229248
end
230249
end
231250

lib/mix/lib/mix/utils.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,4 +1049,22 @@ defmodule Mix.Utils do
10491049
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
10501050
defp to_erl_tail([]), do: []
10511051
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
1052+
1053+
@doc """
1054+
A Depth-First Search to find where is the dependency graph cycle
1055+
and then display the cyclic dependencies back to the developer.
1056+
"""
1057+
def find_cycle!(graph),
1058+
do: find_cycle(graph, :digraph.vertices(graph), MapSet.new()) || raise("no cycle found")
1059+
1060+
defp find_cycle(_, [], _visited), do: nil
1061+
1062+
defp find_cycle(graph, [v | vs], visited) do
1063+
if v in visited do
1064+
visited
1065+
else
1066+
find_cycle(graph, :digraph.out_neighbours(graph, v), MapSet.put(visited, v)) ||
1067+
find_cycle(graph, vs, visited)
1068+
end
1069+
end
10521070
end
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-module(b).
2-
-export([b/0]).
2+
-export([z/0]).
33

4-
-callback c() -> term().
4+
-callback b() -> term().
55
-record(br, {cell=undefined}).
66

7-
b() -> #br{cell=specified}.
7+
z() -> #br{cell=specified}.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-module(c).
2-
-export([c/0]).
2+
-export([b/0]).
33

44
-include("r.hrl").
55
-behaviour(b).
66

7-
c() -> #r{cell=specified}.
7+
b() -> #r{cell=specified}.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
%% Have modules flowing in both directions to check for topsort
2+
-module(z).
3+
-callback z() -> term().

lib/mix/test/mix/tasks/compile.erlang_test.exs

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,29 @@ defmodule Mix.Tasks.Compile.ErlangTest do
2828
end)
2929
end
3030

31-
test "compiles and cleans src/b.erl and src/c.erl" do
31+
test "compiles and cleans src/b.erl, src/c.erl, and src/z.erl" do
3232
in_fixture("compile_erlang", fn ->
3333
assert Mix.Tasks.Compile.Erlang.run(["--verbose"]) == {:ok, []}
3434
assert_received {:mix_shell, :info, ["Compiled src/b.erl"]}
3535
assert_received {:mix_shell, :info, ["Compiled src/c.erl"]}
36+
assert_received {:mix_shell, :info, ["Compiled src/z.erl"]}
3637

3738
assert File.regular?("_build/dev/lib/sample/ebin/b.beam")
3839
assert File.regular?("_build/dev/lib/sample/ebin/c.beam")
40+
assert File.regular?("_build/dev/lib/sample/ebin/z.beam")
3941

4042
assert Mix.Tasks.Compile.Erlang.run(["--verbose"]) == {:noop, []}
4143
refute_received {:mix_shell, :info, ["Compiled src/b.erl"]}
4244

4345
assert Mix.Tasks.Compile.Erlang.run(["--force", "--verbose"]) == {:ok, []}
4446
assert_received {:mix_shell, :info, ["Compiled src/b.erl"]}
4547
assert_received {:mix_shell, :info, ["Compiled src/c.erl"]}
48+
assert_received {:mix_shell, :info, ["Compiled src/z.erl"]}
4649

4750
assert Mix.Tasks.Compile.Erlang.clean()
4851
refute File.regular?("_build/dev/lib/sample/ebin/b.beam")
4952
refute File.regular?("_build/dev/lib/sample/ebin/c.beam")
53+
refute File.regular?("_build/dev/lib/sample/ebin/z.beam")
5054
end)
5155
end
5256

@@ -182,53 +186,16 @@ defmodule Mix.Tasks.Compile.ErlangTest do
182186

183187
ensure_touched(file)
184188

185-
output =
186-
capture_io(fn ->
187-
Mix.Tasks.Compile.Erlang.run(["--all-warnings"])
188-
end)
189-
190-
assert output == ""
191-
end)
192-
end
193-
194-
test "returns syntax error from an Erlang file when --return-errors is set" do
195-
in_fixture("no_mixfile", fn ->
196-
import ExUnit.CaptureIO
197-
198-
file = Path.absname("src/a.erl")
199-
source = deterministic_source(file)
200-
201-
File.mkdir!("src")
202-
203-
File.write!(file, """
204-
-module(b).
205-
def b(), do: b
206-
""")
207-
208-
capture_io(fn ->
209-
assert {:error, [diagnostic]} =
210-
Mix.Tasks.Compile.Erlang.run(["--force", "--return-errors"])
211-
212-
assert %Mix.Task.Compiler.Diagnostic{
213-
compiler_name: "erl_parse",
214-
file: ^source,
215-
source: ^source,
216-
message: "syntax error before: b",
217-
position: position(2, 5),
218-
severity: :error
219-
} = diagnostic
220-
end)
221-
222-
refute File.regular?("ebin/Elixir.A.beam")
223-
refute File.regular?("ebin/Elixir.B.beam")
189+
assert capture_io(fn ->
190+
Mix.Tasks.Compile.Erlang.run(["--all-warnings"])
191+
end) == ""
224192
end)
225193
end
226194

227195
@tag erlc_options: [{:warnings_as_errors, true}]
228196
test "adds :debug_info to erlc_options by default" do
229197
in_fixture("compile_erlang", fn ->
230198
Mix.Tasks.Compile.Erlang.run([])
231-
232199
binary = File.read!("_build/dev/lib/sample/ebin/b.beam")
233200

234201
{:ok, {_, [debug_info: {:debug_info_v1, _, {debug_info, _}}]}} =

0 commit comments

Comments
 (0)