diff --git a/src/cleanup.luau b/src/cleanup.luau index 921a939..3aec998 100644 --- a/src/cleanup.luau +++ b/src/cleanup.luau @@ -13,3 +13,4 @@ local function cleanup(fn: () -> ()) end return cleanup + diff --git a/src/graph.luau b/src/graph.luau index bb27052..396dca2 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -3,34 +3,22 @@ if not game then script = require "test/relative-string" end local throw = require(script.Parent.throw) local flags = require(script.Parent.flags) -export type Scope = { - parent: Scope | false, - cleanups: { () -> () } | false, - [number]: Scope -- children -} - export type StartNode = { cache: T, + n: number, [number]: Node } export type Node = StartNode & { - scope: Scope, effect: (T) -> (), + cleanups: { () -> () } | false, } --- flag used to detect when node reference capturing is active -local reff = false --- array of all nodes referenced since above flag was set -local refs = {} :: { StartNode } - -local scopes = { n = 0 } :: { [number]: Scope, n: number } +local scopes = { n = 0 } :: { [number]: Node, n: number } local WEAK_VALUES = { __mode = "v" } local EVALUATION_ERR = "error while evaluating source:\n\n" -setmetatable(refs :: any, WEAK_VALUES) - -- runs a given callback in a context that Luau does not allow yielding in local check_for_yield: (fn: (T...) -> unknown, T...) -> () do local t = { __mode = "kv" } @@ -57,14 +45,32 @@ local check_for_yield: (fn: (T...) -> unknown, T...) -> () do end end -local function get_scope(): Scope +local function get_scope(): Node return scopes[scopes.n] end -local function open_scope(scope: Scope) +local function add_child(parent: Node, child: Node) + local n = parent.n + 1 + parent.n = n + parent[n] = child +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] = scope + scopes[n] = node + + -- local parent = scopes[n-1] + -- assert(parent) + -- add_child(parent, node) end local function close_scope() @@ -73,124 +79,74 @@ local function close_scope() scopes[n] = nil end -local function add_cleanup(scope: Scope, cleanup: () -> ()) - if scope.cleanups then - table.insert(scope.cleanups, cleanup) +local function add_cleanup(node: Node, cleanup: () -> ()) + if node.cleanups then + table.insert(node.cleanups, cleanup) else - scope.cleanups = { cleanup } + node.cleanups = { cleanup } end end -local function run_cleanups(scope: Scope) - if scope.cleanups then - for _, fn in next, scope.cleanups do +local function run_cleanups(node: Node) + if node.cleanups then + for _, fn in next, node.cleanups do fn() end - table.clear(scope.cleanups) + table.clear(node.cleanups) end end ---[[ - -Each node side-effect is registered with a corresponding weak key. -This makes the lifetime of the side-effect tied to the key's. -The main usecase of this is to tie a side-effect to an instance, while allowing -the instance to be garbage collected even when the node still exists. - -The weak key is passed as an argument to its side-effect callback. - -]] - local function run_effect(node: Node) node.effect(node.cache) end -local function add_child(parent: StartNode, child: Node) - table.insert(parent, child) -end - -local function add_children(parent: Node, children: { Node }) - for _, child in next, children do - table.insert(parent, child) - end -end - -local function destroy(scope: Scope) - run_cleanups(scope) - for _, child in ipairs(scope) do +local function destroy(node: Node) + run_cleanups(node) + for _, child in ipairs(node) do destroy(child) end end -- runs node effects, recalculates descendants and runs descendant effects local function update(node: StartNode) - for _, child in ipairs(node) do - local scope = child.scope - assert(scope) - open_scope(scope :: Scope) - run_cleanups(scope :: Scope) + local children = { unpack(node) } + for i = 1, node.n do + node[i] = nil + end + node.n = 0 + + for _, child in children do + open_scope(child) + run_cleanups(child) run_effect(child) - update(child) close_scope() + update(child) end end --- detect what nodes were referenced in the given callback and returns them in an array -local function capture(fn: (U?) -> T, arg: U?): ({ StartNode }, T) - if reff then throw("recursive capture detected") end - - table.clear(refs) - reff = true - - local ok: boolean, result: T|string - - if arg == nil then - ok, result = pcall(fn) - else - ok, result = pcall(fn, arg) - end - - reff = false - - if not ok then throw(EVALUATION_ERR .. result :: string) end - - return refs, result :: T -end - -local function capture_parents(child: Node, fn: (U?) -> T, arg: U?): T - local refs, result = capture(fn, arg) - - for _, parent in next, refs do - add_child(parent, child) - end - - return result -end - -local function track(node: StartNode) - if reff then table.insert(refs, node :: Node) end -end - -local function create_scope(): Scope - return { - parent = get_scope() or false, - cleanups = false - } +local function track(node: Node) + add_child(node, get_scope()) end local function create_node(value: T): Node return { - scope = create_scope(), cache = value, effect = function() end, + cleanups = false :: false, + n = 0 } end +local function get_children(node: Node): { Node } + return { unpack(node) } +end + local function create_start_node(value: T): StartNode - return { cache = value } + return { cache = value, n = 0 } end return table.freeze { + open_root_scope = open_root_scope, open_scope = open_scope, close_scope = close_scope, get_scope = get_scope, @@ -200,11 +156,7 @@ return table.freeze { track = track, update = update, add_child = add_child, - add_children = add_children, - capture = capture, - capture_parents = capture_parents, create_node = create_node, create_start_node = create_start_node, - create_scope = create_scope, - refs = refs + get_children = get_children } diff --git a/test/tests.luau b/test/tests.luau index 85e4954..52d8c7d 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,5 +1,7 @@ local testkit = require("test/testkit") -local TEST, CASE, CHECK, FINISH = testkit.test() +local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() + +SKIP"graph" local mock = require "test/mock" local Instance, Signal = mock.Instance, mock.Signal @@ -29,40 +31,60 @@ TEST("graph", function() local graph = require "src/graph" local create_node = graph.create_node local track = graph.track - local capture = graph.capture local update = graph.update local add_child = graph.add_child + local open_root_scope = graph.open_root_scope + local open_scope = graph.open_scope + local close_scope = graph.close_scope + local get_children = graph.get_children do CASE "node creation" local node = create_node(1) CHECK(node.cache == 1) end - do CASE "capture nodes" - local node1 = create_node(nil) - local node2 = create_node(nil) - local captured = capture(function() - track(node1) - track(node2) - return nil - end) - CHECK(captured[1] == node1) - CHECK(captured[2] == node2) + do CASE "link nodes" + local a = create_node(nil) + local b = create_node(nil) + local c = create_node(nil) + + open_scope(c) + + track(a) + track(b) + + close_scope() + + CHECK(get_children(a)[1] == c) + CHECK(get_children(b)[1] == c) end - do CASE "linking nodes" - local parent = create_node(1) - local child = create_node(0) + do CASE "rerun linked nodes" + local a = create_node(nil) + local b = create_node(nil) + local c = create_node(nil) - add_child(parent, child) + local count = 0 - local ran = false - child.effect = function() - ran = true + local function effect() + track(a) + track(b) + count += 1 end - update(parent) - CHECK(ran) + c.effect = effect + + open_scope(c) + + effect() + + close_scope() + + CHECK(count == 1) + update(a) + CHECK(count == 2) + update(b) + CHECK(count == 3) end -- todo: further tests @@ -72,6 +94,10 @@ TEST("graph", function() gc() CHECK(not wref[1]) end + + do CASE "test" + local x = 1 + end end) TEST("source()", wrap_root(function()