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

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?

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: https://elixir-lang.org/getting-started/meta/quote-and-unquote.html for more information.

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

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}]}]]}

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

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