Macros not evaluating as per expectation

I am facing a problem in understanding why my macro works the way it does.

I have the following macro

macro create_m(m)
    y = :(a + b * c + 1)
    y.args[3].args[2] = :(2 * $m)
    dxr = m
    global dxy = 2 * m
    println("m, 2*m, dxy in macro ", dxr, " ", 2 * m, " ", dxy)
    push!(y.args, dxy + 25)
    y
end

The idea is to evaluate dxy = 2 * m and try to incorporate it into the AST and also see if it can be escaped by declaring global.

When I print the result with the following:

@macroexpand @create_m(5)
# Meta.dump(@create_m(12))
zz = @create_m(8)
println("zz = ", zz)

# println(dxr)
println("dxy = ", dxy)

I get

m, 2*m, dxy in macro 5 7 7
m, 2*m, dxy in macro 8 10 10
zz = 59
dxy = 10

Which means 2 * m is evaluated as 2 + m

Even @macroexpand @create_m(5) evaluates to :(Main.Heller.a + (2 * 5) * Main.Heller.c + 1 + 32) so 25 + dxy is evaluated as 25 + (2+5) rather than 25 + (2*5)

Though rest of the allocations are fine.

Any reason why this is so? Any help in understanding would be appreciated

Hey @kishaloy,

I tried to reproduce the behavior you reported but to no avail.

Can you retry using a clean environment?

1 Like

Now it is even more weird.

Basically the macro is a part of a file with other functions, but there is no dxy anywhere.

When I run the whole file Ctrl+Shift+Enter in vscode, the error comes as shown.

But when I run only the macros + the relevant lines only (Ctrl + Enter), with a clean state, it all comes good like yours. But if I have run once with all, then the state is spoiled for good.

I am using Version 1.9.3, if that helps.

Pretty strange…

I haven’t understood the issue you’re hitting, but what is your higher level goal?

If I understand your code, you implicitly assume that m is always an integer literal. If so, why not use a function?

Secondly, what is the goal of the global usage? To inject a named variable into the macro caller’s scope, getting around hygiene?

There are lots of pitfalls that you can explore - and I am pretty confident that once we get to the end of it, this will look pretty much expected (as opposed to weird).

The rest of the file can be pretty relevant - especially if you alter the global state from inside functions/macros (which is not necessarily a recommended thing to do).

The macro itself works pretty much as expected - so the issue might be related to when/where you are calling it (REPL vs. script will work differently in certain scenarios).

So, at this point helping us to understand what you actually try to achieve will help solve this faster.

The idea was to get the hang of macros and figuring out how to generate AST in compile time, how it works etc. using a mix of Expr and regular evals. So the example is fictitious.

1 Like

If this is the case, then a good approach would be to perform this exploration without dropping the macro hygiene (you can do that in justified scenarios once you are proficient enough with clean macros).

https://docs.julialang.org/en/v1/manual/metaprogramming/#Hygiene

1 Like

So my personal recommendation for learning how to write macros is one that will actually make your example go away entirely:

  1. Don’t start with a macro. Start with a function that takes in expressions and produces expressions back. Convince yourself you’re doing this correctly.
  2. Make the macro just a call to the expression-to-expression function.

So, for example, you’d do something like this simplification with your example:

function create_m_func(m)
    y = :(a + b * c + 1)
    y.args[3].args[2] = :(2 * $m)
    dxr = m
    global dxy = 2 * m
    println("m, 2*m, dxy in function ", dxr, " ", 2 * m, " ", dxy)
    push!(y.args, dxy + 25)
    y
end

macro create_m(m)
    create_m_func(m)
end

a, b, c = 1, 1, 1
zz = @create_m(8)

println("zz = ", zz)

println("dxy = ", dxy)
4 Likes

Didn’t really help. Also I simplified the macro substantially.

function create_m_fn(m)
    println("1st => m, 2*m ", m, " ", 2 * m) # 1st => m, 2*m 5 7
    2 * m
end

macro create_m(m)
    zz = create_m_fn(m)
    :(1 + $zz)
end

@macroexpand @create_m 5 # :(1 + 7)

zz = @create_m 8
println("zz = ", zz) # zz = 11

Somewhere my understanding in what and how variables are evaluated during macro expansion is off. 2*m is evaluated as 2+m. The textual substitution works as expected.

Works for me as expected. The only reason I could think of is that you somehow run it in an environment where * is bound to +?

1 Like

The packages that I am using are:

  [6e4b80f9] BenchmarkTools v1.3.2
  [336ed68f] CSV v0.10.11
  [8be319e6] Chain v0.5.0
  [a93c6f00] DataFrames v1.6.1
  [1313f7d8] DataFramesMeta v0.14.1
  [d8e11817] MLStyle v0.4.17
  [91a5bcdd] Plots v1.39.0
  [c3e4b0f8] Pluto v0.19.32
  [7f904dfe] PlutoUI v0.7.53
  [8e1ec7a9] SumTypes v0.5.0
  [ade2ca70] Dates
  [37e2e46d] LinearAlgebra
  [8dfed614] Test

Rest is all vanilla Julia that I have written.

Ok, have installed exactly these packages into a fresh environment (on Julia 1.10.0). Still everything works as expected for me. Maybe you could try the following:

  1. Start a new Julia REPL and run
    julia> 2 * 5
    
  2. Change your code to explicitly use * from Base, i.e.,
    function create_m_fn(m)
        println("1st => m, 2*m ", m, " ", Base.:*(2, m)) # 1st => m, 2*m 5 7
        Base.:*(2, m)
    end
    # Everything else as before ...
    
    and then try again.

Just to be clear – if you copy and paste that code into a fresh REPL session, this code works as expected or does not?

If yes, but the code does not work in the broader context you’re not fully sharing, I suspect we are not going to make progress resolving it since the core issues are in the code we’re not discussing.

2 Likes