Skip to content

Elixir does not properly import/override custom @ inside a quote. #10497

@Qqwy

Description

@Qqwy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions