Skip to content

Commit

Permalink
Support dependent bound variables for SliderServer (#2014)
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin Lungwitz <Benjamin.Lungwitz@d-fine.de>
Co-authored-by: Fons van der Plas <fonsvdplas@gmail.com>
  • Loading branch information
3 people authored May 18, 2022
1 parent 76e4dd6 commit 1179cf6
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 5 deletions.
30 changes: 29 additions & 1 deletion src/evaluation/Run.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ import REPL:ends_with_semicolon
import .Configuration
import .ExpressionExplorer: FunctionNameSignaturePair, is_joined_funcname, UsingsImports, external_package_names
import .WorkspaceManager: macroexpand_in_workspace
import .MoreAnalysis: find_bound_variables

Base.push!(x::Set{Cell}) = x

"""
```julia
set_bond_value_pairs!(session::ServerSession, notebook::Notebook, bond_value_pairs::Vector{Tuple{Symbol, Any}})
```
Given a list of tuples of the form `(bound variable name, (untransformed) value)`, assign each (transformed) value to the corresponding global bound variable in the notebook workspace.
`bond_value_pairs` can also be an iterator.
"""
function set_bond_value_pairs!(session::ServerSession, notebook::Notebook, bond_value_pairs)
for (bound_sym, new_value) in bond_value_pairs
WorkspaceManager.eval_in_workspace((session, notebook), :($(bound_sym) = Main.PlutoRunner.transform_bond_value($(QuoteNode(bound_sym)), $(new_value))))
end
end

const _empty_bond_value_pairs = zip(Symbol[],Any[])

"""
Run given cells and all the cells that depend on them, based on the topology information before and after the changes.
"""
Expand All @@ -16,6 +34,7 @@ function run_reactive!(
roots::Vector{Cell};
deletion_hook::Function = WorkspaceManager.move_vars,
user_requested_run::Bool = true,
bond_value_pairs=_empty_bond_value_pairs,
)::TopologicalOrder
withtoken(notebook.executetoken) do
run_reactive_core!(
Expand All @@ -26,6 +45,7 @@ function run_reactive!(
roots;
deletion_hook,
user_requested_run,
bond_value_pairs
)
end
end
Expand All @@ -44,7 +64,8 @@ function run_reactive_core!(
roots::Vector{Cell};
deletion_hook::Function = WorkspaceManager.move_vars,
user_requested_run::Bool = true,
already_run::Vector{Cell} = Cell[]
already_run::Vector{Cell} = Cell[],
bond_value_pairs = _empty_bond_value_pairs,
)::TopologicalOrder
@assert !isready(notebook.executetoken) "run_reactive_core!() was called with a free notebook.executetoken."
@assert will_run_code(notebook)
Expand Down Expand Up @@ -159,6 +180,13 @@ function run_reactive_core!(
capture_stdout = session.options.evaluation.capture_stdout,
)
any_interrupted |= run.interrupted

# Support one bond defining another when setting both simultaneously in PlutoSliderServer
# /~https://github.com/fonsp/Pluto.jl/issues/1695

# set the redefined bound variables to their original value from the request
defs = notebook.topology.nodes[cell].definitions
set_bond_value_pairs!(session, notebook, Iterators.filter(((sym,val),) -> sym defs, bond_value_pairs))
end

cell.running = false
Expand Down
7 changes: 3 additions & 4 deletions src/evaluation/RunBonds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ function set_bond_values_reactive(;
end

new_values = Any[notebook.bonds[bound_sym].value for bound_sym in syms_to_set]
bond_value_pairs = zip(syms_to_set, new_values)

function custom_deletion_hook((session, notebook)::Tuple{ServerSession,Notebook}, old_workspace_name, new_workspace_name, to_delete_vars::Set{Symbol}, methods_to_delete::Set{Tuple{UUID,FunctionName}}, to_reimport::Set{Expr}, invalidated_cell_uuids::Set{UUID}; to_run::AbstractVector{Cell})
to_delete_vars = Set([to_delete_vars..., syms_to_set...]) # also delete the bound symbols
WorkspaceManager.move_vars((session, notebook), old_workspace_name, new_workspace_name, to_delete_vars, methods_to_delete, to_reimport, invalidated_cell_uuids)
for (bound_sym, new_value) in zip(syms_to_set, new_values)
WorkspaceManager.eval_in_workspace((session, notebook), :($(bound_sym) = Main.PlutoRunner.transform_bond_value($(QuoteNode(bound_sym)), $(new_value))))
end
set_bond_value_pairs!(session, notebook, zip(syms_to_set, new_values))
end
to_reeval = where_referenced(notebook, notebook.topology, Set{Symbol}(syms_to_set))

run_reactive_async!(session, notebook, to_reeval; deletion_hook=custom_deletion_hook, user_requested_run=false, run_async=false, kwargs...)
run_reactive_async!(session, notebook, to_reeval; deletion_hook=custom_deletion_hook, user_requested_run=false, run_async=false, bond_value_pairs, kwargs...)
end

"""
Expand Down
119 changes: 119 additions & 0 deletions test/Bonds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,123 @@ import Distributed
end)
Distributed.rmprocs(test_proc)
end

@testset "Dependent Bound Variables" begin
🍭 = ServerSession()
🍭.options.evaluation.workspace_use_distributed = false
fakeclient = ClientSession(:fake, nothing)
🍭.connected_clients[fakeclient.id] = fakeclient
🍭.options.evaluation.workspace_use_distributed = true
notebook = Notebook([
Cell(raw"""@bind x HTML("<input type=range min=1 max=10>")"""),
Cell(raw"""@bind y HTML("<input type=range min=1 max=$(x)>")"""),
Cell(raw"""x"""), #3
Cell(raw"""y"""), #4
Cell(raw"""
begin
struct TransformSlider
range::AbstractRange
end
Base.show(io::IO, m::MIME"text/html", os::TransformSlider) = write(io, "<input type=range value=$(minimum(os.range)) min=$(minimum(os.range)) max=$(maximum(os.range))>")
Bonds.initial_value(os::TransformSlider) = Bonds.transform_value(os, minimum(os.range))
Bonds.possible_values(os::TransformSlider) = os.range
Bonds.transform_value(os::TransformSlider, from_js) = from_js * 2
end
"""),
Cell(raw"""begin
hello1 = 123
@bind a TransformSlider(1:10)
end"""),
Cell(raw"""begin
hello2 = 234
@bind b TransformSlider(1:a)
end"""),
Cell(raw"""a"""), #8
Cell(raw"""b"""), #9
Cell(raw"""hello1"""), #10
Cell(raw"""hello2"""), #11
Cell(raw"""using AbstractPlutoDingetjes"""),
])
fakeclient.connected_notebook = notebook
update_run!(🍭, notebook, notebook.cells)

function set_bond_values!(notebook:: Notebook, bonds:: Dict; is_first_value=false)
for (name, value) in bonds
notebook.bonds[name] = Dict("value" => value)
end
Pluto.set_bond_values_reactive(; session=🍭, notebook, bound_sym_names=collect(keys(bonds)), run_async=false, is_first_values=fill(is_first_value, length(bonds)))
end

@test notebook.cells[3].output.body == "missing"
@test notebook.cells[4].output.body == "missing" # no initial value defined for simple html slider (in contrast to TransformSlider)
@test notebook.cells[8].output.body == "2"
@test notebook.cells[9].output.body == "2"
@test notebook.cells[10].output.body == "123"
@test notebook.cells[11].output.body == "234"

set_bond_values!(notebook, Dict(:x => 1, :a => 1); is_first_value=true)
@test notebook.cells[3].output.body == "1"
@test notebook.cells[4].output.body == "missing" # no initial value defined for simple html slider (in contrast to TransformSlider)
@test notebook.cells[8].output.body == "2" # TransformSlider scales values *2
@test notebook.cells[9].output.body == "2"
@test notebook.cells[10].output.body == "123"
@test notebook.cells[11].output.body == "234"

set_bond_values!(notebook, Dict(:y => 1, :b => 1); is_first_value=true)
@test notebook.cells[3].output.body == "1"
@test notebook.cells[4].output.body == "1"
@test notebook.cells[8].output.body == "2"
@test notebook.cells[9].output.body == "2"
@test notebook.cells[10].output.body == "123"
@test notebook.cells[11].output.body == "234"

set_bond_values!(notebook, Dict(:x => 5))
@test notebook.cells[3].output.body == "5"
@test notebook.cells[4].output.body == "missing" # the slider object is re-defined, therefore its value is the default one

set_bond_values!(notebook, Dict(:y => 3))
@test notebook.cells[3].output.body == "5"
@test notebook.cells[4].output.body == "3"

set_bond_values!(notebook, Dict(:x => 10, :y => 5))
@test notebook.cells[3].output.body == "10"
@test notebook.cells[4].output.body == "5" # this would fail without PR #2014 - previously `y` was reset to the default value `missing`

set_bond_values!(notebook, Dict(:b => 2))
@test notebook.cells[8].output.body == "2"
@test notebook.cells[9].output.body == "4"
@test notebook.cells[10].output.body == "123"
@test notebook.cells[11].output.body == "234"

set_bond_values!(notebook, Dict(:a => 8, :b => 12))
@test notebook.cells[8].output.body == "16"
@test notebook.cells[9].output.body == "24" # this would fail without PR #2014
@test notebook.cells[10].output.body == "123"
@test notebook.cells[11].output.body == "234"

set_bond_values!(notebook, Dict(:a => 1, :b => 1))
setcode!(notebook.cells[10], "a + hello1")
setcode!(notebook.cells[11], "b + hello2")
update_run!(🍭, notebook, notebook.cells[10:11])

@test notebook.cells[10].output.body == "125"
@test notebook.cells[11].output.body == "236"

set_bond_values!(notebook, Dict(:a => 2, :b => 2))
@test notebook.cells[10].output.body == "127"
@test notebook.cells[11].output.body == "238"
set_bond_values!(notebook, Dict(:b => 3))
@test notebook.cells[10].output.body == "127"
@test notebook.cells[11].output.body == "240"
set_bond_values!(notebook, Dict(:a => 1))
@test notebook.cells[10].output.body == "125"
@test notebook.cells[11].output.body == "236" # changing a will reset b



WorkspaceManager.unmake_workspace((🍭, notebook))

end
end

0 comments on commit 1179cf6

Please sign in to comment.