fonsp / Pluto.jl

🎈 Simple reactive notebooks for Julia
https://plutojl.org/
MIT License
4.99k stars 297 forks source link

Interpolation inside expressions #1001

Open fonsp opened 3 years ago

fonsp commented 3 years ago

Currently, we ignore all code inside a quoted expression:

julia> Base.show(io::IO, ::MIME"text/plain", b::Bool) = println(io, b ? "👍" : "😢")

julia> import Pluto: ReactiveNode

julia> ReactiveNode("""
       begin
           x = 1
           y = quote
               z = 3
           end
       end
       """).definitions
Set{Symbol} with 2 elements:
  :y
  :x

julia> :z ∉ ans
👍

which makes sense! (Unless that code is evaled within the same cell, which is almost impossible to track for us, but we want to discourage 'raw' eval altogether #279 )

However, we should detect interpolated code:

julia> ReactiveNode("""
       begin
           x = 1
           y = quote
               z = $(realz = 3)
           end
       end
       """).definitions
Set{Symbol} with 2 elements:
  :y
  :x

julia> :realz ∈ ans
😢

In fact, this can go on for multiple layers:

julia> ReactiveNode("""
       begin
           x = 1
           y = quote
               z1 = $(realz1 = quote
                   z2 = $(realz2 = 3)
               end)
           end
       end
       """).definitions
Set{Symbol} with 2 elements:
  :y
  :x

julia> [:realz1, :realz2] ⊆ ans
😢
mmulet commented 3 years ago

Code Relay task for this is here https://github.com/code-relay-io/Pluto.jl/blob/main/README.md. We will work on this issue!

cmcaine commented 3 years ago

Hello, I took a little go at this, here's an initial attempt that could be turned into a PR, probably:

using MacroTools: prewalk

function top_level_exprs(expr)
    quote_level = 0
    unquoted_exprs = [expr]
    prewalk(expr) do e
        !isa(e, Expr) && return e
        if e.head == :quote
            quote_level += 1
        elseif e.head == :$
            quote_level -= 1
            if quote_level == 0
                push!(unquoted_exprs, e.args[1])
            end
        end
        return e
    end
    return unquoted_exprs
end

And here are some examples of use that look correct to me:

julia> top_level_exprs(:(a = 1))
1-element Array{Expr,1}:
 :(a = 1)

julia> top_level_exprs(:(a = :($(b = 2))))
2-element Array{Expr,1}:
 :(a = $(Expr(:quote, :($(Expr(:$, :(b = 2)))))))
 :(b = 2)

julia> top_level_exprs(:(a = :(:($(b = 2)))))
1-element Array{Expr,1}:
 :(a = $(Expr(:quote, :($(Expr(:quote, :($(Expr(:$, :(b = 2))))))))))

julia> top_level_exprs(:(a = :(:($$(b = 2)))))
2-element Array{Expr,1}:
 :(a = $(Expr(:quote, :($(Expr(:quote, :($(Expr(:$, :($(Expr(:$, :(b = 2)))))))))))))
 :(b = 2)

# like fons' example
julia> top_level_exprs(:(:(a = $(b = :($(c = 1))))))
3-element Array{Expr,1}:
 :($(Expr(:quote, :(a = $(Expr(:$, :(b = $(Expr(:quote, :($(Expr(:$, :(c = 1)))))))))))))
 :(b = $(Expr(:quote, :($(Expr(:$, :(c = 1)))))))
 :(c = 1)

julia> top_level_exprs(Meta.parse(":(a = \$(b = :(\$(c = 1))))"))
3-element Array{Expr,1}:
 :($(Expr(:quote, :(a = $(Expr(:$, :(b = $(Expr(:quote, :($(Expr(:$, :(c = 1)))))))))))))
 :(b = $(Expr(:quote, :($(Expr(:$, :(c = 1)))))))
 :(c = 1)

I did this because of the relay thing, but also it took me closer to an hour to get to this stage than the 15 minutes it suggested. I'm not sure I could have meaningfully cut the work down, tho. Maybe I'm just a bit slow :shrug:

Anyway, hope this is useful!

cmcaine commented 3 years ago

Lol, I got the walk wrong. Too tired. Here's a better one:

function top_level_exprs2(expr)
    unquoted_exprs = Any[expr]
    function walk(e, quote_level)
        !isa(e, Expr) && return
        if e.head == :quote
            quote_level += 1
        elseif e.head == :$
            quote_level -= 1
            if quote_level == 0
                push!(unquoted_exprs, e.args[1])
            end
        end
        walk.(e.args, quote_level)
    end
    walk(expr, 0)
    return unquoted_exprs
end

Now it handles inputs like :(a = :($x; $y)) correctly.