Skip to content

Conversation

sabiwara
Copy link
Contributor

@sabiwara sabiwara commented Aug 21, 2025

Take 2 for #14719.

Leverages :re.import added in erlang/otp#9976.

Here I went for Macro.escape rather than :elixir_expand, because :elixir_expand is harder to work with since we're already in AST land and the AST for a struct is hard to pattern-match on.

This works since Macro.escape is used in ~r, and also fixes the escaping issue discussed previously. But this is violating the assumption that escape emits AST for a literal, which is probably a no-go.

Will work on supporting the :export option separately since this is not strictly needed for this.

@josevalim
Copy link
Member

Notes to self: how we're going to check that the __escape__ function is defined?

  • function_exported? is not enough because there is no guarantee the struct module will be loaded. For example, module Bar.foo may do %Foo{} which is inlined as a map, so when calling Bar.foo, the Foo module is not loaded
  • Code.ensure_loaded? should be enough, however. If there is a struct, then definition its module is available
  • Code.ensure_compiled? is problematic, as it can lead to deadlocks in the compiler

@sabiwara sabiwara force-pushed the import-re2 branch 2 times, most recently from 14f9002 to 6cb4b82 Compare August 30, 2025 02:41
Comment on lines +1025 to +1033
# OTP 28.0 works in degraded mode performance-wise, we need to recompile from the source
true ->
quote do
{:ok, pattern} =
:re.compile(unquote(Macro.escape(regex.source)), unquote(Macro.escape(regex.opts)))

pattern
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was wondering. Perhaps we could/should warn here, so that people notice and pro-actively bump to OTP28.1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, warning sounds good to me!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum I tried e1907ea but it seems to cause some bootstrapping issues.

Not too familiar with IO.warn_once, maybe will give up for now 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's warn on boot in elixir.erl. Check if version is >= 28 and the import function is not available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to work, but would break too many integration tests in the CI?
d1536b4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sabiwara I think for CI we will run on 28.1. So perhaps we add the warning in a separate PR, which we will merge once we move CI to 28.1. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the warning separately sounds good! 💜

Comment on lines +175 to +185
maybe
#{'__struct__' := Module} ?= Map,
true ?= is_atom(Module),
{module, Module} ?= code:ensure_loaded(Module),
true ?= erlang:function_exported(Module, '__escape__', 1),
Module:'__escape__'(Map)
else
_ ->
TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))],
{'%{}', [], TT}
end;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: We can finally use maybe 🎉
It fits perfectly here.

Since we're requiring OTP26, there is no runtime flag anymore, just the module one.

end
end

@tag skip: not function_exported?(:re, :import, 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use exclude in the test/test_helper.exs instead, for consistency? We should probably do Code.ensure_loaded?(:re) as well...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean adding an otp_version_exclude here?
https://github.com/elixir-lang/elixir/blob/main/lib/elixir/test/elixir/test_helper.exs#L138-L139

I'm slightly concerned that the extra indirection would make it harder to follow, esp. if it is just used at one place?
But happy to do it if you think that's best.

We should probably do Code.ensure_loaded?(:re) as well.

Indeed, sorry about that. I'm starting to wish we had one function for this as suggested recently 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add one called re_import_exclude, yes, so we have all of them handled in a single place instead of scattered.

@sabiwara
Copy link
Contributor Author

I think we're good to go!

I was even able to apply it to a fine-based project I'm working on, which also has runtime refs, a text format and a binary format - it worked beautifully ✨
https://codeberg.org/sabiwara/gaia/src/branch/main/lib/gaia.ex#L313-L321
https://codeberg.org/sabiwara/gaia/src/branch/main/test/gaia_test.exs#L16-L25

@sabiwara sabiwara marked this pull request as ready for review August 30, 2025 06:12
@josevalim
Copy link
Member

Agreed, we just need docs and we can ship it! :)

@sabiwara
Copy link
Contributor Author

Agreed, we just need docs and we can ship it! :)

What's a good place for it? Macro.escape?

@josevalim
Copy link
Member

Macro.escape is a good call for now, yes!

@sabiwara
Copy link
Contributor Author

sabiwara commented Aug 30, 2025

Pushed some docs. It's a bit of a complex topic, which makes it tricky to explain simply.
Improvements and suggestions are welcome 🙂

Screenshot 2025-08-30 at 17 41 29 Screenshot 2025-08-30 at 17 41 47

Copy link
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment with a slightly different spin on the documentation. Feel free to use it or discard it as you prefer.

Co-authored-by: José Valim <[email protected]>
@sabiwara
Copy link
Contributor Author

I added a comment with a slightly different spin on the documentation. Feel free to use it or discard it as you prefer.

I love it 💜

@sabiwara sabiwara changed the title Use new OTP28.1 :re.import in escaped regex AST Define __escape__/1 and use it to fix Regex escaping in OTP28.1 Aug 30, 2025
@sabiwara sabiwara changed the title Define __escape__/1 and use it to fix Regex escaping in OTP28.1 Add __escape__/1 and use it to fix Regex escaping in OTP28.1 Aug 30, 2025
@sabiwara sabiwara merged commit 7a8dc78 into elixir-lang:main Aug 30, 2025
13 checks passed
@sabiwara sabiwara deleted the import-re2 branch August 30, 2025 09:15
@sabiwara
Copy link
Contributor Author

@josevalim OK to backport right? (since it's from 1.19)

@josevalim
Copy link
Member

Yes, 100%.

sabiwara added a commit that referenced this pull request Aug 30, 2025
@sabiwara
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants