diff --git a/src/Event.lua b/src/Event.lua deleted file mode 100644 index dc9dfe6..0000000 --- a/src/Event.lua +++ /dev/null @@ -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 = graph.State -type MaybeState = graph.MaybeState -local wrapped = graph.wrapped - -local Types = require(script.Parent.Types) - -type Listener = (unknown) -> () - -local getEventSymbol = memoize(function(name: string): Types.Symbol> - return { - priority = 2, - run = function(instance: Instance, listener: MaybeState) - local event: RBXScriptSignal<...unknown> = (instance :: any)[name] - - if type(listener) == "function" then - event:Connect(listener) - elseif wrapped(listener) then - bind.event(listener :: State, 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 } - diff --git a/src/apply.lua b/src/apply.lua index 15db585..f87fc60 100644 --- a/src/apply.lua +++ b/src/apply.lua @@ -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 = graph.Node -local function apply(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(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 diff --git a/src/applyProperties.lua b/src/applyProperties.lua deleted file mode 100644 index ee24896..0000000 --- a/src/applyProperties.lua +++ /dev/null @@ -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 = graph.State -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(instance: T & Instance, properties: { [string|Types.Symbol ]: 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, 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) - else - instance.Parent = parent :: Instance - end - end - - if postCreation then postCreation(instance) end - - return instance -end - -return applyProperties diff --git a/src/bind.lua b/src/bind.lua index 863aeec..c6f840d 100644 --- a/src/bind.lua +++ b/src/bind.lua @@ -11,9 +11,10 @@ end local graph = require(script.Parent.graph) -type State = graph.State +type Node = graph.Node 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, 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, 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, instance: Instance, updateInstance: (Instance) instance:GetPropertyChangedSignal("Parent"):Connect(ref) end -local function bindProperty(state: State, 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_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, } diff --git a/src/create.lua b/src/create.lua index b4fc1e1..065c3e8 100644 --- a/src/create.lua +++ b/src/create.lua @@ -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 diff --git a/src/derive.lua b/src/derive.lua index c48f39c..ead6911 100644 --- a/src/derive.lua +++ b/src/derive.lua @@ -5,28 +5,28 @@ if not game then script = (require :: any) "test/wrap-require" end local graph = require(script.Parent.graph) -type State = graph.State -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(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): State +local function derive(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 + return function() + return get(node) + end end return derive diff --git a/src/graph.lua b/src/graph.lua index d1c5531..573eeaf 100644 --- a/src/graph.lua +++ b/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 = typeof(setmetatable( - {} :: { - __cache: T, - __updated: boolean, - __derive: (any) -> T, - __effects: { [(unknown) -> ()]: unknown } | false, -- weak values - __children: { State } | 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 - --__lt - --__le - } -)) +export type Node = { + cache: T, + derive: () -> T, + effects: { [(unknown) -> ()]: unknown }, -- weak values + children: { Node } | false -- weak values +} -export type MaybeState = State | T -export type Unwrapper = (T) -> T +local reff = false +local refs = {} :: { Node } local WEAK_VALUES_RESIZABLE = { __mode = "vs" } -local EVALUATION_ERR = "error while evaluating state:\n\n" - -local State = {} +local EVALUATION_ERR = "error while evaluating node:\n\n" -local function wrapped(value: any): boolean - return getmetatable(value) == State -end - -local unwrap: (T) -> T; +setmetatable(refs, WEAK_VALUES_RESIZABLE) -local checkForYield do +local check_for_yield do local t = { __mode = "kv" } setmetatable(t, t) - checkForYield = function(fn: (Unwrapper) -> ()) + check_for_yield = function(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(state: State, 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(node: Node, fn: (T) -> (), key: T) + node.effects[fn :: () -> ()] = key end -local function runEffects(state: State) - if state.__effects then - for effect, key in next, state.__effects do - if flags.strict then effect(key) end - effect(key) - end - end +local function run_effects(node: Node) + for effect, key in next, node.effects do + if flags.strict then effect(key) end + effect(key) + end end --- retrieves a state's cached value +-- retrieves a node's cached value -- recalculates value if an ancestor was updated -local function get(state: State): 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") -end - --- utility function for retrieving a value from state and allowing passthrough of non-state -unwrap = function(value: MaybeState): T - if wrapped(value) then - return get(value :: State) - else - return value :: T - end +local function get(node: Node): T + if reff then table.insert(refs, node) end + return node.cache end -local function addChild(parent: State, child: State) - if parent.__children then - table.insert(parent.__children, child) +local function set_child(parent: Node, child: Node) + if parent.children then + table.insert(parent.children, child) else - parent.__children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any + parent.children = { child } + setmetatable(parent.children :: any, WEAK_VALUES_RESIZABLE) end end --- marks all state descendants for recalculation and runs effects -local function update(state: State) - runEffects(state) - if state.__children then - for _, child in state.__children do - if not child.__updated then - child.__updated = true - update(child) - end +-- runs node effects, recalculates descendants and runs descendant effects +local function update(node: Node) + 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 --- sets a state's cached value and updates all descendants -local function set(state: State, value: T) - state.__cache = value - update(state) +-- sets a node's cached value and updates all descendants +local function set(node: Node, value: T) + node.cache = value + update(node) end --- links two states as parent-child -local function link(parent: State, child: State, 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(parent: Node, child: Node, 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(callback: (Unwrapper) -> T): ({ State }, 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(fn: (U) -> T, arg: U): ({ Node }, 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(value: MaybeState): T - if wrapped(value) then - table.insert(states, value :: State) - return get(value :: State) - 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(child: State, callback: (Unwrapper) -> T): T - local states, value = capture(callback) +-- captures and links any detected nodes +local function capture_and_link(child: Node, fn: () -> T): T + local nodes, value = capture(fn, nil) - child.__derive = callback - for _, parent: State in next, states do - addChild(parent, child) + child.derive = fn + for _, parent: Node in next, nodes do + set_child(parent, child) end return value :: T end -local create: (value: T) -> State - --- 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, b: MaybeState): State - local derived: State = create(nil :: any) - - local aIsState = wrapped(a) - local bIsState = wrapped(b) - - if aIsState and bIsState then - local function derive() return op(get(a :: State), get(b :: State)) end - link(a :: State, derived, derive) - link(b :: State, derived, derive) - elseif aIsState then - link(a :: State, derived, function() return op(get(a :: State), b) end) - else--if bIsState then - link(b :: State, derived, function() return op(a, get(b :: State)) end) - end - - derived.__updated = true - return derived - end -end - -local function __unm(self: State) - 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, 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(value: T): State - return setmetatable({ - __cache = value, - __updated = false, - __derive = function() return nil end :: any, - __effects = false :: false, - __children = false :: false - }, State) +local function create(value: T): Node + 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, } diff --git a/src/init.lua b/src/init.lua index a7b304a..eddf429 100644 --- a/src/init.lua +++ b/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 = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, for local vide = { -- core create = create, - apply = apply, - wrap = (wrap :: any) :: (value: T?) -> (T, Setter), - derive = (derive :: any) :: (deriver: (from: (U) -> U) -> T, cleanup: (T) -> ()?) -> T, - map = (map :: any) :: ( (input: number, transform: (number) -> V, cleanup: (V) -> ()?) -> Map ) & ( (input: Map, transform: (K, VI) -> VO, cleanup: (VO) -> ()?) -> Map ), - watch = watch :: ((Unwrapper) -> ()) -> () -> (), - - -- util - unwrap = unwrap, - wrapped = wrapped, + wrap = wrap, + derive = derive, + map = map, + watch = watch, -- animations - spring = (spring :: any) :: (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, diff --git a/src/spring.lua b/src/spring.lua index 0d7ce38..5ef50c5 100644 --- a/src/spring.lua +++ b/src/spring.lua @@ -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 = graph.State -type MaybeState = graph.MaybeState 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 diff --git a/src/unwrap.lua b/src/unwrap.lua deleted file mode 100644 index 490e53a..0000000 --- a/src/unwrap.lua +++ /dev/null @@ -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 diff --git a/src/watch.lua b/src/watch.lua index 2ab094e..10411e6 100644 --- a/src/watch.lua +++ b/src/watch.lua @@ -5,27 +5,27 @@ if not game then script = (require :: any) "test/wrap-require" end local graph = require(script.Parent.graph) -type State = graph.State -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 diff --git a/src/wrap.lua b/src/wrap.lua index ef00069..b822720 100644 --- a/src/wrap.lua +++ b/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 = graph.State -type MaybeState = graph.MaybeState +type Node = graph.Node 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 = (value: MaybeState | (MaybeState) -> MaybeState, force: boolean?) -> T +type State = (() -> T) & ((T) -> T) -local function wrap(value: MaybeState?): (State, Setter) - local state = create(if wrapped(value) then get(value :: State) else value :: T) +local function wrap(value: T | () -> T): State + if type(value) == "function" then value = value() end - local function setter(vi: MaybeState | (MaybeState) -> MaybeState, 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) 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 diff --git a/src/wrapped.lua b/src/wrapped.lua deleted file mode 100644 index 4a68ee3..0000000 --- a/src/wrapped.lua +++ /dev/null @@ -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 diff --git a/test/testkit.luau b/test/testkit.luau index 144f908..e7ba3c9 100644 --- a/test/testkit.luau +++ b/test/testkit.luau @@ -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, + + white = function(s: string) + return `\27[37;1m{s}\27[0m` + end, -EXAMPLE USAGE: + green = function(s: string) + return `\27[32;1m{s}\27[0m` + end, -local testkit = require "path-to-testkit" + red = function(s: string) + return `\27[31;1m{s}\27[0m` + end, -local TEST, CASE, CHECK = testkit.getUnitTestTools() + yellow = function(s: string) + return `\27[33;1m{s}\27[0m` + end, -TEST("test name", function() - do CASE "A" - CHECK(condition) - end -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 BENCH, START = testkit.getBenchmarkTools() + 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 -BENCH("benchmark name", function() - local x = 0 + while value ~= 0 and value < 1 do + order -= 1 + value *= 1000 + end - for i = 1, START(1e6) do - x += 1 + 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 -end) -]] ------------------------------------------------------------------------------------------- --- Unit Testing ------------------------------------------------------------------------------------------- + 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(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") + case = test.case + end - local result = value and PASS or FAIL + assert(case, "no active case") - if activeCase.result == NONE or activeCase.result == PASS then - activeCase.result = result - activeCase.line = debug.info(2, "l") + 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 result == PASS + 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") - if not success then - print("[\27[41;1mERROR\27[0m] " .. name) - print("\27[31;1m" .. "error: " .. msg :: string .. "\27[0m") - activeBench = nil - return - end + local mem_start = gcinfo() + local time_start = os.clock() + local err_msg: string? - timeStart = bench.timeStart or timeStart - memStart = bench.memStart or memStart + local success = xpcall(fn, function(m: string) + err_msg = m .. debug.traceback(nil, 2) + end) - local n = bench.iterations or 1 - local duration = timeStop - timeStart - local allocated = memStop - memStart + local time_stop = os.clock() + local mem_stop = gcinfo() - print(string.format("[ %.3f us | %4.0f B ] %s", duration/n * 1e6, allocated/n * 1e3, name)) + if not success then + 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 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)) + + 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 + + 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 - activeBench = nil + 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 } diff --git a/test/tests.luau b/test/tests.luau index 04865e5..7310f82 100644 --- a/test/tests.luau +++ b/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 + 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) - local effect1 = factory(node) - local effect2 = factory(node) - wref.node = node - wref.effect1 = effect1 - wref.effect2 = effect2 + + do + local effect1 = factory(node) + local effect2 = factory(node) - 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 + set_effect(node, effect1, {}) + set_effect(node, effect2, true) + end 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 + 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.node and not wref.effect2) -- node should now gc along with effect2 + 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, setA = wrap(1) + local a = 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 effect = factory(state) - - watch(effect) - wref.effect = effect - effect = nil :: any + local wref + + do + local effect = factory(state) + watch(effect) + 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 state = wrap(1) - local effect = factory(state) - - watch(effect) + local wref - wref.state = state - state = nil :: any - - effect = nil :: any + do + local state = wrap(1) + local effect = factory(state) + watch(effect) + 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,15 +505,13 @@ TEST("create()", function() do CASE "Assign children" local frame = create "Frame" { - [Children] = { - create "TextLabel" { Name = "A" } :: any, - create "TextLabel" { Name = "B" }, + create "TextLabel" { Name = "A" } :: any, + create "TextLabel" { Name = "B" }, + { + create "TextLabel" { Name = "C" } :: any, + create "TextLabel" { Name = "D" }, { - create "TextLabel" { Name = "C" } :: any, - create "TextLabel" { Name = "D" }, - { - create "TextLabel" { Name = "E" } - } + create "TextLabel" { Name = "E" } } } } @@ -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