Should macros get an `__istoplevel__` or `__scope__` argument?

Because the same code will behave differently in global vs local scope, if one wants to write a macro which can work in both, in theory it needs to know into which scope its being expanded, right?

I’m running into this trying to a solve a problem with UnPack. I noticed that @unpack accidently leaks variables into toplevel scopes, because it roughtly expands e.g. @unpack x = foo() to

varXXX = foo()
x = varXXX.x
varXXX

(The reason for doing this is to return the entire contents of foo(), rather than just the unpacked variables, consistent with Julia tuple unpacking)

This is fine in a local scope, but in a global scope it leaves all these gensym’ed varXXX variables around whose memory never gets GC’ed.

To fix this, you’d want to expand instead into the following in a global scope (but keep the original thing in a local scope, since the following would not work there correctly):

# when expanding into a global scope:
let varXXX = foo()
    global x = varXXX.x
    varXXX
end

Since macro’s currently don’t know which scope they’re in, you can’t do it. As far as I can tell, no other workaround is currently possible in Julia at all, so its impossible to write an @unpack macro which works in both scopes. If the macro could branch on something like if __istoplevel__ or something, then it would be straightforward though.

(FWIW, another place this would be useful is in my Memoization.jl package where caches need to be cleared on redefinition only in the global scope, although there, unlike here, there does exist a workaround, but which is pretty ugly)

Any thoughts on if something like this could be added? Or other potential workarounds?

1 Like

You can simple declare varXXX as local.

Scope is determined after macro expansion so this is generally impossible to know at macro expansion time.

4 Likes

Do you mean,

local x
let varXXX = foo()
    x = varXXX.x
    varXXX
end

This doesn’t work in the global scope.

Interesting didn’t know that thanks. That sounds like that will make this hard…

No, I said make varXXX local, not x. i.e just add local to varXXX.

Oh sorry, I read that wrong. Yea, that can be used to fix UnPack, thanks!

I hadn’t thought of that mainly because I hadn’t realized Julia has the following behavior:

julia> local x = 1
1

julia> x
ERROR: UndefVarError: x not defined

You learn something new about Julia scope every day :smile:

I would still find a __istoplevel__ macro argument useful fwiw, but sounds like its impossible without some structural changes, and maybe there’s less motivation for it now that I see this.

This feature (allowing local in begin in global scope) pretty much exists for this exact purpose.

3 Likes