This commit is contained in:
Aaron Smith 2023-08-10 13:18:16 +01:00
parent 2050c2585f
commit cc10c80a90
10 changed files with 174 additions and 220 deletions

View file

@ -15,10 +15,10 @@ vide.strict = true
What strict mode will do: What strict mode will do:
1. Run derived callbacks twice when re-evaluating. 1. Run derived sources twice a source updates.
2. Run watcher callbacks twice when a state changes. 2. Run watchers twice when a source updates.
3. Throw an error if yields occur where they are not allowed. 3. Throw an error if yields occur where they are not allowed.
4. Checks for `map()` returning primitive values. 4. Checks for `indexes()` and `values()` returning primitive values.
5. Better error reporting and stack traces. 5. Better error reporting and stack traces.
It is recommend to develop UI with strict mode and to disable it when pushing to It is recommend to develop UI with strict mode and to disable it when pushing to

View file

@ -43,7 +43,7 @@ function setup(instance: Instance, setter: (Instance) -> ())
local trace = traceback() local trace = traceback()
setter = function(instance) setter = function(instance)
local ok, err: string? = pcall(fn, instance) local ok, err: string? = pcall(fn, instance)
if not ok then warn(`error occured updating state binding:\n{err}\nset from:{trace}`) end if not ok then warn(`error occured updating property:\n{err}\nset from:{trace}`) end
end end
end end
@ -68,8 +68,6 @@ function setup(instance: Instance, setter: (Instance) -> ())
instance:GetPropertyChangedSignal("Parent"):Connect(ref) instance:GetPropertyChangedSignal("Parent"):Connect(ref)
end end
-- todo: move `fn` as arg?
local function bind_property(instance: Instance, property: string, fn: () -> unknown) local function bind_property(instance: Instance, property: string, fn: () -> unknown)
setup(instance, function(instance_weak: any) setup(instance, function(instance_weak: any)
instance_weak[property] = fn() instance_weak[property] = fn()
@ -80,13 +78,13 @@ local function bind_parent(instance: Instance, fn: () -> Instance?)
instance.Destroying:Connect(function() instance.Destroying:Connect(function()
instance= nil :: any -- allow gc when destroyed instance= nil :: any -- allow gc when destroyed
end) end)
setup(instance, function(instance) setup(instance, function(instance)
local _ = instance -- state will strongly reference instance when parent is bound local _ = instance -- state will strongly reference instance when parent is bound
instance.Parent = fn() instance.Parent = fn()
end) end)
end end
-- todo: could optimize, see: maps.luau values()
local function bind_children(parent: Instance, fn: () -> { Instance }) local function bind_children(parent: Instance, fn: () -> { Instance })
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update

View file

@ -12,7 +12,7 @@ local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply) local apply = require(script.Parent.apply)
local memoize = require(script.Parent.memoize) local memoize = require(script.Parent.memoize)
local function createInstance(className: string) local function create_instance(className: string)
local success, instance: Instance = pcall(Instance.new, className :: any) local success, instance: Instance = pcall(Instance.new, className :: any)
if success == false then throw(`invalid class name, could not create instance of class { className }`) end if success == false then throw(`invalid class name, could not create instance of class { className }`) end
@ -26,9 +26,9 @@ local function createInstance(className: string)
return function(properties: { [any]: unknown }): Instance return function(properties: { [any]: unknown }): Instance
return apply(instance:Clone(), properties) return apply(instance:Clone(), properties)
end end
end; createInstance = memoize(createInstance) end; create_instance = memoize(create_instance)
local function cloneInstance(instance: Instance) local function clone_instance(instance: Instance)
return function(properties: { [any]: unknown }): Instance return function(properties: { [any]: unknown }): Instance
local clone = instance:Clone() local clone = instance:Clone()
if not clone then error("Attempt to clone a non-archivable instance", 3) end if not clone then error("Attempt to clone a non-archivable instance", 3) end
@ -38,9 +38,9 @@ end
local function create(classNameOrInstance: string|Instance) local function create(classNameOrInstance: string|Instance)
if type(classNameOrInstance) == "string" then if type(classNameOrInstance) == "string" then
return createInstance(classNameOrInstance) return create_instance(classNameOrInstance)
elseif typeof(classNameOrInstance) == "Instance" then elseif typeof(classNameOrInstance) == "Instance" then
return cloneInstance(classNameOrInstance) return clone_instance(classNameOrInstance)
else else
error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2) error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2)
end end

View file

@ -1,5 +1,6 @@
if not game then script = require "test/wrap-require" end if not game then script = require "test/wrap-require" end
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
export type Node<T> = { export type Node<T> = {
@ -13,28 +14,30 @@ local reff = false
local refs = {} :: { Node<unknown> } 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 source:\n\n"
setmetatable(refs :: any, WEAK_VALUES_RESIZABLE) setmetatable(refs :: any, WEAK_VALUES_RESIZABLE)
local check_for_yield do local check_for_yield: <T...>(fn: (T...) -> unknown, T...) -> () do
local t = { __mode = "kv" } local t = { __mode = "kv" }
setmetatable(t, t) setmetatable(t, t)
check_for_yield = function<T..., U...>(fn: (T...) -> (), ...: any) check_for_yield = function(fn, ...: any)
local args = { ... } local args = { ... }
t.__unm = function()
t.__unm = function(_)
fn(unpack(args)) fn(unpack(args))
end end
local ok, err = pcall(function() local ok, err = pcall(function()
return -t :: any local _ = -t
end) end)
if not ok then if not ok then
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" 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 node in watcher", 3) throw(EVALUATION_ERR .. "cannot yield when deriving node in watcher")
else else
error(EVALUATION_ERR..err, 3) throw(EVALUATION_ERR .. err)
end end
end end
end end
@ -45,9 +48,15 @@ local function set_effect<T>(node: Node<unknown>, fn: (T) -> (), key: T)
end end
local function run_effects(node: Node<unknown>) local function run_effects(node: Node<unknown>)
for effect, key in next, node.effects do if flags.strict then
if flags.strict then effect(key) end for effect, key in next, node.effects do
effect(key) effect(key)
effect(key)
end
else
for effect, key in next, node.effects do
effect(key)
end
end end
end end
@ -71,8 +80,10 @@ end
local function update(node: Node<unknown>) local function update(node: Node<unknown>)
run_effects(node) run_effects(node)
if node.children then if node.children then
local strict = flags.strict
for _, child in node.children do for _, child in node.children do
if flags.strict then check_for_yield(child.derive) end if strict then check_for_yield(child.derive) end
child.cache = child.derive() child.cache = child.derive()
update(child) update(child)
end end
@ -108,7 +119,7 @@ local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T)
reff = false reff = false
if not ok then error("error while detecting watcher: " .. result :: string, 0) end if not ok then throw(EVALUATION_ERR .. result :: string) end
return refs, result :: T return refs, result :: T
end end

View file

@ -14,6 +14,7 @@ local indexes, values = require(script.maps)()
local spring, update_springs = require(script.spring)() local spring, update_springs = require(script.spring)()
local action = require(script.action)() local action = require(script.action)()
local throw = require(script.throw)
local flags = require(script.flags) local flags = require(script.flags)
local vide = { local vide = {
@ -49,15 +50,20 @@ local vide = {
} }
setmetatable(vide :: any, { setmetatable(vide :: any, {
__index = function(_, index: unknown) __index = function(_, index: unknown): ()
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2) if index == "strict" then
return flags.strict
else
throw(`{tostring(index)} is not a valid member of vide`)
end
end, end,
__newindex = function(_, index: unknown, value: unknown) __newindex = function(_, index: unknown, value: unknown)
if index == "strict" then if index == "strict" then
flags.strict = if type(value) == "boolean" then value else error("strict must be a boolean", 2) if value ~= true then throw "strict mode can only be set to true" end
flags.strict = true
else else
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2) throw(`{tostring(index)} is not a valid member of vide`)
end end
end end
}) })

View file

