Mirror Mirror On The Wall
Mirror Mirror on the wall
Lol, Hi, so
I was thinking about how people used to mirror websites the other night and I thought of a funny joke.
What if one was born more recently, didn’t remember this, and was told to mirror a site?
Well, that would, possibly, for me at least, make me think maybe it should display its own source code.
JavaScript is great, and it lets you view its source and play with it in the console. It seems reasonable to think that I should be able to see my Elixir source code from the client side, too. And let’s be real, I love a good meta bit.
So, I set out to do this.
(An Aside) Making a quick project to follow along
If you want to follow along, we can make a quick project and ensure that you can do everything in this post, if you are such a silly person that this sounds fun to you.
We begin with a basic live view app
mix phx.new mirror --live --no-ecto
This is all we need, since it will just be one page. From here, to follow along at home, we can use any code samples that have a comment at the top like so:
# lib/foo.ex
defmodule Foo do
# we will use this module later to decompile
@compile [:debug_info]
def bar, do: 1
end
For convenience, in the parts below we removed the bar
function for explaining all this a little quicker.
Starting off with a basic funny bit
We all begin with a joke. In the beginning, I wanted to just render the source code however I could, and figured cheating was okay. So, I figured if I made the file and knew it would be on github I could always get it.
This looked super simple, the entire LiveView is just this:
defmodule BlogWeb.MirrorLive do
use BlogWeb, :live_view
require Logger
@source_url "https://raw.githubusercontent.com/notactuallytreyanastasio/blog/main/lib/blog_web/live/mirror_live.ex"
def mount(_params, _session, socket) do
if connected?(socket) do
# Fetch source code asynchronously when client connects
Task.async(fn -> fetch_source_code() end)
end
{:ok,
assign(socket,
source_code: "Loading source code...",
page_title: "I am looking at myself",
meta_attrs: [
%{name: "title", content: "Mirror Mirror on the wall"},
%{name: "description", content: "A page that shows its own source code"},
%{property: "og:title", content: "Mirror Mirror on the wall"},
%{property: "og:description", content: "A page that shows its own source code"},
%{property: "og:type", content: "website"}
]
)}
end
def handle_info({ref, source_code}, socket) when is_reference(ref) do
# Flush the DOWN message
Process.demonitor(ref, [:flush])
# Split into lines first, then characters
characters = source_code
|> String.split("\n")
|> Enum.map(fn line ->
line
|> String.graphemes()
|> Enum.map(fn char ->
%{
char: char,
duration: :rand.uniform(10) + 5,
delay: :rand.uniform(5000),
direction: if(:rand.uniform() > 0.5, do: 1, else: -1)
}
end)
end)
{:noreply, assign(socket, lines: characters)}
end
defp fetch_source_code do
Req.get!("https://raw.githubusercontent.com/notactuallytreyanastasio/blog/main/lib/blog_web/live/mirror_live.ex") |> Map.get(:body)
end
def render(assigns) do
~H"""
<div class="min-h-screen bg-gray-900 text-white p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-8">Mirror Mirror on the wall, who's the most meta of them all?</h1>
<div class="bg-gray-800 rounded-lg p-6 shadow-lg">
<pre class="text-sm font-mono" style="tab-size: 2;"><code class="language-elixir"><%= if assigns[:lines] do %><%= for line <- @lines do %><%= for char <- line do %><span style={"display: inline-block; animation: spin#{char.duration} #{char.duration}s linear #{char.delay}ms infinite;"}><%= char.char %></span><% end %>
<% end %><% else %>Loading source code...<% end %></code></pre>
</div>
</div>
</div>
<style>
<%= for duration <- 5..15 do %>
@keyframes spin<%= duration %> {
from { transform: rotate(0deg); }
to { transform: rotate(<%= if rem(duration, 2) == 0, do: "360", else: "-360" %>deg); }
}
<% end %>
</style>
"""
end
end
The only fun part here is the frames animating every character individually.
I figured why not just make it trippy, so I got some help from Claude.
This was a decent start, but I figured I should actually decompile some bytecode. This was too easy to really consider our mission accomplished.
It looks pretty standard at this point, though it really kills the browser with all these animations.
I would have thought the browser performed a little better here, but I guess I can see how this would stress it out, looping through every character.
You can see a look at it here.
Let’s actually decompile the code
I figured to really do this, it would be necessary to decompile the code completely. This is going to prove a bit more of a challenge, but will be a very fun exercise. We will begin by attacking what methods we could possibly use here in general. The core of this is all going to center around Elixir’s macro system, and functionally abuse it.
We will derive this piece by piece and explain ourselves as we go along the way, but we will start off with a finished piece of code we can reference to understand the high level API we end up with here.
A basic decompiler
This is just for reference for now, but this is a peek at the API we end up with here:
defmodule CodeDecompiler do
def decompile_to_string(module) when is_atom(module) do
path = :code.which(module)
case :beam_lib.chunks(path, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, abstract_code}}]}} ->
# Convert the abstract format to quoted expressions
quoted = Enum.map(abstract_code, &abstract_code_to_quoted/1)
|> Enum.reject(&is_nil/1)
|> wrap_in_module(module)
# Format the quoted expression into a string
Macro.to_string(quoted)
{:ok, {_, [{:abstract_code, none}]}} when none in [nil, :none] ->
{:error, :no_abstract_code}
{:error, :beam_lib, {:missing_chunk, _, _}} ->
{:error, :no_debug_info}
{:error, :beam_lib, error} ->
{:error, {:beam_lib, error}}
unexpected ->
{:error, {:unexpected_chunk_format, unexpected}}
end
end
This is our highest level entrypoint and only public function.
We can break down what we are doing piece by piece here with some real code, and begin to understand this approach.
Decompiling something extremely basic
Let’s start off by making a file we can use as a piece to decompile:
# lib/foo.ex
defmodule Foo do
end
We will write this in lib/foo.ex
and then restart our shell with iex -S mix
so that we can ensure its compiled.
With this, we can start to take a look at what we will get from our beginning statements above and begin to gradually reconstruct something that will get us what we are looking for to derive this source code naturally.
iex> path = :code.which(Foo)
iex> :beam_lib.chunks(path, [:abstract_code])
And this yields us quite a large chunk, which we are going to examine piece by piece:
{Foo,
[
abstract_code: {:raw_abstract_v1,
[
{:attribute, 1, :file, {~c"lib/foo.ex", 1}},
{:attribute, 1, :module, Foo},
{:attribute, 1, :compile, [:no_auto_import]},
{:attribute, 1, :export, [__info__: 1]},
{:attribute, 1, :spec,
{{:__info__, 1},
[
{:type, 1, :fun,
[
{:type, 1, :product,
[
{:type, 1, :union,
[
{:atom, 1, :attributes},
{:atom, 1, :compile},
{:atom, 1, :functions},
{:atom, 1, :macros},
{:atom, 1, :md5},
{:atom, 1, :exports_md5},
{:atom, 1, :module},
{:atom, 1, :deprecated},
{:atom, 1, :struct}
]}
]},
{:type, 1, :any, []}
]}
]}},
{:function, 0, :__info__, 1,
[
{:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, Foo}]},
{:clause, 0, [{:atom, 0, :functions}], [], [nil: 0]},
{:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]},
{:clause, 0, [{:atom, 0, :struct}], [], [{:atom, 0, nil}]},
{:clause, 0, [{:atom, 0, :exports_md5}], [],
[
{:bin, 0,
[
{:bin_element, 0,
{:string, 0,
[38, 59, 76, 152, 217, 42, 179, 68, 34, 190, 13, ...]},
:default, :default}
]}
]},
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}],
[],
[
{:call, 0,
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
[{:atom, 0, Foo}, {:var, 0, :Key}]}
]},
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :compile}}], [],
[
{:call, 0,
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
[{:atom, 0, Foo}, {:var, 0, :Key}]}
]},
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :md5}}], [],
[
{:call, 0,
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
[{:atom, 0, Foo}, {:var, 0, :Key}]}
]},
{:clause, 0, [{:atom, 0, :deprecated}], [], [nil: 0]}
]}
]}
]}
Let’s attack this piece by piece.
We can begin by thinking of a bit of a simpler match to break down the multiple structures we see being returned.
path = :code.which(Foo)
{:ok, {Foo, [abstract_code: {:raw_abstract_v1, ast}]}} = :beam_lib.chunks(path, [:abstract_code])
What we are really concerned with here, overall, is the AST.
If we have an AST, we can reverse that into source code, right?
So, thinking of it this way, we can get a good look at this specific representation:
:beam_lib.chunks(...) produces
|
v
{:ok, {ModuleName, [abstract_code: {:raw_abstract_v1, <list_of_forms>}]}}
|
v
<list_of_forms> = [
{:attribute, ...},
{:attribute, ...},
{:function, ...},
...
]
Now, if we want to think of the top level of this, we are defining module attributes.
These module attributes are automatically included in our module definition when it is compiled down to Erlang, and later bytecode.
In this case, we see:
{:attribute, 1, :file, {~c"lib/foo.ex", 1}}
This is giving us a representation like so:
{:attribute, <LineNumber>, :file, {<file_name>, <line_number>}}
which is to say:
:attribute
├─ 1 (the line number in the source code)
├─ :file (the type of attribute)
└─ {~c"lib/foo.ex", 1} (the file name and the line number)
Now, with this we can start cooking. This is something we could reconstruct source from.
The next piece is a little simpler:
{:attribute, 1, :module, Foo}
With this, we are basically just declaring that the module has this name at compile time.
:attribute
├─ 1 (line number)
├─ :module (the type of attribute)
└─ Foo (the module name)
Next, we get something like this:
{:attribute, 1, :compile, [:no_auto_import]}
Which means
:attribute
├─ 1 (line number)
├─ :compile (the type of attribute)
└─ [:no_auto_import] (compile option)
And in this case is telling us: “Don’t auto-import general Elixir macros/functions (like some built-in Kernel macros).”.
In general, Elixir modules have :no_auto_import
set by default to help avoid certain name collisions.
Now, we get to something more interesting: exporting functions.
{:attribute, 1, :export, [__info__: 1]}
This is another module attribute, which will be telling Erlang to publicly export the __info__/1
function.
This takes another shape:
{:attribute, <LineNumber>, :export, [{:function_atom, arity}, ...]}
The :export
attribute is how Erlang denotes which functions are publicly exported to support our public API.
Reflection about the module (e.g., Foo.__info__(:functions)
, etc. is the key here.
Next, we get to a generated typespec that is coming along, even though we didn’t define it.
{:attribute, 1, :spec, {{:__info__, 1}, [ {:type, 1, :fun, ... } ]}}
This is the type specification (the @spec
) for the exported function __info__/1
.
We see this sort of representation below:
{:type, 1, :fun,
[
{:type, 1, :product, [...]}, # argument type
{:type, 1, :any, []} # return type
]
}
This means “The function __info__/1
is a function that takes one parameter of a certain union type (like :attributes | :compile | :functions | ...
) and returns any
“.
Almost through here, next we get the actual function definition for __info__/1
:
{:function, 0, :__info__, 1, [clauses...]}
Here we get to the actual function definition for __info__/1
.
This is how Erlang’s abstract format represents a function with multiple clause definitions—each clause typically corresponds to a different pattern match.
{:function, 0, :__info__, 1,
[
{:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, Foo}]},
{:clause, 0, [{:atom, 0, :functions}], [], [nil: 0]},
{:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]},
{:clause, 0, [{:atom, 0, :struct}], [], [{:atom, 0, nil}]},
...
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}], [],
[
{:call, 0, {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
[{:atom, 0, Foo}, {:var, 0, :Key}]}
]
},
...
]}
Each clause has:
{:clause, line, <pattern_list>, <guard_list>, <body_ast>}
Some clauses match {:atom, 0, :module}
, some match {:atom, 0, :functions}
, etc.
Some have a “catch-all” style with a match binding to :Key
and then do :erlang.get_module_info(Foo, Key)
.
:clause
├─ 0 (line number)
├─ [{:atom, 0, :module}] (pattern list)
├─ [] (guard list)
└─ [{:atom, 0, Foo}] (body AST)
Meaning: “If someone calls __info__(:module)
, return Foo
.”
It is similar for :functions
, :macros
, etc.
Elixir auto-generates these for reflection.
Some clauses return nil
explicitly (written as [nil: 0]
in the AST, which is an Erlang way to represent an atom nil
with line number 0
).
Others call :erlang.get_module_info/2
to get further info about the module.
This is a minimal example of how even the simplest Elixir module turns into multiple lines of Abstract Format. It gives you a glimpse of how the BEAM environment sees your code by the time it’s ready to generate bytecode.
Now, we understand the pieces making this up and have an AST, but how do we get this turned into code?
Using the decompiled AST
The next piece of our code we will break down piece by piece to understand how we are going to start breaking this down.
We can see its full form here:
... snip ...
case :beam_lib.chunks(path, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, abstract_code}}]}} ->
# Convert the abstract format to quoted expressions
quoted =
abstract_code
|> Enum.map(&abstract_code_to_quoted/1)
|> Enum.reject(&is_nil/1)
|> wrap_in_module(module)
... snip ...
We have a function here that is our primary entrypoint: abstract_code_to_quoted/1
.
This will be how we start getting the source code itself, by making quoted
expressions that we can turn into “stringy” code.
This function looks something like this, and the first pieces are much simpler than the final bit
# Skip module attribute as we handle it in wrap_in_module
defp abstract_code_to_quoted({:attribute, _, :module, _}), do: nil
# Skip exports
defp abstract_code_to_quoted({:attribute, _, :export, _}), do: nil
# Skip compile attributes
defp abstract_code_to_quoted({:attribute, _, :compile, _}), do: nil
defp abstract_code_to_quoted({:attribute, line, name, value}) do
quote line: normalize_line(line) do
Module.put_attribute(__MODULE__, unquote(name), unquote(convert_attribute_value(value)))
end
end
Now, with abstract_code_to_quoted
we are going to grab the line, normalize it, and then create some code to create the attributes.
To convert these attributes, we do something like this:
defp convert_attribute_value(value) when is_atom(value) or is_integer(value) or is_float(value) or is_binary(value), do: value
defp convert_attribute_value(value) when is_list(value), do: Enum.map(value, &convert_attribute_value/1)
defp convert_attribute_value({a, b}), do: {convert_attribute_value(a), convert_attribute_value(b)}
defp convert_attribute_value(other), do: other
This gets us the values we need to combine with what we are putting into our own module.
Next, we wrap the definitions into a module:
# Wrap the collected definitions in a module
defp wrap_in_module(definitions, module_name) do
quote do
defmodule unquote(module_name) do
unquote_splicing(definitions)
end
end
end
This allows us to have everything prepared to hand off to Macro.to_string
, as we are basically constructing a macro from the AST.
If we look at things after creating quoted
now we can see something like this:
{:defmodule, [context: CodeDecompiler, imports: [{2, Kernel}]],
[
Foo,
[
do: {:__block__, [],
[
{{:., [line: 1],
[{:__aliases__, [line: 1, alias: false], [:Module]}, :put_attribute]},
[line: 1],
[{:__MODULE__, [line: 1], CodeDecompiler}, :file, {~c"lib/foo.ex", 1}]},
{{:., [line: 1],
[{:__aliases__, [line: 1, alias: false], [:Module]}, :put_attribute]},
[line: 1],
[
{:__MODULE__, [line: 1], CodeDecompiler},
:spec,
{{:__info__, 1},
[
{:type, 1, :fun,
[
{:type, 1, :product,
[
{:type, 1, :union,
[
{:atom, 1, :attributes},
{:atom, 1, :compile},
{:atom, 1, :functions},
{:atom, 1, :macros},
{:atom, 1, :md5},
{:atom, 1, :exports_md5},
{:atom, 1, :module},
{:atom, 1, :deprecated},
{:atom, 1, :struct}
]}
]},
{:type, 1, :any, []}
]}
]}
]},
{:def,
[line: 3, context: CodeDecompiler, imports: [{1, Kernel}, {2, Kernel}]],
[
{:bar, [line: 3, context: CodeDecompiler], []},
[do: [[{:->, [line: 3], [[], {:__block__, [line: 3], [1]}]}]]]
]}
]}
]
]}
And, if we have consutrcted it right, we can finally just call Macro.to_string/1
.
If we look at the docs, we see:
def to_string(tree, fun)
@spec to_string(t(), (t(), String.t() -> String.t())) :: String.t()
deprecated: Use Macro.to_string/1 instead
Converts the given expression AST to a string.
The given fun is called for every node in the AST with two arguments: the AST
of the node being printed and the string representation of that same node. The
return value of this function is used as the final string representation for
that AST node.
This function discards all formatting of the original code.
## Examples
Macro.to_string(quote(do: 1 + 2), fn
1, _string -> "one"
2, _string -> "two"
_ast, string -> string
end)
#=> "one + two"
Well, this looks like we just might be able to get what we want, let’s try:
defmodule Foo do
Module.put_attribute(__MODULE__, :file, {~c"lib/foo.ex", 1})
Module.put_attribute(
__MODULE__,
:spec,
{{:__info__, 1},
[
{:type, 1, :fun, [{:type, 1, :product, [{:type, 1, :union, [{:atom, 1, :attributes}, {:atom, 1, :compile}, {:atom, 1, :functions}, {:atom, 1, :macros}, {:atom, 1, :md5}, {:atom, 1, :exports_md5}, {:atom, 1, :module}, {:atom, 1, :deprecated}, {:atom, 1, :struct}]}]}, {:type, 1, :any, []}]}
]}
)
def bar() do
[(-> 1)]
end
end
Boom.
This isn’t the original source code!
But, its valid source code producing the same result.
We could call Foo.bar
and return 1
and this is functionally good valid Elixir source.
Wrapping Up
What a weird little exercise, huh?
This is pretty useless, but I hope you had fun examining it.
Here is the final Decompiler and LiveView.
I extended this to support the entire LiveView, so its a lot more code than above.
# lib/mirror_web/mirror_live.ex
defmodule BlogWeb.MirrorLive do
use BlogWeb, :live_view
require Logger
@source_url "https://raw.githubusercontent.com/notactuallytreyanastasio/blog/main/lib/blog_web/live/mirror_live.ex"
def mount(_params, _session, socket) do
if connected?(socket) do
# Fetch source code asynchronously when client connects
Task.async(fn -> fetch_source_code() end)
end
{:ok,
assign(socket,
source_code: "Loading source code...",
page_title: "I am looking at myself",
meta_attrs: [
%{name: "title", content: "Mirror Mirror on the wall"},
%{name: "description", content: "A page that shows its own source code"},
%{property: "og:title", content: "Mirror Mirror on the wall"},
%{property: "og:description", content: "A page that shows its own source code"},
%{property: "og:type", content: "website"}
]
)}
end
def handle_info({ref, source_code}, socket) when is_reference(ref) do
# Flush the DOWN message
Process.demonitor(ref, [:flush])
# Split into lines first, then characters
characters = source_code
|> String.split("\n")
|> Enum.map(fn line ->
line
|> String.graphemes()
|> Enum.map(fn char ->
%{
char: char,
duration: :rand.uniform(10) + 5,
delay: :rand.uniform(5000),
direction: if(:rand.uniform() > 0.5, do: 1, else: -1)
}
end)
end)
{:noreply, assign(socket, lines: characters)}
end
defp fetch_source_code do
CodeDecompiler.decompile_to_string(__MODULE__)
end
def render(assigns) do
~H"""
<div class="min-h-screen bg-gray-900 text-white p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-8">Mirror Mirror on the wall, who's the most meta of them all?</h1>
<div class="bg-gray-800 rounded-lg p-6 shadow-lg">
<pre class="text-sm font-mono" style="tab-size: 2;"><code class="language-elixir"><%= if assigns[:lines] do %><%= for line <- @lines do %><%= for char <- line do %><span style={"display: inline-block; animation: spin#{char.duration} #{char.duration}s linear #{char.delay}ms infinite;"}><%= char.char %></span><% end %>
<% end %><% else %>Loading source code...<% end %></code></pre>
</div>
</div>
</div>
<style>
<%= for duration <- 5..15 do %>
@keyframes spin<%= duration %> {
from { transform: rotate(0deg); }
to { transform: rotate(<%= if rem(duration, 2) == 0, do: "360", else: "-360" %>deg); }
}
<% end %>
</style>
"""
end
end
This really just is calling out to CodeDecompiler
to get our source, if you ignore all the animation madness.
defp fetch_source_code do
CodeDecompiler.decompile_to_string(__MODULE__)
end
And here is CodeDecompiler
in its full glory extended to support the entire LiveView
# lib/code_decompiler.ex
defmodule CodeDecompiler do
def decompile_to_string(module) when is_atom(module) do
path = :code.which(module)
case :beam_lib.chunks(path, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, abstract_code}}]}} ->
# Convert the abstract format to quoted expressions
quoted = Enum.map(abstract_code, &abstract_code_to_quoted/1)
|> Enum.reject(&is_nil/1)
|> wrap_in_module(module)
# Format the quoted expression into a string
require IEx; IEx.pry
Macro.to_string(quoted)
{:ok, {_, [{:abstract_code, none}]}} when none in [nil, :none] ->
{:error, :no_abstract_code}
{:error, :beam_lib, {:missing_chunk, _, _}} ->
{:error, :no_debug_info}
{:error, :beam_lib, error} ->
{:error, {:beam_lib, error}}
unexpected ->
{:error, {:unexpected_chunk_format, unexpected}}
end
end
# Helper to normalize line numbers from either integers or {line, column} tuples
defp normalize_line(line) when is_integer(line), do: line
defp normalize_line({line, _column}) when is_integer(line), do: line
defp normalize_line(_), do: 0
# Wrap the collected definitions in a module
defp wrap_in_module(definitions, module_name) do
quote do
defmodule unquote(module_name) do
unquote_splicing(definitions)
end
end
end
# Module attributes
defp abstract_code_to_quoted({:attribute, _, :module, _}), do: nil # Skip module attribute as we handle it in wrap_in_module
defp abstract_code_to_quoted({:attribute, _, :export, _}), do: nil # Skip exports
defp abstract_code_to_quoted({:attribute, _, :compile, _}), do: nil # Skip compile attributes
defp abstract_code_to_quoted({:attribute, line, name, value}) do
quote line: normalize_line(line) do
Module.put_attribute(__MODULE__, unquote(name), unquote(convert_attribute_value(value)))
end
end
# Functions
defp abstract_code_to_quoted({:function, line, name, arity, clauses}) do
# Skip module_info functions as they're automatically generated
case name do
:__info__ -> nil
:module_info -> nil
name when is_atom(name) ->
function_clauses = Enum.map(clauses, &clause_to_quoted/1)
quote line: normalize_line(line) do
def unquote(name)(unquote_splicing(make_vars(arity))) do
unquote(function_clauses)
end
end
end
end
# Function clauses
defp clause_to_quoted({:clause, line, params, guards, body}) do
converted_params = Enum.map(params, &pattern_to_quoted/1)
converted_guards = Enum.map(guards, &guard_to_quoted/1)
converted_body = Enum.map(body, &expression_to_quoted/1)
case converted_guards do
[] ->
quote line: normalize_line(line) do
unquote_splicing(converted_params) -> unquote_splicing(converted_body)
end
guards ->
quote line: normalize_line(line) do
unquote_splicing(converted_params) when unquote_splicing(guards) -> unquote_splicing(converted_body)
end
end
end
# Patterns (used in function heads and pattern matching)
defp pattern_to_quoted({:match, line, pattern1, pattern2}) do
quote line: normalize_line(line) do
unquote(pattern_to_quoted(pattern1)) = unquote(pattern_to_quoted(pattern2))
end
end
# Add binary pattern support
defp pattern_to_quoted({:bin, line, elements}) do
quoted_elements = Enum.map(elements, &binary_element_to_quoted/1)
quote line: normalize_line(line) do
<<unquote_splicing(quoted_elements)>>
end
end
defp pattern_to_quoted({:var, line, name}) do
quote line: normalize_line(line) do
unquote(Macro.var(name, nil))
end
end
defp pattern_to_quoted({:integer, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp pattern_to_quoted({:atom, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp pattern_to_quoted({:cons, line, head, tail}) do
quote line: normalize_line(line) do
[unquote(pattern_to_quoted(head)) | unquote(pattern_to_quoted(tail))]
end
end
defp pattern_to_quoted({:nil, line}) do
quote line: normalize_line(line) do
[]
end
end
defp pattern_to_quoted({:tuple, line, elements}) do
quoted_elements = Enum.map(elements, &pattern_to_quoted/1)
quote line: normalize_line(line) do
{unquote_splicing(quoted_elements)}
end
end
defp pattern_to_quoted({:map, line, pairs}) do
quoted_pairs = Enum.map(pairs, fn
{:map_field_assoc, _, key, value} ->
{:%{}, [], [{pattern_to_quoted(key), pattern_to_quoted(value)}]}
{:map_field_exact, _, key, value} ->
{pattern_to_quoted(key), pattern_to_quoted(value)}
{op, k, v} ->
{map_op_to_quoted(op), pattern_to_quoted(k), pattern_to_quoted(v)}
end)
quote line: normalize_line(line) do
%{unquote_splicing(quoted_pairs)}
end
end
# Guards
defp guard_to_quoted(guards) when is_list(guards) do
Enum.map(guards, fn guard -> guard_to_quoted_expr(guard) end)
end
defp guard_to_quoted(guard), do: guard_to_quoted_expr(guard)
# Guard expressions
defp guard_to_quoted_expr({:op, line, operator, left, right}) do
quote line: normalize_line(line) do
unquote({operator, [], [expression_to_quoted(left), expression_to_quoted(right)]})
end
end
defp guard_to_quoted_expr({:op, line, operator, operand}) do
quote line: normalize_line(line) do
unquote({operator, [], [expression_to_quoted(operand)]})
end
end
defp guard_to_quoted_expr({:call, line, {:remote, _, {:atom, _, module}, {:atom, _, fun}}, args}) do
quoted_args = Enum.map(args, &expression_to_quoted/1)
quote line: normalize_line(line) do
unquote(module).unquote(fun)(unquote_splicing(quoted_args))
end
end
defp guard_to_quoted_expr({:call, line, {:atom, _, fun}, args}) do
quoted_args = Enum.map(args, &expression_to_quoted/1)
quote line: normalize_line(line) do
unquote(fun)(unquote_splicing(quoted_args))
end
end
# Add support for variables and other basic terms in guards
defp guard_to_quoted_expr(expr), do: expression_to_quoted(expr)
# Expressions (function bodies)
# Binary expressions need to come before general constructs
defp expression_to_quoted({:bin, line, elements}) do
quoted_elements = Enum.map(elements, &binary_element_to_quoted/1)
quote line: normalize_line(line) do
<<unquote_splicing(quoted_elements)>>
end
end
# Anonymous functions
defp expression_to_quoted({:fun, line, {:clauses, clauses}}) do
quoted_clauses = Enum.map(clauses, fn {:clause, clause_line, params, guards, body} ->
converted_params = Enum.map(params, &pattern_to_quoted/1)
converted_guards = Enum.map(guards, &guard_to_quoted/1)
converted_body = Enum.map(body, &expression_to_quoted/1)
case converted_guards do
[] ->
{:->, [line: normalize_line(clause_line)],
[converted_params, {:__block__, [], converted_body}]}
guards ->
{:->, [line: normalize_line(clause_line)],
[[{:when, [], converted_params ++ guards}], {:__block__, [], converted_body}]}
end
end)
{:fn, [line: normalize_line(line)], quoted_clauses}
end
defp binary_element_to_quoted({:bin_element, _line, {:string, _sline, value}, :default, :default}) do
value
end
defp binary_element_to_quoted({:bin_element, _line, expr, size, type}) do
quoted_expr = expression_to_quoted(expr)
build_bin_element(quoted_expr, size, type)
end
defp build_bin_element(expr, :default, :default), do: expr
defp build_bin_element(expr, size, :default) when is_integer(size), do: quote do: unquote(expr)::size(unquote(size))
defp build_bin_element(expr, :default, type), do: quote do: unquote(expr)::unquote(type)
defp build_bin_element(expr, size, type), do: quote do: unquote(expr)::size(unquote(size))-unquote(type)
# List construction
defp expression_to_quoted({:cons, line, head, {:nil, _}}) do
quote line: normalize_line(line) do
[unquote(expression_to_quoted(head))]
end
end
defp expression_to_quoted({:cons, line, head, tail}) do
quote line: normalize_line(line) do
[unquote(expression_to_quoted(head)) | unquote(expression_to_quoted(tail))]
end
end
defp expression_to_quoted({:nil, line}) do
quote line: normalize_line(line) do
[]
end
end
# Other expressions
defp expression_to_quoted({:match, line, pattern, expr}) do
quote line: normalize_line(line) do
unquote(pattern_to_quoted(pattern)) = unquote(expression_to_quoted(expr))
end
end
defp expression_to_quoted({:call, line, {:remote, _, mod, fun}, args}) do
quoted_mod = expression_to_quoted(mod)
quoted_fun = expression_to_quoted(fun)
quoted_args = Enum.map(args, &expression_to_quoted/1)
quote line: normalize_line(line) do
unquote(quoted_mod).unquote(quoted_fun)(unquote_splicing(quoted_args))
end
end
defp expression_to_quoted({:call, line, {:atom, _, fun}, args}) do
quoted_args = Enum.map(args, &expression_to_quoted/1)
quote line: normalize_line(line) do
unquote(fun)(unquote_splicing(quoted_args))
end
end
defp expression_to_quoted({:case, line, expr, clauses}) do
quoted_expr = expression_to_quoted(expr)
quoted_clauses = Enum.map(clauses, &clause_to_quoted/1)
quote line: normalize_line(line) do
case unquote(quoted_expr) do
unquote(quoted_clauses)
end
end
end
defp expression_to_quoted({:block, line, exprs}) do
quoted_exprs = Enum.map(exprs, &expression_to_quoted/1)
quote line: normalize_line(line) do
unquote_splicing(quoted_exprs)
end
end
defp expression_to_quoted({:tuple, line, elements}) do
quoted_elements = Enum.map(elements, &expression_to_quoted/1)
quote line: normalize_line(line) do
{unquote_splicing(quoted_elements)}
end
end
# Operator expressions
defp expression_to_quoted({:op, line, operator, left, right}) do
quote line: normalize_line(line) do
unquote({operator, [], [expression_to_quoted(left), expression_to_quoted(right)]})
end
end
defp expression_to_quoted({:op, line, operator, operand}) do
quote line: normalize_line(line) do
unquote({operator, [], [expression_to_quoted(operand)]})
end
end
# Literals and basic terms
defp expression_to_quoted({:atom, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp expression_to_quoted({:integer, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp expression_to_quoted({:float, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp expression_to_quoted({:string, line, value}) do
quote line: normalize_line(line) do
unquote(value)
end
end
defp expression_to_quoted({:var, line, name}) do
quote line: normalize_line(line) do
unquote(Macro.var(name, nil))
end
end
# Maps
defp expression_to_quoted({:map, line, []}) do
quote line: normalize_line(line) do
%{}
end
end
defp expression_to_quoted({:map, line, pairs}) do
quoted_pairs = Enum.map(pairs, fn
{:map_field_assoc, _, key, value} ->
{:%{}, [], [{expression_to_quoted(key), expression_to_quoted(value)}]}
{op, k, v} ->
{map_op_to_quoted(op), expression_to_quoted(k), expression_to_quoted(v)}
end)
quote line: normalize_line(line) do
%{unquote_splicing(quoted_pairs)}
end
end
# Helpers
defp make_vars(n) when n > 0 do
for i <- 1..n//1, do: Macro.var(:"arg#{i}", nil)
end
defp make_vars(_), do: []
defp map_op_to_quoted(:exact), do: :%{}
defp map_op_to_quoted(:assoc), do: :%{}
defp convert_attribute_value(value) when is_atom(value) or is_integer(value) or is_float(value) or is_binary(value), do: value
defp convert_attribute_value(value) when is_list(value), do: Enum.map(value, &convert_attribute_value/1)
defp convert_attribute_value({a, b}), do: {convert_attribute_value(a), convert_attribute_value(b)}
defp convert_attribute_value(other), do: other
end
Happy Hacking
I hope you enjoyed this silliness.