Skip to content

Commit

Permalink
🐾 Explorer: structs and locals
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp committed Mar 16, 2020
1 parent a7e7b3d commit 56baf3b
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name = "Pluto"
uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781"
license = "MIT"
authors = ["Fons van der Plas <[email protected]>", "Mikołaj Bochenski <[email protected]>"]
version = "0.3.0"
version = "0.3.1"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand Down
16 changes: 11 additions & 5 deletions assets/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
body {
margin: 0px;
font-size: 17px;
overflow-anchor: none;
}

/* more sensible defaults for html tags: */
Expand Down Expand Up @@ -182,7 +183,6 @@
min-height: 25px;
padding-left: 10px;
padding-right: 10px;
overflow: auto;
}

code {
Expand Down Expand Up @@ -495,9 +495,10 @@ <h2>
}

function updateLocalCellOutput(cellNode, mime, output, errormessage) {
console.log(mime)
cellNode.classList.remove("running")

oldHeight = cellNode.querySelector("celloutput").scrollHeight

if (errormessage) {
cellNode.querySelector("celloutput").innerHTML = "<pre><code></code></pre>"
cellNode.querySelector("celloutput").querySelector("code").innerText = errormessage
Expand Down Expand Up @@ -538,6 +539,13 @@ <h2>
cellNode.querySelector("celloutput").querySelector("code").innerText = output
}
}

newHeight = cellNode.querySelector("celloutput").scrollHeight

focusedCell = document.querySelector("cell:focus-within")
if(focusedCell == cellNode){
window.scrollBy(0, newHeight - oldHeight)
}
}

function updateLocalCellInput(byMe, uuid, code) {
Expand Down Expand Up @@ -776,11 +784,9 @@ <h2>
startDisconnectedBanner()
}

console.log("update event received")
// console.log(event)
console.log("update received:")
try {
update = JSON.parse(event.data)
console.log("output deserialized")
console.log(update)

forMe = !(("notebookID" in update) && (update.notebookID != notebookID))
Expand Down
106 changes: 82 additions & 24 deletions src/ExploreExpression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,27 @@ function union(a::ScopeState, b::ScopeState)
end

function ==(a::SymbolsState, b::SymbolsState)
return a.references == b.references && a.assignments == b.assignments
a.references == b.references && a.assignments == b.assignments
end

function will_assign_global(assignee::Symbol, scopestate::ScopeState)::Bool
(scopestate.inglobalscope || assignee in scopestate.exposedglobals) && !(assignee in scopestate.hiddenglobals)
end

function get_global_assignees(assignee_exprs, scopestate::ScopeState)
global_assignees = Set{Symbol}()
for ae in assignee_exprs
if isa(ae, Symbol)
will_assign_global(ae, scopestate) && push!(global_assignees, ae)
else
if ae.head == :(::)
will_assign_global(ae.args[1], scopestate) && push!(global_assignees, ae.args[1])
else
@warn "Unknown assignee expression"
end
end
end
global_assignees
end

# We handle a list of function arguments separately.
Expand All @@ -46,10 +66,12 @@ function extractfunctionarguments(funcdef::Expr)::Set{Symbol}
if isa(a, Symbol)
push!(argnames, a)
elseif isa(a, Expr)
if a.head == :parameters || a.head == :tuple # second is for ((a,b),(c,d)) -> a*b*c*d stuff
if a.head == :(::)
push!(argnames, a.args[1])
elseif a.head == :parameters || a.head == :tuple # second is for ((a,b),(c,d)) -> a*b*c*d stuff
push!(argnames, extractfunctionarguments(a)...)
elseif a.head == :kw || a.head == :(=) # first is for unnamed function arguments, second is for lambdas
push!(argnames, a.args[1])
push!(argnames, extractfunctionarguments(a.args[1])...)
end
end
end
Expand Down Expand Up @@ -96,6 +118,9 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
if ex.args[1].head == :tuple
# (x, y) = (1, 23)
ex.args[1].args
elseif ex.args[1].head == :(::)
# TODO: type is referenced
[ex.args[1].args[1]]
elseif ex.args[1].head == :ref
# TODO: what is the desired behaviour here?
# right now, it registers no reference, and no assignment
Expand All @@ -115,9 +140,7 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
end
val = ex.args[2]

global_assignees = filter(assignees) do assignee
scopestate.inglobalscope || assignee in scopestate.exposedglobals
end
global_assignees = get_global_assignees(assignees, scopestate)

# If we are _not_ assigning a global variable
for assignee in setdiff(assignees, global_assignees)
Expand All @@ -144,7 +167,7 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
operator = Symbol(string(ex.head)[1:end - 1])
expanded_expr = Expr(:(=), ex.args[1], Expr(:call, operator, ex.args[1], ex.args[2]))
return explore!(expanded_expr, scopestate)
elseif ex.head == :let || ex.head == :for
elseif ex.head == :let || ex.head == :for || ex.head == :while
# Creates local scope

# Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions.
Expand All @@ -156,6 +179,21 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
end

return symstate
elseif ex.head == :struct
# Creates local scope

structname = assignee = if isa(ex.args[2], Symbol)
ex.args[2]
else
# We have: struct a <: b
ex.args[2].args[1]
# TODO: record reactive reference to type
end
structfields = ex.args[3].args

equiv_func = Expr(:function, Expr(:call, structname, structfields...), Expr(:block, nothing))

return explore!(equiv_func, scopestate)
elseif ex.head == :generator
# Creates local scope

Expand All @@ -167,36 +205,40 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
elseif ex.head == :function
# Creates local scope

funcname = assignee = ex.args[1].args[1]
funcargs = ex.args[1]
funcroot = if ex.args[1].head == :(::)
# TODO: record reactive reference to type
ex.args[1].args[1]
else
ex.args[1]
end

assigning_global = scopestate.inglobalscope || assignee in scopestate.exposedglobals

funcname = assignee = funcroot.args[1]

# is either [funcname] or []
global_assignees = get_global_assignees([funcname], scopestate)

# Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions.
innerscopestate = deepcopy(scopestate)
innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcargs))
innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcroot))
innerscopestate.inglobalscope = false
for a in ex.args[2:end]
innersymstate = explore!(a, innerscopestate)
symstate = symstate innersymstate
end

if assigning_global
scopestate.hiddenglobals = union(scopestate.hiddenglobals, [assignee])
symstate.assignments = union(symstate.assignments, [assignee])
end
scopestate.hiddenglobals = union(scopestate.hiddenglobals, global_assignees)
symstate.assignments = union(symstate.assignments, global_assignees)

return symstate
elseif ex.head == :(->)
# Creates local scope

funcargs = ex.args[1]
funcroot = ex.args[1]


# Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions.
innerscopestate = deepcopy(scopestate)
innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcargs))
innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcroot))
innerscopestate.inglobalscope = false
for a in ex.args[2:end]
innersymstate = explore!(a, innerscopestate)
Expand All @@ -212,13 +254,13 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
# global x = 1;
# global x += 1;

# `globalised` is everything that comes after `global`
# where x can also be a tuple:
# global a,b = 1,2

globalisee = ex.args[1]

if isa(globalisee, Symbol)
scopestate.exposedglobals = union(scopestate.exposedglobals, [globalisee])
# symstate.assignments = union(symstate.assignments, [globalisee])
elseif isa(globalisee, Expr)
innerscopestate = deepcopy(scopestate)
innerscopestate.inglobalscope = true
Expand All @@ -228,6 +270,24 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
@error "unknow global use"
end

return symstate
elseif ex.head == :local
# Does not create scope

# Logic similar to :global
localisee = ex.args[1]

if isa(localisee, Symbol)
scopestate.hiddenglobals = union(scopestate.hiddenglobals, [localisee])
elseif isa(localisee, Expr)
innerscopestate = deepcopy(scopestate)
innerscopestate.inglobalscope = false
innersymstate = explore!(localisee, innerscopestate)
symstate = symstate innersymstate
else
@error "unknow local use"
end

return symstate
elseif ex.head == :tuple
# Does not create scope
Expand All @@ -249,10 +309,8 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState
if indexoffirstassignment !== nothing
recursers = ex.args[indexoffirstassignment:end]

exposed = filter(ex.args[1:indexoffirstassignment - 1]) do a::Symbol
(scopestate.inglobalscope || a in scopestate.exposedglobals) && !(a in scopestate.hiddenglobals)
end

exposed = get_global_assignees(ex.args[1:indexoffirstassignment - 1], scopestate)

scopestate.exposedglobals = union(scopestate.exposedglobals, exposed)
symstate.assignments = union(symstate.assignments, exposed)
end
Expand Down
68 changes: 48 additions & 20 deletions test/ExploreExpression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ using Test
using Pluto
import Pluto.ExploreExpression: SymbolsState, compute_symbolreferences

verbose = true

function testee(expr, ref, def)
function testee(expr, ref, def, verbose=true)
expected = SymbolsState(Set(ref), Set(def))
result = compute_symbolreferences(expr)
if verbose && expected != result
Expand All @@ -21,6 +19,10 @@ function testee(expr, ref, def)
return expected == result
end

# nowarn tests are for functionality that is not yet implemented
# (but we don't want any expressions to error/warn)
# Once the functionality has been implemented, they should be changed to normal tests

@testset "Explore Expressions" begin
@testset "Basics" begin
@test testee(:(a), [:a], [])
Expand All @@ -30,25 +32,35 @@ end
@test testee(:(x = 1 + y), [:+, :y], [:x])
@test testee(:(x = +(a...)), [:+, :a], [:x])

@test_nowarn testee(:(x::Int64 = 3), [], [:x, :Int64], false)
end
@testset "Lists and structs" begin
@test testee(:(1:3), [:(:)], [])
@test testee(:(a[1:3,4]), [:a, :(:)], [])
@test testee(:(a[1:3,4] = b[5]), [:b], [])
@test testee(:(a.property), [:a], [])
@test testee(:(a.property = 1), [], [])

@test testee(:(struct a; c; d; end), [], [:a])

@test_nowarn testee(:(struct a <: b; c; d::Int64; end), [:b, :Int64], [:a], false)
end
@testset "Modifiers" begin
@test testee(:(a = a + 1), [:a, :(+)], [:a])
@test testee(:(a += 1), [:a, :(+)], [:a])
@test testee(:(a[1] += 1), [:a, :(+)], [])
@test testee(:(x = let a = 1; a += b end), [:(+), :b], [:x])
@test testee(:(minimum(x) do (a, b); a + b end), [:(+), :x, :minimum], [])
end
@testset "`for` & `while`" begin
@test testee(:(for k in 1:n; k + s; end), [:n, :s, :+, :(:)], [])
@test testee(:(for k in 1:2, r in 3:4; global z = k + r; end), [:+, :(:)], [:z])
@test testee(:(while k < 2; r = w; global z = k + r; end), [:k, :(<), :w, :+], [:z])
end
@testset "Comprehensions" begin
@test testee(:([sqrt(s) for s in 1:n]), [:sqrt, :n, :(:)], [])
@test testee(:([s + j + r + m for s in 1:3 for j in 4:5 for (r, l) in [(1, 2)]]), [:+, :m, :(:)], [])
# @test testee(:([a for a in a]), [:a], [])
# @test testee(:(a = [a for a in a]), [:a], [:a])

@test testee(:("a $(b = c)"), [:c], [:b])
@test_nowarn testee(:([a for a in a]), [:a], [], false)
@test_nowarn testee(:(a = [a for a in a]), [:a], [:a], false)
end
@testset "Multiple expressions" begin
@test testee(:(x = let r = 1; r + r end), [:+], [:x])
Expand All @@ -57,29 +69,42 @@ end
@test testee(:((k = 2; 123)), [], [:k])
@test testee(:((a = 1; b = a + 1)), [:+], [:a, :b])
@test testee(:(let k = 2; 123 end), [], [])

