diff --git a/src/apply.luau b/src/apply.luau index 9bc5d25..8ef87ed 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -138,7 +138,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown -- finally set parent if any if parent then if type(parent) == "function" then - error("cannot set parent to state") + bind.parent(instance, parent :: () -> ()) else instance.Parent = parent :: Instance end diff --git a/src/bind.luau b/src/bind.luau index ffe1305..d309c45 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -6,10 +6,11 @@ local flags = require(script.Parent.flags) local graph = require(script.Parent.graph) type Node = graph.Node local create_node = graph.create_node +local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope +local track = graph.track local add_child = graph.add_child -local capture_parents = graph.capture_parents --[[ @@ -70,9 +71,13 @@ function bind(instance: Instance, property: string, setter: (Instance) -> ()) local binding = create_node(instance) binding.effect = setter - open_scope(binding.scope) + local owner = get_scope() + assert(owner) - capture_parents(binding, setter :: () -> any, instance) + open_scope(binding) + track(owner) + + setter(instance) close_scope() end @@ -84,12 +89,7 @@ local function bind_property(instance: Instance, property: string, fn: () -> unk end local function bind_parent(instance: Instance, fn: () -> Instance?) - instance.Destroying:Connect(function() - instance = nil :: any -- allow gc when destroyed - end) - bind(instance, "Parent", function(instance) - local _ = instance -- state will strongly reference instance when parent is bound instance.Parent = fn() end) end diff --git a/src/derive.luau b/src/derive.luau index 3bf778b..9aaa83e 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -2,7 +2,6 @@ if not game then script = require "test/relative-string" end local graph = require(script.Parent.graph) local create_node = graph.create_node -local capture_parents = graph.capture_parents local add_child = graph.add_child local update = graph.update local track = graph.track @@ -11,17 +10,19 @@ local open_scope = graph.open_scope local close_scope = graph.close_scope local function derive(fn: () -> T): () -> T - assert(get_scope()) + local owner = get_scope() + assert(owner) local node = create_node((false :: any) :: T) node.effect = function() node.cache = fn() - update(node) end - open_scope(node.scope) + add_child(owner, node) - node.cache = capture_parents(node, fn) + open_scope(node) + + node.cache = fn() close_scope() diff --git a/src/graph.luau b/src/graph.luau index 0e16de6..b058f66 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -11,13 +11,16 @@ export type StartNode = { export type Node = { cache: T, children: { [Node]: true } | false, - effect: (T) -> (), + effect: (T) -> () | false, cleanups: { () -> () } | false, } +local active = {} :: { [Node]: true } + local scopes = { n = 0 } :: { [number]: Node, n: number } local WEAK_VALUES = { __mode = "v" } +local WEAK_KEYS = { __mode = "k" } local EVALUATION_ERR = "error while evaluating source:\n\n" -- runs a given callback in a context that Luau does not allow yielding in @@ -46,34 +49,27 @@ local check_for_yield: (fn: (T...) -> unknown, T...) -> () do end end -local function get_scope(): Node +local function get_stack_scope(offset: number): Node? + return scopes[scopes.n - offset] +end + +local function get_scope(): Node? return scopes[scopes.n] end -local function add_child(parent: Node, child: Node) +local function add_child(parent: StartNode, child: Node) if parent.children then parent.children[child] = true else parent.children = { [child] = true :: true } + setmetatable(parent.children :: any, WEAK_KEYS) -- todo: end end --- local function open_root_scope(node: Node) --- assert(not scopes[1]) - --- local n = scopes.n + 1 --- scopes.n = n --- scopes[n] = node --- end - local function open_scope(node: Node) local n = scopes.n + 1 scopes.n = n scopes[n] = node - - -- local parent = scopes[n-1] - -- assert(parent) - -- add_child(parent, node) end local function close_scope() @@ -100,49 +96,71 @@ local function run_cleanups(node: Node) end local function run_effect(node: Node) - node.effect(node.cache) + if node.effect then + node.effect(node.cache) + end end local function destroy(node: Node) run_cleanups(node) - for _, child in ipairs(node) do - destroy(child) + active[node] = nil + if node.children then + for child in node.children do + destroy(child) + end end end -- runs node effects, recalculates descendants and runs descendant effects -local function update(node: StartNode) +local function rec(node: StartNode, update_queue: { Node }) if not node.children then return end - local cache = {} - for child in node.children do - table.insert(cache, child) + for child in next, node.children do + table.insert(update_queue, child) + rec(child, update_queue) end - for _, child in next, cache do - open_scope(child) - run_cleanups(child) - run_effect(child) - close_scope() - update(child) - end + table.clear(node.children) end -local function track(node: Node) - add_child(node, get_scope()) +local function update(node: StartNode) + local update_queue = {} :: { Node } + --assert(#update_queue == 0, "update already in progress") + rec(node, update_queue) + + for _, n in next, update_queue do + open_scope(n) -- todo + run_cleanups(n) + run_effect(n) + close_scope() + end + + table.clear(update_queue) +end + +local function track(node: StartNode) + local scope = get_scope() + assert(scope) + if scope.effect then -- todo + add_child(node, scope) + end end local function create_node(value: T): Node - return { + local node: Node = { cache = value, - effect = function() end, - cleanups = false, - children = false + effect = false, + cleanups = false :: false, + children = false :: false } + + active[node] = true + + return node end -local function get_children(node: Node): { Node } +local function get_children(node: Node): { Node } if not node.children then return {} end local children = {} @@ -151,7 +169,7 @@ local function get_children(node: Node): { Node } table.insert(children, child) end - return children + return children :: { Node } end local function create_start_node(value: T): StartNode @@ -159,10 +177,10 @@ local function create_start_node(value: T): StartNode end return table.freeze { - open_root_scope = open_root_scope, open_scope = open_scope, close_scope = close_scope, get_scope = get_scope, + get_stack_scope = get_stack_scope, add_cleanup = add_cleanup, destroy = destroy, run_cleanups = run_cleanups, diff --git a/src/maps.luau b/src/maps.luau index 4fba18b..9027a6d 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -9,12 +9,8 @@ type Scope = graph.Scope type StartNode = graph.StartNode local create_node = graph.create_node local create_start_node = graph.create_start_node -local create_scope = graph.create_scope local track = graph.track local update = graph.update -local capture = graph.capture -local capture_parents = graph.capture_parents -local add_child = graph.add_child local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope @@ -33,9 +29,8 @@ end -- todo: optimize output array local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO } - assert(get_scope()) - - local root = create_scope() + local owner = get_scope() + assert(owner) local input_cache = {} :: Map local output_cache = {} :: Map @@ -65,7 +60,7 @@ local function indexes(input: () -> Map, transform: (() -> VI, table.clear(remove_queue) - open_scope(root) + open_scope(owner) -- todo: needed? -- process new or changed values for i, v in next, data do @@ -73,10 +68,11 @@ local function indexes(input: () -> Map, transform: (() -> VI, if cv ~= v then if cv == nil then - local scope = create_scope() + local scope = create_node(false) scopes[i] = scope open_scope(scope) + track(owner) local node = create_start_node(v) input_nodes[i] = node @@ -112,9 +108,11 @@ local function indexes(input: () -> Map, transform: (() -> VI, update_children(input()) end - local value = capture_parents(output, input) + open_scope(output) - output.cache = update_children(value) + output.cache = update_children(input()) + + close_scope() return function() track(output) @@ -124,9 +122,8 @@ end -- todo: optimize output array local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO } - assert(get_scope()) - - local root = create_scope() + local owner = get_scope() + assert(owner) local cur_input_cache_up = {} :: Map local new_input_cache_up = {} :: Map @@ -159,7 +156,7 @@ local function values(input: () -> Map, transform: (VI, () -> local cv = cur_input_cache[v] if cv == nil then - local scope = create_scope() + local scope = create_node(false) scopes[v] = scope open_scope(scope) @@ -213,9 +210,11 @@ local function values(input: () -> Map, transform: (VI, () -> update_children(input()) end - local value = capture_parents(output, input) + open_scope(output) - output.cache = update_children(value) + output.cache = update_children(input()) + + close_scope() return function() track(output) diff --git a/src/root.luau b/src/root.luau index 4cb6e2c..e495e34 100644 --- a/src/root.luau +++ b/src/root.luau @@ -5,24 +5,24 @@ local throw = require(script.Parent.throw) local on_gc = require(script.Parent.on_gc)() local graph = require(script.Parent.graph) type Node = graph.Node -local create_scope = graph.create_scope +local create_node = graph.create_node local open_scope = graph.open_scope local close_scope = graph.close_scope local get_scope = graph.get_scope local destroy = graph.destroy local function root(fn: () -> T): (T, () -> ()) - assert(not get_scope()) - local scope = create_scope() + --assert(not get_scope()) + local node = create_node(false) - open_scope(scope) + open_scope(node) local v = fn() close_scope() return v, function() - destroy(scope) + destroy(node) end end diff --git a/src/spring.luau b/src/spring.luau index 2294c3c..32d3d04 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -27,8 +27,10 @@ type Node = graph.Node type StartNode = graph.StartNode local create_node = graph.create_node local create_start_node = graph.create_start_node +local get_scope = graph.get_scope +local open_scope = graph.open_scope +local close_scope = graph.close_scope local update = graph.update -local capture = graph.capture local add_child = graph.add_child local track = graph.track @@ -148,7 +150,18 @@ local springs: { [SpringData]: StartNode } = {} setmetatable(springs, { __mode = "v" }) local function spring(source: () -> T, period: number?, damping_ratio: number?): () -> T - local inputs, initial_value = capture(source) + local owner = get_scope() + assert(owner) + + local updater = create_node(false) + updater.effect = true :: any -- todo + + add_child(owner, updater) + open_scope(updater) + + local initial_value = source() + + close_scope() local vtype = typeof(initial_value) @@ -178,10 +191,9 @@ local function spring(source: () -> T, period: number?, damping_ratio: number source_value = initial_value, } - + local output = create_start_node(initial_value) - local updater = create_node(false) updater.effect = function() local v = source() data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v) @@ -189,10 +201,6 @@ local function spring(source: () -> T, period: number?, damping_ratio: number springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs end - for _, input in next, inputs do - add_child(input, updater) - end - return function() track(output) return output.cache @@ -258,6 +266,7 @@ local function update_spring_sources() else output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456) end + update(output) end diff --git a/src/untrack.luau b/src/untrack.luau index ec1e774..d56b606 100644 --- a/src/untrack.luau +++ b/src/untrack.luau @@ -1,20 +1,25 @@ if not game then script = require "test/relative-string" end +local create = require(script.Parent.create) local graph = require(script.Parent.graph) type Node = graph.Node -local refs = graph.refs +local create_node = graph.create_node +local get_stack_scope = graph.get_stack_scope +local open_scope = graph.open_scope +local close_scope = graph.close_scope + +local non_tracking_scope = create_node(false) local function untrack(source: () -> T): T - local initial = #refs + local scope = get_stack_scope(1) - local value = source() + open_scope(scope or non_tracking_scope) - -- remove any references made since `untrack()` was called - for i = initial, #refs do - refs[i] = nil - end + local v = source() - return value + close_scope() + + return v end return untrack diff --git a/src/watch.luau b/src/watch.luau index bc04cd1..b9a28ea 100644 --- a/src/watch.luau +++ b/src/watch.luau @@ -2,20 +2,24 @@ if not game then script = require "test/relative-string" end local graph = require(script.Parent.graph) local create_node = graph.create_node -local capture_parents = graph.capture_parents local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope +local add_child = graph.add_child +local track = graph.track local function watch(effect: () -> ()) - assert(get_scope()) + local owner = get_scope() + assert(owner) local node = create_node(false) node.effect = effect - open_scope(node.scope) + add_child(owner, node) - capture_parents(node, effect :: () -> any) + open_scope(node) + + effect() close_scope() end diff --git a/test/tests.luau b/test/tests.luau index f6e4ac6..c67caf1 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,13 +1,15 @@ local testkit = require("test/testkit") local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() -SKIP"graph" - local mock = require "test/mock" local Instance, Signal = mock.Instance, mock.Signal local Vector2, UDim2 = mock.Vector2, mock.UDim2 local vide = require "src/init" +local graph = require "src/graph" +type Node = graph.Node + +type Map = { [K] : V } local function gc(n: number?) for i = 1, n or 3 do @@ -27,26 +29,40 @@ local function wrap_root(fn: () -> ()) end end +local NIL = NIL + TEST("graph", function() - local graph = require "src/graph" local create_node = graph.create_node + local create_start_node = graph.create_start_node local track = graph.track local update = graph.update local add_child = graph.add_child - local open_root_scope = graph.open_root_scope + local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope local get_children = graph.get_children + local add_cleanup = graph.add_cleanup + local destroy = graph.destroy - do CASE "node creation" - local node = create_node(1) - CHECK(node.cache == 1) + local function node(v: T?) + local n = create_node(v or false) + n.effect = function() end + return n + end + + local function scope() + return create_node(false) + end + + local function cleanup(fn: () -> ()) + local node = assert(get_scope()) + add_cleanup(node, fn) end do CASE "link nodes" - local a = create_node(nil) - local b = create_node(nil) - local c = create_node(nil) + local a = node() + local b = node() + local c = node() open_scope(c) @@ -60,9 +76,9 @@ TEST("graph", function() end do CASE "rerun linked nodes" - local a = create_node(nil) - local b = create_node(nil) - local c = create_node(nil) + local a = node() + local b = node() + local c = node() local count = 0 @@ -87,53 +103,166 @@ TEST("graph", function() CHECK(count == 3) end - do CASE "etst" - --[[ + do CASE "case 1" + -- local function indexes(input: Node>): Map> + -- local root = get_scope() + -- local updated = create_node(false) + + -- local scopes = {} + -- local outputs = {} + + -- function updated.effect() + -- open_scope(root) + + -- for i, v in input do + -- if not scopes[i] then + -- scopes[i] = create_node(false) + -- outputs[i] = create_start_node(v) + -- end - root - Items -> Indexes() + -- open_scope(scopes[i]) + -- outputs[i].cache = v + -- update(outputs[i]) + -- close_scope() + -- end - indexes_root - v1 + sel -> bind - v2 + sel -> bind + -- for i, v in outputs do + -- if input[i] == nil then + -- destroy(scopes[i]) + -- end + -- end - ]] - local items = create_node { 1, 2 } + -- close_scope() + -- end - local count = 0 + -- open_scope(updated) + -- updated.effect(false) + -- close_scope() + + -- return outputs + -- end - local function effect() - track(a) - track(b) - count += 1 + -- construct graph + + local items = node { "a", "b" } + local selected = node "a" + + local root = scope() + + local scope1 = scope() + local scope2 = scope() + + local items_updated + + local bind1 + local bind2 + + local cleaned = {} :: { [any]: any } + + local function clean(s) + cleanup(function() + cleaned[s] = true + end) end - c.effect = effect + do open_scope(root) + clean "root" + items_updated = node() + items_updated.effect = function() end + track(items_updated) -- should not - open_scope(c) + add_child(root, items_updated) + do open_scope(items_updated) + track(items) - effect() + do open_scope(root) + add_child(root, scope1) + do open_scope(scope1) + clean "scope1" + bind1 = node() + bind1.effect = function() end - close_scope() + add_child(scope1, bind1) + do open_scope(bind1) + clean "bind1" + track(selected) + close_scope() end + close_scope() end + + add_child(root, scope2) + do open_scope(scope2) + clean "scope2" + bind2 = node() + bind2.effect = function() end + add_child(scope2, bind2) + do open_scope(bind2) + clean "bind2" + track(selected) + close_scope() end + close_scope() end + close_scope() end + close_scope() end + close_scope() end - CHECK(count == 1) - update(a) - CHECK(count == 2) - update(b) - CHECK(count == 3) + + -- verify graph + + do + local c = get_children(items_updated) + CHECK(#c == 0) + end + + do + local c = get_children(root) + CHECK(#c == 3) + CHECK(table.find(c, items_updated)) + CHECK(table.find(c, scope1 :: Node)) + CHECK(table.find(c, scope2 :: Node)) + end + + do + local c = get_children(selected) + CHECK(#c == 2) + CHECK(table.find(c, bind1)) + CHECK(table.find(c, bind2)) + end + + do + local c = get_children(scope1) + CHECK(#c == 1) + CHECK(table.find(c, bind1)) + end + + do + local c = get_children(scope2) + CHECK(#c == 1) + CHECK(table.find(c, bind2)) + end + + -- destroy + + CHECK(table.find(get_children(root), scope1 :: Node)) + + destroy(scope1) + CHECK(table.find(get_children(root), scope1 :: Node)) + CHECK(cleaned.scope1) + CHECK(cleaned.bind1) + scope1 = NIL + bind1 = NIL + bind2 = NIL + gc() + CHECK(#get_children(root) == 2) + CHECK(#get_children(selected) == 1) end -- todo: further tests do CASE "nodes garbage collection" local wref = weak { create_node(1) } + destroy(wref[1]) gc() CHECK(not wref[1]) end - - do CASE "test" - local x = 1 - end end) TEST("source()", wrap_root(function() @@ -154,31 +283,31 @@ TEST("source()", wrap_root(function() do CASE "does not update if same value" local src = source(1) - local count = -1 + local count = 0 watch(function() src() count += 1 end) - CHECK(count == 0) - src(1) - CHECK(count == 0) - src(2) CHECK(count == 1) + src(1) + CHECK(count == 1) + src(2) + CHECK(count == 2) end do CASE "does update if same value is mutable table" local src = source {} - local count = -1 + local count = 0 watch(function() src() count += 1 end) - CHECK(count == 0) - src(src()) CHECK(count == 1) + src(src()) + CHECK(count == 2) end do CASE "does not update if same value is frozen table" @@ -187,25 +316,26 @@ TEST("source()", wrap_root(function() local src = source(a) - local count = -1 + local count = 0 watch(function() src() count += 1 end) - CHECK(count == 0) + CHECK(count == 1) src(a) - CHECK(count == 0) - src(b) CHECK(count == 1) src(b) - CHECK(count == 1) + CHECK(count == 2) + src(b) + CHECK(count == 2) end end)) TEST("derive()", wrap_root(function() local source = vide.source local derive = vide.derive + local watch = vide.watch do CASE "derive new value on source change" local a = source(1) @@ -236,6 +366,30 @@ TEST("derive()", wrap_root(function() CHECK(c() == 2) end + do CASE "does not update if same value" + local num = source(0) + + local is_even = derive(function() + return num() % 2 == 0 + end) + + local count = 0 + + watch(function() + is_even() + count += 1 + end) + + num(1) -- odd + CHECK(count == 2) + num(2) -- even + CHECK(count == 3) + num(4) -- even + CHECK(count == 3) + num(5) -- odd + CHECK(count == 4) + end + do CASE "garbage collection" -- check that `b` does not allow gc of `a` local a = source(1) @@ -244,7 +398,7 @@ TEST("derive()", wrap_root(function() return a() end) - b = nil :: any + b = NIL local wref = weak { a } @@ -256,9 +410,8 @@ end)) TEST("watch()", wrap_root(function() local source = vide.source local watch = vide.watch - local cleanup = vide.cleanup - do CASE "capture sources" + do CASE "rerun on source change" local a = source(1) local b = source(1) @@ -275,70 +428,31 @@ TEST("watch()", wrap_root(function() b(2) CHECK(count == 3) end - - do CASE "side-effect cleanup" - local state = source(1) - - local effect_count = 0 - local cleanup_count = 0 - - watch(function() - state() - effect_count += 1 - cleanup(function() cleanup_count += 1 end) - end) - - CHECK(effect_count == 1) - CHECK(cleanup_count == 0) - state(2) - CHECK(effect_count == 2) - CHECK(cleanup_count == 1) - end - - do CASE "garbage collection" - local function factory(p) - return function() - p() - end - end - - do -- state prevents gc of watcher - local state = source(1) - - local wref - - do - local effect = factory(state) - watch(effect) - wref = { effect } - end - - gc() - CHECK(wref[1]) - end - - do -- state can gc with watcher - local wref - - do - local state = source(1) - local effect = factory(state) - watch(effect) - wref = weak { state } - end - - gc() - CHECK(not wref[1]) - end - end end)) TEST("cleanup()", wrap_root(function() + local root = vide.root local source = vide.source local watch = vide.watch local cleanup = vide.cleanup - do CASE "cleanup runs for watcher" + do CASE "root cleanup" + local count = 0 + + local _, destroy = root(function() + cleanup(function() + count += 1 + end) + + return nil + end) + + CHECK(count == 0) + destroy() + CHECK(count == 1) + end + + do CASE "cleanup on rerun" local state = source(1) local watched = 0 @@ -381,8 +495,10 @@ TEST("cleanup()", wrap_root(function() end)) TEST("create()", wrap_root(function() + local root = vide.root local create = vide.create local source = vide.source + local cleanup = vide.cleanup do CASE "apply default properties" local defaults = require("src/defaults") @@ -429,9 +545,7 @@ TEST("create()", wrap_root(function() { Text = "1", - { - Text = "2" - } + { Text = "2" } } } @@ -489,129 +603,38 @@ TEST("create()", wrap_root(function() CHECK(label.Text == "Bar") end - do CASE "binding garbage collection" - --[[ - do -- instance should gc when unparented - local state = source("Hi") + do CASE "binding destroy" + local count = 0 - local wref = weak { - create "TextLabel" { - Text = state, - } + local _, destroy = root(function() + local src = source(0) + + return create "TextLabel" { + Text = function() + cleanup(function() + count += 1 + end) + + return src() + end } + end) - gc() - CHECK(not wref[1]) - end - ]] - - --[[ - do -- instance should not gc when parented - local state = source("Hi") - - local parent = create "Frame" {} - - local wref = weak { - create "TextLabel" { - Parent = parent, - Text = state, - } - } - - gc() - CHECK(wref[1]) - - wref[1].Parent = nil - wref[1].Parent = parent - - gc() - CHECK(wref[1]) - - wref[1]:Destroy() - - gc() - CHECK(not wref[1]) - end - ]] - - --[[ - do -- instance does not allow gc of state - local label - local wref - - do - local state = source("Hi") - label = create "TextLabel" { - Name = state, - } - wref = weak { state :: any, label } - - end - - gc() - CHECK(wref[2]) - CHECK(wref[1]) - end - ]] - - do -- state and instance should gc once both exit scope - local wref - - do - local text = source("Hi") - - local box = create "TextLabel" { - Text = text, - } - - wref = weak { text = text, box = box} - end - - gc() - CHECK(not wref.text) - CHECK(not wref.box) - end - - --[[ - do -- binding should gc despite state still existing after instance is gc - local state = source("Hi") - - local node = require "src/graph".capture(state)[1] - - local wref - - do - local instance = create "TextLabel" { - Text = state, - } - - local binding = assert(node[1]) - - wref = weak { - instance = instance, - binding = binding - } - end - - CHECK(wref.binding) - - gc() - CHECK(not wref.instance) - CHECK(not wref.binding) - end - ]] + CHECK(count == 0) + destroy() + CHECK(count == 1) end do CASE "bind same state to multiple instance properties" - local state = source "1" + local src = source "1" local text = create "TextBox" { - Name = state, - Text = state, - PlaceholderText = state + Name = src, + Text = src, + PlaceholderText = src } - state "2" + src "2" CHECK(text.Name == "2") CHECK(text.Text == "2") @@ -619,7 +642,7 @@ TEST("create()", wrap_root(function() end do CASE "bind children" - local state = source() + local children = source() local a, b, c = create "TextLabel" { Name = "A" }, @@ -627,50 +650,53 @@ TEST("create()", wrap_root(function() create "TextLabel" { Name = "C" } local frame = create "Frame" { - state + children } - state { a, b } + children { a, b } CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "B") -- check that b is removed and c is added while a remains untouched - state { a, c } + children { a, c } CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "C") CHECK(not frame:FindFirstChild "B") - state(nil) + children(nil) CHECK(#frame:GetChildren() == 0) end - --[[ - do CASE "Parent set to nil by state does not allow gc" - local frame = create "Frame" { Name = "Parent" } - local parent = source(frame :: Frame?) + do CASE "parent bound to source" + local wref, destroy = root(function() + local frame = create "Frame" { Name = "Parent" } + local parent = source(frame :: Frame?) - local wref = weak { - create "TextLabel" { Parent = parent, Name = "Child" } - } + local wref = weak { + create "TextLabel" { Parent = parent, Name = "Child" } + } + + gc() + CHECK(wref[1]) + + parent(nil) + + return wref + end) gc() CHECK(wref[1]) - parent(nil) - - gc() - CHECK(wref[1]) - - wref[1]:Destroy() + destroy() + destroy = NIL gc() CHECK(not wref[1]) end - ]] do CASE "garbage collection test" local wref @@ -761,7 +787,7 @@ TEST("indexes()", wrap_root(function() CHECK(t[1].Text == "1") CHECK(t[2].Text == "2") - CHECK(t[3] == nil :: any) + CHECK(t[3] == NIL) CHECK(destroyed == true) end @@ -775,7 +801,7 @@ TEST("indexes()", wrap_root(function() local wref = weak { input } - input = nil :: any + input = NIL gc() CHECK(wref[1]) @@ -790,7 +816,7 @@ TEST("indexes()", wrap_root(function() local wref = weak { output } - output = nil :: any + output = NIL gc() CHECK(not wref[1]) @@ -880,7 +906,7 @@ TEST("values()", wrap_root(function() CHECK(t[1].Text == "1") CHECK(t[2].Text == "2") - CHECK(t[3] == nil :: any) + CHECK(t[3] == NIL) CHECK(destroyed == true) end @@ -931,13 +957,14 @@ TEST("spring()", wrap_root(function() do CASE "update source (on next step)" local value = source(10) - local springed = spring(value, 1, 1) + local sprung = spring(value, 1, 1) + CHECK(sprung() == 10) value(20) - CHECK(springed() == 10) + CHECK(sprung() == 10) vide.step(1/60) - CHECK(springed() ~= 10) - CHECK(springed() > 10) + CHECK(sprung() ~= 10) + CHECK(sprung() > 10) end do CASE "garbage collection" @@ -947,7 +974,7 @@ TEST("spring()", wrap_root(function() local _output = spring(input) local wref = weak { input } - input = nil :: any + input = NIL gc() CHECK(wref[1]) @@ -959,28 +986,26 @@ TEST("spring()", wrap_root(function() local output = spring(input) local wref = weak { output } - output = nil :: any + output = NIL gc() CHECK(not wref[1]) end - do -- spring data gc - local capture = require "src/graph".capture + -- do -- spring data gc + -- local input = source(10) - local input = source(10) - - local wref do - local output, data = (spring :: any)(input) - input(input() + 1) -- schedule spring calculation - local output_node = unpack(capture(output)) - wref = weak { output_node, data } - end + -- local wref do + -- local output, data = (spring :: any)(input) + -- input(input() + 1) -- schedule spring calculation + -- local output_node = unpack(capture(output)) + -- wref = weak { output_node, data } + -- end - gc() - CHECK(not wref[1]) - CHECK(not wref[2]) - end + -- gc() + -- CHECK(not wref[1]) + -- CHECK(not wref[2]) + -- end end do CASE "garbage collection (binded)" @@ -992,7 +1017,7 @@ TEST("spring()", wrap_root(function() } local wref = { output } - output = nil :: any + output = NIL gc() CHECK(wref[1]) -- `output` should not gc @@ -1026,8 +1051,11 @@ TEST("spring()", wrap_root(function() end)) TEST("untrack()", wrap_root(function() + local root = vide.root local source = vide.source + local derive = vide.derive local watch = vide.watch + local cleanup = vide.cleanup local untrack = vide.untrack do CASE "does not register dependency" @@ -1075,6 +1103,55 @@ TEST("untrack()", wrap_root(function() b(1) CHECK(count == 1) end + + do CASE "outer scope" + local outer_count = 0 + local inner_count = 0 + local cleaned_count = 0 + + local input = source(0) + + local output, destroy = root(function() + local output = derive(function() + outer_count += 1 + + return untrack(function() + return derive(function() + inner_count += 1 + + cleanup(function() + cleaned_count += 1 + end) + + return tostring(input()) + end) + end) + end) + + return output + end) + + CHECK(outer_count == 1) + CHECK(inner_count == 1) + CHECK(cleaned_count == 0) + + local output2 = output() + + CHECK(output2() == "0") + + input(1) + + -- todo + CHECK(outer_count == 2) + CHECK(inner_count == 3) + CHECK(cleaned_count == 1) + + local output3 = output() + CHECK(output2() == "1") + CHECK(output3() == "1") + + CHECK(output2 ~= output3) + end end)) TEST("events", function()