-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add local_for_callback option to Macro.Env.expand_import #14620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add local_for_callback option to Macro.Env.expand_import #14620
Conversation
lib/elixir/lib/macro/env.ex
Outdated
end | ||
|
||
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do | ||
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace, local_for_callback) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of adding a new option, we can augment allow_locals
. If it is false, it is disabled, if it is true, it is the default implementation (elixir_locals
), otherwise it is a custom function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done on the erlang side. Do you also want that on the elixir API side?
[] | ||
else | ||
case allow_locals and function_exported?(module, :__info__, 1) do | ||
true -> [{module, module.__info__(:macros)}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be moved inside the local_for_callback
function, threfore we don't need to pass either extra or allow_locals as argument to elixir_dispatch.expand_import
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you like me to remove extra
argument from the dispatch call and instead pass a resolver callback using module.__info__(:macros)
or macro_exported?
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is possible, yes, it would be perfect.
lib/elixir/src/elixir_dispatch.erl
Outdated
erlang:raise(Kind, Reason, prune_stacktrace(Stacktrace, MFA, Info, {ok, Caller})) | ||
%% Check if Fun is actually a function, as it might be a fake value for local macros | ||
%% when using custom local_for_callback | ||
case is_function(Fun) of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should enforce it to either be Fun
or false
. What is the use case where you need it to be a separate value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed a way of distinguishing between
false
- local function or not found
function - expandable macro
truthy value - fake macro
The code in :elixir_dispatch
relies on the result being truthy for local macros. Originally I wanted to return a dummy macro from my callback but that is not easy to do or at least I couldn't find an easy way. I would have to generate a function of a particular arity in runtime or pregenerate multiple arity variants at compile time and keep them in dictionary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would you return in those cases? Could you instead return false and match on the result of the expand_import function returning not_found?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that returning false
changes the behavior of the caller code in
elixir/lib/elixir/src/elixir_dispatch.erl
Lines 77 to 84 in b29c83a
case (Function /= nil) andalso (Function /= Tuple) andalso | |
elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E) of | |
false -> | |
elixir_env:trace({local_function, Meta, Name, Arity}, E), | |
{local, Name, Arity}; | |
_ -> | |
false | |
end |
the emitted trace is then
local_function
instead of local_macro
and in
elixir/lib/elixir/src/elixir_dispatch.erl
Lines 175 to 191 in b29c83a
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 | |
%% the receiver is the same as module (happens on bootstrap). | |
{_, Receiver} when Local /= false, Receiver /= Module -> | |
{conflict, Receiver}; | |
%% There is no local. Dispatch the import. | |
_ when Local == false -> | |
do_expand_import(Dispatch, Meta, Name, Arity, Module, E, Trace); | |
%% Dispatch to the local. | |
_ -> | |
Trace andalso elixir_env:trace({local_macro, Meta, Name, Arity}, E), | |
{macro, Module, expander_macro_fun(Meta, Local, Module, Name, E)} | |
end |
local_macro
trace is not emitted and the code calls into do_expand_import
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. You can do this for now:
for arity <- 0..255 do
defp ok_fun(unquote(arity)), do: fn unquote_splicing(Macro.generate_arguments(arity, __MODULE__)) -> :ok end
end
And that will generate the functions that you need. We use a similar trick in Nx, perhaps we should add something to the stdlib specifically for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that hack works 👍
We can definitely get this in. I have dropped some comments, some of them are about refactoring the current code, but I can also do that after merging. |
Allow hooking into local macro resolution
a1349a8
to
997eb4c
Compare
lib/elixir/lib/macro/env.ex
Outdated
@@ -561,6 +562,12 @@ defmodule Macro.Env do | |||
* `:check_deprecations` - when set to `false`, does not check for deprecations | |||
when expanding macros | |||
* `:local_for_callback` - a function that receives the metadata, name, arity, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not add a new option, let's augment allow_locals
to be a function? Then we don't need to worry about how these two different APIs compose.
use one param
@josevalim I removed the
|
@lukaszsamson so please keep the extra for now. we can merge it and i will investigate it later :) |
This PR adds a new option
:local_for_callback
toMacro.Env.expand_import
in order to allow hooking into local macro resolution and providing a custom resolver.Rationale:
In ElixirSense minicompiler I need to resolve local macros using the metadata extracted from the AST. The current mechanism relying on
:elixir_def.local_for
is not going to work correctly when there is no real compilation in process. The current hacky code usingextra
filled with__info__(:macros)
only returns public macros and with that only those already compiled and loadable. The result is improper tracking of references in traversed code.Example code using the new option:
The PR is based on the current code used in ElixirSense. I'm opening it as a draft. If there is will to merge this I'll work on tests.
Related PR hooking into
define_import
:#13628