How to get the free variables of a function?

Is there a way to get thefree variables of a function?
For example:

myfunc(a) = a+some_func(b,a)
free_variables(myfunc) # returns (:b,)  or  (:b, :some_func) is also fine

A follow-up question (that I will likely have to ask somewhere else): Is there a lint in vscode that marks these variables as they could prevent some bugs?

The technical term for this is “free variables”: Free variables and bound variables - Wikipedia. I don’t know of a utility function that computes that list. Perhaps @jeff.bezanson knows of one.

probably meant nonlocal_vars(myfunc).

In theory, this shoud be derivable from the lowered form of the function.

I got it to work. I’m sure it is not perfect but it works atleast for my test case. Not sure what I need to do with the allowed_modules in the free_variables function.

For future reference, I will edit the title to contain free variables and I will fix my typo in the question.

A short explanation: free variables are put in the lowered code as Main.<>. But also functions get put in it as Main.<>. So we need to filter the valid functions out. (i.e. those that are really defined in Main).

If the goal was to only get free symbols, not free functions. Then expr.name in defined_in_main would just become something like getproperty(Main,expr.name) isa Function.

using MacroTools

macro free_variables(ex0...)
    thecall = InteractiveUtils.gen_call_with_extracted_types_and_kwargs(__module__, :free_variables, ex0)
    quote
        local results = $thecall
        length(results) == 1 ? results[1] : results
    end
end

function free_variables(@nospecialize(f), @nospecialize(t=Tuple))
    allowed_from_main = filter(name->getproperty(Main,name) isa Function,names(Main,imported=true))
    allowed_modules = (Core,Base) #setdiff(values(Base.loaded_modules),:Main)
    defined_in_main = union(map(mod->Set(names(mod,imported=true)),allowed_modules)...,allowed_from_main)

    _codelines = code_lowered(f,t)
    length(_codelines) == 0 && error("Wrong arguments given for function $f")
    codelines = length(_codelines) == 1 ? _codelines[1] : _codelines

    set = Set{Symbol}()
    for line_expr in codelines.code
        MacroTools.postwalk(line_expr) do expr
            expr isa GlobalRef && !(expr.name in defined_in_main) && push!(set,expr.name)
            expr
        end
    end
    (set...,)
end

Testing:

function hypot(x,y)
    x = abs(x+c)
    y = abs(y) + func(x)
    if x > y
        r = y/x
        return x*sqrt(1+r*r) + c + d
    end
    if y == 0
        return zero(x)
    end
    r = x/y
    return y*sqrt(1+r*r)
end

free_vars = free_variables(hypot,(Float64,Float64)) # (:d, :c, :func)
@free_variables hypot(5.,5.)  #  (:d, :c, :func)
1 Like