mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
1aa74310f3
commit
676bbbcc00
15 changed files with 665 additions and 873 deletions
|
|
@ -1,41 +0,0 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Event.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local memoize = require(script.Parent.memoize)
|
||||
local bind = require(script.Parent.bind)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
type Listener = (unknown) -> ()
|
||||
|
||||
local getEventSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
|
||||
return {
|
||||
priority = 2,
|
||||
run = function(instance: Instance, listener: MaybeState<Listener>)
|
||||
local event: RBXScriptSignal<...unknown> = (instance :: any)[name]
|
||||
|
||||
if type(listener) == "function" then
|
||||
event:Connect(listener)
|
||||
elseif wrapped(listener) then
|
||||
bind.event(listener :: State<Listener>, instance, event)
|
||||
else
|
||||
error("Attempt to connect non-function to event", 2)
|
||||
end
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
local Event = table.freeze(setmetatable({}, {__index = function(_, index: string)
|
||||
return getEventSymbol(index)
|
||||
end})) :: any
|
||||
|
||||
return Event :: { [string]: unknown }
|
||||
|
||||
|
|
@ -4,12 +4,57 @@
|
|||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local applyProperties = require(script.Parent.applyProperties)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
|
||||
local function apply<T>(instance: T & Instance)
|
||||
return function(properties: { [any]: unknown }): T
|
||||
return applyProperties(instance, properties)
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
|
||||
local function recurse(instance: Instance, properties: { [unknown]: unknown }, event_buffer)
|
||||
for property, value in properties do
|
||||
if type(value) == "table" then
|
||||
recurse(instance, value :: {}, event_buffer)
|
||||
elseif type(property) == "string" then
|
||||
if type(value) == "function" then
|
||||
if typeof((instance :: any)[property] == "RBXScriptSignal") then
|
||||
event_buffer[property] = value
|
||||
else
|
||||
bind.property(instance, property, value :: () -> ())
|
||||
end
|
||||
else
|
||||
(instance :: any)[property] = value
|
||||
end
|
||||
elseif type(property) == "number" then
|
||||
if type(value) == "function" then
|
||||
bind.children(instance, value :: () -> { Instance })
|
||||
else
|
||||
(value :: Instance).Parent = instance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
|
||||
local parent: unknown = properties.Parent
|
||||
if parent then properties.Parent = nil end
|
||||
|
||||
local event_buffer: { [string]: () -> () } = {} -- connect events after setting properties
|
||||
|
||||
recurse(instance, properties, event_buffer)
|
||||
|
||||
for event, fn in next, event_buffer do
|
||||
(instance :: any)[event]:Connect(fn)
|
||||
end
|
||||
|
||||
if parent then
|
||||
if type(parent) == "function" then
|
||||
error("cannot set parent to state")
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
return apply
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/applyProperties.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local function applyProperty(instance: Instance, property: string, value: unknown)
|
||||
|
||||
end
|
||||
|
||||
local function applyProperties<T>(instance: T & Instance, properties: { [string|Types.Symbol<unknown> ]: unknown }): T
|
||||
local parent: unknown = properties.Parent
|
||||
if parent then properties.Parent = nil end
|
||||
|
||||
local eventBuffer: { [(Instance, () -> ()) -> ()]: () -> () } = {} -- connect events after setting properties
|
||||
local postCreation: (Instance) -> ()? = nil -- buffer for post-creation callback
|
||||
|
||||
for property, value in next, properties do
|
||||
if type(property) == "string" then
|
||||
if wrapped(value) then
|
||||
bind.property(value :: State<unknown>, instance, property)
|
||||
else
|
||||
(instance :: any)[property] = value
|
||||
end
|
||||
elseif type(property) == "table" then
|
||||
local priority = property.priority
|
||||
if priority == 1 then
|
||||
property.run(instance, value)
|
||||
elseif priority == 2 then
|
||||
eventBuffer[property.run] = value :: () -> ()
|
||||
elseif priority == 3 then
|
||||
assert(not postCreation)
|
||||
postCreation = value :: () -> ()
|
||||
else
|
||||
error("invalid priority")
|
||||
end
|
||||
else throw(`Invalid property { tostring(property) }, expected string or symbol`) end
|
||||
end
|
||||
|
||||
for fn, v in next, eventBuffer do
|
||||
fn(instance, v)
|
||||
end
|
||||
|
||||
if parent then
|
||||
applyProperty(instance, "Parent", parent)
|
||||
if wrapped(parent) then
|
||||
bind.parent(parent :: State<Instance?>, instance)
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
if postCreation then postCreation(instance) end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
return applyProperties
|
||||
75
src/bind.lua
75
src/bind.lua
|
|
@ -11,9 +11,10 @@ end
|
|||
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Node<T> = graph.Node<T>
|
||||
local get = graph.get
|
||||
local setEffect = graph.setEffect
|
||||
local set_effect = graph.set_effect
|
||||
local capture = graph.capture
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
|
@ -22,15 +23,18 @@ local hold: { Instance? } = {}
|
|||
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
|
||||
local bindcount = 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
local srcs do
|
||||
local src1 = debug.info(1, "s")
|
||||
local srctrunc = string.sub(src1, 1, #src1-4)
|
||||
|
||||
srcs = {
|
||||
src1,
|
||||
srctrunc .. "applyProperties",
|
||||
srctrunc .. "apply",
|
||||
srctrunc .. "create",
|
||||
srctrunc .. "apply"
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -43,18 +47,22 @@ local function traceback() -- ensures trace begins outside of any vide library f
|
|||
return debug.traceback("", s)
|
||||
end
|
||||
|
||||
function setup(state: State<any>, instance: Instance, updateInstance: (Instance) -> ())
|
||||
function setup(instance: Instance, deriver: () -> unknown, setter: (Instance) -> ())
|
||||
if flags.strict then
|
||||
local fn = updateInstance
|
||||
local fn = setter
|
||||
local trace = traceback()
|
||||
updateInstance = function(instance)
|
||||
setter = function(instance)
|
||||
local ok, err: string? = pcall(fn, instance)
|
||||
if not ok then warn(`error occured updating state binding:\n{err}\nset from:{trace}`) end
|
||||
end
|
||||
end
|
||||
|
||||
updateInstance(instance)
|
||||
setEffect(state, updateInstance, instance)
|
||||
local nodes = table.clone((capture(setter :: (Instance) -> unknown, instance)))
|
||||
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, setter, instance)
|
||||
end
|
||||
|
||||
|
||||
bindcount += 1
|
||||
local key = bindcount
|
||||
|
|
@ -62,7 +70,7 @@ function setup(state: State<any>, instance: Instance, updateInstance: (Instance)
|
|||
weak[key] = instance
|
||||
|
||||
local function ref()
|
||||
local _ = state -- prevent gc of state while instance exists
|
||||
local _ = nodes -- prevent gc of state while instance exists
|
||||
local instance = weak[key] :: Instance
|
||||
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
|
||||
end
|
||||
|
|
@ -71,28 +79,28 @@ function setup(state: State<any>, instance: Instance, updateInstance: (Instance)
|
|||
instance:GetPropertyChangedSignal("Parent"):Connect(ref)
|
||||
end
|
||||
|
||||
local function bindProperty(state: State<unknown>, instance_STRONG: Instance, property: string)
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
(instance :: any)[property] = get(state)
|
||||
local function bind_property(instance: Instance, property: string, fn: () -> unknown)
|
||||
setup(instance, fn, function(instance_weak: any)
|
||||
instance_weak[property] = fn()
|
||||
end)
|
||||
end
|
||||
|
||||
local function bindParent(state: State<Instance?>, instance_STRONG)
|
||||
instance_STRONG.Destroying:Connect(function()
|
||||
instance_STRONG = nil :: any -- allow gc when destroyed
|
||||
local function bind_parent(instance: Instance, fn: () -> Instance?)
|
||||
instance.Destroying:Connect(function()
|
||||
instance= nil :: any -- allow gc when destroyed
|
||||
end)
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
local _ = instance_STRONG -- state will strongly reference instance when parent is bound
|
||||
instance.Parent = get(state)
|
||||
setup(instance, fn, function(instance)
|
||||
local _ = instance -- state will strongly reference instance when parent is bound
|
||||
instance.Parent = fn()
|
||||
end)
|
||||
end
|
||||
|
||||
local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance)
|
||||
local function bind_children(parent: Instance, fn: () -> { Instance })
|
||||
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update
|
||||
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update
|
||||
|
||||
setup(state, parent_STRONG, function(parent)
|
||||
local newChildren = get(state) -- all (and only) children that should be parented after this update
|
||||
setup(parent, fn, function(parent_weak)
|
||||
local newChildren = fn() -- all (and only) children that should be parented after this update
|
||||
if newChildren and type(newChildren) ~= "table" then
|
||||
throw(`Cannot parent instance of type { type(newChildren) } `)
|
||||
end
|
||||
|
|
@ -101,7 +109,7 @@ local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance
|
|||
for _, child in next, newChildren do
|
||||
newChildrenSet[child] = true -- record child set from this update
|
||||
if not currentChildrenSet[child] then
|
||||
child.Parent = parent -- if child wasn't already parented then parent it
|
||||
child.Parent = parent_weak -- if child wasn't already parented then parent it
|
||||
else
|
||||
currentChildrenSet[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
|
|
@ -117,23 +125,8 @@ local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance
|
|||
end)
|
||||
end
|
||||
|
||||
local function bindEvent(state: State<() -> ()?>, instance_STRONG: Instance, event: RBXScriptSignal)
|
||||
local current: RBXScriptConnection? = nil
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
if current then
|
||||
current:Disconnect()
|
||||
current = nil
|
||||
end
|
||||
local listener = get(state)
|
||||
if listener then
|
||||
current = event:Connect(listener)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
property = bindProperty,
|
||||
parent = bindParent,
|
||||
children = bindChildren,
|
||||
event = bindEvent
|
||||
property = bind_property,
|
||||
parent = bind_parent,
|
||||
children = bind_children,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ end
|
|||
|
||||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local applyProperties = require(script.Parent.applyProperties)
|
||||
local apply = require(script.Parent.apply)
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
local function createInstance(className: string)
|
||||
|
|
@ -24,7 +24,7 @@ local function createInstance(className: string)
|
|||
end
|
||||
|
||||
return function(properties: { [any]: unknown }): Instance
|
||||
return applyProperties(instance:Clone(), properties)
|
||||
return apply(instance:Clone(), properties)
|
||||
end
|
||||
end; createInstance = memoize(createInstance)
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ local function cloneInstance(instance: Instance)
|
|||
return function(properties: { [any]: unknown }): Instance
|
||||
local clone = instance:Clone()
|
||||
if not clone then error("Attempt to clone a non-archivable instance", 3) end
|
||||
return applyProperties(clone, properties)
|
||||
return apply(clone, properties)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,28 +5,28 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Unwrapper = graph.Unwrapper
|
||||
local create = graph.create
|
||||
local captureAndLink = graph.captureAndLink
|
||||
local get = graph.get
|
||||
local capture_and_link = graph.capture_and_link
|
||||
|
||||
local function derive<T>(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): State<T>
|
||||
local function derive<T>(fn: () -> T, cleanup: (T) -> ()?): () -> T
|
||||
local node = create((nil :: any) :: T)
|
||||
|
||||
if cleanup then
|
||||
local fn = deriveValue
|
||||
local f = fn
|
||||
local last: T? = nil
|
||||
deriveValue = function(from)
|
||||
fn = function()
|
||||
if last ~= nil then cleanup(last) end
|
||||
last = fn(from)
|
||||
last = f()
|
||||
return last :: T
|
||||
end
|
||||
end
|
||||
|
||||
local value: T = captureAndLink(node, deriveValue)
|
||||
rawset(node, "__cache", value)
|
||||
node.cache = capture_and_link(node, fn)
|
||||
|
||||
return node :: State<T>
|
||||
return function()
|
||||
return get(node)
|
||||
end
|
||||
end
|
||||
|
||||
return derive
|
||||
|
|
|
|||
253
src/graph.lua
253
src/graph.lua
|
|
@ -6,49 +6,29 @@ if not game then script = (require :: any) "test/wrap-require" end
|
|||
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
export type State<T> = typeof(setmetatable(
|
||||
{} :: {
|
||||
__cache: T,
|
||||
__updated: boolean,
|
||||
__derive: (any) -> T,
|
||||
__effects: { [(unknown) -> ()]: unknown } | false, -- weak values
|
||||
__children: { State<T> } | false -- weak values
|
||||
}, {} :: {
|
||||
__concat: (any, any) -> any,
|
||||
__add: (any, any) -> any,
|
||||
__sub: (any, any) -> any,
|
||||
__mul: (any, any) -> any,
|
||||
__div: (any, any) -> any,
|
||||
--__pow
|
||||
--__mod
|
||||
--__unm
|
||||
--__eq: (unknown, unknown) -> State<boolean>
|
||||
--__lt
|
||||
--__le
|
||||
}
|
||||
))
|
||||
export type Node<T> = {
|
||||
cache: T,
|
||||
derive: () -> T,
|
||||
effects: { [(unknown) -> ()]: unknown }, -- weak values
|
||||
children: { Node<T> } | false -- weak values
|
||||
}
|
||||
|
||||
export type MaybeState<T> = State<T> | T
|
||||
export type Unwrapper = <T>(T) -> T
|
||||
local reff = false
|
||||
local refs = {} :: { Node<unknown> }
|
||||
|
||||
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
|
||||
local EVALUATION_ERR = "error while evaluating state:\n\n"
|
||||
local EVALUATION_ERR = "error while evaluating node:\n\n"
|
||||
|
||||
local State = {}
|
||||
setmetatable(refs, WEAK_VALUES_RESIZABLE)
|
||||
|
||||
local function wrapped(value: any): boolean
|
||||
return getmetatable(value) == State
|
||||
end
|
||||
|
||||
local unwrap: <T>(T) -> T;
|
||||
|
||||
local checkForYield do
|
||||
local check_for_yield do
|
||||
local t = { __mode = "kv" }
|
||||
setmetatable(t, t)
|
||||
|
||||
checkForYield = function(fn: (Unwrapper) -> ())
|
||||
check_for_yield = function<T..., U...>(fn: (T...) -> (), ...: U...)
|
||||
local args = { ... }
|
||||
t.__unm = function()
|
||||
fn(unwrap)
|
||||
fn(unpack(args))
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
return -t :: any
|
||||
|
|
@ -56,7 +36,7 @@ local checkForYield do
|
|||
|
||||
if not ok then
|
||||
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
|
||||
error(EVALUATION_ERR .. "cannot yield when deriving state in watcher", 3)
|
||||
error(EVALUATION_ERR .. "cannot yield when deriving node in watcher", 3)
|
||||
else
|
||||
error(EVALUATION_ERR..err, 3)
|
||||
end
|
||||
|
|
@ -64,199 +44,100 @@ local checkForYield do
|
|||
end
|
||||
end
|
||||
|
||||
local function setEffect<T>(state: State<unknown>, fn: (T) -> (), key: T)
|
||||
if not state.__effects then
|
||||
state.__effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
|
||||
else
|
||||
state.__effects[fn :: () -> ()] = key
|
||||
end
|
||||
local function set_effect<T>(node: Node<unknown>, fn: (T) -> (), key: T)
|
||||
node.effects[fn :: () -> ()] = key
|
||||
end
|
||||
|
||||
local function runEffects(state: State<unknown>)
|
||||
if state.__effects then
|
||||
for effect, key in next, state.__effects do
|
||||
local function run_effects(node: Node<unknown>)
|
||||
for effect, key in next, node.effects do
|
||||
if flags.strict then effect(key) end
|
||||
effect(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- retrieves a state's cached value
|
||||
-- retrieves a node's cached value
|
||||
-- recalculates value if an ancestor was updated
|
||||
local function get<T>(state: State<T>): T
|
||||
if state.__updated then
|
||||
state.__updated = false
|
||||
|
||||
if flags.strict then checkForYield(state.__derive) end
|
||||
|
||||
local ok, result: T|string? = pcall(state.__derive, unwrap); if ok then
|
||||
rawset(state :: any, "__cache", result :: T)
|
||||
else error(EVALUATION_ERR .. result :: string, 0) end
|
||||
end
|
||||
|
||||
return rawget(state :: any, "__cache")
|
||||
local function get<T>(node: Node<T>): T
|
||||
if reff then table.insert(refs, node) end
|
||||
return node.cache
|
||||
end
|
||||
|
||||
-- utility function for retrieving a value from state and allowing passthrough of non-state
|
||||
unwrap = function<T>(value: MaybeState<T>): T
|
||||
if wrapped(value) then
|
||||
return get(value :: State<T>)
|
||||
local function set_child(parent: Node<unknown>, child: Node<unknown>)
|
||||
if parent.children then
|
||||
table.insert(parent.children, child)
|
||||
else
|
||||
return value :: T
|
||||
parent.children = { child }
|
||||
setmetatable(parent.children :: any, WEAK_VALUES_RESIZABLE)
|
||||
end
|
||||
end
|
||||
|
||||
local function addChild(parent: State<unknown>, child: State<unknown>)
|
||||
if parent.__children then
|
||||
table.insert(parent.__children, child)
|
||||
else
|
||||
parent.__children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
|
||||
end
|
||||
end
|
||||
|
||||
-- marks all state descendants for recalculation and runs effects
|
||||
local function update(state: State<unknown>)
|
||||
runEffects(state)
|
||||
if state.__children then
|
||||
for _, child in state.__children do
|
||||
if not child.__updated then
|
||||
child.__updated = true
|
||||
-- runs node effects, recalculates descendants and runs descendant effects
|
||||
local function update(node: Node<unknown>)
|
||||
run_effects(node)
|
||||
if node.children then
|
||||
for _, child in node.children do
|
||||
if flags.strict then check_for_yield(child.derive) end
|
||||
child.cache = child.derive()
|
||||
update(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sets a state's cached value and updates all descendants
|
||||
local function set<T>(state: State<T>, value: T)
|
||||
state.__cache = value
|
||||
update(state)
|
||||
-- sets a node's cached value and updates all descendants
|
||||
local function set<T>(node: Node<T>, value: T)
|
||||
node.cache = value
|
||||
update(node)
|
||||
end
|
||||
|
||||
-- links two states as parent-child
|
||||
local function link(parent: State<unknown>, child: State<unknown>, derive: () -> unknown)
|
||||
child.__derive = derive
|
||||
addChild(parent, 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)
|
||||
child.derive = derive
|
||||
set_child(parent, child)
|
||||
end
|
||||
|
||||
-- detect what states were referenced in the given callback and returns them in an array
|
||||
local function capture<T>(callback: (Unwrapper) -> T): ({ State<unknown> }, T)
|
||||
if flags.strict then checkForYield(callback) end
|
||||
-- detect what nodes were referenced in the given callback and returns them in an array
|
||||
local function capture<T, U>(fn: (U) -> T, arg: U): ({ Node<unknown> }, T)
|
||||
if flags.strict then check_for_yield(fn, arg) end
|
||||
|
||||
local states = table.create(2)
|
||||
table.clear(refs)
|
||||
reff = true
|
||||
|
||||
local ok: boolean, result: T|string = pcall(callback, function<T>(value: MaybeState<T>): T
|
||||
if wrapped(value) then
|
||||
table.insert(states, value :: State<T>)
|
||||
return get(value :: State<T>)
|
||||
else
|
||||
return value :: T
|
||||
end
|
||||
end)
|
||||
local ok: boolean, result: T|string = pcall(fn, arg)
|
||||
|
||||
reff = false
|
||||
|
||||
if not ok then error("error while detecting watcher: " .. result :: string, 0) end
|
||||
|
||||
return states, result :: T
|
||||
return refs, result :: T
|
||||
end
|
||||
|
||||
-- captures and links any detected states
|
||||
local function captureAndLink<T>(child: State<T>, callback: (Unwrapper) -> T): T
|
||||
local states, value = capture(callback)
|
||||
-- captures and links any detected nodes
|
||||
local function capture_and_link<T>(child: Node<T>, fn: () -> T): T
|
||||
local nodes, value = capture(fn, nil)
|
||||
|
||||
child.__derive = callback
|
||||
for _, parent: State<unknown> in next, states do
|
||||
addChild(parent, child)
|
||||
child.derive = fn
|
||||
for _, parent: Node<unknown> in next, nodes do
|
||||
set_child(parent, child)
|
||||
end
|
||||
|
||||
return value :: T
|
||||
end
|
||||
|
||||
local create: <T>(value: T) -> State<T>
|
||||
|
||||
-- factory function for creating operator overloads for shorthands to derive state
|
||||
local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any
|
||||
return function(a: MaybeState<unknown>, b: MaybeState<unknown>): State<unknown>
|
||||
local derived: State<unknown> = create(nil :: any)
|
||||
|
||||
local aIsState = wrapped(a)
|
||||
local bIsState = wrapped(b)
|
||||
|
||||
if aIsState and bIsState then
|
||||
local function derive() return op(get(a :: State<unknown>), get(b :: State<unknown>)) end
|
||||
link(a :: State<unknown>, derived, derive)
|
||||
link(b :: State<unknown>, derived, derive)
|
||||
elseif aIsState then
|
||||
link(a :: State<unknown>, derived, function() return op(get(a :: State<unknown>), b) end)
|
||||
else--if bIsState then
|
||||
link(b :: State<unknown>, derived, function() return op(a, get(b :: State<unknown>)) end)
|
||||
end
|
||||
|
||||
derived.__updated = true
|
||||
return derived
|
||||
end
|
||||
end
|
||||
|
||||
local function __unm(self: State<unknown>)
|
||||
local derived = create(nil :: any)
|
||||
|
||||
link(self, derived, function()
|
||||
return -get(self) :: number
|
||||
end)
|
||||
|
||||
derived.__updated = true
|
||||
return derived
|
||||
end
|
||||
|
||||
local function __index(self: State<unknown>, index: unknown)
|
||||
local derived = create(nil :: any)
|
||||
|
||||
link(self, derived, function()
|
||||
return (get(self) :: {})[index]
|
||||
end)
|
||||
|
||||
derived.__updated = true
|
||||
return derived
|
||||
end
|
||||
|
||||
State.__index = __index
|
||||
State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end)
|
||||
State.__add = overload(function(a: any, b: any) return a + b end)
|
||||
State.__sub = overload(function(a: any, b: any) return a - b end)
|
||||
State.__mul = overload(function(a: any, b: any) return a * b end)
|
||||
State.__div = overload(function(a: any, b: any) return a / b end)
|
||||
State.__pow = overload(function(a: any, b: any) return a ^ b end)
|
||||
State.__mod = overload(function(a: any, b: any) return a % b end)
|
||||
State.__unm = __unm
|
||||
|
||||
-- todo: what to do
|
||||
do
|
||||
local function err()
|
||||
error("cannot perform equality comparison with state", 2)
|
||||
end
|
||||
State.__eq = err
|
||||
State.__lt = err
|
||||
State.__le = err
|
||||
end
|
||||
|
||||
|
||||
function create<T>(value: T): State<T>
|
||||
return setmetatable({
|
||||
__cache = value,
|
||||
__updated = false,
|
||||
__derive = function() return nil end :: any,
|
||||
__effects = false :: false,
|
||||
__children = false :: false
|
||||
}, State)
|
||||
local function create<T>(value: T): Node<T>
|
||||
return {
|
||||
cache = value,
|
||||
derive = function() return nil :: any end,
|
||||
effects = setmetatable({}, WEAK_VALUES_RESIZABLE) :: any,
|
||||
children = false :: false
|
||||
}
|
||||
end
|
||||
|
||||
return table.freeze {
|
||||
setEffect = setEffect,
|
||||
set_effect = set_effect,
|
||||
get = get,
|
||||
set = set,
|
||||
unwrap = unwrap,
|
||||
link = link,
|
||||
capture = capture,
|
||||
captureAndLink = captureAndLink,
|
||||
wrapped = wrapped,
|
||||
capture_and_link = capture_and_link,
|
||||
create = create,
|
||||
}
|
||||
|
|
|
|||
36
src/init.lua
36
src/init.lua
|
|
@ -6,20 +6,13 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local create = require(script.create)
|
||||
local apply = require(script.apply)
|
||||
local wrap = require(script.wrap)
|
||||
local watch = require(script.watch)
|
||||
local derive = require(script.derive)
|
||||
local map = require(script.map)
|
||||
|
||||
local unwrap = require(script.unwrap)
|
||||
local wrapped = require(script.wrapped)
|
||||
|
||||
local Layout = require(script.Layout)
|
||||
local Children = require(script.Children)
|
||||
local Event = require(script.Event)
|
||||
local Changed = require(script.Change)
|
||||
local Created = require(script.Created)
|
||||
-- local Changed = require(script.Change)
|
||||
-- local Created = require(script.Created)
|
||||
|
||||
local spring, updateSprings = require(script.spring)()
|
||||
|
||||
|
|
@ -32,25 +25,20 @@ type Setter<T> = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, for
|
|||
local vide = {
|
||||
-- core
|
||||
create = create,
|
||||
apply = apply,
|
||||
wrap = (wrap :: any) :: <T>(value: T?) -> (T, Setter<T>),
|
||||
derive = (derive :: any) :: <T>(deriver: (from: <U>(U) -> U) -> T, cleanup: (T) -> ()?) -> T,
|
||||
map = (map :: any) :: ( <V>(input: number, transform: (number) -> V, cleanup: (V) -> ()?) -> Map<number, V> ) & ( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?) -> Map<K, VO> ),
|
||||
watch = watch :: ((Unwrapper) -> ()) -> () -> (),
|
||||
|
||||
-- util
|
||||
unwrap = unwrap,
|
||||
wrapped = wrapped,
|
||||
wrap = wrap,
|
||||
derive = derive,
|
||||
map = map,
|
||||
watch = watch,
|
||||
|
||||
-- animations
|
||||
spring = (spring :: any) :: <T>(input: T, period: number, damping: number?) -> T,
|
||||
spring = spring,
|
||||
|
||||
-- symbols
|
||||
Event = Event,
|
||||
Changed = Changed,
|
||||
Layout = Layout,
|
||||
Children = Children,
|
||||
Created = Created,
|
||||
-- Event = Event,
|
||||
-- Changed = Changed,
|
||||
-- Layout = Layout,
|
||||
-- Children = Children,
|
||||
-- Created = Created,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
|
|
|
|||
|
|
@ -25,13 +25,9 @@ if not game then script = (require :: any) "test/wrap-require" end
|
|||
local throw = require(script.Parent.throw)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local set = graph.set
|
||||
local unwrap = graph.unwrap
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/unwrap.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
return graph.unwrap
|
||||
|
|
@ -5,27 +5,27 @@
|
|||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Unwrapper = graph.Unwrapper
|
||||
local setEffect = graph.setEffect
|
||||
local set_effect = graph.set_effect
|
||||
local capture = graph.capture
|
||||
local unwrap = graph.unwrap
|
||||
|
||||
local function watch(effect: (Unwrapper) -> ()): () -> ()
|
||||
local states, cleanup = capture(effect :: () -> (() -> ()?))
|
||||
local function watch(effect: () -> ()): () -> ()
|
||||
-- todo: call cleanup on initial debug call when strict
|
||||
local nodes, cleanup = capture(effect :: () -> (() -> ()?))
|
||||
|
||||
nodes = table.clone(nodes)
|
||||
|
||||
local function fn()
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
cleanup = effect(unwrap)
|
||||
cleanup = effect()
|
||||
end
|
||||
|
||||
for _, state in next, states do
|
||||
setEffect(state, fn, true)
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, fn, true)
|
||||
end
|
||||
|
||||
local function unwatch()
|
||||
for _, state in next, states do
|
||||
setEffect(state, fn, nil)
|
||||
for _, node in next, nodes do
|
||||
set_effect(node, fn, nil)
|
||||
end
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
end
|
||||
|
|
|
|||
32
src/wrap.lua
32
src/wrap.lua
|
|
@ -2,41 +2,31 @@
|
|||
-- vide/wrap.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
if not game then script = require "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
type Node<T> = graph.Node<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local set = graph.set
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
type Setter<T> = (value: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?) -> T
|
||||
type State<T> = (() -> T) & ((T) -> T)
|
||||
|
||||
local function wrap<T>(value: MaybeState<T>?): (State<T>, Setter<T>)
|
||||
local state = create(if wrapped(value) then get(value :: State<T>) else value :: T)
|
||||
local function wrap<T>(value: T | () -> T): State<T>
|
||||
if type(value) == "function" then value = value() end
|
||||
|
||||
local function setter(vi: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?): T
|
||||
if type(vi) == "function" then
|
||||
vi = vi(get(state))
|
||||
end
|
||||
local node = create(value :: T)
|
||||
|
||||
local v = if wrapped(vi) then get(vi :: State<T>) else vi :: T
|
||||
return function(...): T
|
||||
if select("#", ...) == 0 then return get(node) end
|
||||
|
||||
if v ~= state.__cache or force then
|
||||
set(state, v)
|
||||
elseif flags.strict and type(v) == "table" then
|
||||
throw("attempt to set same table object")
|
||||
end
|
||||
local v = ... :: T
|
||||
if node.cache == v and type(v) ~= "table" then return v end
|
||||
|
||||
set(node, v)
|
||||
return v
|
||||
end
|
||||
|
||||
return state, setter
|
||||
end
|
||||
|
||||
return wrap
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/wrapped.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
return graph.wrapped
|
||||
|
|
@ -1,43 +1,101 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- testkit.luau
|
||||
-- v0.2.0
|
||||
------------------------------------------------------------------------------------------
|
||||
-- v0.7.0
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--[[
|
||||
local color = {
|
||||
white_underline = function(s: string)
|
||||
return `\27[1;4m{s}\27[0m`
|
||||
end,
|
||||
|
||||
EXAMPLE USAGE:
|
||||
white = function(s: string)
|
||||
return `\27[37;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
local testkit = require "path-to-testkit"
|
||||
green = function(s: string)
|
||||
return `\27[32;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
local TEST, CASE, CHECK = testkit.getUnitTestTools()
|
||||
red = function(s: string)
|
||||
return `\27[31;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
TEST("test name", function()
|
||||
do CASE "A"
|
||||
CHECK(condition)
|
||||
yellow = function(s: string)
|
||||
return `\27[33;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
red_highlight = function(s: string)
|
||||
return `\27[41;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
green_highlight = function(s: string)
|
||||
return `\27[42;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
gray = function(s: string)
|
||||
return `\27[30;1m{s}\27[0m`
|
||||
end,
|
||||
}
|
||||
|
||||
local function convert_units(unit: string, value: number): (number, string)
|
||||
local prefix_colors = {
|
||||
[3] = color.red,
|
||||
[2] = color.yellow,
|
||||
[1] = color.yellow,
|
||||
[0] = color.green,
|
||||
[-1] = color.red,
|
||||
[-2] = color.yellow,
|
||||
[-3] = color.green
|
||||
}
|
||||
|
||||
local prefixes = {
|
||||
[3] ="G",
|
||||
[2] ="M",
|
||||
[1] = "k",
|
||||
[0] = " ",
|
||||
[-1] = "m",
|
||||
[-2] = "u",
|
||||
[-3] = "n"
|
||||
}
|
||||
|
||||
local order = 0
|
||||
|
||||
while value >= 1000 do
|
||||
order += 1
|
||||
value /= 1000
|
||||
end
|
||||
end)
|
||||
|
||||
local BENCH, START = testkit.getBenchmarkTools()
|
||||
|
||||
BENCH("benchmark name", function()
|
||||
local x = 0
|
||||
|
||||
for i = 1, START(1e6) do
|
||||
x += 1
|
||||
while value ~= 0 and value < 1 do
|
||||
order -= 1
|
||||
value *= 1000
|
||||
end
|
||||
end)
|
||||
]]
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
-- Unit Testing
|
||||
------------------------------------------------------------------------------------------
|
||||
if value >= 100 then
|
||||
value = math.floor(value)
|
||||
elseif value >= 10 then
|
||||
value = math.floor(value * 1e1) / 1e1
|
||||
elseif value >= 1 then
|
||||
value = math.floor(value * 1e2) / 1e2
|
||||
end
|
||||
|
||||
return value, prefix_colors[order](prefixes[order] .. unit)
|
||||
end
|
||||
|
||||
local WALL = color.gray "│"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Testing
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
type Test = {
|
||||
name: string,
|
||||
activeCase: Case?,
|
||||
case: Case?,
|
||||
cases: { Case },
|
||||
duration: number,
|
||||
error: string?
|
||||
error: {
|
||||
message: string,
|
||||
trace: string
|
||||
}?
|
||||
}
|
||||
|
||||
type Case = {
|
||||
|
|
@ -46,185 +104,220 @@ type Case = {
|
|||
line: number?
|
||||
}
|
||||
|
||||
local PASS = 1
|
||||
local FAIL = 2
|
||||
local NONE = 3
|
||||
local ERROR = 4
|
||||
local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4
|
||||
|
||||
local activeTest: Test?
|
||||
local skip: string?
|
||||
local test: Test?
|
||||
local tests: { Test } = {}
|
||||
|
||||
local function outputTestResults(test: Test)
|
||||
print("\27[1;4m"..test.name.."\27[0m")
|
||||
local function output_test_result(test: Test)
|
||||
print(color.white(test.name))
|
||||
|
||||
for _, case in test.cases do
|
||||
print(
|
||||
"[" ..
|
||||
(if case.result == PASS then
|
||||
"\27[32;1mPASS\27[0m"
|
||||
elseif case.result == FAIL then
|
||||
"\27[31;1mFAIL:"..assert(case.line).."\27[0m"
|
||||
elseif case.result == NONE then
|
||||
"\27[33;1mNONE\27[0m"
|
||||
else
|
||||
"\27[41;1;30mERROR\27[0m")
|
||||
.. "] " .. case.name
|
||||
)
|
||||
local status = ({
|
||||
[PASS] = color.green "PASS",
|
||||
[FAIL] = color.red "FAIL",
|
||||
[NONE] = color.yellow "NONE",
|
||||
[ERROR] = color.red "FAIL"
|
||||
})[case.result]
|
||||
|
||||
local line = case.result == FAIL and color.red(`{case.line}:`) or ""
|
||||
|
||||
print(`{status}{WALL} {line}{color.gray(case.name)}`)
|
||||
end
|
||||
|
||||
if test.error then
|
||||
print("\27[31;1;30merror: " .. test.error .. "\27[0m")
|
||||
print(color.gray "error: " .. color.red(test.error.message))
|
||||
print(color.gray "trace: " .. color.red(test.error.trace))
|
||||
else
|
||||
print()
|
||||
end
|
||||
|
||||
print ""
|
||||
end
|
||||
|
||||
local function CASE(name: string)
|
||||
assert(activeTest, "no active test")
|
||||
assert(test, "no active test")
|
||||
|
||||
local case: Case = {
|
||||
local case = {
|
||||
name = name,
|
||||
result = NONE
|
||||
}
|
||||
|
||||
activeTest.activeCase = case
|
||||
table.insert(activeTest.cases, case)
|
||||
test.case = case
|
||||
table.insert(test.cases, case)
|
||||
end
|
||||
|
||||
local function CHECK(value: any): boolean
|
||||
assert(activeTest, "no active test")
|
||||
local activeCase = activeTest.activeCase
|
||||
local function CHECK<T>(value: T, stack: number?): T
|
||||
assert(test, "no active test")
|
||||
local case = test.case
|
||||
|
||||
if not activeCase then
|
||||
if not case then
|
||||
CASE ""
|
||||
activeCase = activeTest.activeCase
|
||||
end; assert(activeCase, "no active case")
|
||||
|
||||
local result = value and PASS or FAIL
|
||||
|
||||
if activeCase.result == NONE or activeCase.result == PASS then
|
||||
activeCase.result = result
|
||||
activeCase.line = debug.info(2, "l")
|
||||
case = test.case
|
||||
end
|
||||
|
||||
return result == PASS
|
||||
assert(case, "no active case")
|
||||
|
||||
if case.result ~= FAIL then
|
||||
case.result = value and PASS or FAIL
|
||||
case.line = debug.info(stack and stack + 1 or 2, "l")
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
local function TEST(name: string, fn: () -> ())
|
||||
assert(not activeTest, "new test was started while a test was in progress")
|
||||
local test: Test = {
|
||||
if skip and name ~= skip then return end
|
||||
|
||||
local active = test
|
||||
assert(not active, "cannot start test while another test is in progress")
|
||||
|
||||
test = {
|
||||
name = name,
|
||||
cases = {},
|
||||
duration = 0
|
||||
}
|
||||
}; assert(test)
|
||||
|
||||
activeTest = test
|
||||
table.insert(tests, test)
|
||||
|
||||
local start = os.clock()
|
||||
local msg: string?
|
||||
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||
local err
|
||||
local success = xpcall(fn, function(m: string)
|
||||
err = { message = m, trace = debug.traceback(nil, 2) }
|
||||
end)
|
||||
test.duration = os.clock() - start
|
||||
|
||||
if not test.activeCase then CASE "" end
|
||||
assert(test.activeCase, "no active case")
|
||||
if not test.case then CASE "" end
|
||||
assert(test.case, "no active case")
|
||||
|
||||
if not success then
|
||||
test.activeCase.result = ERROR
|
||||
test.error = msg
|
||||
test.case.result = ERROR
|
||||
test.error = err
|
||||
end
|
||||
|
||||
activeTest = nil
|
||||
outputTestResults(test)
|
||||
test = nil
|
||||
end
|
||||
|
||||
local function FINISH(): boolean
|
||||
local success = true
|
||||
local totalCases = 0
|
||||
local passedCases = 0
|
||||
local total_cases = 0
|
||||
local passed_cases = 0
|
||||
local duration = 0
|
||||
|
||||
for _, test in tests do
|
||||
duration += test.duration
|
||||
for _, case in test.cases do
|
||||
totalCases += 1
|
||||
total_cases += 1
|
||||
if case.result == PASS or case.result == NONE then
|
||||
passedCases += 1
|
||||
passed_cases += 1
|
||||
else
|
||||
success = false
|
||||
end
|
||||
end
|
||||
|
||||
output_test_result(test)
|
||||
end
|
||||
|
||||
print(string.format("%d/%d test cases passed in %.3f ms.", passedCases, totalCases, duration*1e3))
|
||||
print(color.gray(string.format(
|
||||
`{passed_cases}/{total_cases} test cases passed in %.3f ms.`,
|
||||
duration*1e3
|
||||
)))
|
||||
|
||||
local fails = totalCases - passedCases
|
||||
local fails = total_cases - passed_cases
|
||||
|
||||
print(string.format("\27[%d;1;30m%d fail%s\27[0m", fails > 0 and 41 or 42, fails, fails == 1 and "" or "s"))
|
||||
print(
|
||||
(
|
||||
fails > 0
|
||||
and color.red
|
||||
or color.green
|
||||
)(`{fails} {fails == 1 and "fail" or "fails"}`)
|
||||
)
|
||||
|
||||
return success, table.clear(tests)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
local function SKIP(name: string)
|
||||
assert(not test, "cannot skip during test")
|
||||
skip = name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Benchmarking
|
||||
------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
type Bench = {
|
||||
timeStart: number?,
|
||||
memStart: number?,
|
||||
time_start: number?,
|
||||
memory_start: number?,
|
||||
iterations: number?
|
||||
}
|
||||
|
||||
local activeBench: Bench? = nil
|
||||
local bench: Bench?
|
||||
|
||||
function START(iter: number?): number
|
||||
local n = iter or 1
|
||||
if n < 1 then error("iteration count must be greater than 0", 2) end
|
||||
assert(activeBench, "no active benchmark")
|
||||
assert(not activeBench.timeStart, "clock was already started")
|
||||
assert(n > 0, "iterations must be greater than 0")
|
||||
assert(bench, "no active benchmark")
|
||||
assert(not bench.time_start, "clock was already started")
|
||||
|
||||
activeBench.iterations = n
|
||||
activeBench.memStart = gcinfo()
|
||||
activeBench.timeStart = os.clock()
|
||||
bench.iterations = n
|
||||
bench.memory_start = gcinfo()
|
||||
bench.time_start = os.clock()
|
||||
return n
|
||||
end
|
||||
|
||||
local function BENCH(name: string, fn: () -> ())
|
||||
assert(not activeBench, "cannot run benchmark, a benchmark is already in progress")
|
||||
local active = bench
|
||||
assert(not active, "a benchmark is already in progress")
|
||||
|
||||
local bench: Bench = {}
|
||||
activeBench = bench
|
||||
bench = {}; assert(bench)
|
||||
|
||||
local memStart = gcinfo()
|
||||
local timeStart = os.clock()
|
||||
local msg: string?
|
||||
local success = xpcall(fn, function(m: string) msg = m .. debug.traceback("", 2) end)
|
||||
local timeStop = os.clock()
|
||||
local memStop = gcinfo()
|
||||
;(collectgarbage :: any)("collect")
|
||||
|
||||
local mem_start = gcinfo()
|
||||
local time_start = os.clock()
|
||||
local err_msg: string?
|
||||
|
||||
local success = xpcall(fn, function(m: string)
|
||||
err_msg = m .. debug.traceback(nil, 2)
|
||||
end)
|
||||
|
||||
local time_stop = os.clock()
|
||||
local mem_stop = gcinfo()
|
||||
|
||||
if not success then
|
||||
print("[\27[41;1mERROR\27[0m] " .. name)
|
||||
print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m")
|
||||
activeBench = nil
|
||||
return
|
||||
end
|
||||
|
||||
timeStart = bench.timeStart or timeStart
|
||||
memStart = bench.memStart or memStart
|
||||
print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
|
||||
print(color.gray(err_msg :: string))
|
||||
else
|
||||
time_start = bench.time_start or time_start
|
||||
mem_start = bench.memory_start or mem_start
|
||||
|
||||
local n = bench.iterations or 1
|
||||
local duration = timeStop - timeStart
|
||||
local allocated = memStop - memStart
|
||||
local d, d_unit = convert_units("s", (time_stop - time_start) / n)
|
||||
local a, a_unit = convert_units("B", math.floor((mem_stop - mem_start) / n * 1e3))
|
||||
|
||||
print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name))
|
||||
local function round(x: number): string
|
||||
return x > 0 and x < 10 and (x - math.floor(x)) > 0
|
||||
and string.format("%2.1f", x)
|
||||
or string.format("%3.f", x)
|
||||
end
|
||||
|
||||
activeBench = nil
|
||||
print(string.format(
|
||||
`%s %s %s %s{WALL} %s`,
|
||||
color.gray(tostring(round(d))),
|
||||
d_unit,
|
||||
color.gray(tostring(round(a))),
|
||||
a_unit,
|
||||
color.gray(name)
|
||||
))
|
||||
end
|
||||
|
||||
bench = nil
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- Printing
|
||||
------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function printa(v: unknown)
|
||||
local function print2(v: unknown)
|
||||
type Buffer = { n: number, [number]: string }
|
||||
|
||||
-- overkill concatenationless string buffer
|
||||
|
|
@ -291,31 +384,67 @@ local function printa(v: unknown)
|
|||
print(table.concat(str))
|
||||
end
|
||||
|
||||
printa "string"
|
||||
--------------------------------------------------------------------------------
|
||||
-- Equality
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
printa(1)
|
||||
local function shallow_eq(a: {}, b: {}): boolean
|
||||
if #a ~= #b then return false end
|
||||
|
||||
printa {
|
||||
hello = 1,
|
||||
bye = "ok",
|
||||
for i, v in next, a do
|
||||
if b[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
test = {
|
||||
1, 2, 3
|
||||
}
|
||||
}
|
||||
for i, v in next, b do
|
||||
if a[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------------------
|
||||
return true
|
||||
end
|
||||
|
||||
local function deep_eq(a: {}, b: {}): boolean
|
||||
if #a ~= #b then return false end
|
||||
|
||||
for i, v in next, a do
|
||||
if type(b[i]) == "table" and type(v) == "table" then
|
||||
if deep_eq(b[i], v) == false then return false end
|
||||
elseif b[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for i, v in next, b do
|
||||
if type(a[i]) == "table" and type(v) == "table" then
|
||||
if deep_eq(a[i], v) == false then return false end
|
||||
elseif a[i] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Return
|
||||
------------------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
getUnitTestTools = function()
|
||||
return TEST, CASE, CHECK, FINISH
|
||||
test = function()
|
||||
return TEST, CASE, CHECK, FINISH, SKIP
|
||||
end,
|
||||
|
||||
getBenchmarkTools = function()
|
||||
benchmark = function()
|
||||
return BENCH, START
|
||||
end,
|
||||
|
||||
printa = printa
|
||||
print2 = print2,
|
||||
|
||||
seq = shallow_eq,
|
||||
deq = deep_eq,
|
||||
|
||||
color = color
|
||||
}
|
||||
|
|
|
|||
489
test/tests.luau
489
test/tests.luau
|
|
@ -2,7 +2,7 @@
|
|||
-- unit.lua
|
||||
----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
local TEST, CASE, CHECK, FINISH = require("test/testkit").getUnitTestTools()
|
||||
local TEST, CASE, CHECK, FINISH, SKIP = require("test/testkit").test()
|
||||
|
||||
local mock = require "test/mock"
|
||||
local Signal = require "test/goodsignal"
|
||||
|
|
@ -17,19 +17,23 @@ local function gc(n: number?)
|
|||
end
|
||||
end
|
||||
|
||||
local function weak<T>(t: T & {}): T
|
||||
setmetatable(t :: {}, { __mode = "kv" })
|
||||
return t
|
||||
end
|
||||
|
||||
-- weak reference table used for gc tests
|
||||
local wref = setmetatable({}, { __mode = "kv" }) :: any
|
||||
--local wref = setmetatable({}, { __mode = "kv" }) :: any
|
||||
|
||||
TEST("graph", function()
|
||||
local graph = require "src/graph"
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local unwrap = graph.unwrap
|
||||
local set = graph.set
|
||||
local capture = graph.capture
|
||||
local captureAndLink = graph.captureAndLink
|
||||
local capture_and_link = graph.capture_and_link
|
||||
local link = graph.link
|
||||
local setEffect = graph.setEffect
|
||||
local set_effect = graph.set_effect
|
||||
|
||||
do CASE "Create"
|
||||
local node = create(1)
|
||||
|
|
@ -47,8 +51,8 @@ TEST("graph", function()
|
|||
do CASE "Capture"
|
||||
local node1 = create(nil)
|
||||
local node2 = create(nil)
|
||||
local nodes = capture(function(from)
|
||||
return from(node1), from(node2)
|
||||
local nodes = capture(function()
|
||||
return get(node1), get(node2)
|
||||
end)
|
||||
CHECK(nodes[1] == node1)
|
||||
CHECK(nodes[2] == node2)
|
||||
|
|
@ -56,7 +60,7 @@ TEST("graph", function()
|
|||
|
||||
do CASE "Link"
|
||||
local parent = create(1)
|
||||
local child = create(nil)
|
||||
local child = create(0)
|
||||
link(parent, child, function()
|
||||
return get(parent)
|
||||
end)
|
||||
|
|
@ -68,14 +72,15 @@ TEST("graph", function()
|
|||
local parent = create(1)
|
||||
local child = create(nil :: any)
|
||||
|
||||
captureAndLink(child, function(from)
|
||||
return tostring(from(parent))
|
||||
capture_and_link(child, function()
|
||||
return tostring(get(parent))
|
||||
end)
|
||||
|
||||
set(parent, get(parent) + 1)
|
||||
CHECK(get(child) == "2")
|
||||
end
|
||||
|
||||
--[[
|
||||
do CASE "Scoped captures"
|
||||
-- table.find but uses `rawequal` since nodes have overloaded __eq metamethod
|
||||
local function rawfind(t, x)
|
||||
|
|
@ -100,52 +105,54 @@ TEST("graph", function()
|
|||
CHECK(not rawfind(captures, a))
|
||||
CHECK(rawfind(captures, b))
|
||||
|
||||
-- repeat for `captureAndLink`
|
||||
-- repeat for `capture_and_link`
|
||||
local c = graph.create(1)
|
||||
set(a, get(a) + 1) -- mark `b` for recomputation again
|
||||
captureAndLink(c, function(from) return from(b) end)
|
||||
capture_and_link(c, function(from) return from(b) end)
|
||||
-- check that only `b` was linked
|
||||
CHECK(not rawfind(assert(a.__children), c))
|
||||
CHECK(rawfind(assert(b.__children), c))
|
||||
end
|
||||
]]
|
||||
|
||||
do CASE "Nodes garbage collection"
|
||||
local node = create(1) :: Node?
|
||||
wref.node, node = node, nil
|
||||
|
||||
local wref
|
||||
do
|
||||
wref = weak { create(1) }
|
||||
end
|
||||
gc()
|
||||
CHECK(not wref.node)
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do CASE "Node effect garbage collection"
|
||||
local wref
|
||||
do
|
||||
local function factory(p) -- factory function to prevent closure caching
|
||||
return function()
|
||||
return unwrap(p)
|
||||
return get(p)
|
||||
end
|
||||
end
|
||||
|
||||
local node = create(1)
|
||||
|
||||
do
|
||||
local effect1 = factory(node)
|
||||
local effect2 = factory(node)
|
||||
wref.node = node
|
||||
wref.effect1 = effect1
|
||||
wref.effect2 = effect2
|
||||
|
||||
local t = {}
|
||||
setEffect(node, effect1, t)
|
||||
setEffect(node, effect2, true)
|
||||
wref = weak { effect1, effect2, node :: any }
|
||||
|
||||
t = nil :: any
|
||||
effect1, effect2 = nil :: any, nil :: any
|
||||
|
||||
gc()
|
||||
|
||||
CHECK(not wref.effect1) -- effect1 should gc since nothing is referencing table `t`
|
||||
CHECK(wref.effect2) -- effect2 should not gc as `true` is not garbage collectable
|
||||
set_effect(node, effect1, {})
|
||||
set_effect(node, effect2, true)
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(not wref.node and not wref.effect2) -- node should now gc along with effect2
|
||||
|
||||
CHECK(not wref[1]) -- effect1 should gc since nothing is referencing table `t`
|
||||
CHECK(wref[2]) -- effect2 should not gc as `true` is not garbage collectable
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(not wref[3] and not wref[2]) -- node should now gc along with effect2
|
||||
|
||||
do -- same test but for multiple nodes referenced by watcher
|
||||
local function factory(a, b)
|
||||
|
|
@ -162,8 +169,8 @@ TEST("graph", function()
|
|||
wref.effect = effect
|
||||
|
||||
local t1 = {}
|
||||
setEffect(node1, effect, t1)
|
||||
setEffect(node2, effect, t1)
|
||||
set_effect(node1, effect, t1)
|
||||
set_effect(node2, effect, t1)
|
||||
|
||||
t1 = nil :: any
|
||||
effect = nil :: any
|
||||
|
|
@ -182,171 +189,69 @@ end)
|
|||
|
||||
TEST("wrap()", function()
|
||||
local wrap = vide.wrap
|
||||
local unwrap = vide.unwrap
|
||||
local wrapped = vide.wrapped
|
||||
local watch = vide.watch
|
||||
|
||||
do CASE "Wrap value"
|
||||
local state = wrap(1)
|
||||
CHECK(wrapped(state))
|
||||
CHECK(unwrap(state) == 1)
|
||||
CHECK(state() == 1)
|
||||
end
|
||||
|
||||
do CASE "Setter"
|
||||
local state, set = wrap(1)
|
||||
set(2) -- set directly
|
||||
CHECK(unwrap(state) == 2)
|
||||
set(function(x) return x + 1 end) -- set via function
|
||||
CHECK(unwrap(state) == 3)
|
||||
set((wrap(4))) -- set using value of another state
|
||||
CHECK(unwrap(state) == 4)
|
||||
local state = wrap(1)
|
||||
state(2) -- set directly
|
||||
CHECK(state() == 2)
|
||||
end
|
||||
|
||||
do CASE "Does not update if same value"
|
||||
local state, set = wrap(1)
|
||||
local state = wrap(1)
|
||||
|
||||
local updates = -1
|
||||
watch(function(from)
|
||||
from(state)
|
||||
watch(function()
|
||||
state()
|
||||
updates += 1
|
||||
end)
|
||||
set(1)
|
||||
CHECK(unwrap(state) == 1)
|
||||
state(1)
|
||||
CHECK(updates == 0)
|
||||
set(1, true)
|
||||
state(2)
|
||||
CHECK(updates == 1)
|
||||
end
|
||||
|
||||
do CASE "Does not rewrap state"
|
||||
local state = wrap((wrap(1)))
|
||||
CHECK(not wrapped(unwrap(state)))
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("unwrap()", function()
|
||||
local wrap = vide.wrap
|
||||
local unwrap = vide.unwrap
|
||||
|
||||
do CASE "Gets state value"
|
||||
local a = wrap(1)
|
||||
CHECK(unwrap(a) == 1)
|
||||
end
|
||||
|
||||
do CASE "Allow passthrough of non-state"
|
||||
CHECK(unwrap(5) == 5)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
TEST("wrapped()", function()
|
||||
local wrap = vide.wrap
|
||||
local wrapped = vide.wrapped
|
||||
|
||||
do CASE "Check if value is a state object"
|
||||
local state = wrap()
|
||||
CHECK(wrapped(state))
|
||||
end
|
||||
|
||||
do CASE "Refuse non-state"
|
||||
CHECK(not wrapped(0))
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("derive()", function()
|
||||
local wrap = vide.wrap
|
||||
local unwrap = vide.unwrap
|
||||
local derive = vide.derive
|
||||
|
||||
do CASE "Derive new value on state change"
|
||||
local state, set = wrap(1)
|
||||
local state = wrap(1)
|
||||
|
||||
local derived = derive(function(from)
|
||||
return tostring(from(state))
|
||||
local derived = derive(function()
|
||||
return tostring(state())
|
||||
end)
|
||||
|
||||
CHECK(unwrap(derived) == "1") -- check initial run during detection
|
||||
set(function(x) return x + 1 end)
|
||||
CHECK(unwrap(derived) == "2") -- check actually updates
|
||||
end
|
||||
|
||||
do CASE "Shorthand derivation"
|
||||
do
|
||||
local count, set = wrap(1 :: any)
|
||||
local text = "Count: " .. count
|
||||
CHECK(unwrap(text) == "Count: 1")
|
||||
set(2)
|
||||
CHECK(unwrap(text) == "Count: 2")
|
||||
end
|
||||
do
|
||||
local count, set = wrap(1 :: any)
|
||||
local text = count .. "x"
|
||||
CHECK(unwrap(text) == "1x")
|
||||
set(2)
|
||||
CHECK(unwrap(text) == "2x")
|
||||
end
|
||||
do
|
||||
local count, set = wrap(1 :: any)
|
||||
local text = count .. count
|
||||
CHECK(unwrap(text) == "11")
|
||||
set(2)
|
||||
CHECK(unwrap(text) == "22")
|
||||
end
|
||||
|
||||
do
|
||||
local a, seta = wrap { b = { c = 1 } }
|
||||
local b = a.b
|
||||
local c = b.c
|
||||
|
||||
CHECK(unwrap(c) == 1)
|
||||
|
||||
seta { b = { c = 2 } }
|
||||
|
||||
CHECK(unwrap(c) == 2)
|
||||
end
|
||||
|
||||
do
|
||||
local state, set = wrap { profiles = { decimal = { level = 1 }}}
|
||||
local stringified = "Level: " .. state.profiles.decimal.level
|
||||
|
||||
set(function(state)
|
||||
state.profiles.decimal.level += 1
|
||||
return state
|
||||
end, true)
|
||||
|
||||
CHECK(unwrap(stringified) == "Level: 2")
|
||||
end
|
||||
CHECK(derived() == "1") -- check initial run during detection
|
||||
state(state() + 1)
|
||||
CHECK(derived() == "2") -- check updates
|
||||
end
|
||||
|
||||
do CASE "Derive from updated"
|
||||
do
|
||||
local a, set = wrap(1)
|
||||
local a = wrap(1)
|
||||
|
||||
local b = derive(function(from)
|
||||
return from(a) + 1
|
||||
local b = derive(function()
|
||||
return a() + 1
|
||||
end)
|
||||
|
||||
set(2)
|
||||
a(2)
|
||||
|
||||
local c = derive(function(from)
|
||||
return from(b) + 1
|
||||
local c = derive(function()
|
||||
return b() + 1
|
||||
end)
|
||||
|
||||
CHECK(unwrap(c) == 4)
|
||||
end
|
||||
|
||||
do
|
||||
local a, set = wrap(1)
|
||||
|
||||
local b = a + 1
|
||||
|
||||
set(2)
|
||||
|
||||
local c = b + 1
|
||||
|
||||
CHECK(unwrap(c) == 4)
|
||||
CHECK(c() == 4)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
do CASE "Cleanup"
|
||||
local count, set = wrap(1)
|
||||
|
||||
|
|
@ -356,69 +261,67 @@ TEST("derive()", function()
|
|||
v.Destroyed = true
|
||||
end)
|
||||
|
||||
local first = unwrap(derived)
|
||||
local first = derived()
|
||||
CHECK(first.Destroyed == false)
|
||||
set(2)
|
||||
local _ = unwrap(derived) -- trigger recalc
|
||||
local _ = derived() -- trigger recalc
|
||||
CHECK(first.Destroyed == true)
|
||||
end
|
||||
]]
|
||||
|
||||
do CASE "Garbage collection"
|
||||
do -- check that `derived` does not allow gc of `state`
|
||||
local state = wrap(1) :: State?
|
||||
do -- check that `b` does not allow gc of `a`
|
||||
local a = wrap(1)
|
||||
|
||||
local _derived = derive(function(from)
|
||||
return from(state)
|
||||
local _b = derive(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
wref.state, state = state, nil
|
||||
local wref = weak { a }
|
||||
a = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(not wref.state)
|
||||
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do -- check that `state` allows gc of `derived`
|
||||
local state = wrap(1)
|
||||
do -- check that `a` allows gc of `b`
|
||||
local a = wrap(1)
|
||||
|
||||
local derived = derive(function(from)
|
||||
return from(state)
|
||||
end) :: State?
|
||||
local b = derive(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
wref.derived, derived = derived, nil
|
||||
local wref = weak { b }
|
||||
b = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(not wref.derived)
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
end
|
||||
|
||||
do CASE "Garbage collection 2"
|
||||
-- creats a chain `a -> b -> c` where `a` is the root
|
||||
local function setup()
|
||||
local weak = setmetatable({}, { __mode = "v" })
|
||||
local a = wrap(1)
|
||||
|
||||
local a, setA = wrap(1)
|
||||
|
||||
local b = derive(function(from)
|
||||
return from(a)
|
||||
local b = derive(function()
|
||||
return a()
|
||||
end)
|
||||
|
||||
local c = derive(function(from)
|
||||
return from(b)
|
||||
local c = derive(function()
|
||||
return b()
|
||||
end)
|
||||
|
||||
weak.a = a
|
||||
weak.b = b
|
||||
weak.c = c
|
||||
|
||||
return weak, a, b, c, setA
|
||||
return weak { a, b, c }, a, b, c
|
||||
end
|
||||
|
||||
do -- check that b and c can gc if a is referenced
|
||||
local weak, _a = setup()
|
||||
local wref, _a = setup()
|
||||
|
||||
gc()
|
||||
CHECK(not weak.b)
|
||||
CHECK(not weak.c)
|
||||
CHECK(not wref[2])
|
||||
CHECK(not wref[3])
|
||||
end
|
||||
|
||||
do -- check that a and b wont gc if c is referenced
|
||||
|
|
@ -427,21 +330,21 @@ TEST("derive()", function()
|
|||
_a, _b = nil :: any, nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(weak.a)
|
||||
CHECK(weak.b)
|
||||
CHECK(weak[1])
|
||||
CHECK(weak[2])
|
||||
end
|
||||
|
||||
do -- check that b wont gc if a and c are referenced
|
||||
local weak, _a, _b, c, setA = setup()
|
||||
local weak, a, _b, c = setup()
|
||||
|
||||
_b = nil :: any
|
||||
|
||||
gc()
|
||||
|
||||
setA(2)
|
||||
a(2)
|
||||
|
||||
CHECK(weak.b)
|
||||
CHECK(unwrap(c) == 2)
|
||||
CHECK(weak[2])
|
||||
CHECK(c() == 2)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
@ -451,54 +354,54 @@ TEST("watch()", function()
|
|||
local watch = vide.watch
|
||||
|
||||
do CASE "Capture states"
|
||||
local a, setA = wrap(1)
|
||||
local b, setB = wrap(1)
|
||||
local a = wrap(1)
|
||||
local b = wrap(1)
|
||||
local runcount = 0
|
||||
|
||||
watch(function(from)
|
||||
local _ = from(a) + from(b)
|
||||
watch(function()
|
||||
a()
|
||||
b()
|
||||
runcount += 1
|
||||
end)
|
||||
|
||||
CHECK(runcount == 1) -- immediate callback execution
|
||||
setA(2)
|
||||
a(2)
|
||||
CHECK(runcount == 2)
|
||||
setB(2)
|
||||
b(2)
|
||||
CHECK(runcount == 3)
|
||||
end
|
||||
|
||||
do CASE "Stop watch"
|
||||
local a, setA = wrap(1)
|
||||
local b, setB = wrap(1)
|
||||
local a = wrap(1)
|
||||
local b = wrap(1)
|
||||
local runcount = 0
|
||||
|
||||
local unwatch = watch(function(from)
|
||||
local _ = from(a) + from(b)
|
||||
local unwatch = watch(function()
|
||||
a()
|
||||
runcount += 1
|
||||
end)
|
||||
|
||||
CHECK(runcount == 1)
|
||||
unwatch()
|
||||
setA(2)
|
||||
setB(2)
|
||||
a(2)
|
||||
CHECK(runcount == 1)
|
||||
end
|
||||
|
||||
do CASE "Side-effect cleanup"
|
||||
local state, set = wrap(1)
|
||||
local state = wrap(1)
|
||||
|
||||
local effect_runcount = 0
|
||||
local cleanup_runcount = 0
|
||||
|
||||
local unwatch = watch(function(from)
|
||||
local _ = from(state)
|
||||
local unwatch = watch(function()
|
||||
state()
|
||||
effect_runcount += 1
|
||||
return function() cleanup_runcount += 1 end
|
||||
end)
|
||||
|
||||
CHECK(effect_runcount == 1)
|
||||
CHECK(cleanup_runcount == 0)
|
||||
set(2)
|
||||
state(2)
|
||||
CHECK(effect_runcount == 2)
|
||||
CHECK(cleanup_runcount == 1)
|
||||
unwatch()
|
||||
|
|
@ -508,22 +411,24 @@ TEST("watch()", function()
|
|||
|
||||
do CASE "Garbage collection"
|
||||
local function factory(p)
|
||||
return function(from: any)
|
||||
from(p)
|
||||
return function()
|
||||
p()
|
||||
end
|
||||
end
|
||||
|
||||
do -- state prevents gc of watcher
|
||||
local state = wrap(1)
|
||||
|
||||
local wref
|
||||
|
||||
do
|
||||
local effect = factory(state)
|
||||
|
||||
watch(effect)
|
||||
|
||||
wref.effect = effect
|
||||
effect = nil :: any
|
||||
wref = { effect }
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(wref.effect) -- should still exist
|
||||
CHECK(wref[1]) -- should still exist
|
||||
|
||||
end
|
||||
|
||||
|
|
@ -533,42 +438,42 @@ TEST("watch()", function()
|
|||
|
||||
local unwatch = watch(effect)
|
||||
|
||||
wref.effect = effect
|
||||
local wref = weak { effect }
|
||||
effect = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(wref.effect) -- should still exist
|
||||
CHECK(wref[1]) -- should still exist
|
||||
unwatch(); unwatch = nil :: any
|
||||
gc()
|
||||
CHECK(not wref.effect) -- should gc
|
||||
CHECK(not wref[1]) -- should gc
|
||||
|
||||
end
|
||||
|
||||
do -- state can gc with watcher
|
||||
local wref
|
||||
|
||||
do
|
||||
local state = wrap(1)
|
||||
local effect = factory(state)
|
||||
|
||||
watch(effect)
|
||||
|
||||
wref.state = state
|
||||
state = nil :: any
|
||||
|
||||
effect = nil :: any
|
||||
wref = weak { state }
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(not wref.state) -- should gc
|
||||
|
||||
CHECK(not wref[1]) -- should gc
|
||||
end
|
||||
|
||||
do -- watcher can gc if no state captured
|
||||
local effect = factory()
|
||||
local effect = factory(function() end)
|
||||
|
||||
watch(effect)
|
||||
|
||||
wref.effect = effect
|
||||
local wref = weak { effect }
|
||||
effect = nil :: any
|
||||
|
||||
gc()
|
||||
CHECK(not wref.effect) -- should gc
|
||||
CHECK(not wref[1]) -- should gc
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
@ -576,7 +481,6 @@ end)
|
|||
TEST("create()", function()
|
||||
local create = vide.create
|
||||
local wrap = vide.wrap
|
||||
local Children: "Children" = vide.Children :: "Children"
|
||||
|
||||
do CASE "Apply default properties"
|
||||
local defaults = require("src/defaults")
|
||||
|
|
@ -601,7 +505,6 @@ TEST("create()", function()
|
|||
|
||||
do CASE "Assign children"
|
||||
local frame = create "Frame" {
|
||||
[Children] = {
|
||||
create "TextLabel" { Name = "A" } :: any,
|
||||
create "TextLabel" { Name = "B" },
|
||||
{
|
||||
|
|
@ -612,7 +515,6 @@ TEST("create()", function()
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(frame:FindFirstChild "A")
|
||||
CHECK(frame:FindFirstChild "B")
|
||||
CHECK(frame:FindFirstChild "C")
|
||||
|
|
@ -620,14 +522,14 @@ TEST("create()", function()
|
|||
CHECK(frame:FindFirstChild "E")
|
||||
|
||||
local image = create "ImageLabel" {
|
||||
[Children] = create "TextLabel" { Name = "A" }
|
||||
create "TextLabel" { Name = "A" }
|
||||
}
|
||||
CHECK(image:FindFirstChild "A")
|
||||
end
|
||||
|
||||
do CASE "Binding properties to state"
|
||||
local name, setName = wrap("Hi")
|
||||
local text, setText = wrap("Bye")
|
||||
local name = wrap("Hi")
|
||||
local text = wrap("Bye")
|
||||
|
||||
local label = create "TextLabel" {
|
||||
Name = name,
|
||||
|
|
@ -637,8 +539,8 @@ TEST("create()", function()
|
|||
CHECK(label.Name == "Hi")
|
||||
CHECK(label.Text == "Bye")
|
||||
|
||||
setName "Foo"
|
||||
setText "Bar"
|
||||
name "Foo"
|
||||
text "Bar"
|
||||
|
||||
CHECK(label.Name == "Foo")
|
||||
CHECK(label.Text == "Bar")
|
||||
|
|
@ -648,14 +550,14 @@ TEST("create()", function()
|
|||
do -- instance should gc despite property bound to state
|
||||
local state = wrap("Hi")
|
||||
|
||||
do
|
||||
wref.instance = create "TextLabel" {
|
||||
local wref = weak {
|
||||
create "TextLabel" {
|
||||
Text = state,
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
gc()
|
||||
CHECK(not wref.instance)
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do -- instance should NOT gc despite property bound to state when parented
|
||||
|
|
@ -663,45 +565,47 @@ TEST("create()", function()
|
|||
|
||||
local parent = create "Frame" {}
|
||||
|
||||
do
|
||||
wref.instance = create "TextLabel" {
|
||||
local wref = weak {
|
||||
create "TextLabel" {
|
||||
Parent = parent,
|
||||
Text = state,
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
gc()
|
||||
CHECK(wref.instance)
|
||||
CHECK(wref[1])
|
||||
|
||||
wref.instance.Parent = nil
|
||||
wref.instance.Parent = parent
|
||||
wref[1].Parent = nil
|
||||
wref[1].Parent = parent
|
||||
|
||||
gc()
|
||||
CHECK(wref.instance)
|
||||
CHECK(wref[1])
|
||||
|
||||
wref.instance:Destroy()
|
||||
wref[1]:Destroy()
|
||||
|
||||
gc()
|
||||
CHECK(not wref.instance)
|
||||
CHECK(not wref[1])
|
||||
end
|
||||
|
||||
do -- state should not gc once exits scope while instance still exists
|
||||
local _label
|
||||
local wref
|
||||
|
||||
do
|
||||
local state = wrap("Hi")
|
||||
wref.state = state
|
||||
|
||||
wref = weak { state }
|
||||
_label = create "TextLabel" {
|
||||
Name = state,
|
||||
}
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(wref.state)
|
||||
CHECK(wref[1])
|
||||
end
|
||||
|
||||
do -- state and instance should gc once both exit scope
|
||||
local wref
|
||||
|
||||
do
|
||||
local text = wrap("Hi")
|
||||
|
||||
|
|
@ -709,16 +613,16 @@ TEST("create()", function()
|
|||
Text = text,
|
||||
}
|
||||
|
||||
wref.text = text
|
||||
wref.box = box
|
||||
wref = weak { text = text, box = box}
|
||||
end
|
||||
|
||||
gc()
|
||||
CHECK(not wref.state)
|
||||
CHECK(not wref.text)
|
||||
CHECK(not wref.box)
|
||||
|
||||
end
|
||||
|
||||
--[[
|
||||
do -- binding should gc despite state still existing after instance is gc
|
||||
local state = wrap("Hi")
|
||||
|
||||
|
|
@ -727,8 +631,13 @@ TEST("create()", function()
|
|||
Text = state,
|
||||
}
|
||||
|
||||
local wref = {
|
||||
instance = instance,
|
||||
brinding = next((state :: any).effects
|
||||
}
|
||||
|
||||
wref.instance = instance
|
||||
wref.binding = next((state :: any).__effects)
|
||||
wref.binding = )
|
||||
end
|
||||
|
||||
CHECK(wref.binding)
|
||||
|
|
@ -737,11 +646,11 @@ TEST("create()", function()
|
|||
|
||||
CHECK(not wref.instance)
|
||||
CHECK(not wref.binding)
|
||||
end
|
||||
end]]
|
||||
end
|
||||
|
||||
do CASE "Bind same state to multiple instance properties"
|
||||
local state, set = wrap "1"
|
||||
local state = wrap "1"
|
||||
|
||||
local text = create "TextBox" {
|
||||
Name = state,
|
||||
|
|
@ -749,7 +658,7 @@ TEST("create()", function()
|
|||
PlaceholderText = state
|
||||
}
|
||||
|
||||
set "2"
|
||||
state "2"
|
||||
|
||||
CHECK(text.Name == "2")
|
||||
CHECK(text.Text == "2")
|
||||
|
|
@ -757,7 +666,7 @@ TEST("create()", function()
|
|||
end
|
||||
|
||||
do CASE "Bind children"
|
||||
local state, set = wrap({} :: {}?)
|
||||
local state = wrap({} :: {}?)
|
||||
|
||||
local a, b, c =
|
||||
create "TextLabel" { Name = "A" },
|
||||
|
|
@ -765,31 +674,32 @@ TEST("create()", function()
|
|||
create "TextLabel" { Name = "C" }
|
||||
|
||||
local frame = create "Frame" {
|
||||
[Children] = state
|
||||
state
|
||||
}
|
||||
|
||||
set { a, b }
|
||||
state { a, b }
|
||||
|
||||
CHECK(frame:FindFirstChild "A")
|
||||
CHECK(frame:FindFirstChild "B")
|
||||
|
||||
-- check that b is removed and c is added while a remains untouched
|
||||
|
||||
set { a, c }
|
||||
state { a, c }
|
||||
|
||||
CHECK(frame:FindFirstChild "A")
|
||||
CHECK(frame:FindFirstChild "C")
|
||||
CHECK(not frame:FindFirstChild "B")
|
||||
|
||||
set(nil)
|
||||
state(nil)
|
||||
|
||||
CHECK(#frame:GetChildren() == 0)
|
||||
end
|
||||
|
||||
--[[
|
||||
do CASE "Parent set to nil by state does not allow gc"
|
||||
-- this is technically a bug but we test for this anyways to confirm behavior
|
||||
local parent = create "Frame" { Name = "Parent" }
|
||||
local state, set = wrap(parent :: Frame?)
|
||||
local state = wrap(parent :: Frame?)
|
||||
|
||||
do
|
||||
wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame?
|
||||
|
|
@ -808,6 +718,7 @@ TEST("create()", function()
|
|||
gc()
|
||||
CHECK(not wref.child)
|
||||
end
|
||||
]]
|
||||
|
||||
do CASE "GC test"
|
||||
local data = setmetatable({}, {})
|
||||
|
|
@ -872,9 +783,9 @@ TEST("map()", function()
|
|||
return tostring(v)
|
||||
end)
|
||||
|
||||
local t = unwrap(derived)
|
||||
local t = derived()
|
||||
|
||||
for i, v in next, unwrap(state) do
|
||||
for i, v in next, state() do
|
||||
CHECK(tostring(v) == t[i])
|
||||
end
|
||||
end
|
||||
|
|
@ -889,10 +800,10 @@ TEST("map()", function()
|
|||
return tostring(v)
|
||||
end)
|
||||
|
||||
local _ = unwrap(derived) -- trigger evaluation (so the next set is forced to be re-calculated)
|
||||
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
|
||||
set { 1, 2, 4 }
|
||||
|
||||
local t = unwrap(derived)
|
||||
local t = derived()
|
||||
|
||||
CHECK(t[1] == "1")
|
||||
CHECK(t[2] == "2")
|
||||
|
|
@ -910,10 +821,10 @@ TEST("map()", function()
|
|||
return tostring(v)
|
||||
end)
|
||||
|
||||
local _ = unwrap(derived) -- trigger evaluation (so the next set is forced to be re-calculated)
|
||||
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
|
||||
set { 1, 2 }
|
||||
|
||||
local t = unwrap(derived)
|
||||
local t = derived()
|
||||
|
||||
CHECK(t[1] == "1")
|
||||
CHECK(t[2] == "2")
|
||||
|
|
@ -962,13 +873,13 @@ TEST("map()", function()
|
|||
v.Destroyed = true
|
||||
end)
|
||||
|
||||
local first = unwrap(derived)
|
||||
local first = derived()
|
||||
|
||||
CHECK(first[1].Destroyed == false)
|
||||
|
||||
set { 1, 2, 4 }
|
||||
|
||||
local _ = unwrap(derived)
|
||||
local _ = derived()
|
||||
|
||||
CHECK(first[1].Destroyed == false)
|
||||
CHECK(first[2].Destroyed == false)
|
||||
|
|
@ -1033,10 +944,10 @@ TEST("spring()", function()
|
|||
local springed = spring(number, 1, 1)
|
||||
|
||||
set(20)
|
||||
CHECK(unwrap(springed) == 10)
|
||||
CHECK(springed() == 10)
|
||||
vide.step(1/60)
|
||||
CHECK(unwrap(springed) ~= 10)
|
||||
CHECK(unwrap(springed) > 10)
|
||||
CHECK(springed() ~= 10)
|
||||
CHECK(springed() > 10)
|
||||
end
|
||||
|
||||
do CASE "Garbage collection"
|
||||
|
|
@ -1080,23 +991,6 @@ TEST("spring()", function()
|
|||
end
|
||||
end)
|
||||
|
||||
TEST("Layout", function()
|
||||
local create = vide.create
|
||||
local Layout = vide.Layout
|
||||
|
||||
do CASE "Apply layout properties"
|
||||
local frame = create "Frame" {
|
||||
[Layout] = {
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
CHECK(frame.AnchorPoint == Vector2.new(0, 0.5))
|
||||
CHECK(frame.Position == UDim2.fromScale(0.5, 0.5))
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("Event", function()
|
||||
local create = vide.create
|
||||
local Event = vide.Event
|
||||
|
|
@ -1199,6 +1093,7 @@ TEST("Changed", function()
|
|||
end
|
||||
end)
|
||||
|
||||
--[[
|
||||
TEST("Created", function()
|
||||
local create = vide.create
|
||||
local Created = vide.Created
|
||||
|
|
@ -1218,7 +1113,7 @@ TEST("Created", function()
|
|||
|
||||
CHECK(ran)
|
||||
end
|
||||
end)
|
||||
end)]]
|
||||
|
||||
TEST("strict", function()
|
||||
vide.strict = true
|
||||
|
|
@ -1265,7 +1160,7 @@ TEST("strict", function()
|
|||
|
||||
CHECK(runcount == 2)
|
||||
set(2)
|
||||
local _ = unwrap(derived)
|
||||
local _ = derived()
|
||||
CHECK(runcount == 4)
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue