This commit is contained in:
Aaron Smith 2023-08-01 16:26:09 +01:00
parent bf1b1978d9
commit 5b270f0f36
12 changed files with 308 additions and 477 deletions

View file

@ -1,6 +1,6 @@
{ {
"languageMode": "strict", "languageMode": "strict",
"lint": { "BuiltinGlobalWrite": false, "UnknownGlobal": false }, "lint": { "BuiltinGlobalWrite": false, "UnknownGlobal": false },
"globals": [] "globals": [ "Instance" ]
} }

View file

@ -1,39 +0,0 @@
------------------------------------------------------------------------------------------
-- vide/Children.lua
------------------------------------------------------------------------------------------
if not game then
script = (require :: any) "test/wrap-require"
typeof = require "test/mock".typeof
end
local graph = require(script.Parent.graph)
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local Types = require(script.Parent.Types)
type Children = Types.Children
local function setChildren(instance: Instance, children: Children)
if typeof(children) == "Instance" then
if children.Parent then throw(`Cannot parent instance { instance.Name }, instance already parented`) end
children.Parent = instance
elseif wrapped(children) then
bind.children(children :: any, instance )
elseif type(children) == "table" then
for _, child: Children in next, children do
setChildren(instance, child)
end
else
throw(`Cannot parent non-instance { typeof(children) }`)
end
end
local Children = {
priority = 1,
run = setChildren
} :: Types.Symbol<Children>
return Children :: unknown

View file

@ -1,16 +0,0 @@
------------------------------------------------------------------------------------------
-- vide/Created.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local Types = require(script.Parent.Types)
local Created = {
priority = 3,
run = function(instance: Instance, callback: (Instance) -> ())
callback(instance)
end
} :: Types.Symbol<(Instance) -> ()>
return Created :: unknown

View file

@ -1,46 +0,0 @@
------------------------------------------------------------------------------------------
-- vide/Layout.lua
------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
local wrapped = graph.wrapped
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local flags = require(script.Parent.flags)
local Types = require(script.Parent.Types)
local layoutProperties = {
Parent = true,
AnchorPoint = true,
LayoutOrder = true,
Position = true,
Rotation = true,
Size = true,
SizeConstraint = true,
Visible = true,
ZIndex = true
}
local function setLayout(instance: Instance, properties: { [string]: unknown })
for property, value in next, properties do
if flags.strict and not layoutProperties[property] then
throw(`{ property } is not a valid layout property`)
end
if wrapped(value) then
bind.property(value :: graph.State<unknown>, instance, property)
else
(instance :: any)[property] = value
end
end
end
local Layout = {
priority = 1,
run = setLayout
} :: Types.Symbol<{ [string]: unknown }>
return Layout :: unknown

View file

@ -1,19 +0,0 @@
--------------------------------------------------------------------------------------------------------------
-- vide/Types.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type MaybeState<T> = graph.MaybeState<T>
export type Symbol<T> = {
priority: number,
run: (Instance, T) -> ()
}
export type Children = Instance | State<Children> | { Children }
return {}

View file

@ -1,24 +0,0 @@
--------------------------------------------------------------------------------------------------------------
-- vide/each.lua
--------------------------------------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type State<T> = graph.State<T>
type Node<T> = graph.Node<T>
local create = graph.create
local get = graph.get
local wrapped = graph.wrapped
local link = graph.link
type Map<K, V> = { [K]: V }
local function each<K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
end
return (each :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
-> State<Map<K, VO>> )

View file

@ -19,13 +19,13 @@ local refs = {} :: { Node<unknown> }
local WEAK_VALUES_RESIZABLE = { __mode = "vs" } local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
local EVALUATION_ERR = "error while evaluating node:\n\n" local EVALUATION_ERR = "error while evaluating node:\n\n"
setmetatable(refs, WEAK_VALUES_RESIZABLE) setmetatable(refs :: any, WEAK_VALUES_RESIZABLE)
local check_for_yield do local check_for_yield do
local t = { __mode = "kv" } local t = { __mode = "kv" }
setmetatable(t, t) setmetatable(t, t)
check_for_yield = function<T..., U...>(fn: (T...) -> (), ...: U...) check_for_yield = function<T..., U...>(fn: (T...) -> (), ...: any)
local args = { ... } local args = { ... }
t.__unm = function() t.__unm = function()
fn(unpack(args)) fn(unpack(args))
@ -96,13 +96,19 @@ local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T)
end end
-- detect what nodes were referenced in the given callback and returns them in an array -- 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) local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T)
if flags.strict then check_for_yield(fn, arg) end if flags.strict then check_for_yield(fn, arg) end
table.clear(refs) table.clear(refs)
reff = true reff = true
local ok: boolean, result: T|string = pcall(fn, arg) local ok: boolean, result: T|string
if arg == nil then
ok, result = pcall(fn)
else
ok, result = pcall(fn, arg)
end
reff = false reff = false
@ -123,13 +129,19 @@ local function capture_and_link<T>(child: Node<T>, fn: () -> T): T
return value :: T return value :: T
end end
local function create<T>(value: T): Node<T> local function create<T>(value: T): (Node<T>, () -> T)
return { local node = {
cache = value, cache = value,
derive = function() return nil :: any end, derive = function() return nil :: any end,
effects = setmetatable({}, WEAK_VALUES_RESIZABLE) :: any, effects = setmetatable({}, WEAK_VALUES_RESIZABLE) :: any,
children = false :: false children = false :: false
} }
local function get_value()
return get(node)
end
return node, get_value
end end
return table.freeze { return table.freeze {
@ -139,5 +151,5 @@ return table.freeze {
link = link, link = link,
capture = capture, capture = capture,
capture_and_link = capture_and_link, capture_and_link = capture_and_link,
create = create, create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)),
} }

View file

