Minor refactors

Also fixed potential bug with `create()` being called recursively if a
property binding passed as a property to `create()` also calls
`create()`
This commit is contained in:
Aaron Smith 2023-11-20 16:53:30 +00:00
parent 8eb5f96c5b
commit c288cb92c4
16 changed files with 169 additions and 130 deletions

View file

@ -10,14 +10,14 @@ local function is_action(v: any)
end end
local function action(callback: (Instance) -> (), priority: number?): Action local function action(callback: (Instance) -> (), priority: number?): Action
local t = { local a = {
priority = priority or 1, priority = priority or 1,
callback = callback callback = callback
} }
setmetatable(t :: any, ActionMT) setmetatable(a :: any, ActionMT)
return table.freeze(t) return table.freeze(a)
end end
return function() return function()

View file

@ -13,41 +13,58 @@ type Node<T> = graph.Node<T>
type Array<V> = { V } type Array<V> = { V }
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
-- buffer of event -> callback to connect after properties are set local free_caches: {
local event_buffer = {} :: Map<string, () -> ()> -- event listeners to connect after properties are set
events: Map<
string, -- event name
() -> () -- listener
>,
-- buffer of priority -> callback to run after events are connected -- actions to run after events are connected
local action_buffers = {} :: Map<number, Array<(Instance) -> ()>> actions: Map<
number, -- priority
Array<(Instance) -> ()> -- action callbacks
>,
-- lazily create buffers on nil index -- cache to detect duplicate property setting at same nesting depth
setmetatable(action_buffers :: any, { nested_debug: Map<
__index = function(_, i: number) number, -- depth
action_buffers[i] = {} Map<string, true> -- set of property names
return action_buffers[i] >,
end
})
-- cache in strict mode to detect duplicate property set at same nesting level -- use stack instead of recursive function to process nesting layers one at time
local nested_debug_cache = {} :: Map<number, Map<string, true>>
setmetatable(nested_debug_cache :: any, {
__index = function(_, i: number)
nested_debug_cache[i] = {}
return nested_debug_cache[i]
end
})
-- use stack instead of recursive function to process nested layers one at time
-- deeper-nested properties take precedence over shallower-nested ones -- deeper-nested properties take precedence over shallower-nested ones
-- each nested layer occupies two indexes: 1. table ref 2. nested depth -- each nested layer occupies two indexes: 1. table ref 2. nested depth
-- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 } -- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
local nested_stack = {} :: { {} | number } nested_stack: { {} | number }
}?
local function borrow_caches(): typeof(assert(free_caches))
if free_caches then
local caches = free_caches :: typeof(assert(free_caches))
free_caches = nil
return caches
else
return {
events = {},
actions = setmetatable({} :: any, { -- lazy init
__index = function(self, i) self[i] = {}; return self[i] end
}),
nested_debug = setmetatable({} :: any, {
__index = function(self, i: number) self[i] = {}; return self[i] end
}),
nested_stack = {}
}
end
end
local function return_caches(caches: typeof(free_caches) )
free_caches = caches
end
-- todo: solution without manual updating of this table
-- map of datatype names to class default constructor for aggregate init -- map of datatype names to class default constructor for aggregate init
local aggregates = {} local aggregates = {}
for name, class in {
for i, v in next, {
CFrame = CFrame, CFrame = CFrame,
Color3 = Color3, Color3 = Color3,
UDim = UDim, UDim = UDim,
@ -55,27 +72,39 @@ for i, v in next, {
Vector2 = Vector2, Vector2 = Vector2,
Vector3 = Vector3, Vector3 = Vector3,
Rect = Rect Rect = Rect
} do } :: Map<string, { [string]: any }> do
aggregates[i] = v.new aggregates[name] = class.new
end
-- applies table of nested properties to an instance using full vide semantics
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
if not properties then
throw("attempt to call a constructor returned by create() with no properties")
end end
-- processes a potentially nested table of values to assign to an instance
local function process_props(instance: Instance, properties: Map<unknown, unknown>)
local strict = flags.strict local strict = flags.strict
table.clear(nested_stack) -- queue parent assignment if any for last
if strict then table.clear(nested_debug_cache) end local parent: unknown = properties.Parent
local caches = borrow_caches()
local events = caches.events
local actions = caches.actions
local nested_debug = caches.nested_debug
local nested_stack = caches.nested_stack
-- process all properties
local depth = 1 local depth = 1
repeat repeat
for property, value in properties do for property, value in properties do
if property == "Parent" then continue end
if type(property) == "string" then if type(property) == "string" then
if strict then -- check for duplicate prop assignment at nesting layer if strict then -- check for duplicate prop assignment at nesting depth
if nested_debug_cache[depth][property] then if nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`) throw(`duplicate property {property} at depth {depth}`)
end end
nested_debug_cache[depth][property] = true nested_debug[depth][property] = true
end end
if type(value) == "table" then -- attempt aggregate init if type(value) == "table" then -- attempt aggregate init
@ -86,7 +115,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
(instance :: any)[property] = ctor(unpack(value :: {})) (instance :: any)[property] = ctor(unpack(value :: {}))
elseif type(value) == "function" then elseif type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> () -- add event to buffer events[property] = value :: () -> () -- add event to buffer
else else
bind.property(instance, property, value :: () -> ()) -- bind property bind.property(instance, property, value :: () -> ()) -- bind property
end end
@ -98,7 +127,7 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children bind.children(instance, value :: () -> Instance | Array<Instance>) -- bind children
elseif type(value) == "table" then elseif type(value) == "table" then
if is_action(value) then if is_action(value) then
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
else else
table.insert(nested_stack, value :: {}) table.insert(nested_stack, value :: {})
table.insert(nested_stack, depth + 1) -- push table to stack for later processing table.insert(nested_stack, depth + 1) -- push table to stack for later processing
@ -109,40 +138,17 @@ local function process_props(instance: Instance, properties: Map<unknown, unknow
end end
end end
-- pop next nested table off stack
depth = table.remove(nested_stack) :: number depth = table.remove(nested_stack) :: number
properties = table.remove(nested_stack) :: {} properties = table.remove(nested_stack) :: {}
until not properties until not properties
for event, listener in next, events do
(instance :: any)[event]:Connect(listener)
end end
-- applies table of nested properties to an instance using full vide semantics for _, queued in next, actions do
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T for _, callback in next, queued do
if not properties then
throw("no properties given, did you forget to call the constructor returned by create()?")
end
-- queue parent assignment if any for last
local parent: unknown = properties.Parent
if parent then properties.Parent = nil end
-- reset buffers
table.clear(event_buffer)
for _, buffer in next, action_buffers do
table.clear(buffer)
end
-- process all properties for immediate setting or buffering
process_props(instance, properties)
-- connect buffered events
for event, fn in next, event_buffer do
(instance :: any)[event]:Connect(fn)
end
-- run buffered actions
for _, buffer in next, action_buffers do
for _, callback in next, buffer do
callback(instance) callback(instance)
end end
end end
@ -156,6 +162,14 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
end end
end end
-- clear caches
table.clear(events)
for _, queued in next, actions do table.clear(queued) end
if strict then table.clear(nested_debug) end
table.clear(nested_stack)
return_caches(caches)
return instance return instance
end end

View file

@ -5,7 +5,7 @@ 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 create_node = graph.create_node local create_node = graph.create_node
local get_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
@ -31,8 +31,7 @@ function create_binding<T>(updater: (T) -> T, binding: T)
end end
end end
local owner = assert_owning_scope()
local owner = get_owning_scope()
local node = create_node(binding, updater) local node = create_node(binding, updater)

View file

@ -5,9 +5,11 @@ local Instance = game and Instance or require "test/mock".Instance :: never
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local defaults = require(script.Parent.defaults) local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply) local apply = require(script.Parent.apply)
local memoize = require(script.Parent.memoize)
local function create_instance(class: string) local ctor_cache = {} :: { [string]: () -> Instance }
setmetatable(ctor_cache :: any, {
__index = function(self, class)
local ok, instance: Instance = pcall(Instance.new, class :: any) local ok, instance: Instance = pcall(Instance.new, class :: any)
if not ok then throw(`invalid class name, could not create instance of class { class }`) end if not ok then throw(`invalid class name, could not create instance of class { class }`) end
@ -18,29 +20,37 @@ local function create_instance(class: string)
end end
end end
return function(properties: { [any]: unknown }): Instance local function ctor(properties: Props): Instance
return apply(instance:Clone(), properties) return apply(instance:Clone(), properties)
end end
end; create_instance = memoize(create_instance) -- always return same constructor for given class
self[class] = ctor
return ctor
end
})
local function create_instance(class: string)
return ctor_cache[class]
end
local function clone_instance(instance: Instance) local function clone_instance(instance: Instance)
return function(properties: { [any]: unknown }): Instance return function(properties: Props): Instance
local clone = instance:Clone() local clone = instance:Clone()
if not clone then error("Attempt to clone a non-archivable instance", 3) end if not clone then throw "attempt to clone a non-archivable instance" end
return apply(clone, properties) return apply(clone, properties)
end end
end end
local function create(class_or_instance: string|Instance) local function create(class_or_instance: string|Instance): (Props) -> Instance
if type(class_or_instance) == "string" then if type(class_or_instance) == "string" then
return create_instance(class_or_instance) return create_instance(class_or_instance)
elseif typeof(class_or_instance) == "Instance" then elseif typeof(class_or_instance) == "Instance" then
return clone_instance(class_or_instance) return clone_instance(class_or_instance)
else else
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance)) throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
end
return nil :: never return nil :: never
end end
end
type Props = { [any]: any } type Props = { [any]: any }
return (create :: any) :: return (create :: any) ::

View file

@ -4,11 +4,11 @@ local graph = require(script.Parent.graph)
local create_node = graph.create_node 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_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local function derive<T>(source: () -> T): () -> T local function derive<T>(source: () -> T): () -> T
local owner = get_owning_scope() local owner = assert_owning_scope()
local node = create_node(false :: any, source) local node = create_node(false :: any, source)

View file

@ -2,12 +2,12 @@ if not game then script = require "test/relative-string" end
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_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local function effect<T>(callback: (T) -> T, initial_value: T) local function effect<T>(callback: (T) -> T, initial_value: T)
local owner = get_owning_scope() local owner = assert_owning_scope()
local node = create_node(initial_value, callback) local node = create_node(initial_value, callback)

View file

@ -40,7 +40,7 @@ local function get_scope(): Node<unknown>?
return scopes[scopes.n] return scopes[scopes.n]
end end
local function get_owning_scope(): Node<unknown> local function assert_owning_scope(): Node<unknown>
local scope = get_scope() local scope = get_scope()
if not scope then if not scope then
@ -187,7 +187,7 @@ end
local _flushing = false local _flushing = false
local function flush_update_queue() local function flush_update_queue()
assert(not flushing, "recursive queue flush occured") -- todo assert(not _flushing, "recursive queue flush occured") -- todo
_flushing = true _flushing = true
local n0 = 0 local n0 = 0
@ -266,7 +266,7 @@ return table.freeze {
close_scope = close_scope, close_scope = close_scope,
evaluate_node = evaluate_node, evaluate_node = evaluate_node,
get_scope = get_scope, get_scope = get_scope,
get_owning_scope = get_owning_scope, assert_owning_scope = assert_owning_scope,
add_cleanup = add_cleanup, add_cleanup = add_cleanup,
set_owner = set_owner, set_owner = set_owner,
destroy = destroy, destroy = destroy,

View file

@ -14,6 +14,7 @@ local effect = require(script.effect)
local derive = require(script.derive) local derive = require(script.derive)
local cleanup = require(script.cleanup) local cleanup = require(script.cleanup)
local untrack = require(script.untrack) local untrack = require(script.untrack)
local read = require(script.read)
local batch = require(script.batch) local batch = require(script.batch)
local switch = require(script.switch) local switch = require(script.switch)
local show = require(script.show) local show = require(script.show)
@ -60,10 +61,8 @@ local vide = {
-- util -- util
cleanup = cleanup, cleanup = cleanup,
untrack = untrack, untrack = untrack,
read = read,
batch = batch, batch = batch,
read = function<T>(value: T | () -> T): T
return if type(value) == "function" then value() else value
end,
-- animations -- animations
spring = spring, spring = spring,

View file

@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local update = graph.update local update = graph.update
local get_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
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 evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
@ -28,7 +28,7 @@ local function check_primitives(t: {})
end end
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO } local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = get_owning_scope() local owner = assert_owning_scope()
local subowner = create_node(false, false) local subowner = create_node(false, false)
set_owner(subowner, owner) set_owner(subowner, owner)
@ -123,7 +123,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
end end
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO } local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = get_owning_scope() local owner = assert_owning_scope()
local subowner = create_node(false, false) local subowner = create_node(false, false)
set_owner(subowner, owner) set_owner(subowner, owner)

View file

@ -1,17 +0,0 @@
local function memoize<X, Y>(f: (X) -> Y): (X) -> Y
local cache: { [X]: Y? } = {}
return function(x: X): Y
local y = cache[x]
if not y then
y = f(x)
cache[x] = y
end
return y :: Y
end
end
return memoize

7
src/read.luau Normal file
View file

@ -0,0 +1,7 @@
if not game then script = require "test/relative-string" end
local function read<T>(value: T | () -> T): T
return if type(value) == "function" then value() else value
end
return read

View file

@ -6,7 +6,7 @@ local create_start_node = graph.create_start_node
local track = graph.track local track = graph.track
local update = graph.update local update = graph.update
export type Source<T> = (() -> T) & ((T) -> T) export type Source<T> = (() -> T) & ((value: T) -> T)
local function source<T>(initial_value: T): Source<T> local function source<T>(initial_value: T): Source<T>
local node = create_start_node(initial_value) local node = create_start_node(initial_value)

View file

@ -27,7 +27,7 @@ type Node<T> = graph.Node<T>
type StartNode<T> = graph.StartNode<T> type StartNode<T> = graph.StartNode<T>
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
local get_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local update = graph.update local update = graph.update
local set_owner = graph.set_owner local set_owner = graph.set_owner
@ -150,7 +150,7 @@ local springs: { [SpringData<any>]: StartNode<any> } = {}
setmetatable(springs, { __mode = "v" }) setmetatable(springs, { __mode = "v" })
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
local owner = get_owning_scope() local owner = assert_owning_scope()
-- https://en.wikipedia.org/wiki/Damping -- https://en.wikipedia.org/wiki/Damping

View file

@ -9,14 +9,14 @@ local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local destroy = graph.destroy local destroy = graph.destroy
local get_owning_scope = graph.get_owning_scope local assert_owning_scope = graph.assert_owning_scope
local open_scope = graph.open_scope local open_scope = graph.open_scope
local close_scope = graph.close_scope local close_scope = graph.close_scope
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U? local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
local owner = get_owning_scope() local owner = assert_owning_scope()
return function(map) return function(map)
local last_scope: Node<false>? local last_scope: Node<false>?
@ -35,7 +35,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
if component == nil then return nil end if component == nil then return nil end
if type(component) ~= "function" then if type(component) ~= "function" then
throw("map must map a value to a function") throw "map must map a value to a function"
end end
local new_scope = create_node(false, false) local new_scope = create_node(false, false)

View file

@ -942,6 +942,33 @@ TEST("create()", wrap_root(function()
CHECK(not wref[1]) CHECK(not wref[1])
end end
do CASE "recursive create"
local set_test_to_true = vide.action(function(self) (self :: any).test = true end)
local f2
local to_apply = {
{ a = 1 },
set_test_to_true,
b = function() f2 = create "Frame" { a = 2 } end,
} :: { [number|string]: unknown }
-- do -- confirm iteration order
-- local t = {}
-- for i in to_apply do
-- table.insert(t, i)
-- end
-- assert(t[1] == "a")
-- end
local f = create "Frame" (to_apply)
CHECK((f :: any).a == 1)
CHECK((f :: any).test == true )
CHECK((f2 :: any).a == 2)
end
do CASE "garbage collection test" do CASE "garbage collection test"
local wref local wref