This commit is contained in:
Aaron Smith 2023-09-05 18:15:18 +01:00
parent 0e439f084f
commit fe3af737be
11 changed files with 249 additions and 79 deletions

View file

@ -5,7 +5,10 @@ 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)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local set_effect = graph.set_effect local create = graph.create
local create_and_open_scope = graph.create_and_open_scope
local close_scope = graph.close_scope
local set_child = graph.set_child
local capture = graph.capture local capture = graph.capture
--[[ --[[
@ -71,23 +74,37 @@ function bind(instance: Instance, property: string, setter: (Instance) -> ())
end end
end end
local node = create(false)
create_and_open_scope(node)
-- run setter to capture any nodes being depended on -- run setter to capture any nodes being depended on
local nodes = (capture(setter :: () -> unknown, instance)) local nodes = (capture(setter :: () -> unknown, instance))
-- register the setter as a side-effect of each node close_scope()
for _, node in next, nodes do
set_effect(node, setter, instance)
end
-- get binding id -- get binding id
bind_count += 1 bind_count += 1
local bind_id = bind_count local bind_id = bind_count
node.effect = function()
local instance = weak[bind_id]
if instance == nil then return end
setter(weak[bind_id] :: Instance)
end
-- register the setter as a side-effect of each node
for _, n in next, nodes do
set_child(n, node)
end
-- store reference of instance proxy without preventing gc -- store reference of instance proxy without preventing gc
weak[bind_id] = instance weak[bind_id] = instance
local function ref() local function ref()
local _ = setter -- prevent gc of nodes being depended on local _ = node -- prevent gc of node being depended on
local instance = weak[bind_id] :: Instance local instance = weak[bind_id] :: Instance
-- keep proxy in memory if instance is still parented -- keep proxy in memory if instance is still parented

View file

@ -2,6 +2,17 @@ if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup
local function cleanup(fn: () -> ())
local node = get_scope()
if node == nil then throw("cannot call cleanup() in a non-reactive scope") end
add_cleanup(node, fn)
end
return cleanup
--[[ --[[
@ -22,6 +33,7 @@ todo: remove need for ref to id maps?
]] ]]
--[[
-- maps a ref to cleanup id -- maps a ref to cleanup id
local ref_to_id = {} :: { [string]: number } local ref_to_id = {} :: { [string]: number }
-- maps a cleanup id to a ref -- maps a cleanup id to a ref
@ -127,3 +139,4 @@ local manual_cleanup_mode = function(caller: () -> ()?)
end :: ( (caller: (...any) -> ()) -> () ) & ( (nil) -> { () -> () } ) end :: ( (caller: (...any) -> ()) -> () ) & ( (nil) -> { () -> () } )
return function() return cleanup, clean_garbage, manual_cleanup_mode, cleanup_ref end return function() return cleanup, clean_garbage, manual_cleanup_mode, cleanup_ref end
]]

View file

@ -3,12 +3,18 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local create = graph.create local create = graph.create
local capture_and_link = graph.capture_and_link local capture_and_link = graph.capture_and_link
local create_and_open_scope = graph.create_and_open_scope
local close_scope = graph.close_scope
local function derive<T>(fn: () -> T): () -> T local function derive<T>(fn: () -> T): () -> T
local node, read_node_value = create((false :: any) :: T) local node, read_node_value = create((false :: any) :: T)
create_and_open_scope(node)
node.cache = capture_and_link(node, fn) node.cache = capture_and_link(node, fn)
close_scope()
return read_node_value return read_node_value
end end

View file

@ -2,12 +2,13 @@ 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 on_gc = require(script.Parent.on_gc)()
export type Node<T> = { export type Node<T> = {
cache: T, cache: T,
derive: () -> T, effect: () -> (),
effects: { [(unknown) -> ()]: unknown }, -- weak values children: { Node<T> } | false, -- weak values
children: { Node<T> } | false -- weak values cleanups: { () -> () } | false
} }
-- flag used to detect when node reference capturing is active -- flag used to detect when node reference capturing is active
@ -15,6 +16,8 @@ local reff = false
-- array of all nodes referenced since above flag was set -- array of all nodes referenced since above flag was set
local refs = {} :: { Node<unknown> } local refs = {} :: { Node<unknown> }
local scopes = { n = 0 } :: { [number]: Node<unknown>, n: number }
local WEAK_VALUES = { __mode = "v" } local WEAK_VALUES = { __mode = "v" }
local EVALUATION_ERR = "error while evaluating source:\n\n" local EVALUATION_ERR = "error while evaluating source:\n\n"
@ -46,6 +49,39 @@ local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
end end
end end
local function get_scope(): Node<unknown>
return scopes[scopes.n]
end
local function open_scope(node: Node<unknown>)
local n = scopes.n + 1
scopes.n = n
scopes[n] = node
end
local function close_scope()
local n = scopes.n
scopes.n = n - 1
scopes[n] = nil
end
local function add_cleanup(node: Node<unknown>, cleanup: () -> ())
if node.cleanups then
table.insert(node.cleanups, cleanup)
else
node.cleanups = { cleanup }
end
end
local function run_cleanups(node: { cleanups: { () -> () } | false})
if node.cleanups then
for _, fn in next, node.cleanups do
fn()
end
table.clear(node.cleanups)
end
end
--[[ --[[
Each node side-effect is registered with a corresponding weak key. Each node side-effect is registered with a corresponding weak key.
@ -57,21 +93,12 @@ The weak key is passed as an argument to its side-effect callback.
]] ]]
local function set_effect<T>(node: Node<unknown>, fn: (T) -> (), key: T) local function set_effect<T>(node: Node<unknown>, fn: () -> ())
node.effects[fn :: () -> ()] = key node.effect = fn
end end
local function run_effects(node: Node<unknown>) local function run_effect(node: Node<unknown>)
if flags.strict then -- run effects twice if strict node.effect()
for effect, key in next, node.effects do
effect(key)
effect(key)
end
else
for effect, key in next, node.effects do
effect(key)
end
end
end end
-- retrieves a node's cached value -- retrieves a node's cached value
@ -91,15 +118,31 @@ local function set_child(parent: Node<unknown>, child: Node<unknown>)
end end
end end
local function create_and_open_scope(node: Node<unknown>)
local parent = scopes[scopes.n]
if parent then
set_child(parent, node)
node.effect = function()
return parent
end
else
node.cleanups = {}
local cleanups = node.cleanups :: { () -> () }
on_gc(node, function()
run_cleanups({ cleanups = cleanups })
end)
end
open_scope(node)
end
-- runs node effects, recalculates descendants and runs descendant effects -- runs node effects, recalculates descendants and runs descendant effects
local function update(node: Node<unknown>) local function update(node: Node<unknown>)
run_effects(node) open_scope(node)
run_cleanups(node)
run_effect(node)
close_scope()
if node.children then if node.children then
local strict = flags.strict
for _, child in node.children do for _, child in node.children do
if strict then check_for_yield(child.derive) end
child.cache = child.derive()
update(child) update(child)
end end
end end
@ -113,7 +156,9 @@ end
-- links two nodes as parent-child with a function to compute a new value for child -- links two nodes as parent-child with a function to compute a new value for child
local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T) local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T)
child.derive = derive child.effect = function()
child.cache = derive()
end
set_child(parent, child) set_child(parent, child)
end end
@ -121,8 +166,6 @@ end
local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T) local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T)
if reff then throw("recursive capture detected") end if reff then throw("recursive capture detected") end
if flags.strict then check_for_yield(fn, arg) end
table.clear(refs) table.clear(refs)
reff = true reff = true
@ -142,10 +185,12 @@ local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T)
end end
-- captures and links any detected nodes -- captures and links any detected nodes
local function capture_and_link<T>(child: Node<T>, fn: () -> T): T local function capture_and_link<T>(child: Node<T>, derive: () -> T): T
local nodes, value = capture(fn, nil) local nodes, value = capture(derive, nil)
child.derive = fn child.effect = function()
child.cache = derive()
end
for _, parent: Node<unknown> in next, nodes do for _, parent: Node<unknown> in next, nodes do
set_child(parent, child) set_child(parent, child)
end end
@ -156,9 +201,9 @@ end
local function create<T>(value: T): (Node<T>, () -> T) local function create<T>(value: T): (Node<T>, () -> T)
local node = { local node = {
cache = value, cache = value,
derive = function() return nil :: any end, effect = function() end,
effects = setmetatable({}, WEAK_VALUES) :: any, children = false :: false,
children = false :: false cleanups = false :: false
} }
local function read_node_value() local function read_node_value()
@ -169,10 +214,17 @@ local function create<T>(value: T): (Node<T>, () -> T)
end end
return table.freeze { return table.freeze {
create_and_open_scope = create_and_open_scope,
open_scope = open_scope,
close_scope = close_scope,
get_scope = get_scope,
add_cleanup = add_cleanup,
run_cleanups = run_cleanups,
set_effect = set_effect, set_effect = set_effect,
get = get, get = get,
set = set, set = set,
link = link, link = link,
set_child = set_child,
capture = capture, capture = capture,
capture_and_link = capture_and_link, capture_and_link = capture_and_link,
create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)), create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)),