@ -1,5 +1,7 @@
if not game then script = require "test/wrap-require" end if not game then script = require "test/wrap-require" end
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create = graph.create local create = graph.create
@ -9,6 +11,15 @@ local link = graph.link
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local function check_primitives(t: {})
if not flags.strict then return end
for _, v in next, t do
if type(v) == "table" or type(v) == "userdata" then continue end
throw("table source map cannot return primitives")
end
end
-- todo: optimize output array -- todo: optimize output array
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO } local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local input_cache = {} :: Map<K, VI> local input_cache = {} :: Map<K, VI>
@ -54,6 +65,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
for _, v in next, output_cache do for _, v in next, output_cache do
table.insert(output_array, v) table.insert(output_array, v)
end end
check_primitives(output_array)
return output_array return output_array
end end
@ -87,6 +99,16 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local function recompute(data: Map<K, VI>) local function recompute(data: Map<K, VI>)
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
if flags.strict then
local cache = {}
for _, v in next, data do
if cache[v] ~= nil then
throw "duplicate table value detected"
end
cache[v] = true
end
end
-- process data -- process data
for i, v in next, data do for i, v in next, data do
new_input_cache[v] = i new_input_cache[v] = i
@ -136,6 +158,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
for _, node in next, nodes do for _, node in next, nodes do
link(node, output, derive) link(node, output, derive)
end end
check_primitives(output_array)
output.cache = recompute(value) output.cache = recompute(value)

View file

@ -157,7 +157,7 @@ local function update_springs(dt: number)
typeof(initial_position), typeof(initial_position),
target_type target_type
)) ))
throw(`Cannot tween type { typeof(initial_position) } and { target_type }`) throw(`cannot tween type { typeof(initial_position) } and { target_type }`)
continue continue
end end
@ -165,14 +165,13 @@ local function update_springs(dt: number)
if lerp == nil then if lerp == nil then
springs[data] = nil springs[data] = nil
throw(`Cannot animate type { target_type }`) throw(`cannot animate type { target_type }`)
continue continue
end end
local new_time = data.duration + dt local new_time = data.duration + dt
local new_alpha = solve(data.period, data.damping_ratio, data.initial_velocity, new_time) local new_alpha = solve(data.period, data.damping_ratio, data.initial_velocity, new_time)
local new_velocity = -(new_alpha - data.alpha)/dt local new_velocity = -(new_alpha - data.alpha)/dt
local acceleration = (new_velocity - data.velocity)/dt local acceleration = (new_velocity - data.velocity)/dt
data.velocity = new_velocity data.velocity = new_velocity
@ -181,7 +180,8 @@ local function update_springs(dt: number)
local value = lerp(initial_position, target_position, new_alpha) local value = lerp(initial_position, target_position, new_alpha)
if math.abs(acceleration) < 0.01 then if math.abs(acceleration) < (0.01 / data.period) then
-- close enough to target, remove and set value to target
table.insert(remove_queue, data) table.insert(remove_queue, data)
set(output, target_position) set(output, target_position)
else else

View file

