What does `counter` value represent in AST metadata?

For example this:

ast = quote do
    if 2 == 2 do

Macro.expand(ast, __ENV__)

Resolves to:

{:case, [optimize_boolean: true],
 [{:==, [context: Elixir, import: Kernel], [2, 2]},
  [do: [{:->, [],
     [[{:when, [],
        [{:x, [counter: -576460752303422810], Kernel},
         {:in, [context: Kernel, import: Kernel],
          [{:x, [counter: -576460752303422810], Kernel}, [false, nil]]}]}],
      nil]}, {:->, [], [[{:_, [], Kernel}], :foo]}]]]}

I’m curious as to what counter refers to in:

{:x, [counter: -576460752303422810], Kernel}

My first thoughts were that it has something to do with staying hygienic and keeping track of scope, but then I found the line metadata which seems to be responsible for that.

Is there somewhere where the different AST metadata is documented?

To allow rebinding, names are suffixed by a counter when erl core is emitted, I do think it’s to keep track of those suffices. Perhaps you can experiment with some code that assigns twice to the same name.

I can’t right now, I’m on mobile.

Rebinding doesn’t appear to have any effect on the AST accessible through elixir unless there is a lower or more expanded form than what I can get after Macro.expand.

quote do
  x = 1
  x = 2


{:=, [], [{:x, [], Elixir}, 1]},
{:=, [], [{:x, [], Elixir}, 2]}

And likewise:

quote do
  x = 1
  ^x = 2


{:=, [], [{:x, [], Elixir}, 1]}, 
{:=, [], [{:^, [], [{:x, [], Elixir}]}, 2]}

When converting the quoted forms to the erlang abstract format using :elixir.quoted_to_erl/2 the values get changed as expected to _@1, _@2 for the first case and _@1, _@1 in the second. So it seems like elixir only needs to worry about rebinding ‘at the edges’ so to speak and doesn’t need to keep track of it in it’s own AST. I may be wrong about that though, there could be another layer that I am missing.

When I expand a quoted module/func definition:

quote do
  defmodule Foo do
    def bar(x), do: x

I end up with the counter metadata attached to the module’s {:alias, _, [Foo, _]} item and also the {:__ENV__, _, _} item.

Curiously, I also get it in the case expression after expanding an if macro, but I don’t get it if I write the case myself.

I’ve found the following looking through the elixir source. From the Macro.update_meta doc:

This is often useful when used with Macro.prewalk/2 to remove information like lines and hygienic counters from the expression for either storage or comparison.

And also from the Kernel.var! macro definition:

# Remove counter and force them to be vars
meta = :lists.keydelete(:counter, 1, meta)

So it does appear to be involved with scope/hygiene somehow, though I’m still curious to know what exactly it represents.

My two cents, mostly speculation :slight_smile:

I guess is that it’s about the temporary variable x which is made unique by using the global meta counter.

Macro.expand(ast, __ENV__) |> Macro.to_string would output "case(2 == 2) do\n x when x in [false, nil] ->\n nil\n _ ->\n :foo\nend" and counter metadata makes this x “special”

Earlier versions also seem to have counted from 0 (or 1) (s. [{:x, [counter: 6], Kernel}).