From fd1f1c6fd309e9449c55a83bad22896dc42887c5 Mon Sep 17 00:00:00 2001 From: Vitor Cavalcanti <1920397+vrcca@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:31:37 +0200 Subject: [PATCH 01/59] Add Access.slice/1 (#12008) --- lib/elixir/lib/access.ex | 114 +++++++++++++++++++++++++ lib/elixir/test/elixir/access_test.exs | 80 +++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/lib/elixir/lib/access.ex b/lib/elixir/lib/access.ex index 7dfe056ff62..49a8c9a4955 100644 --- a/lib/elixir/lib/access.ex +++ b/lib/elixir/lib/access.ex @@ -829,4 +829,118 @@ defmodule Access do defp get_and_update_filter([], _func, _next, updates, gets) do {:lists.reverse(gets), :lists.reverse(updates)} end + + @doc ~S""" + Returns a function that accesses all items of a list that are within the provided range. + + The range will be normalized following the same rules from `Enum.slice/2`. + + The returned function is typically passed as an accessor to `Kernel.get_in/2`, + `Kernel.get_and_update_in/3`, and friends. + + ## Examples + + iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}] + iex> get_in(list, [Access.slice(1..2), :name]) + ["francine", "vitor"] + iex> get_and_update_in(list, [Access.slice(1..3//2), :name], fn prev -> + ...> {prev, String.upcase(prev)} + ...> end) + {["francine"], [%{name: "john", salary: 10}, %{name: "FRANCINE", salary: 30}, %{name: "vitor", salary: 25}]} + + `slice/1` can also be used to pop elements out of a list or + a key inside of a list: + + iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}] + iex> pop_in(list, [Access.slice(-2..-1)]) + {[%{name: "francine", salary: 30}, %{name: "vitor", salary: 25}], [%{name: "john", salary: 10}]} + iex> pop_in(list, [Access.slice(-2..-1), :name]) + {["francine", "vitor"], [%{name: "john", salary: 10}, %{salary: 30}, %{salary: 25}]} + + When no match is found, an empty list is returned and the update function is never called + + iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}] + iex> get_in(list, [Access.slice(5..10//2), :name]) + [] + iex> get_and_update_in(list, [Access.slice(5..10//2), :name], fn prev -> + ...> {prev, String.upcase(prev)} + ...> end) + {[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]} + + An error is raised if the accessed structure is not a list: + + iex> get_in(%{}, [Access.slice(2..10//3)]) + ** (ArgumentError) Access.slice/1 expected a list, got: %{} + + An error is raised if the step of the range is negative: + + iex> get_in([], [Access.slice(2..10//-1)]) + ** (ArgumentError) Access.slice/1 does not accept ranges with negative steps, got: 2..10//-1 + + """ + @doc since: "1.14" + @spec slice(Range.t()) :: access_fun(data :: list, current_value :: list) + def slice(%Range{} = range) do + if range.step > 0 do + fn op, data, next -> slice(op, data, range, next) end + else + raise ArgumentError, + "Access.slice/1 does not accept ranges with negative steps, got: #{inspect(range)}" + end + end + + defp slice(:get, data, %Range{} = range, next) when is_list(data) do + data + |> Enum.slice(range) + |> Enum.map(next) + end + + defp slice(:get_and_update, data, range, next) when is_list(data) do + range = normalize_range(range, data) + + if range.first > range.last do + {[], data} + else + get_and_update_slice(data, range, next, [], [], 0) + end + end + + defp slice(_op, data, _range, _next) do + raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}" + end + + defp normalize_range(%Range{first: first, last: last, step: step}, list) + when first < 0 or last < 0 do + count = length(list) + first = if first >= 0, do: first, else: Kernel.max(first + count, 0) + last = if last >= 0, do: last, else: last + count + Range.new(first, last, step) + end + + defp normalize_range(range, _list), do: range + + defp get_and_update_slice([head | rest], range, next, updates, gets, index) do + if index in range do + case next.(head) do + :pop -> + get_and_update_slice(rest, range, next, updates, [head | gets], index + 1) + + {get, update} -> + get_and_update_slice( + rest, + range, + next, + [update | updates], + [get | gets], + index + 1 + ) + end + else + get_and_update_slice(rest, range, next, [head | updates], gets, index + 1) + end + end + + defp get_and_update_slice([], _range, _next, updates, gets, _index) do + {:lists.reverse(gets), :lists.reverse(updates)} + end end diff --git a/lib/elixir/test/elixir/access_test.exs b/lib/elixir/test/elixir/access_test.exs index 04b7ade8377..b1ae1fb8848 100644 --- a/lib/elixir/test/elixir/access_test.exs +++ b/lib/elixir/test/elixir/access_test.exs @@ -135,6 +135,86 @@ defmodule AccessTest do end end + describe "slice/1" do + @test_list [1, 2, 3, 4, 5, 6, 7] + + test "retrieves a range from the start of the list" do + assert [2, 3] == get_in(@test_list, [Access.slice(1..2)]) + end + + test "retrieves a range from the end of the list" do + assert [6, 7] == get_in(@test_list, [Access.slice(-2..-1)]) + end + + test "retrieves a range from positive first and negative last" do + assert [2, 3, 4, 5, 6] == get_in(@test_list, [Access.slice(1..-2//1)]) + end + + test "retrieves a range from negative first and positive last" do + assert [6, 7] == get_in(@test_list, [Access.slice(-2..7//1)]) + end + + test "retrieves a range with steps" do + assert [1, 3] == get_in(@test_list, [Access.slice(0..2//2)]) + assert [2, 5] == get_in(@test_list, [Access.slice(1..4//3)]) + assert [2] == get_in(@test_list, [Access.slice(1..2//3)]) + assert [1, 3, 5, 7] == get_in(@test_list, [Access.slice(0..6//2)]) + end + + test "pops a range from the start of the list" do + assert {[2, 3], [1, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2)]) + end + + test "pops a range from the end of the list" do + assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..-1)]) + end + + test "pops a range from positive first and negative last" do + assert {[2, 3, 4, 5, 6], [1, 7]} == pop_in(@test_list, [Access.slice(1..-2//1)]) + end + + test "pops a range from negative first and positive last" do + assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..7//1)]) + end + + test "pops a range with steps" do + assert {[1, 3, 5], [2, 4, 6, 7]} == pop_in(@test_list, [Access.slice(0..4//2)]) + assert {[2], [1, 3, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2//2)]) + assert {[1, 4], [1, 2, 5, 6, 7]} == pop_in([1, 2, 1, 4, 5, 6, 7], [Access.slice(2..3)]) + end + + test "updates range from the start of the list" do + assert [-1, 2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(0..0)], &(&1 * -1)) + + assert [1, -2, -3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(1..2)], &(&1 * -1)) + end + + test "updates range from the end of the list" do + assert [1, 2, 3, 4, 5, -6, -7] == update_in(@test_list, [Access.slice(-2..-1)], &(&1 * -1)) + + assert [-1, -2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(-7..-6)], &(&1 * -1)) + end + + test "updates a range from positive first and negative last" do + assert [1, -2, -3, -4, -5, -6, 7] == + update_in(@test_list, [Access.slice(1..-2//1)], &(&1 * -1)) + end + + test "updates a range from negative first and positive last" do + assert [1, 2, 3, 4, 5, -6, -7] == + update_in(@test_list, [Access.slice(-2..7//1)], &(&1 * -1)) + end + + test "updates a range with steps" do + assert [-1, 2, -3, 4, -5, 6, 7] == + update_in(@test_list, [Access.slice(0..4//2)], &(&1 * -1)) + end + + test "returns empty when the start of the range is greater than the end" do + assert [] == get_in(@test_list, [Access.slice(2..1//1)]) + end + end + describe "at/1" do @test_list [1, 2, 3, 4, 5, 6] From c47b731f75d69c4a90de78c0d0cdd1b0f7ecb82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 26 Jul 2022 18:51:35 +0200 Subject: [PATCH 02/59] Track all arities in imports inside quotes, closes #11651 --- lib/elixir/lib/kernel/lexical_tracker.ex | 23 ++++++ lib/elixir/src/elixir_dispatch.erl | 79 +++++++++++++------ lib/elixir/src/elixir_lexical.erl | 3 + lib/elixir/src/elixir_quote.erl | 4 +- .../test/elixir/kernel/expansion_test.exs | 24 +++--- lib/elixir/test/elixir/kernel/quote_test.exs | 9 ++- 6 files changed, 103 insertions(+), 39 deletions(-) diff --git a/lib/elixir/lib/kernel/lexical_tracker.ex b/lib/elixir/lib/kernel/lexical_tracker.ex index 0b7ed7c0505..8c9aa523b82 100644 --- a/lib/elixir/lib/kernel/lexical_tracker.ex +++ b/lib/elixir/lib/kernel/lexical_tracker.ex @@ -59,6 +59,11 @@ defmodule Kernel.LexicalTracker do :gen_server.cast(pid, {:alias_dispatch, module}) end + @doc false + def import_quoted(pid, module, function, arities) when is_atom(module) do + :gen_server.cast(pid, {:import_quoted, module, function, arities}) + end + @doc false def add_compile_env(pid, app, path, return) do :gen_server.cast(pid, {:compile_env, app, path, return}) @@ -170,6 +175,24 @@ defmodule Kernel.LexicalTracker do {:noreply, %{state | aliases: Map.delete(aliases, module)}} end + def handle_cast({:import_quoted, module, function, arities}, state) do + %{imports: imports} = state + + imports = + case imports do + %{^module => modules_and_fas} -> + arities + |> Enum.reduce(modules_and_fas, &Map.delete(&2, {function, &1})) + |> Map.delete(module) + |> then(&Map.put(imports, module, &1)) + + %{} -> + imports + end + + {:noreply, %{state | imports: imports}} + end + def handle_cast({:set_file, file}, state) do {:noreply, %{state | file: file}} end diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 0f20e6ddfe5..77216f9a633 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -6,7 +6,7 @@ require_function/5, import_function/4, expand_import/7, expand_require/6, default_functions/0, default_macros/0, default_requires/0, - find_import/4, find_imports/4, format_error/1]). + find_import/4, find_imports/3, format_error/1]). -include("elixir.hrl"). -import(ordsets, [is_element/2]). -define(kernel, 'Elixir.Kernel'). @@ -25,7 +25,7 @@ default_requires() -> find_import(Meta, Name, Arity, E) -> Tuple = {Name, Arity}, - case find_dispatch(Meta, Tuple, [], E) of + case find_import_by_name_arity(Meta, Tuple, [], E) of {function, Receiver} -> elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), Receiver; @@ -36,25 +36,37 @@ find_import(Meta, Name, Arity, E) -> false end. -find_imports(Meta, Name, Arity, E) -> - Tuple = {Name, Arity}, +find_imports(Meta, Name, E) -> + Funs = ?key(E, functions), + Macs = ?key(E, macros), - case find_dispatch(Meta, Tuple, [], E) of - {function, Receiver} -> - elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), - [{Receiver, Arity}]; - {macro, Receiver} -> - elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E), - [{Receiver, Arity}]; - _ -> - [] - end. + Acc0 = #{}, + Acc1 = find_imports_by_name(Funs, Acc0, Name, Meta, E), + Acc2 = find_imports_by_name(Macs, Acc1, Name, Meta, E), + + Imports = lists:sort(maps:to_list(Acc2)), + trace_import_quoted(Imports, Meta, Name, E), + Imports. + +trace_import_quoted([{Arity, Mod} | Imports], Meta, Name, E) -> + {Rest, Arities} = collect_trace_import_quoted(Imports, Mod, [], [Arity]), + elixir_env:trace({imported_quoted, Meta, Mod, Name, Arities}, E), + trace_import_quoted(Rest, Meta, Name, E); +trace_import_quoted([], _Meta, _Name, _E) -> + ok. + +collect_trace_import_quoted([{Arity, Mod} | Imports], Mod, Acc, Arities) -> + collect_trace_import_quoted(Imports, Mod, Acc, [Arity | Arities]); +collect_trace_import_quoted([Import | Imports], Mod, Acc, Arities) -> + collect_trace_import_quoted(Imports, Mod, [Import | Acc], Arities); +collect_trace_import_quoted([], _Mod, Acc, Arities) -> + {lists:reverse(Acc), lists:reverse(Arities)}. %% Function retrieval import_function(Meta, Name, Arity, E) -> Tuple = {Name, Arity}, - case find_dispatch(Meta, Tuple, [], E) of + case find_import_by_name_arity(Meta, Tuple, [], E) of {function, Receiver} -> elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), elixir_locals:record_import(Tuple, Receiver, ?key(E, module), ?key(E, function)), @@ -134,15 +146,14 @@ dispatch_require(_Meta, Receiver, Name, Args, _S, _E, Callback) -> expand_import(Meta, {Name, Arity} = Tuple, Args, S, E, Extra, External) -> Module = ?key(E, module), Function = ?key(E, function), - Dispatch = find_dispatch(Meta, Tuple, Extra, E), + Dispatch = find_import_by_name_arity(Meta, Tuple, Extra, E), case Dispatch of {import, _} -> do_expand_import(Meta, Tuple, Args, Module, S, E, Dispatch); _ -> AllowLocals = External orelse ((Function /= nil) andalso (Function /= Tuple)), - Local = AllowLocals andalso - elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E), + Local = AllowLocals andalso elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E), case Dispatch of %% There is a local and an import. This is a conflict unless @@ -246,15 +257,35 @@ caller(Line, E) -> required(Meta) -> lists:keyfind(required, 1, Meta) == {required, true}. -find_dispatch(Meta, {_Name, Arity} = Tuple, Extra, E) -> +find_imports_by_name([{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> + NewAcc = find_imports_by_name(Name, Imports, Acc, Mod, Meta, E), + find_imports_by_name(ModImports, NewAcc, Name, Meta, E); +find_imports_by_name([], Acc, _Name, _Meta, _E) -> + Acc. + +find_imports_by_name(Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> + case Acc of + #{Arity := OtherMod} -> + Error = {ambiguous_call, {Mod, OtherMod, Name, Arity}}, + elixir_errors:form_error(Meta, E, ?MODULE, Error); + + #{} -> + find_imports_by_name(Name, Imports, Acc#{Arity => Mod}, Mod, Meta, E) + end; +find_imports_by_name(Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> + find_imports_by_name(Name, Imports, Acc, Mod, Meta, E); +find_imports_by_name(_Name, _Imports, Acc, _Mod, _Meta, _E) -> + Acc. + +find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> case is_import(Meta, Arity) of {import, _} = Import -> Import; false -> Funs = ?key(E, functions), Macs = Extra ++ ?key(E, macros), - FunMatch = find_dispatch(Tuple, Funs), - MacMatch = find_dispatch(Tuple, Macs), + FunMatch = find_import_by_name_arity(Tuple, Funs), + MacMatch = find_import_by_name_arity(Tuple, Macs), case {FunMatch, MacMatch} of {[], [Receiver]} -> {macro, Receiver}; @@ -268,7 +299,7 @@ find_dispatch(Meta, {_Name, Arity} = Tuple, Extra, E) -> end end. -find_dispatch(Tuple, List) -> +find_import_by_name_arity(Tuple, List) -> [Receiver || {Receiver, Set} <- List, is_element(Tuple, Set)]. is_import(Meta, Arity) -> @@ -276,8 +307,8 @@ is_import(Meta, Arity) -> {imports, Imports} -> case lists:keyfind(context, 1, Meta) of {context, _} -> - case lists:keyfind(Arity, 2, Imports) of - {Receiver, Arity} -> {import, Receiver}; + case lists:keyfind(Arity, 1, Imports) of + {Arity, Receiver} -> {import, Receiver}; false -> false end; false -> false diff --git a/lib/elixir/src/elixir_lexical.erl b/lib/elixir/src/elixir_lexical.erl index 4cfd53bc267..ba0fa455e28 100644 --- a/lib/elixir/src/elixir_lexical.erl +++ b/lib/elixir/src/elixir_lexical.erl @@ -70,6 +70,9 @@ trace({imported_function, _Meta, Module, Function, Arity}, #{lexical_tracker := trace({imported_macro, _Meta, Module, Function, Arity}, #{lexical_tracker := Pid}) -> ?tracker:import_dispatch(Pid, Module, {Function, Arity}, compile), ok; +trace({imported_quoted, _Meta, Module, Function, Arities}, #{lexical_tracker := Pid}) -> + ?tracker:import_quoted(Pid, Module, Function, Arities), + ok; trace({compile_env, App, Path, Return}, #{lexical_tracker := Pid}) -> ?tracker:add_compile_env(Pid, App, Path, Return), ok; diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index b5e53739403..dc77ca685ce 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -330,7 +330,7 @@ do_quote({'&', Meta, [{'/', _, [{F, _, C}, A]}] = Args}, Meta; Receiver -> - keystore(context, keystore(imports, Meta, [{Receiver, A}]), Q#elixir_quote.context) + keystore(context, keystore(imports, Meta, [{A, Receiver}]), Q#elixir_quote.context) end, do_quote_tuple('&', NewMeta, Args, Q, E); @@ -380,7 +380,7 @@ do_quote(Other, _, _) -> import_meta(Meta, Name, Arity, Q, E) -> case (keyfind(import, Meta) == false) andalso - elixir_dispatch:find_imports(Meta, Name, Arity, E) of + elixir_dispatch:find_imports(Meta, Name, E) of [] -> case (Arity == 1) andalso keyfind(ambiguous_op, Meta) of {ambiguous_op, nil} -> keystore(ambiguous_op, Meta, Q#elixir_quote.context); diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 300ea617d4f..0a55616ed8f 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -2306,7 +2306,7 @@ defmodule Kernel.ExpansionTest do end test "expands size * unit" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] import Kernel.ExpansionTarget assert expand(quote(do: <>)) |> clean_meta([:alignment]) == @@ -2338,7 +2338,7 @@ defmodule Kernel.ExpansionTest do end test "expands binary/bitstring specifiers" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == quote(do: <>) @@ -2363,7 +2363,7 @@ defmodule Kernel.ExpansionTest do end test "expands utf* specifiers" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == quote(do: <>) @@ -2386,7 +2386,7 @@ defmodule Kernel.ExpansionTest do end test "expands numbers specifiers" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == quote(do: <>) @@ -2412,7 +2412,7 @@ defmodule Kernel.ExpansionTest do end test "expands macro specifiers" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] import Kernel.ExpansionTarget assert expand(quote(do: <>)) |> clean_meta([:alignment]) == @@ -2424,7 +2424,7 @@ defmodule Kernel.ExpansionTest do end test "expands macro in args" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] before_expansion = quote do @@ -2442,7 +2442,7 @@ defmodule Kernel.ExpansionTest do end test "supports dynamic size" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] before_expansion = quote do @@ -2471,7 +2471,7 @@ defmodule Kernel.ExpansionTest do end test "merges bitstrings" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>, z>>)) |> clean_meta([:alignment]) == quote(do: <>) @@ -2482,7 +2482,7 @@ defmodule Kernel.ExpansionTest do end test "merges binaries" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: "foo" <> x)) |> clean_meta([:alignment]) == quote(do: <<"foo"::binary(), x()::binary()>>) @@ -2496,7 +2496,7 @@ defmodule Kernel.ExpansionTest do end test "guard expressions on size" do - import Kernel, except: [-: 2, +: 2, length: 1] + import Kernel, except: [-: 1, -: 2, +: 1, +: 2, length: 1] # Arithmetic operations with literals and variables are valid expressions # for bitstring size in OTP 23+ @@ -2524,7 +2524,7 @@ defmodule Kernel.ExpansionTest do end test "map lookup on size" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] before_expansion = quote do @@ -2572,7 +2572,7 @@ defmodule Kernel.ExpansionTest do # TODO: Simplify when we require Erlang/OTP 24 if System.otp_release() >= "24" do test "16-bit floats" do - import Kernel, except: [-: 2] + import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <<12.3::float-16>>)) |> clean_meta([:alignment]) == quote(do: <<12.3::float()-size(16)>>) diff --git a/lib/elixir/test/elixir/kernel/quote_test.exs b/lib/elixir/test/elixir/kernel/quote_test.exs index c0b81d55ba7..41609c8d054 100644 --- a/lib/elixir/test/elixir/kernel/quote_test.exs +++ b/lib/elixir/test/elixir/kernel/quote_test.exs @@ -566,6 +566,12 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do end end + defmacrop get_list_length_with_pipe do + quote do + 'hello' |> length() + end + end + defmacrop get_list_length_with_partial do quote do (&length(&1)).('hello') @@ -581,6 +587,7 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do test "expand imports" do import Kernel, except: [length: 1] assert get_list_length() == 5 + assert get_list_length_with_pipe() == 5 assert get_list_length_with_partial() == 5 assert get_list_length_with_function() == 5 end @@ -631,6 +638,6 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do test "checks the context also for variables to zero-arity functions" do import BinaryUtils {:int32, meta, __MODULE__} = quote(do: int32) - assert meta[:imports] == [{BinaryUtils, 0}] + assert meta[:imports] == [{0, BinaryUtils}] end end From fc7d8de987dd0ea8bb10440cf3bd1e820f73c2cb Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 27 Jul 2022 15:25:13 +0900 Subject: [PATCH 03/59] Remove dialyzer workarounds (#12014) --- lib/ex_unit/lib/ex_unit/assertions.ex | 37 ++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index c2332ab958c..d4def8fe9c7 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -106,9 +106,6 @@ defmodule ExUnit.Assertions do defmacro assert({:=, meta, [left, right]} = assertion) do code = escape_quoted(:assert, meta, assertion) - # If the match works, we need to check if the value - # is not nil nor false. We need to rewrite the if - # to avoid silly dialyzer warnings though. check = quote generated: true do if right do @@ -151,15 +148,13 @@ defmodule ExUnit.Assertions do {args, value} = extract_args(assertion, __CALLER__) quote generated: true do - case unquote(value) do - value when value in [nil, false] -> - raise ExUnit.AssertionError, - args: unquote(args), - expr: unquote(escape_quoted(:assert, [], assertion)), - message: "Expected truthy, got #{inspect(value)}" - - value -> - value + if value = unquote(value) do + value + else + raise ExUnit.AssertionError, + args: unquote(args), + expr: unquote(escape_quoted(:assert, [], assertion)), + message: "Expected truthy, got #{inspect(value)}" end end end @@ -213,18 +208,14 @@ defmodule ExUnit.Assertions do else {args, value} = extract_args(assertion, __CALLER__) - # if value, raise - # We need to rewrite it to avoid dialyzer warnings though. quote generated: true do - case unquote(value) do - value when value in [nil, false] -> - value - - value -> - raise ExUnit.AssertionError, - args: unquote(args), - expr: unquote(escape_quoted(:refute, [], assertion)), - message: "Expected false or nil, got #{inspect(value)}" + if value = unquote(value) do + raise ExUnit.AssertionError, + args: unquote(args), + expr: unquote(escape_quoted(:refute, [], assertion)), + message: "Expected false or nil, got #{inspect(value)}" + else + value end end end From 06f5f313915390cff576b5a2680d11f9eb46f1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Wed, 27 Jul 2022 08:42:55 +0200 Subject: [PATCH 04/59] Fix passing of File.cp_r! on_conflict option (#12015) --- lib/mix/lib/mix/release.ex | 2 +- lib/mix/lib/mix/utils.ex | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/mix/lib/mix/release.ex b/lib/mix/lib/mix/release.ex index d5664b3c57b..3e0ed337dff 100644 --- a/lib/mix/lib/mix/release.ex +++ b/lib/mix/lib/mix/release.ex @@ -773,7 +773,7 @@ defmodule Mix.Release do release.erts_source |> Path.join("bin") - |> File.cp_r!(destination, fn _, _ -> false end) + |> File.cp_r!(destination, on_conflict: fn _, _ -> false end) _ = File.rm(Path.join(destination, "erl")) _ = File.rm(Path.join(destination, "erl.ini")) diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex index 1e481bd073f..9c1914860c3 100644 --- a/lib/mix/lib/mix/utils.ex +++ b/lib/mix/lib/mix/utils.ex @@ -518,11 +518,13 @@ defmodule Mix.Utils do {:error, _} -> files = - File.cp_r!(source, target, fn orig, dest -> - {orig_mtime, orig_size} = last_modified_and_size(orig) - {dest_mtime, dest_size} = last_modified_and_size(dest) - orig_mtime > dest_mtime or orig_size != dest_size - end) + File.cp_r!(source, target, + on_conflict: fn orig, dest -> + {orig_mtime, orig_size} = last_modified_and_size(orig) + {dest_mtime, dest_size} = last_modified_and_size(dest) + orig_mtime > dest_mtime or orig_size != dest_size + end + ) {:ok, files} end From 7cc21eaf88d16d9a171b52cc24dd26fdea91f15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 10:25:59 +0200 Subject: [PATCH 05/59] Convert Mix.State to a process and use better storage * Use ETS instead of going through an Agent * Use persistent_term for long term storage --- lib/mix/lib/mix/scm.ex | 7 ++-- lib/mix/lib/mix/scm/git.ex | 52 ++++++++++++---------------- lib/mix/lib/mix/state.ex | 60 +++++++++++++++------------------ lib/mix/lib/mix/tasks_server.ex | 20 +++++------ lib/mix/test/mix/task_test.exs | 4 +-- 5 files changed, 63 insertions(+), 80 deletions(-) diff --git a/lib/mix/lib/mix/scm.ex b/lib/mix/lib/mix/scm.ex index 95ed77ee979..5efeb0c67d8 100644 --- a/lib/mix/lib/mix/scm.ex +++ b/lib/mix/lib/mix/scm.ex @@ -128,21 +128,20 @@ defmodule Mix.SCM do until a matching one is found. """ def available do - {:ok, scm} = Mix.State.fetch(:scm) - scm + Mix.State.get(:scm) end @doc """ Prepends the given SCM module to the list of available SCMs. """ def prepend(mod) when is_atom(mod) do - Mix.State.prepend_scm(mod) + Mix.State.update(:scm, &[mod | List.delete(&1, mod)]) end @doc """ Appends the given SCM module to the list of available SCMs. """ def append(mod) when is_atom(mod) do - Mix.State.append_scm(mod) + Mix.State.update(:scm, &(List.delete(&1, mod) ++ [mod])) end end diff --git a/lib/mix/lib/mix/scm/git.ex b/lib/mix/lib/mix/scm/git.ex index 090b594ff96..7b46434443a 100644 --- a/lib/mix/lib/mix/scm/git.ex +++ b/lib/mix/lib/mix/scm/git.ex @@ -67,7 +67,6 @@ defmodule Mix.SCM.Git do @impl true def lock_status(opts) do - assert_git!() lock = opts[:lock] cond do @@ -102,7 +101,6 @@ defmodule Mix.SCM.Git do @impl true def checkout(opts) do - assert_git!() path = opts[:checkout] File.rm_rf!(path) File.mkdir_p!(opts[:dest]) @@ -116,7 +114,6 @@ defmodule Mix.SCM.Git do @impl true def update(opts) do - assert_git!() path = opts[:checkout] File.cd!(path, fn -> checkout(path, opts) end) end @@ -286,7 +283,17 @@ defmodule Mix.SCM.Git do defp git!(args, into \\ default_into()) do opts = cmd_opts(into: into, stderr_to_stdout: true) - case System.cmd("git", args, opts) do + try do + System.cmd("git", args, opts) + catch + :error, :enoent -> + Mix.raise( + "Error fetching/updating Git repository: the \"git\" " <> + "executable is not available in your PATH. Please install " <> + "Git on this machine or pass --no-deps-check if you want to " <> + "run a previously built application on a system without Git." + ) + else {response, 0} -> response @@ -305,25 +312,18 @@ defmodule Mix.SCM.Git do end end - defp assert_git! do - case Mix.State.fetch(:git_available) do - {:ok, true} -> - :ok - - :error -> - if System.find_executable("git") do - Mix.State.put(:git_available, true) - else - Mix.raise( - "Error fetching/updating Git repository: the \"git\" " <> - "executable is not available in your PATH. Please install " <> - "Git on this machine or pass --no-deps-check if you want to " <> - "run a previously built application on a system without Git." - ) - end + # Attempt to set the current working directory by default. + # This addresses an issue changing the working directory when executing from + # within a secondary node since file I/O is done through the main node. + defp cmd_opts(opts) do + case File.cwd() do + {:ok, cwd} -> Keyword.put(opts, :cd, cwd) + _ -> opts end end + # Also invoked by lib/mix/test/test_helper.exs + @doc false def git_version do case Mix.State.fetch(:git_version) do {:ok, version} -> @@ -333,7 +333,7 @@ defmodule Mix.SCM.Git do version = ["--version"] |> git!("") - |> parse_version + |> parse_version() Mix.State.put(:git_version, version) version @@ -352,14 +352,4 @@ defmodule Mix.SCM.Git do {int, _} = Integer.parse(string) int end - - # Attempt to set the current working directory by default. - # This addresses an issue changing the working directory when executing from - # within a secondary node since file I/O is done through the main node. - defp cmd_opts(opts) do - case File.cwd() do - {:ok, cwd} -> Keyword.put(opts, :cd, cwd) - _ -> opts - end - end end diff --git a/lib/mix/lib/mix/state.ex b/lib/mix/lib/mix/state.ex index ff50dbe12d7..41b41969cad 100644 --- a/lib/mix/lib/mix/state.ex +++ b/lib/mix/lib/mix/state.ex @@ -1,22 +1,25 @@ defmodule Mix.State do @moduledoc false @name __MODULE__ - @timeout :infinity - use Agent + use GenServer def start_link(_opts) do - Agent.start_link(__MODULE__, :init, [], name: @name) + GenServer.start_link(__MODULE__, :ok, name: @name) end - def init() do - %{ + @impl true + def init(:ok) do + table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true]) + + :ets.insert(table, shell: Mix.Shell.IO, env: from_env("MIX_ENV", :dev), target: from_env("MIX_TARGET", :host), - scm: [Mix.SCM.Git, Mix.SCM.Path], - cache: :ets.new(@name, [:public, :set, :named_table, read_concurrency: true]) - } + scm: [Mix.SCM.Git, Mix.SCM.Path] + ) + + {:ok, table} end defp from_env(varname, default) do @@ -28,50 +31,43 @@ defmodule Mix.State do end def fetch(key) do - Agent.get(@name, Map, :fetch, [key], @timeout) + case :ets.lookup(@name, key) do + [{^key, value}] -> {:ok, value} + [] -> :error + end end def get(key, default \\ nil) do - Agent.get(@name, Map, :get, [key, default], @timeout) + case :ets.lookup(@name, key) do + [{^key, value}] -> value + [] -> default + end end def put(key, value) do - Agent.update(@name, Map, :put, [key, value], @timeout) - end - - def prepend_scm(value) do - Agent.update( - @name, - fn state -> update_in(state.scm, &[value | List.delete(&1, value)]) end, - @timeout - ) + :ets.insert(@name, {key, value}) end - def append_scm(value) do - Agent.update( - @name, - fn state -> update_in(state.scm, &(List.delete(&1, value) ++ [value])) end, - @timeout - ) + def update(key, fun) do + :ets.insert(@name, {key, fun.(:ets.lookup_element(@name, key, 2))}) end def read_cache(key) do - case :ets.lookup(@name, key) do - [{^key, value}] -> value - [] -> nil - end + :persistent_term.get({__MODULE__, key}, nil) end def write_cache(key, value) do - :ets.insert(@name, {key, value}) + :persistent_term.put({__MODULE__, key}, value) value end def delete_cache(key) do - :ets.delete(@name, key) + :persistent_term.erase({__MODULE__, key}) end def clear_cache do - :ets.delete_all_objects(@name) + for {{__MODULE__, _} = key, _value} <- :persistent_term.get() do + :persistent_term.erase(key) + end end end diff --git a/lib/mix/lib/mix/tasks_server.ex b/lib/mix/lib/mix/tasks_server.ex index b73ac8e5872..cc4cadd6e85 100644 --- a/lib/mix/lib/mix/tasks_server.ex +++ b/lib/mix/lib/mix/tasks_server.ex @@ -1,35 +1,33 @@ defmodule Mix.TasksServer do @moduledoc false @name __MODULE__ - @timeout :infinity use Agent def start_link(_opts) do - Agent.start_link(fn -> %{} end, name: @name) + Agent.start_link( + fn -> :ets.new(@name, [:public, :set, :named_table]) end, + name: @name + ) end def run(tuple) do - Agent.get_and_update( - @name, - fn set -> {not Map.has_key?(set, tuple), Map.put(set, tuple, true)} end, - @timeout - ) + :ets.insert_new(@name, {tuple}) end def put(tuple) do - Agent.update(@name, &Map.put(&1, tuple, true), @timeout) + :ets.insert(@name, {tuple}) end def get(tuple) do - Agent.get(@name, &Map.get(&1, tuple), @timeout) + :ets.member(@name, tuple) end def delete_many(many) do - Agent.update(@name, &Map.drop(&1, many), @timeout) + Enum.each(many, &:ets.delete(@name, &1)) end def clear() do - Agent.update(@name, fn _ -> %{} end, @timeout) + :ets.delete_all_objects(@name) end end diff --git a/lib/mix/test/mix/task_test.exs b/lib/mix/test/mix/task_test.exs index 8f1829f816e..7f624de950f 100644 --- a/lib/mix/test/mix/task_test.exs +++ b/lib/mix/test/mix/task_test.exs @@ -94,7 +94,7 @@ defmodule Mix.TaskTest do """) # Clean up the tasks and update task - Mix.TasksServer.clear() + Mix.Task.clear() # Task was found from deps loadpaths assert Mix.Task.run("task_hello") == "Hello World v1" @@ -103,7 +103,7 @@ defmodule Mix.TaskTest do assert Mix.Task.run("compile") != :noop # Clean up the tasks and update task - Mix.TasksServer.clear() + Mix.Task.clear() File.write!("custom/raw_repo/lib/task_hello.ex", """ defmodule Mix.Tasks.TaskHello do From 7f8e8b65d477c2c8ab14f78f7c146619da661584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 12:38:45 +0200 Subject: [PATCH 06/59] Lock mix compile.elixir to avoid concurrent usage, closes #12013 --- lib/mix/lib/mix/state.ex | 110 ++++++++++++++++++++---- lib/mix/lib/mix/tasks/compile.elixir.ex | 23 +++-- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/lib/mix/lib/mix/state.ex b/lib/mix/lib/mix/state.ex index 41b41969cad..ed64b1379dd 100644 --- a/lib/mix/lib/mix/state.ex +++ b/lib/mix/lib/mix/state.ex @@ -1,6 +1,7 @@ defmodule Mix.State do @moduledoc false @name __MODULE__ + @timeout :infinity use GenServer @@ -8,28 +9,17 @@ defmodule Mix.State do GenServer.start_link(__MODULE__, :ok, name: @name) end - @impl true - def init(:ok) do - table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true]) - - :ets.insert(table, - shell: Mix.Shell.IO, - env: from_env("MIX_ENV", :dev), - target: from_env("MIX_TARGET", :host), - scm: [Mix.SCM.Git, Mix.SCM.Path] - ) - - {:ok, table} - end - - defp from_env(varname, default) do - case System.get_env(varname) do - nil -> default - "" -> default - value -> String.to_atom(value) + def lock(key, fun) do + try do + GenServer.call(@name, {:lock, key}, @timeout) + fun.() + after + GenServer.call(@name, {:unlock, key}, @timeout) end end + ## ETS state storage (mutable, not cleared ion tests) + def fetch(key) do case :ets.lookup(@name, key) do [{^key, value}] -> {:ok, value} @@ -52,6 +42,8 @@ defmodule Mix.State do :ets.insert(@name, {key, fun.(:ets.lookup_element(@name, key, 2))}) end + ## Persistent term cache (persistent, cleared in tests) + def read_cache(key) do :persistent_term.get({__MODULE__, key}, nil) end @@ -70,4 +62,84 @@ defmodule Mix.State do :persistent_term.erase(key) end end + + ## Callbacks + + @impl true + def init(:ok) do + table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true]) + + :ets.insert(table, + shell: Mix.Shell.IO, + env: from_env("MIX_ENV", :dev), + target: from_env("MIX_TARGET", :host), + scm: [Mix.SCM.Git, Mix.SCM.Path] + ) + + {:ok, {%{}, %{}}} + end + + defp from_env(varname, default) do + case System.get_env(varname) do + nil -> default + "" -> default + value -> String.to_atom(value) + end + end + + @impl true + def handle_call({:lock, key}, {pid, _} = from, {key_to_waiting, pid_to_key}) do + key_to_waiting = + case key_to_waiting do + %{^key => {locked, waiting}} -> + Map.put(key_to_waiting, key, {locked, :queue.in(from, waiting)}) + + %{} -> + go!(from) + Map.put(key_to_waiting, key, {pid, :queue.new()}) + end + + ref = Process.monitor(pid) + {:noreply, {key_to_waiting, Map.put(pid_to_key, pid, {key, ref})}} + end + + @impl true + def handle_call({:unlock, key}, {pid, _}, {key_to_waiting, pid_to_key}) do + {{^key, ref}, pid_to_key} = Map.pop(pid_to_key, pid) + Process.demonitor(ref, [:flush]) + {:reply, :ok, {unlock(key_to_waiting, pid_to_key, key), pid_to_key}} + end + + @impl true + def handle_info({:DOWN, ref, _type, pid, _reason}, {key_to_waiting, pid_to_key}) do + {{key, ^ref}, pid_to_key} = Map.pop(pid_to_key, pid) + + key_to_waiting = + case key_to_waiting do + %{^key => {^pid, _}} -> + unlock(key_to_waiting, pid_to_key, key) + + %{^key => {locked, waiting}} -> + Map.put(key_to_waiting, key, {locked, List.keydelete(waiting, pid, 0)}) + end + + {:noreply, {key_to_waiting, pid_to_key}} + end + + defp unlock(key_to_waiting, pid_to_key, key) do + %{^key => {_locked, waiting}} = key_to_waiting + + case :queue.out(waiting) do + {{:value, {pid, _} = from}, waiting} -> + # Assert that we still know this PID + _ = Map.fetch!(pid_to_key, pid) + go!(from) + Map.put(key_to_waiting, key, {pid, waiting}) + + {:empty, _waiting} -> + Map.delete(key_to_waiting, key) + end + end + + defp go!(from), do: GenServer.reply(from, :ok) end diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 3922dbc32ed..34a4689b1c3 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -111,15 +111,20 @@ defmodule Mix.Tasks.Compile.Elixir do |> tracers_opts(tracers) |> profile_opts() - Mix.Compilers.Elixir.compile( - manifest, - srcs, - dest, - cache_key, - Mix.Tasks.Compile.Erlang.manifests(), - Mix.Tasks.Compile.Erlang.modules(), - opts - ) + # The Elixir compiler relies on global state in the application tracer. + # However, even without it, having compilations racing with other is most + # likely undesired, so we wrap the compiler in a lock. + Mix.State.lock(__MODULE__, fn -> + Mix.Compilers.Elixir.compile( + manifest, + srcs, + dest, + cache_key, + Mix.Tasks.Compile.Erlang.manifests(), + Mix.Tasks.Compile.Erlang.modules(), + opts + ) + end) end @impl true From 2f15ba48589c59447e5d9bc440a20698118bdda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 12:50:28 +0200 Subject: [PATCH 07/59] Commit pending test file --- lib/mix/test/mix/state_test.exs | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 lib/mix/test/mix/state_test.exs diff --git a/lib/mix/test/mix/state_test.exs b/lib/mix/test/mix/state_test.exs new file mode 100644 index 00000000000..9a84e511125 --- /dev/null +++ b/lib/mix/test/mix/state_test.exs @@ -0,0 +1,100 @@ +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Mix.StateTest do + use ExUnit.Case, async: true + + describe "lock" do + test "executes functions" do + assert Mix.State.lock(:key, fn -> :it_works! end) == :it_works! + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + end + + test "releases lock on error" do + assert_raise RuntimeError, fn -> + Mix.State.lock(:key, fn -> raise "oops" end) + end + + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + end + + test "releases lock on exit" do + {_pid, ref} = + spawn_monitor(fn -> + Mix.State.lock(:key, fn -> Process.exit(self(), :kill) end) + end) + + assert_receive {:DOWN, ^ref, _, _, _} + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + end + + test "blocks until released" do + parent = self() + + task = + Task.async(fn -> + Mix.State.lock(:key, fn -> + send(parent, :locked) + assert_receive :will_lock + :it_works! + end) + end) + + assert_receive :locked + send(task.pid, :will_lock) + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + assert Task.await(task) == :it_works! + end + + test "blocks until released on error" do + parent = self() + + {pid, ref} = + spawn_monitor(fn -> + Mix.State.lock(:key, fn -> + send(parent, :locked) + assert_receive :will_lock + raise "oops" + end) + end) + + assert_receive :locked + send(pid, :will_lock) + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + assert_receive {:DOWN, ^ref, _, _, _} + end + + test "blocks until released on exit" do + parent = self() + + {pid, ref} = + spawn_monitor(fn -> + Mix.State.lock(:key, fn -> + send(parent, :locked) + assert_receive :will_not_lock + end) + end) + + assert_receive :locked + Process.exit(pid, :kill) + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + assert_receive {:DOWN, ^ref, _, _, _} + end + + test "scheduls and releases on exit" do + assert Mix.State.lock(:key, fn -> + {pid, ref} = + spawn_monitor(fn -> + Mix.State.lock(:key, fn -> + raise "this will never be invoked" + end) + end) + + Process.exit(pid, :kill) + assert_receive {:DOWN, ^ref, _, _, :killed} + :it_works! + end) == :it_works! + + assert Mix.State.lock(:key, fn -> :still_works! end) == :still_works! + end + end +end From 79653a7c80301de9aa1cece71ce24b250d359118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 12:54:37 +0200 Subject: [PATCH 08/59] Start v1.15.0-dev --- CHANGELOG.md | 423 +----------------- SECURITY.md | 6 +- VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- .../pages/compatibility-and-deprecations.md | 11 +- 6 files changed, 17 insertions(+), 429 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bfeda6fee..3713e011998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,430 +1,15 @@ -# Changelog for Elixir v1.14 +# Changelog for Elixir v1.15 -Elixir v1.14 brings many improvements to the debugging experience in Elixir -and data-type inspection. It also includes a new abstraction for easy -partitioning of processes called `PartitionSupervisor`, as well as improved -compilation times and error messages. - -Elixir v1.14 is the last version to support Erlang/OTP 23. Consider updating -to Erlang/OTP 24 or Erlang/OTP 25. - -## `dbg` - -`Kernel.dbg/2` is a new macro that's somewhat similar to `IO.inspect/2`, but -specifically tailored for **debugging**. - -When called, it prints the value of whatever you pass to it, plus the debugged -code itself as well as its location. This code: - -```elixir -# In my_file.exs -feature = %{name: :dbg, inspiration: "Rust"} -dbg(feature) -dbg(Map.put(feature, :in_version, "1.14.0")) -``` - -Prints this: - -```shell -$ elixir my_file.exs -[my_file.exs:2: (file)] -feature #=> %{inspiration: "Rust", name: :dbg} - -[my_file.exs:3: (file)] -Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg} -``` - -`dbg/2` can do more. It's a macro, so it *understands Elixir code*. You can see -that when you pass a series of `|>` pipes to it. `dbg/2` will print the value -for every step of the pipeline. This code: - -```elixir -# In dbg_pipes.exs -__ENV__.file -|> String.split("/", trim: true) -|> List.last() -|> File.exists?() -|> dbg() -``` - -Prints this: - -```shell -$ elixir dbg_pipes.exs -[dbg_pipes.exs:5: (file)] -__ENV__.file #=> "/home/myuser/dbg_pipes.exs" -|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"] -|> List.last() #=> "dbg_pipes.exs" -|> File.exists?() #=> true -``` - -### IEx and Prying - -`dbg/2` supports configurable backends. IEx automatically replaces the default -backend by one that halts the code execution with `IEx.Pry`, giving developers -the option to access local variables, imports, and more. This also works with -pipelines: if you pass a series of `|>` pipe calls to `dbg` (or pipe into it at the -end, like `|> dbg()`), you'll be able to step through every line in the pipeline. - -You can keep the default behaviour by passing the `--no-pry` option to IEx. - -## PartitionSupervisor - -`PartitionSupervisor` is a new module that implements a new supervisor type. The -partition supervisor is designed to help with situations where you have a single -supervised process that becomes a bottleneck. If that process's state can be -easily partitioned, then you can use `PartitionSupervisor` to supervise multiple -isolated copies of that process running concurrently, each assigned its own -partition. - -For example, imagine you have an `ErrorReporter` process that you use to report -errors to a monitoring service. - -```elixir -# Application supervisor: -children = [ - # ..., - ErrorReporter -] - -Supervisor.start_link(children, strategy: :one_for_one) -``` - -As the concurrency of your application goes up, the `ErrorReporter` process -might receive requests from many other processes and eventually become a -bottleneck. In a case like this, it could help to spin up multiple copies of the -`ErrorReporter` process under a `PartitionSupervisor`. - -```elixir -# Application supervisor -children = [ - {PartitionSupervisor, child_spec: ErrorReporter, name: Reporters} -] -``` - -The `PartitionSupervisor` will spin up a number of processes equal to -`System.schedulers_online()` by default (most often one per core). Now, when -routing requests to `ErrorReporter` processes we can use a `:via` tuple and -route the requests through the partition supervisor. - -```elixir -partitioning_key = self() -ErrorReporter.report({:via, PartitionSupervisor, {Reporters, partitioning_key}}, error) -``` - -Using `self()` as the partitioning key here means that the same process will -always report errors to the same `ErrorReporter` process, ensuring a form of -back-pressure. You can use any term as the partitioning key. - -### A Common Example - -A common and practical example of a good use case for `PartitionSupervisor` is -partitioning something like a `DynamicSupervisor`. When starting many processes -under it, a dynamic supervisor can be a bottleneck, especially if said processes -take a long time to initialize. Instead of starting a single `DynamicSupervisor`, -you can start multiple: - -```elixir -children = [ - {PartitionSupervisor, child_spec: DynamicSupervisor, name: MyApp.DynamicSupervisors} -] - -Supervisor.start_link(children, strategy: :one_for_one) -``` - -Now you start processes on the dynamic supervisor for the right partition. -For instance, you can partition by PID, like in the previous example: - -```elixir -DynamicSupervisor.start_child( - {:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}}, - my_child_specification -) -``` - -## Improved errors on binaries and evaluation - -Erlang/OTP 25 improved errors on binary construction and evaluation. These improvements -apply to Elixir as well. Before v1.14, errors when constructing binaries would -often be hard-to-debug generic "argument errors". With Erlang/OTP 25 and Elixir v1.14, -more detail is provided for easier debugging. This work is part of [EEP -54](https://www.erlang.org/eeps/eep-0054). - -Before: - -```elixir -int = 1 -bin = "foo" -int <> bin -#=> ** (ArgumentError) argument error -``` - -Now: - -```elixir -int = 1 -bin = "foo" -int <> bin -#=> ** (ArgumentError) construction of binary failed: -#=> segment 1 of type 'binary': -#=> expected a binary but got: 1 -``` - -## Slicing with steps - -Elixir v1.12 introduced **stepped ranges**, which are ranges where you can -specify the "step": - -```elixir -Enum.to_list(1..10//3) -#=> [1, 4, 7, 10] -``` - -Stepped ranges are particularly useful for numerical operations involving -vectors and matrices (see [Nx](https://github.com/elixir-nx/nx), for example). -However, the Elixir standard library was not making use of stepped ranges in its -APIs. Elixir v1.14 starts to take advantage of steps with support for stepped -ranges in a couple of functions. One of them is `Enum.slice/2`: - -```elixir -letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] -Enum.slice(letters, 0..5//2) -#=> ["a", "c", "e"] -``` - -`binary_slice/2` (and `binary_slice/3` for completeness) has been added to the -`Kernel` module, that works with bytes and also support stepped ranges: - -```elixir -binary_slice("Elixir", 1..5//2) -#=> "lx" -``` - -## Expression-based inspection and `Inspect` improvements - -In Elixir, it's conventional to implement the `Inspect` protocol for opaque -structs so that they're inspected with a special notation, resembling this: - -```elixir -MapSet.new([:apple, :banana]) -#MapSet<[:apple, :banana]> -``` - -This is generally done when the struct content or part of it is private and the -`%name{...}` representation would reveal fields that are not part of the public -API. - -The downside of the `#name<...>` convention is that *the inspected output is not -valid Elixir code*. For example, you cannot copy the inspected output and paste -it into an IEx session. - -Elixir v1.14 changes the convention for some of the standard-library structs. -The `Inspect` implementation for those structs now returns a string with a valid -Elixir expression that recreates the struct when evaluated. In the `MapSet` -example above, this is what we have now: - -```elixir -fruits = MapSet.new([:apple, :banana]) -MapSet.put(fruits, :pear) -#=> MapSet.new([:apple, :banana, :pear]) -``` - -The `MapSet.new/1` expression evaluates to exactly the struct that we're -inspecting. This allows us to hide the internals of `MapSet`, while keeping -it as valid Elixir code. This expression-based inspection has been -implemented for `Version.Requirement`, `MapSet`, and `Date.Range`. - -Finally, we have improved the `Inspect` protocol for structs so that -fields are inspected in the order they are declared in `defstruct`. -The option `:optional` has also been added when deriving the `Inspect` -protocol, giving developers more control over the struct representation. -See the updated documentation for `Inspect` for a general rundown on -the approaches and options available. - -## v1.14.0-dev +## v1.15.0-dev ### 1. Enhancements -#### EEx - - * [EEx] Support multi-line comments to EEx via `<%!-- --%>` - * [EEx] Add `EEx.tokenize/2` - -#### Elixir - - * [Application] Add `Application.compile_env/4` and `Application.compile_env!/3` to read the compile-time environment inside macros - * [Calendar] Support ISO8601 basic format parsing with `DateTime.from_iso8601/2` - * [Calendar] Add `day`/`hour`/`minute` on `add`/`diff` across different calendar modules - * [Code] Add `:normalize_bitstring_modifiers` to `Code.format_string!/2` - * [Code] Emit deprecation and type warnings for invalid options in on `Code.compile_string/2` and `Code.compile_quoted/2` - * [Code] Warn if an outdated lexical tracker is given on eval - * [Code] Add `Code.env_for_eval/1` and `Code.eval_quoted_with_env/3` - * [Code] Improve stacktraces from eval operations on Erlang/OTP 25+ - * [Code.Fragment] Add support for `__MODULE__` in several functions - * [Code.Fragment] Support surround and context suggestions across multiple lines - * [Enum] Allow slicing with steps in `Enum.slice/2` - * [File] Support `dereference_symlinks: true` in `File.cp/3` and `File.cp_r/3` - * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero - * [Float] Add `Float.min_finite/0` and `Float.max_finite/0` - * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Allow `:optional` when deriving the Inspect protocol for hiding fields that match their default value - * [Inspect] Inspect struct fields in the order they are declared in `defstruct` - * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` - * [IO] Support `Macro.Env` and keywords as stacktrace definitions in `IO.warn/2` - * [IO] Add `IO.ANSI.syntax_colors/0` and related configuration to be shared across IEx and `dbg` - * [Kernel] Add new `dbg/0-2` macro - * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match - * [Kernel] Allow composite types with pins as the map key in a pattern match - * [Kernel] Print escaped version of control chars when they show up as unexpected tokens - * [Kernel] Warn on confusable non-ASCII identifiers - * [Kernel] Add `..` as a nullary operator that returns `0..-1//1` - * [Kernel] Implement Unicode Technical Standard #39 recommendations. In particular, we warn for confusable scripts and restrict identifiers to single-scripts or highly restrictive mixed-scripts - * [Kernel] Automatically perform NFC conversion of identifiers - * [Kernel] Add `binary_slice/2` and `binary_slice/3` - * [Kernel] Lazily expand module attributes to avoid compile-time deps - * [Kernel] Automatically cascade `generated: true` annotations on macro expansion - * [Keyword] Add `Keyword.from_keys/2` and `Keyword.replace_lazy/3` - * [List] Add `List.keysort/3` with support for a `sorter` function - * [Macro] Add `Macro.classify_atom/1` and `Macro.inspect_atom/2` - * [Macro] Add `Macro.expand_literal/2` and `Macro.path/2` - * [Macro.Env] Add `Macro.Env.prune_compile_info/1` - * [Map] Add `Map.from_keys/2` and `Map.replace_lazy/3` - * [MapSet] Add `MapSet.filter/2`, `MapSet.reject/2`, and `MapSet.symmetric_difference/2` - * [Node] Add `Node.spawn_monitor/2` and `Node.spawn_monitor/4` - * [Module] Support new `@after_verify` attribute for executing code whenever a module is verified - * [PartitionSupervisor] Add `PartitionSupervisor` that starts multiple isolated partitions of the same child for scalability - * [Path] Add `Path.safe_relative/1` and `Path.safe_relative_to/2` - * [Registry] Add `Registry.count_select/2` - * [Stream] Add `Stream.duplicate/2` and `Stream.transform/5` - * [String] Support empty lookup lists in `String.replace/3`, `String.split/3`, and `String.splitter/3` - * [String] Allow slicing with steps in `String.slice/2` - * [Task] Add `:zip_input_on_exit` option to `Task.async_stream/3` - * [Task] Store `:mfa` in the `Task` struct for reflection purposes - * [URI] Add `URI.append_query/2` - * [Version] Add `Version.to_string/1` - * [Version] Colorize `Version.Requirement` source in the `Inspect` protocol - -#### ExUnit - - * [ExUnit] Add `ExUnit.Callbacks.start_link_supervised!/2` - * [ExUnit] Add `ExUnit.run/1` to rerun test modules - * [ExUnit] Colorize summary in yellow with message when all tests are excluded - * [ExUnit] Display friendly error when test name is too long - -#### IEx - - * [IEx] Evaluate `--dot-iex` line by line - * [IEx] Add line-by-line evaluation of IEx breakpoints - * [IEx.Autocomplete] Autocomplete bitstrings modifiers (after `::` inside `<<...>>`) - * [IEx.Helpers] Allow an atom to be given to `pid/1` - -#### Logger - - * [Logger] Add `Logger.put_process_level/2` - -#### Mix - - * [mix compile] Add `--no-optional-deps` to skip optional dependencies to test compilation works without optional dependencies - * [mix compile] Include column information on error diagnostics when possible - * [mix deps] `Mix.Dep.Converger` now tells which deps formed a cycle - * [mix do] Support `--app` option to restrict recursive tasks in umbrella projects - * [mix do] Allow using `+` as a task separator instead of comma - * [mix format] Support filename in `mix format -` when reading from stdin - * [mix format] Compile if `mix format` plugins are missing - * [mix new] Do not allow projects to be created with application names that conflict with multi-arg Erlang VM switches - * [mix profile] Return the return value of the profiled function - * [mix release] Make BEAM compression opt-in - * [mix release] Let `:runtime_config_path` accept `false` to skip the `config/runtime.exs` - * [mix test] Improve error message when suite fails due to coverage - * [mix test] Support `:test_elixirc_options` and default to not generating docs nor debug info chunk for tests - * [mix xref] Support `--group` flag in `mix xref graph` - ### 2. Bug fixes -#### Elixir - - * [Calendar] Handle widths with "0" in them in `Calendar.strftime/3` - * [CLI] Improve errors on incorrect `--rpc-eval` usage - * [CLI] Return proper exit code on Windows - * [Code] Do not emit warnings when formatting code - * [Enum] Allow slices to overflow on both starting and ending positions - * [Kernel] Do not allow restricted characters in identifiers according to UTS39 - * [Kernel] Define `__exception__` field as `true` when expanding exceptions in typespecs - * [Kernel] Warn if any of `True`, `False`, and `Nil` aliases are used - * [Kernel] Warn on underived `@derive` attributes - * [Kernel] Remove compile-time dependency from `defimpl :for` - * [Protocol] Warn if a protocol has no definitions - * [Regex] Show list options when inspecting a Regex manually defined with `Regex.compile/2` - * [String] Allow slices to overflow on both starting and ending positions - -#### ExUnit - - * [ExUnit] Do not crash when diffing unknown bindings in guards - * [ExUnit] Properly print diffs when comparing improper lists with strings at the tail position - * [ExUnit] Add short hash to `tmp_dir` in ExUnit to avoid test name collision - * [ExUnit] Do not store logs in the CLI formatter (this reduces memory usage for suites with `capture_log`) - * [ExUnit] Run `ExUnit.after_suite/1` callback even when no tests run - * [ExUnit] Fix scenario where `setup` with imported function from within `describe` failed to compile - -#### IEx - - * [IEx] Disallow short-hand pipe after matches - * [IEx] Fix `exports/1` in IEx for long function names - -#### Mix - - * [mix compile.elixir] Fix `--warnings-as-errors` when used with `--all-warnings` - * [mix compile.elixir] Ensure semantic recompilation cascades to path dependencies - * [mix format] Do not add new lines if the formatted file is empty - * [mix release] Only set `RELEASE_MODE` after `env.{sh,bat}` are executed - * [mix release] Allow application mode configuration to cascade to dependencies - * [mix xref] Do not emit already consolidated warnings during `mix xref trace` - * [Mix] Do not start apps with `runtime: false` on `Mix.install/2` - ### 3. Soft deprecations (no warnings emitted) -#### Elixir - - * [File] Passing a callback as third argument to `File.cp/3` and `File.cp_r/3` is deprecated. - Instead pass the callback the `:on_conflict` key of a keyword list - -#### EEx - - * [EEx] Using `<%# ... %>` for comments is deprecated. Please use `<% # ... %>` or the new multi-line comments with `<%!-- ... --%>` - -#### Logger - - * [Logger] Deprecate `Logger.enable/1` and `Logger.disable/1` in favor of `Logger.put_process_level/2` - -#### Mix - - * [mix cmd] The `--app` option in `mix cmd CMD` is deprecated in favor of the more efficient `mix do --app app cmd CMD` - ### 4. Hard deprecations -#### Elixir - - * [Application] Calling `Application.get_env/3` and friends in the module body is now discouraged, use `Application.compile_env/3` instead - * [Bitwise] `use Bitwise` is deprecated, use `import Bitwise` instead - * [Bitwise] `~~~` is deprecated in favor of `bnot` for clarity - * [Kernel.ParallelCompiler] Returning a list or two-element tuple from `:each_cycle` is deprecated, return a `{:compile | :runtime, modules, warnings}` tuple instead - * [Kernel] Deprecate the operator `<|>` to avoid ambiguity with upcoming extended numerical operators - * [String] Deprecate passing a binary compiled pattern to `String.starts_with?/2` - -#### Logger - - * [Logger] Deprecate `$levelpad` on message formatting - -#### Mix - - * [Mix] `Mix.Tasks.Xref.calls/1` is deprecated in favor of compilation tracers - -### 5. Backwards incompatible changes - -#### Mix - - * [mix local.rebar] Remove support for rebar2, which has not been updated in 5 years, and is no longer supported on recent Erlang/OTP versions - -## v1.13 +## v1.14 -The CHANGELOG for v1.13 releases can be found [in the v1.13 branch](https://github.com/elixir-lang/elixir/blob/v1.13/CHANGELOG.md). +The CHANGELOG for v1.14 releases can be found [in the v1.14 branch](https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md). diff --git a/SECURITY.md b/SECURITY.md index 128bcd458ba..895f63dd738 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,12 +6,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.14 | Development -1.13 | Bug fixes and security patches +1.15 | Development +1.14 | Bug fixes and security patches +1.13 | Security patches only 1.12 | Security patches only 1.11 | Security patches only 1.10 | Security patches only -1.9 | Security patches only ## Announcements diff --git a/VERSION b/VERSION index 16772dd8ade..0dec25d15b3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.0-dev \ No newline at end of file +1.15.0-dev \ No newline at end of file diff --git a/bin/elixir b/bin/elixir index 350703c5252..2d719db3508 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.14.0-dev +ELIXIR_VERSION=1.15.0-dev if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index 210753d00c3..227808830ed 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off) -set ELIXIR_VERSION=1.14.0-dev +set ELIXIR_VERSION=1.15.0-dev setlocal enabledelayedexpansion if ""%1""=="""" if ""%2""=="""" goto documentation diff --git a/lib/elixir/pages/compatibility-and-deprecations.md b/lib/elixir/pages/compatibility-and-deprecations.md index d7dcae7d2e9..32abc7923f6 100644 --- a/lib/elixir/pages/compatibility-and-deprecations.md +++ b/lib/elixir/pages/compatibility-and-deprecations.md @@ -8,12 +8,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.14 | Development -1.13 | Bug fixes and security patches +1.15 | Development +1.14 | Bug fixes and security patches +1.13 | Security patches only 1.12 | Security patches only 1.11 | Security patches only 1.10 | Security patches only -1.9 | Security patches only New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). @@ -84,6 +84,8 @@ Version | Deprecated feature | Replaced by (ava [v1.14] | `Application.get_env/3` and similar in module body | `Application.compile_env/3` (v1.10) [v1.14] | Compiled patterns in `String.starts_with?/2` | Pass a list of strings instead (v1.0) [v1.14] | `Mix.Tasks.Xref.calls/1` | Compilation tracers (outlined in `Code`) (v1.10) +[v1.14] | `$levelpad` in Logger | *None* +[v1.14] | `<|>` as a custom operator | Another custom operator (v1.0) [v1.13] | `!` and `!=` in Version requirements | `~>` or `>=` (v1.0) [v1.13] | `Mix.Config` | `Config` (v1.9) [v1.13] | `:strip_beam` config to `mix escript.build` | `:strip_beams` (v1.9) @@ -195,4 +197,5 @@ Version | Deprecated feature | Replaced by (ava [v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations [v1.12]: https://github.com/elixir-lang/elixir/blob/v1.12/CHANGELOG.md#4-hard-deprecations [v1.13]: https://github.com/elixir-lang/elixir/blob/v1.13/CHANGELOG.md#4-hard-deprecations -[v1.14]: CHANGELOG.md#4-hard-deprecations +[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations +[v1.15]: CHANGELOG.md#4-hard-deprecations From 0c92fe34bccb48829a7ebe6750142625b9d0e80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 13:20:42 +0200 Subject: [PATCH 09/59] Require Erlang/OTP 24 --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 2 - Makefile | 4 +- lib/elixir/lib/map.ex | 8 +-- lib/elixir/lib/map_set.ex | 22 ++------- lib/elixir/lib/node.ex | 6 --- lib/elixir/lib/path.ex | 11 ++--- lib/elixir/lib/process.ex | 6 +-- lib/elixir/lib/supervisor.ex | 4 +- lib/elixir/lib/system.ex | 17 ++----- lib/elixir/lib/task.ex | 22 ++------- lib/elixir/lib/task/supervisor.ex | 17 ++----- .../pages/compatibility-and-deprecations.md | 1 + lib/elixir/src/elixir.erl | 4 +- lib/elixir/src/elixir_bitstring.erl | 3 +- lib/elixir/src/elixir_erl_compiler.erl | 18 ------- lib/elixir/src/elixir_erl_var.erl | 23 +-------- .../test/elixir/kernel/expansion_test.exs | 49 +++++-------------- .../test/elixir/kernel/warning_test.exs | 13 +---- lib/elixir/test/elixir/map_set_test.exs | 29 ----------- lib/mix/lib/mix/tasks/compile.erlang.ex | 3 +- 21 files changed, 46 insertions(+), 220 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1fdbe42ccb..f0f7b8860fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,6 @@ jobs: otp_latest: true - otp_release: OTP-24.3 - otp_release: OTP-24.0 - - otp_release: OTP-23.3 - - otp_release: OTP-23.0 - otp_release: master development: true - otp_release: maint @@ -81,7 +79,7 @@ jobs: name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019 strategy: matrix: - otp_release: ['23.3'] + otp_release: ['24.0'] runs-on: windows-2019 steps: - name: Configure Git diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 091d4050dde..e4aaeea968d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,8 +32,6 @@ jobs: fail-fast: true matrix: include: - - otp: 23 - otp_version: 23.3 - otp: 24 otp_version: 24.3 - otp: 25 diff --git a/Makefile b/Makefile index d6e21a59c36..dddefcad938 100644 --- a/Makefile +++ b/Makefile @@ -29,9 +29,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH #==> Functions define CHECK_ERLANG_RELEASE - erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 23)])' -s erlang halt | grep -q '^true'; \ + erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \ if [ $$? != 0 ]; then \ - echo "At least Erlang/OTP 23.0 is required to build Elixir"; \ + echo "At least Erlang/OTP 24.0 is required to build Elixir"; \ exit 1; \ fi endef diff --git a/lib/elixir/lib/map.ex b/lib/elixir/lib/map.ex index 38df2687fce..2357dcc3c42 100644 --- a/lib/elixir/lib/map.ex +++ b/lib/elixir/lib/map.ex @@ -123,8 +123,6 @@ defmodule Map do @type value :: any @compile {:inline, fetch: 2, fetch!: 2, get: 2, put: 3, delete: 2, has_key?: 2, replace!: 3} - # TODO: Remove conditional on Erlang/OTP 24+ - @compile {:no_warn_undefined, {:maps, :from_keys, 2}} @doc """ Builds a map from the given `keys` and the fixed `value`. @@ -137,11 +135,7 @@ defmodule Map do @doc since: "1.14.0" @spec from_keys([key], value) :: map def from_keys(keys, value) do - if function_exported?(:maps, :from_keys, 2) do - :maps.from_keys(keys, value) - else - :maps.from_list(:lists.map(&{&1, value}, keys)) - end + :maps.from_keys(keys, value) end @doc """ diff --git a/lib/elixir/lib/map_set.ex b/lib/elixir/lib/map_set.ex index 3b0224b5b52..c7783868293 100644 --- a/lib/elixir/lib/map_set.ex +++ b/lib/elixir/lib/map_set.ex @@ -58,9 +58,8 @@ defmodule MapSet do @type t(value) :: %__MODULE__{map: internal(value)} @type t :: t(term) - # TODO: Remove version key when we require Erlang/OTP 24 # TODO: Implement the functions in this module using Erlang/OTP 24 new sets - defstruct map: %{}, version: 2 + defstruct map: %{} @doc """ Returns a new set. @@ -92,7 +91,7 @@ defmodule MapSet do def new(enumerable) do keys = Enum.to_list(enumerable) - %MapSet{map: Map.from_keys(keys, @dummy_value)} + %MapSet{map: :maps.from_keys(keys, @dummy_value)} end @doc """ @@ -107,7 +106,7 @@ defmodule MapSet do @spec new(Enumerable.t(), (term -> val)) :: t(val) when val: value def new(enumerable, transform) when is_function(transform, 1) do keys = Enum.map(enumerable, transform) - %MapSet{map: Map.from_keys(keys, @dummy_value)} + %MapSet{map: :maps.from_keys(keys, @dummy_value)} end @doc """ @@ -254,14 +253,8 @@ defmodule MapSet do """ @spec equal?(t, t) :: boolean - def equal?(%MapSet{map: map1, version: version}, %MapSet{map: map2, version: version}) do - map1 === map2 - end - - # Elixir v1.5 changed the map representation, so on - # version mismatch we need to compare the keys directly. def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do - map_size(map1) == map_size(map2) and all_in?(map1, map2) + map1 === map2 end @doc """ @@ -385,15 +378,10 @@ defmodule MapSet do @spec union(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value def union(map_set1, map_set2) - def union(%MapSet{map: map1, version: version} = map_set, %MapSet{map: map2, version: version}) do + def union(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do %{map_set | map: Map.merge(map1, map2)} end - def union(%MapSet{map: map1}, %MapSet{map: map2}) do - keys = Map.keys(map1) ++ Map.keys(map2) - %MapSet{map: Map.from_keys(keys, @dummy_value)} - end - @compile {:inline, [order_by_size: 2]} defp order_by_size(map1, map2) when map_size(map1) > map_size(map2), do: {map2, map1} defp order_by_size(map1, map2), do: {map1, map2} diff --git a/lib/elixir/lib/node.ex b/lib/elixir/lib/node.ex index 812fd998270..ffd4f5908a9 100644 --- a/lib/elixir/lib/node.ex +++ b/lib/elixir/lib/node.ex @@ -261,9 +261,6 @@ defmodule Node do Spawns the given function on a node, monitors it and returns its PID and monitoring reference. - This functionality was added on Erlang/OTP 23. Using this function to - communicate with nodes running on earlier versions will fail. - Inlined by the compiler. """ @doc since: "1.14.0" @@ -276,9 +273,6 @@ defmodule Node do Spawns the given module and function passing the given args on a node, monitors it and returns its PID and monitoring reference. - This functionality was added on Erlang/OTP 23. Using this function - to communicate with nodes running on earlier versions will fail. - Inlined by the compiler. """ @doc since: "1.14.0" diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 2fd703dd2f4..d015ce4086f 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -433,12 +433,8 @@ defmodule Path do @doc """ Returns the extension of the last component of `path`. - The behaviour of this function changed in Erlang/OTP 24 for filenames - starting with a dot and without an extension. For example, for a file - named `.gitignore`, `extname/1` now returns an empty string, while it - would return `".gitignore"` in previous Erlang/OTP versions. This was - done to match the behaviour of `rootname/1`, which would return - `".gitignore"` as its name (and therefore it cannot also be an extension). + For filenames starting with a dot and without an extension, it returns + an empty string. See `basename/1` and `rootname/1` for related functions to extract information from paths. @@ -451,6 +447,9 @@ defmodule Path do iex> Path.extname("~/foo/bar") "" + iex> Path.extname(".gitignore") + "" + """ @spec extname(t) :: binary def extname(path) do diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index d440330bccb..0ca8c8348ea 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -383,7 +383,7 @@ defmodule Process do @type spawn_opt :: :link | :monitor - | {:monitor, monitor_option()} + | {:monitor, :erlang.monitor_option()} | {:priority, :low | :normal | :high} | {:fullsweep_after, non_neg_integer} | {:min_heap_size, non_neg_integer} @@ -392,10 +392,6 @@ defmodule Process do | {:message_queue_data, :off_heap | :on_heap} @type spawn_opts :: [spawn_opt] - # TODO: Use :erlang.monitor_option() on Erlang/OTP 24+ - @typep monitor_option :: - [alias: :explicit_unalias | :demonitor | :reply_demonitor, tag: term()] - @doc """ Spawns the given function according to the given options. diff --git a/lib/elixir/lib/supervisor.ex b/lib/elixir/lib/supervisor.ex index c81e627e43c..a6d75bfe33b 100644 --- a/lib/elixir/lib/supervisor.ex +++ b/lib/elixir/lib/supervisor.ex @@ -536,10 +536,8 @@ defmodule Supervisor do @typedoc "Supported restart options" @type restart :: :permanent | :transient | :temporary - # TODO: Update :shutdown to "timeout() | :brutal_kill" when we require Erlang/OTP 24. - # Additionally apply https://github.com/elixir-lang/elixir/pull/11836 @typedoc "Supported shutdown options" - @type shutdown :: pos_integer() | :infinity | :brutal_kill + @type shutdown :: timeout() | :brutal_kill @typedoc "Supported strategies" @type strategy :: :one_for_one | :one_for_all | :rest_for_one diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index 625a3fab5dc..9b923fa2236 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -597,9 +597,6 @@ defmodule System do end end - # TODO: Remove this once we require Erlang/OTP 24+ - @compile {:no_warn_undefined, {:os, :env, 0}} - @doc """ Returns all system environment variables. @@ -608,17 +605,9 @@ defmodule System do """ @spec get_env() :: %{optional(String.t()) => String.t()} def get_env do - if function_exported?(:os, :env, 0) do - Map.new(:os.env(), fn {k, v} -> - {IO.chardata_to_string(k), IO.chardata_to_string(v)} - end) - else - Enum.into(:os.getenv(), %{}, fn var -> - var = IO.chardata_to_string(var) - [k, v] = String.split(var, "=", parts: 2) - {k, v} - end) - end + Map.new(:os.env(), fn {k, v} -> + {IO.chardata_to_string(k), IO.chardata_to_string(v)} + end) end @doc """ diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 750d79a3a75..a8c9c243385 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -436,9 +436,6 @@ defmodule Task do async(:erlang, :apply, [fun, []]) end - # TODO: Remove conditional on Erlang/OTP 24 - @compile {:no_warn_undefined, {:erlang, :monitor, 3}} - @doc """ Starts a task that must be awaited on. @@ -454,16 +451,9 @@ defmodule Task do owner = self() {:ok, pid} = Task.Supervised.start_link(get_owner(owner), :nomonitor) - {reply_to, ref} = - if function_exported?(:erlang, :monitor, 3) do - ref = :erlang.monitor(:process, pid, alias: :demonitor) - {ref, ref} - else - {owner, Process.monitor(pid)} - end - - send(pid, {owner, ref, reply_to, get_callers(owner), mfargs}) - %Task{pid: pid, ref: ref, owner: owner, mfa: {module, function_name, length(args)}} + alias = :erlang.monitor(:process, pid, alias: :demonitor) + send(pid, {owner, alias, alias, get_callers(owner), mfargs}) + %Task{pid: pid, ref: alias, owner: owner, mfa: {module, function_name, length(args)}} end @doc """ @@ -843,15 +833,9 @@ defmodule Task do Important: avoid using [`Task.async/1,3`](`async/1`) and then immediately ignoring the task. If you want to start tasks you don't care about their results, use `Task.Supervisor.start_child/2` instead. - - Requires Erlang/OTP 24+. """ @doc since: "1.13.0" def ignore(%Task{ref: ref, pid: pid, owner: owner} = task) do - unless function_exported?(:erlang, :monitor, 3) do - raise "Task.ignore/1 requires Erlang/OTP 24+" - end - if owner != self() do raise ArgumentError, invalid_owner_error(task) end diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex index db74f7af82e..370b592018c 100644 --- a/lib/elixir/lib/task/supervisor.ex +++ b/lib/elixir/lib/task/supervisor.ex @@ -504,9 +504,6 @@ defmodule Task.Supervisor do end end - # TODO: Remove conditional on Erlang/OTP 24 - @compile {:no_warn_undefined, {:erlang, :monitor, 3}} - defp async(supervisor, link_type, module, fun, args, options) do owner = self() shutdown = options[:shutdown] @@ -514,17 +511,9 @@ defmodule Task.Supervisor do case start_child_with_spec(supervisor, [get_owner(owner), :monitor], :temporary, shutdown) do {:ok, pid} -> if link_type == :link, do: Process.link(pid) - - {reply_to, ref} = - if function_exported?(:erlang, :monitor, 3) do - ref = :erlang.monitor(:process, pid, alias: :demonitor) - {ref, ref} - else - {owner, Process.monitor(pid)} - end - - send(pid, {owner, ref, reply_to, get_callers(owner), {module, fun, args}}) - %Task{pid: pid, ref: ref, owner: owner, mfa: {module, fun, length(args)}} + alias = :erlang.monitor(:process, pid, alias: :demonitor) + send(pid, {owner, alias, alias, get_callers(owner), {module, fun, args}}) + %Task{pid: pid, ref: alias, owner: owner, mfa: {module, fun, length(args)}} {:error, :max_children} -> raise """ diff --git a/lib/elixir/pages/compatibility-and-deprecations.md b/lib/elixir/pages/compatibility-and-deprecations.md index 32abc7923f6..4b1e91c2f9d 100644 --- a/lib/elixir/pages/compatibility-and-deprecations.md +++ b/lib/elixir/pages/compatibility-and-deprecations.md @@ -43,6 +43,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea Elixir version | Supported Erlang/OTP versions :------------- | :------------------------------- +1.15 | 24 - 26 1.14 | 23 - 25 1.13 | 22 - 24 (and Erlang/OTP 25 from v1.13.4) 1.12 | 22 - 24 diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 681da9ec156..1e9b02a5303 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -111,10 +111,10 @@ preload_common_modules() -> parse_otp_release() -> %% Whenever we change this check, we should also change Makefile. case string:to_integer(erlang:system_info(otp_release)) of - {Num, _} when Num >= 23 -> + {Num, _} when Num >= 24 -> Num; _ -> - io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 23+~n", []), + io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 24+~n", []), erlang:halt(1) end. diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index b3c3c1081c2..40a29668e06 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -317,8 +317,7 @@ number_size(Size, default) when is_integer(Size) -> Size; number_size(Size, Unit) when is_integer(Size) -> Size * Unit; number_size(Size, _) -> Size. -%% TODO: Simplify when we require Erlang/OTP 24 -valid_float_size(16) -> erlang:system_info(otp_release) >= "24"; +valid_float_size(16) -> true; valid_float_size(32) -> true; valid_float_size(64) -> true; valid_float_size(_) -> false. diff --git a/lib/elixir/src/elixir_erl_compiler.erl b/lib/elixir/src/elixir_erl_compiler.erl index d3c8983cbd2..e9c8cdeac10 100644 --- a/lib/elixir/src/elixir_erl_compiler.erl +++ b/lib/elixir/src/elixir_erl_compiler.erl @@ -95,12 +95,6 @@ format_warnings(Opts, Warnings) -> handle_file_warning(_, _File, {_Line, v3_core, {map_key_repeated, _}}) -> ok; handle_file_warning(_, _File, {_Line, sys_core_fold, {ignored, useless_building}}) -> ok; -%% TODO: remove when we require Erlang/OTP 24 -handle_file_warning(_, _File, {_Line, sys_core_fold, useless_building}) -> ok; -handle_file_warning(true, _File, {_Line, sys_core_fold, nomatch_guard}) -> ok; -handle_file_warning(true, _File, {_Line, sys_core_fold, {nomatch_shadow, _}}) -> ok; -%% - %% Ignore all linting errors (only come up on parse transforms) handle_file_warning(_, _File, {_Line, erl_lint, _}) -> ok; @@ -155,18 +149,6 @@ custom_format(sys_core_fold, {failed, {eval_failure, {Mod, Name, Arity}, Error}} end, ["the call to ", Trimmed, " will fail with ", elixir_aliases:inspect(Struct)]; -%% TODO: remove when we require Erlang/OTP 24 -custom_format(sys_core_fold, {nomatch_shadow, Line, FA}) -> - custom_format(sys_core_fold, {nomatch, {shadow, Line, FA}}); -custom_format(sys_core_fold, nomatch_guard) -> - custom_format(sys_core_fold, {nomatch, guard}); -custom_format(sys_core_fold, {no_effect, X}) -> - custom_format(sys_core_fold, {ignored, {no_effect, X}}); -custom_format(sys_core_fold, {eval_failure, Error}) -> - #{'__struct__' := Struct} = 'Elixir.Exception':normalize(error, Error), - ["this expression will fail with ", elixir_aliases:inspect(Struct)]; -%% - custom_format([], Desc) -> io_lib:format("~p", [Desc]); diff --git a/lib/elixir/src/elixir_erl_var.erl b/lib/elixir/src/elixir_erl_var.erl index b4b27ebe282..ad3deb002fb 100644 --- a/lib/elixir/src/elixir_erl_var.erl +++ b/lib/elixir/src/elixir_erl_var.erl @@ -88,15 +88,7 @@ load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter) -> ) end; load_binding([], ExVars, ErlVars, Normalized, _Counter) -> - %% TODO: Remove me once we require Erlang/OTP 24+ - %% Also revisit dump_binding below and remove the vars field for simplicity. - Mod = - case erlang:system_info(otp_release) >= "24" of - true -> maps; - false -> orddict - end, - - {ExVars, maps:from_list(ErlVars), Mod:from_list(lists:reverse(Normalized))}. + {ExVars, maps:from_list(ErlVars), maps:from_list(lists:reverse(Normalized))}. load_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value}; load_pair({Pair, Value}) -> {Pair, Value}. @@ -110,20 +102,9 @@ dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVar end, ErlName = maps:get(Version, ErlVars), - Value = find_binding(ErlName, Binding), + Value = maps:get(ErlName, Binding, nil), [{Key, Value} | Acc]; (_, _, Acc) -> Acc end, [], ExVars). - -find_binding(ErlName, Binding = #{}) -> - case Binding of - #{ErlName := V} -> V; - _ -> nil - end; -find_binding(ErlName, Binding) -> - case orddict:find(ErlName, Binding) of - {ok, V} -> V; - error -> nil - end. diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 0a55616ed8f..56bde6883a7 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -2569,49 +2569,24 @@ defmodule Kernel.ExpansionTest do end end - # TODO: Simplify when we require Erlang/OTP 24 - if System.otp_release() >= "24" do - test "16-bit floats" do - import Kernel, except: [-: 1, -: 2] - - assert expand(quote(do: <<12.3::float-16>>)) |> clean_meta([:alignment]) == - quote(do: <<12.3::float()-size(16)>>) - end - - test "raises for invalid size * unit for floats" do - message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 128" - - assert_raise CompileError, message, fn -> - expand(quote(do: <<12.3::32*4>>)) - end + test "16-bit floats" do + import Kernel, except: [-: 1, -: 2] - message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 256" + assert expand(quote(do: <<12.3::float-16>>)) |> clean_meta([:alignment]) == + quote(do: <<12.3::float()-size(16)>>) + end - assert_raise CompileError, message, fn -> - expand(quote(do: <<12.3::256>>)) - end - end - else - test "16-bit floats" do - message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 16" + test "raises for invalid size * unit for floats" do + message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 128" - assert_raise CompileError, message, fn -> - expand(quote(do: <<12.3::16>>)) - end + assert_raise CompileError, message, fn -> + expand(quote(do: <<12.3::32*4>>)) end - test "raises for invalid size * unit for floats" do - message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 128" - - assert_raise CompileError, message, fn -> - expand(quote(do: <<12.3::32*4>>)) - end - - message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 256" + message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 256" - assert_raise CompileError, message, fn -> - expand(quote(do: <<12.3::256>>)) - end + assert_raise CompileError, message, fn -> + expand(quote(do: <<12.3::256>>)) end end diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 1c493b72c52..e23fd0223af 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -1067,15 +1067,6 @@ defmodule Kernel.WarningTest do purge(Sample) end - # TODO: Simplify when we require Erlang/OTP 24 - if System.otp_release() >= "24" do - @argument_error_message "the call to Atom.to_string/1" - @arithmetic_error_message "the call to +/2" - else - @argument_error_message "this expression" - @arithmetic_error_message "this expression" - end - test "eval failure warning" do assert capture_err(fn -> Code.eval_string(""" @@ -1083,7 +1074,7 @@ defmodule Kernel.WarningTest do def foo, do: Atom.to_string "abc" end """) - end) =~ "#{@argument_error_message} will fail with ArgumentError\n nofile:2" + end) =~ "the call to Atom.to_string/1 will fail with ArgumentError\n nofile:2" assert capture_err(fn -> Code.eval_string(""" @@ -1091,7 +1082,7 @@ defmodule Kernel.WarningTest do def foo, do: 1 + nil end """) - end) =~ "#{@arithmetic_error_message} will fail with ArithmeticError\n nofile:2" + end) =~ "the call to +/2 will fail with ArithmeticError\n nofile:2" after purge([Sample1, Sample2]) end diff --git a/lib/elixir/test/elixir/map_set_test.exs b/lib/elixir/test/elixir/map_set_test.exs index 46aa9b57e67..7741cb422f0 100644 --- a/lib/elixir/test/elixir/map_set_test.exs +++ b/lib/elixir/test/elixir/map_set_test.exs @@ -142,35 +142,6 @@ defmodule MapSetTest do assert MapSet.equal?(result, MapSet.new([1, 3])) end - test "MapSet v1 compatibility" do - result = 1..5 |> map_set_v1() |> MapSet.new() - assert MapSet.equal?(result, MapSet.new(1..5)) - - result = MapSet.put(map_set_v1(1..5), 6) - assert MapSet.equal?(result, MapSet.new(1..6)) - - result = MapSet.union(map_set_v1(1..5), MapSet.new(6..10)) - assert MapSet.equal?(result, MapSet.new(1..10)) - - result = MapSet.intersection(map_set_v1(1..10), MapSet.new(6..15)) - assert MapSet.equal?(result, MapSet.new(6..10)) - - result = MapSet.difference(map_set_v1(1..10), MapSet.new(6..50)) - assert MapSet.equal?(result, MapSet.new(1..5)) - - result = MapSet.delete(map_set_v1(1..10), 1) - assert MapSet.equal?(result, MapSet.new(2..10)) - - assert MapSet.size(map_set_v1(1..5)) == 5 - assert MapSet.to_list(map_set_v1(1..5)) == Enum.to_list(1..5) - - assert MapSet.disjoint?(map_set_v1(1..5), MapSet.new(10..15)) - refute MapSet.disjoint?(map_set_v1(1..5), MapSet.new(5..10)) - - assert MapSet.subset?(map_set_v1(3..7), MapSet.new(1..10)) - refute MapSet.subset?(map_set_v1(7..12), MapSet.new(1..10)) - end - test "inspect" do assert inspect(MapSet.new([?a])) == "MapSet.new([97])" end diff --git a/lib/mix/lib/mix/tasks/compile.erlang.ex b/lib/mix/lib/mix/tasks/compile.erlang.ex index fcaee3148b4..82ef4b12f78 100644 --- a/lib/mix/lib/mix/tasks/compile.erlang.ex +++ b/lib/mix/lib/mix/tasks/compile.erlang.ex @@ -91,8 +91,7 @@ defmodule Mix.Tasks.Compile.Erlang do file = Erlang.to_erl_file(Path.rootname(input, ".erl")) case :compile.file(file, erlc_options) do - # TODO: Don't handle {:error, :badarg} when we require Erlang/OTP 24 - error when error == :error or error == {:error, :badarg} -> + :error -> message = "Compiling Erlang file #{inspect(file)} failed, probably because of invalid :erlc_options" From 0960c92f783068512c96a76b05acbc4b64682adc Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 27 Jul 2022 20:28:24 +0900 Subject: [PATCH 10/59] Warn on missing parentheses in bitstring modifiers (#11862) --- lib/elixir/src/elixir_bitstring.erl | 61 ++++++++++++------- lib/elixir/src/elixir_quote.erl | 2 +- lib/elixir/test/elixir/kernel/binary_test.exs | 30 +++++++-- lib/elixir/test/elixir/macro_test.exs | 2 +- .../test/elixir/module/types/expr_test.exs | 6 +- .../test/elixir/module/types/pattern_test.exs | 6 +- 6 files changed, 78 insertions(+), 29 deletions(-) diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index 40a29668e06..7977a289fdd 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -184,9 +184,14 @@ type(_, default, Type, _) -> type(Meta, Other, Value, E) -> form_error(Meta, E, ?MODULE, {bittype_mismatch, Value, Other, type}). -expand_each_spec(Meta, [{Expr, _, Args} = H | T], Map, S, OriginalS, E) when is_atom(Expr) -> +expand_each_spec(Meta, [{Expr, MetaE, Args} = H | T], Map, S, OriginalS, E) when is_atom(Expr) -> case validate_spec(Expr, Args) of {Key, Arg} -> + case Args of + [] -> + elixir_errors:form_warn(Meta, E, ?MODULE, {parens_bittype, Expr}); + _ -> ok + end, {Value, SE, EE} = expand_spec_arg(Arg, S, OriginalS, E), validate_spec_arg(Meta, Key, Value, SE, OriginalS, EE), @@ -199,8 +204,14 @@ expand_each_spec(Meta, [{Expr, _, Args} = H | T], Map, S, OriginalS, E) when is_ expand_each_spec(Meta, T, maps:put(Key, Value, Map), SE, OriginalS, EE); none -> - case 'Elixir.Macro':expand(H, E#{line := ?line(Meta)}) of - H -> + HA = case Args of + nil -> + elixir_errors:form_warn(Meta, E, ?MODULE, {unknown_bittype, Expr}), + {Expr, MetaE, []}; + _ -> H + end, + case 'Elixir.Macro':expand(HA, E#{line := ?line(Meta)}) of + HA -> form_error(Meta, E, ?MODULE, {undefined_bittype, H}); NewTypes -> @@ -221,28 +232,29 @@ unpack_specs({'*', _, [Size, Unit]}, Acc) -> unpack_specs(Size, Acc) when is_integer(Size) -> [{size, [], [Size]} | Acc]; unpack_specs({Expr, Meta, Args}, Acc) when is_atom(Expr) -> - ListArgs = if is_atom(Args) -> []; is_list(Args) -> Args end, + ListArgs = if is_atom(Args) -> nil; is_list(Args) -> Args end, [{Expr, Meta, ListArgs} | Acc]; unpack_specs(Other, Acc) -> [Other | Acc]. -validate_spec(big, []) -> {endianness, big}; -validate_spec(little, []) -> {endianness, little}; -validate_spec(native, []) -> {endianness, native}; -validate_spec(size, [Size]) -> {size, Size}; -validate_spec(unit, [Unit]) -> {unit, Unit}; -validate_spec(integer, []) -> {type, integer}; -validate_spec(float, []) -> {type, float}; -validate_spec(binary, []) -> {type, binary}; -validate_spec(bytes, []) -> {type, binary}; -validate_spec(bitstring, []) -> {type, bitstring}; -validate_spec(bits, []) -> {type, bitstring}; -validate_spec(utf8, []) -> {type, utf8}; -validate_spec(utf16, []) -> {type, utf16}; -validate_spec(utf32, []) -> {type, utf32}; -validate_spec(signed, []) -> {sign, signed}; -validate_spec(unsigned, []) -> {sign, unsigned}; -validate_spec(_, _) -> none. +validate_spec(Spec, []) -> validate_spec(Spec, nil); +validate_spec(big, nil) -> {endianness, big}; +validate_spec(little, nil) -> {endianness, little}; +validate_spec(native, nil) -> {endianness, native}; +validate_spec(size, [Size]) -> {size, Size}; +validate_spec(unit, [Unit]) -> {unit, Unit}; +validate_spec(integer, nil) -> {type, integer}; +validate_spec(float, nil) -> {type, float}; +validate_spec(binary, nil) -> {type, binary}; +validate_spec(bytes, nil) -> {type, binary}; +validate_spec(bitstring, nil) -> {type, bitstring}; +validate_spec(bits, nil) -> {type, bitstring}; +validate_spec(utf8, nil) -> {type, utf8}; +validate_spec(utf16, nil) -> {type, utf16}; +validate_spec(utf32, nil) -> {type, utf32}; +validate_spec(signed, nil) -> {sign, signed}; +validate_spec(unsigned, nil) -> {sign, unsigned}; +validate_spec(_, _) -> none. expand_spec_arg(Expr, S, _OriginalS, E) when is_atom(Expr); is_integer(Expr) -> {Expr, S, E}; @@ -373,6 +385,13 @@ format_error({invalid_literal, Literal}) -> io_lib:format("invalid literal ~ts in <<>>", ['Elixir.Macro':to_string(Literal)]); format_error({undefined_bittype, Expr}) -> io_lib:format("unknown bitstring specifier: ~ts", ['Elixir.Macro':to_string(Expr)]); +format_error({unknown_bittype, Name}) -> + io_lib:format("bitstring specifier \"~ts\" does not exist and is being expanded to \"~ts()\"," + " please use parentheses to remove the ambiguity", [Name, Name]); +format_error({parens_bittype, Name}) -> + io_lib:format("extra parentheses on a bitstring specifier \"~ts()\" have been deprecated. " + "Please remove the parentheses: \"~ts\"", + [Name, Name]); format_error({bittype_mismatch, Val1, Val2, Where}) -> io_lib:format("conflicting ~ts specification for bit field: \"~p\" and \"~p\"", [Where, Val1, Val2]); format_error({bad_unit_argument, Unit}) -> diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index dc77ca685ce..3916435e31b 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -140,7 +140,7 @@ do_escape(BitString, _, _) when is_bitstring(BitString) -> BitString; Size -> <> = BitString, - {'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], []}]}]} + {'<<>>', [], [{'::', [], [Bits, {size, [], [Size]}]}, {'::', [], [Bytes, {binary, [], nil}]}]} end; do_escape(Map, Q, E) when is_map(Map) -> diff --git a/lib/elixir/test/elixir/kernel/binary_test.exs b/lib/elixir/test/elixir/kernel/binary_test.exs index 884153f70d8..132e2d393c2 100644 --- a/lib/elixir/test/elixir/kernel/binary_test.exs +++ b/lib/elixir/test/elixir/kernel/binary_test.exs @@ -264,13 +264,13 @@ defmodule Kernel.BinaryTest do assert <<1::size(foo.bar)>> = <<1::5>> end - defmacrop signed_16 do + defmacro signed_16 do quote do big - signed - integer - unit(16) end end - defmacrop refb_spec do + defmacro refb_spec do quote do 1 * 8 - big - signed - integer end @@ -281,10 +281,32 @@ defmodule Kernel.BinaryTest do sec_data = "another" << - byte_size(refb)::refb_spec, + byte_size(refb)::refb_spec(), refb::binary, - byte_size(sec_data)::size(1)-signed_16, + byte_size(sec_data)::size(1)-signed_16(), sec_data::binary >> end + + test "bitsyntax macro is expanded with a warning" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + Code.eval_string("<<1::refb_spec>>", [], __ENV__) + end) =~ + "bitstring specifier \"refb_spec\" does not exist and is being expanded to \"refb_spec()\"" + + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + Code.eval_string("<<1::size(1)-signed_16>>", [], __ENV__) + end) =~ + "bitstring specifier \"signed_16\" does not exist and is being expanded to \"signed_16()\"" + end + + test "bitsyntax with extra parentheses warns" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + Code.eval_string("<<1::big()>>") + end) =~ "extra parentheses on a bitstring specifier \"big()\" have been deprecated" + + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + Code.eval_string("<<1::size(8)-integer()>>") + end) =~ "extra parentheses on a bitstring specifier \"integer()\" have been deprecated" + end end diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 9012364a9c3..5c0fd2079e7 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -44,7 +44,7 @@ defmodule MacroTest do test "escapes bitstring" do assert {:<<>>, [], args} = Macro.escape(<<300::12>>) - assert [{:"::", [], [1, {:size, [], [4]}]}, {:"::", [], [",", {:binary, [], []}]}] = args + assert [{:"::", [], [1, {:size, [], [4]}]}, {:"::", [], [",", {:binary, [], nil}]}] = args end test "escapes recursively" do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index ec1b1872812..6479faaf923 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -68,12 +68,16 @@ defmodule Module.Types.ExprTest do assert quoted_expr(<<"foo"::utf8>>) == {:ok, :binary} end + defmacrop custom_type do + quote do: 1 * 8 - big - signed - integer + end + test "variable" do assert quoted_expr([foo], <>) == {:ok, :binary} assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} assert quoted_expr([foo], <>) == {:ok, :binary} assert quoted_expr([foo], <>) == {:ok, :binary} + assert quoted_expr([foo], <>) == {:ok, :binary} end test "infer" do diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index c8bb56d2d9d..f6d20bb6422 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -186,16 +186,20 @@ defmodule Module.Types.PatternTest do ]}} end + defmacrop custom_type do + quote do: 1 * 8 - big - signed - integer + end + test "binary" do assert quoted_pattern(<<"foo"::binary>>) == {:ok, :binary} assert quoted_pattern(<<123::integer>>) == {:ok, :binary} assert quoted_pattern(<>) == {:ok, :binary} assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} assert quoted_pattern(<>) == {:ok, :binary} assert quoted_pattern(<>) == {:ok, :binary} assert quoted_pattern(<<123::utf8>>) == {:ok, :binary} assert quoted_pattern(<<"foo"::utf8>>) == {:ok, :binary} + assert quoted_pattern(<>) == {:ok, :binary} assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :integer]}} assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :binary]}} From 1dcfe9fa9cafd7263dcf3b5d632cc3ee87e71989 Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Wed, 27 Jul 2022 11:43:13 +0000 Subject: [PATCH 11/59] Fix bug DynamicSupervisor child_spec validation (#11836) It would not accept 0 as a valid shutdown value. It only happened in DynamicSupervisor, but tests are added for both DynamicSupervisor and Supervisor modules. --- lib/elixir/lib/dynamic_supervisor.ex | 2 +- .../test/elixir/dynamic_supervisor_test.exs | 32 +++++++++++++++++++ lib/elixir/test/elixir/supervisor_test.exs | 26 +++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/dynamic_supervisor.ex b/lib/elixir/lib/dynamic_supervisor.ex index c3d847682f8..e05b3653b6a 100644 --- a/lib/elixir/lib/dynamic_supervisor.ex +++ b/lib/elixir/lib/dynamic_supervisor.ex @@ -448,7 +448,7 @@ defmodule DynamicSupervisor do defp validate_restart(restart) when restart in [:permanent, :temporary, :transient], do: :ok defp validate_restart(restart), do: {:invalid_restart_type, restart} - defp validate_shutdown(shutdown) when is_integer(shutdown) and shutdown > 0, do: :ok + defp validate_shutdown(shutdown) when is_integer(shutdown) and shutdown >= 0, do: :ok defp validate_shutdown(shutdown) when shutdown in [:infinity, :brutal_kill], do: :ok defp validate_shutdown(shutdown), do: {:invalid_shutdown, shutdown} diff --git a/lib/elixir/test/elixir/dynamic_supervisor_test.exs b/lib/elixir/test/elixir/dynamic_supervisor_test.exs index ae55c014bcf..6ff44ed2dfd 100644 --- a/lib/elixir/test/elixir/dynamic_supervisor_test.exs +++ b/lib/elixir/test/elixir/dynamic_supervisor_test.exs @@ -229,6 +229,13 @@ defmodule DynamicSupervisorTest do assert DynamicSupervisor.start_child(:not_used, %{id: 1, start: {Task, :foo, :bar}}) == {:error, {:invalid_mfa, {Task, :foo, :bar}}} + + assert DynamicSupervisor.start_child(:not_used, %{ + id: 1, + start: {Task, :foo, [:bar]}, + shutdown: -1 + }) == + {:error, {:invalid_shutdown, -1}} end test "with different returns" do @@ -451,6 +458,31 @@ defmodule DynamicSupervisorTest do assert_receive {:EXIT, ^pid, :shutdown} end + test "with valid shutdown" do + Process.flag(:trap_exit, true) + + {:ok, pid} = DynamicSupervisor.start_link(strategy: :one_for_one) + + for n <- 0..1 do + assert {:ok, child_pid} = + DynamicSupervisor.start_child(pid, %{ + id: n, + start: {Task, :start_link, [fn -> :ok end]}, + shutdown: n + }) + + assert_kill(child_pid, :shutdown) + end + end + + test "with invalid valid shutdown" do + assert DynamicSupervisor.start_child(:not_used, %{ + id: 1, + start: {Task, :start_link, [fn -> :ok end]}, + shutdown: -1 + }) == {:error, {:invalid_shutdown, -1}} + end + def start_link(:ok3), do: {:ok, spawn_link(fn -> Process.sleep(:infinity) end), :extra} def start_link(:ok2), do: {:ok, spawn_link(fn -> Process.sleep(:infinity) end)} def start_link(:error), do: {:error, :found} diff --git a/lib/elixir/test/elixir/supervisor_test.exs b/lib/elixir/test/elixir/supervisor_test.exs index 6f18bcc6f9f..e51faa88503 100644 --- a/lib/elixir/test/elixir/supervisor_test.exs +++ b/lib/elixir/test/elixir/supervisor_test.exs @@ -227,6 +227,26 @@ defmodule SupervisorTest do assert Supervisor.start_child(pid, %{id: 1, start: {Task, :foo, :bar}}) == {:error, {:invalid_mfa, {Task, :foo, :bar}}} + + assert Supervisor.start_child(pid, %{id: 1, start: {Task, :foo, [:bar]}, shutdown: -1}) == + {:error, {:invalid_shutdown, -1}} + end + + test "with valid child spec" do + Process.flag(:trap_exit, true) + + {:ok, pid} = Supervisor.start_link([], strategy: :one_for_one) + + for n <- 0..1 do + assert {:ok, child_pid} = + Supervisor.start_child(pid, %{ + id: n, + start: {Task, :start_link, [fn -> :ok end]}, + shutdown: n + }) + + assert_kill(child_pid, :shutdown) + end end end @@ -261,4 +281,10 @@ defmodule SupervisorTest do wait_until_registered(name) end end + + defp assert_kill(pid, reason) do + ref = Process.monitor(pid) + Process.exit(pid, reason) + assert_receive {:DOWN, ^ref, _, _, _} + end end From 14a1ee4c29061a10b2337a14ec7126b2afe4f5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 16:00:59 +0200 Subject: [PATCH 12/59] Improvements to tasks --- lib/elixir/lib/gen_server.ex | 15 ++--- lib/elixir/lib/task.ex | 88 ++++++++++++++----------- lib/elixir/test/elixir/map_set_test.exs | 5 -- lib/elixir/test/elixir/task_test.exs | 88 ++++++++++++++----------- 4 files changed, 104 insertions(+), 92 deletions(-) diff --git a/lib/elixir/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index c08c122c8d5..ae7a42ea39d 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -1043,12 +1043,12 @@ defmodule GenServer do end @doc """ - Sends an asynchronous request to the `server`. + Casts a request to the `server` without waiting for a response. This function always returns `:ok` regardless of whether the destination `server` (or node) exists. Therefore it is unknown whether the destination `server` successfully - handled the message. + handled the request. `server` can be any of the values described in the "Name registration" section of the documentation for this module. @@ -1123,10 +1123,6 @@ defmodule GenServer do `nodes` is a list of node names to which the request is sent. The default value is the list of all known nodes (including this node). - To avoid that late answers (after the timeout) pollute the caller's message - queue, a middleman process is used to do the actual calls. Late answers will - then be discarded when they arrive to a terminated process. - ## Examples Assuming the `Stack` GenServer mentioned in the docs for the `GenServer` @@ -1174,11 +1170,8 @@ defmodule GenServer do """ @spec reply(from, term) :: :ok - def reply(client, reply) - - def reply({to, tag}, reply) when is_pid(to) do - send(to, {tag, reply}) - :ok + def reply(client, reply) do + :gen.reply(client, reply) end @doc """ diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index a8c9c243385..23a59c04963 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -230,10 +230,10 @@ defmodule Task do * `:owner` - the PID of the process that started the task - * `:pid` - the PID of the task process; `nil` if the task does - not use a task process + * `:pid` - the PID of the task process; `nil` if there is no process + specifically assigned for the task - * `:ref` - the task monitor reference + * `:ref` - an opaque term used as the task monitor reference """ @enforce_keys [:mfa, :owner, :pid, :ref] @@ -248,9 +248,14 @@ defmodule Task do mfa: mfa(), owner: pid(), pid: pid() | nil, - ref: reference() + ref: ref() } + @typedoc """ + The task opaque reference. + """ + @opaque ref :: reference() + defguardp is_timeout(timeout) when timeout == :infinity or (is_integer(timeout) and timeout >= 0) @@ -774,7 +779,7 @@ defmodule Task do # If the task succeeds... def handle_info({ref, result}, state) do - # The task succeed so we can cancel the monitoring and discard the DOWN message + # The task succeed so we can demonitor its reference Process.demonitor(ref, [:flush]) {url, state} = pop_in(state.tasks[ref]) @@ -809,14 +814,14 @@ defmodule Task do receive do {^ref, reply} -> - Process.demonitor(ref, [:flush]) + demonitor(ref) reply {:DOWN, ^ref, _, proc, reason} -> exit({reason(reason, proc), {__MODULE__, :await, [task, timeout]}}) after timeout -> - Process.demonitor(ref, [:flush]) + demonitor(ref) exit({:timeout, {__MODULE__, :await, [task, timeout]}}) end end @@ -842,8 +847,8 @@ defmodule Task do receive do {^ref, reply} -> - Process.unlink(pid) - Process.demonitor(ref, [:flush]) + pid && Process.unlink(pid) + demonitor(ref) {:ok, reply} {:DOWN, ^ref, _, proc, :noconnection} -> @@ -853,8 +858,8 @@ defmodule Task do {:exit, reason} after 0 -> - Process.unlink(pid) - Process.demonitor(ref, [:flush]) + pid && Process.unlink(pid) + demonitor(ref) nil end end @@ -958,7 +963,7 @@ defmodule Task do defp demonitor_pending_tasks(awaiting) do Enum.each(awaiting, fn {ref, _} -> - Process.demonitor(ref, [:flush]) + demonitor(ref) end) end @@ -967,7 +972,7 @@ defmodule Task do def find(tasks, {ref, reply}) when is_reference(ref) do Enum.find_value(tasks, fn %Task{ref: ^ref} = task -> - Process.demonitor(ref, [:flush]) + demonitor(ref) {reply, task} %Task{} -> @@ -1047,7 +1052,7 @@ defmodule Task do receive do {^ref, reply} -> - Process.demonitor(ref, [:flush]) + demonitor(ref) {:ok, reply} {:DOWN, ^ref, _, proc, :noconnection} -> @@ -1149,7 +1154,7 @@ defmodule Task do receive do {^ref, reply} -> - Process.demonitor(ref, [:flush]) + demonitor(ref) [{task, {:ok, reply}} | yield_many(rest, timeout_ref, timeout)] {:DOWN, ^ref, _, proc, :noconnection} -> @@ -1190,6 +1195,10 @@ defmodule Task do `:shutdown` to shut down all of its linked processes, including tasks, that are not trapping exits without generating any log messages. + If there is no process linked to the task, such as tasks started by + `Task.completed/1`, we check for a response or error accordingly, but without + shutting a process down. + If a task's monitor has already been demonitored or received and there is not a response waiting in the message queue this function will return `{:exit, :noproc}` as the result or exit reason can not be determined. @@ -1198,7 +1207,7 @@ defmodule Task do def shutdown(task, shutdown \\ 5000) def shutdown(%Task{pid: nil} = task, _) do - raise ArgumentError, "task #{inspect(task)} does not have an associated task process" + ignore(task) end def shutdown(%Task{owner: owner} = task, _) when owner != self() do @@ -1207,7 +1216,7 @@ defmodule Task do def shutdown(%Task{pid: pid} = task, :brutal_kill) do mon = Process.monitor(pid) - exit(pid, :kill) + shutdown_send(pid, :kill) case shutdown_receive(task, mon, :brutal_kill, :infinity) do {:down, proc, :noconnection} -> @@ -1223,7 +1232,7 @@ defmodule Task do def shutdown(%Task{pid: pid} = task, timeout) when is_timeout(timeout) do mon = Process.monitor(pid) - exit(pid, :shutdown) + shutdown_send(pid, :shutdown) case shutdown_receive(task, mon, :shutdown, timeout) do {:down, proc, :noconnection} -> @@ -1237,27 +1246,19 @@ defmodule Task do end end - ## Helpers - - defp reason(:noconnection, proc), do: {:nodedown, monitor_node(proc)} - defp reason(reason, _), do: reason - - defp monitor_node(pid) when is_pid(pid), do: node(pid) - defp monitor_node({_, node}), do: node - - # spawn a process to ensure task gets exit signal if process dies from exit signal - # between unlink and exit. - defp exit(task, reason) do + # Spawn a process to ensure task gets exit signal + # if process dies from exit signal between unlink and exit. + defp shutdown_send(pid, reason) do caller = self() ref = make_ref() - enforcer = spawn(fn -> enforce_exit(task, reason, caller, ref) end) - Process.unlink(task) - Process.exit(task, reason) + enforcer = spawn(fn -> shutdown_send(pid, reason, caller, ref) end) + Process.unlink(pid) + Process.exit(pid, reason) send(enforcer, {:done, ref}) :ok end - defp enforce_exit(pid, reason, caller, ref) do + defp shutdown_send(pid, reason, caller, ref) do mon = Process.monitor(caller) receive do @@ -1269,11 +1270,11 @@ defmodule Task do defp shutdown_receive(%{ref: ref} = task, mon, type, timeout) do receive do {:DOWN, ^mon, _, _, :shutdown} when type in [:shutdown, :timeout_kill] -> - Process.demonitor(ref, [:flush]) + demonitor(ref) flush_reply(ref) {:DOWN, ^mon, _, _, :killed} when type == :brutal_kill -> - Process.demonitor(ref, [:flush]) + demonitor(ref) flush_reply(ref) {:DOWN, ^mon, _, proc, :noproc} -> @@ -1281,7 +1282,7 @@ defmodule Task do flush_reply(ref) || reason {:DOWN, ^mon, _, proc, reason} -> - Process.demonitor(ref, [:flush]) + demonitor(ref) flush_reply(ref) || {:down, proc, reason} after timeout -> @@ -1310,11 +1311,24 @@ defmodule Task do {:down, proc, reason} after 0 -> - Process.demonitor(ref, [:flush]) + demonitor(ref) {:down, proc, :noproc} end end + ## Helpers + + defp demonitor(ref) when is_reference(ref) do + Process.demonitor(ref, [:flush]) + :ok + end + + defp reason(:noconnection, proc), do: {:nodedown, monitor_node(proc)} + defp reason(reason, _), do: reason + + defp monitor_node(pid) when is_pid(pid), do: node(pid) + defp monitor_node({_, node}), do: node + defp invalid_owner_error(task) do "task #{inspect(task)} must be queried from the owner but was queried from #{inspect(self())}" end diff --git a/lib/elixir/test/elixir/map_set_test.exs b/lib/elixir/test/elixir/map_set_test.exs index 7741cb422f0..21a5b51aea9 100644 --- a/lib/elixir/test/elixir/map_set_test.exs +++ b/lib/elixir/test/elixir/map_set_test.exs @@ -145,9 +145,4 @@ defmodule MapSetTest do test "inspect" do assert inspect(MapSet.new([?a])) == "MapSet.new([97])" end - - defp map_set_v1(enumerable) do - map = Map.from_keys(Enum.to_list(enumerable), true) - %{__struct__: MapSet, map: map} - end end diff --git a/lib/elixir/test/elixir/task_test.exs b/lib/elixir/test/elixir/task_test.exs index 71598071837..e0335bc6ded 100644 --- a/lib/elixir/test/elixir/task_test.exs +++ b/lib/elixir/test/elixir/task_test.exs @@ -211,45 +211,47 @@ defmodule TaskTest do assert_receive :done end - if System.otp_release() >= "24" do - describe "ignore/1" do - test "discards on time replies" do - task = Task.async(fn -> :ok end) - wait_until_down(task) - assert Task.ignore(task) == {:ok, :ok} - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - end + describe "ignore/1" do + test "discards on time replies" do + task = Task.async(fn -> :ok end) + wait_until_down(task) + assert Task.ignore(task) == {:ok, :ok} + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + end - test "discards late replies" do - task = Task.async(fn -> assert_receive(:go) && :ok end) - assert Task.ignore(task) == nil - send(task.pid, :go) - wait_until_down(task) - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - end + test "discards late replies" do + task = Task.async(fn -> assert_receive(:go) && :ok end) + assert Task.ignore(task) == nil + send(task.pid, :go) + wait_until_down(task) + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + end - test "discards on-time failures" do - Process.flag(:trap_exit, true) - task = Task.async(fn -> exit(:oops) end) - wait_until_down(task) - assert Task.ignore(task) == {:exit, :oops} - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - end + test "discards on-time failures" do + Process.flag(:trap_exit, true) + task = Task.async(fn -> exit(:oops) end) + wait_until_down(task) + assert Task.ignore(task) == {:exit, :oops} + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + end - test "discards late failures" do - task = Task.async(fn -> assert_receive(:go) && exit(:oops) end) - assert Task.ignore(task) == nil - send(task.pid, :go) - wait_until_down(task) - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - end + test "discards late failures" do + task = Task.async(fn -> assert_receive(:go) && exit(:oops) end) + assert Task.ignore(task) == nil + send(task.pid, :go) + wait_until_down(task) + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + end - test "exits on :noconnection" do - ref = make_ref() - task = %Task{ref: ref, pid: self(), owner: self(), mfa: {__MODULE__, :test, 1}} - send(self(), {:DOWN, ref, self(), self(), :noconnection}) - assert catch_exit(Task.ignore(task)) |> elem(0) == {:nodedown, :nonode@nohost} - end + test "exits on :noconnection" do + ref = make_ref() + task = %Task{ref: ref, pid: self(), owner: self(), mfa: {__MODULE__, :test, 1}} + send(self(), {:DOWN, ref, self(), self(), :noconnection}) + assert catch_exit(Task.ignore(task)) |> elem(0) == {:nodedown, :nonode@nohost} + end + + test "can ignore completed tasks" do + assert Task.ignore(Task.completed(:done)) == {:ok, :done} end end @@ -652,10 +654,18 @@ defmodule TaskTest do {{:nodedown, node()}, {Task, :shutdown, [task, 5000]}} end - test "raises if task PID is nil" do - task = %Task{ref: make_ref(), owner: nil, pid: nil, mfa: {__MODULE__, :test, 1}} - message = "task #{inspect(task)} does not have an associated task process" - assert_raise ArgumentError, message, fn -> Task.shutdown(task) end + test "ignores if task PID is nil" do + ref = make_ref() + send(self(), {ref, :done}) + + assert Task.shutdown(%Task{ref: ref, owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}}) == + {:ok, :done} + + ref = make_ref() + send(self(), {:DOWN, ref, :process, self(), :done}) + + assert Task.shutdown(%Task{ref: ref, owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}}) == + {:exit, :done} end test "raises when invoked from a non-owner process" do From 8271fc5f0eb37316c3abad1d8aa8a9319eb2c720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 16:02:43 +0200 Subject: [PATCH 13/59] Remove more conditional tests --- lib/elixir/test/elixir/exception_test.exs | 48 ++++++++--------- lib/elixir/test/elixir/kernel/raise_test.exs | 24 ++++----- .../test/elixir/task/supervisor_test.exs | 32 ++++++------ lib/elixir/test/elixir/task_test.exs | 26 +++++----- lib/mix/test/mix/tasks/app.tree_test.exs | 52 +++++++++---------- .../test/mix/tasks/compile.erlang_test.exs | 6 +-- lib/mix/test/mix/tasks/compile.yecc_test.exs | 6 +-- lib/mix/test/mix/tasks/compile_test.exs | 6 +-- 8 files changed, 89 insertions(+), 111 deletions(-) diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index e3dfa59696d..6c067634940 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -875,36 +875,34 @@ defmodule ExceptionTest do end end - if System.otp_release() >= "24" do - describe "error_info" do - test "badarg on erlang" do - assert message(:erlang, & &1.element("foo", "bar")) == """ - errors were found at the given arguments: - - * 1st argument: not an integer - * 2nd argument: not a tuple - """ - end + describe "error_info" do + test "badarg on erlang" do + assert message(:erlang, & &1.element("foo", "bar")) == """ + errors were found at the given arguments: - test "badarg on ets" do - ets = :ets.new(:foo, []) - :ets.delete(ets) + * 1st argument: not an integer + * 2nd argument: not a tuple + """ + end - assert message(:ets, & &1.insert(ets, 1)) == """ - errors were found at the given arguments: + test "badarg on ets" do + ets = :ets.new(:foo, []) + :ets.delete(ets) - * 1st argument: the table identifier does not refer to an existing ETS table - * 2nd argument: not a tuple - """ - end + assert message(:ets, & &1.insert(ets, 1)) == """ + errors were found at the given arguments: - test "system_limit on counters" do - assert message(:counters, & &1.new(123_456_789_123_456_789_123_456_789, [])) == """ - a system limit has been reached due to errors at the given arguments: + * 1st argument: the table identifier does not refer to an existing ETS table + * 2nd argument: not a tuple + """ + end - * 1st argument: counters array size reached a system limit - """ - end + test "system_limit on counters" do + assert message(:counters, & &1.new(123_456_789_123_456_789_123_456_789, [])) == """ + a system limit has been reached due to errors at the given arguments: + + * 1st argument: counters array size reached a system limit + """ end end diff --git a/lib/elixir/test/elixir/kernel/raise_test.exs b/lib/elixir/test/elixir/kernel/raise_test.exs index b4265c639cd..5fd61167a0c 100644 --- a/lib/elixir/test/elixir/kernel/raise_test.exs +++ b/lib/elixir/test/elixir/kernel/raise_test.exs @@ -71,21 +71,19 @@ defmodule Kernel.RaiseTest do end end - if System.otp_release() >= "24" do - test "raise with error_info" do - {exception, stacktrace} = - try do - raise "a" - rescue - e -> {e, __STACKTRACE__} - end + test "raise with error_info" do + {exception, stacktrace} = + try do + raise "a" + rescue + e -> {e, __STACKTRACE__} + end - assert [{__MODULE__, _, _, meta} | _] = stacktrace - assert meta[:error_info] == %{module: Exception} + assert [{__MODULE__, _, _, meta} | _] = stacktrace + assert meta[:error_info] == %{module: Exception} - assert Exception.format_error(exception, stacktrace) == - %{general: "a", reason: "#Elixir.RuntimeError"} - end + assert Exception.format_error(exception, stacktrace) == + %{general: "a", reason: "#Elixir.RuntimeError"} end test "reraise message" do diff --git a/lib/elixir/test/elixir/task/supervisor_test.exs b/lib/elixir/test/elixir/task/supervisor_test.exs index aa4e004e5f9..090c9908f13 100644 --- a/lib/elixir/test/elixir/task/supervisor_test.exs +++ b/lib/elixir/test/elixir/task/supervisor_test.exs @@ -319,23 +319,21 @@ defmodule Task.SupervisorTest do end describe "await/1" do - if System.otp_release() >= "24" do - test "demonitors and unalias on timeout", config do - task = - Task.Supervisor.async(config[:supervisor], fn -> - assert_receive :go - :done - end) - - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - new_ref = Process.monitor(task.pid) - old_ref = task.ref - - send(task.pid, :go) - assert_receive {:DOWN, ^new_ref, _, _, _} - refute_received {^old_ref, :done} - refute_received {:DOWN, ^old_ref, _, _, _} - end + test "demonitors and unalias on timeout", config do + task = + Task.Supervisor.async(config[:supervisor], fn -> + assert_receive :go + :done + end) + + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + new_ref = Process.monitor(task.pid) + old_ref = task.ref + + send(task.pid, :go) + assert_receive {:DOWN, ^new_ref, _, _, _} + refute_received {^old_ref, :done} + refute_received {:DOWN, ^old_ref, _, _, _} end test "exits on task throw", config do diff --git a/lib/elixir/test/elixir/task_test.exs b/lib/elixir/test/elixir/task_test.exs index e0335bc6ded..5c35f4a299b 100644 --- a/lib/elixir/test/elixir/task_test.exs +++ b/lib/elixir/test/elixir/task_test.exs @@ -256,22 +256,20 @@ defmodule TaskTest do end describe "await/2" do - if System.otp_release() >= "24" do - test "demonitors and unalias on timeout" do - task = - Task.async(fn -> - assert_receive :go - :done - end) + test "demonitors and unalias on timeout" do + task = + Task.async(fn -> + assert_receive :go + :done + end) - assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} - send(task.pid, :go) - ref = task.ref + assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}} + send(task.pid, :go) + ref = task.ref - wait_until_down(task) - refute_received {^ref, :done} - refute_received {:DOWN, ^ref, _, _, _} - end + wait_until_down(task) + refute_received {^ref, :done} + refute_received {:DOWN, ^ref, _, _, _} end test "exits on timeout" do diff --git a/lib/mix/test/mix/tasks/app.tree_test.exs b/lib/mix/test/mix/tasks/app.tree_test.exs index 82b96b58786..49bfae5e726 100644 --- a/lib/mix/test/mix/tasks/app.tree_test.exs +++ b/lib/mix/test/mix/tasks/app.tree_test.exs @@ -44,34 +44,32 @@ defmodule Mix.Tasks.App.TreeTest do end) end - if System.otp_release() >= "24" do - @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample] - test "shows the application tree with optional apps", context do - in_tmp(context.test, fn -> - Mix.Project.push(AppDepsSample) - - load_apps([:app_deps2_sample]) - Mix.Tasks.App.Tree.run(["--format", "pretty"]) - assert_received {:mix_shell, :info, ["test"]} - assert_received {:mix_shell, :info, ["├── app_deps_sample"]} - assert_received {:mix_shell, :info, ["│ ├── app_deps2_sample (optional)"]} - assert_received {:mix_shell, :info, ["│ │ └── app_deps4_sample (included)"]} - assert_received {:mix_shell, :info, ["│ └── app_deps3_sample"]} - assert_received {:mix_shell, :info, ["├── elixir"]} - assert_received {:mix_shell, :info, ["└── logger"]} - assert_received {:mix_shell, :info, [" └── elixir"]} + @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample] + test "shows the application tree with optional apps", context do + in_tmp(context.test, fn -> + Mix.Project.push(AppDepsSample) - Application.unload(:app_deps2_sample) - Mix.Tasks.App.Tree.run(["--format", "pretty"]) - assert_received {:mix_shell, :info, ["test"]} - assert_received {:mix_shell, :info, ["├── app_deps_sample"]} - assert_received {:mix_shell, :info, ["│ ├── app_deps2_sample (optional - missing)"]} - assert_received {:mix_shell, :info, ["│ └── app_deps3_sample"]} - assert_received {:mix_shell, :info, ["├── elixir"]} - assert_received {:mix_shell, :info, ["└── logger"]} - assert_received {:mix_shell, :info, [" └── elixir"]} - end) - end + load_apps([:app_deps2_sample]) + Mix.Tasks.App.Tree.run(["--format", "pretty"]) + assert_received {:mix_shell, :info, ["test"]} + assert_received {:mix_shell, :info, ["├── app_deps_sample"]} + assert_received {:mix_shell, :info, ["│ ├── app_deps2_sample (optional)"]} + assert_received {:mix_shell, :info, ["│ │ └── app_deps4_sample (included)"]} + assert_received {:mix_shell, :info, ["│ └── app_deps3_sample"]} + assert_received {:mix_shell, :info, ["├── elixir"]} + assert_received {:mix_shell, :info, ["└── logger"]} + assert_received {:mix_shell, :info, [" └── elixir"]} + + Application.unload(:app_deps2_sample) + Mix.Tasks.App.Tree.run(["--format", "pretty"]) + assert_received {:mix_shell, :info, ["test"]} + assert_received {:mix_shell, :info, ["├── app_deps_sample"]} + assert_received {:mix_shell, :info, ["│ ├── app_deps2_sample (optional - missing)"]} + assert_received {:mix_shell, :info, ["│ └── app_deps3_sample"]} + assert_received {:mix_shell, :info, ["├── elixir"]} + assert_received {:mix_shell, :info, ["└── logger"]} + assert_received {:mix_shell, :info, [" └── elixir"]} + end) end @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample] diff --git a/lib/mix/test/mix/tasks/compile.erlang_test.exs b/lib/mix/test/mix/tasks/compile.erlang_test.exs index a486528a707..ae0ccbcade8 100644 --- a/lib/mix/test/mix/tasks/compile.erlang_test.exs +++ b/lib/mix/test/mix/tasks/compile.erlang_test.exs @@ -4,11 +4,7 @@ defmodule Mix.Tasks.Compile.ErlangTest do use MixTest.Case import ExUnit.CaptureIO - if System.otp_release() >= "24" do - defmacro position(line, column), do: {line, column} - else - defmacro position(line, _column), do: line - end + defmacro position(line, column), do: {line, column} setup config do erlc_options = Map.get(config, :erlc_options, []) diff --git a/lib/mix/test/mix/tasks/compile.yecc_test.exs b/lib/mix/test/mix/tasks/compile.yecc_test.exs index dfd7c1e0da3..859ff5417a6 100644 --- a/lib/mix/test/mix/tasks/compile.yecc_test.exs +++ b/lib/mix/test/mix/tasks/compile.yecc_test.exs @@ -4,11 +4,7 @@ defmodule Mix.Tasks.Compile.YeccTest do use MixTest.Case import ExUnit.CaptureIO - if System.otp_release() >= "24" do - defmacro position(line, column), do: {line, column} - else - defmacro position(line, _column), do: line - end + defmacro position(line, column), do: {line, column} setup do Mix.Project.push(MixTest.Case.Sample) diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 0563f4bc1bf..d30a4366624 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -3,11 +3,7 @@ Code.require_file("../../test_helper.exs", __DIR__) defmodule Mix.Tasks.CompileTest do use MixTest.Case - if System.otp_release() >= "24" do - defmacro position(line, column), do: {line, column} - else - defmacro position(line, _column), do: line - end + defmacro position(line, column), do: {line, column} defmodule CustomCompilers do def project do From 87ca5d8b1ee4a2a6b1e230e14136425fd65a501d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 16:22:15 +0200 Subject: [PATCH 14/59] Do not discard config on Logger.configure/1, closes #12016 --- lib/logger/lib/logger.ex | 2 +- lib/logger/lib/logger/config.ex | 3 +-- lib/logger/lib/logger/handler.ex | 26 ++++++-------------------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index 9e25a197f95..7395017c2ae 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -628,7 +628,7 @@ defmodule Logger do Logger.Config.configure(options) # Then we can read from the writes - :ok = :logger.set_handler_config(Logger, %{config: %{}}) + :ok = :logger.update_handler_config(Logger, :config, :refresh) end @doc """ diff --git a/lib/logger/lib/logger/config.ex b/lib/logger/lib/logger/config.ex index a61045ca5bd..06fe5222a68 100644 --- a/lib/logger/lib/logger/config.ex +++ b/lib/logger/lib/logger/config.ex @@ -128,7 +128,6 @@ defmodule Logger.Config do defp update_translators(fun) do {:ok, %{config: data}} = :logger.get_handler_config(Logger) translators = fun.(data.translators) - Application.put_env(:logger, :translators, translators) - :ok = :logger.update_handler_config(Logger, :config, translators: translators) + :ok = :logger.update_handler_config(Logger, :config, {:translators, translators}) end end diff --git a/lib/logger/lib/logger/handler.ex b/lib/logger/lib/logger/handler.ex index 0105738ee1e..0d5d3b6ffad 100644 --- a/lib/logger/lib/logger/handler.ex +++ b/lib/logger/lib/logger/handler.ex @@ -27,27 +27,13 @@ defmodule Logger.Handler do {:ok, update_in(config.config, &Map.merge(default_config(), &1))} end - def changing_config( - op, - %{config: %{counter: counter} = old_data} = old_config, - %{config: new_data} = new_config - ) do - old_data = - case op do - :set -> default_config() - :update -> old_data - end - - data = - Enum.reduce(new_data, old_data, fn {k, v}, acc -> - case acc do - %{^k => _} -> %{acc | k => v} - %{} -> acc - end - end) + def changing_config(:update, config, %{config: :refresh}) do + {:ok, update_in(config.config, &Map.merge(&1, default_config()))} + end - config = Map.merge(old_config, new_config) - {:ok, Map.put(config, :config, Map.put(data, :counter, counter))} + def changing_config(:update, config, %{config: {:translators, translators}}) do + Application.put_env(:logger, :translators, translators) + {:ok, put_in(config.config.translators, translators)} end def filter_config(%{config: data} = config) do From 1a5ffa2ec1a562f11132e92cec28579d34513cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Jul 2022 17:18:53 +0200 Subject: [PATCH 15/59] Add deprecations --- CHANGELOG.md | 10 + lib/elixir/lib/calendar/iso.ex | 2 +- lib/elixir/lib/exception.ex | 7 +- lib/elixir/lib/module.ex | 10 +- lib/elixir/lib/regex.ex | 8 +- lib/elixir/lib/task.ex | 4 +- .../pages/compatibility-and-deprecations.md | 4 + lib/elixir/src/elixir.erl | 63 +++--- lib/elixir/test/elixir/calendar/iso_test.exs | 6 +- .../code_formatter/integration_test.exs | 2 +- lib/elixir/test/elixir/code_test.exs | 12 -- lib/ex_unit/test/ex_unit/capture_log_test.exs | 4 +- lib/logger/lib/logger.ex | 15 +- lib/logger/lib/logger/app.ex | 33 +-- lib/logger/lib/logger/handler.ex | 7 +- .../test/logger/backends/console_test.exs | 7 +- lib/logger/test/logger_test.exs | 57 ++--- lib/mix/lib/mix/config.ex | 195 +----------------- lib/mix/test/mix/config_test.exs | 79 ------- 19 files changed, 105 insertions(+), 420 deletions(-) delete mode 100644 lib/mix/test/mix/config_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3713e011998..ee650a9ca4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ ### 4. Hard deprecations +#### Elixir + + * [Calendar] `Calendar.ISO.day_of_week/3` is deprecated in favor of `Calendar.ISO.day_of_week/4` + * [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1` + * [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2` + +#### Logger + + * [Logger] `Logger.warn/2` is deprecated in favor of `Logger.warning/2` + ## v1.14 The CHANGELOG for v1.14 releases can be found [in the v1.14 branch](https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md). diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 98d78bc2a31..89cf9281d60 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -877,8 +877,8 @@ defmodule Calendar.ISO do rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0) end - # TODO: Deprecate me on v1.15 @doc false + @deprecated "Use Calendar.ISO.day_of_week/4 instead" def day_of_week(year, month, day) do day_of_week(year, month, day, :default) |> elem(0) end diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index a82a5743326..9cd82050418 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -49,11 +49,8 @@ defmodule Exception do %{general: message(exception), reason: "#" <> Atom.to_string(struct)} end - @doc """ - Returns `true` if the given `term` is an exception. - """ - # TODO: Deprecate this on Elixir v1.15 - @doc deprecated: "Use Kernel.is_exception/1 instead" + @doc false + @deprecated "Use Kernel.is_exception/1 instead" def exception?(term) def exception?(%_{__exception__: true}), do: true def exception?(_), do: false diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index e69814bd685..7433883e9d8 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -807,7 +807,7 @@ defmodule Module do def create(module, quoted, opts) def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do - create(module, quoted, Map.to_list(env)) + create([line: env.line], module, quoted, env) end def create(module, quoted, opts) when is_atom(module) and is_list(opts) do @@ -815,10 +815,14 @@ defmodule Module do raise ArgumentError, "expected :file to be given as option" end - next = :elixir_module.next_counter(nil) meta = Keyword.take(opts, [:line, :generated]) + create(meta, module, quoted, :elixir.env_for_eval(opts)) + end + + defp create(meta, module, quoted, env_or_opts) do + next = :elixir_module.next_counter(nil) quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted) - :elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts)) + :elixir_module.compile(module, quoted, [], :elixir.env_for_eval(env_or_opts)) end @doc """ diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 16f844f1444..8e17ce193af 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -294,12 +294,8 @@ defmodule Regex do safe_run(regex, string, [{:capture, :none}]) == :match end - @doc """ - Returns `true` if the given `term` is a regex. - Otherwise returns `false`. - """ - # TODO: deprecate permanently on Elixir v1.15 - @doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead" + @doc false + @deprecated "Use Kernel.is_struct/2 or pattern match on %Regex{} instead" def regex?(term) def regex?(%Regex{}), do: true def regex?(_), do: false diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 23a59c04963..f530b0cf9c4 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -1024,7 +1024,7 @@ defmodule Task do result nil -> - Logger.warn("Failed to get a result in #{timeout}ms") + Logger.warning("Failed to get a result in #{timeout}ms") nil end @@ -1036,7 +1036,7 @@ defmodule Task do result nil -> - Logger.warn("Failed to get a result in #{timeout}ms") + Logger.warning("Failed to get a result in #{timeout}ms") nil end diff --git a/lib/elixir/pages/compatibility-and-deprecations.md b/lib/elixir/pages/compatibility-and-deprecations.md index 4b1e91c2f9d..84cf4214c71 100644 --- a/lib/elixir/pages/compatibility-and-deprecations.md +++ b/lib/elixir/pages/compatibility-and-deprecations.md @@ -80,6 +80,10 @@ The first column is the version the feature was hard deprecated. The second colu Version | Deprecated feature | Replaced by (available since) :-------| :-------------------------------------------------- | :--------------------------------------------------------------- +[v1.15] | `Calendar.ISO.day_of_week/3` | `Calendar.ISO.day_of_week/4` (v1.11) +[v1.15] | `Exception.exception?/1` | `Kernel.is_exception/1` (v1.11) +[v1.15] | `Regex.regex?/1` | `Kernel.is_struct/2` (v1.11) +[v1.15] | `Logger.warn/2` | `Logger.warning/2` (v1.11) [v1.14] | `use Bitwise` | `import Bitwise` (v1.0) [v1.14] | `~~~/1` | `bnot/2` (v1.0) [v1.14] | `Application.get_env/3` and similar in module body | `Application.compile_env/3` (v1.10) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 1e9b02a5303..6d13ae5619a 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -188,7 +188,6 @@ env_for_eval(#{lexical_tracker := Pid} = Env) -> false -> NewEnv#{tracers := []} end; -%% TODO: Deprecate all options except line, file, module, and function on v1.15. env_for_eval(Opts) when is_list(Opts) -> Env = elixir_env:new(), @@ -202,56 +201,70 @@ env_for_eval(Opts) when is_list(Opts) -> false -> ?key(Env, file) end, - Aliases = case lists:keyfind(aliases, 1, Opts) of - {aliases, AliasesOpt} when is_list(AliasesOpt) -> AliasesOpt; - false -> ?key(Env, aliases) + Module = case lists:keyfind(module, 1, Opts) of + {module, ModuleOpt} when is_atom(ModuleOpt) -> ModuleOpt; + false -> nil end, - Requires = case lists:keyfind(requires, 1, Opts) of - {requires, RequiresOpt} when is_list(RequiresOpt) -> ordsets:from_list(RequiresOpt); - false -> ?key(Env, requires) + FA = case lists:keyfind(function, 1, Opts) of + {function, {Function, Arity}} when is_atom(Function), is_integer(Arity) -> {Function, Arity}; + {function, nil} -> nil; + false -> nil end, - Functions = case lists:keyfind(functions, 1, Opts) of - {functions, FunctionsOpt} when is_list(FunctionsOpt) -> FunctionsOpt; - false -> ?key(Env, functions) + TempTracers = case lists:keyfind(tracers, 1, Opts) of + {tracers, TracersOpt} when is_list(TracersOpt) -> TracersOpt; + false -> [] end, - Macros = case lists:keyfind(macros, 1, Opts) of - {macros, MacrosOpt} when is_list(MacrosOpt) -> MacrosOpt; - false -> ?key(Env, macros) + Aliases = case lists:keyfind(aliases, 1, Opts) of + {aliases, AliasesOpt} when is_list(AliasesOpt) -> + 'Elixir.IO':warn(<<":aliases option in eval is deprecated">>), + AliasesOpt; + false -> + ?key(Env, aliases) end, - Module = case lists:keyfind(module, 1, Opts) of - {module, ModuleOpt} when is_atom(ModuleOpt) -> ModuleOpt; - false -> nil + Requires = case lists:keyfind(requires, 1, Opts) of + {requires, RequiresOpt} when is_list(RequiresOpt) -> + 'Elixir.IO':warn(<<":requires option in eval is deprecated">>), + ordsets:from_list(RequiresOpt); + false -> + ?key(Env, requires) end, - TempTracers = case lists:keyfind(tracers, 1, Opts) of - {tracers, TracersOpt} when is_list(TracersOpt) -> TracersOpt; - false -> [] + Functions = case lists:keyfind(functions, 1, Opts) of + {functions, FunctionsOpt} when is_list(FunctionsOpt) -> + 'Elixir.IO':warn(<<":functions option in eval is deprecated">>), + FunctionsOpt; + false -> + ?key(Env, functions) + end, + + Macros = case lists:keyfind(macros, 1, Opts) of + {macros, MacrosOpt} when is_list(MacrosOpt) -> + 'Elixir.IO':warn(<<":macros option in eval is deprecated">>), + MacrosOpt; + false -> + ?key(Env, macros) end, %% If there is a dead PID or lexical tracker is nil, %% we assume the tracers also cannot be (re)used. {LexicalTracker, Tracers} = case lists:keyfind(lexical_tracker, 1, Opts) of {lexical_tracker, Pid} when is_pid(Pid) -> + 'Elixir.IO':warn(<<":lexical_tracker option in eval is deprecated">>), case is_process_alive(Pid) of true -> {Pid, TempTracers}; false -> {nil, []} end; {lexical_tracker, nil} -> + 'Elixir.IO':warn(<<":lexical_tracker option in eval is deprecated">>), {nil, []}; false -> {nil, TempTracers} end, - FA = case lists:keyfind(function, 1, Opts) of - {function, {Function, Arity}} when is_atom(Function), is_integer(Arity) -> {Function, Arity}; - {function, nil} -> nil; - false -> nil - end, - Env#{ file := File, module := Module, function := FA, tracers := Tracers, macros := Macros, functions := Functions, lexical_tracker := LexicalTracker, diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 6cbd0a00df8..87baebc7d25 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -62,14 +62,14 @@ defmodule Calendar.ISOTest do end end - describe "day_of_week/3" do + describe "day_of_week/4" do test "raises with invalid dates" do assert_raise ArgumentError, "invalid date: 2018-02-30", fn -> - Calendar.ISO.day_of_week(2018, 2, 30) + Calendar.ISO.day_of_week(2018, 2, 30, :default) end assert_raise ArgumentError, "invalid date: 2017-11-00", fn -> - Calendar.ISO.day_of_week(2017, 11, 0) + Calendar.ISO.day_of_week(2017, 11, 0, :default) end end end diff --git a/lib/elixir/test/elixir/code_formatter/integration_test.exs b/lib/elixir/test/elixir/code_formatter/integration_test.exs index 4e584708e0a..3a2a4b75f60 100644 --- a/lib/elixir/test/elixir/code_formatter/integration_test.exs +++ b/lib/elixir/test/elixir/code_formatter/integration_test.exs @@ -177,7 +177,7 @@ defmodule Code.Formatter.IntegrationTest do str |> Model.Node.model_to_node_type() value, _ -> - Logger.warn("Could not extract node type from value: #{inspect(value)}") + Logger.warning("Could not extract node type from value: #{inspect(value)}") nil end) end diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 66144c1c964..45b10fe091f 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -57,18 +57,6 @@ defmodule CodeTest do {"1", [{:c, 2}, {:b, "1"}, {:a, 1}]} end - test "with many options" do - options = [ - functions: [{Kernel, [is_atom: 1]}], - macros: [{Kernel, [and: 2]}], - aliases: [{K, Kernel}], - requires: [Kernel] - ] - - code = "is_atom(:foo) and K.is_list([])" - assert Code.eval_string(code, [], options) == {true, []} - end - test "keeps caller in stacktrace" do try do Code.eval_string("<>", [a: :a, b: :b], file: "myfile") diff --git a/lib/ex_unit/test/ex_unit/capture_log_test.exs b/lib/ex_unit/test/ex_unit/capture_log_test.exs index 3c72949d72f..c12b36e6e7f 100644 --- a/lib/ex_unit/test/ex_unit/capture_log_test.exs +++ b/lib/ex_unit/test/ex_unit/capture_log_test.exs @@ -34,7 +34,7 @@ defmodule ExUnit.CaptureLogTest do end test "level aware" do - assert capture_log([level: :warn], fn -> + assert capture_log([level: :warning], fn -> Logger.info("here") end) == "" end @@ -61,7 +61,7 @@ defmodule ExUnit.CaptureLogTest do logged = capture_log(fn -> Logger.error("one") end) send(test = self(), {:nested, logged}) - Logger.warn("two") + Logger.warning("two") spawn(fn -> Logger.debug("three") diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index 7395017c2ae..e582dbb4b1d 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -914,7 +914,6 @@ defmodule Logger do emergency: :error, alert: :error, critical: :error, - warning: :warn, notice: :info } @@ -958,19 +957,7 @@ defmodule Logger do end end - @doc """ - Logs a warning message. - - Returns `:ok`. - - This macro is deprecated in favour of `warning/2`. - - ## Examples - - Logger.warn("knob turned too far to the right") - - """ - # TODO: Hard deprecate it in favour of `warning/1-2` macro on v1.15 + @deprecated "Use Logger.warning/2 instead" defmacro warn(message_or_fun, metadata \\ []) do maybe_log(:warning, message_or_fun, metadata, __CALLER__) end diff --git a/lib/logger/lib/logger/app.ex b/lib/logger/lib/logger/app.ex index ae8b504c528..ff798bee36d 100644 --- a/lib/logger/lib/logger/app.ex +++ b/lib/logger/lib/logger/app.ex @@ -84,36 +84,11 @@ defmodule Logger.App do end } - %{level: erl_level} = primary_config = :logger.get_primary_config() - - # Elixir's logger level is no longer set by default. - # - # If it is set, it always has higher precedence, but we warn - # in case of mismatches. - # - # If it is not set, we revert Erlang's kernel to debug, if it - # has its default value, otherwise, we keep it as is. - case Application.fetch_env(:logger, :level) do - {:ok, app_level} -> - level = Logger.Handler.elixir_level_to_erlang_level(app_level) - - if erl_level != :notice and erl_level != level do - IO.warn( - "the level for Erlang's logger was set to #{inspect(erl_level)}, " <> - "but Elixir's logger was set to #{inspect(app_level)}. " <> - "Elixir's logger value will take higher precedence" - ) - end - - :ok = :logger.set_primary_config(:level, level) - - :error when erl_level == :notice -> - :ok = :logger.set_primary_config(:level, :debug) - - :error -> - :ok - end + primary_config = :logger.get_primary_config() + level = Application.get_env(:logger, :level, :debug) + level = Logger.Handler.elixir_level_to_erlang_level(level) + :ok = :logger.set_primary_config(:level, level) :ok = :logger.add_primary_filter(:process_level, {&Logger.Filter.process_level/2, []}) :ok = :logger.add_handler(Logger, Logger.Handler, config) primary_config diff --git a/lib/logger/lib/logger/handler.ex b/lib/logger/lib/logger/handler.ex index 0d5d3b6ffad..02b7f8a201d 100644 --- a/lib/logger/lib/logger/handler.ex +++ b/lib/logger/lib/logger/handler.ex @@ -17,8 +17,11 @@ defmodule Logger.Handler do defp erlang_level_to_elixir_level(:debug), do: :debug defp erlang_level_to_elixir_level(:all), do: :debug - # TODO: Warn on deprecated level on v1.15 - def elixir_level_to_erlang_level(:warn), do: :warning + def elixir_level_to_erlang_level(:warn) do + IO.warn("the log level :warn is deprecated, use :warning instead") + :warning + end + def elixir_level_to_erlang_level(other), do: other ## Config management diff --git a/lib/logger/test/logger/backends/console_test.exs b/lib/logger/test/logger/backends/console_test.exs index 7a9921f0b5d..77caf250b80 100644 --- a/lib/logger/test/logger/backends/console_test.exs +++ b/lib/logger/test/logger/backends/console_test.exs @@ -160,14 +160,9 @@ defmodule Logger.Backends.ConsoleTest do assert capture_log(fn -> Logger.info("hello") end) == IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset() - assert capture_log(fn -> Logger.warn("hello") end) == + assert capture_log(fn -> Logger.warning("hello") end) == IO.ANSI.yellow() <> "hello" <> IO.ANSI.reset() - Logger.configure_backend(:console, colors: [warn: :cyan]) - - assert capture_log(fn -> Logger.warn("hello") end) == - IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset() - Logger.configure_backend(:console, colors: [warning: :magenta]) assert capture_log(fn -> Logger.warning("hello") end) == diff --git a/lib/logger/test/logger_test.exs b/lib/logger/test/logger_test.exs index fba45429489..294f8a1c27c 100644 --- a/lib/logger/test/logger_test.exs +++ b/lib/logger/test/logger_test.exs @@ -124,9 +124,6 @@ defmodule LoggerTest do Logger.configure(level: :notice) assert Logger.level() == :notice - Logger.configure(level: :warn) - assert Logger.level() == :warning - Logger.configure(level: :warning) assert Logger.level() == :warning @@ -410,7 +407,6 @@ defmodule LoggerTest do assert Logger.compare_levels(:debug, :debug) == :eq assert Logger.compare_levels(:debug, :info) == :lt assert Logger.compare_levels(:debug, :notice) == :lt - assert Logger.compare_levels(:debug, :warn) == :lt assert Logger.compare_levels(:debug, :warning) == :lt assert Logger.compare_levels(:debug, :error) == :lt assert Logger.compare_levels(:debug, :critical) == :lt @@ -419,32 +415,30 @@ defmodule LoggerTest do assert Logger.compare_levels(:info, :debug) == :gt assert Logger.compare_levels(:info, :info) == :eq - assert Logger.compare_levels(:info, :warn) == :lt + assert Logger.compare_levels(:info, :warning) == :lt assert Logger.compare_levels(:info, :error) == :lt - assert Logger.compare_levels(:warn, :debug) == :gt - assert Logger.compare_levels(:warn, :info) == :gt - assert Logger.compare_levels(:warn, :warn) == :eq - assert Logger.compare_levels(:warn, :error) == :lt + assert Logger.compare_levels(:warning, :debug) == :gt + assert Logger.compare_levels(:warning, :info) == :gt + assert Logger.compare_levels(:warning, :warning) == :eq + assert Logger.compare_levels(:warning, :error) == :lt assert Logger.compare_levels(:error, :debug) == :gt assert Logger.compare_levels(:error, :info) == :gt - assert Logger.compare_levels(:error, :warn) == :gt + assert Logger.compare_levels(:error, :warning) == :gt assert Logger.compare_levels(:error, :error) == :eq end test "deprecated :warn" do - assert capture_log(fn -> - Logger.warn("hello") == :ok - end) =~ "[warning]" - - assert capture_log(fn -> - Logger.log(:warn, "hello") == :ok - end) =~ "[warning]" + ExUnit.CaptureIO.capture_io(:stderr, fn -> + assert capture_log(fn -> + Logger.log(:warn, "hello") == :ok + end) =~ "[warning]" - assert capture_log(fn -> - Logger.bare_log(:warn, "hello") == :ok - end) =~ "[warning]" + assert capture_log(fn -> + Logger.bare_log(:warn, "hello") == :ok + end) =~ "[warning]" + end) end describe "levels" do @@ -490,20 +484,6 @@ defmodule LoggerTest do end) == "" end - test "warn/2" do - assert capture_log(fn -> - assert Logger.warn("hello", []) == :ok - end) =~ msg_with_meta("[warning] hello") - - assert capture_log(:error, fn -> - assert Logger.warn("hello", []) == :ok - end) == "" - - assert capture_log(:error, fn -> - assert Logger.warn(raise("not invoked"), []) == :ok - end) == "" - end - test "error/2" do assert capture_log(fn -> assert Logger.error("hello", []) == :ok @@ -568,7 +548,7 @@ defmodule LoggerTest do [module: LoggerTest.PurgeMatching, function: "two_filters/0"], [function: "one_filter/0"], [custom: true], - [function: "level_filter/0", level_lower_than: :warn], + [function: "level_filter/0", level_lower_than: :warning], [application: :sample_app, level_lower_than: :info] ] ) @@ -588,7 +568,6 @@ defmodule LoggerTest do def level_filter do Logger.info("info_filter") - Logger.warn("warn_filter") Logger.warning("warning_filter") end @@ -605,7 +584,7 @@ defmodule LoggerTest do assert capture_log(fn -> assert PurgeMatching.one_filter() == :ok end) == "" assert capture_log(fn -> assert PurgeMatching.two_filters() == :ok end) == "" assert capture_log(fn -> assert PurgeMatching.custom_filters() == :ok end) == "" - assert capture_log(fn -> assert PurgeMatching.level_filter() == :ok end) =~ "warn_filter" + assert capture_log(fn -> assert PurgeMatching.level_filter() == :ok end) =~ "warning_filter" refute capture_log(fn -> assert PurgeMatching.level_filter() == :ok end) =~ "info_filter" capture_log(fn -> assert PurgeMatching.log(:info) == :ok end) @@ -695,10 +674,10 @@ defmodule LoggerTest do Application.start(:logger) end - test "starts the application with warn level" do + test "starts the application with warning level" do Logger.App.stop() assert %{level: :notice} = :logger.get_primary_config() - Application.put_env(:logger, :level, :warn) + Application.put_env(:logger, :level, :warning) Application.start(:logger) assert %{level: :warning} = :logger.get_primary_config() after diff --git a/lib/mix/lib/mix/config.ex b/lib/mix/lib/mix/config.ex index ed894e2da9b..d0a7a8690ac 100644 --- a/lib/mix/lib/mix/config.ex +++ b/lib/mix/lib/mix/config.ex @@ -1,58 +1,6 @@ defmodule Mix.Config do - # TODO: Convert them to hard deprecations on v1.15 + @moduledoc false - @moduledoc deprecated: "Use Config and Config.Reader instead" - @moduledoc ~S""" - A simple configuration API and functions for managing config files. - - This module is deprecated, use the modules `Config` and `Config.Reader` - from Elixir's standard library instead. - - ## Setting configuration - - Most commonly, this module is used to define your own configuration: - - use Mix.Config - - config :root_key, - key1: "value1", - key2: "value2" - - import_config "#{Mix.env()}.exs" - - `use Mix.Config` will import the functions `config/2`, `config/3` - and `import_config/1` to help you manage your configuration. - - ## Evaluating configuration - - Once a configuration is written to a file, the functions in this - module can be used to read and merge said configuration. The `eval!/2` - function allows you to evaluate a given configuration file and the `merge/2` - function allows you to deep merge the results of multiple configurations. Those - functions should not be invoked by users writing configurations but - rather by library authors. - - ## Examples - - The most common use of `Mix.Config` is to define application - configuration so that `Application.get_env/3` and other `Application` - functions can be used to retrieve or further change them. - - Application config files are typically placed in the `config/` - directory of your Mix projects. For example, the following config - - # config/config.exs - config :my_app, :key, "value" - - will be automatically loaded by Mix and persisted into the - `:my_app`'s application environment, which can be accessed in - its source code as follows: - - "value" = Application.fetch_env!(:my_app, :key1) - - """ - - @doc false @deprecated "Use the Config module instead" defmacro __using__(_) do quote do @@ -60,87 +8,13 @@ defmodule Mix.Config do end end - @doc """ - Configures the given `root_key`. - - Keyword lists are always deep merged. - - ## Examples - - The given `opts` are merged into the existing configuration - for the given `root_key`. Conflicting keys are overridden by the - ones specified in `opts`. For example, the application - configuration below - - config :logger, - level: :warn, - backends: [:console] - - config :logger, - level: :info, - truncate: 1024 - - will have a final configuration for `:logger` of: - - [level: :info, backends: [:console], truncate: 1024] - - """ - @doc deprecated: "Use the Config module instead" + @deprecated "Use the Config module instead" defdelegate config(root_key, opts), to: Config - @doc """ - Configures the given `key` for the given `root_key`. - - Keyword lists are always deep merged. - - ## Examples - - The given `opts` are merged into the existing values for `key` - in the given `root_key`. Conflicting keys are overridden by the - ones specified in `opts`. For example, the application - configuration below - - config :ecto, Repo, - log_level: :warn, - adapter: Ecto.Adapters.Postgres - - config :ecto, Repo, - log_level: :info, - pool_size: 10 - - will have a final value of the configuration for the `Repo` - key in the `:ecto` application of: - - [log_level: :info, pool_size: 10, adapter: Ecto.Adapters.Postgres] - - """ - @doc deprecated: "Use the Config module instead" + @deprecated "Use the Config module instead" defdelegate config(root_key, key, opts), to: Config - @doc ~S""" - Imports configuration from the given file or files. - - If `path_or_wildcard` is a wildcard, then all the files - matching that wildcard will be imported; if no file matches - the wildcard, no errors are raised. If `path_or_wildcard` is - not a wildcard but a path to a single file, then that file is - imported; in case the file doesn't exist, an error is raised. - - If path/wildcard is a relative path/wildcard, it will be expanded - relatively to the directory the current configuration file is in. - - ## Examples - - This is often used to emulate configuration across environments: - - import_config "#{Mix.env()}.exs" - - Or to import files from children in umbrella projects: - - import_config "../apps/*/config/config.exs" - - """ - @doc deprecated: "Use the Config module instead" + @deprecated "Use the Config module instead" defmacro import_config(path_or_wildcard) do quote do Mix.Config.__import__!(unquote(path_or_wildcard), __DIR__) @@ -167,14 +41,6 @@ defmodule Mix.Config do ## Mix API - @doc """ - Evaluates the given configuration file. - - It accepts a list of `imported_paths` that should raise if attempted - to be imported again (to avoid recursive imports). - - It returns a tuple with the configuration and the imported paths. - """ @deprecated "Use Config.Reader.read_imports!/2 instead" def eval!(file, imported_paths \\ []) do Config.Reader.read_imports!(file, @@ -184,73 +50,21 @@ defmodule Mix.Config do ) end - @doc """ - Reads the configuration file. - - The same as `eval!/2` but only returns the configuration - in the given file, without returning the imported paths. - - It exists for convenience purposes. For example, you could - invoke it inside your `mix.exs` to read some external data - you decided to move to a configuration file: - - subsystem: Mix.Config.read!("rel/subsystem.exs") - - """ @deprecated "Use Config.Reader.read!/2 instead" - @spec read!(Path.t(), [Path.t()]) :: keyword def read!(file, imported_paths \\ []) do Config.Reader.read!(file, imports: imported_paths, env: Mix.env(), target: Mix.target()) end - @doc """ - Merges two configurations. - - The configurations are merged together with the values in - the second one having higher preference than the first in - case of conflicts. In case both values are set to keyword - lists, it deep merges them. - - ## Examples - - iex> Mix.Config.merge([app: [k: :v1]], [app: [k: :v2]]) - [app: [k: :v2]] - - iex> Mix.Config.merge([app: [k: [v1: 1, v2: 2]]], [app: [k: [v2: :a, v3: :b]]]) - [app: [k: [v1: 1, v2: :a, v3: :b]]] - - iex> Mix.Config.merge([app1: []], [app2: []]) - [app1: [], app2: []] - - """ @deprecated "Use Config.Reader.merge/2 instead" def merge(config1, config2) do Config.__merge__(config1, config2) end - @doc """ - Persists the given configuration by modifying - the configured applications environment. - - `config` should be a list of `{app, app_config}` tuples or a - `%{app => app_config}` map where `app` are the applications to - be configured and `app_config` are the configuration (as key-value - pairs) for each of those applications. - - Returns the configured applications. - - ## Examples - - Mix.Config.persist(logger: [level: :error], my_app: [my_config: 1]) - #=> [:logger, :my_app] - - """ @deprecated "Use Application.put_all_env/2 instead" def persist(config) do Application.put_all_env(config, persistent: true) end - @doc false @deprecated "Use the Config.Reader module instead" def read_wildcard!(path, loaded_paths \\ []) do paths = @@ -263,7 +77,6 @@ defmodule Mix.Config do Enum.reduce(paths, [], &merge(&2, read!(&1, loaded_paths))) end - @doc false @deprecated "Manually validate the data instead" def validate!(config) do validate!(config, "runtime") diff --git a/lib/mix/test/mix/config_test.exs b/lib/mix/test/mix/config_test.exs deleted file mode 100644 index 27d05393912..00000000000 --- a/lib/mix/test/mix/config_test.exs +++ /dev/null @@ -1,79 +0,0 @@ -Code.require_file("../test_helper.exs", __DIR__) - -defmodule Mix.ConfigTest do - use MixTest.Case, async: true - - import Mix.Config - - setup do - Process.put({Config, :config}, []) - Process.put({Config, :imports}, []) - :ok - end - - defp config do - Process.get({Config, :config}) - end - - defp files do - Process.get({Config, :imports}) - end - - test "config/2" do - assert config() == [] - - config :lager, key: :value - assert config() == [lager: [key: :value]] - - config :lager, other: :value - assert config() == [lager: [key: :value, other: :value]] - - config :lager, key: :other - assert config() == [lager: [other: :value, key: :other]] - - # Works inside functions too... - f = fn -> config(:lager, key: :fn) end - f.() - assert config() == [lager: [other: :value, key: :fn]] - - # ...and in for comprehensions. - for _ <- 0..0, do: config(:lager, key: :for) - assert config() == [lager: [other: :value, key: :for]] - end - - test "config/3" do - config :app, Repo, key: :value - assert config() == [app: [{Repo, key: :value}]] - - config :app, Repo, other: :value - assert config() == [app: [{Repo, key: :value, other: :value}]] - - config :app, Repo, key: :other - assert config() == [app: [{Repo, other: :value, key: :other}]] - - config :app, Repo, key: [nested: false] - assert config() == [app: [{Repo, other: :value, key: [nested: false]}]] - - config :app, Repo, key: [nested: true] - assert config() == [app: [{Repo, other: :value, key: [nested: true]}]] - - config :app, Repo, key: :other - assert config() == [app: [{Repo, other: :value, key: :other}]] - end - - test "import_config/1" do - import_config fixture_path("config.exs") - assert config() == [my_app: [key: :value]] - assert files() == [fixture_path("config.exs")] - end - - test "import_config/1 with wildcards" do - import_config fixture_path("confi*.exs") - assert config() == [my_app: [key: :value]] - end - - test "import_config/1 with wildcard with no matches" do - import_config fixture_path("configs/nonexistent_*.exs") - assert config() == [] - end -end From 553f1ee79644b4f25211d8bdb87c56596353417b Mon Sep 17 00:00:00 2001 From: Eksperimental Date: Wed, 27 Jul 2022 20:11:38 +0000 Subject: [PATCH 16/59] Improve spec for System.get_env/2 (#12019) --- lib/elixir/lib/system.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index 9b923fa2236..d137250bd39 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -631,7 +631,8 @@ defmodule System do """ @doc since: "1.9.0" - @spec get_env(String.t(), String.t() | nil) :: String.t() | nil + @spec get_env(String.t(), String.t()) :: String.t() + @spec get_env(String.t(), nil) :: String.t() | nil def get_env(varname, default \\ nil) when is_binary(varname) and (is_binary(default) or is_nil(default)) do From 3bdb5e70623b9b4a276c54496aaf90ff06efd6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Thu, 28 Jul 2022 08:35:13 +0200 Subject: [PATCH 17/59] Keep documented return values after changing Mix.State to use :ets (#12017) --- lib/mix/lib/mix/state.ex | 2 ++ lib/mix/lib/mix/tasks_server.ex | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/mix/lib/mix/state.ex b/lib/mix/lib/mix/state.ex index ed64b1379dd..7252512b281 100644 --- a/lib/mix/lib/mix/state.ex +++ b/lib/mix/lib/mix/state.ex @@ -36,10 +36,12 @@ defmodule Mix.State do def put(key, value) do :ets.insert(@name, {key, value}) + :ok end def update(key, fun) do :ets.insert(@name, {key, fun.(:ets.lookup_element(@name, key, 2))}) + :ok end ## Persistent term cache (persistent, cleared in tests) diff --git a/lib/mix/lib/mix/tasks_server.ex b/lib/mix/lib/mix/tasks_server.ex index cc4cadd6e85..a8825d7dc7d 100644 --- a/lib/mix/lib/mix/tasks_server.ex +++ b/lib/mix/lib/mix/tasks_server.ex @@ -17,6 +17,7 @@ defmodule Mix.TasksServer do def put(tuple) do :ets.insert(@name, {tuple}) + :ok end def get(tuple) do @@ -29,5 +30,6 @@ defmodule Mix.TasksServer do def clear() do :ets.delete_all_objects(@name) + :ok end end From fd35c0925747d31085fbc3ee46d7964058896af0 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 28 Jul 2022 09:30:04 +0200 Subject: [PATCH 18/59] Add nonempty_binary/0 and nonempty_bitstring/0 (#12022) --- lib/elixir/lib/string.ex | 4 +--- lib/elixir/pages/typespecs.md | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index f91f6bd0d77..fa142c5a235 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -285,11 +285,9 @@ defmodule String do * a compiled search pattern created by `:binary.compile_pattern/1` """ - # TODO: Replace "nonempty_binary :: <<_::8, _::_*8>>" with "nonempty_binary()" - # when minimum requirement is >= OTP 24. @type pattern :: t() - | [nonempty_binary :: <<_::8, _::_*8>>] + | [nonempty_binary] | (compiled_search_pattern :: :binary.cp()) @conditional_mappings [:greek, :turkic] diff --git a/lib/elixir/pages/typespecs.md b/lib/elixir/pages/typespecs.md index b645c3b7878..49f601d9041 100644 --- a/lib/elixir/pages/typespecs.md +++ b/lib/elixir/pages/typespecs.md @@ -124,7 +124,9 @@ Built-in type | Defined as `arity()` | `0..255` `as_boolean(t)` | `t` `binary()` | `<<_::_*8>>` +`nonempty_binary()` | `<<_::8, _::_*8>>` `bitstring()` | `<<_::_*1>>` +`nonempty_bitstring()` | `<<_::1, _::_*1>>` `boolean()` | `true` \| `false` `byte()` | `0..255` `char()` | `0..0x10FFFF` From 877210178fa30b0ede221c258884144a0b03f551 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 28 Jul 2022 09:30:24 +0200 Subject: [PATCH 19/59] Use :sets v2 under the hood in MapSet (#12018) We can do this now that Elixir requires OTP 24+, since the new :sets API is also built on top of maps. --- lib/elixir/lib/map_set.ex | 219 ++++++++---------------- lib/elixir/test/elixir/map_set_test.exs | 6 + 2 files changed, 73 insertions(+), 152 deletions(-) diff --git a/lib/elixir/lib/map_set.ex b/lib/elixir/lib/map_set.ex index c7783868293..a182f7c2a6a 100644 --- a/lib/elixir/lib/map_set.ex +++ b/lib/elixir/lib/map_set.ex @@ -43,23 +43,25 @@ defmodule MapSet do `MapSet`s can also be constructed starting from other collection-type data structures: for example, see `MapSet.new/1` or `Enum.into/2`. - `MapSet` is built on top of `Map`, this means that they share many properties, - including logarithmic time complexity. See the documentation for `Map` for more - information on its execution time complexity. + `MapSet` is built on top of Erlang's + [`:sets`](https://www.erlang.org/doc/man/sets.html) (version 2). This means + that they share many properties, including logarithmic time complexity. Erlang + `:sets` (version 2) are implemented on top of maps, so see the documentation + for `Map` for more information on its execution time complexity. """ - # MapSets have an underlying Map. MapSet elements are keys of said map, - # and this empty list is their associated dummy value. - @dummy_value [] - @type value :: term - @opaque internal(value) :: %{optional(value) => []} + @opaque internal(value) :: :sets.set(value) @type t(value) :: %__MODULE__{map: internal(value)} @type t :: t(term) - # TODO: Implement the functions in this module using Erlang/OTP 24 new sets - defstruct map: %{} + # The key name is :map because the MapSet implementation used to be based on top of maps before + # Elixir 1.15 (and Erlang/OTP 24, which introduced :sets version 2). :sets v2's internal + # representation is, anyways, exactly the same as MapSet's previous implementation. We cannot + # change the :map key name here because we'd break backwards compatibility with code compiled + # with Elixir 1.14 and earlier and executed on Elixir 1.15+. + defstruct map: :sets.new(version: 2) @doc """ Returns a new set. @@ -90,8 +92,12 @@ defmodule MapSet do def new(%__MODULE__{} = map_set), do: map_set def new(enumerable) do - keys = Enum.to_list(enumerable) - %MapSet{map: :maps.from_keys(keys, @dummy_value)} + set = + enumerable + |> Enum.to_list() + |> :sets.from_list(version: 2) + + %MapSet{map: set} end @doc """ @@ -105,8 +111,12 @@ defmodule MapSet do """ @spec new(Enumerable.t(), (term -> val)) :: t(val) when val: value def new(enumerable, transform) when is_function(transform, 1) do - keys = Enum.map(enumerable, transform) - %MapSet{map: :maps.from_keys(keys, @dummy_value)} + set = + enumerable + |> Enum.map(transform) + |> :sets.from_list(version: 2) + + %MapSet{map: set} end @doc """ @@ -124,8 +134,8 @@ defmodule MapSet do """ @spec delete(t(val1), val2) :: t(val1) when val1: value, val2: value - def delete(%MapSet{map: map} = map_set, value) do - %{map_set | map: Map.delete(map, value)} + def delete(%MapSet{map: set} = map_set, value) do + %{map_set | map: :sets.del_element(value, set)} end @doc """ @@ -138,36 +148,8 @@ defmodule MapSet do """ @spec difference(t(val1), t(val2)) :: t(val1) when val1: value, val2: value - def difference(map_set1, map_set2) - - # If the first set is less than twice the size of the second map, it is fastest - # to re-accumulate elements in the first set that are not present in the second set. - def difference(%MapSet{map: map1}, %MapSet{map: map2}) - when map_size(map1) < map_size(map2) * 2 do - map = - map1 - |> :maps.iterator() - |> :maps.next() - |> filter_not_in(map2, []) - - %MapSet{map: map} - end - - # If the second set is less than half the size of the first set, it's fastest - # to simply iterate through each element in the second set, deleting them from - # the first set. - def difference(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do - %{map_set | map: Map.drop(map1, Map.keys(map2))} - end - - defp filter_not_in(:none, _map2, acc), do: Map.from_keys(acc, @dummy_value) - - defp filter_not_in({key, _val, iter}, map2, acc) do - if is_map_key(map2, key) do - filter_not_in(:maps.next(iter), map2, acc) - else - filter_not_in(:maps.next(iter), map2, [key | acc]) - end + def difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.subtract(set1, set2)} end @doc """ @@ -177,35 +159,23 @@ defmodule MapSet do iex> MapSet.symmetric_difference(MapSet.new([1, 2, 3]), MapSet.new([2, 3, 4])) MapSet.new([1, 4]) + """ @doc since: "1.14.0" @spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value - def symmetric_difference(%MapSet{map: map1}, %MapSet{map: map2}) do - {small, large} = order_by_size(map1, map2) - - map = - large - |> :maps.iterator() - |> :maps.next() - |> disjointer(small, []) - - %MapSet{map: map} - end - - defp disjointer(:none, small, list) do - list |> Map.from_keys(@dummy_value) |> Map.merge(small) - end - - defp disjointer({key, _val, iter}, small, list) do - if is_map_key(small, key) do - iter - |> :maps.next() - |> disjointer(Map.delete(small, key), list) - else - iter - |> :maps.next() - |> disjointer(small, [key | list]) + def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + {small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1} + + disjointer_fun = fn elem, {small, acc} -> + if :sets.is_element(elem, small) do + {:sets.del_element(elem, small), acc} + else + {small, [elem | acc]} + end end + + {new_small, list} = :sets.fold(disjointer_fun, {small, []}, large) + %{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))} end @doc """ @@ -220,19 +190,8 @@ defmodule MapSet do """ @spec disjoint?(t, t) :: boolean - def disjoint?(%MapSet{map: map1}, %MapSet{map: map2}) do - {map1, map2} = order_by_size(map1, map2) - - map1 - |> :maps.iterator() - |> :maps.next() - |> none_in?(map2) - end - - defp none_in?(:none, _), do: true - - defp none_in?({key, _val, iter}, map2) do - not is_map_key(map2, key) and none_in?(:maps.next(iter), map2) + def disjoint?(%MapSet{map: set1}, %MapSet{map: set2}) do + :sets.is_disjoint(set1, set2) end @doc """ @@ -253,8 +212,8 @@ defmodule MapSet do """ @spec equal?(t, t) :: boolean - def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do - map1 === map2 + def equal?(%MapSet{map: set1}, %MapSet{map: set2}) do + set1 === set2 end @doc """ @@ -270,9 +229,8 @@ defmodule MapSet do """ @spec intersection(t(val), t(val)) :: t(val) when val: value - def intersection(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do - {map1, map2} = order_by_size(map1, map2) - %{map_set | map: Map.take(map2, Map.keys(map1))} + def intersection(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.intersection(set1, set2)} end @doc """ @@ -287,8 +245,8 @@ defmodule MapSet do """ @spec member?(t, value) :: boolean - def member?(%MapSet{map: map}, value) do - is_map_key(map, value) + def member?(%MapSet{map: set}, value) do + :sets.is_element(value, set) end @doc """ @@ -303,8 +261,8 @@ defmodule MapSet do """ @spec put(t(val), new_val) :: t(val | new_val) when val: value, new_val: value - def put(%MapSet{map: map} = map_set, value) do - %{map_set | map: Map.put(map, value, @dummy_value)} + def put(%MapSet{map: set} = map_set, value) do + %{map_set | map: :sets.add_element(value, set)} end @doc """ @@ -317,8 +275,8 @@ defmodule MapSet do """ @spec size(t) :: non_neg_integer - def size(%MapSet{map: map}) do - map_size(map) + def size(%MapSet{map: set}) do + :sets.size(set) end @doc """ @@ -335,21 +293,8 @@ defmodule MapSet do """ @spec subset?(t, t) :: boolean - def subset?(%MapSet{map: map1}, %MapSet{map: map2}) do - map_size(map1) <= map_size(map2) and all_in?(map1, map2) - end - - defp all_in?(:none, _), do: true - - defp all_in?({key, _val, iter}, map2) do - :erlang.is_map_key(key, map2) and all_in?(:maps.next(iter), map2) - end - - defp all_in?(map1, map2) when is_map(map1) and is_map(map2) do - map1 - |> :maps.iterator() - |> :maps.next() - |> all_in?(map2) + def subset?(%MapSet{map: set1}, %MapSet{map: set2}) do + :sets.is_subset(set1, set2) end @doc """ @@ -362,8 +307,8 @@ defmodule MapSet do """ @spec to_list(t(val)) :: [val] when val: value - def to_list(%MapSet{map: map}) do - Map.keys(map) + def to_list(%MapSet{map: set}) do + :sets.to_list(set) end @doc """ @@ -376,16 +321,10 @@ defmodule MapSet do """ @spec union(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value - def union(map_set1, map_set2) - - def union(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do - %{map_set | map: Map.merge(map1, map2)} + def union(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.union(set1, set2)} end - @compile {:inline, [order_by_size: 2]} - defp order_by_size(map1, map2) when map_size(map1) > map_size(map2), do: {map2, map1} - defp order_by_size(map1, map2), do: {map1, map2} - @doc """ Filters the set by returning only the elements from `set` for which invoking `fun` returns a truthy value. @@ -409,21 +348,9 @@ defmodule MapSet do """ @doc since: "1.14.0" @spec filter(t(a), (a -> as_boolean(term))) :: t(a) when a: value - def filter(%MapSet{map: map}, fun) when is_map(map) and is_function(fun) do - iter = :maps.iterator(map) - next = :maps.next(iter) - keys = filter_keys(next, fun) - %MapSet{map: Map.from_keys(keys, @dummy_value)} - end - - defp filter_keys(:none, _fun), do: [] - - defp filter_keys({key, _value, iter}, fun) do - if fun.(key) do - [key | filter_keys(:maps.next(iter), fun)] - else - filter_keys(:maps.next(iter), fun) - end + def filter(%MapSet{map: set} = map_set, fun) when is_function(fun) do + pred = fn element -> !!fun.(element) end + %{map_set | map: :sets.filter(pred, set)} end @doc """ @@ -443,21 +370,9 @@ defmodule MapSet do """ @doc since: "1.14.0" @spec reject(t(a), (a -> as_boolean(term))) :: t(a) when a: value - def reject(%MapSet{map: map}, fun) when is_map(map) and is_function(fun) do - iter = :maps.iterator(map) - next = :maps.next(iter) - keys = reject_keys(next, fun) - %MapSet{map: Map.from_keys(keys, @dummy_value)} - end - - defp reject_keys(:none, _fun), do: [] - - defp reject_keys({key, _value, iter}, fun) do - if fun.(key) do - reject_keys(:maps.next(iter), fun) - else - [key | reject_keys(:maps.next(iter), fun)] - end + def reject(%MapSet{map: set} = map_set, fun) when is_function(fun) do + pred = fn element -> !fun.(element) end + %{map_set | map: :sets.filter(pred, set)} end defimpl Enumerable do @@ -480,10 +395,10 @@ defmodule MapSet do end defimpl Collectable do - def into(map_set) do + def into(%@for{map: set} = map_set) do fun = fn list, {:cont, x} -> [x | list] - list, :done -> %{map_set | map: Map.merge(map_set.map, Map.from_keys(list, []))} + list, :done -> %{map_set | map: :sets.union(set, :sets.from_list(list, version: 2))} _, :halt -> :ok end diff --git a/lib/elixir/test/elixir/map_set_test.exs b/lib/elixir/test/elixir/map_set_test.exs index 21a5b51aea9..5bd86365a67 100644 --- a/lib/elixir/test/elixir/map_set_test.exs +++ b/lib/elixir/test/elixir/map_set_test.exs @@ -68,6 +68,12 @@ defmodule MapSetTest do result = MapSet.symmetric_difference(MapSet.new(1..5), MapSet.new(1..5)) assert MapSet.equal?(result, MapSet.new()) + + result = MapSet.symmetric_difference(MapSet.new([1, 2, 3]), MapSet.new()) + assert MapSet.equal?(result, MapSet.new([1, 2, 3])) + + result = MapSet.symmetric_difference(MapSet.new(), MapSet.new([1, 2, 3])) + assert MapSet.equal?(result, MapSet.new([1, 2, 3])) end test "disjoint?/2" do From 729882b8f66907ebdc913e1433f9b937d344d762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 09:40:20 +0200 Subject: [PATCH 20/59] Support --sname/--name undefined and use it by default in releases --- lib/elixir/lib/kernel/cli.ex | 22 ++++++++++++++++------ lib/iex/lib/iex/cli.ex | 5 ++++- lib/mix/lib/mix/tasks/release.init.ex | 6 +++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index d3946e5d8f3..a3985cd06aa 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -299,7 +299,7 @@ defmodule Kernel.CLI do defp append_hostname(node) do case :string.find(node, "@") do - :nomatch -> node <> :string.find(Atom.to_string(node()), "@") + :nomatch -> node <> :string.find(Atom.to_string(:net_kernel.nodename()), "@") _ -> node end end @@ -447,11 +447,21 @@ defmodule Kernel.CLI do end defp process_command({:rpc_eval, node, expr}, _config) when is_binary(expr) do - case :rpc.call(String.to_atom(node), __MODULE__, :rpc_eval, [expr]) do - :ok -> :ok - {:badrpc, {:EXIT, exit}} -> Process.exit(self(), exit) - {:badrpc, reason} -> {:error, "--rpc-eval : RPC failed with reason #{inspect(reason)}"} - {kind, error, stack} -> :erlang.raise(kind, error, stack) + if Node.alive?() do + node = String.to_atom(node) + + # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. + _ = :net_kernel.connect_node(node) + + case :rpc.call(node, __MODULE__, :rpc_eval, [expr]) do + :ok -> :ok + {:badrpc, {:EXIT, exit}} -> Process.exit(self(), exit) + {:badrpc, reason} -> {:error, "--rpc-eval : RPC failed with reason #{inspect(reason)}"} + {kind, error, stack} -> :erlang.raise(kind, error, stack) + end + else + {:error, + "--rpc-eval : Cannot run --rpc-eval if the node is not alive (set --name or --sname)"} end end diff --git a/lib/iex/lib/iex/cli.ex b/lib/iex/lib/iex/cli.ex index 3190656627c..6a9577fab89 100644 --- a/lib/iex/lib/iex/cli.ex +++ b/lib/iex/lib/iex/cli.ex @@ -89,6 +89,9 @@ defmodule IEx.CLI do defp tty_args do if remote = get_remsh(:init.get_plain_arguments()) do if Node.alive?() do + # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. + _ = :net_kernel.connect_node(remote) + case :rpc.call(remote, :code, :ensure_loaded, [IEx]) do {:badrpc, reason} -> message = @@ -166,7 +169,7 @@ defmodule IEx.CLI do defp append_hostname(node) do case :string.find(node, '@') do - :nomatch -> node ++ :string.find(Atom.to_charlist(node()), '@') + :nomatch -> node ++ :string.find(Atom.to_charlist(:net_kernel.nodename()), '@') _ -> node end end diff --git a/lib/mix/lib/mix/tasks/release.init.ex b/lib/mix/lib/mix/tasks/release.init.ex index f324fba7415..45e54d977e4 100644 --- a/lib/mix/lib/mix/tasks/release.init.ex +++ b/lib/mix/lib/mix/tasks/release.init.ex @@ -141,7 +141,7 @@ defmodule Mix.Tasks.Release.Init do rpc () { exec "$REL_VSN_DIR/elixir" \ --hidden --cookie "$RELEASE_COOKIE" \ - $(release_distribution "rpc-$(rand)-$RELEASE_NODE") \ + $(release_distribution "undefined") \ --boot "$REL_VSN_DIR/$RELEASE_BOOT_SCRIPT_CLEAN" \ --boot-var RELEASE_LIB "$RELEASE_ROOT/lib" \ --vm-args "$RELEASE_REMOTE_VM_ARGS" \ @@ -398,7 +398,7 @@ defmodule Mix.Tasks.Release.Init do if "!RELEASE_DISTRIBUTION!" == "none" ( set RELEASE_DISTRIBUTION_FLAG= ) else ( - set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!" + set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "undefined" ) "!REL_VSN_DIR!\iex.bat" ^ @@ -414,7 +414,7 @@ defmodule Mix.Tasks.Release.Init do if "!RELEASE_DISTRIBUTION!" == "none" ( set RELEASE_DISTRIBUTION_FLAG= ) else ( - set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rpc-!RANDOM!-!RELEASE_NODE!" + set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "undefined" ) "!REL_VSN_DIR!\elixir.bat" ^ From b9d967be63b925c95272d22d26999db14146a64b Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 28 Jul 2022 09:53:37 +0200 Subject: [PATCH 21/59] Add Process.alias/0,1 and Process.unalias/1 (#12020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can have these now that Elixir requires Erlang/OTP 24+. Co-authored-by: José Valim --- lib/elixir/lib/process.ex | 155 ++++++++++++++++++++++++ lib/elixir/src/elixir_rewrite.erl | 4 + lib/elixir/test/elixir/process_test.exs | 57 ++++++++- 3 files changed, 214 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index 0ca8c8348ea..32f9ffe0f8f 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -17,6 +17,59 @@ defmodule Process do `Registry`, `Supervisor` and `Task` for building their systems and resort to this module for gathering information, trapping exits, links and monitoring. + + ## Aliases + + Aliases are a feature introduced in Erlang/OTP 24. An alias is a way + to refer to a PID in order to send messages to it. The advantage of using + aliases is that they can be deactivated even if the aliased process is still + running. If you send a message to a deactivated alias, nothing will happen. + This makes request/response scenarios easier to implement. + + You can use `alias/0` or `alias/1` to set an alias, and then you can send + messages to that alias like you do with PIDs using `send/2`. To deactivate + an alias, you can use `unalias/1`. If you send a message to a deactivated alias, + nothing will happen. + + For example, you could have a process that listens for `:ping` messages: + + def server do + receive do + {:ping, source_alias} -> + send(source_alias, :pong) + server() + end + end + + Now, another process might ping this process: + + server = spawn(&server/0) + + source_alias = Process.alias() + send(server, {:ping, source_alias}) + + receive do + :pong -> :pong + end + #=> :pong + + If now you deactivate the `source_alias` and ping the server again, you + won't receive any response since the server will `send/2` the `:pong` response + to a deactivated alias. + + Process.unalias(source_alias) + send(server, {:ping, source_alias}) + + receive do + :pong -> :pong + after + 1000 -> :timeout + end + #=> :timeout + + See also the [Process Aliases + section](https://www.erlang.org/doc/reference_manual/processes.html#process-aliases) + of the *Erlang reference manual*. """ @typedoc """ @@ -475,6 +528,43 @@ defmodule Process do :erlang.monitor(:process, item) end + @doc """ + Starts monitoring the given `item` from the calling process. + + This function is similar to `monitor/1`, but accepts options to customize how + `item` is monitored. See `:erlang.monitor/3` for documentation on those + options. + + Inlined by the compiler. + + ## Examples + + pid = + spawn(fn -> + receive do + {:ping, source_alias} -> send(source_alias, :pong) + end + end) + #=> #PID<0.118.0> + + ref_and_alias = Process.monitor(pid, alias: :reply_demonitor) + #=> #Reference<0.906660723.3006791681.40191> + + send(pid, {:ping, ref_and_alias}) + + receive do: msg -> msg + #=> :pong + + receive do: msg -> msg + #=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc} + + """ + @doc since: "1.15.0" + @spec monitor(pid | {name, node} | name, :erlang.monitor_option()) :: reference when name: atom + def monitor(item, options) do + :erlang.monitor(:process, item, options) + end + @doc """ Demonitors the monitor identified by the given `reference`. @@ -784,6 +874,71 @@ defmodule Process do @spec hibernate(module, atom, list) :: no_return defdelegate hibernate(mod, fun_name, args), to: :erlang + @type alias_opt :: :explicit_unalias | :reply + + @typedoc """ + An alias returned by `alias/0` or `alias/1`. + + See [the module documentation](#module-aliases) for more information about aliases. + """ + @type alias :: reference + + @doc """ + Creates a process alias. + + This is the same as calling `alias/1` as `alias([:explicit_unalias])`. See + also `:erlang.alias/0`. + + Inlined by the compiler. + + ## Examples + + alias = Process.alias() + + """ + @doc since: "1.15.0" + @spec alias() :: alias + defdelegate alias(), to: :erlang + + @doc """ + Creates a process alias. + + See [the module documentation](#module-aliases) for more information about aliases. + See also `:erlang.alias/1`. + + Inlined by the compiler. + + ## Examples + + alias = Process.alias([:reply]) + + """ + @doc since: "1.15.0" + @spec alias([alias_opt]) :: alias + defdelegate alias(options), to: :erlang + + @doc """ + Explicitly deactivates a process alias. + + Returns `true` if `alias` was a currently-active alias for current processes, + or `false` otherwise. + + See [the module documentation](#module-aliases) for more information about aliases. + See also `:erlang.unalias/1`. + + Inlined by the compiler. + + ## Examples + + alias = Process.alias() + Process.unalias(alias) + #=> true + + """ + @doc since: "1.15.0" + @spec unalias(alias) :: boolean + defdelegate unalias(alias), to: :erlang + @compile {:inline, nilify: 1} defp nilify(:undefined), do: nil defp nilify(other), do: other diff --git a/lib/elixir/src/elixir_rewrite.erl b/lib/elixir/src/elixir_rewrite.erl index 138bf8082de..3aa56ac35d7 100644 --- a/lib/elixir/src/elixir_rewrite.erl +++ b/lib/elixir/src/elixir_rewrite.erl @@ -168,6 +168,8 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity). ?inline(?port, list, 0, erlang, ports); ?inline(?port, open, 2, erlang, open_port); +?inline(?process, alias, 0, erlang, alias); +?inline(?process, alias, 1, erlang, alias); ?inline(?process, 'alive?', 1, erlang, is_process_alive); ?inline(?process, cancel_timer, 1, erlang, cancel_timer); ?inline(?process, cancel_timer, 2, erlang, cancel_timer); @@ -188,6 +190,7 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity). ?inline(?process, send, 3, erlang, send); ?inline(?process, spawn, 2, erlang, spawn_opt); ?inline(?process, spawn, 4, erlang, spawn_opt); +?inline(?process, unalias, 1, erlang, unalias); ?inline(?process, unlink, 1, erlang, unlink); ?inline(?process, unregister, 1, erlang, unregister); @@ -245,6 +248,7 @@ rewrite(Receiver, DotMeta, Right, Meta, Args) -> ?rewrite(?port, monitor, [Arg], erlang, monitor, [port, Arg]); ?rewrite(?process, group_leader, [Pid, Leader], erlang, group_leader, [Leader, Pid]); ?rewrite(?process, monitor, [Arg], erlang, monitor, [process, Arg]); +?rewrite(?process, monitor, [Arg, Opts], erlang, monitor, [process, Arg, Opts]); ?rewrite(?process, send_after, [Dest, Msg, Time], erlang, send_after, [Time, Dest, Msg]); ?rewrite(?process, send_after, [Dest, Msg, Time, Opts], erlang, send_after, [Time, Dest, Msg, Opts]); ?rewrite(?string, to_atom, [Arg], erlang, binary_to_atom, [Arg, utf8]); diff --git a/lib/elixir/test/elixir/process_test.exs b/lib/elixir/test/elixir/process_test.exs index ef8ec8200bc..3241dc25c34 100644 --- a/lib/elixir/test/elixir/process_test.exs +++ b/lib/elixir/test/elixir/process_test.exs @@ -27,11 +27,30 @@ defmodule ProcessTest do end # In contrast with other inlined functions, - # it is important to test that monitor/1 is inlined, + # it is important to test that monitor/1,2 are inlined, # this way we gain the monitor receive optimisation. - test "monitor/1 is inlined" do + test "monitor/1 and monitor/2 are inlined" do assert expand(quote(do: Process.monitor(pid())), __ENV__) == quote(do: :erlang.monitor(:process, pid())) + + assert expand(quote(do: Process.monitor(pid(), alias: :demonitor)), __ENV__) == + quote(do: :erlang.monitor(:process, pid(), alias: :demonitor)) + end + + test "monitor/2 with monitor options" do + pid = + spawn(fn -> + receive do + {:ping, source_alias} -> send(source_alias, :pong) + end + end) + + ref_and_alias = Process.monitor(pid, alias: :explicit_unalias) + + send(pid, {:ping, ref_and_alias}) + + assert_receive :pong + assert_receive {:DOWN, ^ref_and_alias, _, _, _} end test "sleep/1" do @@ -117,6 +136,40 @@ defmodule ProcessTest do refute Process.alive?(pid) end + describe "alias/0, alias/1, and unalias/1" do + test "simple alias + unalias flow" do + server = + spawn(fn -> + receive do + {:ping, alias} -> send(alias, :pong) + end + end) + + alias = Process.alias() + Process.unalias(alias) + + send(server, {:ping, alias}) + refute_receive :pong, 20 + end + + test "with :reply option when aliasing" do + server = + spawn(fn -> + receive do + {:ping, alias} -> + send(alias, :pong) + send(alias, :extra_pong) + end + end) + + alias = Process.alias([:reply]) + + send(server, {:ping, alias}) + assert_receive :pong + refute_receive :extra_pong, 20 + end + end + defp expand(expr, env) do {expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env) expr From 179e5b13bd67d098da2742b0aebfee7399c0845c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 12:07:37 +0200 Subject: [PATCH 22/59] Do not lead body inference in cond, closes #12024 --- lib/elixir/lib/module/types/expr.ex | 24 ++++++++++++++++++ .../test/elixir/module/types/expr_test.exs | 25 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 1fb7ea3651a..fee80ecdbff 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -216,6 +216,30 @@ defmodule Module.Types.Expr do end end + # cond do pat -> expr end + def of_expr({:cond, _meta, [[{:do, clauses}]]} = expr, _expected, stack, context) do + stack = push_expr_stack(expr, stack) + + {result, context} = + reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> + case of_expr(head, :dynamic, stack, context) do + {:ok, _, context} -> + with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do + {:ok, keep_warnings(acc, context)} + end + + error -> + # Skip the clause if it the head has an error + if meta[:generated], do: {:ok, acc}, else: error + end + end) + + case result do + :ok -> {:ok, :dynamic, context} + :error -> {:error, context} + end + end + # case expr do pat -> expr end def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, _expected, stack, context) do stack = push_expr_stack(expr, stack) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 6479faaf923..812b57f5d04 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -230,6 +230,31 @@ defmodule Module.Types.ExprTest do end end + describe "cond" do + test "do not leak body inference between clauses" do + assert quoted_expr( + [], + cond do + 1 -> + b = :foo + b + + 2 -> + b = :bar + b + end + ) == {:ok, :dynamic} + + assert quoted_expr( + [b], + cond do + 1 -> :foo = b + 2 -> :bar = b + end + ) == {:ok, :dynamic} + end + end + test "fn" do assert quoted_expr(fn :foo = b -> :foo = b end) == {:ok, :dynamic} From 2c65758f56f8c723e90c99095be5ad74eda9d90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 12:08:32 +0200 Subject: [PATCH 23/59] Remove deprecated option test --- lib/elixir/test/elixir/code_test.exs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 45b10fe091f..bc086fa8116 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -40,10 +40,6 @@ defmodule CodeTest do Code.eval_string("a = (try do (raise \"hello\") rescue e -> e end)") end - test "supports the :requires option" do - assert Code.eval_string("Kernel.if true, do: :ok", [], requires: [Z, Kernel]) == {:ok, []} - end - test "returns bindings from a different context" do assert Code.eval_string("var!(a, Sample) = 1") == {1, [{{:a, Sample}, 1}]} end From e4b6bf5b68d52b71a63785285b04a23bbfc46df1 Mon Sep 17 00:00:00 2001 From: Benjamin Milde Date: Thu, 28 Jul 2022 12:09:51 +0200 Subject: [PATCH 24/59] Improve logger documentation (#12025) --- lib/logger/lib/logger.ex | 14 +++----------- lib/logger/lib/logger/backends/console.ex | 3 ++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index e582dbb4b1d..f05689f1a25 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -138,17 +138,9 @@ defmodule Logger do Logger.error("We have a problem", [error_code: :pc_load_letter]) - In your app's logger configuration, you would need to include the - `:error_code` key and you would need to include `$metadata` as part of - your log format template: - - config :logger, :console, - format: "[$level] $message $metadata\n", - metadata: [:error_code, :file] - - Your logs might then receive lines like this: - - [error] We have a problem error_code=pc_load_letter file=lib/app.ex + You might need to configure your logger backends to handle those metadata + values. For the default `:console` backend there's an example in + `Logger.Backends.Console`. ## Configuration diff --git a/lib/logger/lib/logger/backends/console.ex b/lib/logger/lib/logger/backends/console.ex index 09a7f9f0b8e..edc9bb635d2 100644 --- a/lib/logger/lib/logger/backends/console.ex +++ b/lib/logger/lib/logger/backends/console.ex @@ -18,7 +18,8 @@ defmodule Logger.Backends.Console do * `:metadata` - the metadata to be printed by `$metadata`. Defaults to an empty list (no metadata). Setting `:metadata` to `:all` prints all metadata. See - the "Metadata" section for more information. + the "Metadata" section in the `Logger` documentation for + more information. * `:colors` - a keyword list of coloring options. From 004d2126fb5664eed19a3e99edfdaf57e1251c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 12:53:47 +0200 Subject: [PATCH 25/59] Fix --sname/--name undefined handling on Erlang/OTP 24 --- lib/elixir/lib/kernel/cli.ex | 45 ++++++++++++++++++--------------- lib/iex/lib/iex/cli.ex | 48 ++++++++++++++++++------------------ 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index a3985cd06aa..dbd4b33ca4d 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -298,8 +298,10 @@ defmodule Kernel.CLI do end defp append_hostname(node) do - case :string.find(node, "@") do - :nomatch -> node <> :string.find(Atom.to_string(:net_kernel.nodename()), "@") + with :nomatch <- :string.find(node, "@"), + <> <- :string.find(Atom.to_string(:net_kernel.nodename()), "@") do + node <> suffix + else _ -> node end end @@ -435,11 +437,7 @@ defmodule Kernel.CLI do # Process commands defp process_command({:cookie, h}, _config) do - if Node.alive?() do - wrapper(fn -> Node.set_cookie(String.to_atom(h)) end) - else - {:error, "--cookie : Cannot set cookie if the node is not alive (set --name or --sname)"} - end + wrapper(fn -> Node.set_cookie(String.to_atom(h)) end) end defp process_command({:eval, expr}, _config) when is_binary(expr) do @@ -447,21 +445,28 @@ defmodule Kernel.CLI do end defp process_command({:rpc_eval, node, expr}, _config) when is_binary(expr) do - if Node.alive?() do - node = String.to_atom(node) + node = String.to_atom(node) - # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. - _ = :net_kernel.connect_node(node) + # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. + _ = :net_kernel.connect_node(node) - case :rpc.call(node, __MODULE__, :rpc_eval, [expr]) do - :ok -> :ok - {:badrpc, {:EXIT, exit}} -> Process.exit(self(), exit) - {:badrpc, reason} -> {:error, "--rpc-eval : RPC failed with reason #{inspect(reason)}"} - {kind, error, stack} -> :erlang.raise(kind, error, stack) - end - else - {:error, - "--rpc-eval : Cannot run --rpc-eval if the node is not alive (set --name or --sname)"} + case :rpc.call(node, __MODULE__, :rpc_eval, [expr]) do + :ok -> + :ok + + {:badrpc, {:EXIT, exit}} -> + Process.exit(self(), exit) + + {:badrpc, reason} -> + if reason == :nodedown and :net_kernel.nodename() == :ignored do + {:error, + "--rpc-eval : Cannot run --rpc-eval if the node is not alive (set --name or --sname)"} + else + {:error, "--rpc-eval : RPC failed with reason #{inspect(reason)}"} + end + + {kind, error, stack} -> + :erlang.raise(kind, error, stack) end end diff --git a/lib/iex/lib/iex/cli.ex b/lib/iex/lib/iex/cli.ex index 6a9577fab89..1ba38f8b302 100644 --- a/lib/iex/lib/iex/cli.ex +++ b/lib/iex/lib/iex/cli.ex @@ -88,33 +88,31 @@ defmodule IEx.CLI do defp tty_args do if remote = get_remsh(:init.get_plain_arguments()) do - if Node.alive?() do - # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. - _ = :net_kernel.connect_node(remote) - - case :rpc.call(remote, :code, :ensure_loaded, [IEx]) do - {:badrpc, reason} -> - message = + # Explicitly connect the node in case the rpc node was started with --sname/--name undefined. + _ = :net_kernel.connect_node(remote) + + case :rpc.call(remote, :code, :ensure_loaded, [IEx]) do + {:badrpc, reason} -> + message = + if reason == :nodedown and :net_kernel.nodename() == :ignored do + "In order to use --remsh, you need to name the current node using --name or --sname. Aborting..." + else "Could not contact remote node #{remote}, reason: #{inspect(reason)}. Aborting..." + end - abort(message) + abort(message) - {:module, IEx} -> - case :rpc.call(remote, :net_kernel, :get_net_ticktime, []) do - seconds when is_integer(seconds) -> :net_kernel.set_net_ticktime(seconds) - _ -> :ok - end + {:module, IEx} -> + case :rpc.call(remote, :net_kernel, :get_net_ticktime, []) do + seconds when is_integer(seconds) -> :net_kernel.set_net_ticktime(seconds) + _ -> :ok + end - {mod, fun, args} = remote_start_mfa() - {remote, mod, fun, args} + {mod, fun, args} = remote_start_mfa() + {remote, mod, fun, args} - _ -> - abort("Could not find IEx on remote node #{remote}. Aborting...") - end - else - abort( - "In order to use --remsh, you need to name the current node using --name or --sname. Aborting..." - ) + _ -> + abort("Could not find IEx on remote node #{remote}. Aborting...") end else local_start_mfa() @@ -168,8 +166,10 @@ defmodule IEx.CLI do defp get_remsh([]), do: nil defp append_hostname(node) do - case :string.find(node, '@') do - :nomatch -> node ++ :string.find(Atom.to_charlist(:net_kernel.nodename()), '@') + with :nomatch <- :string.find(node, "@"), + [_ | _] = suffix <- :string.find(Atom.to_charlist(:net_kernel.nodename()), "@") do + node ++ suffix + else _ -> node end end From 704c520abffdcbe4c72d86af507d7718a3177aed Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Thu, 28 Jul 2022 23:35:37 +0700 Subject: [PATCH 26/59] Use setup-beam on Windows platform (#12026) --- .github/workflows/ci.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0f7b8860fc..16159afc500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019 strategy: matrix: - otp_release: ['24.0'] + otp_release: ['24', '25'] runs-on: windows-2019 steps: - name: Configure Git @@ -87,16 +87,12 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 50 - - name: Cache Erlang/OTP package - uses: actions/cache@v3 + - uses: erlef/setup-beam@v1 with: - path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang - key: OTP-${{ matrix.otp_release }}-windows-2019 - - name: Install Erlang/OTP - run: choco install -y erlang --version ${{ matrix.otp_release }} + otp-version: ${{ matrix.otp_release }} - name: Compile Elixir run: | - remove-item '.git' -recurse -force + Remove-Item -Recurse -Force '.git' make compile - name: Build info run: bin/elixir --version @@ -106,7 +102,7 @@ jobs: run: make --keep-going test_erlang - name: Elixir test suite run: | - del c:/Windows/System32/drivers/etc/hosts + Remove-Item 'c:/Windows/System32/drivers/etc/hosts' make --keep-going test_elixir check_posix_compliant: From 8d612e338570675641df973a8439d546408608c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 22:10:55 +0200 Subject: [PATCH 27/59] Do not use undefined as node name due to regressions in Erlang/OTP 24 --- lib/mix/lib/mix/tasks/release.init.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/release.init.ex b/lib/mix/lib/mix/tasks/release.init.ex index 45e54d977e4..a29ef8689fe 100644 --- a/lib/mix/lib/mix/tasks/release.init.ex +++ b/lib/mix/lib/mix/tasks/release.init.ex @@ -141,7 +141,7 @@ defmodule Mix.Tasks.Release.Init do rpc () { exec "$REL_VSN_DIR/elixir" \ --hidden --cookie "$RELEASE_COOKIE" \ - $(release_distribution "undefined") \ + $(release_distribution "rpc-$(rand)-$RELEASE_NODE") \ --boot "$REL_VSN_DIR/$RELEASE_BOOT_SCRIPT_CLEAN" \ --boot-var RELEASE_LIB "$RELEASE_ROOT/lib" \ --vm-args "$RELEASE_REMOTE_VM_ARGS" \ @@ -398,7 +398,7 @@ defmodule Mix.Tasks.Release.Init do if "!RELEASE_DISTRIBUTION!" == "none" ( set RELEASE_DISTRIBUTION_FLAG= ) else ( - set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "undefined" + set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!" ) "!REL_VSN_DIR!\iex.bat" ^ @@ -414,7 +414,7 @@ defmodule Mix.Tasks.Release.Init do if "!RELEASE_DISTRIBUTION!" == "none" ( set RELEASE_DISTRIBUTION_FLAG= ) else ( - set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "undefined" + set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!" ) "!REL_VSN_DIR!\elixir.bat" ^ From 87f38d078a4acc6e07518a47d8c0c3c1e38c4991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 23:03:45 +0200 Subject: [PATCH 28/59] Write optional_applications to .app file --- .../lib/mix/compilers/application_tracer.ex | 5 ++--- lib/mix/lib/mix/tasks/compile.all.ex | 4 ++-- lib/mix/lib/mix/tasks/compile.app.ex | 21 ++++++++++--------- lib/mix/test/mix/tasks/compile.app_test.exs | 10 +++++++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/mix/lib/mix/compilers/application_tracer.ex b/lib/mix/lib/mix/compilers/application_tracer.ex index b309b0c1ccc..8a62ff0225a 100644 --- a/lib/mix/lib/mix/compilers/application_tracer.ex +++ b/lib/mix/lib/mix/compilers/application_tracer.ex @@ -206,11 +206,10 @@ defmodule Mix.Compilers.ApplicationTracer do defp build_manifest(config) do table = :ets.new(@table, [:public, :named_table, :set, read_concurrency: true]) - {required, optional} = Mix.Tasks.Compile.App.project_apps(config) + {all, _optional} = Mix.Tasks.Compile.App.project_apps(config) %{} - |> store_apps(table, required) - |> store_apps(table, optional) + |> store_apps(table, all) |> store_apps(table, extra_apps(config)) table diff --git a/lib/mix/lib/mix/tasks/compile.all.ex b/lib/mix/lib/mix/tasks/compile.all.ex index 489ee6592d5..c33ac07b0f3 100644 --- a/lib/mix/lib/mix/tasks/compile.all.ex +++ b/lib/mix/lib/mix/tasks/compile.all.ex @@ -96,12 +96,12 @@ defmodule Mix.Tasks.Compile.All do ## App loading helpers defp load_apps(config, lib_path, validate_compile_env?) do - {runtime, optional} = Mix.Tasks.Compile.App.project_apps(config) + {all, _optional} = Mix.Tasks.Compile.App.project_apps(config) parent = self() opts = [ordered: false, timeout: :infinity] deps = for dep <- Mix.Dep.cached(), into: %{}, do: {dep.app, lib_path} - stream_apps(runtime ++ optional, deps) + stream_apps(all, deps) |> Task.async_stream(&load_stream_app(&1, parent, validate_compile_env?), opts) |> Stream.run() end diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 2873409341a..8676bec06c8 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -342,13 +342,14 @@ defmodule Mix.Tasks.Compile.App do defp handle_extra_applications(properties, config) do {extra, properties} = Keyword.pop(properties, :extra_applications, []) - {required, _optional} = + {all, optional} = project_apps(properties, config, extra, fn -> apps_from_runtime_prod_deps(properties, config) end) - # TODO: Also store optional applications once support lands in Erlang/OTP 24+ - Keyword.put(properties, :applications, required) + properties + |> Keyword.put(:applications, all) + |> Keyword.put(:optional_applications, optional) end defp apps_from_runtime_prod_deps(properties, config) do @@ -409,17 +410,17 @@ defmodule Mix.Tasks.Compile.App do defp project_apps(properties, config, extra, deps_loader) do apps = Keyword.get(properties, :applications) || deps_loader.() - {required, optional} = split_by_type(extra ++ apps) - required = Enum.uniq(language_apps(config) ++ Enum.reverse(required)) + {all, required, optional} = split_by_type(extra ++ apps) + all = Enum.uniq(language_apps(config) ++ Enum.reverse(all)) optional = Enum.uniq(Enum.reverse(optional -- required)) - {required, optional} + {all, optional} end defp split_by_type(apps) do - Enum.reduce(apps, {[], []}, fn - app, {required, optional} when is_atom(app) -> {[app | required], optional} - {app, :required}, {required, optional} -> {[app | required], optional} - {app, :optional}, {required, optional} -> {required, [app | optional]} + Enum.reduce(apps, {[], [], []}, fn + app, {all, required, optional} when is_atom(app) -> {[app | all], [app | required], optional} + {app, :required}, {all, required, optional} -> {[app | all], [app | required], optional} + {app, :optional}, {all, required, optional} -> {[app | all], required, [app | optional]} end) end diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 287aa77137f..ddadb14c8de 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -116,8 +116,12 @@ defmodule Mix.Tasks.Compile.AppTest do properties = parse_resource_file(:custom_project) assert properties[:vsn] == '0.2.0' assert properties[:maxT] == :infinity - assert properties[:applications] == [:kernel, :stdlib, :elixir, :logger, :example_app] + assert properties[:optional_applications] == [:ex_unit, :mix] assert properties[:description] == 'Some UTF-8 dëscriptión' + + assert properties[:applications] == + [:kernel, :stdlib, :elixir, :logger, :ex_unit, :example_app, :mix] + refute Keyword.has_key?(properties, :extra_applications) end) end @@ -132,7 +136,9 @@ defmodule Mix.Tasks.Compile.AppTest do properties = parse_resource_file(:custom_deps) assert properties[:applications] == - [:kernel, :stdlib, :elixir, :logger, :ok1, :ok3, :ok4, :ok7, :ok11] + [:kernel, :stdlib, :elixir, :logger, :ok1, :ok3, :ok4, :ok6, :ok7, :ok11] + + assert properties[:optional_applications] == [:ok6] end) end From ee8ae8f257c9f66de8ea3549de4309e0e1aab81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Jul 2022 23:04:02 +0200 Subject: [PATCH 29/59] mix format --- lib/mix/lib/mix/tasks/compile.app.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 8676bec06c8..768bb6990a3 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -418,9 +418,14 @@ defmodule Mix.Tasks.Compile.App do defp split_by_type(apps) do Enum.reduce(apps, {[], [], []}, fn - app, {all, required, optional} when is_atom(app) -> {[app | all], [app | required], optional} - {app, :required}, {all, required, optional} -> {[app | all], [app | required], optional} - {app, :optional}, {all, required, optional} -> {[app | all], required, [app | optional]} + app, {all, required, optional} when is_atom(app) -> + {[app | all], [app | required], optional} + + {app, :required}, {all, required, optional} -> + {[app | all], [app | required], optional} + + {app, :optional}, {all, required, optional} -> + {[app | all], required, [app | optional]} end) end From 8a61a884eb350974e5bc0a04397adbfdfa27bc99 Mon Sep 17 00:00:00 2001 From: Zulfiqar Soomro Date: Fri, 29 Jul 2022 02:01:34 -0400 Subject: [PATCH 30/59] Fixes typo: "past" -> "paste" (#12029) --- lib/mix/lib/mix/tasks/test.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 502d8c13895..d49c592b363 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -86,7 +86,7 @@ defmodule Mix.Tasks.Test do test/foo_test.exs:5 If you want to re-run only this test, all you need to do is to - copy the line above and past it in front of `mix test`: + copy the line above and paste it in front of `mix test`: mix test test/foo_test.exs:5 From 687afac223d761b5aeab5b1fdf5db4503a2ee970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Fri, 29 Jul 2022 08:16:43 +0200 Subject: [PATCH 31/59] Fix Process.monitor/2 spec (#12030) --- lib/elixir/lib/process.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index 32f9ffe0f8f..ff9630cbac9 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -560,7 +560,8 @@ defmodule Process do """ @doc since: "1.15.0" - @spec monitor(pid | {name, node} | name, :erlang.monitor_option()) :: reference when name: atom + @spec monitor(pid | {name, node} | name, [:erlang.monitor_option()]) :: reference + when name: atom def monitor(item, options) do :erlang.monitor(:process, item, options) end From b89e351412f256a80036d9dbe70a7fc3e99d9cc3 Mon Sep 17 00:00:00 2001 From: Tom Nicklin Date: Fri, 29 Jul 2022 16:25:59 +0100 Subject: [PATCH 32/59] Another example for IO.stream (#12031) --- lib/elixir/lib/io.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index 34443955bbf..496459423e0 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -598,6 +598,14 @@ defmodule IO do Enum.each(IO.stream(:stdio, :line), &IO.write(&1)) + Another example where you might want to collect a user input + every new line and break on an empty line, followed by removing + redundant new line characters (`"\n"`): + + IO.stream(:stdio, :line) + |> Enum.take_while(&(&1 != "\n")) + |> Enum.map(&String.replace(&1, "\n", "")) + """ @spec stream(device, :line | pos_integer) :: Enumerable.t() def stream(device \\ :stdio, line_or_codepoints) From 27857609b0a922c5ee138e4d2609a5b189410bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 30 Jul 2022 18:21:47 +0200 Subject: [PATCH 33/59] Allow key-deletions and atom keys in System.put_env/1 --- lib/elixir/lib/system.ex | 21 +++++++++++++++++++-- lib/elixir/test/elixir/system_test.exs | 16 ++++++++++++---- lib/mix/lib/mix.ex | 7 +++---- lib/mix/lib/mix/dep/loader.ex | 11 +++-------- lib/mix/test/mix_test.exs | 4 +++- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index d137250bd39..f5d5f21a298 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -723,11 +723,28 @@ defmodule System do Sets multiple environment variables. Sets a new value for each environment variable corresponding - to each `{key, value}` pair in `enum`. + to each `{key, value}` pair in `enum`. Keys are automatically + converted to strings, values are sent as is. `nil` values erase + the given keys. """ @spec put_env(Enumerable.t()) :: :ok def put_env(enum) do - Enum.each(enum, fn {key, val} -> put_env(key, val) end) + Enum.each(enum, fn + {key, nil} -> + :os.unsetenv(to_charlist(key)) + + {key, val} -> + key = to_charlist(key) + + case :string.find(key, "=") do + :nomatch -> + :os.putenv(key, to_charlist(val)) + + _ -> + raise ArgumentError, + "cannot execute System.put_env/1 for key with \"=\", got: #{inspect(key)}" + end + end) end @doc """ diff --git a/lib/elixir/test/elixir/system_test.exs b/lib/elixir/test/elixir/system_test.exs index 2df500ed0d7..8b8542c2202 100644 --- a/lib/elixir/test/elixir/system_test.exs +++ b/lib/elixir/test/elixir/system_test.exs @@ -50,7 +50,7 @@ defmodule SystemTest do @test_var "SYSTEM_ELIXIR_ENV_TEST_VAR" - test "*_env/*" do + test "get_env/put_env/delete_env" do assert System.get_env(@test_var) == nil assert System.get_env(@test_var, "SAMPLE") == "SAMPLE" assert System.fetch_env(@test_var) == :error @@ -68,14 +68,22 @@ defmodule SystemTest do System.delete_env(@test_var) assert System.get_env(@test_var) == nil - System.put_env(%{@test_var => "OTHER_SAMPLE"}) - assert System.get_env(@test_var) == "OTHER_SAMPLE" - assert_raise ArgumentError, ~r[cannot execute System.put_env/2 for key with \"=\"], fn -> System.put_env("FOO=BAR", "BAZ") end end + test "put_env/2" do + System.put_env(%{@test_var => "MAP_STRING"}) + assert System.get_env(@test_var) == "MAP_STRING" + + System.put_env([{String.to_atom(@test_var), "KW_ATOM"}]) + assert System.get_env(@test_var) == "KW_ATOM" + + System.put_env([{String.to_atom(@test_var), nil}]) + assert System.get_env(@test_var) == nil + end + test "cmd/2 raises for null bytes" do assert_raise ArgumentError, ~r"cannot execute System.cmd/3 for program with null byte", fn -> System.cmd("null\0byte", []) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index c6377a8c1b8..7fbd1241b10 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -567,9 +567,8 @@ defmodule Mix do apps * `:system_env` (since v1.13.0) - a list or a map of system environment variable - names as binary keys and their respective values as binaries. The system environment - is made part of the `Mix.install/2` cache, so different configurations will lead - to different apps + names with respective values as binaries. The system environment is made part + of the `Mix.install/2` cache, so different configurations will lead to different apps ## Examples @@ -589,7 +588,7 @@ defmodule Mix do nx: [default_backend: EXLA] ], system_env: [ - {"XLA_TARGET", "cuda111"} + XLA_TARGET: "cuda111" ] ) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 3ecdf733219..2c7daa3a543 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -61,6 +61,7 @@ defmodule Mix.Dep.Loader do def with_system_env(%Mix.Dep{system_env: new_env}, callback) do old_env = for {key, _} <- new_env do + key = to_string(key) {key, System.get_env(key)} end @@ -68,13 +69,7 @@ defmodule Mix.Dep.Loader do System.put_env(new_env) callback.() after - for {key, value} <- old_env do - if value do - System.put_env(key, value) - else - System.delete_env(key) - end - end + System.put_env(old_env) end end @@ -208,7 +203,7 @@ defmodule Mix.Dep.Loader do requirement: req, status: scm_status(scm, opts), opts: Keyword.put_new(opts, :env, :prod), - system_env: system_env + system_env: Enum.to_list(system_env) } end diff --git a/lib/mix/test/mix_test.exs b/lib/mix/test/mix_test.exs index 49611a9f5b2..3cb90f1824d 100644 --- a/lib/mix/test/mix_test.exs +++ b/lib/mix/test/mix_test.exs @@ -141,13 +141,15 @@ defmodule MixTest do {:install_test, path: Path.join(tmp_dir, "install_test")} ], config: [unknown_app: [foo: :bar]], - system_env: %{"MIX_INSTALL_FOO" => "BAR"} + system_env: %{"MIX_INSTALL_FOO" => "BAR", MIX_INSTALL_BAZ: "BAT"} ) assert Application.fetch_env!(:unknown_app, :foo) == :bar assert System.fetch_env!("MIX_INSTALL_FOO") == "BAR" + assert System.fetch_env!("MIX_INSTALL_BAZ") == "BAT" after System.delete_env("MIX_INSTALL_FOO") + System.delete_env("MIX_INSTALL_BAZ") Application.delete_env(:unknown_app, :foo, persistent: true) end From b0b3cc6de43d12d3a2a249c290685dc094b0e15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 31 Jul 2022 12:12:16 +0200 Subject: [PATCH 34/59] Only add if_undefined: :apply if necessary --- lib/elixir/lib/kernel/special_forms.ex | 2 +- lib/elixir/lib/macro.ex | 2 + lib/iex/lib/iex/evaluator.ex | 13 ++- lib/iex/test/iex/interaction_test.exs | 6 +- lib/iex/test/iex/server_test.exs | 112 ++++++++++++++----------- 5 files changed, 78 insertions(+), 57 deletions(-) diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index 0f2711dc7f4..0fdfd95f0ef 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -1320,7 +1320,7 @@ defmodule Kernel.SpecialForms do Which the argument for the `:sum` function call is not the expected result: - {:sum, [], [1, {:value, [if_undefined: :apply], Elixir}, 3]} + {:sum, [], [1, {:value, [], Elixir}, 3]} For this, we use `unquote`: diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index a36a71a3e97..5bf9543b20c 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -162,6 +162,8 @@ defmodule Macro do * `:generated` - Whether the code should be considered as generated by the compiler or not. This means the compiler and tools like Dialyzer may not emit certain warnings. + * `:if_undefined` - How to expand a variable that is undefined. Set it to + `:apply` if you want a variable to become a nullary call without warning. * `:keep` - Used by `quote/2` with the option `location: :keep` to annotate the file and the line number of the quoted source. * `:line` - The line number of the AST node. diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 662cd91ebdb..f0ea63f9e15 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -324,8 +324,9 @@ defmodule IEx.Evaluator do end defp eval_and_inspect(forms, line, state) do - forms = add_if_undefined_apply_to_vars(forms) - {result, binding, env} = eval_expr_by_expr(forms, state.binding, state.env) + %{env: env, binding: binding} = state + forms = add_if_undefined_apply_to_vars(forms, env) + {result, binding, env} = eval_expr_by_expr(forms, binding, env) unless result == IEx.dont_display_result() do io_inspect(result) @@ -335,10 +336,14 @@ defmodule IEx.Evaluator do %{state | env: env, binding: binding, history: history} end - defp add_if_undefined_apply_to_vars(forms) do + defp add_if_undefined_apply_to_vars(forms, env) do Macro.prewalk(forms, fn {var, meta, context} when is_atom(var) and is_atom(context) -> - {var, Keyword.put_new(meta, :if_undefined, :apply), context} + if Macro.Env.lookup_import(env, {var, 0}) != [] do + {var, Keyword.put_new(meta, :if_undefined, :apply), context} + else + {var, meta, context} + end other -> other diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 57430848836..cc541450fc8 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -198,8 +198,10 @@ defmodule IEx.InteractionTest do describe ".iex" do test "no .iex" do - assert "** (CompileError) iex:1: undefined function my_variable/0" <> _ = - capture_iex("my_variable") + capture_io(:stderr, fn -> + assert "** (CompileError) iex:1: undefined function my_variable/0" <> _ = + capture_iex("my_variable") + end) end @tag :tmp_dir diff --git a/lib/iex/test/iex/server_test.exs b/lib/iex/test/iex/server_test.exs index 512937b3fca..76326ffcb1d 100644 --- a/lib/iex/test/iex/server_test.exs +++ b/lib/iex/test/iex/server_test.exs @@ -42,12 +42,14 @@ defmodule IEx.ServerTest do test "outside of the evaluator with refusal", config do Process.register(self(), config.test) - {server, evaluator} = pry_session(config.test, "N\niex_context") - client = pry_request([server]) - send(evaluator, :run) + capture_io(:stderr, fn -> + {server, evaluator} = pry_session(config.test, "N\niex_context") + client = pry_request([server]) + send(evaluator, :run) - assert Task.await(client) == {:error, :refused} - assert Task.await(server) =~ "undefined function iex_context" + assert Task.await(client) == {:error, :refused} + assert Task.await(server) =~ "undefined function iex_context" + end) end test "outside of the evaluator with crash", config do @@ -63,71 +65,79 @@ defmodule IEx.ServerTest do test "outside of the evaluator with double acceptance", config do Process.register(self(), config.test) - {server1, evaluator1} = pry_session(config.test, "Y\niex_context") - {server2, evaluator2} = pry_session(config.test, "Y\niex_context") - client = pry_request([server1, server2]) + capture_io(:stderr, fn -> + {server1, evaluator1} = pry_session(config.test, "Y\niex_context") + {server2, evaluator2} = pry_session(config.test, "Y\niex_context") + client = pry_request([server1, server2]) - send(evaluator1, :run) - send(evaluator2, :run) - reply1 = Task.await(server1) - reply2 = Task.await(server2) + send(evaluator1, :run) + send(evaluator2, :run) + reply1 = Task.await(server1) + reply2 = Task.await(server2) - {accepted, refused} = - if reply1 =~ ":inside_pry", do: {reply1, reply2}, else: {reply2, reply1} + {accepted, refused} = + if reply1 =~ ":inside_pry", do: {reply1, reply2}, else: {reply2, reply1} - assert accepted =~ ":inside_pry" - assert refused =~ "** session was already accepted elsewhere" - assert refused =~ "undefined function iex_context" + assert accepted =~ ":inside_pry" + assert refused =~ "** session was already accepted elsewhere" + assert refused =~ "undefined function iex_context" - assert Task.await(client) == {:ok, false} + assert Task.await(client) == {:ok, false} + end) end test "outside of the evaluator with double refusal", config do Process.register(self(), config.test) - {server1, evaluator1} = pry_session(config.test, "N\niex_context") - {server2, evaluator2} = pry_session(config.test, "N\niex_context") - client = pry_request([server1, server2]) + capture_io(:stderr, fn -> + {server1, evaluator1} = pry_session(config.test, "N\niex_context") + {server2, evaluator2} = pry_session(config.test, "N\niex_context") + client = pry_request([server1, server2]) - send(evaluator1, :run) - send(evaluator2, :run) - reply1 = Task.await(server1) - reply2 = Task.await(server2) + send(evaluator1, :run) + send(evaluator2, :run) + reply1 = Task.await(server1) + reply2 = Task.await(server2) - assert reply1 =~ "undefined function iex_context" - assert reply2 =~ "undefined function iex_context" + assert reply1 =~ "undefined function iex_context" + assert reply2 =~ "undefined function iex_context" - assert Task.await(client) == {:error, :refused} + assert Task.await(client) == {:error, :refused} + end) end test "outside of the evaluator with acceptance and then refusal", config do Process.register(self(), config.test) - {server1, evaluator1} = pry_session(config.test, "Y\niex_context") - {server2, evaluator2} = pry_session(config.test, "N\niex_context") - client = pry_request([server1, server2]) + capture_io(:stderr, fn -> + {server1, evaluator1} = pry_session(config.test, "Y\niex_context") + {server2, evaluator2} = pry_session(config.test, "N\niex_context") + client = pry_request([server1, server2]) - send(evaluator1, :run) - send(evaluator2, :run) - assert Task.await(server1) =~ ":inside_pry" - assert Task.await(server2) =~ "undefined function iex_context" + send(evaluator1, :run) + send(evaluator2, :run) + assert Task.await(server1) =~ ":inside_pry" + assert Task.await(server2) =~ "undefined function iex_context" - assert Task.await(client) == {:ok, false} + assert Task.await(client) == {:ok, false} + end) end test "outside of the evaluator with refusal and then acceptance", config do Process.register(self(), config.test) - {server1, evaluator1} = pry_session(config.test, "N\niex_context") - {server2, evaluator2} = pry_session(config.test, "Y\niex_context") - client = pry_request([server1, server2]) + capture_io(:stderr, fn -> + {server1, evaluator1} = pry_session(config.test, "N\niex_context") + {server2, evaluator2} = pry_session(config.test, "Y\niex_context") + client = pry_request([server1, server2]) - send(evaluator1, :run) - send(evaluator2, :run) - assert Task.await(server1) =~ "undefined function iex_context" - assert Task.await(server2) =~ ":inside_pry" + send(evaluator1, :run) + send(evaluator2, :run) + assert Task.await(server1) =~ "undefined function iex_context" + assert Task.await(server2) =~ ":inside_pry" - assert Task.await(client) == {:ok, false} + assert Task.await(client) == {:ok, false} + end) end @tag :tmp_dir @@ -136,14 +146,16 @@ defmodule IEx.ServerTest do path = Path.join(tmp_dir, "dot-iex") File.write!(path, "my_variable = 144") - {server, evaluator} = pry_session(config.test, "Y\nmy_variable", dot_iex_path: path) - client = pry_request([server]) - send(evaluator, :run) + capture_io(:stderr, fn -> + {server, evaluator} = pry_session(config.test, "Y\nmy_variable", dot_iex_path: path) + client = pry_request([server]) + send(evaluator, :run) - assert Task.await(server) =~ - "** (UndefinedFunctionError) function :erl_eval.my_variable/0 is undefined or private" + assert Task.await(server) =~ + "** (UndefinedFunctionError) function :erl_eval.my_variable/0 is undefined or private" - assert Task.await(client) == {:ok, false} + assert Task.await(client) == {:ok, false} + end) end @tag :tmp_dir From 62c0c59ebd002ead1923b92b720545817ccbba38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 1 Aug 2022 12:06:06 +0200 Subject: [PATCH 35/59] Fix deprecation table rendering, closes #12034 --- lib/elixir/pages/compatibility-and-deprecations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/compatibility-and-deprecations.md b/lib/elixir/pages/compatibility-and-deprecations.md index 84cf4214c71..b79b59f00c7 100644 --- a/lib/elixir/pages/compatibility-and-deprecations.md +++ b/lib/elixir/pages/compatibility-and-deprecations.md @@ -90,7 +90,7 @@ Version | Deprecated feature | Replaced by (ava [v1.14] | Compiled patterns in `String.starts_with?/2` | Pass a list of strings instead (v1.0) [v1.14] | `Mix.Tasks.Xref.calls/1` | Compilation tracers (outlined in `Code`) (v1.10) [v1.14] | `$levelpad` in Logger | *None* -[v1.14] | `<|>` as a custom operator | Another custom operator (v1.0) +[v1.14] | `<\|>` as a custom operator | Another custom operator (v1.0) [v1.13] | `!` and `!=` in Version requirements | `~>` or `>=` (v1.0) [v1.13] | `Mix.Config` | `Config` (v1.9) [v1.13] | `:strip_beam` config to `mix escript.build` | `:strip_beams` (v1.9) From e8579addfbd23081ba7d33a65eb94eebf388e3bc Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 1 Aug 2022 16:25:04 +0200 Subject: [PATCH 36/59] Set correct permission in release action (#12036) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4aaeea968d..049e00823e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ env: jobs: create_draft_release: permissions: - contents: none + contents: write runs-on: ubuntu-18.04 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 93d9e8cba2e433ae6fc94603fec52a7c8676aca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 1 Aug 2022 22:34:21 +0200 Subject: [PATCH 37/59] Fix secrets syntax --- .github/workflows/notify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index af779483625..cf536243a67 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -22,7 +22,7 @@ jobs: elixir-version: 1.13.4 - name: Run Elixir script env: - ELIXIR_FORUM_TOKEN: ${{ secret.ELIXIR_FORUM_TOKEN }} - ELIXIR_LANG_ANN_TOKEN: ${{ secret.ELIXIR_LANG_ANN_TOKEN }} + ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }} + ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }} run: | elixir .github./workflows/notify.exs ${{ github.ref_name }} From 2d484cf417991c391f4225b94ccfb0e64c9db473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 1 Aug 2022 22:43:44 +0200 Subject: [PATCH 38/59] Allow dry-run of notify workflow --- .github/workflows/notify.exs | 50 +++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/.github/workflows/notify.exs b/.github/workflows/notify.exs index 6fd136111b3..2933bb5bd5c 100644 --- a/.github/workflows/notify.exs +++ b/.github/workflows/notify.exs @@ -6,17 +6,17 @@ Mix.install([ {:jason, "~> 1.0"} ]) -%{status: 200, body: body} = +%{status: 200, body: release} = Req.get!("https://api.github.com/repos/elixir-lang/elixir/releases/tags/#{tag}") -if body["draft"] do +if release["draft"] do raise "cannot notify a draft release" end ## Notify on elixir-lang-ann names_and_checksums = - for asset <- body["assets"], + for asset <- release["assets"], name = asset["name"], name =~ ~r/.sha\d+sum$/, do: {name, Req.get!(asset["browser_download_url"]).body} @@ -29,34 +29,44 @@ line_items = " * #{root} - #{type} - #{checksum}\n" end -headers = %{ - "X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN") -} - -body = %{ +mail = %{ "From" => "jose.valim@dashbit.co", "To" => "elixir-lang-ann@googlegroups.com", "Subject" => "Elixir #{tag} released", "HtmlBody" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{line_items}", - "MessageStream": "outbound" + "MessageStream" => "outbound" } -resp = Req.post!("https://api.postmarkapp.com/email", {:json, body}, headers: headers) -IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}") +if System.get_env("DRYRUN") do + IO.puts("MAIL") + IO.inspect(mail) +else + headers = %{ + "X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN") + } -## Notify on Elixir Forum + resp = Req.post!("https://api.postmarkapp.com/email", {:json, mail}, headers: headers) + IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}") +end -headers = %{ - "api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"), - "api-username" => "Elixir" -} +## Notify on Elixir Forum -body = %{ +post = %{ "title" => "Elixir #{tag} released", - "raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{body["body"]}", + "raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{release["body"]}", # Elixir News "category" => 28 } -resp = Req.post!("https://elixirforum.com/posts.json", {:json, body}, headers: headers) -IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}") +if System.get_env("DRYRUN") do + IO.puts("POST") + IO.inspect(post) +else + headers = %{ + "api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"), + "api-username" => "Elixir" + } + + resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers) + IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}") +end From 7d8d43b109d8b61ac3634793a3d7ef286091a1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Tue, 2 Aug 2022 09:44:12 +0200 Subject: [PATCH 39/59] Emit consistent position meta on fn capture traces (#12033) --- lib/elixir/src/elixir_expand.erl | 8 +++--- lib/elixir/src/elixir_fn.erl | 16 +++++------ .../test/elixir/kernel/expansion_test.exs | 3 +- .../test/elixir/kernel/tracers_test.exs | 28 ++++++++++++++++--- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 1c746505911..bf3613727d1 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -536,14 +536,14 @@ resolve_super(Meta, Arity, E) -> expand_fn_capture(Meta, Arg, S, E) -> case elixir_fn:capture(Meta, Arg, S, E) of - {{remote, Remote, Fun, Arity}, SE, EE} -> + {{remote, Remote, Fun, Arity}, RemoteMeta, SE, EE} -> is_atom(Remote) andalso - elixir_env:trace({remote_function, Meta, Remote, Fun, Arity}, E), + elixir_env:trace({remote_function, RemoteMeta, Remote, Fun, Arity}, E), AttachedMeta = attach_context_module(Remote, Meta, E), {{'&', AttachedMeta, [{'/', [], [{{'.', [], [Remote, Fun]}, [], []}, Arity]}]}, SE, EE}; - {{local, Fun, Arity}, _SE, #{function := nil}} -> + {{local, Fun, Arity}, _LocalMeta, _SE, #{function := nil}} -> form_error(Meta, E, ?MODULE, {undefined_local_capture, Fun, Arity}); - {{local, Fun, Arity}, SE, EE} -> + {{local, Fun, Arity}, _LocalMeta, SE, EE} -> {{'&', Meta, [{'/', [], [{Fun, [], nil}, Arity]}]}, SE, EE}; {expand, Expr, SE, EE} -> expand(Expr, SE, EE) diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl index 7085cda7e42..1341be54581 100644 --- a/lib/elixir/src/elixir_fn.erl +++ b/lib/elixir/src/elixir_fn.erl @@ -38,14 +38,14 @@ fn_arity(Args) -> length(Args). %% Capture -capture(Meta, {'/', _, [{{'.', _, [M, F]} = Dot, DotMeta, []}, A]}, S, E) when is_atom(F), is_integer(A) -> +capture(Meta, {'/', _, [{{'.', _, [M, F]} = Dot, RequireMeta, []}, A]}, S, E) when is_atom(F), is_integer(A) -> Args = args_from_arity(Meta, A, E), - handle_capture_possible_warning(Meta, DotMeta, M, F, A, E), - capture_require(Meta, {Dot, Meta, Args}, S, E, true); + handle_capture_possible_warning(Meta, RequireMeta, M, F, A, E), + capture_require(Meta, {Dot, RequireMeta, Args}, S, E, true); -capture(Meta, {'/', _, [{F, _, C}, A]}, S, E) when is_atom(F), is_integer(A), is_atom(C) -> +capture(Meta, {'/', _, [{F, ImportMeta, C}, A]}, S, E) when is_atom(F), is_integer(A), is_atom(C) -> Args = args_from_arity(Meta, A, E), - capture_import(Meta, {F, Meta, Args}, S, E, true); + capture_import(Meta, {F, ImportMeta, Args}, S, E, true); capture(Meta, {{'.', _, [_, Fun]}, _, Args} = Expr, S, E) when is_atom(Fun), is_list(Args) -> capture_require(Meta, Expr, S, E, is_sequential_and_not_empty(Args)); @@ -94,7 +94,7 @@ capture_require(Meta, {{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, end, Dot = {{'.', DotMeta, [ELeft, Right]}, RequireMeta, Args}, - handle_capture(Res, Meta, Dot, SE, EE, Sequential); + handle_capture(Res, RequireMeta, Dot, SE, EE, Sequential); {EscLeft, Escaped} -> Dot = {{'.', DotMeta, [EscLeft, Right]}, RequireMeta, Args}, @@ -103,8 +103,8 @@ capture_require(Meta, {{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, handle_capture(false, Meta, Expr, S, E, Sequential) -> capture_expr(Meta, Expr, S, E, Sequential); -handle_capture(LocalOrRemote, _Meta, _Expr, S, E, _Sequential) -> - {LocalOrRemote, S, E}. +handle_capture(LocalOrRemote, Meta, _Expr, S, E, _Sequential) -> + {LocalOrRemote, Meta, S, E}. capture_expr(Meta, Expr, S, E, Sequential) -> capture_expr(Meta, Expr, S, E, [], Sequential). diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 56bde6883a7..accaac01073 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -1081,7 +1081,8 @@ defmodule Kernel.ExpansionTest do fn -> 17 end end - assert expand(before_expansion) == after_expansion + assert clean_meta(expand(before_expansion), [:import, :context, :no_parens]) == + after_expansion end test "fails on non-continuous" do diff --git a/lib/elixir/test/elixir/kernel/tracers_test.exs b/lib/elixir/test/elixir/kernel/tracers_test.exs index e38757a8322..03ffefad6a1 100644 --- a/lib/elixir/test/elixir/kernel/tracers_test.exs +++ b/lib/elixir/test/elixir/kernel/tracers_test.exs @@ -83,6 +83,26 @@ defmodule Kernel.TracersTest do assert meta[:column] == 11 end + test "traces imports via capture" do + compile_string(""" + import Integer, only: [is_odd: 1, parse: 1] + &is_odd/1 + &parse/1 + """) + + assert_receive {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _} + assert meta[:line] == 1 + assert meta[:column] == 1 + + assert_receive {{:imported_macro, meta, Integer, :is_odd, 1}, _} + assert meta[:line] == 2 + assert meta[:column] == 2 + + assert_receive {{:imported_function, meta, Integer, :parse, 1}, _} + assert meta[:line] == 3 + assert meta[:column] == 2 + end + test "traces structs" do compile_string(""" %URI{path: "/"} @@ -118,11 +138,11 @@ defmodule Kernel.TracersTest do assert_receive {{:remote_macro, meta, Integer, :is_odd, 1}, _} assert meta[:line] == 2 - assert meta[:column] == 1 + assert meta[:column] == 10 assert_receive {{:remote_function, meta, Integer, :parse, 1}, _} assert meta[:line] == 3 - assert meta[:column] == 1 + assert meta[:column] == 10 end test "traces locals" do @@ -157,11 +177,11 @@ defmodule Kernel.TracersTest do assert_receive {{:local_macro, meta, :foo, 1}, _} assert meta[:line] == 4 - assert meta[:column] == 20 + assert meta[:column] == 21 assert_receive {{:local_function, meta, :bar, 1}, _} assert meta[:line] == 4 - assert meta[:column] == 28 + assert meta[:column] == 29 after :code.purge(Sample) :code.delete(Sample) From 9ac5acff65e0ca30b5b0d2bda24e012b34074b28 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 2 Aug 2022 17:47:25 +0900 Subject: [PATCH 40/59] Fix bug in Enum.drop/2 (#12040) --- lib/elixir/lib/enum.ex | 2 +- lib/elixir/test/elixir/enum_test.exs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index e0a954a870a..ec825d62501 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -4491,7 +4491,7 @@ defmodule Enum do {count, fn start, amount, _step -> - list |> :lists.reverse() |> slice_exact(start, amount, 1, 1) + list |> :lists.reverse() |> slice_exact(start, amount, 1, count) end} end end diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 1e74ce0d56e..5a6fe412b5a 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -265,6 +265,20 @@ defmodule EnumTest do end end + test "drop/2 with streams" do + drop_stream = fn list, count -> list |> Stream.map(& &1) |> Enum.drop(count) end + + assert drop_stream.([1, 2, 3], 0) == [1, 2, 3] + assert drop_stream.([1, 2, 3], 1) == [2, 3] + assert drop_stream.([1, 2, 3], 2) == [3] + assert drop_stream.([1, 2, 3], 3) == [] + assert drop_stream.([1, 2, 3], 4) == [] + assert drop_stream.([1, 2, 3], -1) == [1, 2] + assert drop_stream.([1, 2, 3], -2) == [1] + assert drop_stream.([1, 2, 3], -4) == [] + assert drop_stream.([], 3) == [] + end + test "drop_every/2" do assert Enum.drop_every([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2) == [2, 4, 6, 8, 10] assert Enum.drop_every([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3) == [2, 3, 5, 6, 8, 9] From 3a1493c062d00beae62504ff3b04ff316aeaa6f6 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Tue, 2 Aug 2022 22:59:40 +0900 Subject: [PATCH 41/59] Fix bug in Enum.slice with step>1 (#12042) --- lib/elixir/lib/enum.ex | 28 +++++++--------------------- lib/elixir/test/elixir/enum_test.exs | 3 +++ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index ec825d62501..27cee0fa950 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -4469,29 +4469,15 @@ defmodule Enum do {:error, module} -> {list, count} = - if step == 1 do - enumerable - |> module.reduce({:cont, {[], 0}}, fn elem, {acc, count} -> - {:cont, {[elem | acc], count + 1}} - end) - |> elem(1) - else - # We want to count all elements but we only keep the ones we need - {_, {list, _, count}} = - module.reduce(enumerable, {:cont, {[], 1, 0}}, fn - elem, {acc, 1, count} -> - {:cont, {[elem | acc], step, count + 1}} - - _elem, {acc, to_drop, count} -> - {:cont, {acc, to_drop - 1, count + 1}} - end) - - {list, count} - end + enumerable + |> module.reduce({:cont, {[], 0}}, fn elem, {acc, count} -> + {:cont, {[elem | acc], count + 1}} + end) + |> elem(1) {count, - fn start, amount, _step -> - list |> :lists.reverse() |> slice_exact(start, amount, 1, count) + fn start, amount, step -> + list |> :lists.reverse() |> slice_exact(start, amount, step, count) end} end end diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 5a6fe412b5a..4b64b8c955a 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -1094,6 +1094,9 @@ defmodule EnumTest do assert [1, 2, 3] |> Stream.cycle() |> Stream.take(10) |> Enum.slice(-10..-6//2) == [1, 3, 2] + + assert [1, 2, 3] |> Stream.cycle() |> Stream.take(10) |> Enum.slice(-9..-5//2) == + [2, 1, 3] end test "sort/1" do From a15e548464e3d1ad1ff55bbf4c5e2061be35d89d Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 3 Aug 2022 01:27:36 +0900 Subject: [PATCH 42/59] Fix bug: Enum.slice selecting extra element (#12043) --- lib/elixir/lib/enum.ex | 5 ++++- lib/elixir/test/elixir/enum_test.exs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 27cee0fa950..c2f6a8138bb 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -4436,7 +4436,10 @@ defmodule Enum do end entry, {start, amount, to_drop, list} -> - {:halt, {start, amount, to_drop, [entry | list]}} + case to_drop do + 1 -> {:halt, {start, amount, to_drop, [entry | list]}} + _ -> {:halt, {start, amount, to_drop, list}} + end end) :lists.reverse(slice) diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 4b64b8c955a..2120ec6585b 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -1077,6 +1077,7 @@ defmodule EnumTest do assert [1, 2, 3] |> Stream.cycle() |> Enum.slice(0..1) == [1, 2] assert [1, 2, 3] |> Stream.cycle() |> Enum.slice(0..4) == [1, 2, 3, 1, 2] assert [1, 2, 3] |> Stream.cycle() |> Enum.slice(0..4//2) == [1, 3, 2] + assert [1, 2, 3] |> Stream.cycle() |> Enum.slice(0..5//2) == [1, 3, 2] end test "slice on pruned infinite streams" do From f8f7b4f23e1a3932295b0fc0655d96bafd772b2c Mon Sep 17 00:00:00 2001 From: Nathan Long Date: Tue, 2 Aug 2022 13:06:41 -0400 Subject: [PATCH 43/59] Show usage of @typedoc and @doc for structs (#12037) * Show usage of @typedoc * Show adding '@doc' above a 'defstruct' --- lib/elixir/lib/kernel.ex | 8 ++++++++ lib/elixir/pages/typespecs.md | 11 ++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 02a253b167c..a610af8d7d3 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5142,6 +5142,14 @@ defmodule Kernel do defstruct [:title, :content, :author] end + Documentation can be added to a struct the same way as to a function, using + the `@doc` attribute. + + defmodule Post do + @doc "A post. The content should be valid Markdown." + defstruct [:title, :content, :author] + end + ## Deriving Although structs are maps, by default structs do not implement diff --git a/lib/elixir/pages/typespecs.md b/lib/elixir/pages/typespecs.md index 49f601d9041..41888ff4dbb 100644 --- a/lib/elixir/pages/typespecs.md +++ b/lib/elixir/pages/typespecs.md @@ -14,11 +14,14 @@ Type specifications (sometimes referred to as *typespecs*) are defined in differ * `@callback` * `@macrocallback` +In addition, you can use `@typedoc` to describe a custom `@type` definition. + See the "User-defined types" and "Defining a specification" sub-sections below for more information on defining types and typespecs. ## A simple example defmodule StringHelpers do + @typedoc "A word from the dictionary" @type word() :: String.t() @spec long_word?(word()) :: boolean() @@ -27,11 +30,13 @@ See the "User-defined types" and "Defining a specification" sub-sections below f end end -In the example above, this happens: +In the example above: + + * We declare a new type (`word()`) that is equivalent to the string type (`String.t()`). - * we declare a new type (`word()`) that is equivalent to the string type (`String.t()`); + * We describe the type using a `@typedoc`, which will be included in the generated documentation. - * we specify that the `long_word?/1` function takes an argument of type `word()` and + * We specify that the `long_word?/1` function takes an argument of type `word()` and returns a boolean (`boolean()`), that is, either `true` or `false`. ## Types and their syntax From 319152d9a9b87864efd8ded99e0be3e8e0b6c448 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 3 Aug 2022 20:42:54 +0900 Subject: [PATCH 44/59] Fix bugs in binary_slice/2 (#12045) --- lib/elixir/lib/kernel.ex | 33 ++++++++++---------------- lib/elixir/test/elixir/kernel_test.exs | 7 ++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index a610af8d7d3..3d0b08ffd07 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -4649,29 +4649,22 @@ defmodule Kernel do when is_binary(binary) and step > 0 do total = byte_size(binary) - first = - case first < 0 do - true -> max(first + total, 0) - false -> first - end + first = if first < 0, do: max(first + total, 0), else: first + last = if last < 0, do: last + total, else: last - last = - case last < 0 do - true -> last + total - false -> last - end + amount = last - first + 1 - case first < total do - true -> - part = binary_part(binary, first, min(total - first, last - first + 1)) - - case step do - 1 -> part - _ -> for <>, into: "", do: <> - end + if first < total and amount > 0 do + part = binary_part(binary, first, min(amount, total - first)) - false -> - "" + if step == 1 do + part + else + <> = part + for <<_::size(step - 1)-bytes, byte <- rest>>, into: <>, do: <> + end + else + "" end end diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index 4f51c917f79..209799551d0 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -1424,6 +1424,13 @@ defmodule KernelTest do assert match?(x when ceil(x) == 1, 0.2) end + test "binary_slice/2" do + assert binary_slice("abc", -1..0) == "" + assert binary_slice("abc", -5..-5) == "" + assert binary_slice("x", 0..0//2) == "x" + assert binary_slice("abcde", 1..3//2) == "bd" + end + test "sigil_U/2" do assert ~U[2015-01-13 13:00:07.123Z] == %DateTime{ calendar: Calendar.ISO, From 7472e65cff6a5b51517cd12ace9d005e9ce005a7 Mon Sep 17 00:00:00 2001 From: Matthew O'Gorman Date: Wed, 3 Aug 2022 11:57:57 -0400 Subject: [PATCH 45/59] String.valid? should only take strings (#12046) --- lib/elixir/lib/string.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index fa142c5a235..8ff0329a74f 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1795,12 +1795,14 @@ defmodule String do iex> String.valid?("asd" <> <<0xFFFF::16>>) false + iex> String.valid?(4) + ** (FunctionClauseError) no function clause matching in String.valid?/1 + """ @spec valid?(t) :: boolean def valid?(string) def valid?(<>), do: valid_utf8?(string) - def valid?(_), do: false defp valid_utf8?(<<_::utf8, rest::bits>>), do: valid_utf8?(rest) defp valid_utf8?(<<>>), do: true From ff3046cd45503c82410528fbe312344f0680bfd8 Mon Sep 17 00:00:00 2001 From: Andrea leopardi Date: Thu, 4 Aug 2022 08:41:26 +0200 Subject: [PATCH 46/59] Polish docs in the Calendar module --- lib/elixir/lib/calendar.ex | 84 +++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 382be823500..5be11139437 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -3,17 +3,18 @@ defmodule Calendar do This module defines the responsibilities for working with calendars, dates, times and datetimes in Elixir. - Currently it defines types and the minimal implementation - for a calendar behaviour in Elixir. The goal of the Calendar + It defines types and the minimal implementation + for a calendar behaviour in Elixir. The goal of the calendar features in Elixir is to provide a base for interoperability - instead of full-featured datetime API. + rather than a full-featured datetime API. - For the actual date, time and datetime structures, see `Date`, - `Time`, `NaiveDateTime` and `DateTime`. + For the actual date, time and datetime structs, see `Date`, + `Time`, `NaiveDateTime`, and `DateTime`. - Note designations for year, month, day, and the like, are overspecified - (i.e. an integer instead of `1..12` for months) because different - calendars may have a different number of days per month, months per year and so on. + Types for year, month, day, and more are *overspecified*. + For example, the `t:month/0` type is specified as an integer + instead of `1..12`. This is because different calendars may + have a different number of days per month. """ @type year :: integer @@ -37,7 +38,7 @@ defmodule Calendar do It represents time as a fraction of a day (starting from midnight). `parts_in_day` specifies how much of the day is already passed, - while `parts_per_day` signifies how many parts there fit in a day. + while `parts_per_day` signifies how many parts are there in a day. """ @type day_fraction :: {parts_in_day :: non_neg_integer, parts_per_day :: pos_integer} @@ -45,8 +46,8 @@ defmodule Calendar do The internal date format that is used when converting between calendars. This is the number of days including the fractional part that has passed of - the last day since 0000-01-01+00:00T00:00.000000 in ISO 8601 notation (also - known as midnight 1 January BC 1 of the proleptic Gregorian calendar). + the last day since `0000-01-01+00:00T00:00.000000` in ISO 8601 notation (also + known as *midnight 1 January BC 1* of the proleptic Gregorian calendar). """ @type iso_days :: {days :: integer, day_fraction} @@ -54,18 +55,18 @@ defmodule Calendar do Microseconds with stored precision. The precision represents the number of digits that must be used when - representing the microseconds to external format. If the precision is 0, + representing the microseconds to external format. If the precision is `0`, it means microseconds must be skipped. """ @type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer} - @typedoc "A calendar implementation" + @typedoc "A calendar implementation." @type calendar :: module - @typedoc "The time zone ID according to the IANA tz database (for example, Europe/Zurich)" + @typedoc "The time zone ID according to the IANA tz database (for example, `Europe/Zurich`)." @type time_zone :: String.t() - @typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)" + @typedoc "The time zone abbreviation (for example, `CET` or `CEST` or `BST`)." @type zone_abbr :: String.t() @typedoc """ @@ -82,10 +83,10 @@ defmodule Calendar do """ @type std_offset :: integer - @typedoc "Any map/struct that contains the date fields" + @typedoc "Any map or struct that contains the date fields." @type date :: %{optional(any) => any, calendar: calendar, year: year, month: month, day: day} - @typedoc "Any map/struct that contains the time fields" + @typedoc "Any map or struct that contains the time fields." @type time :: %{ optional(any) => any, hour: hour, @@ -94,7 +95,7 @@ defmodule Calendar do microsecond: microsecond } - @typedoc "Any map/struct that contains the naive_datetime fields" + @typedoc "Any map or struct that contains the naive datetime fields." @type naive_datetime :: %{ optional(any) => any, calendar: calendar, @@ -107,7 +108,7 @@ defmodule Calendar do microsecond: microsecond } - @typedoc "Any map/struct that contains the datetime fields" + @typedoc "Any map or struct that contains the datetime fields." @type datetime :: %{ optional(any) => any, calendar: calendar, @@ -128,9 +129,9 @@ defmodule Calendar do Specifies the time zone database for calendar operations. Many functions in the `DateTime` module require a time zone database. - By default, it uses the default time zone database returned by + By default, this module uses the default time zone database returned by `Calendar.get_time_zone_database/0`, which defaults to - `Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC" + `Calendar.UTCOnlyTimeZoneDatabase`. This database only handles `Etc/UTC` datetimes and returns `{:error, :utc_only_time_zone_database}` for any other time zone. @@ -147,7 +148,7 @@ defmodule Calendar do @type time_zone_database :: module() @doc """ - Returns how many days there are in the given year-month. + Returns how many days there are in the given month of the given year. """ @callback days_in_month(year, month) :: day @@ -168,7 +169,7 @@ defmodule Calendar do @doc """ Calculates the day of the week from the given `year`, `month`, and `day`. - The `starting_on` represents the starting day of the week. All + `starting_on` represents the starting day of the week. All calendars must support at least the `:default` value. They may also support other values representing their days of the week. """ @@ -202,7 +203,7 @@ defmodule Calendar do @callback date_to_string(year, month, day) :: String.t() @doc """ - Converts the datetime (without time zone) into a string according to the calendar. + Converts the naive datetime (without time zone) into a string according to the calendar. """ @callback naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) :: String.t() @@ -230,13 +231,13 @@ defmodule Calendar do @callback time_to_string(hour, minute, second, microsecond) :: String.t() @doc """ - Converts the given datetime (without time zone) into the `t:iso_days/0` format. + Converts the datetime (without time zone) into the `t:iso_days/0` format. """ @callback naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) :: iso_days @doc """ - Converts `t:iso_days/0` to the Calendar's datetime format. + Converts `t:iso_days/0` to the calendar's datetime format. """ @callback naive_datetime_from_iso_days(iso_days) :: {year, month, day, hour, minute, second, microsecond} @@ -247,17 +248,17 @@ defmodule Calendar do @callback time_to_day_fraction(hour, minute, second, microsecond) :: day_fraction @doc """ - Converts `t:day_fraction/0` to the Calendar's time format. + Converts `t:day_fraction/0` to the calendar's time format. """ @callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond} @doc """ - Define the rollover moment for the given calendar. + Define the rollover moment for the calendar. This is the moment, in your calendar, when the current day ends and the next day starts. - The result of this function is used to check if two calendars rollover at + The result of this function is used to check if two calendars roll over at the same time of day. If they do not, we can only convert datetimes and times between them. If they do, this means that we can also convert dates as well as naive datetimes between them. @@ -266,10 +267,10 @@ defmodule Calendar do ## Examples - * If, in your Calendar, a new day starts at midnight, return {0, 1}. - * If, in your Calendar, a new day starts at sunrise, return {1, 4}. - * If, in your Calendar, a new day starts at noon, return {1, 2}. - * If, in your Calendar, a new day starts at sunset, return {3, 4}. + * If in your calendar a new day starts at midnight, return `{0, 1}`. + * If in your calendar a new day starts at sunrise, return `{1, 4}`. + * If in your calendar a new day starts at noon, return `{1, 2}`. + * If in your calendar a new day starts at sunset, return `{3, 4}`. """ @callback day_rollover_relative_to_midnight_utc() :: day_fraction @@ -286,7 +287,7 @@ defmodule Calendar do @doc """ Parses the string representation for a time returned by `c:time_to_string/4` - into a time-tuple. + into a time tuple. """ @doc since: "1.10.0" @callback parse_time(String.t()) :: @@ -295,7 +296,7 @@ defmodule Calendar do @doc """ Parses the string representation for a date returned by `c:date_to_string/3` - into a date-tuple. + into a date tuple. """ @doc since: "1.10.0" @callback parse_date(String.t()) :: @@ -304,7 +305,7 @@ defmodule Calendar do @doc """ Parses the string representation for a naive datetime returned by - `c:naive_datetime_to_string/7` into a naive-datetime-tuple. + `c:naive_datetime_to_string/7` into a naive datetime tuple. The given string may contain a timezone offset but it is ignored. """ @@ -315,7 +316,7 @@ defmodule Calendar do @doc """ Parses the string representation for a datetime returned by - `c:datetime_to_string/11` into a datetime-tuple. + `c:datetime_to_string/11` into a datetime tuple. The returned datetime must be in UTC. The original `utc_offset` it was written in must be returned in the result. @@ -346,7 +347,7 @@ defmodule Calendar do @doc """ Returns a microsecond tuple truncated to a given precision (`:microsecond`, - `:millisecond` or `:second`). + `:millisecond`, or `:second`). """ @doc since: "1.6.0" @spec truncate(Calendar.microsecond(), :microsecond | :millisecond | :second) :: @@ -379,9 +380,9 @@ defmodule Calendar do end @doc """ - Formats received datetime into a string. + Formats the given date, time, or datetime into a string. - The datetime can be any of the Calendar types (`Time`, `Date`, + The datetime can be any of the `Calendar` types (`Time`, `Date`, `NaiveDateTime`, and `DateTime`) or any map, as long as they contain all of the relevant fields necessary for formatting. For example, if you use `%Y` to format the year, the datetime @@ -473,7 +474,7 @@ defmodule Calendar do Z | Time zone abbreviation (empty string if naive) | CET, BRST % | Literal "%" character | % - Any other character will be interpreted as an invalid format and raise an error + Any other character will be interpreted as an invalid format and raise an error. ## Examples @@ -517,6 +518,7 @@ defmodule Calendar do ...> end ...>) "август" + """ @doc since: "1.11.0" @spec strftime(map(), String.t(), keyword()) :: String.t() From 36586b8c600e6e7cc50086ba1206bc17369b0428 Mon Sep 17 00:00:00 2001 From: Andrea leopardi Date: Thu, 4 Aug 2022 08:49:06 +0200 Subject: [PATCH 47/59] Polish docs for Calendar.TimeZoneDatabase --- lib/elixir/lib/calendar/time_zone_database.ex | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/elixir/lib/calendar/time_zone_database.ex b/lib/elixir/lib/calendar/time_zone_database.ex index ac597f9ed3f..7dce5971df0 100644 --- a/lib/elixir/lib/calendar/time_zone_database.ex +++ b/lib/elixir/lib/calendar/time_zone_database.ex @@ -7,12 +7,13 @@ defmodule Calendar.TimeZoneDatabase do """ @typedoc """ - A period where a certain combination of UTC offset, standard offset and zone + A period where a certain combination of UTC offset, standard offset, and zone abbreviation is in effect. - For instance one period could be the summer of 2018 in "Europe/London" where summer time / - daylight saving time is in effect and lasts from spring to autumn. At autumn the `std_offset` - changes along with the `zone_abbr` so a different period is needed during winter. + For example, one period could be the summer of 2018 in the `Europe/London` timezone, + where summer time/daylight saving time is in effect and lasts from spring to autumn. + In autumn, the `std_offset` changes along with the `zone_abbr` so a different + period is needed during winter. """ @type time_zone_period :: %{ optional(any) => any, @@ -24,14 +25,14 @@ defmodule Calendar.TimeZoneDatabase do @typedoc """ Limit for when a certain time zone period begins or ends. - A beginning is inclusive. An ending is exclusive. Eg. if a period is from - 2015-03-29 01:00:00 and until 2015-10-25 01:00:00, the period includes and - begins from the beginning of 2015-03-29 01:00:00 and lasts until just before - 2015-10-25 01:00:00. + A beginning is inclusive. An ending is exclusive. For example, if a period is from + `2015-03-29 01:00:00` and until `2015-10-25 01:00:00`, the period includes and + begins from the beginning of `2015-03-29 01:00:00` and lasts until just before + `2015-10-25 01:00:00`. - A beginning or end for certain periods are infinite. For instance the latest - period for time zones without DST or plans to change. However for the purpose - of this behaviour they are only used for gaps in wall time where the needed + A beginning or end for certain periods are infinite, such as the latest + period for time zones without DST or plans to change. However, for the purpose + of this behaviour, they are only used for gaps in wall time where the needed period limits are at a certain time. """ @type time_zone_period_limit :: Calendar.naive_datetime() @@ -50,17 +51,18 @@ defmodule Calendar.TimeZoneDatabase do @doc """ Possible time zone periods for a certain time zone and wall clock date and time. - When the provided `datetime` is ambiguous a tuple with `:ambiguous` and two possible - periods. The periods in the list are sorted with the first element being the one that begins first. + When the provided naive datetime is ambiguous, return a tuple with `:ambiguous` + and the two possible periods. The periods in the tuple must be sorted with the + first element being the one that begins first. - When the provided `datetime` is in a gap - for instance during the "spring forward" when going - from winter time to summer time, a tuple with `:gap` and two periods with limits are returned + When the provided naive datetime is in a gap, such as during the "spring forward" when going + from winter time to summer time, return a tuple with `:gap` and two periods with limits in a nested tuple. The first nested two-tuple is the period before the gap and a naive datetime with a limit for when the period ends (wall time). The second nested two-tuple is the period just after the gap and a datetime (wall time) for when the period begins just after the gap. - If there is only a single possible period for the provided `datetime`, then a tuple with `:ok` - and the `time_zone_period` is returned. + If there is only a single possible period for the provided `datetime`, then return a tuple + with `:ok` and the `time_zone_period`. """ @doc since: "1.8.0" @callback time_zone_periods_from_wall_datetime(Calendar.naive_datetime(), Calendar.time_zone()) :: @@ -73,7 +75,7 @@ end defmodule Calendar.UTCOnlyTimeZoneDatabase do @moduledoc """ - Built-in time zone database that works only in Etc/UTC. + Built-in time zone database that works only in the `Etc/UTC` timezone. For all other time zones, it returns `{:error, :utc_only_time_zone_database}`. """ From c48f43a7725b98ef99cbd07d0613e6747ea8ae57 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 4 Aug 2022 16:10:03 +0900 Subject: [PATCH 48/59] Fix infinite loop in Enum.take/2 (#12048) --- lib/elixir/lib/range.ex | 2 +- lib/elixir/test/elixir/enum_test.exs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index 3f745ab93cb..c0ce5e053fe 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -425,7 +425,7 @@ defimpl Enumerable, for: Range do slice(Map.put(range, :step, step)) end - defp slice(current, _step, 1), do: [current] + 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/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 2120ec6585b..b398c567fdc 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -2286,6 +2286,7 @@ defmodule EnumTest.Range do assert Enum.take(1..3, -2) == [2, 3] assert Enum.take(1..3, -4) == [1, 2, 3] assert Enum.take(1..0, 3) == [1, 0] + assert Enum.take(1..0//1, -3) == [] end test "take_every/2" do From 6a3e398d9f6994a6948da11ac4b91ab0d6f36a45 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 4 Aug 2022 09:28:50 +0200 Subject: [PATCH 49/59] Add guards to Calendar.put_time_zone_database/1 (#12049) --- lib/elixir/lib/calendar.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 5be11139437..9f6822f087b 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -366,7 +366,7 @@ defmodule Calendar do """ @doc since: "1.8.0" @spec put_time_zone_database(time_zone_database()) :: :ok - def put_time_zone_database(database) do + def put_time_zone_database(database) when is_atom(database) do Application.put_env(:elixir, :time_zone_database, database) end From 2c1e6463e1c08880b4d69c64ee0e4f06b2926597 Mon Sep 17 00:00:00 2001 From: Andrea leopardi Date: Thu, 4 Aug 2022 09:39:55 +0200 Subject: [PATCH 50/59] Improve Calendar.strftime/3 docs --- lib/elixir/lib/calendar.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 9f6822f087b..05283ea3fc8 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -512,12 +512,12 @@ defmodule Calendar do ...> ~U[2019-08-26 13:52:06.0Z], ...> "%B", ...> month_names: fn month -> - ...> {"январь", "февраль", "март", "апрель", "май", "июнь", - ...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"} + ...> {"січень", "лютий", "березень", "квітень", "травень", "червень", + ...> "липень", "серпень", "вересень", "жовтень", "листопад", "грудень"} ...> |> elem(month - 1) ...> end ...>) - "август" + "серпень" """ @doc since: "1.11.0" From 10cffeed1c6f249aaa3fdae8f8d127cdeba2e64d Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 4 Aug 2022 23:15:50 +0900 Subject: [PATCH 51/59] Add spec for Enum.slide/3 (#12050) --- lib/elixir/lib/enum.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index c2f6a8138bb..4cb26a72338 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2651,6 +2651,7 @@ defmodule Enum do """ @doc since: "1.13.0" + @spec slide(t, Range.t() | index, index) :: list def slide(enumerable, range_or_single_index, insertion_index) def slide(enumerable, single_index, insertion_index) when is_integer(single_index) do From 57f3931ad19c846ca78f40e2fc21b74ec8618c1a Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 4 Aug 2022 23:34:18 +0900 Subject: [PATCH 52/59] Fixing edge cases in Enum.slide/3 (#12052) * Fix bug in Enum.slide/3: empty lists * Fix edge case in Enum.slide/3 --- lib/elixir/lib/enum.ex | 5 +++++ lib/elixir/test/elixir/enum_test.exs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 4cb26a72338..a401465c51a 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2693,6 +2693,10 @@ defmodule Enum do "(tried to insert #{first}..#{last} at #{insertion_index})" end + def slide(enumerable, first..last, _insertion_index) when first > last do + Enum.to_list(enumerable) + end + # Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last) def slide(enumerable, first..last, insertion_index) do impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4 @@ -2741,6 +2745,7 @@ defmodule Enum do end defp slide_list_start(list, 0, middle, last), do: slide_list_middle(list, middle, last, []) + defp slide_list_start([], _start, _middle, _last), do: [] defp slide_list_middle([h | t], middle, last, acc) when middle > 0 do slide_list_middle(t, middle - 1, last - 1, [h | acc]) diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index b398c567fdc..f24967b5079 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -821,6 +821,7 @@ defmodule EnumTest do test "on an empty enum produces an empty list" do for enum <- [[], %{}, 0..-1//1, MapSet.new()] do assert Enum.slide(enum, 0..0, 0) == [] + assert Enum.slide(enum, 1..1, 2) == [] end end @@ -955,7 +956,8 @@ defmodule EnumTest do {4..8, 19}, {4..8, 0}, {4..8, 2}, - {10..20, 0} + {10..20, 0}, + {2..1//1, -20} ] for {slide_range, insertion_point} <- test_specs do From 82c05a63c60495385f1d5d3cac7de4d4e96292c6 Mon Sep 17 00:00:00 2001 From: 100phlecs Date: Thu, 4 Aug 2022 10:39:12 -0400 Subject: [PATCH 53/59] Allow multiple formatters per file extension and sigil (#12032) --- lib/mix/lib/mix/tasks/format.ex | 43 +++++++-- lib/mix/test/mix/tasks/format_test.exs | 125 +++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 10 deletions(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 3153303eac9..2bcc6039d16 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -117,10 +117,10 @@ defmodule Mix.Tasks.Format do Now any application can use your formatter as follows: - # .formatters.exs + # .formatter.exs [ # Define the desired plugins - plugins: [MixMarkdownFormatter], + plugins: [MixMarkdownFormatter, AnotherMarkdownFormatter], # Remember to update the inputs list to include the new extensions inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "posts/*.{md,markdown}"] ] @@ -129,6 +129,10 @@ defmodule Mix.Tasks.Format do sure that your dependencies and your application have been compiled, so the relevant plugin code can be loaded. Otherwise a warning is logged. + In addition, the order by which you input your plugins is the format order. + So, in the above `.formatter.exs`, the `MixMarkdownFormatter` will format + the markdown files and sigils before `AnotherMarkdownFormatter`. + ## Importing dependencies configuration This task supports importing formatter configuration from dependencies. @@ -298,7 +302,19 @@ defmodule Mix.Tasks.Format do sigils = for plugin <- plugins, sigil <- find_sigils_from_plugins(plugin, formatter_opts), - do: {sigil, &plugin.format(&1, &2 ++ formatter_opts)} + do: {sigil, plugin} + + sigils = + sigils + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Enum.map(fn {sigil, plugins} -> + {sigil, + fn input, opts -> + Enum.reduce(plugins, input, fn plugin, input -> + plugin.format(input, opts ++ formatter_opts) + end) + end} + end) formatter_opts = formatter_opts @@ -515,8 +531,12 @@ defmodule Mix.Tasks.Format do ext = Path.extname(file) cond do - plugin = find_plugin_for_extension(formatter_opts, ext) -> - &plugin.format(&1, [extension: ext, file: file] ++ formatter_opts) + plugins = find_plugins_for_extension(formatter_opts, ext) -> + fn input -> + Enum.reduce(plugins, input, fn plugin, input -> + plugin.format(input, [extension: ext, file: file] ++ formatter_opts) + end) + end ext in ~w(.ex .exs) -> &elixir_format(&1, [file: file] ++ formatter_opts) @@ -526,13 +546,16 @@ defmodule Mix.Tasks.Format do end end - defp find_plugin_for_extension(formatter_opts, ext) do + defp find_plugins_for_extension(formatter_opts, ext) do plugins = Keyword.get(formatter_opts, :plugins, []) - Enum.find(plugins, fn plugin -> - Code.ensure_loaded?(plugin) and function_exported?(plugin, :features, 1) and - ext in List.wrap(plugin.features(formatter_opts)[:extensions]) - end) + plugins = + Enum.filter(plugins, fn plugin -> + Code.ensure_loaded?(plugin) and function_exported?(plugin, :features, 1) and + ext in List.wrap(plugin.features(formatter_opts)[:extensions]) + end) + + if plugins != [], do: plugins, else: nil end defp find_formatter_and_opts_for_file(file, formatter_opts_and_subs) do diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 25532da5388..2a21f5dbd13 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -274,6 +274,37 @@ defmodule Mix.Tasks.FormatTest do end end + defmodule Elixir.NewlineToDotPlugin do + @behaviour Mix.Tasks.Format + + def features(opts) do + assert opts[:from_formatter_exs] == :yes + [extensions: ~w(.w), sigils: [:W]] + end + + def format(contents, opts) do + assert opts[:from_formatter_exs] == :yes + + cond do + opts[:extension] -> + assert opts[:extension] == ".w" + assert opts[:file] =~ ~r/\/a\.w$/ + assert [W: sigil_fun] = opts[:sigils] + assert is_function(sigil_fun, 2) + + opts[:sigil] -> + assert opts[:sigil] == :W + assert opts[:inputs] == ["a.ex"] + assert opts[:modifiers] == 'abc' + + true -> + flunk("Plugin not loading in correctly.") + end + + contents |> String.replace("\n", ".") + end + end + test "uses extension plugins from .formatter.exs", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """ @@ -298,6 +329,100 @@ defmodule Mix.Tasks.FormatTest do end) end + test "uses multiple plugins from .formatter.exs targetting the same file extension", context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [ + inputs: ["a.w"], + plugins: [ExtensionWPlugin, NewlineToDotPlugin], + from_formatter_exs: :yes + ] + """) + + File.write!("a.w", """ + foo bar baz + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("a.w") == "foo.bar.baz." + end) + end + + test "uses multiple plugins from .formatter.exs with the same file extension in declared order", + context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [ + inputs: ["a.w"], + plugins: [NewlineToDotPlugin, ExtensionWPlugin], + from_formatter_exs: :yes + ] + """) + + File.write!("a.w", """ + foo bar baz + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("a.w") == "foo\nbar\nbaz." + end) + end + + test "uses multiple plugins from .formatter.exs targetting the same sigil", context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [ + inputs: ["a.ex"], + plugins: [NewlineToDotPlugin, SigilWPlugin], + from_formatter_exs: :yes + ] + """) + + File.write!("a.ex", """ + def sigil_test(assigns) do + ~W"foo bar baz\n"abc + end + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("a.ex") == """ + def sigil_test(assigns) do + ~W"foo\nbar\nbaz."abc + end + """ + end) + end + + test "uses multiple plugins from .formatter.exs with the same sigil in declared order", + context do + in_tmp(context.test, fn -> + File.write!(".formatter.exs", """ + [ + inputs: ["a.ex"], + plugins: [SigilWPlugin, NewlineToDotPlugin], + from_formatter_exs: :yes + ] + """) + + File.write!("a.ex", """ + def sigil_test(assigns) do + ~W"foo bar baz"abc + end + """) + + Mix.Tasks.Format.run([]) + + assert File.read!("a.ex") == """ + def sigil_test(assigns) do + ~W"foo.bar.baz"abc + end + """ + end) + end + test "uses extension plugins with --stdin-filename", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """ From f3fef857c78211d822bc87cb5cebced2435ed3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 4 Aug 2022 19:23:51 +0200 Subject: [PATCH 54/59] Raise specific error for missing env --- lib/elixir/lib/system.ex | 13 ++++++++++--- lib/elixir/test/elixir/system_test.exs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index f5d5f21a298..5db7f5ff3a4 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -62,6 +62,15 @@ defmodule System do in the Erlang docs. """ + defmodule EnvError do + defexception [:env] + + @impl true + def message(%{env: env}) do + "could not fetch environment variable #{inspect(env)} because it is not set" + end + end + @typedoc """ The time unit to be passed to functions like `monotonic_time/1` and others. @@ -684,9 +693,7 @@ defmodule System do @doc since: "1.9.0" @spec fetch_env!(String.t()) :: String.t() def fetch_env!(varname) when is_binary(varname) do - get_env(varname) || - raise ArgumentError, - "could not fetch environment variable #{inspect(varname)} because it is not set" + get_env(varname) || raise(EnvError, env: varname) end @doc """ diff --git a/lib/elixir/test/elixir/system_test.exs b/lib/elixir/test/elixir/system_test.exs index 8b8542c2202..3424ca10cfd 100644 --- a/lib/elixir/test/elixir/system_test.exs +++ b/lib/elixir/test/elixir/system_test.exs @@ -56,7 +56,7 @@ defmodule SystemTest do assert System.fetch_env(@test_var) == :error message = "could not fetch environment variable #{inspect(@test_var)} because it is not set" - assert_raise ArgumentError, message, fn -> System.fetch_env!(@test_var) end + assert_raise System.EnvError, message, fn -> System.fetch_env!(@test_var) end System.put_env(@test_var, "SAMPLE") From 1c77deb1996023350b0e05b6b510ad9952dc9106 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sat, 6 Aug 2022 16:28:07 +0900 Subject: [PATCH 55/59] Replace inner AST for bitstring modifiers (#12055) --- lib/elixir/src/elixir_bitstring.erl | 20 +-- lib/elixir/src/elixir_erl_pass.erl | 2 +- .../test/elixir/kernel/expansion_test.exs | 140 +++++++++++------- 3 files changed, 101 insertions(+), 61 deletions(-) diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index 7977a289fdd..97d61fc6092 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -65,11 +65,11 @@ expr_type(Binary) when is_binary(Binary) -> binary; expr_type({'<<>>', _, _}) -> bitstring; expr_type(_) -> default. -infer_spec(bitstring, Meta) -> {bitstring, Meta, []}; -infer_spec(binary, Meta) -> {binary, Meta, []}; -infer_spec(float, Meta) -> {float, Meta, []}; -infer_spec(integer, Meta) -> {integer, Meta, []}; -infer_spec(default, Meta) -> {integer, Meta, []}. +infer_spec(bitstring, Meta) -> {bitstring, Meta, nil}; +infer_spec(binary, Meta) -> {binary, Meta, nil}; +infer_spec(float, Meta) -> {float, Meta, nil}; +infer_spec(integer, Meta) -> {integer, Meta, nil}; +infer_spec(default, Meta) -> {integer, Meta, nil}. concat_or_prepend_bitstring(_Meta, {'<<>>', _, []}, _ERight, Acc, _E, _RequireSize) -> Acc; @@ -77,10 +77,10 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac case E of #{context := match} when RequireSize -> case lists:last(Parts) of - {'::', SpecMeta, [Bin, {binary, _, []}]} when not is_binary(Bin) -> + {'::', SpecMeta, [Bin, {binary, _, nil}]} when not is_binary(Bin) -> form_error(SpecMeta, E, ?MODULE, unsized_binary); - {'::', SpecMeta, [_, {bitstring, _, []}]} -> + {'::', SpecMeta, [_, {bitstring, _, nil}]} -> form_error(SpecMeta, E, ?MODULE, unsized_binary); _ -> @@ -91,7 +91,7 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac end, case ERight of - {binary, _, []} -> + {binary, _, nil} -> {alignment, Alignment} = lists:keyfind(alignment, 1, PartsMeta), if @@ -104,7 +104,7 @@ concat_or_prepend_bitstring(Meta, {'<<>>', PartsMeta, Parts} = ELeft, ERight, Ac true -> [{'::', Meta, [ELeft, ERight]} | Acc] end; - {bitstring, _, []} -> + {bitstring, _, nil} -> lists:reverse(Parts, Acc) end; concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, _E, _RequireSize) -> @@ -335,7 +335,7 @@ valid_float_size(64) -> true; valid_float_size(_) -> false. add_spec(default, Spec) -> Spec; -add_spec(Key, Spec) -> [{Key, [], []} | Spec]. +add_spec(Key, Spec) -> [{Key, [], nil} | Spec]. find_match([{'=', _, [_Left, _Right]} = Expr | _Rest]) -> Expr; diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index f5143cf1720..77771b81f63 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -566,7 +566,7 @@ extract_bit_type({'-', _, [L, R]}, Acc) -> extract_bit_type(L, extract_bit_type(R, Acc)); extract_bit_type({unit, _, [Arg]}, Acc) -> [{unit, Arg} | Acc]; -extract_bit_type({Other, _, []}, Acc) -> +extract_bit_type({Other, _, nil}, Acc) -> [Other | Acc]. %% Optimizations that are specific to Erlang and change diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index accaac01073..5584c350142 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -730,11 +730,12 @@ defmodule Kernel.ExpansionTest do after_expansion = quote do - for(<<(<> <- b())>>, do: c = 1) + for(<<(<> <- b())>>, do: c = 1) c() end - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) end test "variables inside generator args do not leak" do @@ -2182,13 +2183,16 @@ defmodule Kernel.ExpansionTest do describe "bitstrings" do test "parallel match" do assert expand(quote(do: <> = <>)) |> clean_meta([:alignment]) == - quote(do: <> = <>) + quote(do: <> = <>) + |> clean_bit_modifiers() assert expand(quote(do: <> = baz = <>)) |> clean_meta([:alignment]) == - quote(do: <> = baz = <>) + quote(do: <> = baz = <>) + |> clean_bit_modifiers() assert expand(quote(do: <> = {<>} = bar())) |> clean_meta([:alignment]) == - quote(do: <> = {<>} = bar()) + quote(do: <> = {<>} = bar()) + |> clean_bit_modifiers() message = ~r"binary patterns cannot be matched in parallel using \"=\"" @@ -2265,11 +2269,12 @@ defmodule Kernel.ExpansionTest do test "nested match" do assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <> = rest()::binary>>)) |> clean_meta([:alignment]) == - quote(do: <<45::integer(), <<_::integer(), _::binary()>> = rest()::binary()>>) + quote(do: <<45::integer, <<_::integer, _::binary>> = rest()::binary>>) + |> clean_bit_modifiers() message = ~r"cannot pattern match inside a bitstring that is already in match" @@ -2287,14 +2292,15 @@ defmodule Kernel.ExpansionTest do # Check expansion happens only once assert expand(quote(do: "foo#{message_hello("bar")}")) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), "bar"::binary()>>) + quote(do: <<"foo"::binary, "bar"::binary>>) |> clean_bit_modifiers() assert_received :hello refute_received :hello # And it also works in match assert expand(quote(do: "foo#{bar()}" = "foobar")) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), "bar"::binary()>> = "foobar") + quote(do: <<"foo"::binary, "bar"::binary>> = "foobar") + |> clean_bit_modifiers() end test "inlines binaries inside interpolation is isomorphic after manual expansion" do @@ -2303,7 +2309,8 @@ defmodule Kernel.ExpansionTest do quoted = Macro.prewalk(quote(do: "foo#{bar()}" = "foobar"), &Macro.expand(&1, __ENV__)) assert expand(quoted) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), "bar"::binary()>> = "foobar") + quote(do: <<"foo"::binary, "bar"::binary>> = "foobar") + |> clean_bit_modifiers() end test "expands size * unit" do @@ -2311,50 +2318,50 @@ defmodule Kernel.ExpansionTest do import Kernel.ExpansionTarget assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() end test "expands binary/bitstring specifiers" do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() message = ~r"signed and unsigned specifiers are supported only on integer and float type" @@ -2367,13 +2374,13 @@ defmodule Kernel.ExpansionTest do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() message = ~r"signed and unsigned specifiers are supported only on integer and float type" @@ -2390,19 +2397,19 @@ defmodule Kernel.ExpansionTest do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() message = ~r"integer and float types require a size specifier if the unit specifier is given" @@ -2417,11 +2424,12 @@ defmodule Kernel.ExpansionTest do import Kernel.ExpansionTarget assert expand(quote(do: <>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) |> clean_bit_modifiers() assert expand(quote(do: <> = 1)) |> clean_meta([:alignment]) == - quote(do: <> = 1) + quote(do: <> = 1) + |> clean_bit_modifiers() end test "expands macro in args" do @@ -2436,10 +2444,11 @@ defmodule Kernel.ExpansionTest do after_expansion = quote do :"Elixir.Kernel.ExpansionTarget" - <> + <> end - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) end test "supports dynamic size" do @@ -2454,10 +2463,11 @@ defmodule Kernel.ExpansionTest do after_expansion = quote do var = 1 - <> + <> end - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) end defmacro offset(size, binary) do @@ -2475,25 +2485,29 @@ defmodule Kernel.ExpansionTest do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <>, z>>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) + |> clean_bit_modifiers() assert expand(quote(do: <>::bitstring, z>>)) |> clean_meta([:alignment]) == - quote(do: <>) + quote(do: <>) + |> clean_bit_modifiers() end test "merges binaries" do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: "foo" <> x)) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), x()::binary()>>) + quote(do: <<"foo"::binary, x()::binary>>) |> clean_bit_modifiers() assert expand(quote(do: "foo" <> <>)) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), x()::integer()-size(4), y()::integer()-size(4)>>) + quote(do: <<"foo"::binary, x()::integer-size(4), y()::integer-size(4)>>) + |> clean_bit_modifiers() assert expand(quote(do: <<"foo", <>::binary>>)) |> clean_meta([:alignment]) == - quote(do: <<"foo"::binary(), x()::integer()-size(4), y()::integer()-size(4)>>) + quote(do: <<"foo"::binary, x()::integer-size(4), y()::integer-size(4)>>) + |> clean_bit_modifiers() end test "guard expressions on size" do @@ -2511,17 +2525,19 @@ defmodule Kernel.ExpansionTest do after_expansion = quote do var = 1 - <> + <> end - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) # Other valid guard expressions are also legal for bitstring size in OTP 23+ before_expansion = quote(do: <>) - after_expansion = quote(do: <>) + after_expansion = quote(do: <>) - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) end test "map lookup on size" do @@ -2536,10 +2552,11 @@ defmodule Kernel.ExpansionTest do after_expansion = quote do var = %{foo: 3} - <> + <> end - assert expand(before_expansion) |> clean_meta([:alignment]) == after_expansion + assert expand(before_expansion) |> clean_meta([:alignment]) == + clean_bit_modifiers(after_expansion) end test "raises on unaligned binaries in match" do @@ -2574,7 +2591,7 @@ defmodule Kernel.ExpansionTest do import Kernel, except: [-: 1, -: 2] assert expand(quote(do: <<12.3::float-16>>)) |> clean_meta([:alignment]) == - quote(do: <<12.3::float()-size(16)>>) + quote(do: <<12.3::float-size(16)>>) |> clean_bit_modifiers() end test "raises for invalid size * unit for floats" do @@ -2861,6 +2878,29 @@ defmodule Kernel.ExpansionTest do Macro.prewalk(expr, &Macro.update_meta(&1, cleaner)) end + @bitstring_modifiers [ + :integer, + :float, + :binary, + :utf8, + :utf16, + :utf32, + :native, + :signed, + :bitstring, + :little + ] + + defp clean_bit_modifiers(expr) do + Macro.prewalk(expr, fn + {expr, meta, atom} when expr in @bitstring_modifiers and is_atom(atom) -> + {expr, meta, nil} + + other -> + other + end) + end + defp expand(expr) do expand_env(expr, __ENV__) |> elem(0) end From 9f93d0582a555299aa1a0b982017dd0ae3036181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 7 Aug 2022 15:11:59 +0200 Subject: [PATCH 56/59] Split the compiler process from the Mix process This makes sure that further processing within the Mix process won't slowdown the compiler. --- lib/mix/lib/mix/compilers/elixir.ex | 452 +++++++++++++++------------- 1 file changed, 246 insertions(+), 206 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index c5609875681..1863826fc6d 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -150,15 +150,12 @@ defmodule Mix.Compilers.Elixir do {pending_tracer, tracer_opts} = Mix.Compilers.ApplicationTracer.prepare(app_tracer, opts) previous_opts = set_compiler_opts(tracer_opts) - # Stores state for keeping track which files were compiled - # and the dependencies between them. - put_compiler_info({[], exports, sources, modules, removed_modules}) - try do - compile_path(stale, dest, timestamp, opts) + state = {[], exports, sources, modules, removed_modules} + compiler_loop(stale, dest, timestamp, opts, state) else - {:ok, _, warnings} -> - {modules, _exports, sources, pending_modules, _pending_exports} = get_compiler_info() + {:ok, warnings, state} -> + {modules, _exports, sources, pending_modules, _pending_exports} = state # We only collect the warnings if --all-warnings is given. # In this case, we print them too. Then we apply the new warnings. @@ -182,10 +179,10 @@ defmodule Mix.Compilers.Elixir do all_warnings = previous_warnings ++ Enum.map(warnings, &diagnostic(&1, :warning)) unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings}) - {:error, errors, warnings} -> + {:error, errors, warnings, state} -> # In case of errors, we show all previous warnings and all new ones. # Print the new ones if --all-warnings was given. - {_, _, sources, _, _} = get_compiler_info() + {_, _, sources, _, _} = state errors = Enum.map(errors, &diagnostic(&1, :error)) warnings = Enum.map(warnings, &diagnostic(&1, :warning)) {:error, previous_warnings(sources, opts[:all_warnings]) ++ warnings ++ errors} @@ -193,7 +190,6 @@ defmodule Mix.Compilers.Elixir do Code.compiler_options(previous_opts) Mix.Compilers.ApplicationTracer.stop(pending_tracer) Code.purge_compiler_modules() - delete_compiler_info() end else # We need to return ok if deps_changed? or stale_modules changed, @@ -409,28 +405,6 @@ defmodule Mix.Compilers.Elixir do |> :erlang.md5() end - defp compile_path(stale, dest, timestamp, opts) do - cwd = File.cwd!() - long_compilation_threshold = opts[:long_compilation_threshold] || 10 - verbose = opts[:verbose] || false - - compile_opts = [ - each_cycle: fn -> each_cycle(dest, timestamp) end, - each_file: &each_file(&1, &2, cwd, verbose), - each_module: &each_module(&1, &2, &3, cwd), - each_long_compilation: &each_long_compilation(&1, cwd, long_compilation_threshold), - long_compilation_threshold: long_compilation_threshold, - profile: opts[:profile], - beam_timestamp: timestamp - ] - - Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) - end - - defp get_compiler_info(), do: Process.get(__MODULE__) - defp put_compiler_info(value), do: Process.put(__MODULE__, value) - defp delete_compiler_info(), do: Process.delete(__MODULE__) - defp set_compiler_opts(opts) do opts |> Keyword.take(Code.available_compiler_options()) @@ -446,168 +420,6 @@ defmodule Mix.Compilers.Elixir do Mix.ProjectStack.compile_env(all_compile_env) end - defp each_cycle(compile_path, timestamp) do - {modules, _exports, sources, pending_modules, pending_exports} = get_compiler_info() - - {pending_modules, exports, changed} = - update_stale_entries(pending_modules, sources, [], %{}, pending_exports, compile_path) - - # For each changed file, mark it as changed. - # If compilation fails mid-cycle, they will - # be picked next time around. - for file <- changed do - File.touch!(file, timestamp) - end - - if changed == [] do - modules = Enum.map(modules, &module(&1, :module)) - warnings = Mix.Compilers.ApplicationTracer.warnings(modules) - - modules_set = Map.from_keys(modules, true) - {_, runtime_modules} = fixpoint_runtime_modules(sources, modules_set) - {:runtime, runtime_modules, warnings} - else - Mix.Utils.compiling_n(length(changed), :ex) - - # If we have a compile time dependency to a module, as soon as its file - # change, we will detect the compile time dependency and recompile. However, - # the whole goal of pending exports is to delay this decision, so we need to - # track which modules were removed and start them as our pending exports and - # remove the pending exports as we notice they have not gone stale. - {sources, removed_modules} = update_stale_sources(sources, changed) - put_compiler_info({modules, exports, sources, pending_modules, removed_modules}) - {:compile, changed, []} - end - end - - defp each_module(file, module, _binary, cwd) do - {modules, exports, sources, pending_modules, pending_exports} = get_compiler_info() - - kind = detect_kind(module) - file = Path.relative_to(file, cwd) - external = get_external_resources(module, cwd) - - old_export = Map.get(exports, module) - new_export = exports_md5(module, true) - - pending_exports = - if old_export && old_export != new_export do - pending_exports - else - Map.delete(pending_exports, module) - end - - {module_sources, existing_module?} = - case List.keyfind(modules, module, module(:module)) do - module(sources: old_sources) -> {[file | List.delete(old_sources, file)], true} - nil -> {[file], false} - end - - {source, sources} = - List.keytake(sources, file, source(:source)) || - Mix.raise( - "Could not find source for #{inspect(file)}. Make sure the :elixirc_paths configuration " <> - "is a list of relative paths to the current project or absolute paths to external directories" - ) - - source = - source( - source, - external: external ++ source(source, :external), - modules: [module | source(source, :modules)] - ) - - module = - module( - module: module, - kind: kind, - sources: module_sources, - export: new_export, - recompile?: function_exported?(module, :__mix_recompile__?, 0) - ) - - modules = prepend_or_merge(modules, module, module(:module), module, existing_module?) - put_compiler_info({modules, exports, [source | sources], pending_modules, pending_exports}) - :ok - end - - defp recompile_module?(module, recompile?) do - recompile? and Code.ensure_loaded?(module) and - function_exported?(module, :__mix_recompile__?, 0) and - module.__mix_recompile__?() - end - - defp prepend_or_merge(collection, key, pos, value, true) do - List.keystore(collection, key, pos, value) - end - - defp prepend_or_merge(collection, _key, _pos, value, false) do - [value | collection] - end - - defp detect_kind(module) do - protocol_metadata = Module.get_attribute(module, :__impl__) - - cond do - is_list(protocol_metadata) and protocol_metadata[:protocol] -> - {:impl, protocol_metadata[:protocol]} - - is_list(Module.get_attribute(module, :__protocol__)) -> - :protocol - - true -> - :module - end - end - - defp get_external_resources(module, cwd) do - for file <- Module.get_attribute(module, :external_resource) do - {Path.relative_to(file, cwd), File.exists?(file)} - end - end - - defp each_file(file, lexical, cwd, verbose) do - file = Path.relative_to(file, cwd) - - if verbose do - Mix.shell().info("Compiled #{file}") - end - - {modules, exports, sources, pending_modules, pending_exports} = get_compiler_info() - {source, sources} = List.keytake(sources, file, source(:source)) - - {compile_references, export_references, runtime_references, compile_env} = - Kernel.LexicalTracker.references(lexical) - - compile_references = - Enum.reject(compile_references, &match?("elixir_" <> _, Atom.to_string(&1))) - - source(modules: source_modules) = source - compile_references = compile_references -- source_modules - export_references = export_references -- source_modules - runtime_references = runtime_references -- source_modules - - source = - source( - source, - # We preserve the digest if the file is recompiled but not changed - digest: source(source, :digest) || digest(file), - compile_references: compile_references, - export_references: export_references, - runtime_references: runtime_references, - compile_env: compile_env - ) - - put_compiler_info({modules, exports, [source | sources], pending_modules, pending_exports}) - :ok - end - - defp each_long_compilation(file, cwd, threshold) do - Mix.shell().info( - "Compiling #{Path.relative_to(file, cwd)} (it's taking more than #{threshold}s)" - ) - end - ## Resolution defp remove_removed_sources(sources, removed) do @@ -637,18 +449,6 @@ defmodule Mix.Compilers.Elixir do end) end - # Define empty records for the sources that needs - # to be recompiled (but were not changed on disk) - defp update_stale_sources(sources, changed) do - Enum.reduce(changed, {sources, %{}}, fn file, {acc_sources, acc_modules} -> - {source(size: size, digest: digest, modules: modules), acc_sources} = - List.keytake(acc_sources, file, source(:source)) - - acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) - {[source(source: file, size: size, digest: digest) | acc_sources], acc_modules} - end) - end - # This function receives the manifest entries and some source # files that have changed. Then it recursively figures out # all the files that changed (via the module dependencies) and @@ -1033,4 +833,244 @@ defmodule Mix.Compilers.Elixir do {status, all_warnings} end end + + ## Compiler loop + # The compiler is invoked in a separate process so we avoid blocking its main loop. + + defp compiler_loop(stale, dest, timestamp, opts, state) do + ref = make_ref() + parent = self() + threshold = opts[:long_compilation_threshold] || 10 + profile = opts[:profile] + verbose = opts[:verbose] || false + + pid = + spawn_link(fn -> + compile_opts = [ + each_cycle: fn -> compiler_call(parent, ref, {:each_cycle, dest, timestamp}) end, + each_file: fn file, lexical -> + compiler_call(parent, ref, {:each_file, file, lexical, verbose}) + end, + each_module: fn file, module, _binary -> + compiler_call(parent, ref, {:each_module, file, module}) + end, + each_long_compilation: fn file -> + Mix.shell().info( + "Compiling #{Path.relative_to(file, File.cwd!())} (it's taking more than #{threshold}s)" + ) + end, + long_compilation_threshold: threshold, + profile: profile, + beam_timestamp: timestamp + ] + + response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) + send(parent, {ref, response}) + end) + + compiler_loop(ref, pid, state, File.cwd!()) + end + + defp compiler_call(parent, ref, info) do + send(parent, {ref, info}) + + receive do + {^ref, response} -> response + end + end + + defp compiler_loop(ref, pid, state, cwd) do + receive do + {^ref, {:each_cycle, dest, timestamp}} -> + {response, state} = each_cycle(dest, timestamp, state) + send(pid, {ref, response}) + compiler_loop(ref, pid, state, cwd) + + {^ref, {:each_file, file, lexical, verbose}} -> + # Read the relevant file information and unblock the compiler + references = Kernel.LexicalTracker.references(lexical) + send(pid, {ref, :ok}) + state = each_file(file, references, verbose, cwd, state) + compiler_loop(ref, pid, state, cwd) + + {^ref, {:each_module, file, module}} -> + # Read the relevant module information and unblock the compiler + kind = detect_kind(module) + external = Module.get_attribute(module, :external_resource) + new_export = exports_md5(module, true) + send(pid, {ref, :ok}) + state = each_module(file, module, cwd, kind, external, new_export, state) + compiler_loop(ref, pid, state, cwd) + + {^ref, {:ok, _modules, warnings}} -> + {:ok, warnings, state} + + {^ref, {:error, errors, warnings}} -> + {:error, errors, warnings, state} + end + end + + defp each_cycle(compile_path, timestamp, state) do + {modules, _exports, sources, pending_modules, pending_exports} = state + + {pending_modules, exports, changed} = + update_stale_entries(pending_modules, sources, [], %{}, pending_exports, compile_path) + + # For each changed file, mark it as changed. + # If compilation fails mid-cycle, they will be picked next time around. + for file <- changed do + File.touch!(file, timestamp) + end + + if changed == [] do + warnings = Mix.Compilers.ApplicationTracer.warnings(modules) + + modules_set = + modules + |> Enum.map(&module(&1, :module)) + |> Map.from_keys(true) + + {_, runtime_modules} = fixpoint_runtime_modules(sources, modules_set) + {{:runtime, runtime_modules, warnings}, state} + else + Mix.Utils.compiling_n(length(changed), :ex) + + # If we have a compile time dependency to a module, as soon as its file + # change, we will detect the compile time dependency and recompile. However, + # the whole goal of pending exports is to delay this decision, so we need to + # track which modules were removed and start them as our pending exports and + # remove the pending exports as we notice they have not gone stale. + {sources, removed_modules} = + Enum.reduce(changed, {sources, %{}}, fn file, {acc_sources, acc_modules} -> + {source(size: size, digest: digest, modules: modules), acc_sources} = + List.keytake(acc_sources, file, source(:source)) + + acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) + + # Define empty records for the sources that needs + # to be recompiled (but were not changed on disk) + {[source(source: file, size: size, digest: digest) | acc_sources], acc_modules} + end) + + state = {modules, exports, sources, pending_modules, removed_modules} + {{:compile, changed, []}, state} + end + end + + defp each_file(file, references, verbose, cwd, state) do + {compile_references, export_references, runtime_references, compile_env} = references + {modules, exports, sources, pending_modules, pending_exports} = state + + file = Path.relative_to(file, cwd) + + if verbose do + Mix.shell().info("Compiled #{file}") + end + + {source, sources} = List.keytake(sources, file, source(:source)) + + compile_references = + Enum.reject(compile_references, &match?("elixir_" <> _, Atom.to_string(&1))) + + source(modules: source_modules) = source + compile_references = compile_references -- source_modules + export_references = export_references -- source_modules + runtime_references = runtime_references -- source_modules + + source = + source( + source, + # We preserve the digest if the file is recompiled but not changed + digest: source(source, :digest) || digest(file), + compile_references: compile_references, + export_references: export_references, + runtime_references: runtime_references, + compile_env: compile_env + ) + + {modules, exports, [source | sources], pending_modules, pending_exports} + end + + defp each_module(file, module, cwd, kind, external, new_export, state) do + {modules, exports, sources, pending_modules, pending_exports} = state + + file = Path.relative_to(file, cwd) + external = process_external_resources(external, cwd) + + old_export = Map.get(exports, module) + + pending_exports = + if old_export && old_export != new_export do + pending_exports + else + Map.delete(pending_exports, module) + end + + {module_sources, existing_module?} = + case List.keyfind(modules, module, module(:module)) do + module(sources: old_sources) -> {[file | List.delete(old_sources, file)], true} + nil -> {[file], false} + end + + {source, sources} = + List.keytake(sources, file, source(:source)) || + Mix.raise( + "Could not find source for #{inspect(file)}. Make sure the :elixirc_paths configuration " <> + "is a list of relative paths to the current project or absolute paths to external directories" + ) + + source = + source( + source, + external: external ++ source(source, :external), + modules: [module | source(source, :modules)] + ) + + module = + module( + module: module, + kind: kind, + sources: module_sources, + export: new_export, + recompile?: function_exported?(module, :__mix_recompile__?, 0) + ) + + modules = prepend_or_merge(modules, module, module(:module), module, existing_module?) + {modules, exports, [source | sources], pending_modules, pending_exports} + end + + defp recompile_module?(module, recompile?) do + recompile? and Code.ensure_loaded?(module) and + function_exported?(module, :__mix_recompile__?, 0) and + module.__mix_recompile__?() + end + + defp prepend_or_merge(collection, key, pos, value, true) do + List.keystore(collection, key, pos, value) + end + + defp prepend_or_merge(collection, _key, _pos, value, false) do + [value | collection] + end + + defp detect_kind(module) do + protocol_metadata = Module.get_attribute(module, :__impl__) + + cond do + is_list(protocol_metadata) and protocol_metadata[:protocol] -> + {:impl, protocol_metadata[:protocol]} + + is_list(Module.get_attribute(module, :__protocol__)) -> + :protocol + + true -> + :module + end + end + + defp process_external_resources(external, cwd) do + for file <- external do + {Path.relative_to(file, cwd), File.exists?(file)} + end + end end From 778d876ee11edc2ad9e653fba0553c0dd4fde60b Mon Sep 17 00:00:00 2001 From: ruslandoga <67764432+ruslandoga@users.noreply.github.com> Date: Mon, 8 Aug 2022 09:46:08 +0300 Subject: [PATCH 57/59] typo Where -> where (#12059) --- lib/elixir/lib/task.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index f530b0cf9c4..c58181aaed7 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -212,7 +212,7 @@ defmodule Task do The list of callers of the current process can be retrieved from the Process dictionary with `Process.get(:"$callers")`. This will return either `nil` or - a list `[pid_n, ..., pid2, pid1]` with at least one entry Where `pid_n` is + a list `[pid_n, ..., pid2, pid1]` with at least one entry where `pid_n` is the PID that called the current process, `pid2` called `pid_n`, and `pid2` was called by `pid1`. From 4e17aedeb92006681bdcdecfa916c39fd62c9ed3 Mon Sep 17 00:00:00 2001 From: ruslandoga <67764432+ruslandoga@users.noreply.github.com> Date: Mon, 8 Aug 2022 09:46:56 +0300 Subject: [PATCH 58/59] Improve wording in Task docs (#12060) --- lib/elixir/lib/task.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index c58181aaed7..5aaba97d67b 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -84,7 +84,7 @@ defmodule Task do Here is a summary: * Using `Task.Supervisor.start_child/2` allows you to start a fire-and-forget - task that you don't care about its results or if it completes successfully or not. + task when you don't care about its results or if it completes successfully or not. * Using `Task.Supervisor.async/2` + `Task.await/2` allows you to execute tasks concurrently and retrieve its result. If the task fails, @@ -96,8 +96,8 @@ defmodule Task do the caller won't fail. You will receive the error reason either on `yield` or `shutdown`. - Furthermore, the supervisor guarantee all tasks first terminate, within a - configurable shutdown period, when your application shuts down. See the + Furthermore, the supervisor guarantees all tasks terminate within a + configurable shutdown period when your application shuts down. See the `Task.Supervisor` module for details on the supported operations. ### Distributed tasks From 64580b10e2c84c9d8571ef9cef2e5b4e4b9d2755 Mon Sep 17 00:00:00 2001 From: ruslandoga <67764432+ruslandoga@users.noreply.github.com> Date: Mon, 8 Aug 2022 09:48:57 +0300 Subject: [PATCH 59/59] Improve docs in Task (#12058) --- lib/elixir/lib/task.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 5aaba97d67b..3c4f7f8aa43 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -78,8 +78,8 @@ defmodule Task do |> Task.await() We encourage developers to rely on supervised tasks as much as possible. - Supervised tasks improves the visibility of how many tasks are running - at a given moment and enable a huge variety of patterns that gives you + Supervised tasks improve the visibility of how many tasks are running + at a given moment and enable a variety of patterns that give you explicit control on how to handle the results, errors, and timeouts. Here is a summary: