Julia: Julep: Expr provenance when lowering

Created on 23 Feb 2019  ยท  3Comments  ยท  Source: JuliaLang/julia

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.

feature lowering

Most helpful comment

I've wanted this for a long time, though ideally with lowering from CSTParser in order to be able to give exact source locations.

All 3 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

TotalVerb picture TotalVerb  ยท  3Comments

omus picture omus  ยท  3Comments

yurivish picture yurivish  ยท  3Comments

i-apellaniz picture i-apellaniz  ยท  3Comments

sbromberger picture sbromberger  ยท  3Comments