-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Description
Environment
Tested on OTP 23 with Elixir 1.10.3, 1.11.2 and the current master (Elixir 1.12.0-dev (7edb223) )
- Elixir & Erlang/OTP versions (elixir --version):
- Operating system:
Current behavior
Minimal example:
defmodule AtMacro do
defmacro __using__(_opts) do
quote do
import Kernel, except: [@: 1]
import AtMacro, only: [@: 1]
end
end
import Kernel, except: [@: 1]
defmacro @ast do
IO.puts(Macro.to_string(ast))
case ast do
{name, _, expr} when name in ~w[type!]a ->
IO.inspect("Special type handling here.")
_ ->
quote do
Kernel.@(unquote(ast))
end
end
end
end
defmodule AtExample do
use AtMacro
@doc "Supported like normal"
@type! "This is printed"
@type! this is printed too!
@type! this :: is a problem?
end
defmodule MetaMacro do
defmacro my_macro(name) do
quote location: :keep do
use AtMacro
# Errors occur here.
# For some reason Kernel.@/1 is used
# although we clearly override it just like before.
@doc "Supported like normal"
@type! "This is printed"
@type! this is printed too!
@type! this :: is a problem?
end
end
end
defmodule AtMetaExample do
require MetaMacro
MetaMacro.my_macro("hi")
end
This fails to compile with the following stacktrace:
== Compilation error in file lib/at_example.ex ==
** (CompileError) lib/at_example.ex:43: undefined function this/1
(stdlib 3.13) lists.erl:1354: :lists.mapfoldl/3
(stdlib 3.13) lists.erl:1355: :lists.mapfoldl/3
(elixir 1.11.2) expanding macro: Kernel.@/1
lib/at_example.ex:51: AtMetaExample (module)
expanding macro: MetaMacro.my_macro/1
lib/at_example.ex:51: AtMetaExample (module)
Expected behavior
I would expect this code to compile, and AtMetaExample
to behave identically as AtExample
.
It seems like inside the quote in MetaMacro.my_macro/1
we should be calling AtMacro.@/1
but Elixir is calling Kernel.@/1
instead, even though the use AtMacro
-statement should have overridden this.
Replacing the breaking @
-statements inside the quote by a call to IO.inspect(__ENV__, structs: false, limit: :infinity)
prints the following:
%{
__struct__: Macro.Env,
aliases: [],
context: nil,
context_modules: [AtMetaExample, MetaMacro, AtExample, AtMacro],
contextual_vars: [],
current_vars: {%{}, %{}},
file: "/run/media/qqwy/Serendipity/Programming/Personal/elixir/type_check_example/lib/at_example.ex",
function: nil,
functions: [
{Kernel,
[
!=: 2,
!==: 2,
*: 2,
+: 1,
+: 2,
++: 2,
-: 1,
-: 2,
--: 2,
/: 2,
<: 2,
<=: 2,
==: 2,
===: 2,
=~: 2,
>: 2,
>=: 2,
abs: 1,
apply: 2,
apply: 3,
binary_part: 3,
bit_size: 1,
byte_size: 1,
ceil: 1,
div: 2,
elem: 2,
exit: 1,
floor: 1,
function_exported?: 3,
get_and_update_in: 3,
get_in: 2,
hd: 1,
inspect: 1,
inspect: 2,
is_atom: 1,
is_binary: 1,
is_bitstring: 1,
is_boolean: 1,
is_float: 1,
is_function: 1,
is_function: 2,
is_integer: 1,
is_list: 1,
is_map: 1,
is_map_key: 2,
is_number: 1,
is_pid: 1,
is_port: 1,
is_reference: 1,
is_tuple: 1,
length: 1,
macro_exported?: 3,
make_ref: 0,
map_size: 1,
max: 2,
min: 2,
node: 0,
node: 1,
not: 1,
pop_in: 2,
put_elem: 3,
put_in: 3,
rem: 2,
round: 1,
self: 0,
send: 2,
spawn: 1,
spawn: 3,
spawn_link: 1,
spawn_link: 3,
spawn_monitor: 1,
spawn_monitor: 3,
struct: 1,
struct: 2,
struct!: 1,
struct!: 2,
throw: 1,
tl: 1,
trunc: 1,
tuple_size: 1,
update_in: 3
]}
],
lexical_tracker: #PID<0.180.0>,
line: 53,
macro_aliases: [],
macros: [
{AtMacro, [@: 1]},
{Kernel,
[
!: 1,
&&: 2,
..: 2,
<>: 2,
alias!: 1,
and: 2,
binding: 0,
binding: 1,
def: 1,
def: 2,
defdelegate: 2,
defexception: 1,
defguard: 1,
defguardp: 1,
defimpl: 2,
defimpl: 3,
defmacro: 1,
defmacro: 2,
defmacrop: 1,
defmacrop: 2,
defmodule: 2,
defoverridable: 1,
defp: 1,
defp: 2,
defprotocol: 2,
defstruct: 1,
destructure: 2,
get_and_update_in: 2,
if: 2,
in: 2,
is_exception: 1,
is_exception: 2,
is_nil: 1,
is_struct: 1,
is_struct: 2,
match?: 2,
or: 2,
pop_in: 1,
put_in: 2,
raise: 1,
raise: 2,
reraise: 2,
reraise: 3,
sigil_C: 2,
sigil_D: 2,
sigil_N: 2,
sigil_R: 2,
sigil_S: 2,
sigil_T: 2,
sigil_U: 2,
sigil_W: 2,
sigil_c: 2,
sigil_r: 2,
sigil_s: 2,
sigil_w: 2,
to_char_list: 1,
to_charlist: 1,
to_string: 1,
unless: 2,
update_in: 2,
use: 1,
use: 2,
var!: 1,
var!: 2,
|>: 2,
||: 2
]}
],
module: AtMetaExample,
prematch_vars: :warn,
requires: [Application, AtMacro, Kernel, Kernel.Typespec, MetaMacro],
tracers: [:elixir_lexical, Mix.Compilers.ApplicationTracer],
unused_vars: {%{}, 0},
vars: []
}
. The macros:
-field contains, as expected, {AtMacro, [@: 1]}
and this entry is missing from the Kernel
module.
Nonetheless, it seems like some kind of edge case is triggered here where the environment is ignored and Kernel.@/1
is still used instead.
For context: I encountered this issue while working on the runtime type-checking library TypeCheck, which overrides @
to parse Elixir's builtin typespec syntax when called as @type!
or @spec!
. This works fine in normal code but when someone tries to use it from within a macro we run into this issue. See Qqwy/elixir-type_check#36 for where it originally came up.