This commit is contained in:
Aaron Smith 2023-09-13 13:11:38 +01:00
parent d353f71619
commit 0d542c66a6
10 changed files with 76 additions and 105 deletions

View file

@ -7,8 +7,7 @@ local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create_node = graph.create_node local create_node = graph.create_node
local get_scope = graph.get_scope local get_scope = graph.get_scope
local open_scope = graph.open_scope local evaluate_node = graph.evaluate_node
local close_scope = graph.close_scope
local set_owner = graph.set_owner local set_owner = graph.set_owner
-- todo: replace with throw's method -- todo: replace with throw's method
@ -47,19 +46,14 @@ function create_binding<T>(updater: (T) -> T, binding_data: T)
-- end -- end
-- end -- end
local binding = create_node(binding_data) local binding = create_node(binding_data, updater)
binding.effect = updater
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot bind property in non-reactive scope") end if not owner then throw("cannot bind property in non-reactive scope") end
assert(owner) assert(owner)
set_owner(binding, owner) set_owner(binding, owner)
open_scope(binding) evaluate_node(binding)
updater(binding_data)
close_scope()
end end
type PropertyBinding = { type PropertyBinding = {

View file

@ -6,26 +6,17 @@ local create_node = graph.create_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local get_scope = graph.get_scope local get_scope = graph.get_scope
local open_scope = graph.open_scope local evaluate_node = graph.evaluate_node
local close_scope = graph.close_scope
local function derive<T>(fn: () -> T): () -> T local function derive<T>(fn: () -> T): () -> T
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end if not owner then throw("cannot derive in non-reactive scope") end
assert(owner) assert(owner)
local node = create_node((false :: any) :: T) local node = create_node(false :: any, fn)
node.effect = function()
return fn()
end
set_owner(node, owner) set_owner(node, owner)
open_scope(node) evaluate_node(node)
node.cache = fn()
close_scope()
return function() return function()
track(node) track(node)

View file

@ -4,24 +4,18 @@ local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local create_node = graph.create_node local create_node = graph.create_node
local get_scope = graph.get_scope local get_scope = graph.get_scope
local open_scope = graph.open_scope local evaluate_node = graph.evaluate_node
local close_scope = graph.close_scope
local set_owner = graph.set_owner local set_owner = graph.set_owner
local function effect<T>(effect: (T) -> T, initial_value: T) local function effect<T>(effect: (T) -> T, initial_value: T)
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot effect in non-reactive scope") end if not owner then
assert(owner) throw("cannot create effect in non-reactive scope")
end; assert(owner)
local node = create_node(initial_value)
node.effect = effect
local node = create_node(initial_value, effect)
set_owner(node, owner) set_owner(node, owner)
open_scope(node) evaluate_node(node)
effect(initial_value)
close_scope()
end end
return effect :: (<T>(effect: (T) -> T, initial_value: T) -> ()) & ((effect: () -> ()) -> ()) return effect :: (<T>(effect: (T) -> T, initial_value: T) -> ()) & ((effect: () -> ()) -> ())

View file

@ -100,7 +100,8 @@ end
local function run_cleanups<T>(node: Node<T>) local function run_cleanups<T>(node: Node<T>)
if node.cleanups then if node.cleanups then
for _, fn in next, node.cleanups do for _, fn in next, node.cleanups do
fn() local ok, err: string? = pcall(fn)
if not ok then throw(`cleanup error: {err}`) end
end end
table.clear(node.cleanups) table.clear(node.cleanups)
end end
@ -134,6 +135,26 @@ local function destroy<T>(node: Node<T>)
end end
end end
local function evaluate_node<T>(node: Node<T>)
local cur_value = node.cache
run_cleanups(node) -- todo: move in scope?
open_scope(node)
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
close_scope()
if not ok then
throw(`side-effect error\n{new_value}`)
end
node.cache = new_value
return cur_value ~= new_value -- node has changed value
end
local update_queue = {} :: { Node<any> } local update_queue = {} :: { Node<any> }
local function update<T>(node: StartNode<T>) local function update<T>(node: StartNode<T>)
@ -144,6 +165,10 @@ local function update<T>(node: StartNode<T>)
local first_update = n0 == 0 local first_update = n0 == 0
local n = n0 local n = n0
if first_update then
table.clear(update_queue)
end
do do
local child = children[1] local child = children[1]
while child do -- todo: case where child in owner context while child do -- todo: case where child in owner context
@ -158,16 +183,9 @@ local function update<T>(node: StartNode<T>)
for i = n0 + 1, n do for i = n0 + 1, n do
local child = update_queue[i] local child = update_queue[i]
if not child.effect then continue end
local old_value = child.cache if evaluate_node(child) then
open_scope(child)
run_cleanups(child)
local new_value = child.effect and child.effect(old_value)
close_scope()
if old_value ~= new_value then
child.cache = new_value
update(child) update(child)
end end
end end
@ -184,11 +202,11 @@ local function track<T>(node: StartNode<T>)
end end
end end
local function create_node<T>(value: T): Node<T> local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
local node: Node<T> = { local node: Node<T> = {
cache = value, cache = value,
owner = false, owner = false,
effect = false :: false, effect = effect,
cleanups = false :: false, cleanups = false :: false,
parents = {}, parents = {},
children = false :: false children = false :: false
@ -216,6 +234,7 @@ end
return table.freeze { return table.freeze {
open_scope = open_scope, open_scope = open_scope,
close_scope = close_scope, close_scope = close_scope,
evaluate_node = evaluate_node,
get_scope = get_scope, get_scope = get_scope,
get_stack_scope = get_stack_scope, get_stack_scope = get_stack_scope,
add_cleanup = add_cleanup, add_cleanup = add_cleanup,

View file

@ -6,6 +6,7 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local root = require(script.root) local root = require(script.root)
local mount = require(script.mount)
local create = require(script.create) local create = require(script.create)
local apply = require(script.apply) local apply = require(script.apply)
local source = require(script.source) local source = require(script.source)
@ -13,11 +14,12 @@ local effect = require(script.effect)
local cleanup = require(script.cleanup) local cleanup = require(script.cleanup)
local untrack = require(script.untrack) local untrack = require(script.untrack)
local derive = require(script.derive) local derive = require(script.derive)
local match = require(script.match)
local indexes, values = require(script.maps)() local indexes, values = require(script.maps)()
local spring, update_springs = require(script.spring)() local spring, update_springs = require(script.spring)()
local action = require(script.action)() local action = require(script.action)()
local changed = require(script.changed)
local throw = require(script.throw) local throw = require(script.throw)
local _, sweep = require(script.on_gc)()
local flags = require(script.flags) local flags = require(script.flags)
export type Source<T> = source.Source<T> export type Source<T> = source.Source<T>
@ -30,13 +32,6 @@ local function step(dt: number)
update_springs(dt) update_springs(dt)
if game then
debug.profileend()
debug.profilebegin("VIDE GARBAGE CLEANUP")
end
sweep()
if game then if game then
debug.profileend() debug.profileend()
debug.profileend() debug.profileend()
@ -50,10 +45,12 @@ end)
local vide = { local vide = {
-- core -- core
root = root, root = root,
mount = mount,
create = create, create = create,
source = source, source = source,
effect = effect, effect = effect,
derive = derive, derive = derive,
match = match,
indexes = indexes, indexes = indexes,
values = values, values = values,
@ -66,6 +63,7 @@ local vide = {
-- actions -- actions
action = action, action = action,
changed = changed,
-- flags -- flags
strict = (nil :: any) :: boolean, strict = (nil :: any) :: boolean,

View file

@ -1,3 +1,5 @@
if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
@ -15,18 +17,23 @@ local destroy = graph.destroy
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local function match<T, U>(source: () -> T, map: Map<T, () -> U>): () -> U local function match<T, U>(source: () -> T, map: Map<T, () -> U?>): () -> U?
local owner = get_scope() local owner = get_scope()
assert(owner) assert(owner)
local match_updater = create_node(nil) local match_updater = create_node(nil :: U?)
function match_updater.effect() function match_updater.effect()
local value = source() local value = source()
open_scope(owner) open_scope(owner)
local component = map[value]() local component = map[value]()
close_scope() close_scope()
output.cache = component return component
end end
local output = create_start_node() return function()
track(match_updater)
return match_updater.cache
end end
end
return match

View file

@ -1,40 +0,0 @@
if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw)
-- array of all cleanup callbacks
local cleanup_callbacks = {} :: { [number]: () -> () } -- always dense
-- weak array of all cleanup lifetimes
local cleanup_lifetime = {} :: { [number]: unknown } -- can be sparse
setmetatable(cleanup_lifetime :: any, { __mode = "v" })
local function on_gc(lifetime: unknown, callback: () -> ())
local id = #cleanup_callbacks + 1
cleanup_lifetime[id :: any] = lifetime -- todo
cleanup_callbacks[id] = callback
end
local function sweep()
for id = #cleanup_callbacks, 1, -1 do
if cleanup_lifetime[id] == nil then -- lifetime was garbage collected
local callback = cleanup_callbacks[id]
do -- swap and pop
local max_id = #cleanup_callbacks
cleanup_callbacks[id] = cleanup_callbacks[max_id]
cleanup_callbacks[max_id] = nil
cleanup_lifetime[id] = cleanup_lifetime[max_id]
cleanup_lifetime[max_id] = nil
end
local ok, err: string? = pcall(callback)
if not ok then warn(`error occured during cleanup: {err}`) end
end
end
end
return function() return on_gc, sweep end

View file

@ -1,30 +1,31 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local on_gc = require(script.Parent.on_gc)()
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create_node = graph.create_node local create_node = graph.create_node
local open_scope = graph.open_scope local open_scope = graph.open_scope
local close_scope = graph.close_scope local close_scope = graph.close_scope
local get_scope = graph.get_scope
local destroy = graph.destroy local destroy = graph.destroy
local refs = {} local refs = {}
local function root<T>(fn: () -> T): (T, () -> ()) local function root<T>(fn: () -> T): (T, () -> ())
local node = create_node(false) local node = create_node(false, false)
open_scope(node) open_scope(node)
local ok, v = pcall(fn) local ok, result = pcall(fn)
close_scope() close_scope()
refs[node] = true if not ok then
throw(`mount error\n{result}`)
end
return v, function() refs[node] = true -- prevent gc of root node
return result, function()
refs[node] = nil refs[node] = nil
destroy(node) destroy(node)
end end

View file

@ -3,7 +3,6 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create_start_node = graph.create_start_node local create_start_node = graph.create_start_node
local get_scope = graph.get_scope
local track = graph.track local track = graph.track
local update = graph.update local update = graph.update

View file

@ -31,6 +31,14 @@ end
local NIL = nil local NIL = nil
-- vide.mount(function()
-- local src = vide.source(0)
-- vide.effect(function()
-- axasd += 1
-- end)
-- end)
TEST("graph", function() TEST("graph", function()
local create_node = graph.create_node local create_node = graph.create_node
local create_start_node = graph.create_start_node local create_start_node = graph.create_start_node