# Macro utilities for working with function definitions in 2025

**URL:** https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687
**Category:** General Usage
**Tags:** macros
**Created:** [July 13, 2025, 10:43pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687 "2025-07-13T22:43:40Z")
**Posts on this page:** 18
**Page:** 1

<div class="post-metadata">

### Author: ![CameronBieganek](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cameronbieganek/32/6915_2.png) [@CameronBieganek](https://discourse.julialang.org/u/CameronBieganek)
#### Post date: [July 13, 2025, 10:43pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/1 "2025-07-13T22:43:40Z")

</div>

What are the recommended tools these days for working with function definition expressions in macros? I’m aware of ExprTools.jl and MacroTools.jl, but I don’t know how maintained they are and whether those packages or other packages are recommended these days.

---

<div class="post-metadata">

### Author: ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)
#### Post date: [July 13, 2025, 11:51pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/2 "2025-07-13T23:51:45Z")

</div>

Hi! Maintainer of MacroTools.jl and author of `splitdef/combinedef` here. MacroTools is not actively developed ATM, but it’s been around for a long time, and basically it does what it says it does? It’s one of the most used packages in the ecosystem, so the bugs have been ironed out.

ExprTools offers a `splitdef/combinedef` with very similar interfaces. There was [talk to merge the two](https://github.com/FluxML/MacroTools.jl/pull/132) five years ago, but that didn’t happen. Not clear what the benefits are in 2025. I’m not aware of any outstanding bug in `splitdef`.

There’s also MLStyle.jl which can do it. It comes with a [benchmark!](https://github.com/thautwarm/MLStyle.jl?tab=readme-ov-file#extracting-struct-definitions) The graph looks off, but looking at the data behind it, it seems to be that MLStyle is faster for small-ish expressions, but slower for big ones? But that’s only precompilation time, so pretty small potatoes unless you have a widely used macro. It’s used in the SciML space; that’s a good recommendation in its favor.

One important point regardless: if your macro is returning function definitions, consider using [`@qq begin`](https://fluxml.ai/MacroTools.jl/dev/utilities/#MacroTools.@qq) instead of `quote`, so that line numbers are correct (then go-to-definition and stacktraces become much more useful).

---

<div class="post-metadata">

### Author: ![CameronBieganek](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cameronbieganek/32/6915_2.png) [@CameronBieganek](https://discourse.julialang.org/u/CameronBieganek)
#### Post date: [July 14, 2025, 12:23am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/3 "2025-07-14T00:23:04Z")

</div>

Thanks @cstjean!

Some time ago I saw a comment on Github from a core developer that mentioned that MacroTools.jl has some issues with latency or invalidation or something like that. So, since then I’ve been somewhat hesitant to use MacroTools.jl. Do you have any insight into that matter?

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [July 14, 2025, 1:21am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/4 "2025-07-14T01:21:04Z")

</div>

> [@CameronBieganek](#):
>
> What are the recommended tools these days for working with function definition expressions in macros?

What are you trying to do? (For a lot of simple things the built-in functions are enough.)

---

<div class="post-metadata">

### Author: ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)
#### Post date: [July 14, 2025, 8:01am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/5 "2025-07-14T08:01:36Z")

</div>

> [@CameronBieganek](#):
>
> Some time ago I saw a comment on Github from a core developer that mentioned that [MacroTools.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/MacroTools) has some issues with latency or invalidation or something like that.

I remember reading such a comment about it years ago from @tim.holy, and I still don’t get it. In any case, MacroTools is an indirect dependency of a big chunk of the ecosystem, so if there’s a major issue there we should fix it.

> [@CameronBieganek](#):
>
> So, since then I’ve been somewhat hesitant to use [MacroTools.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/MacroTools).

I can see how importing it might introduce undesirable methods that affect precompilation time, but I don’t see how _using_ it can possibly be problematic.

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 14, 2025, 3:46pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/6 "2025-07-14T15:46:14Z")

</div>

I haven’t checked in ages, but in general terms the reason it was problematic was the same reason that CoreLogging was initially problematic: both injected poorly-inferred functions into your code. For example, if you write

```julia
function foo(x)
    x < 0 && @warn "expected $x to be positive"
    return x
end

```

that expands to

```julia
:(function foo(x)
      #= REPL[3]:1 =#
      #= REPL[3]:2 =#
      x < 0 && begin
              #= logging.jl:384 =#
              let
                  #= logging.jl:385 =#
                  var"#48#level" = Base.CoreLogging.Warn
                  #= logging.jl:387 =#
                  var"#49#std_level" = var"#48#level"
                  #= logging.jl:388 =#
                  if (var"#49#std_level").level >= (Base.Threads.Atomic{Int32}(-1000))[]
                      #= logging.jl:389 =#
                      var"#50#group" = Symbol("REPL[3]")
                      #= logging.jl:390 =#
                      var"#51#_module" = Main
                      #= logging.jl:391 =#
                      var"#52#logger" = (Base.CoreLogging.current_logger_for_env)(var"#49#std_level", var"#50#group", var"#51#_module")
                      #= logging.jl:392 =#
                      if !(var"#52#logger" === Base.CoreLogging.nothing)
                          #= logging.jl:393 =#
                          var"#53#id" = :Main_e4489fe9
                          #= logging.jl:396 =#
                          if Base.CoreLogging.invokelatest(Base.CoreLogging.shouldlog, var"#52#logger", var"#48#level", var"#51#_module", var"#50#group", var"#53#id")
                              #= logging.jl:397 =#
                              var"#54#file" = "REPL[3]"
                              #= logging.jl:398 =#
                              if var"#54#file" isa Base.CoreLogging.String
                                  #= logging.jl:399 =#
                                  var"#54#file" = (Base.CoreLogging.Base).fixup_stdlib_path(var"#54#file")
                              end
                              #= logging.jl:401 =#
                              var"#55#line" = 2
                              #= logging.jl:402 =#
                              local var"#56#msg", var"#57#kwargs"
                              #= logging.jl:403 =#
                              begin
                                      #= logging.jl:373 =#
                                      try
                                          #= logging.jl:374 =#
                                          var"#56#msg" = "expected $(x) to be positive"
                                          #= logging.jl:375 =#
                                          var"#57#kwargs" = (;)
                                          #= logging.jl:376 =#
                                          true
                                      catch var"#70#err"
                                          #= logging.jl:378 =#
                                          Base.invokelatest(Base.CoreLogging.logging_error, var"#52#logger", var"#48#level", var"#51#_module", var"#50#group", var"#53#id", var"#54#file", var"#55#line", var"#70#err", true)
                                          #= logging.jl:379 =#
                                          false
                                      end
                                  end && Base.CoreLogging.invokelatest(Base.CoreLogging.handle_message, var"#52#logger", var"#48#level", var"#56#msg", var"#51#_module", var"#50#group", var"#53#id", var"#54#file", var"#55#line"; var"#57#kwargs"...)
                          end
                      end
                  end
                  #= logging.jl:409 =#
                  Base.CoreLogging.nothing
              end
          end
      #= REPL[3]:3 =#
      return x
  end)

```

You’ll notice that the expanded code includes many function calls, some of which are made via `invokelatest`: this was because a lot of the code in `CoreLogging` was uninferrable, and poorly-inferred code is vastly more vulnerable to invalidation than well-inferred code. The nasty part was that if you loaded some package that invalidated code in CoreLogging, that percolated up through your code that made use of it. `invokelatest` forces runtime dispatch, and thus breaks the “chain of invalidation” that would otherwise percolate through the entire caller path.

We spent several days going over CoreLogging finding and fixing type inference where possible, and adding `invokelatest` where not. But I never did that for MacroTools, and I honestly don’t know whether someone else has done so in the meantime.

One way you can test it is schematized like this:

```julia
using MySmallPackageThatUsesMacroTools
exercise_my_package() # forces compilation of a lot of your code
using SnoopCompileCore
invalidations = @snoop_invalidations using PkgA, PkgB, PkgC

```

Good candidates for `PkgA` etc are packages that extend Julia’s own methods: things like new number types (e.g., BFloat16.jl) or string types (e.g., InlineStrings.jl). Obviously you would need to first check that those packages aren’t already loaded by `MySmallPackageThatUsesMacroTools` or you’ll fool yourself into thinking you’re safe when you aren’t.

Another more comprehensive way is to use JET on your package, but you’d have to distinguish problems caused by MacroTools from ones that are not.

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [July 14, 2025, 8:22pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/7 "2025-07-14T20:22:21Z")

</div>

> [@tim.holy](#):
>
> But I never did that for MacroTools, and I honestly don’t know whether someone else has done so in the meantime.

In normal usage, MacroTools functions are only evaluated at precompile-time, when macros are expanded, not at runtime. So why should type stability matter?

---

<div class="post-metadata">

### Author: ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)
#### Post date: [July 15, 2025, 12:45am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/8 "2025-07-15T00:45:20Z")

</div>

On top of that, macros don’t have backedges, so they cannot cascade into invalidating other functions (to my chagrin).

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 16, 2025, 11:31pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/9 "2025-07-16T23:31:49Z")

</div>

It’s more a question of what they expand to than the macro itself. If the code they expand to has inference problems that wouldn’t be present without the macro, that’s when it becomes a problem. It’s not very common that a macro would change none of the dispatches in the code (if not, what is it doing?), even little things like `===` vs `==` can make a big difference for certain operations (e.g., [Use === in more places to reduce invalidation by timholy · Pull Request #167 · FluxML/MacroTools.jl · GitHub](https://github.com/FluxML/MacroTools.jl/pull/167)).

Perhaps it used to inject calls to internal MacroTools methods and now doesn’t? I last looked at this many years ago so lots could have changed. I only responded here because I got pinged to explain the issue, not because I know anything about the current package state.

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [July 17, 2025, 12:19am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/10 "2025-07-17T00:19:51Z")

</div>

> [@tim.holy](#):
>
> Perhaps it used to inject calls to internal MacroTools methods and now doesn’t

MacroTools is only utilities for pattern matching and recursive transformation of expressions. It doesn’t inject _any_ of its own code into expressions as far as I know.

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 17, 2025, 1:30am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/11 "2025-07-17T01:30:58Z")

</div>

I’m not a MacroTools user, so this isn’t as easy for me as it would be for y’all. But I’m a big fan of just measuring things. Using an example from the manual:

```julia
julia> using MacroTools

julia> macroexpand(Main, :(@capture(:[1, 2, 3, 4, 5, 6, 7], [1, a_, 3, b__, c_])))
quote
    #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:66 =#
    a = MacroTools.nothing
    b = MacroTools.nothing
    c = MacroTools.nothing
    #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:67 =#
    var"#25#env" = MacroTools.trymatch($(Expr(:copyast, :($(QuoteNode(:([1, a_, 3, b__, c_])))))), $(Expr(:copyast, :($(QuoteNode(:([1, 2, 3, 4, 5, 6, 7])))))))
    #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:68 =#
    if var"#25#env" === MacroTools.nothing
        #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:69 =#
        false
    else
        #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:71 =#
        a = MacroTools.get(var"#25#env", :a, MacroTools.nothing)
        b = MacroTools.get(var"#25#env", :b, MacroTools.nothing)
        c = MacroTools.get(var"#25#env", :c, MacroTools.nothing)
        #= /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/macro.jl:72 =#
        true
    end
end

julia> methods(MacroTools.trymatch)
# 1 method for generic function "trymatch" from MacroTools:
 [1] trymatch(pat, ex)
     @ ~/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:112

julia> using MethodAnalysis, JET

julia> mis = methodinstances(MacroTools.trymatch)
2-element Vector{Core.MethodInstance}:
 MethodInstance for MacroTools.trymatch(::Expr, ::Any)
 MethodInstance for MacroTools.trymatch(::Expr, ::Expr)

julia> report_opt(mis[2])
[ Info: tracking Base
═════ 7 possible errors found ═════
┌ trymatch(pat::Expr, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:113
│┌ match(pat::Expr, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:108
││┌ match(pat::Expr, ex::Expr, env::Dict{Any, Any}) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:99
│││┌ normalise(ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:88
││││┌ unblock(ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/utils.jl:122
│││││ runtime dispatch detected: unblock(%11::Any)::Any
││││└────────────────────
││┌ match(pat::Expr, ex::Expr, env::Dict{Any, Any}) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:101
│││┌ store!(env::Dict{Any, Any}, name::Symbol, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:20
││││┌ haskey(h::Dict{Any, Any}, key::Symbol) @ Base ./dict.jl:569
│││││┌ ht_keyindex(h::Dict{Any, Any}, key::Symbol) @ Base ./dict.jl:275
││││││┌ ==(w::Symbol, v::WeakRef) @ Base ./gcutils.jl:36
│││││││ runtime dispatch detected: isequal(w::Symbol, %1::Any)::Bool
││││││└────────────────────
│││┌ store!(env::Dict{Any, Any}, name::Symbol, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:21
││││┌ assoc!(d::Dict{Any, Any}, k::Symbol, v::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/utils.jl:9
│││││┌ setindex!(h::Dict{Any, Any}, v::Expr, key::Symbol) @ Base ./dict.jl:392
││││││┌ ht_keyindex2_shorthash!(h::Dict{Any, Any}, key::Symbol) @ Base ./dict.jl:335
│││││││┌ rehash!(h::Dict{Any, Any}, newsz::Int64) @ Base ./dict.jl:194
││││││││ runtime dispatch detected: Base.hashindex(%236::Any, %19::Int64)::Tuple{Int64, UInt8}
│││││││└────────────────────
│││┌ store!(env::Dict{Any, Any}, name::Symbol, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:20
││││ runtime dispatch detected: (%12::Any MacroTools.:(==) ex::Expr)::Any
│││└────────────────────
│││┌ store!(env::Dict{Any, Any}, name::Symbol, ex::Expr) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:20
││││ runtime dispatch detected: MacroTools.:!(%14::Any)::Any
│││└────────────────────
││┌ match(pat::Expr, ex::Expr, env::Dict{Any, Any}) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:104
│││┌ store!(env::Dict{Any, Any}, name::Symbol, ex::Vector{Any}) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:20
││││ runtime dispatch detected: (%12::Any MacroTools.:(==) ex::Vector{Any})::Any
│││└────────────────────
││┌ match(pat::Expr, ex::Expr, env::Dict{Any, Any}) @ MacroTools /home/tim/.julia/packages/MacroTools/Ar0jT/src/match/match.jl:105
│││ runtime dispatch detected: MacroTools.match_inner(%91::Any, %92::Any, env::Dict{Any, Any})::Any
││└────────────────────

```

Does that really need to be a `Dict{Any,Any}` or could it be something more specific? Are there type-asserts you can add? Do the returned `Union`s have to be so complicated?

For people who want to fix the problems with MacroTools, these are really easy things to check. Fixing them could be harder, but until you actually look you just don’t know.

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [July 17, 2025, 12:47pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/12 "2025-07-17T12:47:36Z")

</div>

> [@tim.holy](#):
>
> I’m not a MacroTools user, so this isn’t as easy for me as it would be for y’all. But I’m a big fan of just measuring things. Using an example from the manual: `macroexpand(Main, :(@capture(:[1, 2, 3, 4, 5, 6, 7], [1, a_, 3, b__, c_])))`

I think you’re missing the point, here: `@capture` is something that is intended to be used in the _body_ of a macro, not in the generated code. An example they gave in the manual of a typical pattern is:

```julia
macro foo(ex)
  postwalk(ex) do x
    @capture(x, some_pattern) || return x
    return new_x
  end
end

```

> [@tim.holy](#):
>
> Does that really need to be a `Dict{Any,Any}` or could it be something more specific?

Usually, code operating on expressions (`Expr`) is type-unstable, because expressions contain a `Vector{Any}`.

In any case, none of the `@capture`-generated code executes at runtime, so what does a `Dict{Any,Any}` matter?

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 17, 2025, 3:12pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/13 "2025-07-17T15:12:28Z")

</div>

Yes, if it never appears in the final code, agreed.

The reason I initially reported issues with MacroTools is not because I was checking it directly: it’s because ages ago (back when I was working on reducing latency in the SciML stack in preparation for Julia 1.8), an invalidation hunt in widely-used packages traced a substantial number of invalidations to MacroTools. I made 2 PRs trying to fix it before deciding it was a bigger job than I was prepared to tackle. I think SciML responded by reducing their usage of MacroTools. That’s really all I know. And MacroTools may have changed since.

---

<div class="post-metadata">

### Author: ![cstjean](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cstjean/32/1444_2.png) [@cstjean](https://discourse.julialang.org/u/cstjean)
#### Post date: [July 18, 2025, 2:56am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/14 "2025-07-18T02:56:44Z")

</div>

> [@tim.holy](#):
>
> I think SciML responded by reducing their usage of MacroTools.

Correct. They’re using MLStyle now. I’d be interested to know if it lead to any measurable difference loading/precompilation time. @cryptic.ax ?

> [@](#):
>
> That’s really all I know. And MacroTools may have changed since.

It hasn’t.

> [@tim.holy](#):
>
> Does that really need to be a `Dict{Any,Any}` or could it be something more specific? Are there type-asserts you can add? Do the returned `Union`s have to be so complicated?
> 
> For people who want to fix the problems with MacroTools, these are really easy things to check. Fixing them could be harder, but until you actually look you just don’t know.

Thank you for the examples and workflow, that’s a great starting point. I feel like working on `Expr` trees is unavoidably type-unstable, but I’m sure it can be improved.

---

<div class="post-metadata">

### Author: ![cryptic.ax](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cryptic.ax/32/220540_2.png) [@cryptic.ax](https://discourse.julialang.org/u/cryptic.ax)
#### Post date: [July 18, 2025, 4:59am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/15 "2025-07-18T04:59:04Z")

</div>

We still have MacroTools in a couple places iirc. I do not have any timing numbers on me for what MLStyle costs, so can’t comment there, but we’re also gradually shifting to Moshi/ExproniconLite now.

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 19, 2025, 8:30am UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/16 "2025-07-19T08:30:21Z")

</div>

> I feel like working on `Expr` trees is unavoidably type-unstable, but I’m sure it can be improved.

Is it, but this issue has also been faced repeatedly in Julia’s type inference and in JuliaInterpreter. In many cases you know what kinds of objects go in which “slots” of a `Vector{Any}` and can add type-asserts or `if isa(x, Symbol)` and eliminate inference failures that way. The more restrictions you can place on what goes into those Exprs, the more often you can eliminate inference failures.

> [@cstjean](#):
>
> I’d be interested to know if it lead to any measurable difference loading/precompilation time.

That’s less likely to be directly the issue. Instead the issue is this: a package developer might do a nice thing for their users and add a bunch of `@compile_workload`s to minimize TTFX. For the purposes of discussion, let’s say that

```julia
using CoolPackage
do_something_cool(args...)

```

initially had a TTFX (for the `do_something_cool` call) of 15s, and thanks to those `@compile_workload`s the package maintainer was able to knock that down to just 0.2s. Big win! However, if you have invalidation risk then all that lovely precompiled code may have to be thrown out if your users do this instead:

```julia
using CoolPackage
using InlineStrings, BFloat16
do_something_cool(args...)

```

If the code underlying `do_something_cool` (and all its callees) gets invalidated by loading those extra packages, suddenly you might be right back up to a 15s TTFX. Terrible outcome. But it depends on the specific vulnerabilities in `CoolPackage` and the specific extra packages you load.

Ecosystem composability is the main reason it’s worth eliminating invalidation risk wherever you can.

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [July 19, 2025, 1:10pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/17 "2025-07-19T13:10:47Z")

</div>

> [@tim.holy](#):
>
> However, if you have invalidation risk then all that lovely precompiled code may have to be thrown out if

If type-unstable code working with `Expr` trees, or using MacroTools, only appears in macro bodies, then no invalidations will occur for the generated & precompiled code.

Code doing _runtime_ evaluation of `Expr` trees should be quite unusual — mainly interactive interpreter code (like the REPL or IJulia), parsers like JuliaSyntax, and other parts of a compiler/interpreter toolchain.

---

<div class="post-metadata">

### Author: ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)
#### Post date: [July 22, 2025, 2:20pm UTC](https://discourse.julialang.org/t/macro-utilities-for-working-with-function-definitions-in-2025/130687/18 "2025-07-22T14:20:53Z")

</div>

I was analyzing invalidations for someone on RxInfer.jl and stumbled across a few MacroTools cases, so I’ll post a few samples here. First the high-level result:

```julia
julia> thinned = filtermod(MacroTools, trees; recursive=true)
3-element Vector{SnoopCompile.MethodInvalidations}:
 inserting ==(left::GraphPPL.VariableRef, right) @ GraphPPL ~/.julia/packages/GraphPPL/xPNyo/src/graph_engine.jl:913 invalidated:
   backedges: 1: superseding ==(x, y) @ Base Base.jl:165 with MethodInstance for ==(::Any, ::Symbol) (108 children)

 inserting show(io::IO, ::Type{<:RxInferenceEventExecutor}) @ RxInfer ~/.julia/packages/RxInfer/gOFgs/src/inference/streaming.jl:282 invalidated:
   backedges: 1: superseding show(io::IO, x::Type) @ Base show.jl:953 with MethodInstance for show(::IOBuffer, ::Type) (165 children)

 inserting ==(left, right::GraphPPL.VariableRef) @ GraphPPL ~/.julia/packages/GraphPPL/xPNyo/src/graph_engine.jl:918 invalidated:
   backedges: 1: superseding ==(x, y) @ Base Base.jl:165 with MethodInstance for ==(::Symbol, ::Any) (230 children)

```

Specific causes (a sampling):

- [JLD2.jl/src/macros\_utils.jl at f6f1d0d6ba4b03340d7d65b47c121ae21aa700de · JuliaIO/JLD2.jl · GitHub](https://github.com/JuliaIO/JLD2.jl/blob/f6f1d0d6ba4b03340d7d65b47c121ae21aa700de/src/macros_utils.jl#L208-L214)
- [ReactiveMP.jl/src/rule.jl at c355e6771055912354a05825d231383f1dacf4bb · ReactiveBayes/ReactiveMP.jl · GitHub](https://github.com/ReactiveBayes/ReactiveMP.jl/blob/c355e6771055912354a05825d231383f1dacf4bb/src/rule.jl#L170)
- [GraphPPL.jl/src/plugins/meta/meta\_macro.jl at b9f19f27fe4cc1664450275fad6cb34794cd20d6 · ReactiveBayes/GraphPPL.jl · GitHub](https://github.com/ReactiveBayes/GraphPPL.jl/blob/b9f19f27fe4cc1664450275fad6cb34794cd20d6/src/plugins/meta/meta_macro.jl#L48)
- [Unrolled.jl/src/Unrolled.jl at e09fd85b8ec4fd84cbfd78df675938a5945c437e · cstjean/Unrolled.jl · GitHub](https://github.com/cstjean/Unrolled.jl/blob/e09fd85b8ec4fd84cbfd78df675938a5945c437e/src/Unrolled.jl#L120)

I don’t know these code-bases, but I’d guess that most or all of these seem likely to be only called from macros. The main consequence is slowing precompilation. But post-expansion “runtime code”—the code created from the macros—should not be affected.
