How to write a macro that replaces one identifier with another, but it needs to respect lexical scope?

I would like to write a macro that replaces one identifier with another, but it needs to respect lexical scope.

Here is an example illustrating what I mean:

defmodule Foo do
  defmacro rename({i1, _, c1}, {_i2, _, _} = i2c, do: body) do
    Macro.postwalk(body, fn x ->
      case x do
        {^i1, _, ^c1} -> i2c
        w -> w
      end
    end)
  end

  def foo() do
    quote do
      rename x, y do
        x = 42
        z = fn x -> x + 2 end
        x + 1
      end
    end
    |> Macro.expand_once(__ENV__)
    |> Macro.to_string()
    |> IO.puts()
  end
end

Foo.foo()

When I run this code it prints the following: (meaning: the reanme do ... end block expands to the following)

y = 42
z = fn y -> y + 2 end
y + 1

The problem here is that the x variable introduced on line 3 by the fn form gets rewritten, even though that x refers to a different variable than the x on line 1. What I want is a definition for rename that, when run on the same code, prints something like this:

y = 42
z = fn x -> x + 2 end
y + 1

Context

Above is just a toy problem meant to illustrate what I would like to try to do. I’m working on a bigger and much more complicated macro that might need to rewrite variables in arbitrary expressions, but I want to make sure that my macro respects lexical scoping.

Racket can do something like this with make-rename-transformer, which creates an identifier macro that expands to another identifier. When you run this code:

#lang racket

(require syntax/parse syntax/parse/define
         (for-syntax racket/match))

(define-syntax (replace stx)
  (syntax-parse stx
    [(_ (i1:id i2:id) body:expr)
     #'(let-syntax ([i1 (make-rename-transformer (syntax i2))]) body)]))

(expand-once #'(let ([x 1] [y 2]) (replace (x y) (+ x (let ([x 1]) x)))))

you get this output:

#<syntax:ident_test.rkt:11:10
  (let-values (((x) (quote 1)) ((y) (quote 2)))
    (let-values ()
      (let-values () (#%app + y
                            (let-values (((x) (quote 1)))
                              x)))))>

if it’s not immediately obvious, the variable x has been swapped out for y except inside of the inner let block, which introduces a new variable x.

I would like to see if I can do the same in Elixir. I know that this is a little unusual; I’m a researcher, so I do unusual stuff by definition. :slight_smile:

Thanks in advance for any help.

2 Likes

We don’t have built in affordances in the language for this. You would need to traverse the constructs yourself, expanding nodes, and tracking when variables are introduced.

Perhaps some of the tooling being used by folks for refactoring, such as sourceror and styler, have higher level conveniences for that, but they are more on the static analysis / source code rewriting side of things.

4 Likes

Thank you very much for the answer José!

2 Likes