jeffdeville

jeffdeville

Can't seem to unquote a map

In iex, this I can do this:

quote do
   unquote(%{})
end

And get the expected:

%{}

But for some reason, in the macro I’m working on, I’m getting: invalid quoted expression: %{}

defmodule ExModbus do
  defmacro __using__(_options) do
    quote do
      Module.register_attribute __MODULE__, :fields, accumulate: true, persist: false
      require Logger
      import unquote(__MODULE__), only: [field: 6, field: 7, to_bytes: 2, map_enum_value: 2]
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(env) do
    compile(Module.get_attribute(env.module, :fields))
  end

###########################

changing the default enum_map from to %{} results in the error

###########################
defmacro field(name, type, addr, num_bytes, perms, desc, enum_map \ %{}) do
quote bind_quoted: [name: name, type: type, addr: addr,
num_bytes: num_bytes, perms: perms, desc: desc, enum_map: enum_map] do
@fields {name, type, addr, num_bytes, perms, desc, enum_map}
end
end

  def compile(fields) do
    ast = for {name, type, addr, num_bytes, perms, desc, enum_map} <- fields do
      getter_ast = defgetter(name, type, addr, num_bytes, desc, enum_map)
      case perms do
        :r -> getter_ast
        :rw -> [defsetter(String.to_atom("set_" <> Atom.to_string(name)), type, addr, desc) | [getter_ast] ]
      end
    end

    quote do
      def field_defs(), do: @fields
      unquote(ast)
    end
  end

  def to_bytes(data, :int16), do: <<data::signed-integer-size(16)>>
  def to_bytes(data, :uint16), do: <<data::unsigned-integer-size(16)>>
  def to_bytes(data, :enum16, map), do: <<Map.get(map, data, data)::unsigned-integer-size(16)>>

  def defgetter(name, type, addr, num_bytes, desc, enum_map) do
    quote do
      @doc """
      # Description:

      #{unquote(desc)}

      * Field Type: #{unquote(type)}
      """
      # @spec unquote(name)(pid, integer) :: unquote(map_type(type))
      def unquote(name)(pid, slave_id \\ 1) do
        with {:ok,
                %{data: {:read_holding_registers, data},
                  transaction_id: transaction_id,
                  unit_id: unit_id}} <- ExModbus.Client.read_data(pid, slave_id, unquote(addr - 1), unquote(num_bytes)),
             {:ok, value} <- ExModbus.Types.convert_type(data, unquote(type)),
             {:ok, value} <- map_enum_value(unquote(enum_map), value)
        do
          {:ok, %{data: value, transaction_id: transaction_id, slave_id: unit_id}}
        else
          {:type_conversion_error, {data, type}} -> {:type_conversion_error, {data, type}}
          {:enum_not_found_error, message} -> {:enum_not_found_error, message}
        end
      end
    end
  end

  def defsetter(name, type, addr, desc) do
    quote do
      @doc """
      # Description:

      #{unquote(desc)}

      * Field Type: #{unquote(type)}
      """
      def unquote(name)(pid, slave_id, data) do
        case ExModbus.Client.write_multiple_registers(pid, slave_id, unquote(addr - 1), to_bytes(data, unquote(type))) do
          {:ok, %{data: {:write_multiple_registers, data}, transaction_id: transaction_id, unit_id: unit_id}} ->
            {:ok, %{data: data, transaction_id: transaction_id, slave_id: unit_id}}
          other ->
            IO.puts inspect other
        end
      end
    end
  end

  def map_enum_value([], value), do: {:ok, value}
  def map_enum_value(nil, value), do: {:ok, value}
  def map_enum_value(enum_map, value) do
    Map.get(enum_map, value, {:enum_not_found_error, "#{inspect enum_map} either has no member #{value}, or it is out of range"})
  end
end

Any thoughts as to why this might be? (also, if there’s a way to stitch this AST together a little better, please feel free to illuminate me!

Marked As Solved

josevalim

josevalim

Creator of Elixir

quote and unquote work on ASTs and maps are not valid ASTs. If you want to inject values, such as maps, tuples with any element and so on into an AST, you need to escape the value with Macro.escape/1.

10
Post #2

Also Liked

jeffdeville

jeffdeville

Perfect! Thank you @josevalim

In case anyone needs a working (if convoluted, and needing refactor) example later:

defmodule ExModbus do
  defmacro __using__(_options) do
    quote do
      Module.register_attribute __MODULE__, :fields, accumulate: true, persist: false
      require Logger
      import unquote(__MODULE__), only: [field: 6, field: 7, to_bytes: 2, map_enum_value: 2]
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(env) do
    compile(Module.get_attribute(env.module, :fields))
  end

  defmacro field(name, type, addr, num_bytes, perms, desc, enum_map \\ Macro.escape(nil)) do
    quote bind_quoted: [name: name, type: type, addr: addr,
                        num_bytes: num_bytes, perms: perms, desc: desc, enum_map: enum_map] do
      @fields {name, type, addr, num_bytes, perms, desc, enum_map}
    end
  end

  def compile(fields) do
    ast = for {name, type, addr, num_bytes, perms, desc, enum_map} <- fields do
      getter_ast = defgetter(name, type, addr, num_bytes, desc, enum_map)
      case perms do
        :r -> getter_ast
        :rw -> [defsetter(String.to_atom("set_" <> Atom.to_string(name)), type, addr, desc) | [getter_ast] ]
      end
    end

    quote do
      def field_defs(), do: @fields
      unquote(ast)
    end
  end

  def to_bytes(data, :int16), do: <<data::signed-integer-size(16)>>
  def to_bytes(data, :uint16), do: <<data::unsigned-integer-size(16)>>
  def to_bytes(data, :enum16, map), do: <<Map.get(map, data, data)::unsigned-integer-size(16)>>

  def defgetter(name, type, addr, num_bytes, desc, enum_map) do
    quote do
      @doc """
      # Description:

      #{unquote(desc)}

      * Field Type: #{unquote(type)}
      """
      # @spec unquote(name)(pid, integer) :: unquote(map_type(type))
      def unquote(name)(pid, slave_id \\ 1) do
        with {:ok,
                %{data: {:read_holding_registers, data},
                  transaction_id: transaction_id,
                  unit_id: unit_id}} <- ExModbus.Client.read_data(pid, slave_id, unquote(addr - 1), unquote(num_bytes)),
             {:ok, value} <- ExModbus.Types.convert_type(data, unquote(type)),
             value <- map_enum_value(unquote(Macro.escape(enum_map)), value)
        do
          {:ok, %{data: value, transaction_id: transaction_id, slave_id: unit_id}}
        else
          {:type_conversion_error, {data, type}} -> {:type_conversion_error, {data, type}}
          {:enum_not_found_error, message} -> {:enum_not_found_error, message}
        end
      end
    end
  end

  def defsetter(name, type, addr, desc) do
    quote do
      @doc """
      # Description:

      #{unquote(desc)}

      * Field Type: #{unquote(type)}
      """
      def unquote(name)(pid, slave_id, data) do
        case ExModbus.Client.write_multiple_registers(pid, slave_id, unquote(addr - 1), to_bytes(data, unquote(type))) do
          {:ok, %{data: {:write_multiple_registers, data}, transaction_id: transaction_id, unit_id: unit_id}} ->
            {:ok, %{data: data, transaction_id: transaction_id, slave_id: unit_id}}
          :ok -> :ok
        end
      end
    end
  end

  def map_enum_value(nil, value), do: value
  def map_enum_value(enum_map, value) do
    Map.get(enum_map, value, {:enum_not_found_error, "#{inspect enum_map} either has no member #{value}, or it is out of range"})
  end
end

Where Next?

Popular in Questions Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

We're in Beta

About us Mission Statement