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 v0.12.1 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!

sodapopcan

sodapopcan

I’m just digging into all of this myself and third Sourceror.

The examples directory in Sourceror’s repo gives a good example of matching on a node then manipulating a that node’s children. This is half of what you want, you just need to also replace the parent in the same go.

@zachallaun That’s good to know the channel is open for questions—I joined the other day and wasn’t sure if it was just for development talk or not. I certainly won’t be flooding it with questions (I’m pretty new to working with trees so I have a lot, but everything has been pretty searchable) but good to know it’s open for them!

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
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
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
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
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement