From 64220bf10e5782cd3a314f70fe14fbd80153c3f7 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 09:30:18 +0900 Subject: [PATCH 1/7] Add 'e' modifier to Regex for :export opt --- lib/elixir/lib/inspect.ex | 1 + lib/elixir/lib/regex.ex | 19 +++++++++++++++++-- lib/elixir/test/elixir/inspect_test.exs | 1 + lib/elixir/test/elixir/regex_test.exs | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 4362ae2c5ae..2c69b325cf2 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -567,6 +567,7 @@ defimpl Inspect, for: Regex do defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc]) defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc]) defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc]) + defp translate_options([:export | t], acc), do: translate_options(t, [?e | acc]) defp translate_options([], acc), do: acc defp translate_options(_t, _acc), do: :error diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 836f2c62de1..d687ee60e86 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -101,6 +101,10 @@ defmodule Regex do * `:ungreedy` (U) - inverts the "greediness" of the regexp (the previous `r` option is deprecated in favor of `U`) + * `:export` (e) (from Erlang OTP 28.1 and Elixir 1.20) - uses an exported pattern + which can be shared across nodes or through config, at the cost of a runtime + overhead every time to re-import it every time it is executed. + ## Captures Many functions in this module handle what to capture in a regex @@ -515,7 +519,7 @@ defmodule Regex do """ @spec names(t) :: [String.t()] def names(%Regex{re_pattern: re_pattern}) do - {:namelist, names} = :re.inspect(re_pattern, :namelist) + {:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist) names end @@ -585,10 +589,16 @@ defmodule Regex do %Regex{source: source, opts: compile_opts} = regex :re.run(string, source, compile_opts ++ options) else - _ -> :re.run(string, re_pattern, options) + _ -> :re.run(string, maybe_import_pattern(re_pattern), options) end end + @compile {:inline, maybe_import_pattern: 1} + defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported), + do: :re.import(exported) + + defp maybe_import_pattern(pattern), do: pattern + @typedoc """ Options for regex functions that capture matches. """ @@ -1007,6 +1017,8 @@ defmodule Regex do translate_options(t, [:ungreedy | acc]) end + defp translate_options(<>, acc), do: translate_options(t, [:export | acc]) + defp translate_options(<<>>, acc), do: acc defp translate_options(t, _acc), do: {:error, t} @@ -1022,6 +1034,9 @@ defmodule Regex do :erlang.system_info(:otp_release) < [?2, ?8] -> Macro.escape(regex.re_pattern) + :lists.member(:export, regex.opts) -> + Macro.escape(regex.re_pattern) + # OTP 28.1+ introduced the ability to export and import regexes from compiled binaries Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) -> {:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts) diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index be589836b33..eab8fe325ea 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -891,6 +891,7 @@ defmodule Inspect.OthersTest do test "regex" do assert inspect(~r(foo)m) == "~r/foo/m" + assert inspect(~r(foo)e) == "~r/foo/e" assert inspect(~r[\\\#{2,}]iu) == ~S"~r/\\\#{2,}/iu" assert inspect(Regex.compile!("a\\/b")) == "~r/a\\/b/" diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 1145b0d0a34..aed5880cc9f 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -52,6 +52,20 @@ defmodule RegexTest do assert Regex.match?(~r/^b$/m, "a\nb\nc") end + @tag :re_import + test "export" do + # exported patterns have no structs, so these are structurally equal + assert ~r/foo/e == Regex.compile!("foo", [:export]) + + assert Regex.match?(~r/foo/e, "foo") + refute Regex.match?(~r/foo/e, "Foo") + + assert Regex.run(~r/c(d)/e, "abcd") == ["cd", "d"] + assert Regex.run(~r/e/e, "abcd") == nil + + assert Regex.names(~r/(?foo)/e) == ["FOO"] + end + test "precedence" do assert {"aa", :unknown} |> elem(0) =~ ~r/(a)\1/ end From ef342686acd03688a8b7602c0b3a150ed20a488c Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 18:41:58 +0900 Subject: [PATCH 2/7] e -> E --- lib/elixir/lib/inspect.ex | 2 +- lib/elixir/lib/regex.ex | 2 +- lib/elixir/test/elixir/inspect_test.exs | 2 +- lib/elixir/test/elixir/regex_test.exs | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 2c69b325cf2..169ddfac73a 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -567,7 +567,7 @@ defimpl Inspect, for: Regex do defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc]) defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc]) defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc]) - defp translate_options([:export | t], acc), do: translate_options(t, [?e | acc]) + defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc]) defp translate_options([], acc), do: acc defp translate_options(_t, _acc), do: :error diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index d687ee60e86..765280b1175 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -1017,7 +1017,7 @@ defmodule Regex do translate_options(t, [:ungreedy | acc]) end - defp translate_options(<>, acc), do: translate_options(t, [:export | acc]) + defp translate_options(<>, acc), do: translate_options(t, [:export | acc]) defp translate_options(<<>>, acc), do: acc defp translate_options(t, _acc), do: {:error, t} diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index eab8fe325ea..863b8e9f332 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -891,7 +891,7 @@ defmodule Inspect.OthersTest do test "regex" do assert inspect(~r(foo)m) == "~r/foo/m" - assert inspect(~r(foo)e) == "~r/foo/e" + assert inspect(~r(foo)E) == "~r/foo/E" assert inspect(~r[\\\#{2,}]iu) == ~S"~r/\\\#{2,}/iu" assert inspect(Regex.compile!("a\\/b")) == "~r/a\\/b/" diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index aed5880cc9f..1fed09c2480 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -55,15 +55,15 @@ defmodule RegexTest do @tag :re_import test "export" do # exported patterns have no structs, so these are structurally equal - assert ~r/foo/e == Regex.compile!("foo", [:export]) + assert ~r/foo/E == Regex.compile!("foo", [:export]) - assert Regex.match?(~r/foo/e, "foo") - refute Regex.match?(~r/foo/e, "Foo") + assert Regex.match?(~r/foo/E, "foo") + refute Regex.match?(~r/foo/E, "Foo") - assert Regex.run(~r/c(d)/e, "abcd") == ["cd", "d"] - assert Regex.run(~r/e/e, "abcd") == nil + assert Regex.run(~r/c(d)/E, "abcd") == ["cd", "d"] + assert Regex.run(~r/e/E, "abcd") == nil - assert Regex.names(~r/(?foo)/e) == ["FOO"] + assert Regex.names(~r/(?foo)/E) == ["FOO"] end test "precedence" do From cd7945521df7202a75c48036d3270498c8b93103 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 20:10:03 +0900 Subject: [PATCH 3/7] Fix CI and defer compilation of E regex on older versions --- lib/elixir/lib/kernel.ex | 10 +++++++++- lib/elixir/test/elixir/inspect_test.exs | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index c2476ea290a..1fefa4739af 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6645,7 +6645,15 @@ defmodule Kernel do defp compile_regex(binary_or_tuple, options) do bin_opts = :binary.list_to_bin(options) - case is_binary(binary_or_tuple) do + # TODO: Remove the second member when we require Erlang/OTP 28+ + # We defer to a runtime compilation if using the :export flag on older OTP version + # that don't support it. + compile_time = + is_binary(binary_or_tuple) and + (:binary.match(bin_opts, "E") == :nomatch or + (Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1))) + + case compile_time do true -> Macro.escape(Regex.compile!(binary_or_tuple, bin_opts)) diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 863b8e9f332..0a23b6aa1fc 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -891,7 +891,6 @@ defmodule Inspect.OthersTest do test "regex" do assert inspect(~r(foo)m) == "~r/foo/m" - assert inspect(~r(foo)E) == "~r/foo/E" assert inspect(~r[\\\#{2,}]iu) == ~S"~r/\\\#{2,}/iu" assert inspect(Regex.compile!("a\\/b")) == "~r/a\\/b/" @@ -907,6 +906,11 @@ defmodule Inspect.OthersTest do assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])' end + @tag :re_import + test "exported regex" do + assert inspect(~r(foo)E) == "~r/foo/E" + end + test "inspect_fun" do fun = fn integer, _opts when is_integer(integer) -> From 1b009af01341bd1993c2a3c02358b0f3b02eb9ae Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 20:34:07 +0900 Subject: [PATCH 4/7] Add guidance in error message for regexes in config --- lib/mix/lib/mix/tasks/compile.app.ex | 8 ++++++ lib/mix/test/mix/tasks/compile.app_test.exs | 27 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index cc62e464c92..79876339538 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -217,6 +217,14 @@ defmodule Mix.Tasks.Compile.App do [?[, to_erl_head(list), ?]] end + defp to_erl_term(%Regex{re_pattern: {:re_pattern, _, _, _, ref}} = regex) + when is_reference(ref) do + Mix.raise(""" + \"def application\" has a term which cannot be written to .app files: #{inspect(regex)}. + Use the E modifier to store regexes in application config. + """) + end + defp to_erl_term(map) when is_map(map) do inner = Enum.map_intersperse( diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 2bd9b6711de..8d6f9343f95 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -263,6 +263,33 @@ defmodule Mix.Tasks.Compile.AppTest do end) end + @tag :re_import + test "accepts only regexes without a reference" do + in_fixture("no_mixfile", fn -> + Mix.Project.push(CustomProject) + + Process.put(:application, env: [regex: ~r/foo/]) + + message = """ + "def application" has a term which cannot be written to \.app files: ~r\/foo\/. + Use the E modifier to store regexes in application config. + """ + + assert_raise Mix.Error, message, fn -> + Mix.Tasks.Compile.App.run([]) + end + + Process.put(:application, env: [exported: ~r/foo/E]) + + Mix.Tasks.Compile.Elixir.run([]) + Mix.Tasks.Compile.App.run([]) + + properties = parse_resource_file(:custom_project) + + assert properties[:env] == [exported: ~r/foo/E] + end) + end + test ".app contains description and registered (as required by systools)" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) From ddfc0800c329a3b9e23e6762a4e77edba116e85f Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 20:48:18 +0900 Subject: [PATCH 5/7] Make E a no-op on older OTP versions --- lib/elixir/lib/kernel.ex | 10 +--------- lib/elixir/lib/regex.ex | 14 ++++++++++++-- lib/elixir/test/elixir/inspect_test.exs | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 1fefa4739af..c2476ea290a 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6645,15 +6645,7 @@ defmodule Kernel do defp compile_regex(binary_or_tuple, options) do bin_opts = :binary.list_to_bin(options) - # TODO: Remove the second member when we require Erlang/OTP 28+ - # We defer to a runtime compilation if using the :export flag on older OTP version - # that don't support it. - compile_time = - is_binary(binary_or_tuple) and - (:binary.match(bin_opts, "E") == :nomatch or - (Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1))) - - case compile_time do + case is_binary(binary_or_tuple) do true -> Macro.escape(Regex.compile!(binary_or_tuple, bin_opts)) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 765280b1175..dfffbde2999 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -101,9 +101,12 @@ defmodule Regex do * `:ungreedy` (U) - inverts the "greediness" of the regexp (the previous `r` option is deprecated in favor of `U`) - * `:export` (e) (from Erlang OTP 28.1 and Elixir 1.20) - uses an exported pattern + * `:export` (E) (since Elixir 1.20) - uses an exported pattern which can be shared across nodes or through config, at the cost of a runtime overhead every time to re-import it every time it is executed. + This modifier only has an effect starting on Erlang/OTP 28, and it is ignored + on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot + and do not need to be exported in order to be shared in these versions. ## Captures @@ -1017,7 +1020,14 @@ defmodule Regex do translate_options(t, [:ungreedy | acc]) end - defp translate_options(<>, acc), do: translate_options(t, [:export | acc]) + defp translate_options(<>, acc) do + # on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed + # (regexes aren't using references and can be shared across nodes or stored in config) + case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do + true -> translate_options(t, [:export | acc]) + false -> translate_options(t, acc) + end + end defp translate_options(<<>>, acc), do: acc defp translate_options(t, _acc), do: {:error, t} diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 0a23b6aa1fc..67a1544c9e6 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -908,7 +908,7 @@ defmodule Inspect.OthersTest do @tag :re_import test "exported regex" do - assert inspect(~r(foo)E) == "~r/foo/E" + assert inspect(~r/foo/E) == "~r/foo/E" end test "inspect_fun" do From 2d30d5fc26ae7ca9647b1d68fe4b8cb53a722e2c Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Thu, 13 Nov 2025 21:05:55 +0900 Subject: [PATCH 6/7] Add TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/elixir/lib/regex.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index dfffbde2999..805da91a60b 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -1023,6 +1023,7 @@ defmodule Regex do defp translate_options(<>, acc) do # on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed # (regexes aren't using references and can be shared across nodes or stored in config) + # TODO: remove this check on Erlang/OTP 28+ and update docs case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do true -> translate_options(t, [:export | acc]) false -> translate_options(t, acc) From 187b7201de355e103036ddfb20e963d737af36e4 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 13 Nov 2025 21:16:42 +0900 Subject: [PATCH 7/7] Ignore :re_import in mix tests too --- lib/mix/test/test_helper.exs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index e6bce9251b0..671e4e0f5b1 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -43,12 +43,22 @@ cover_exclude = [] end +# OTP 28.1+ +re_import_exclude = + if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do + [] + else + [:re_import] + end + Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__) CoverageRecorder.maybe_record("mix") ExUnit.start( trace: !!System.get_env("TRACE"), - exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude, + exclude: + epmd_exclude ++ + os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude, include: line_include, assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300")) )