From bdd725659e505cef640deaa52db2a273caecfc26 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Wed, 13 Sep 2023 23:57:38 +0100 Subject: [PATCH] --- src/effect.luau | 1 + src/graph.luau | 26 ++++++-------- src/init.luau | 4 +-- src/maps.luau | 51 ++++++++++++++++------------ src/match.luau | 39 --------------------- src/switch.luau | 66 ++++++++++++++++++++++++++++++++++++ test/benchmark.luau | 3 +- test/tests.luau | 82 ++++++++++++++++++++++++++++++++++++--------- 8 files changed, 177 insertions(+), 95 deletions(-) delete mode 100644 src/match.luau create mode 100644 src/switch.luau diff --git a/src/effect.luau b/src/effect.luau index 303683f..8c54d23 100644 --- a/src/effect.luau +++ b/src/effect.luau @@ -14,6 +14,7 @@ local function effect(effect: (T) -> T, initial_value: T) end; assert(owner) local node = create_node(initial_value, effect) + set_owner(node, owner) evaluate_node(node) end diff --git a/src/graph.luau b/src/graph.luau index 249d2f6..b355e6f 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -11,11 +11,8 @@ export type StartNode = { export type Node = { cache: T, effect: ((T) -> T) | false, - - owner: Node | false, cleanups: { () -> () } | false, - - parents: { StartNode }, + parents: { owner: StartNode?, [number]: StartNode }, [number]: Node } @@ -57,7 +54,7 @@ local function add_child(parent: StartNode, child: Node) end local function set_owner(node: Node, owner: Node) - node.owner = owner + node.parents.owner = owner table.insert(owner, node) end @@ -100,20 +97,21 @@ local function remove_child(parent: StartNode, child: Node) end local function unparent(node: Node) - for _, parent in node.parents do - remove_child(parent, node) - end + local parents = node.parents - table.clear(node.parents) + for i, parent in ipairs(parents) do + remove_child(parent, node) + parents[i] = nil + end end local function destroy(node: Node) run_cleanups(node) unparent(node) - if node.owner then - remove_child(node.owner, node) - node.owner = false + if node.parents.owner then + remove_child(node.parents.owner, node) + node.parents.owner = nil end while node[1] do destroy(node[1]) end @@ -188,11 +186,9 @@ end local function create_node(value: T, effect: false | (T) -> T): Node local node: Node = { cache = value, - owner = false, effect = effect, cleanups = false :: false, parents = {}, - children = false :: false } return node @@ -203,7 +199,7 @@ local function get_children(node: Node): { Node } end local function create_start_node(value: T): StartNode - return { cache = value, children = false } + return { cache = value } end return table.freeze { diff --git a/src/init.luau b/src/init.luau index 8c2d88c..8958dd9 100644 --- a/src/init.luau +++ b/src/init.luau @@ -14,7 +14,7 @@ local effect = require(script.effect) local cleanup = require(script.cleanup) local untrack = require(script.untrack) local derive = require(script.derive) -local match = require(script.match) +local switch = require(script.switch) local indexes, values = require(script.maps)() local spring, update_springs = require(script.spring)() local action = require(script.action)() @@ -50,7 +50,7 @@ local vide = { source = source, effect = effect, derive = derive, - match = match, + switch = switch, indexes = indexes, values = values, diff --git a/src/maps.luau b/src/maps.luau index 9b1560a..3f2b821 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -29,9 +29,6 @@ local function check_primitives(t: {}) end end --- todo: verify destruction of subscopes when owner scope is destroyed - --- todo: optimize output array local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO } local owner = get_scope() if not owner then @@ -78,18 +75,23 @@ local function indexes(input: () -> Map, transform: (() -> VI, local scope = create_node(false, false) scopes[i] = scope :: Node + local node = create_start_node(v) + set_owner(scope, subowner) open_scope(scope) - local node = create_start_node(v) - input_nodes[i] = node - input_cache[i] = v - output_cache[i] = transform(function() + local ok, result = pcall(transform, function() track(node) return node.cache end, i) - + close_scope() + + if not ok then error(result, 0) end + + input_nodes[i] = node + input_cache[i] = v + output_cache[i] = result else input_nodes[i].cache = v update(input_nodes[i]) @@ -98,6 +100,7 @@ local function indexes(input: () -> Map, transform: (() -> VI, end end + -- todo: handle early error close_scope() local output_array = table.create(#scopes) @@ -109,16 +112,16 @@ local function indexes(input: () -> Map, transform: (() -> VI, return output_array end - local output = create_node(false :: any, function() + local node = create_node(false :: any, function() return update_children(input()) end) - evaluate_node(output) + evaluate_node(node) return function() - track(output) - return output.cache + track(node) + return node.cache end end @@ -162,17 +165,22 @@ local function values(input: () -> Map, transform: (VI, () -> local scope = create_node(false, false) scopes[v] = scope :: Node + local node = create_start_node(i) + set_owner(scope, subowner) open_scope(scope) - - local node = create_start_node(i) - input_nodes[v] = node - output_cache[v] = transform(v, function() + + local ok, result = pcall(transform, v, function() track(node) return node.cache end) - + close_scope() + + if not ok then error(result, 0) end + + input_nodes[v] = node + output_cache[v] = result else if cv ~= i then input_nodes[v].cache = i @@ -182,6 +190,7 @@ local function values(input: () -> Map, transform: (VI, () -> end end + -- todo: handle early error close_scope() -- remove old values @@ -206,15 +215,15 @@ local function values(input: () -> Map, transform: (VI, () -> return output_array end - local output = create_node(false :: any, function() + local node = create_node(false :: any, function() return update_children(input()) end) - evaluate_node(output) + evaluate_node(node) return function() - track(output) - return output.cache + track(node) + return node.cache end end diff --git a/src/match.luau b/src/match.luau deleted file mode 100644 index 0cb4a52..0000000 --- a/src/match.luau +++ /dev/null @@ -1,39 +0,0 @@ -if not game then script = require "test/relative-string" end - -local throw = require(script.Parent.throw) -local flags = require(script.Parent.flags) -local graph = require(script.Parent.graph) -type Node = graph.Node -type StartNode = graph.StartNode -local create_node = graph.create_node -local create_start_node = graph.create_start_node -local set_owner = graph.set_owner -local track = graph.track -local update = graph.update -local get_scope = graph.get_scope -local open_scope = graph.open_scope -local close_scope = graph.close_scope -local destroy = graph.destroy - -type Map = { [K]: V } - -local function match(source: () -> T, map: Map U?>): () -> U? - local owner = get_scope() - assert(owner) - - local match_updater = create_node(nil :: U?) - function match_updater.effect() - local value = source() - open_scope(owner) - local component = map[value]() - close_scope() - return component - end - - return function() - track(match_updater) - return match_updater.cache - end -end - -return match diff --git a/src/switch.luau b/src/switch.luau new file mode 100644 index 0000000..97a5f72 --- /dev/null +++ b/src/switch.luau @@ -0,0 +1,66 @@ +if not game then script = require "test/relative-string" end + +local throw = require(script.Parent.throw) +local graph = require(script.Parent.graph) +type Node = graph.Node +type StartNode = graph.StartNode +local create_node = graph.create_node +local evaluate_node = graph.evaluate_node +local set_owner = graph.set_owner +local track = graph.track +local destroy = graph.destroy +local get_scope = graph.get_scope +local open_scope = graph.open_scope +local close_scope = graph.close_scope + +type Map = { [K]: V } + +local function switch(source: () -> T): (map: Map U)?)>) -> () -> U? + return function(map) + local owner = get_scope() + if not owner then + throw("cannot switch in non-reactive scope") + end; assert(owner) + + local scope: Node? + local last_component: (() -> U)? + + local function update(): U? + local component = map[source()] + if component == last_component then return nil end + last_component = component + + if scope then + destroy(scope :: Node) + end + + if component == nil then return nil end + + local new_scope = create_node(false, false) + scope = new_scope :: Node + + set_owner(new_scope, owner) + open_scope(new_scope) + + local ok, result = pcall(component) + + close_scope() + + if not ok then error(result, 0) end + + return result + end + + local node = create_node(nil, update :: () -> any) + + set_owner(node, owner) + evaluate_node(node) + + return function() + track(node) + return node.cache + end + end +end + +return switch diff --git a/test/benchmark.luau b/test/benchmark.luau index 87fc2f5..47f766e 100644 --- a/test/benchmark.luau +++ b/test/benchmark.luau @@ -68,7 +68,6 @@ BENCH("derive 4 sources", function() end) end) --- todo: why is this so fast? BENCH("set derived value", function() local src = vide.source(1) @@ -110,7 +109,7 @@ BENCH("apply 8 properties", function() end end) -BENCH("bind source", function() +BENCH("bind property", function() local apply = require "src/apply" local instance = vide.create("Frame") {} diff --git a/test/tests.luau b/test/tests.luau index 4cf873e..b374479 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -453,22 +453,6 @@ TEST("derive()", wrap_root(function() gc() CHECK(wref[1]) end - - do CASE "raw derive" - local a = source(1) - local b = derive(a) - - local count = 0 - - effect(function() - b() - count += 1 - end) - - CHECK(count == 1) - a(2) - CHECK(count == 2) - end end)) TEST("effect()", wrap_root(function() @@ -822,6 +806,72 @@ TEST("create()", wrap_root(function() end end)) +TEST("switch()", wrap_root(function() + local create = vide.create + local source = vide.source + local switch = vide.switch + local effect = vide.effect + local derive = vide.derive + local cleanup = vide.cleanup + + do CASE "update on source change" + local input = source(true) + + local output = switch(input) { + [true] = function() return 1 end, + [false] = function() return 0 end + } + + local count = 0 + + effect(function() output(); count += 1 end) + + CHECK(count == 1) + CHECK(output() == 1) + input(false) + CHECK(output() == 0) + CHECK(count == 2) + input(false) + CHECK(count == 2) + input(nil) + CHECK(output() == nil) + end + + do CASE "scoped switch" + local input = source(true) + + local owner_count = 0 + local switch0_count = 0 + local switch1_count = 0 + + cleanup(function() owner_count += 1 end) + + local output = switch(input) { + [true] = function() + cleanup(function() switch1_count += 1 end) + return 1 + end, + + [false] = function() + cleanup(function() switch0_count += 1 end) + return 0 + end + } + + CHECK(output() == 1) + input(false) + CHECK(switch1_count == 1) + CHECK(switch0_count == 0) + input(true) + CHECK(switch1_count == 1) + CHECK(switch0_count == 1) + input(nil) + CHECK(switch1_count == 2) + CHECK(switch0_count == 1) + CHECK(owner_count == 0) + end +end)) + TEST("indexes()", wrap_root(function() local create = vide.create local source = vide.source