A macro to inspect global variables used in a function?

Hello,
When packing inside a function a code previously developed on the Main module in the command line (e.g. work done in a Jupyter notebook), it’s easy to forget to put parameters as function arguments. See e.g. this simplified example:

# first REPL work:
a = 1.0
b = 2.0
c = sin(a+b) # 0.14112

# Then consolidated in a function:
fun_c(a) = sin(a+b) # oops, b is forgotten as an argument
# quick test:
fun_c(a) # 0.14112 → apparently it works, but will in a later session...

Now, it was mentioned e.g. in Benny’s answer here that the use of a global variable inside a function can be spotted with @code_warntype, as it gets prefixed by Main.:

julia> @code_warntype fun_c(a)
MethodInstance for fun_c(::Float64)
  from fun_c(a) @ Main REPL[4]:1
Arguments
  #self#::Core.Const(fun_c)
  a::Float64
Body::Any
1 ─ %1 = (a + Main.b)::Any # Ohoh Main.b...
│   %2 = Main.sin(%1)::Any
└──      return %2

However, this needs to be distinguished from the legitimate use of Main.sim. Also, it a largish real life function (ex: large JuMP model), these Main.variable text fragments may get hidden.

So my question is: is there a simple macro to just list all the external references used in a function?

I’m not aware of such a macro … it seems the information is easily available from the lowered code though and the following might be a start:

macro warn_nonconst_globals(expr)
    :(let low = @code_lowered($expr)
          walker(x) = if x isa GlobalRef && !isconst(x); println(x) end
          MacroTools.postwalk.(Ref(walker), low.code)
          nothing
     end)
end
julia> @warn_nonconst_globals fun_c(a)
Main.b
1 Like

Note that @code_warntype will show any type instability in red. This implies that if you have included any global variable, the result will include some red line.

In case your question is more general about forgetting to add variables, it’s probably a symptom that you have long functions. This means that if you have long functions, it’s probably better if you split them into smaller functions.
Overall, it’s always recommend to have self contained functions. As for parameters, you can handle them by having a tuple of them that you unpack in the first line of the function. Alternatively, you could define the parameters globally as const if you know they won’t change their value.

edit: maybe this package that forces you to write stable functions can be helpful?

Very useful, thanks!

Testing it on largish functions, the only improvements I see are:

  • memorize symbols already met to avoid duplicate prints
  • also analyze sub-functions (“Step Into” using debuggers’ vocabulary)

Also, I found a tricky case where the global gets undetected when used in a JuMP macro:

# using JuMP; model creation...
@variable(model, 0 <= x  <= x_bound)

x_bound isn’t detected. And detecting it involves not sending a false positive about x which ends up being locally bound by @variable