View file

@ -9,13 +9,14 @@ 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)
local watch = require(script.watch) local watch = require(script.watch)
local cleanup, clean_garbage = 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 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 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>
@ -33,7 +34,7 @@ local function step(dt: number)
debug.profilebegin("VIDE GARBAGE CLEANUP") debug.profilebegin("VIDE GARBAGE CLEANUP")
end end
clean_garbage() sweep()
if game then if game then
debug.profileend() debug.profileend()

View file

@ -5,11 +5,16 @@ 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)
local _, _, manual_cleanup_mode, cleanup_ref = require(script.Parent.cleanup)()
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create = graph.create local create = graph.create
local set = graph.set local set = graph.set
local capture = graph.capture local capture = graph.capture
local run_cleanups = graph.run_cleanups
local set_child = graph.set_child
local create_and_open_scope = graph.create_and_open_scope
local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local link = graph.link local link = graph.link
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
@ -31,7 +36,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
local remove_queue = {} :: { K } local remove_queue = {} :: { K }
local output_array = {} :: { VO } local output_array = {} :: { VO }
local cleanups = {} :: Map<K, { () -> () }> local scopes = {} :: Map<K, Node<unknown>>
local function recompute(data) local function recompute(data)
-- queue removed values -- queue removed values
@ -43,14 +48,13 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
-- remove queued values -- remove queued values
for _, i in next, remove_queue do for _, i in next, remove_queue do
for _, callback in next, cleanups[i] do run_cleanups(scopes[i])
callback() -- todo: pcall
end
input_cache[i] = nil input_cache[i] = nil
output_cache[i] = nil output_cache[i] = nil
input_nodes[i] = nil input_nodes[i] = nil
cleanups[i] = nil scopes[i] = nil
end end
table.clear(remove_queue) table.clear(remove_queue)
@ -61,14 +65,16 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
if cv ~= v then if cv ~= v then
if cv == nil then if cv == nil then
manual_cleanup_mode(transform) local scope = create(false)
create_and_open_scope(scope)
local node, get_value = create(v) local node, get_value = create(v)
input_nodes[i] = node input_nodes[i] = node
output_cache[i] = transform(get_value, i) output_cache[i] = transform(get_value, i)
input_cache[i] = v input_cache[i] = v
cleanups[i] = manual_cleanup_mode(nil) close_scope()
else else
set(input_nodes[i], v) set(input_nodes[i], v)
input_cache[i] = v input_cache[i] = v

40
src/on_gc.luau Normal file
View file

@ -0,0 +1,40 @@
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

24
src/root.luau Normal file
View file

@ -0,0 +1,24 @@
if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
local create = graph.create
local create_and_open_scope = graph.create_and_open_scope
local close_scope = graph.close_scope
local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup
local function root<T>(fn: () -> T): T
local node = create(nil) -- todo: lifetime with return vaue from fn
create_and_open_scope(node)
local v = fn()
close_scope()
return v
end
return root

View file

@ -26,7 +26,7 @@ local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create = graph.create local create = graph.create
local set = graph.set local set = graph.set
local set_effect = graph.set_effect local set_child = graph.set_child
local capture = graph.capture local capture = graph.capture
local UPDATE_RATE = 120 local UPDATE_RATE = 120
@ -178,19 +178,19 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
} }
-- reschedule spring for simulation on input update -- reschedule spring for simulation on input update
local function input_updated(node) local function input_updated()
local v = source() local v = source()
data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v) data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v)
data.source_value = v data.source_value = v
springs[data] = node -- todo: investigate why insertion is not O(1) at ~20k springs springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
end end
-- unused field, use so output prevents gc of inputs -- unused field, use so output prevents gc of inputs
output.derive = source :: any output.effect = input_updated
-- register above function as side-effect for all inputs -- register above function as side-effect for all inputs
for _, input in next, inputs do for _, input in next, inputs do
set_effect(input, input_updated, output) set_child(input, output)
end end
return output_get, data return output_get, data

View file

@ -1,25 +1,36 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local set_effect = graph.set_effect local create = graph.create
local set_child = graph.set_child
local capture = graph.capture local capture = graph.capture
local create_and_open_scope = graph.create_and_open_scope
local close_scope = graph.close_scope
local ref = {}
local function watch(effect: () -> ()): () -> () local function watch(effect: () -> ()): () -> ()
local node = create(nil)
create_and_open_scope(node)
local nodes = capture(effect :: () -> nil) local nodes = capture(effect :: () -> nil)
-- store aside captured nodes in new table close_scope()
nodes = table.clone(nodes)
node.effect = effect
-- register effect with permanent lifetime -- register effect with permanent lifetime
for _, node in next, nodes do for _, parent in next, nodes do
set_effect(node, effect, true) set_child(parent, node)
end end
ref[node] = true -- prevent gc of node
local function unwatch() local function unwatch()
-- unregister effect from all nodes -- unregister effect from all nodes
for _, node in next, nodes do node.effect = function() end
set_effect(node, effect, nil) ref[node] = nil
end
end end
return unwatch return unwatch

View file

@ -1431,31 +1431,31 @@ TEST("strict", function()
local indexes, values = vide.indexes, vide.values local indexes, values = vide.indexes, vide.values
local cleanup = vide.cleanup local cleanup = vide.cleanup
do CASE "error on derived callback yield" -- do CASE "error on derived callback yield"
local state = source(1) -- local state = source(1)
local ok = pcall(function() -- local ok = pcall(function()
local _derived = derive(function() -- local _derived = derive(function()
coroutine.yield() -- coroutine.yield()
return state() -- return state()
end) -- end)
end) -- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
do CASE "error on watcher callback yield" -- do CASE "error on watcher callback yield"
local state = source(1) -- local state = source(1)
local ok = pcall(function() -- local ok = pcall(function()
local _derived = watch(function() -- local _derived = watch(function()
coroutine.yield() -- coroutine.yield()
state() -- state()
end) -- end)
end) -- end)
CHECK(not ok) -- CHECK(not ok)
end -- end
do CASE "run derived callback twice" do CASE "run derived callback twice"
local state = source(1) local state = source(1)