Determine the scope of symbols in an Expr

I asked about this on Zulip and cleaned it up and posted to Stack Overflow but I figure it might be helpful to cross pollinate here as well. I know I’ve wondered how to do this a few times, so probably other people have too.

Here’s the question:

Is there any way to run name resolution on an arbitrary expression without running it? e.g. I would like to take an expression such as

    x = 1
    y = 2*x + 1
    z = x^2 - 1
    f(x) = 2*x + 1

and be told that the names defined in the scope of this block are x, y, z, f and the names *, +, ^, - are pulled in from outside the scope of this block. Bonus points if it can tell me that there’s a sub-scope defined in the body of f which creates it’s own name x and pulls in + from an enclosing scope.

This question appeared in the Julia Zulip community


And the answer:

Thanks to @tkf for showing me how to solve this on Zulip

We can get a list of locally defined names in the outermost scope of a julia expression like so:

ex = quote
    x = 1
    y = 2*x + 1
    z = x^2 - 1
    f(x) = 2*x + 1
using JuliaVariables, MLStyle

function get_locals(ex::Expr)
    vars = (solve_from_local ∘ simplify_ex)(ex).args[1].bounds
    map(x ->, vars)

julia> get_locals(ex)
 4-element Array{Symbol,1}:

and we can get the symbols pulled in from outside the scope like this:

_get_outers(_) = Symbol[]
_get_outers(x::Var) = x.is_global ? [] : Symbol[]
function _get_outers(ex::Expr)
    @match ex begin
        Expr(:(=), _, rhs) => _get_outers(rhs)
        Expr(:tuple, _..., Expr(:(=), _, rhs)) => _get_outers(rhs)
        Expr(_, args...) => mapreduce(_get_outers, vcat, args)

get_outers(ex) = (unique! ∘ _get_outers ∘ solve_from_local ∘ simplify_ex)(ex)

julia> get_outers(ex)
 6-element Array{Symbol,1}:
1 Like