gmile

gmile

A script for re-writing AST: where to start?

I’m trying to write a tool, a script in Elixir, that will read a file containing code written in Elixir, rewrite a part of it, then save the contents as a new file.

I understand that reading Elixir code and writing it back could be done like this:

{:ok, ast} = Code.string_to_quoted(File.read!("my_module.ex"))

# magic that generates new_ast

File.write!("my_updated_module.ex", Macro.to_string(new_ast))

I’m looking for how to re-write the following code:

defmodule MyModule do
  use DSL

  declare :operation do
    description("My description")
  end
end

…into the following code:

defmodule MyModule do
  alias MyApp.Operation

  def operation do
    %Operation{
      description: "My description"
    }
  end
end

I am most curious about the “magic” part, e.g. re-writing the AST.

Elixir documentation gives example on Macro.prewalk/2, which looks like exactly something I’d need to use in my case. But since my case is more complicated than example in the docs, I am a bit lost & need to figure out a principle on which I’ll build.

For context, I’m trying to get rid of a custom DSL in favour of (arguably simpler) plain functions & looking for examples or ideas how to achieve this & do so in the entire app, automatically.

A project I am working on has a ton of invocations of custom DSLs like the above, and I’d like to write a tool that will go over all files that contain a DLS & convert all of them to function. My previous attempt (although only partially successful) was based on a elaborate bash script that utilised a combination of ripgrep & sed invocations.

Marked As Solved

gmile

gmile

Thank you, everyone!

At the time of posting this, I was still learning and couldn’t wrap my head around even the simplest replacement like in my example.

Here’s the code I eventually ended up with, to make the conversion from one module to another, like in my original post:

{:ok, ast} = Code.string_to_quoted(File.read!("/tmp/my_module.ex"))

new_ast =
  Macro.prewalk(ast, fn
    {:use, _meta, [{:__aliases__, _another_meta, [:DSL]}]} ->
      quote do
        alias MyApp.Operation
      end

    {:declare, _meta, children} ->
      [
        my_operation,
        [
          do: {my_key, _meta, [my_value]}
        ]
      ] = children

      quote do
        def unquote(my_operation)() do
          %Operation{
            unquote(my_key) => unquote(my_value)
          }
        end
      end

    other ->
      other
  end)

File.write!("/tmp/my_updated_module.ex", Macro.to_string(new_ast))
File.read!("/tmp/my_updated_module.ex") |> IO.puts()

Right now it appears that Macro.to_string(new_ast) formats things nicely, which is enough for my simple use case. For more complicated cases I might use Sourceror, which I looked at and got and impression it pays a lot of attention to how code is formatted, as well as provides support for comments.

Also Liked

dorgan

dorgan

Sourceror author here, just wanted to say that I regularly check the sourceror channel in the Elixir slack, but I’m most active in the elixir Discord server so feel free to reach out if you have questions about it.

You can use Sourceror for simple cases too, the focus of Sourceror is to make it easy to change AST and preserve comments, which is hard to do yourself until someone writes an alternative parser and formatter for Elixir. It’s not about “complex manipulations” but rather the primitive for these manipulations.

In your code example you just need to replace Code.string_to_quoted with Sourceror.parse_string, and Macro.to_string with Sourceror.to_string and you’ll get the comments preserved as long as you don’t discard the nodes metadata. It’s a drop-in replacement for standard lib functions.

For more complex use cases you can look at Sourceror.Zipper for traversal/modification, and the Recode project. Folks in the community are already building higher level tools on top of Sourceror so with time more tools wil be available for more complex tasks, but for now you can think of Sourceror as a drop-in replacement to parse and format elixir code with a bunch of small utilities.

To give a bit more context, I added Code.string_to_quoted_with_comments and Code.quoted_to_algebra to Elixir to enable source code manipulation, the latter is what today powers Macro.to_string, but actually handling comments was better handled by a community library(it’s deceivingly complex and Elixir itself doesn’t use that internally) so that portion of the work is what became Sourceror.

jerdew

jerdew

Overview — Sourceror v1.12.2 might be helpful both as a library and for some examples

zachallaun

zachallaun

Highly recommend Sourceror. I’ve been using it recently for a library of mine and it’s been a pleasure. There’s a sourceror channel in the Elixir Slack as well that’s not extremely chatty but gets quick responses if you have a question or issue!

Where Next?

Popular in Questions Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement