Unquoute not working for maps even with Macro.escape

I have the following code:

defmodule Esperanto.Olx.Parsers.CorrectChoice do
  use Esperanto.Parsers.Generics.EnclosingTag,
    start_delimiter: ~r/\(x\)/,
    end_delimiter: ~r/^\n/,
    enclosing_tag: :choice,
    attrs: %{:foo => "bar"}
end
  defmacro __using__(options) do
    start_delimiter = Keyword.get(options, :start_delimiter)
    end_delimiter = Keyword.get(options, :end_delimiter)
    tag = Keyword.get(options, :enclosing_tag)
    attrs = Keyword.get(options, :attrs, %{})

    quote do
      require Logger
      @behaviour Esperanto.Parser

      @start_delimiter unquote(start_delimiter)
      @end_delimiter unquote(end_delimiter)
      @attrs Keyword.get(unquote(options), :attrs, %{})
      @attrs_not_working unquote(Macro.escape(attrs))

      @impl Esperanto.Parser
      def parse(walker, tree, parent_id, opts) do
        IO.inspect(@attrs, label: :attrs)
        IO.inspect(@attrs_not_working, label: :attrs_not_working)

This code prints:

attrs: %{correct: false}
attrs_not_working: {:%{}, [line: 10], [correct: false]}

attrs_not_working should be %{correct: false}, no? if no, what’s the correctly way to do it

The options you receive are quoted but the default value isn’t quoted. So you could do this:

attrs = Keyword.get(options, :attrs, Macro.escape(%{}))

But it is better to avoid work outside of quote, so do this:

  defmacro __using__(options) do
    quote bind_quoted: [options: options] do
      require Logger
      @behaviour Esperanto.Parser

      @start_delimiter Keyword.get(options, :start_delimiter)
      @end_delimiter Keyword.get(options, :end_delimiter)
      @attrs Keyword.get(options, :attrs, %{})
3 Likes

Hi @josevalim, Thanks for the reply!

I will definitely go with binds quote. However, even when Macro.escape(%{}) is used the same error occurs. I really want to understand why, do you have any clues?

The error should not occur with Macro.escape as long as you don’t escape it again.

So maybe this is a bug?

I’ve create a minimal example:


defmodule Foo do
  defmacro __using__(options) do
    attrs = Keyword.get(options, :attrs, Macro.escape(%{}))
    quote do
      def show, do: IO.inspect(unquote(attrs))
    end
  end
end

defmodule MacroBug do

  use Foo,
    attrs: Macro.escape(%{:correct => true})
end

result:

iex(5)> MacroBug.show
{:%{}, [], [correct: true]}
{:%{}, [], [correct: true]}

You’re double escaping which is what Jose meant by “don’t escape it again”. This works correctly:

defmodule Foo do
  defmacro __using__(options) do
    attrs = Keyword.get(options, :attrs, Macro.escape(%{}))
    quote do
      def show, do: IO.inspect(unquote(attrs))
    end
  end
end

defmodule MacroBug do

  use Foo,
    attrs: %{:correct => true}
end
iex(7)> MacroBug.show
%{correct: true}
%{correct: true}
4 Likes

I get it! But why the map %{:correct => true} does not need to escape and the %{} need?

because there you’re inside defmacro, The escape has nothing to with the empty map.

Outside you can

use Foo, attrs: %{}

Yeah it’ll be more easy to understand if you do:

defmodule Foo do
  defmacro __using__(options) do
    options |> IO.inspect
    attrs = Keyword.get(options, :attrs, Macro.escape(%{}))
    quote do
      def show, do: IO.inspect(unquote(attrs))
    end
  end
end

What you’ll see is that options comes through with {:%{}, [], [correct: true]} because it’s coming in as AST. So your issue is with the default value you have on Keyword.get. If no options are supplied you were returning an actual map, not map AST.