When lowering code we have line-number provenance, but I want expression-provenance too.
I'd love it if
ex = quote
if Sys.islinux()
f() = 1; g() = 2
else
g() = 3
end
end
thunk, src = Meta.lower_logged(m, ex)
resulted in something along the lines of
# Lowered result Corresponding src entry
:($(Expr(:thunk, CodeInfo(
@ REPL[3]:2 within `top-level scope'
1 โ %1 = Base.getproperty(Sys, :islinux) :(Sys.islinux)
โ %2 = (%1)() :(Sys.islinux())
โโโ goto #3 if not %2 ex
@ REPL[3]:3 within `top-level scope'
2 โ $(Expr(:thunk, CodeInfo( :(f() = 1)
@ none within `top-level scope'
1 โ return $(Expr(:method, :f))
)))
โ $(Expr(:method, :f)) :(f() = 1)
โ %6 = Core.Typeof(f) :(f)
โ %7 = Core.svec(%6) :(f())
โ %8 = Core.svec() :(f())
โ %9 = Core.svec(%7, %8) :(f())
โ $(Expr(:method, :f, :(%9), CodeInfo(quote :(f() = 1)
return 1
end)))
โ $(Expr(:thunk, CodeInfo( :(g() = 2)
@ none within `top-level scope'
1 โ return $(Expr(:method, :g))
)))
โ $(Expr(:method, :g)) :(g() = 2)
โ %13 = Core.Typeof(g) :(g)
โ %14 = Core.svec(%13) :(g())
โ %15 = Core.svec() :(g() )
โ %16 = Core.svec(%14, %15) :(g())
โ $(Expr(:method, :g, :(%16), CodeInfo(quote :(g() = 2)
return 2
end)))
โโโ return g :(g)
@ REPL[3]:5 within `top-level scope'
3 โ $(Expr(:method, :g)) :(g() = 3)
โ %20 = Core.Typeof(g) :(g)
โ %21 = Core.svec(%20) :(g())
โ %22 = Core.svec() :(g())
โ %23 = Core.svec(%21, %22) :(g())
โ $(Expr(:method, :g, :(%23), CodeInfo(quote :(g() = 3)
return 3
end)))
โโโ return g :(g)
))))
src here doesn't contain any "new" expressions, just references to the nested contents of ex, specifically the object you've recursed into at the moment you inserted the corresponding line in thunk. It's essentially a log of "what I'm working on now."
You can sometimes get this from the line number info, but as this example partially demonstrates it's fragile (not guaranteed to return complete expressions) and incomplete (might correspond to a block independent of expressions). Moreover, if there aren't any LineNumberNodes in ex, it's impossible. In contrast, the log approach seems quite robust.
I'd implement this myself except for the fact that the key part of this work is in scheme, and I simply don't have time to develop the relevant mastery. I should therefore say I'm not particularly picky about the exact contents of each entry in src, this is just to convey the general idea.
There are interesting questions about whether logs should have sub-logs for the lowered bodies of methods and other internal :thunk expressions. Rather than returning src one could simply make this part of CodeInfo, and then the sub-logs would happen naturally. If one went that way, then there's question of whether this information should survive compression and later retrieval with uncompressed_ast. (I'm guessing that's not on the table as I think it would substantially slow Julia's startup due to pointer relocation.) I'm fine with having this information being transient; getting it out just when lowering manually would be a big win.
I've wanted this for a long time, though ideally with lowering from CSTParser in order to be able to give exact source locations.
That would be great too, but I'd guess that's a much harder change. If this were Julia code I'd guess this one would be pretty easy, just adding src === nothing || push!(src, ex) every place you see push!(code.code, stmt).
The place this is immediately noticeable is in the debugger(s) when stepping through calls to kwarg methods; it would be great to be able to detect that all the "preparatory" steps are associated with a single call. Unfortunately there is a certain amount of diversity here:
julia> a = [4,2,3,1];
julia> m = @which sort(a)
sort(v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742
julia> Base.uncompressed_ast(m)
CodeInfo(
1 โ %1 = (Core.NamedTuple)()
โ %2 = (Base.pairs)(%1)
โ %3 = (Base.Sort.:(#sort#8))(%2, #self#, v)
โโโ return %3
)
julia> m = first(methods(getfield(Base.Sort, Symbol("#sort#8"))))
#sort#8(kws, ::Any, v::AbstractArray{T,1} where T) in Base.Sort at sort.jl:742
julia> Base.uncompressed_ast(m)
CodeInfo(
1 โ %1 = (Base.Sort.copymutable)(v)
โ %2 = (Base.NamedTuple)()
โ %3 = (Base.merge)(%2, kws)
โ %4 = (Base.isempty)(%3)
โโโ goto #3 if not %4
2 โ %6 = (Base.Sort.sort!)(%1)
โโโ return %6
3 โ %8 = (Core.kwfunc)(Base.Sort.sort!)
โ %9 = (%8)(%3, Base.Sort.sort!, %1)
โโโ return %9
)
julia> g(a) = sort(a; rev=true)
g (generic function with 1 method)
julia> Base.uncompressed_ast(first(methods(g)))
CodeInfo(
1 โ %1 = (:rev,)
โ %2 = (Core.apply_type)(Core.NamedTuple, %1)
โ %3 = (Core.tuple)(true)
โ %4 = (%2)(%3)
โ %5 = (Core.kwfunc)(Main.sort)
โ %6 = (%5)(%4, Main.sort, a)
โโโ return %6
)
That's diverse enough that I'm a bit fearful about making guesses. I think I can write a pattern-matcher (there's already a limited one) that will pick up most of these and yet not get confused with explicitly-written operations like
julia> f() = (x=1,)
f (generic function with 1 method)
julia> Base.uncompressed_ast(first(methods(f)))
CodeInfo(
1 โ %1 = (:x,)
โ %2 = (Core.apply_type)(Core.NamedTuple, %1)
โ %3 = (Core.tuple)(1)
โ %4 = (%2)(%3)
โโโ return %4
)
but I worry it might be a little magical.
Even if there's nothing stored for use by Base.uncompressed_ast, Revise is starting from top-level expressions and lowering everything again; that gives it the opportunity to hang on to any extra returned arguments from a special call to lowering that returns the provenance. Revise can then insert what it learns into its own internal data stores.
Most helpful comment
I've wanted this for a long time, though ideally with lowering from
CSTParserin order to be able to give exact source locations.