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
end if data[k] == nil then
table.insert(remove_queue, k)
return output
elseif wrapped(input) then
local lastInput: Map<K, VI> = {}
local lastOutput: Map<K, VO> = {}
local output = create(lastOutput)
local function derive()
local newInput = get(input :: State<Map<K, VI>>)
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
end
for k, v in next, lastInputClone do -- remove queued values
lastInput[k] = nil for _, k in next, remove_queue do
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end input_cache[k] = nil
lastOutput[k] = nil output_cache[k] = nil
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 end
input_cache[k] = v
return table.clone(lastOutput)
end end
link(input :: State<Map<K, VI>>, output, derive) return output_cache
output.__updated = true end
return output local function derive()
elseif type(input) == "table" then return recompute(input())
local output: Map<K, VO> = table.create(#input :: any) :: {} end
for k, vi in input :: Map<K, VI> do local output, get_output = create(output_cache)
local vo = transform(k, vi)
output[k] = vo
end
return output local nodes, value = capture(input)
else error(string.format("Invalid type arg #1, expected number or table or state (got %s)", tostring(input)), 2) end
for _, node in next, nodes do
link(node, output, derive)
end
output.cache = recompute(value)
return get_output
end end
return (map :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) & return map
( <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,132 +55,126 @@ 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"
local wref
do do
local function factory(p) -- factory function to prevent closure caching local wref
return function()
return get(p)
end
end
local node = create(1)
do do
local effect1 = factory(node) local function factory(p) -- factory function to prevent closure caching
local effect2 = factory(node) return function()
return get(p)
wref = weak { effect1, effect2, node :: any } end
set_effect(node, effect1, {})
set_effect(node, effect2, true)
end
gc()
CHECK(not wref[1]) -- effect1 should gc since nothing is referencing table `t`
CHECK(wref[2]) -- effect2 should not gc as `true` is not garbage collectable
end
gc()
CHECK(not wref[3] and not wref[2]) -- node should now gc along with effect2
do -- same test but for multiple nodes referenced by watcher
local function factory(a, b)
return function()
return get(a), get(b)
end end
local node = create(1)
do
local effect1 = factory(node)
local effect2 = factory(node)
wref = weak { e1 = effect1, e2 = effect2, n = node}
set_effect(node, effect1, {})
set_effect(node, effect2, true)
end
gc()
CHECK(not wref.e1) -- effect1 should gc since nothing is referencing table `t`
CHECK(wref.e2) -- effect2 should not gc as `true` is not garbage collectable
end end
local node1 = create(1)
local node2 = graph.create(1)
local effect = factory(node1, node2)
wref.node1 = node1
wref.node2 = node2
wref.effect = effect
local t1 = {}
set_effect(node1, effect, t1)
set_effect(node2, effect, t1)
t1 = nil :: any
effect = nil :: any
gc() gc()
CHECK(not wref.n and not wref.e2) -- node should now gc along with effect2
CHECK(not wref.effect)
end end
gc() do
local wref
CHECK(not wref.node1) do -- same test but for multiple nodes referenced by watcher
CHECK(not wref.node2) local function factory(a, b)
return (function(c, d)
return function()
return get(c), get(d)
end
end)(a, b)
end
local node1 = create(1)
local node2 = create(1)
do
local effect = factory(node1, node2)
wref = weak { n1 = node1, n2 = node2, e = effect }
local t1 = {}
set_effect(node1, effect, t1)
set_effect(node2, effect, t1)
end
gc()
CHECK(not wref.e)
end
gc()
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 a = source(1) local wref, b
local _b = derive(function() do
return a() local a = source(1)
end)
local wref = weak { a } b = derive(function()
a = nil :: any return a()
end)
wref = weak { a }
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 b = derive(function() local wref
return a()
end)
local wref = weak { b } do
b = nil :: any local b = derive(function()
return a()
end)
wref = weak { b }
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 effect = factory(state)
local unwatch = watch(effect) local wref, unwatch
local wref = weak { effect } do
effect = nil :: any local effect = factory(state)
unwatch = watch(effect)
wref = weak { effect }
end
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,30 +707,27 @@ 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" }
end }
gc() gc()
CHECK(wref.child) CHECK(wref[1])
set(nil) parent(nil)
gc() gc()
CHECK(wref.child) CHECK(wref[1])
wref.child:Destroy() wref[1]:Destroy()
gc() gc()
CHECK(not wref.child) CHECK(not wref[1])
end 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