Using `is_*` functions on AST nodes

Hello,
I’d like to use is_*\1 functions on the AST node (Generated by using Code.string_to_quoted\1), but invoking is_*\1 function directly on the node results in an incorrect result.

E.g. I have an AST node of the map - {:%{}, [line: 33], [a: :b]} and is_map returns false for this node.

I’m wondering whether is_*\1 could be used here or I should do the pattern matching.

Thanks for all the answers and sorry if this question is improper - I’m pretty new to Elixir :upside_down_face:

Hi @Deividas, the AST node itself is a tuple, perhaps you can achieve the goal by pattern-matching, a minimal example:

case Code.string_to_quoted!("%{a: :b}") do
  {:%{}, [line: _line], kv_list} ->
    IO.puts("is_map")
    # processing ...
end
1 Like

In that case, welcome! Particularly if you’re new I’d be cautious about some of the metaprogramming, you usually want to have a strong grasp of the regular language before heading over into meta land.

Can you talk a bit about your use case so that we can maybe suggest an alternative approach?

2 Likes

All you need to do is to handle all of the edge cases (in your case special forms), for example:

defmodule Example do
  def sample(ast, opts \\ [env: __ENV__])

  def sample({:%, _, [aliases, {:%{}, _, keyword}]}, opts) do
    aliases |> Macro.expand_literal(opts[:env]) |> struct(keyword)
  end

  def sample({:%{}, _, keyword}, _opts) do
    Map.new(keyword)
  end

  def sample({:{}, [], list}, _opts) do
    List.to_tuple(list)
  end

  def sample({:<<>>, [], attrs}, _opts) do
    Enum.reduce(attrs, <<>>, fn
      {:"::", [], [value, size]}, acc -> <<acc::binary, value::size(size)>>
      value, acc -> <<acc::binary, value>>
    end)
  end

  def sample(ast, _opts), do: ast
end

[
  {quote(do: :foo), &is_atom/1},
  {quote(do: "foo"), &is_binary/1},
  {quote(do: <<1::3>>), &is_bitstring/1},
  {quote(do: true), &is_boolean/1},
  {quote(do: %RuntimeError{}), &is_exception/1},
  {quote(do: %RuntimeError{}), &is_exception(&1, RuntimeError)},
  {quote(do: 1.2), &is_float/1},
  {quote(do: 1), &is_integer/1},
  {quote(do: [1, 2, 3]), &is_list/1},
  {quote(do: %{foo: "bar"}), &is_map/1},
  {quote(do: %{foo: "bar"}), &is_map_key(&1, :foo)},
  {quote(do: nil), &is_nil/1},
  {quote(do: 1), &is_number/1},
  {quote(do: %URI{path: "/"}), &is_struct/1},
  {quote(do: %URI{path: "/"}), &is_struct(&1, URI)},
  {quote(do: {1, 2, 3, 4}), &is_tuple/1}
]
|> Enum.map(fn {quoted, func} ->
  quoted
  |> Example.sample()
  |> func.()
  |> dbg()
end)

You can rewrite this code to create functions like is_map_ast/1, is_struct_ast/2 and so on …

Also you need to be careful as not everything can be converted to quoted expression, see: Quote and unquote - The Elixir programming language for more information.

2 Likes

It is, indeed, not a map :stuck_out_tongue: Only some literals are represented as themselves in AST:

  • atoms
  • numbers / double-quoted binaries / charlists
  • lists
  • two-element tuples
4 Likes

Thanks for reminder, I forgot to add ast version for Tuple in my example. :+1:

Shameless plug as I tried to document all I know about the AST :slight_smile:

I think I only missed interpolations:

iex> quote do "hello world" end
"hello world"

iex> quote do 'hello world' end
'hello world'

iex> quote do "hello, #{name}" end
{:<<>>, [],
 [
   "hello, ",
   {:"::", [],
    [
      {{:., [], [Kernel, :to_string]}, [], [{:name, [], Elixir}]},
      {:binary, [], Elixir}
    ]}
 ]}

iex> quote do 'hello, #{name}' end
{{:., [], [List, :to_charlist]}, [],
 [["hello, ", {{:., [], [Kernel, :to_string]}, [], [{:name, [], Elixir}]}]]}
1 Like

Thank you! I’m writing an Elixir -> PlusCal converter for my Master’s degree :slight_smile:

2 Likes

Thanks! I’ve been already doing something similar, but thought maybe I could use built-in functions :slight_smile: