This commit is contained in:
Aaron Smith 2023-09-13 18:23:46 +01:00
parent 0d542c66a6
commit 7dead44ac5
9 changed files with 207 additions and 173 deletions

View file

@ -17,7 +17,7 @@ local function action(callback: (Instance) -> (), priority: number?): Action
setmetatable(t :: any, ActionMT)
return t
return table.freeze(t)
end
return function()

View file

@ -10,11 +10,16 @@ local _, is_action = require(script.Parent.action)()
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
type Array<V> = { V }
type Map<K, V> = { [K]: V }
-- buffer of event -> callback to connect after properties are set
local event_buffer: { [string]: () -> () } = {}
local event_buffer = {} :: Map<string, () -> ()>
-- buffer of priority -> callback to run after events are connected
local action_buffers = {} :: { { (Instance) -> () } }
local action_buffers = {} :: Map<number, Array<(Instance) -> ()>>
-- lazily create buffers on nil index
setmetatable(action_buffers :: any, {
__index = function(_, i: number)
action_buffers[i] = {}
@ -22,8 +27,9 @@ setmetatable(action_buffers :: any, {
end
})
-- cache used in strict mode to detect duplicate property sets at same nesting levels
local nested_debug_cache: { [number]: { [string]: true } } = {}
-- cache in strict mode to detect duplicate property set at same nesting level
local nested_debug_cache = {} :: Map<number, Map<string, true>>
setmetatable(nested_debug_cache :: any, {
__index = function(_, i: number)
nested_debug_cache[i] = {}
@ -31,28 +37,30 @@ setmetatable(nested_debug_cache :: any, {
end
})
-- a stack used in place of a recursive function to process nesting layers one at a time
-- enforces the behavior of deeper-nested properties taking precedence of lesser-nested ones
-- each nested table occupies two indexes, reference to table itself and the depth number
-- e.g. props = { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
-- use stack instead of recursive function to process nested layers one at time
-- deeper-nested properties take precedence over shallower-nested ones
-- each nested layer occupies two indexes: 1. table ref 2. nested depth
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
local nested_stack = {} :: { {} | number }
-- todo: solution without manual updating of this table
-- map of datatype names to class default constructor for aggregate initialization
-- map of datatype names to class default constructor for aggregate init
local aggregates = {}
for i, v in next, {
Vector2 = Vector2,
UDim2 = UDim2,
CFrame = CFrame,
Color3 = Color3,
UDim = UDim,
Rect = Rect,
Color3 = Color3
UDim2 = UDim2,
Vector2 = Vector2,
Vector3 = Vector3,
Rect = Rect
} do
aggregates[i] = v.new
end
-- processes a potentially nested table of values to assign to an instance
local function process_nested(instance: Instance, properties: { [unknown]: unknown })
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
local strict = flags.strict
table.clear(nested_stack)
@ -73,27 +81,27 @@ local function process_nested(instance: Instance, properties: { [unknown]: unkno
if type(value) == "table" then -- attempt aggregate init
local ctor = aggregates[typeof((instance :: any)[property])]
if ctor == nil then
throw(`cannot aggregate construct type {typeof(value)} for property {property}`)
throw(`cannot aggregate type {typeof(value)} for property {property}`)
end
(instance :: any)[property] = ctor(unpack(value :: {}))
elseif type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> () -- add event to buffer
else
bind.property(instance, property, value :: () -> ()) -- bind source
bind.property(instance, property, value :: () -> ()) -- bind property
end
else
(instance :: any)[property] = value -- set property
end
elseif type(property) == "number" then
if type(value) == "function" then
bind.children(instance, value :: () -> { Instance }) -- bind children
bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children
elseif type(value) == "table" then
if is_action(value) then
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
else
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
table.insert(nested_stack, value :: {})
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
end
else
(value :: Instance).Parent = instance -- parent child
@ -102,8 +110,8 @@ local function process_nested(instance: Instance, properties: { [unknown]: unkno
end
-- pop next nested table off stack
properties = table.remove(nested_stack) :: {}
depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {}
until not properties
end
@ -121,14 +129,14 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
end
-- process all properties for immediate setting or buffering
process_nested(instance, properties)
process_props(instance, properties)
-- connect buffered events
for event, fn in next, event_buffer do
(instance :: any)[event]:Connect(fn)
end
-- run buffered actions respecting their priorities
-- run buffered actions
for _, buffer in next, action_buffers do
for _, callback in next, buffer do
callback(instance)

View file

@ -1,5 +1,4 @@
if not game then script = require "test/relative-string" end
local warn = game and warn or print :: never
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
@ -33,27 +32,38 @@ local function traceback(skips: number) -- ensures trace begins outside of any v
return debug.traceback(nil, s)
end
function create_binding<T>(updater: (T) -> T, binding_data: T)
-- if flags.strict then
-- -- wrap setter in function with stack inspection for better error msgs
-- local fn = setter
-- local bind_trace = traceback(0)
-- setter = function(instance)
-- local ok, err: string? = xpcall(fn, function(err: string)
-- return err .. "\nsource updated at: " .. traceback(2)
-- end, instance)
-- if not ok then warn(`error occured updating {property}: {err}bound at: {bind_trace}`) end
-- end
-- end
function create_binding<T>(updater: (T) -> T, binding: T)
if flags.strict then
-- track bind creation trace
local fn = updater
local bind_trace = traceback(0)
updater = function(...)
local ok, result = xpcall(fn, function(err: string)
return err
end, ...)
if not ok then
local btype =
if (binding :: any).property then (binding :: any).property
elseif (binding :: any).parent then "Parent"
else "children"
error(`PROPERTY BINDING ERROR: Property {btype}\n{result}\nBIND CREATION TRACE:\n{bind_trace}`, 0)
end
return result
end
end
local binding = create_node(binding_data, updater)
local owner = get_scope()
if not owner then throw("cannot bind property in non-reactive scope") end
assert(owner)
if not owner then
throw("cannot bind property in non-reactive scope")
end; assert(owner)
set_owner(binding, owner)
evaluate_node(binding)
local node = create_node(binding, updater)
set_owner(node, owner)
evaluate_node(node)
end
type PropertyBinding = {
@ -81,7 +91,7 @@ type ChildrenBinding = {
instance: Instance,
cur_children_set: { [Instance]: true },
new_children_set: { [Instance]: true },
children: () -> { Instance }
children: () -> Instance | { Instance }
}
local function update_children(p: ChildrenBinding)
@ -95,7 +105,7 @@ local function update_children(p: ChildrenBinding)
end
if new_children then
for _, child in next, new_children do
for _, child in next, new_children :: { Instance } do
new_child_set[child] = true -- record child set from this update
if not cur_children_set[child] then
child.Parent = p.instance -- if child wasn't already parented then parent it

View file

@ -10,8 +10,9 @@ local evaluate_node = graph.evaluate_node
local function derive<T>(fn: () -> T): () -> T
local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner)
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local node = create_node(false :: any, fn)

View file

@ -5,26 +5,24 @@ local flags = require(script.Parent.flags)
export type StartNode<T> = {
cache: T,
children: { Node<T> } | false
[number]: Node<T>
}
export type Node<T> = {
cache: T,
owner: Node<T> | false,
parents: { StartNode<T> },
children: { Node<T> } | false,
effect: ((T) -> T) | false,
owner: Node<T> | false,
cleanups: { () -> () } | false,
parents: { StartNode<T> },
[number]: Node<T>
}
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
local WEAK_VALUES = { __mode = "v" }
local WEAK_KEYS = { __mode = "k" }
local EVALUATION_ERR = "error while evaluating source:\n\n"
-- runs a given callback in a context that Luau does not allow yielding in
local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
local check_for_yield: <T...>(fn: (T...) -> (boolean, string?), T...) -> () do
local t = { __mode = "kv" }
setmetatable(t, t)
@ -35,17 +33,13 @@ local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
fn(unpack(args))
end
local ok, err = pcall(function()
local ok, err: string? = pcall(function()
local _ = -t
end)
if not ok then
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
throw(EVALUATION_ERR .. "cannot yield when deriving node in effecter")
else
throw(EVALUATION_ERR .. err)
end
end
return ok, if err == "attempt to yield across metamethod/C-call boundary"
or err == "thread is not yieldable" then "yield occured"
else err
end
end
@ -57,24 +51,14 @@ local function get_scope(): Node<unknown>?
return scopes[scopes.n]
end
local function add_child<T>(parent: StartNode<any>, child: Node<any>)
if parent.children then
table.insert(parent.children :: { Node<T> }, child)
else
parent.children = { child }
end
table.insert(parent, child)
table.insert(child.parents, parent)
end
local function set_owner(node: Node<any>, owner: Node<any>)
node.owner = owner
if owner.children then
table.insert(owner.children :: { Node<any> }, node)
else
owner.children = { node }
end
table.insert(owner, node)
end
local function open_scope<T>(node: Node<T>)
@ -108,12 +92,11 @@ local function run_cleanups<T>(node: Node<T>)
end
local function remove_child<T>(parent: StartNode<T>, child: Node<T>)
local children = parent.children :: {}
local idx = table.find(children :: {}, child)
local n = #children
children[idx] = children[n]
children[n] = nil
local idx = table.find(parent, child)
assert(idx, "child not found")
local n = #parent
parent[idx] = parent[n]
parent[n] = nil
end
local function unparent<T>(node: Node<T>)
@ -127,12 +110,13 @@ end
local function destroy<T>(node: Node<T>)
run_cleanups(node)
unparent(node)
if node.owner then remove_child(node.owner, node) end
node.owner = false
local children = node.children :: {}
if children then
while children[1] do destroy(children[1]) end
if node.owner then
remove_child(node.owner, node)
node.owner = false
end
while node[1] do destroy(node[1]) end
end
local function evaluate_node<T>(node: Node<T>)
@ -147,7 +131,7 @@ local function evaluate_node<T>(node: Node<T>)
close_scope()
if not ok then
throw(`side-effect error\n{new_value}`)
throw(`side-effect error from source update\n{new_value}`)
end
node.cache = new_value
@ -158,8 +142,7 @@ end
local update_queue = {} :: { Node<any> }
local function update<T>(node: StartNode<T>)
local children = node.children :: {}
if not children then return end
if not node[1] then return end
local n0 = #update_queue
local first_update = n0 == 0
@ -170,14 +153,14 @@ local function update<T>(node: StartNode<T>)
end
do
local child = children[1]
local child = node[1]
while child do -- todo: case where child in owner context
unparent(child)
n += 1
update_queue[n] = child
child = children[1]
child = node[1]
end
end
@ -216,15 +199,7 @@ local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
end
local function get_children<T>(node: Node<T>): { Node<unknown> }
if not node.children then return {} end
local children = {}
for _, child in node.children do
table.insert(children, child)
end
return children :: { Node<any> }
return { unpack(node) } :: { Node<any> }
end
local function create_start_node<T>(value: T): StartNode<T>

View file

@ -15,6 +15,7 @@ local update = graph.update
local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local evaluate_node = graph.evaluate_node
local destroy = graph.destroy
type Map<K, V> = { [K]: V }
@ -33,14 +34,17 @@ end
-- todo: optimize output array
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner)
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local subowner = create_node(false, false)
set_owner(subowner, owner)
local input_cache = {} :: Map<K, VI>
local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, StartNode<VI>>
local remove_queue = {} :: { K }
local scopes = {} :: Map<K, Node<unknown>>
local function update_children(data)
@ -63,7 +67,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
table.clear(remove_queue)
open_scope(owner) -- todo: needed?
open_scope(subowner)
-- process new or changed values
for i, v in next, data do
@ -71,10 +75,10 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
if cv ~= v then
if cv == nil then
local scope = create_node(false)
local scope = create_node(false, false)
scopes[i] = scope :: Node<any>
set_owner(scope, owner)
set_owner(scope, subowner)
open_scope(scope)
local node = create_start_node(v)
@ -125,15 +129,17 @@ end
-- todo: optimize output array
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner)
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local subowner = create_node(false, false)
set_owner(subowner, owner)
local cur_input_cache_up = {} :: Map<VI, K>
local new_input_cache_up = {} :: Map<VI, K>
local output_cache = {} :: Map<VI, VO>
local input_nodes = {} :: Map<VI, StartNode<K>>
local scopes = {} :: Map<VI, Node<unknown>>
local function update_children(data: Map<K, VI>)
@ -149,7 +155,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
end
end
open_scope(owner)
open_scope(subowner)
-- process data
for i, v in next, data do
@ -158,10 +164,10 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local cv = cur_input_cache[v]
if cv == nil then
local scope = create_node(false)
local scope = create_node(false, false)
scopes[v] = scope :: Node<any>
set_owner(scope, owner)
set_owner(scope, subowner)
open_scope(scope)
local node = create_start_node(i)

View file

@ -4,7 +4,9 @@ local vide = require "src/init"
local N = 2^18 -- 262144
BENCH("create state", function()
-- todo: wide and deep graph benchmarks
BENCH("create source", function()
local source = vide.source
local cache = table.create(N)
@ -15,50 +17,50 @@ BENCH("create state", function()
end)
BENCH("get value", function()
local state = vide.source(1)
local src = vide.source(1)
for i = 1, START(N) do
state()
src()
end
end)
BENCH("set value", function()
local state = vide.source(1)
local src = vide.source(1)
for i = 1, START(N) do
state(i)
src(i)
end
end)
BENCH("derive 1 state", function()
BENCH("derive 1 source", function()
local derive = vide.derive
local cache = table.create(N)
local state = vide.source(1)
local src = vide.source(1)
vide.root(function()
for i = 1, START(N) do
cache[i] = derive(function()
return state()
return src()
end)
end
return nil
end)
end)
BENCH("derive 4 states", function()
BENCH("derive 4 sources", function()
local derive = vide.derive
local cache = table.create(N)
local state = vide.source(1)
local state2 = vide.source(2)
local state3 = vide.source(3)
local state4 = vide.source(4)
local src = vide.source(1)
local src2 = vide.source(2)
local src3 = vide.source(3)
local src4 = vide.source(4)
vide.root(function()
for i = 1, START(N) do
cache[i] = derive(function()
return state() + state2() + state3() + state4()
return src() + src2() + src3() + src4()
end)
end
@ -66,14 +68,15 @@ BENCH("derive 4 states", function()
end)
end)
-- todo: why is this so fast?
BENCH("set derived value", function()
local state = vide.source(1)
local src = vide.source(1)
vide.root(function()
local _derived = vide.derive(state)
local _derived = vide.derive(src)
for i = 1, START(N) do
state(i)
src(i)
end
return nil
@ -95,14 +98,14 @@ BENCH("apply 8 properties", function()
for i = 1, START(N) do
apply(instance, {
Name = i,
Name2 = i,
Name3 = i,
Name4 = i,
Name5 = i,
Name6 = i,
Name7 = i,
Name8 = i,
Text = i,
Text2 = i,
Text3 = i,
Text4 = i,
Text5 = i,
Text6 = i,
Text7 = i,
Text8 = i,
})
end
end)
@ -111,12 +114,12 @@ BENCH("bind source", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local state = vide.source(1)
local src = vide.source(1)
vide.root(function()
for i = 1, START(N) do
apply(instance, {
Name = state
Text = src
})
end
@ -128,15 +131,15 @@ BENCH("update binding", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local state = vide.source(1)
local src = vide.source(1)
vide.root(function()
apply(instance, {
Name = state
Text = src
})
for i = 1, START(N) do
state(i)
src(i)
end
return nil
@ -145,6 +148,26 @@ end)
N /= 1024
BENCH("indexes() all new", function()
local data = {}
for i = 1, N do
data[i] = i
end
local src = vide.source(data)
vide.root(function()
START(N)
local _list = vide.indexes(src, function(v, i)
return {}
end)
return nil
end)
end)
BENCH("indexes() no change", function()
local data = {}
@ -152,18 +175,16 @@ BENCH("indexes() no change", function()
data[i] = i
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.indexes(state, function(v, i)
local _list = vide.indexes(src, function(v, i)
return {}
end)
--state(state()) -- fill double buffer
START(N)
state(data)
src(data)
return nil
end)
@ -176,15 +197,15 @@ BENCH("indexes() all change", function()
data[i] = i
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.indexes(state, function(v, i)
local _list = vide.indexes(src, function(v, i)
return {}
end)
--state(state()) -- fill double buffer
--src(src()) -- fill double buffer
for i, v in data do
data[i] = v + 1
@ -192,7 +213,7 @@ BENCH("indexes() all change", function()
START(N)
state(data)
src(data)
return nil
end)
@ -205,10 +226,10 @@ BENCH("indexes() all remove", function()
data[i] = i
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.indexes(state, function(v, i)
local _list = vide.indexes(src, function(v, i)
return {}
end)
@ -216,7 +237,27 @@ BENCH("indexes() all remove", function()
START(N)
state(data)
src(data)
return nil
end)
end)
BENCH("values() all new", function()
local data = {}
for i = 1, N do
data[i] = {}
end
local src = vide.source(data)
vide.root(function()
START(N)
local _list = vide.values(src, function(v, i)
return {}
end)
return nil
end)
@ -229,18 +270,18 @@ BENCH("values() no change", function()
data[i] = {}
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.values(state, function(v, i)
local _list = vide.values(src, function(v, i)
return {}
end)
state(state()) -- fill double buffer
src(src()) -- fill double buffer
START(N)
state(data)
src(data)
return nil
end)
@ -253,14 +294,14 @@ BENCH("values() all change", function()
data[i] = {}
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.values(state, function(v, i)
local _list = vide.values(src, function(v, i)
return {}
end)
state(state()) -- fill double buffer
src(src()) -- fill double buffer
for i = 1, N do
local r = math.random(1, #data)
@ -269,7 +310,7 @@ BENCH("values() all change", function()
START(N)
state(data)
src(data)
return nil
end)
@ -282,10 +323,10 @@ BENCH("values() all remove", function()
data[i] = {}
end
local state = vide.source(data)
local src = vide.source(data)
vide.root(function()
local _list = vide.values(state, function(v, i)
local _list = vide.values(src, function(v, i)
return {}
end)
@ -293,7 +334,7 @@ BENCH("values() all remove", function()
START(N)
state(data)
src(data)
return nil
end)

View file

@ -113,6 +113,7 @@ local Instance = {} :: any do
local function __newindex(userdata: userdata, property: string, value: unknown)
local data = get_data(userdata)
if property == "Name" then
if type(value) ~= "string" then error("name must be a string", 2) end
data.name = value :: string
elseif property == "Parent" then
assert(value == nil or is_instance(value), "attempt to set non-instance as parent")

View file

@ -29,15 +29,7 @@ local function wrap_root(fn: () -> ())
end
end
local NIL = nil
-- vide.mount(function()
-- local src = vide.source(0)
-- vide.effect(function()
-- axasd += 1
-- end)
-- end)
local NIL = nil :: any
TEST("graph", function()
local create_node = graph.create_node