diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 7c8e8545494..096408b177f 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6647,16 +6647,26 @@ defmodule Kernel do end defp compile_regex(binary_or_tuple, options) do - # TODO: Remove this when we require Erlang/OTP 28+ - case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do + bin_opts = :binary.list_to_bin(options) + + # TODO: Remove this when we require Erlang/OTP 28.1+ + case is_binary(binary_or_tuple) and compile_time_regexes_supported?() do true -> - Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options))) + Macro.escape(Regex.compile!(binary_or_tuple, bin_opts)) false -> - quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options)))) + quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts))) end end + defp compile_time_regexes_supported? do + # OTP 28.0 introduced refs in patterns, which can't be used in AST anymore + # OTP 28.1 introduced :re.import/1 which allows us to fix this in Macro.escape + :erlang.system_info(:otp_release) < [?2, ?8] or + (Code.ensure_loaded?(:re) and + function_exported?(:re, :import, 1)) + end + @doc ~S""" Handles the sigil `~D` for dates. diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 8ca5a226d34..1d6c2abf939 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -1000,4 +1000,11 @@ defmodule Regex do defp translate_options(<<>>, acc), do: acc defp translate_options(t, _acc), do: {:error, t} + + if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do + def __expand_compile__(%Regex{source: source, opts: opts}, :re_pattern) do + {:ok, exported} = :re.compile(source, [:export] ++ opts) + quote do: :re.import(unquote(Macro.escape(exported))) + end + end end diff --git a/lib/elixir/src/elixir_map.erl b/lib/elixir/src/elixir_map.erl index c9d9e3e1db4..704833eadbb 100644 --- a/lib/elixir/src/elixir_map.erl +++ b/lib/elixir/src/elixir_map.erl @@ -16,7 +16,42 @@ expand_map(Meta, [{'|', _, [_, _]}] = Args, _S, #{context := Context, file := Fi expand_map(Meta, Args, S, E) -> {EArgs, SE, EE} = elixir_expand:expand_args(Args, S, E), validate_kv(Meta, EArgs, Args, E), - {{'%{}', Meta, EArgs}, SE, EE}. + PArgs = post_process_map_args(EArgs, E), + {{'%{}', Meta, PArgs}, SE, EE}. + +post_process_map_args(Args, E) -> + % TODO rewrite with maybe once we require OTP 2X+ + case lists:keyfind('__expand_compile__', 2, Args) of + {_Key, '__expand_compile__'} -> + case lists:keyfind('__struct__', 1, Args) of + {'__struct__', Module} -> + case code:ensure_loaded(Module) == {module, Module} andalso erlang:function_exported(Module, '__expand_compile__', 2) andalso is_literal(Args) of + true -> + case E of + #{context := nil} -> + Map = eval_literal({'%{}', [], Args}), + lists:map(fun + ({Key, '__expand_compile__'}) -> {Key, Module:'__expand_compile__'(Map, Key)}; + ({Key, Value}) -> {Key, Value} + end, Args); + #{context := match} -> + % replace it by wildcards in matches + lists:map(fun + ({Key, '__expand_compile__'}) -> {Key, {'_,', [], nil}}; + ({Key, Value}) -> {Key, Value} + end, Args); + _ -> + Args + end; + + false -> + Args + end; + _ -> Args + end; + false -> + Args + end. expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = E) -> CleanMapArgs = delete_struct_key(Meta, MapArgs, E), @@ -103,11 +138,19 @@ validate_not_repeated(Meta, Key, Used, E) -> Used end. +is_literal({'{}', _, List}) when is_list(List) -> lists:all(fun is_literal/1, List); +is_literal({'%{}', _, List}) when is_list(List) -> lists:all(fun is_literal/1, List); is_literal({_, _, _}) -> false; is_literal({Left, Right}) -> is_literal(Left) andalso is_literal(Right); is_literal([_ | _] = List) -> lists:all(fun is_literal/1, List); is_literal(_) -> true. +eval_literal({'{}', _, List}) when is_list(List) -> list_to_tuple(eval_literal(List)); +eval_literal({'%{}', _, List}) when is_list(List) -> maps:from_list(eval_literal(List)); +eval_literal({Left, Right}) -> {eval_literal(Left), eval_literal(Right)}; +eval_literal([_ | _] = List) -> lists:map(fun eval_literal/1, List); +eval_literal(Other) when is_binary(Other); is_atom(Other); is_number(Other); is_pid(Other); Other == [] -> Other. + validate_kv(Meta, KV, Original, #{context := Context} = E) -> lists:foldl(fun ({K, _V}, {Index, Used}) -> diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 4260985f28b..9cb2c5dbe3a 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -211,9 +211,20 @@ escape_map_key_value(K, V, Map, Q) -> end, if is_reference(MaybeRef) -> - argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (", - ('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ", - "(it must be defined within a function instead). ", (bad_escape_hint())/binary>>); + ExpandCompile = case Map of + #{'__struct__' := Module} -> + code:ensure_loaded(Module) == {module, Module} andalso erlang:function_exported(Module, '__expand_compile__', 2); + _ -> + false + end, + case ExpandCompile of + true -> + {do_quote(K, Q), '__expand_compile__'}; + false -> + argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (", + ('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ", + "(it must be defined within a function instead). ", (bad_escape_hint())/binary>>) + end; true -> {do_quote(K, Q), do_quote(V, Q)} end. diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index 3c8944defde..94c802b3922 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -141,11 +141,26 @@ defmodule MacroTest do assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]} end - test "inspects container when a reference cannot be escaped" do + @tag skip: System.otp_release() < "28" or function_exported?(:re, :import, 1) + test "escape container when a reference cannot be escaped" do assert_raise ArgumentError, ~r"~r/foo/ contains a reference", fn -> Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}}) end end + + @tag skip: not function_exported?(:re, :import, 1) + test "escape regex will remove references and replace it by a call to :re.import/1" do + assert { + :%{}, + [], + [ + __struct__: Regex, + opts: [], + re_pattern: :__expand_compile__, + source: "foo" + ] + } = Macro.escape(%{~r/foo/ | re_pattern: {:re_pattern, 0, 0, 0, make_ref()}}) + end end describe "expand_once/2" do