Is there something wrong with the iex?

I was trying to see for myself how the process of AST -> Expanded AST -> Bytecode works by using Macro.expand and Code.compile_quoted:

iex(1)> quote do
...(1)> defmodule Bar do
...(1)>   1+2
...(1)> end
...(1)> end |> Macro.expand(__ENV__) |> Code.compile_quoted
[{Bar,
  <<70, 79, 82, 49, 0, 0, 3, 228, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 94,
    131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99,
    115, 95, 118, 49, 108, 0, 0, 0, 4, 104, 2, 100, 0, ...>>}]

I notice that when there is any variable in the iex scope the previous stops working

iex(2)> x = 1
1

iex(3)> quote do
...(3)> defmodule Bar do
...(3)>   1+2
...(3)> end
...(3)> end |> Macro.expand(__ENV__) |> Code.compile_quoted
warning: variable "x" does not exist and is being expanded to "x()", please use parentheses to remove the ambiguity or change the var
iable name
  nofile

** (CompileError) nofile: undefined function x/0
...

Why did the variable screw up everything? I noticed that the variable x was actually in the expanded AST…

1 Like

It is good before pipe into Code.compile_quote. But the x in the AST looks weird, though.

I think x is part of the environment

iex(4)> __ENV__.vars
[x: nil]

Yes I understand that but why should it be included in the expanded ast
when it’s not involved or seen at all in the source code

Your environment has a binding (as @idi527 said) but you don’t pass that binding when evaluating the code. This works:

iex(2)> x = 1
1
iex(3)> quote do
...(3)> defmodule Bar do
...(3)>   1+2
...(3)> end
...(3)> end |> Macro.expand(__ENV__) |> Code.eval_quoted(binding())
{{:module, Bar,
  <<70, 79, 82, 49, 0, 0, 3, 220, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 86,
    131, 80, 0, 0, 0, 93, 120, 156, 203, 96, 74, 97, 224, 75, 205, 201, 172,
    200, 44, 138, 79, 201, 79, 46, 142, 47, ...>>, 3}, [x: 1]}
3 Likes

Newbie question: I would expect the expanded AST to only contain variables and values related to the source code which in my example is mainly 1 + 2. I don’t understand why is the variable x is the context involved?

For example in this scenario the variable x is not included

iex> quote do
...> 1+2
...> end |> Macro.expand(__ENV__) |> Code.eval_quoted()
{3, []}

It was involved because x was inside __ENV__.vars.

Ha! i think i figured out.

quote do 
1+2 
end

Has no macro hence there is no AST expansion hence x was not in the picture.

quote do
  defmodule Bar do
    1+2
  end
end

Involves macro and hence AST expansion and hence x was brought in

1 Like

More actually it is because 2+2 does not need to close over its environment, but a defmodule and so forth do close over the outer environment.

Wow, do you know of any links where I can learn more about “closing over the environment”

Most languages do this, but in Elixir’s case it is based on how it passes or not passes bindings down in to different blocks, an example:

iex(1)> quote do 2+2 end |> Macro.expand(__ENV__)
{:+, [context: Elixir, import: Kernel], [2, 2]}
iex(2)> quote do defmodule Testering do def test, do: 2 end end |> Macro.expand(__ENV__)
{:__block__, [],
 [{:alias,
   [defined: Testering, context: nil, alias: false,
    counter: -576460752303423466], [Testering, [as: nil, warn: false]]},
  {{:., [], [:elixir_module, :compile]}, [],
   [Testering,
    {:{}, [],
     [:__block__, [],
      [{:{}, [],
        [:=, [],
         [{:{}, [], [:result, [], Kernel]},
          {:{}, [],
           [:def, [context: Elixir, import: Kernel],
            [{:{}, [], [:test, [context: Elixir], Elixir]}, [do: 2]]]}]]},
       {:{}, [], [{:{}, [], [:., [], [:elixir_utils, :noop]]}, [], []]},
       {:{}, [], [:result, [], Kernel]}]]}, [],
    {:__ENV__, [counter: -576460752303423466], Kernel}]}]}
iex(3)> blahblahblah = 42
42
iex(4)> quote do 2+2 end |> Macro.expand(__ENV__)                                       
{:+, [context: Elixir, import: Kernel], [2, 2]}
iex(5)> quote do defmodule Testering do def test, do: 2 end end |> Macro.expand(__ENV__)
{:__block__, [],
 [{:alias,
   [defined: Testering, context: nil, alias: false,
    counter: -576460752303423442], [Testering, [as: nil, warn: false]]},
  {{:., [], [:elixir_module, :compile]}, [],
   [Testering,
    {:{}, [],
     [:__block__, [],
      [{:{}, [],
        [:=, [],
         [{:{}, [], [:result, [], Kernel]},
          {:{}, [],
           [:def, [context: Elixir, import: Kernel],
            [{:{}, [], [:test, [context: Elixir], Elixir]}, [do: 2]]]}]]},
       {:{}, [], [{:{}, [], [:., [], [:elixir_utils, :noop]]}, [], []]},
       {:{}, [], [:result, [], Kernel]}]]},
    [{:{}, [],
      [:blahblahblah, nil, :_@0, {:blahblahblah, [generated: true], nil}]}],
    {:__ENV__, [counter: -576460752303423442], Kernel}]}]}

defmodule is special in that bindings that exist in its outer scope get brought in at ‘module level’. Specifically when you defmodule it makes an alias in its outer scope (that is why you can call an inner module’s name directly without needing to fully scope it), then it calls the internal function that actually compiles a module of :elixir_module.compile(TheModuleNameHere, moduleASTHere, outerBindingsHere, environment). The defmodule macro you can see its source at:

But in essence it grabs active bindings from its parent environment (emulated in my macro calls in iex above via the Macro.expand(__ENV__) call) and adds them to the function that compiles the module so that it can resolve calls to those outer bindings.

1 Like