@ -5,72 +5,66 @@
if not game then script = (require :: any) "test/wrap-require" end if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type State<T> = graph.State<T> type Node<T> = graph.Node<T>
type MaybeState<T> = graph.MaybeState<T>
local create = graph.create local create = graph.create
local get = graph.get local set = graph.set
local wrapped = graph.wrapped local capture = graph.capture
local link = graph.link local link = graph.link
local capture_and_link = graph.capture_and_link
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local function map<K, VI, VO>(input: unknown, transform: (K, VI) -> VO, cleanup: (VO) -> ()?): (MaybeState<Map<K, VO>>) local function map<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> Map<K, VO>
if type(input) == "number" then local input_cache = {} :: Map<K, VI>
local output: Map<K, VO> = table.create(input) :: {} local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, Node<VI>>
local remove_queue = {} :: { K }
for i = 1, input do local function recompute(data)
local v = transform(i :: any, nil :: any) -- queue removed values
output[i :: any] = v for k in next, input_cache do
if data[k] == nil then
table.insert(remove_queue, k)
end
end end
return output -- remove queued values
elseif wrapped(input) then for _, k in next, remove_queue do
local lastInput: Map<K, VI> = {} input_cache[k] = nil
local lastOutput: Map<K, VO> = {} output_cache[k] = nil
local output = create(lastOutput) input_nodes[k] = nil
end
-- process new or changed values
for k, v in next, data do
if input_cache[k] == nil then
local node, get_value = create(v)
input_nodes[k] = node
output_cache[k] = transform(get_value, k)
elseif input_cache[k] ~= v then
set(input_nodes[k], v)
end
input_cache[k] = v
end
return output_cache
end
local function derive() local function derive()
local newInput = get(input :: State<Map<K, VI>>) return recompute(input())
if type(newInput) ~= "table" then error("Attempt to run map on a non-table state", 0) end
local lastInputClone = table.clone(lastInput)
for k, vi in next, newInput do
if vi ~= lastInputClone[k] then
local vo = transform(k, vi)
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
lastInput[k] = vi
lastOutput[k] = vo
end
lastInputClone[k] = nil
end end
for k, v in next, lastInputClone do local output, get_output = create(output_cache)
lastInput[k] = nil
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end local nodes, value = capture(input)
lastOutput[k] = nil
for _, node in next, nodes do
link(node, output, derive)
end end
return table.clone(lastOutput) output.cache = recompute(value)
return get_output
end end
link(input :: State<Map<K, VI>>, output, derive) return map
output.__updated = true
return output
elseif type(input) == "table" then
local output: Map<K, VO> = table.create(#input :: any) :: {}
for k, vi in input :: Map<K, VI> do
local vo = transform(k, vi)
output[k] = vo
end
return output
else error(string.format("Invalid type arg #1, expected number or table or state (got %s)", tostring(input)), 2) end
end
return (map :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
-> State<Map<K, VO>> )

View file

@ -11,15 +11,13 @@ local get = graph.get
local set = graph.set local set = graph.set
type State<T> = (() -> T) & ((T) -> T) type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(value: T | () -> T): State<T> local function source<T>(value: T): Source<T>
if type(value) == "function" then value = value() end local node, get_value = create(value :: T)
local node = create(value :: T)
return function(...): T return function(...): T
if select("#", ...) == 0 then return get(node) end if select("#", ...) == 0 then return get_value() end
local v = ... :: T local v = ... :: T
if node.cache == v and type(v) ~= "table" then return v end if node.cache == v and type(v) ~= "table" then return v end
@ -29,4 +27,4 @@ local function source<T>(value: T | () -> T): State<T>
end end
end end
return source return source :: (<T>(value: T) -> Source<T>) & (<T>() -> Source<T>)

View file

@ -10,7 +10,7 @@ local capture = graph.capture
local function watch(effect: () -> ()): () -> () local function watch(effect: () -> ()): () -> ()
-- todo: call cleanup on initial debug call when strict -- todo: call cleanup on initial debug call when strict
local nodes, cleanup = capture(effect :: () -> (() -> ()?)) local nodes, cleanup = capture(effect :: () -> (() -> ()?), nil)
nodes = table.clone(nodes) nodes = table.clone(nodes)

View file

@ -22,9 +22,6 @@ local function weak<T>(t: T & {}): T
return t return t
end end
-- weak reference table used for gc tests
--local wref = setmetatable({}, { __mode = "kv" }) :: any
TEST("graph", function() TEST("graph", function()
local graph = require "src/graph" local graph = require "src/graph"
local create = graph.create local create = graph.create
@ -35,12 +32,12 @@ TEST("graph", function()
local link = graph.link local link = graph.link
local set_effect = graph.set_effect local set_effect = graph.set_effect
do CASE "Create" do CASE "Node creation"
local node = create(1) local node = create(1)
CHECK(get(node) == 1) CHECK(get(node) == 1)
end end
do CASE "Read/write" do CASE "Node value"
local node = create(0) local node = create(0)
set(node, 1) set(node, 1)
CHECK(get(node) == 1) CHECK(get(node) == 1)
@ -48,7 +45,7 @@ TEST("graph", function()
CHECK(get(node) == 2) CHECK(get(node) == 2)
end end
do CASE "Capture" do CASE "Capture nodes"
local node1 = create(nil) local node1 = create(nil)
local node2 = create(nil) local node2 = create(nil)
local nodes = capture(function() local nodes = capture(function()
@ -58,74 +55,65 @@ TEST("graph", function()
CHECK(nodes[2] == node2) CHECK(nodes[2] == node2)
end end
do CASE "Link" do CASE "Linking nodes"
local parent = create(1) local parent = create(1)
local child = create(0) local child = create(0)
link(parent, child, function() link(parent, child, function()
return get(parent) return get(parent)
end) end)
set(parent, get(parent) + 1) set(parent, get(parent) + 1)
CHECK(get(child) == 2) CHECK(get(child) == 2) -- child should automatically update
end end
do CASE "Capture and link" do CASE "Capture and link nodes"
local parent = create(1) local parent = create(1)
local child = create(nil :: any) local child = create()
capture_and_link(child, function() child.cache = capture_and_link(child, function()
return tostring(get(parent)) return tostring(get(parent))
end) end)
set(parent, get(parent) + 1) set(parent, 2)
CHECK(get(child) == "2") CHECK(get(child) == "2")
end end
--[[ --[[
do CASE "Scoped captures" do CASE "Scoped captures"
-- table.find but uses `rawequal` since nodes have overloaded __eq metamethod local a = create(0)
local function rawfind(t, x) local b = create(nil :: any)
for i, v in ipairs(t) do
if rawequal(x, v) then
return true
end
end
return false
end
local a = graph.create(1) local count = 0
local b = graph.create(1)
graph.link(a, b, function() return get(a) end) b.cache = capture_and_link(b, function()
set(a, get(a) + 1) -- mark `b` for recomputation count += 1
local data = get(a)
local c = create(data)
get(c)
return c
end)
-- `a` and `b` should be referenced local c = get(b)
local captures = graph.capture(function(from) return from(b) end)
-- check that only `b` was referenced CHECK(count == 1)
CHECK(not rawfind(captures, a)) set(c, 1)
CHECK(rawfind(captures, b)) CHECK(count == 1)
set(a, 1)
-- repeat for `capture_and_link` CHECK(count == 2)
local c = graph.create(1)
set(a, get(a) + 1) -- mark `b` for recomputation again
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 end
]] ]]
do CASE "Nodes garbage collection" do CASE "Nodes garbage collection"
local wref local wref = weak { create(1) }
do
wref = weak { create(1) }
end
gc() gc()
CHECK(not wref[1]) CHECK(not wref[1])
end end
do CASE "Node effect garbage collection" do CASE "Node effect garbage collection"
do
local wref local wref
do do
local function factory(p) -- factory function to prevent closure caching local function factory(p) -- factory function to prevent closure caching
return function() return function()
@ -139,51 +127,54 @@ TEST("graph", function()
local effect1 = factory(node) local effect1 = factory(node)
local effect2 = factory(node) local effect2 = factory(node)
wref = weak { effect1, effect2, node :: any } wref = weak { e1 = effect1, e2 = effect2, n = node}
set_effect(node, effect1, {}) set_effect(node, effect1, {})
set_effect(node, effect2, true) set_effect(node, effect2, true)
end end
gc() gc()
CHECK(not wref.e1) -- effect1 should gc since nothing is referencing table `t`
CHECK(not wref[1]) -- effect1 should gc since nothing is referencing table `t` CHECK(wref.e2) -- effect2 should not gc as `true` is not garbage collectable
CHECK(wref[2]) -- effect2 should not gc as `true` is not garbage collectable
end end
gc() gc()
CHECK(not wref[3] and not wref[2]) -- node should now gc along with effect2 CHECK(not wref.n and not wref.e2) -- node should now gc along with effect2
end
do
local wref
do -- same test but for multiple nodes referenced by watcher do -- same test but for multiple nodes referenced by watcher
local function factory(a, b) local function factory(a, b)
return (function(c, d)
return function() return function()
return get(a), get(b) return get(c), get(d)
end end
end)(a, b)
end end
local node1 = create(1) local node1 = create(1)
local node2 = graph.create(1) local node2 = create(1)
do
local effect = factory(node1, node2) local effect = factory(node1, node2)
wref.node1 = node1
wref.node2 = node2 wref = weak { n1 = node1, n2 = node2, e = effect }
wref.effect = effect
local t1 = {} local t1 = {}
set_effect(node1, effect, t1) set_effect(node1, effect, t1)
set_effect(node2, effect, t1) set_effect(node2, effect, t1)
t1 = nil :: any
effect = nil :: any
gc()
CHECK(not wref.effect)
end end
gc() gc()
CHECK(not wref.e)
end
CHECK(not wref.node1) gc()
CHECK(not wref.node2) CHECK(not wref.n1)
CHECK(not wref.n2)
end
end end
end) end)
@ -191,14 +182,14 @@ TEST("source()", function()
local source = vide.source local source = vide.source
local watch = vide.watch local watch = vide.watch
do CASE "source value" do CASE "Create source"
local state = source(1) local state = source(1)
CHECK(state() == 1) CHECK(state() == 1)
end end
do CASE "Setter" do CASE "Set and get source value"
local state = source(1) local state = source(1)
state(2) -- set directly state(2)
CHECK(state() == 2) CHECK(state() == 2)
end end
@ -210,45 +201,60 @@ TEST("source()", function()
state() state()
updates += 1 updates += 1
end) end)
CHECK(updates == 0)
state(1) state(1)
CHECK(updates == 0) CHECK(updates == 0)
state(2) state(2)
CHECK(updates == 1) CHECK(updates == 1)
end end
do CASE "Does update if same value is table"
local state = source {}
local updates = -1
watch(function()
state()
updates += 1
end)
CHECK(updates == 0)
state(state())
CHECK(updates == 1)
end
end) end)
TEST("derive()", function() TEST("derive()", function()
local source = vide.source local source = vide.source
local derive = vide.derive local derive = vide.derive
do CASE "Derive new value on state change" do CASE "Derive new value on source change"
local state = source(1) local inputA = source(1)
local inputB = source(2)
local derived = derive(function() local output = derive(function()
return tostring(state()) return tostring(inputA() + inputB())
end) end)
CHECK(derived() == "1") -- check initial run during detection CHECK(output() == "3")
state(state() + 1) inputA(2)
CHECK(derived() == "2") -- check updates CHECK(output() == "4")
end end
do CASE "Derive from updated" do CASE "Derive transformed source"
do local input = source(1)
local a = source(1)
local b = derive(function() local transform = function()
return a() + 1 return tostring(input())
end)
a(2)
local c = derive(function()
return b() + 1
end)
CHECK(c() == 4)
end end
local output = derive(function()
return tonumber(transform())
end)
CHECK(output() == 1)
input(2)
CHECK(output() == 2)
end end
--[[ --[[
@ -271,29 +277,35 @@ TEST("derive()", function()
do CASE "Garbage collection" do CASE "Garbage collection"
do -- check that `b` does not allow gc of `a` do -- check that `b` does not allow gc of `a`
local wref, b
do
local a = source(1) local a = source(1)
local _b = derive(function() b = derive(function()
return a() return a()
end) end)
local wref = weak { a } wref = weak { a }
a = nil :: any end
gc() gc()
CHECK(wref[1])
CHECK(not wref[1]) b()
end end
do -- check that `a` allows gc of `b` do -- check that `a` allows gc of `b`
local a = source(1) local a = source(1)
local wref
do
local b = derive(function() local b = derive(function()
return a() return a()
end) end)
local wref = weak { b } wref = weak { b }
b = nil :: any end
gc() gc()
CHECK(not wref[1]) CHECK(not wref[1])
@ -301,9 +313,9 @@ TEST("derive()", function()
end end
do CASE "Garbage collection 2" do CASE "Garbage collection 2"
-- creats a chain `a -> b -> c` where `a` is the root -- creats a chain `a -> b -> c` where `a` is the source
local function setup() local function setup()
local a = source(1) local a = source(0)
local b = derive(function() local b = derive(function()
return a() return a()
@ -316,7 +328,7 @@ TEST("derive()", function()
return weak { a, b, c }, a, b, c return weak { a, b, c }, a, b, c
end end
do -- check that b and c can gc if a is referenced do -- check that `b` and `c` can gc if `a` is referenced
local wref, _a = setup() local wref, _a = setup()
gc() gc()
@ -324,7 +336,7 @@ TEST("derive()", function()
CHECK(not wref[3]) CHECK(not wref[3])
end end
do -- check that a and b wont gc if c is referenced do -- check that `a` and `b` wont gc if `c` is referenced
local weak, _a, _b, _c = setup() local weak, _a, _b, _c = setup()
_a, _b = nil :: any, nil :: any _a, _b = nil :: any, nil :: any
@ -334,16 +346,15 @@ TEST("derive()", function()
CHECK(weak[2]) CHECK(weak[2])
end end
do -- check that b wont gc if a and c are referenced do -- check that `b` wont gc if `a` and `c` are referenced
local weak, a, _b, c = setup() local weak, a, _b, c = setup()
_b = nil :: any _b = nil :: any
gc() gc()
CHECK(weak[2])
a(2) a(2)
CHECK(weak[2])
CHECK(c() == 2) CHECK(c() == 2)
end end
end end
@ -356,35 +367,33 @@ TEST("watch()", function()
do CASE "Capture states" do CASE "Capture states"
local a = source(1) local a = source(1)
local b = source(1) local b = source(1)
local runcount = 0
local runcount = -1
watch(function() watch(function()
a() a()
b() b()
runcount += 1 runcount += 1
end) end)
CHECK(runcount == 1) -- immediate callback execution CHECK(runcount == 0)
a(2) a(2)
CHECK(runcount == 2) CHECK(runcount == 1)
b(2) b(2)
CHECK(runcount == 3) CHECK(runcount == 2)
end end
do CASE "Stop watch" do CASE "Stop watch"
local a = source(1) local a = source(1)
local b = source(1)
local runcount = 0
local runcount = -1
local unwatch = watch(function() local unwatch = watch(function()
a() a()
runcount += 1 runcount += 1
end) end)
CHECK(runcount == 1)
unwatch() unwatch()
a(2) a(2)
CHECK(runcount == 1) CHECK(runcount == 0)
end end
do CASE "Side-effect cleanup" do CASE "Side-effect cleanup"
@ -428,24 +437,28 @@ TEST("watch()", function()
end end
gc() gc()
CHECK(wref[1]) -- should still exist CHECK(wref[1])
end end
do -- watcher can gc if stopped do -- watcher can gc if stopped
local state = source(1) local state = source(1)
local wref, unwatch
do
local effect = factory(state) local effect = factory(state)
unwatch = watch(effect)
local unwatch = watch(effect) wref = weak { effect }
end
local wref = weak { effect }
effect = nil :: any
gc() gc()
CHECK(wref[1]) -- should still exist CHECK(wref[1])
unwatch(); unwatch = nil :: any
unwatch()
unwatch = nil :: any -- unwatch holds ref to effect
gc() gc()
CHECK(not wref[1]) -- should gc CHECK(not wref[1])
end end
@ -460,20 +473,7 @@ TEST("watch()", function()
end end
gc() gc()
CHECK(not wref[1])
CHECK(not wref[1]) -- should gc
end
do -- watcher can gc if no state captured
local effect = factory(function() end)
watch(effect)
local wref = weak { effect }
effect = nil :: any
gc()
CHECK(not wref[1]) -- should gc
end end
end end
end) end)
@ -489,7 +489,7 @@ TEST("create()", function()
CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3) CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3)
end end
do CASE "Assign custom properties" do CASE "Set properties"
local text = create "TextLabel" { local text = create "TextLabel" {
Name = "Label", Name = "Label",
Text = "test" Text = "test"
@ -498,14 +498,23 @@ TEST("create()", function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end end
do CASE "Set nested properties"
local text = create "TextLabel" {
{ Name = "Label" },
Group = { Text = "test" }
}
CHECK(text.Name == "Label")
CHECK(text.Text == "test")
end
do CASE "Independent" do CASE "Independent"
local frame = create "Frame" local frame = create "Frame"
CHECK(frame {} ~= frame {}) CHECK(frame {} ~= frame {})
end end
do CASE "Assign children" do CASE "Set children"
local frame = create "Frame" { local frame = create "Frame" {
create "TextLabel" { Name = "A" } :: any, create "TextLabel" { Name = "A" },
create "TextLabel" { Name = "B" }, create "TextLabel" { Name = "B" },
{ {
create "TextLabel" { Name = "C" } :: any, create "TextLabel" { Name = "C" } :: any,
@ -513,18 +522,21 @@ TEST("create()", function()
{ {
create "TextLabel" { Name = "E" } create "TextLabel" { Name = "E" }
} }
},
Children = {
create "TextLabel" { Name = "F" } :: any,
{ create "TextLabel" { Name = "G" } }
} }
} }
CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "A")
CHECK(frame:FindFirstChild "B") CHECK(frame:FindFirstChild "B")
CHECK(frame:FindFirstChild "C") CHECK(frame:FindFirstChild "C")
CHECK(frame:FindFirstChild "D") CHECK(frame:FindFirstChild "D")
CHECK(frame:FindFirstChild "E") CHECK(frame:FindFirstChild "E")
local image = create "ImageLabel" { CHECK(frame:FindFirstChild "F")
create "TextLabel" { Name = "A" } CHECK(frame:FindFirstChild "G")
}
CHECK(image:FindFirstChild "A")
end end
do CASE "Binding properties to state" do CASE "Binding properties to state"
@ -547,7 +559,7 @@ TEST("create()", function()
end end
do CASE "Binding garbage collection" do CASE "Binding garbage collection"
do -- instance should gc despite property bound to state do -- instance should gc when unparented
local state = source("Hi") local state = source("Hi")
local wref = weak { local wref = weak {
@ -560,7 +572,7 @@ TEST("create()", function()
CHECK(not wref[1]) CHECK(not wref[1])
end end
do -- instance should NOT gc despite property bound to state when parented do -- instance should not gc when parented
local state = source("Hi") local state = source("Hi")
local parent = create "Frame" {} local parent = create "Frame" {}
@ -587,7 +599,7 @@ TEST("create()", function()
CHECK(not wref[1]) CHECK(not wref[1])
end end
do -- state should not gc once exits scope while instance still exists do -- instance does not allow gc of state
local label local label
local wref local wref
@ -621,34 +633,32 @@ TEST("create()", function()
gc() gc()
CHECK(not wref.text) CHECK(not wref.text)
CHECK(not wref.box) CHECK(not wref.box)
end end
--[[
do -- binding should gc despite state still existing after instance is gc do -- binding should gc despite state still existing after instance is gc
local state = source("Hi") local state = source("Hi")
local node = require "src/graph".capture(state)[1]
local wref
do do
local instance = create "TextLabel" { local instance = create "TextLabel" {
Text = state, Text = state,
} }
local wref = { wref = weak {
instance = instance, instance = instance,
brinding = next((state :: any).effects binding = next(node.effects)
} }
wref.instance = instance
wref.binding = )
end end
CHECK(wref.binding) CHECK(wref.binding)
gc() gc()
CHECK(not wref.instance) CHECK(not wref.instance)
CHECK(not wref.binding) CHECK(not wref.binding)
end]] end
end end
do CASE "Bind same state to multiple instance properties" do CASE "Bind same state to multiple instance properties"
@ -668,7 +678,7 @@ TEST("create()", function()
end end
do CASE "Bind children" do CASE "Bind children"
local state = source({} :: {}?) local state = source()
local a, b, c = local a, b, c =
create "TextLabel" { Name = "A" }, create "TextLabel" { Name = "A" },
@ -697,31 +707,28 @@ TEST("create()", function()
CHECK(#frame:GetChildren() == 0) CHECK(#frame:GetChildren() == 0)
end end
--[[
do CASE "Parent set to nil by state does not allow gc" 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 frame = create "Frame" { Name = "Parent" }
local parent = create "Frame" { Name = "Parent" } local parent = source(frame :: Frame?)
local state = source(parent :: Frame?)
do local wref = weak {
wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame? create "TextLabel" { Parent = parent, Name = "Child" }
}
gc()
CHECK(wref[1])
parent(nil)
gc()
CHECK(wref[1])
wref[1]:Destroy()
gc()
CHECK(not wref[1])
end end
gc()
CHECK(wref.child)
set(nil)
gc()
CHECK(wref.child)
wref.child:Destroy()
gc()
CHECK(not wref.child)
end
]]
do CASE "GC test" do CASE "GC test"
local wref local wref
@ -745,100 +752,64 @@ TEST("create()", function()
end end
gc() gc()
CHECK(wref.data and wref.proxy) CHECK(wref.data and wref.proxy)
end end
end) end)
TEST("map()", function() TEST("map()", function()
local source = vide.source local source = vide.source
local unsource = vide.unsource
local map = vide.map local map = vide.map
do CASE "Use integer"
local n = 5
local t = map(n, function(i)
return tostring(i)
end)
for i = 1, n do
CHECK(tostring(i) == t[i])
end
end
do CASE "Use table"
local t = { 1, 2, 3, 4, 5 }
local t2 = map(t, function(_, v)
return tostring(v)
end)
for i, v in t do
CHECK(tostring(v) == t2[i])
end
end
do CASE "Use state" do CASE "Use state"
local state = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local derived = map(state, function(_, v) local output = map(input, function(v, k)
return tostring(v) return tostring(v())
end) end)
local t = derived() CHECK("" .. input()[1] == output()[1])
CHECK("" .. input()[2] == output()[2])
for i, v in next, state() do CHECK("" .. input()[3] == output()[3])
CHECK(tostring(v) == t[i])
end
end end
do CASE "Cache result" do CASE "Cache result"
local state, set = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local runcount = table.create(3, 0) local runcount = table.create(3, 0)
local derived = map(state, function(i, v) local output = map(input, function(v, i)
runcount[i] += 1 runcount[i] += 1
return tostring(v) return v
end) end)
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated) input { 1, 2, 4 }
set { 1, 2, 4 }
local t = derived() CHECK(output()[1]() == 1)
CHECK(output()[2]() == 2)
CHECK(t[1] == "1") CHECK(output()[3]() == 4)
CHECK(t[2] == "2")
CHECK(t[3] == "4")
CHECK(runcount[1] == 1) CHECK(runcount[1] == 1)
CHECK(runcount[2] == 1) CHECK(runcount[2] == 1)
CHECK(runcount[3] == 2) CHECK(runcount[3] == 1)
end end
do CASE "Removal reflected" do CASE "Removal reflected"
local state, set = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local derived = map(state, function(i, v) local output = map(input, function(v, i)
return tostring(v) return v
end) end)
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated) input { 1, 2 }
set { 1, 2 }
local t = derived() local t = output()
CHECK(t[1] == "1") CHECK(t[1]() == 1)
CHECK(t[2] == "2") CHECK(t[2]() == 2)
CHECK(t[3] == nil) CHECK(t[3] == nil)
end end
local create = vide.create --[[
local Children = vide.Children
do CASE "Bind children" do CASE "Bind children"
local state, set = source { "A", "B", "C" } local state, set = source { "A", "B", "C" }
@ -919,7 +890,7 @@ TEST("map()", function()
gc() gc()
CHECK(not wref.derived) CHECK(not wref.derived)
end end
end end]]
end) end)
TEST("spring()", function() TEST("spring()", function()
@ -980,7 +951,6 @@ end)
TEST("Event", function() TEST("Event", function()
local create = vide.create local create = vide.create
local Event = vide.Event
local source = vide.source local source = vide.source
do CASE "Connect event" do CASE "Connect event"

View file

@ -35,6 +35,7 @@ Index
For For
untrack untrack
batch batch
async/loading/suspense
## version 1 ## version 1