@ -1,14 +1,10 @@
------------------------------------------------------------------------------------------
-- benchmark.lua
------------------------------------------------------------------------------------------
local BENCH, START = require("test/testkit").benchmark() local BENCH, START = require("test/testkit").benchmark()
local vide = require "src/init" local vide = require "src/init"
local N = 2^18 -- 262144 local N = 2^18 -- 262144
BENCH("Create state", function() BENCH("create state", function()
local cache = table.create(N) local cache = table.create(N)
local source = vide.source local source = vide.source
@ -17,7 +13,7 @@ BENCH("Create state", function()
end end
end) end)
BENCH("Get value", function() BENCH("get value", function()
local state = vide.source(1) local state = vide.source(1)
for i = 1, START(N) do for i = 1, START(N) do
@ -25,7 +21,7 @@ BENCH("Get value", function()
end end
end) end)
BENCH("Set value", function() BENCH("set value", function()
local state = vide.source(1) local state = vide.source(1)
for i = 1, START(N) do for i = 1, START(N) do
@ -33,7 +29,7 @@ BENCH("Set value", function()
end end
end) end)
BENCH("Derive 1 state", function() BENCH("derive 1 state", function()
local cache = table.create(N) local cache = table.create(N)
local state = vide.source(1) local state = vide.source(1)
local derive = vide.derive local derive = vide.derive
@ -45,7 +41,7 @@ BENCH("Derive 1 state", function()
end end
end) end)
BENCH("Derive 4 states", function() BENCH("derive 4 states", function()
local cache = table.create(N) local cache = table.create(N)
local state = vide.source(1) local state = vide.source(1)
local state2 = vide.source(2) local state2 = vide.source(2)
@ -60,7 +56,7 @@ BENCH("Derive 4 states", function()
end end
end) end)
BENCH("Set derived value", function() BENCH("set derived value", function()
local state = vide.source(1) local state = vide.source(1)
local _derived = vide.derive(state) local _derived = vide.derive(state)
@ -69,7 +65,7 @@ BENCH("Set derived value", function()
end end
end) end)
BENCH("Apply 0 properties", function() BENCH("apply 0 properties", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = vide.create("Frame") {}
@ -78,7 +74,7 @@ BENCH("Apply 0 properties", function()
end end
end) end)
BENCH("Apply 8 properties", function() BENCH("apply 8 properties", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = vide.create("Frame") {}
@ -96,7 +92,7 @@ BENCH("Apply 8 properties", function()
end end
end) end)
BENCH("Bind state", function() BENCH("bind source", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = vide.create("Frame") {}
local state = vide.source(1) local state = vide.source(1)
@ -108,7 +104,7 @@ BENCH("Bind state", function()
end end
end) end)
BENCH("Update binding", function() BENCH("update binding", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = vide.create("Frame") {}
local state = vide.source(1) local state = vide.source(1)

View file

@ -1,13 +1,11 @@
local testkit = require("test/testkit") local testkit = require("test/testkit")
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local TEST, CASE, CHECK, FINISH = testkit.test()
local mock = require "test/mock" local mock = require "test/mock"
local Instance, Signal = mock.Instance, mock.Signal local Instance, Signal = mock.Instance, mock.Signal
local vide = require "src/init" local vide = require "src/init"
-- force run garbage collector cycles
local function gc(n: number?) local function gc(n: number?)
for i = 1, n or 3 do for i = 1, n or 3 do
(collectgarbage :: any)("collect") (collectgarbage :: any)("collect")
@ -29,12 +27,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 "Node creation" do CASE "node creation"
local node = create(1) local node = create(1)
CHECK(get(node) == 1) CHECK(get(node) == 1)
end end
do CASE "Node value" 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)
@ -42,7 +40,7 @@ TEST("graph", function()
CHECK(get(node) == 2) CHECK(get(node) == 2)
end end
do CASE "Capture nodes" 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()
@ -52,7 +50,7 @@ TEST("graph", function()
CHECK(nodes[2] == node2) CHECK(nodes[2] == node2)
end end
do CASE "Linking nodes" do CASE "linking nodes"
local parent = create(1) local parent = create(1)
local child = create(0) local child = create(0)
@ -64,7 +62,7 @@ TEST("graph", function()
CHECK(get(child) == 2) -- child should automatically update CHECK(get(child) == 2) -- child should automatically update
end end
do CASE "Capture and link nodes" do CASE "capture and link nodes"
local parent = create(1) local parent = create(1)
local child = create() local child = create()
@ -76,38 +74,13 @@ TEST("graph", function()
CHECK(get(child) == "2") CHECK(get(child) == "2")
end end
--[[ do CASE "nodes garbage collection"
do CASE "Scoped captures"
local a = create(0)
local b = create(nil :: any)
local count = 0
b.cache = capture_and_link(b, function()
count += 1
local data = get(a)
local c = create(data)
get(c)
return c
end)
local c = get(b)
CHECK(count == 1)
set(c, 1)
CHECK(count == 1)
set(a, 1)
CHECK(count == 2)
end
]]
do CASE "Nodes garbage collection"
local wref = weak { create(1) } local wref = weak { create(1) }
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 do
local wref local wref
@ -179,18 +152,18 @@ TEST("source()", function()
local source = vide.source local source = vide.source
local watch = vide.watch local watch = vide.watch
do CASE "Create source" do CASE "create source"
local state = source(1) local state = source(1)
CHECK(state() == 1) CHECK(state() == 1)
end end
do CASE "Set and get source value" do CASE "set and get source value"
local state = source(1) local state = source(1)
state(2) state(2)
CHECK(state() == 2) CHECK(state() == 2)
end end
do CASE "Does not update if same value" do CASE "does not update if same value"
local state = source(1) local state = source(1)
local updates = -1 local updates = -1
@ -206,7 +179,7 @@ TEST("source()", function()
CHECK(updates == 1) CHECK(updates == 1)
end end
do CASE "Does update if same value is table" do CASE "does update if same value is table"
local state = source {} local state = source {}
local updates = -1 local updates = -1
@ -225,7 +198,7 @@ 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 source change" do CASE "derive new value on source change"
local inputA = source(1) local inputA = source(1)
local inputB = source(2) local inputB = source(2)
@ -238,7 +211,7 @@ TEST("derive()", function()
CHECK(output() == "4") CHECK(output() == "4")
end end
do CASE "Derive transformed source" do CASE "derive wrapped source"
local input = source(1) local input = source(1)
local transform = function() local transform = function()
@ -254,25 +227,7 @@ TEST("derive()", function()
CHECK(output() == 2) CHECK(output() == 2)
end end
--[[ do CASE "garbage collection"
do CASE "Cleanup"
local count, set = source(1)
local derived = derive(function(from)
return { Value = from(count), Destroyed = false }
end, function(v)
v.Destroyed = true
end)
local first = derived()
CHECK(first.Destroyed == false)
set(2)
local _ = derived() -- trigger recalc
CHECK(first.Destroyed == true)
end
]]
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 local wref, b
@ -309,7 +264,7 @@ TEST("derive()", function()
end end
end end
do CASE "Garbage collection 2" do CASE "garbage collection 2"
-- creats a chain `a -> b -> c` where `a` is the source -- creats a chain `a -> b -> c` where `a` is the source
local function setup() local function setup()
local a = source(0) local a = source(0)
@ -362,7 +317,7 @@ TEST("watch()", function()
local watch = vide.watch local watch = vide.watch
local cleanup = vide.cleanup local cleanup = vide.cleanup
do CASE "Capture states" do CASE "capture sourcess"
local a = source(1) local a = source(1)
local b = source(1) local b = source(1)
@ -380,7 +335,7 @@ TEST("watch()", function()
CHECK(runcount == 2) CHECK(runcount == 2)
end end
do CASE "Stop watch" do CASE "stop watch"
local a = source(1) local a = source(1)
local runcount = -1 local runcount = -1
@ -394,7 +349,7 @@ TEST("watch()", function()
CHECK(runcount == 0) CHECK(runcount == 0)
end end
do CASE "Side-effect cleanup" do CASE "side-effect cleanup"
local state = source(1) local state = source(1)
local effect_runcount = 0 local effect_runcount = 0
@ -421,7 +376,7 @@ TEST("watch()", function()
CHECK(cleanup_runcount == 2) CHECK(cleanup_runcount == 2)
end end
do CASE "Garbage collection" do CASE "garbage collection"
local function factory(p) local function factory(p)
return function() return function()
p() p()
@ -486,7 +441,7 @@ TEST("cleanup()", function()
local watch = vide.watch local watch = vide.watch
local cleanup = vide.cleanup local cleanup = vide.cleanup
do CASE "Cleanup runs for watcher" do CASE "cleanup runs for watcher"
local state = source(1) local state = source(1)
local watched = 0 local watched = 0
@ -520,7 +475,7 @@ TEST("cleanup()", function()
CHECK(cleaned == 2) CHECK(cleaned == 2)
end end
do CASE "Scoped" do CASE "scoped"
local function setup() local function setup()
local state = source(1) local state = source(1)
local obj = { cleaned = 0 } local obj = { cleaned = 0 }
@ -562,7 +517,7 @@ TEST("cleanup()", function()
CHECK(objB.cleaned == 2) CHECK(objB.cleaned == 2)
end end
do CASE "Multiple cleanup" do CASE "multiple cleanup"
local state = source(1) local state = source(1)
local queue = {} local queue = {}
@ -595,14 +550,14 @@ TEST("create()", function()
local create = vide.create local create = vide.create
local source = vide.source local source = vide.source
do CASE "Apply default properties" do CASE "apply default properties"
local defaults = require("src/defaults") local defaults = require("src/defaults")
local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any } local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any }
CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel) CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel)
CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3) CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3)
end end
do CASE "Set properties" do CASE "set properties"
local text = create "TextLabel" { local text = create "TextLabel" {
Name = "Label", Name = "Label",
Text = "test" Text = "test"
@ -611,7 +566,7 @@ TEST("create()", function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end end
do CASE "Set nested properties" do CASE "set nested properties"
local text = create "TextLabel" { local text = create "TextLabel" {
{ Name = "Label" }, { Name = "Label" },
Group = { Text = "test" } Group = { Text = "test" }
@ -620,12 +575,12 @@ TEST("create()", function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end 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 "Set children" do CASE "set children"
local frame = create "Frame" { local frame = create "Frame" {
create "TextLabel" { Name = "A" }, create "TextLabel" { Name = "A" },
create "TextLabel" { Name = "B" }, create "TextLabel" { Name = "B" },
@ -652,7 +607,7 @@ TEST("create()", function()
CHECK(frame:FindFirstChild "G") CHECK(frame:FindFirstChild "G")
end end
do CASE "Binding properties to state" do CASE "binding properties to source"
local name = source("Hi") local name = source("Hi")
local text = source("Bye") local text = source("Bye")
@ -671,7 +626,7 @@ TEST("create()", function()
CHECK(label.Text == "Bar") CHECK(label.Text == "Bar")
end end
do CASE "Binding garbage collection" do CASE "binding garbage collection"
do -- instance should gc when unparented do -- instance should gc when unparented
local state = source("Hi") local state = source("Hi")
@ -774,7 +729,7 @@ TEST("create()", function()
end end
end end
do CASE "Bind same state to multiple instance properties" do CASE "bind same state to multiple instance properties"
local state = source "1" local state = source "1"
local text = create "TextBox" { local text = create "TextBox" {
@ -790,7 +745,7 @@ TEST("create()", function()
CHECK(text.PlaceholderText == "2") CHECK(text.PlaceholderText == "2")
end end
do CASE "Bind children" do CASE "bind children"
local state = source() local state = source()
local a, b, c = local a, b, c =
@ -844,7 +799,7 @@ TEST("create()", function()
end end
]] ]]
do CASE "GC test" do CASE "garbage collection test"
local wref local wref
do do
@ -871,13 +826,11 @@ TEST("create()", function()
end end
end) end)
-- todo: more comprehensive tests for maps
TEST("indexes()", function() TEST("indexes()", function()
local source = vide.source local source = vide.source
local indexes = vide.indexes local indexes = vide.indexes
do CASE "Use state" do CASE "use source"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local output = indexes(input, function(v, k) local output = indexes(input, function(v, k)
@ -889,7 +842,7 @@ TEST("indexes()", function()
CHECK("" .. input()[3] == output()[3]) CHECK("" .. input()[3] == output()[3])
end end
do CASE "Cache result" do CASE "cache result"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local runcount = table.create(3, 0) local runcount = table.create(3, 0)
@ -910,7 +863,7 @@ TEST("indexes()", function()
CHECK(runcount[3] == 1) CHECK(runcount[3] == 1)
end end
do CASE "Removal reflected" do CASE "removal reflected"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local output = indexes(input, function(v, i) local output = indexes(input, function(v, i)
@ -926,95 +879,44 @@ TEST("indexes()", function()
CHECK(t[3] == nil) CHECK(t[3] == nil)
end end
--[[ do CASE "garbage collection"
do CASE "Bind children" do -- check that `output` does not allow gc of `input`
local state, set = source { "A", "B", "C" } local input = source {}
local derived = map(state, function(i, v) local _derived = indexes(input, function(i, v)
return create "TextLabel" {
Name = v,
Text = tostring(i)
}
end)
local frame = create "Frame" {
Name = "21",
[Children] = derived
}
local function find(childname: string): TextLabel
return frame:FindFirstChild(childname) :: TextLabel
end
CHECK(find "A".Text == "1")
CHECK(find "B".Text == "2")
CHECK(find "C".Text == "3")
set { "A", "C", "D" }
CHECK(find "A".Text == "1")
CHECK(not find "B")
CHECK(find "C".Text == "2")
CHECK(find "D".Text == "3")
end
do CASE "Use optional destructor"
local state, set = source { 1, 2, 3 }
local derived = map(state, function(i, v)
return { Value = v, Destroyed = false }
end, function(v)
v.Destroyed = true
end)
local first = derived()
CHECK(first[1].Destroyed == false)
set { 1, 2, 4 }
local _ = derived()
CHECK(first[1].Destroyed == false)
CHECK(first[2].Destroyed == false)
CHECK(first[3].Destroyed == true)
end
do CASE "Garbage collection"
do -- check that `derived` does not allow gc of `state`
local state = source {}
local derived = map(state, function(i, v)
return v return v
end) end)
wref.state, state = state, nil :: any local wref = weak { input }
wref.derived = derived
input = nil :: any
gc() gc()
CHECK(wref.state) CHECK(wref[1])
end end
do -- check that `state` allows gc of `derived` do -- check that `input` allows gc of `output`
local state = source {} local input = source {}
local derived = map(state, function(i, v) local output = indexes(input, function(i, v)
return i, v return i, v
end) :: State? end)
wref.state = state local wref = weak { output }
wref.derived, derived = derived, nil
output = nil :: any
gc() gc()
CHECK(not wref.derived) CHECK(not wref[1])
end end
end]] end
end) end)
TEST("values()", function() TEST("values()", function()
local source = vide.source local source = vide.source
local values = vide.values local values = vide.values
do CASE "Use state" do CASE "use source"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local output = values(input, function(v, k) local output = values(input, function(v, k)
@ -1026,7 +928,7 @@ TEST("values()", function()
CHECK("" .. input()[3] == output()[3]) CHECK("" .. input()[3] == output()[3])
end end
do CASE "Cache result" do CASE "cache result"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local runcount = table.create(3, 0) local runcount = table.create(3, 0)
@ -1047,7 +949,7 @@ TEST("values()", function()
CHECK(runcount[3] == 1) CHECK(runcount[3] == 1)
end end
do CASE "Removal reflected" do CASE "removal reflected"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local output = values(input, function(v, i) local output = values(input, function(v, i)
@ -1063,7 +965,7 @@ TEST("values()", function()
CHECK(t[3] == nil) CHECK(t[3] == nil)
end end
do CASE "Removal reflected 2" do CASE "removal reflected 2"
local input = source { 1 } local input = source { 1 }
local output = values(input, function(v, i) local output = values(input, function(v, i)
@ -1088,7 +990,7 @@ TEST("spring()", function()
local spring = vide.spring local spring = vide.spring
local watch = vide.watch local watch = vide.watch
do CASE "Update state (on next hearbeat resumption cycle)" do CASE "update source (on next step)"
local value = source(10) local value = source(10)
local springed = spring(value, 1, 1) local springed = spring(value, 1, 1)
@ -1099,7 +1001,7 @@ TEST("spring()", function()
CHECK(springed() > 10) CHECK(springed() > 10)
end end
do CASE "Garbage collection" do CASE "garbage collection"
do -- `output` should not allow gc of `input` do -- `output` should not allow gc of `input`
local input = source(10) local input = source(10)
local _output = spring(input) local _output = spring(input)
@ -1124,7 +1026,7 @@ TEST("spring()", function()
end end
do CASE "Garbage collection (binded)" do CASE "garbage collection (binded)"
local input = source(10) local input = source(10)
local output = spring(input, 1, 1) local output = spring(input, 1, 1)
@ -1139,7 +1041,7 @@ TEST("spring()", function()
CHECK(wref[1]) -- `output` should not gc CHECK(wref[1]) -- `output` should not gc
end end
do CASE "Spring finished" do CASE "spring finished"
local input = source(0) local input = source(0)
local output = spring(input) local output = spring(input)
@ -1166,11 +1068,11 @@ TEST("spring()", function()
end end
end) end)
TEST("Events", function() TEST("events", function()
local create = vide.create local create = vide.create
local function Thing(props) local function Thing(props)
local instance = Instance.new("Thing") :: any local instance = Instance.new("Thing")
instance.Signal = Signal.new() instance.Signal = Signal.new()
local clone = create(instance)(props) local clone = create(instance)(props)
@ -1178,7 +1080,7 @@ TEST("Events", function()
return clone return clone
end end
do CASE "Connect event" do CASE "connect event"
local connected = false local connected = false
local val = Thing { local val = Thing {
@ -1188,8 +1090,6 @@ TEST("Events", function()
end end
} }
-- testkit.print2(getmetatable(val))
CHECK(not connected) CHECK(not connected)
val.Value = 1; Signal.fire(val.Signal, val.Value) val.Value = 1; Signal.fire(val.Signal, val.Value)
CHECK(connected) CHECK(connected)
@ -1200,7 +1100,7 @@ TEST("actions", function()
local create = vide.create local create = vide.create
local action = vide.action local action = vide.action
do CASE "Run action" do CASE "run action"
local ran = false local ran = false
create "Frame" { create "Frame" {
@ -1212,7 +1112,7 @@ TEST("actions", function()
CHECK(ran) CHECK(ran)
end end
do CASE "Priorities" do CASE "priorities"
local queue = {} local queue = {}
create "Frame" { create "Frame" {
@ -1235,8 +1135,9 @@ TEST("strict", function()
local source = vide.source local source = vide.source
local derive = vide.derive local derive = vide.derive
local watch = vide.watch local watch = vide.watch
local indexes, values = vide.indexes, vide.values
do CASE "Error on derived callback yield" do CASE "error on derived callback yield"
local state = source(1) local state = source(1)
local ok = pcall(function() local ok = pcall(function()
@ -1249,7 +1150,7 @@ TEST("strict", function()
CHECK(not ok) CHECK(not ok)
end end
do CASE "Error on watcher callback yield" do CASE "error on watcher callback yield"
local state = source(1) local state = source(1)
local ok = pcall(function() local ok = pcall(function()
@ -1262,7 +1163,7 @@ TEST("strict", function()
CHECK(not ok) CHECK(not ok)
end end
do CASE "Run derived callback twice" do CASE "run derived callback twice"
local state = source(1) local state = source(1)
local runcount = 0 local runcount = 0
@ -1276,7 +1177,7 @@ TEST("strict", function()
CHECK(runcount == 4) CHECK(runcount == 4)
end end
do CASE "Run watcher callback twice" do CASE "run watcher callback twice"
local state = source(1) local state = source(1)
local runcount = 0 local runcount = 0
@ -1290,7 +1191,25 @@ TEST("strict", function()
CHECK(runcount == 4) CHECK(runcount == 4)
end end
-- todo: add case for strict mode bindings do CASE "indexes() error if primitive"
local state = source { 1 }
local ok = pcall(function()
indexes(state, function() return 1 end)
end)
CHECK(not ok)
end
do CASE "values() error if duplicate"
local state = source { 1, 2, 1 }
local ok = pcall(function()
values(state, function() return {} end)
end)
CHECK(not ok)
end
end) end)
local ok = FINISH() local ok = FINISH()

View file

@ -3,9 +3,10 @@
- cleanup codebase - cleanup codebase
- setup site, improve docs - setup site, improve docs
- cleanup within `values()` and `indexes()` - cleanup within `values()` and `indexes()`
- behavior when a table source value is set to the same table
- address behavior of binding property to multiples states - address behavior of binding property to multiples states
- strict mode - strict mode
- better error reporting - better error reporting and stack traces
- warn when `values()` returns primitive - warn when `values()` returns primitive
- warn when `values()` returns duplicate object - warn when `values()` returns duplicate object
- implement from solid - implement from solid