@test_nowarn testee(:(a::Int64, b::String = 1, "2"), [:Int64, :String], [:a, :b], false)
end
@testset "Functions" begin
@test testee(:(function g() r = 2; r end), [], [:g])
@test testee(:(function f(x, y = 1; r, s = 3 + 3) r + s + x * y * z end), [:z, :+, :*], [:f])
@test testee(:(function f(x) x * y * z end), [:y, :z, :*], [:f])
@test testee(:(function f(x) x = x / 3; x end), [:/], [:f])
@test testee(:(f = x->x * y), [:y, :*], [:f])
@test testee(:(f = (x, y)->x * y), [:*], [:f])
@test testee(:(f(x, y = a + 1) = x * y * z), [:*, :z], [:f])
@test testee(:((((a, b), c), (d, e))->a * b * c * d * e * f), [:*, :f], [])
@test testee(:(f = (x, y = a + 1)->x * y), [:*], [:f])
@test testee(:(function g() r = 2; r end), [], [:g])
@test testee(:(function f(x, y = 1; r, s = 3 + 3) r + s + x * y * z end), [:z, :+, :*], [:f])
@test testee(:(function f(x, y = a; r, s = b) r + s + x * y * z end), [:z, :+, :*], [:f])
@test testee(:(function f(x) x * y * z end), [:y, :z, :*], [:f])
# @test testee(:(function f(x::T; k = 1) where T return x+1 end), [:+], [:f])
@test testee(:(minimum(x) do (a, b); a + b end), [:(+), :x, :minimum], [])

@test_nowarn testee(:(function f(y::Int64 = a)::String string(y) end), [:Int64, :String, :string], [:f], false)
@test_nowarn testee(:(function f(x::T; k = 1) where T return x + 1 end), [:+], [:f], false)
end
@testset "Global exposure" begin
@testset "Scope modifiers" begin
@test testee(:(let global a, b = 1, 2 end), [], [:a, :b])
@test testee(:(let global k = 3; 123 end), [], [:k])
@test testee(:(let global k; k = 2123 end), [], [:k])
@test testee(:(let global a; b = 1 end), [], [])
@test testee(:(let global k = 3 end), [], [:k])
@test testee(:(let global k += 3 end), [:+, :k], [:k])
@test testee(:(let global k; k = 4 end), [], [:k])
@test testee(:(let global k; b = 5 end), [], [])

@test testee(:(begin local a, b = 1, 2 end), [], [])
@test testee(:(begin local k = 3 end), [], [])
@test testee(:(begin local k += 3 end), [:+], [])
@test testee(:(begin local k; k = 4 end), [], [])
@test testee(:(begin local k; b = 5 end), [], [:b])

@test testee(:(function f(x) global k = x end), [], [:k, :f])
@test testee(:((begin x = 1 end, y)), [:y], [:x])
@test testee(:(x = let global a += 1 end), [:(+), :a], [:x, :a])
end
@testset "import/using" begin
@testset "`import` & `using`" begin
@test testee(:(using Plots), [], [:Plots])
@test testee(:(using JSON, UUIDs), [], [:JSON, :UUIDs])
@test testee(:(import Pluto), [], [:Pluto])
Expand All @@ -90,6 +115,9 @@ end
@test testee(:(html"a $(b = c)"), [Symbol("@html_str")], [])
@test testee(:(md"a $(b = c)"), [Symbol("@md_str"), :c], [:b])
@test testee(:(md"a \$(b = c)"), [Symbol("@md_str")], [])
# @test testee(:(), [], [])
end
@testset "String interpolation" begin
@test testee(:("a $b"), [:b], [])
@test testee(:("a $(b = c)"), [:c], [:b])
end
end

2 comments on commit 56baf3b

@fonsp
Copy link
Owner Author

@fonsp fonsp commented on 56baf3b Mar 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/11038

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if Julia TagBot is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.1 -m "<description of version>" 56baf3beb9307d53594ccad45c0cb779db488c80
git push origin v0.3.1

Please sign in to comment.