diff --git a/src/derive.luau b/src/derive.luau index 73bbfeb..b941185 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -1,6 +1,6 @@ local graph = require "./graph" local create_node = graph.create_node -local push_child_to_scope = graph.push_child_to_scope +local push_scope_as_child_of = graph.push_scope_as_child_of local assert_stable_scope = graph.assert_stable_scope local evaluate_node = graph.evaluate_node @@ -10,7 +10,7 @@ local function derive(source: () -> T): () -> T evaluate_node(node) return function() - push_child_to_scope(node) + push_scope_as_child_of(node) return node.cache end end diff --git a/src/graph.luau b/src/graph.luau index 360c0cb..c7e37d5 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -245,7 +245,7 @@ local function update_descendants(root: SourceNode) update_queue.n = n0 end -local function push_child_to_scope(node: SourceNode) +local function push_scope_as_child_of(node: SourceNode) local scope = get_scope() if scope and scope.effect then -- do not track nodes with no effect push_child(node, scope) @@ -302,7 +302,7 @@ return table.freeze { push_cleanup = push_cleanup, destroy = destroy, flush_cleanups = flush_cleanups, - push_child_to_scope = push_child_to_scope, + push_scope_as_child_of = push_scope_as_child_of, update_descendants = update_descendants, push_child = push_child, create_node = create_node, diff --git a/src/indexes.luau b/src/indexes.luau new file mode 100644 index 0000000..acad4c0 --- /dev/null +++ b/src/indexes.luau @@ -0,0 +1,114 @@ +local flags = require "./flags" +local graph = require "./graph" +type Node = graph.Node +type SourceNode = graph.SourceNode +local create_node = graph.create_node +local create_source_node = graph.create_source_node +local push_scope_as_child_of = graph.push_scope_as_child_of +local update_descendants = graph.update_descendants +local assert_stable_scope = graph.assert_stable_scope +local push_scope = graph.push_scope +local pop_scope = graph.pop_scope +local evaluate_node = graph.evaluate_node +local destroy = graph.destroy + +type Map = { [K]: V } + +local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO, delay: number?): () -> { VO } + local owner = assert_stable_scope() + local subowner = create_node(owner, false, false) + + local input_cache = {} :: Map + local output_cache = {} :: Map + local input_nodes = {} :: Map> + local scopes = {} :: Map> + + local function update_children(data) + local children_need_update = false + + -- remove old indexes + for i in input_cache do + if data[i] == nil then + destroy(scopes[i]) + input_cache[i] = nil + output_cache[i] = nil + input_nodes[i] = nil + scopes[i] = nil + children_need_update = true + end + end + + push_scope(subowner) + + -- process new or changed values + for i, v in data do + local cv = input_cache[i] + + if cv ~= v then + input_cache[i] = v + + if cv == nil then -- create new scope and run transform + local scope = create_node(subowner, false, false) + scopes[i] = scope :: Node + + local node = create_source_node(v) + + push_scope(scope) + + local ok, result = xpcall(transform, debug.traceback, function() + push_scope_as_child_of(node) + return node.cache + end, i) + + pop_scope() + + if not ok then + pop_scope() -- subowner scope + error(result, 0) + end + + input_nodes[i] = node + output_cache[i] = result + children_need_update = true + else -- update source + input_nodes[i].cache = v + update_descendants(input_nodes[i]) + end + end + end + + pop_scope() + + if children_need_update then + local output_array_size = #output_cache + local output_array + + -- check if the table contains a dictionary section + if output_array_size > 0 and next(output_cache, output_array_size) == nil then + output_array = table.clone(output_cache) + else + output_array = table.create(output_array_size) + for _, v in output_cache do + table.insert(output_array, v) + end + end + + return output_array + else + return nil + end + end + + local node = create_node(owner, function(pre_children) + return update_children(input()) or pre_children + end, {}) + + evaluate_node(node) + + return function() + push_scope_as_child_of(node) + return node.cache + end +end + +return indexes diff --git a/src/lib.luau b/src/lib.luau index 3d7f3be..9ad2d48 100644 --- a/src/lib.luau +++ b/src/lib.luau @@ -14,10 +14,12 @@ local batch = require "./batch" local context = require "./context" local switch = require "./switch" local show = require "./show" -local indexes, values = require "./maps"() +local indexes = require "./indexes" +local values = require "./values" local spring, update_springs = require "./spring"() local action = require "./action"() local changed = require "./changed" +local timeout, update_timeouts = require "./timeout"() local flags = require "./flags" export type Source = source.Source @@ -26,17 +28,17 @@ export type Context = context.Context export type context = Context local function step(dt: number) - if game then - debug.profilebegin("VIDE STEP") - debug.profilebegin("VIDE SPRING") - end + if game then debug.profilebegin("VIDE STEP") end + if game then debug.profilebegin("VIDE SPRING") end update_springs(dt) + if game then debug.profileend() end - if game then - debug.profileend() - debug.profileend() - end + if game then debug.profilebegin("VIDE SCHEDULER") end + update_timeouts(dt) + if game then debug.profileend() end + + if game then debug.profileend() end end local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number) diff --git a/src/maps.luau b/src/maps.luau deleted file mode 100644 index 0f95695..0000000 --- a/src/maps.luau +++ /dev/null @@ -1,216 +0,0 @@ -local flags = require "./flags" -local graph = require "./graph" -type Node = graph.Node -type SourceNode = graph.SourceNode -local create_node = graph.create_node -local create_source_node = graph.create_source_node -local push_child_to_scope = graph.push_child_to_scope -local update_descendants = graph.update_descendants -local assert_stable_scope = graph.assert_stable_scope -local push_scope = graph.push_scope -local pop_scope = graph.pop_scope -local evaluate_node = graph.evaluate_node -local destroy = graph.destroy - -type Map = { [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" or type(v) == "function" then continue end - error("table source map cannot return primitives", 0) - end -end - -local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO } - local owner = assert_stable_scope() - local subowner = create_node(owner, false, false) - - local input_cache = {} :: Map - local output_cache = {} :: Map - local input_nodes = {} :: Map> - local remove_queue = {} :: { K } - local scopes = {} :: Map> - - local function update_children(data) - -- queue removed values - for i in next, input_cache do - if data[i] == nil then - table.insert(remove_queue, i) - end - end - - -- remove queued values - for _, i in next, remove_queue do - destroy(scopes[i]) - - input_cache[i] = nil - output_cache[i] = nil - input_nodes[i] = nil - scopes[i] = nil - end - - table.clear(remove_queue) - - push_scope(subowner) - - -- process new or changed values - for i, v in next, data do - local cv = input_cache[i] - - if cv ~= v then - input_cache[i] = v - - if cv == nil then -- create new scope and run transform - local scope = create_node(subowner, false, false) - scopes[i] = scope :: Node - - local node = create_source_node(v) - - push_scope(scope) - - local ok, result = xpcall(transform, debug.traceback, function() - push_child_to_scope(node) - return node.cache - end, i) - - pop_scope() - - if not ok then - pop_scope() -- subowner scope - error(result, 0) - end - - input_nodes[i] = node - output_cache[i] = result - else -- update source - input_nodes[i].cache = v - update_descendants(input_nodes[i]) - end - end - end - - pop_scope() - - local output_array = table.create(#scopes) - for _, v in next, output_cache do - table.insert(output_array, v) - end - check_primitives(output_array) - - return output_array - end - - local node = create_node(owner, function() - return update_children(input()) - end, false :: any) - - evaluate_node(node) - - return function() - push_child_to_scope(node) - return node.cache - end -end - -local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO } - local owner = assert_stable_scope() - local subowner = create_node(owner, false, false) - - local cur_input_cache_up = {} :: Map - local new_input_cache_up = {} :: Map - local output_cache = {} :: Map - local input_nodes = {} :: Map> - local scopes = {} :: Map> - - local function update_children(data: Map) - 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 - error "duplicate table value detected" - end - cache[v] = true - end - end - - push_scope(subowner) - - -- process data - for i, v in next, data do - new_input_cache[v] = i - - local cv = cur_input_cache[v] - - if cv == nil then -- create new scope and run transform - local scope = create_node(subowner, false, false) - scopes[v] = scope :: Node - - local node = create_source_node(i) - - push_scope(scope) - - local ok, result = xpcall(transform, debug.traceback, v, function() - push_child_to_scope(node) - return node.cache - end) - - pop_scope() - - if not ok then - pop_scope() -- subowner scope - error(result, 0) - end - - input_nodes[v] = node - output_cache[v] = result - else -- update source - if cv ~= i then - input_nodes[v].cache = i - update_descendants(input_nodes[v]) - end - - cur_input_cache[v] = nil - end - end - - pop_scope() - - -- remove old values - for v in next, cur_input_cache do - destroy(scopes[v]) - - output_cache[v] = nil - input_nodes[v] = nil - scopes[v] = nil - end - - -- update buffer cache - table.clear(cur_input_cache) - cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache - - local output_array = table.create(#scopes) - for _, v in next, output_cache do - table.insert(output_array, v) - end - check_primitives(output_array) - - return output_array - end - - local node = create_node(owner, function() - return update_children(input()) - end, false :: any) - - evaluate_node(node) - - return function() - push_child_to_scope(node) - return node.cache - end -end - -return function() return indexes, values end diff --git a/src/source.luau b/src/source.luau index d7aa53d..1da515a 100644 --- a/src/source.luau +++ b/src/source.luau @@ -1,7 +1,7 @@ local graph = require "./graph" type Node = graph.Node local create_source_node = graph.create_source_node -local push_child_to_scope = graph.push_child_to_scope +local push_scope_as_child_of = graph.push_scope_as_child_of local update_descendants = graph.update_descendants export type Source = (() -> T) & ((value: T) -> T) @@ -11,7 +11,7 @@ local function source(initial_value: T): Source local function update_source(...): T if select("#", ...) == 0 then -- no args were given - push_child_to_scope(node) + push_scope_as_child_of(node) return node.cache end diff --git a/src/spring.luau b/src/spring.luau index aaf6789..988bc00 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -6,7 +6,7 @@ local create_source_node = graph.create_source_node local assert_stable_scope = graph.assert_stable_scope local evaluate_node = graph.evaluate_node local update_descendants = graph.update_descendants -local push_child_to_scope = graph.push_child_to_scope +local push_scope_as_child_of = graph.push_scope_as_child_of local UPDATE_RATE = 120 local TOLERANCE = 0.001 @@ -202,7 +202,7 @@ local function spring(source: () -> T, period: number?, damping_ratio: number return function(...) if select("#", ...) == 0 then -- no args were given - push_child_to_scope(output) + push_scope_as_child_of(output) return output.cache end diff --git a/src/switch.luau b/src/switch.luau index 547fc80..c6390dd 100644 --- a/src/switch.luau +++ b/src/switch.luau @@ -3,7 +3,7 @@ type Node = graph.Node type SourceNode = graph.SourceNode local create_node = graph.create_node local evaluate_node = graph.evaluate_node -local push_child_to_scope = graph.push_child_to_scope +local push_scope_as_child_of = graph.push_scope_as_child_of local destroy = graph.destroy local assert_stable_scope = graph.assert_stable_scope local push_scope = graph.push_scope @@ -53,7 +53,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> () evaluate_node(node) return function() - push_child_to_scope(node) + push_scope_as_child_of(node) return node.cache end end diff --git a/src/timeout.luau b/src/timeout.luau new file mode 100644 index 0000000..16319e3 --- /dev/null +++ b/src/timeout.luau @@ -0,0 +1,27 @@ +local queue = {} :: { + { t: number, fn: () -> (), cancel: boolean } +} + +local function timeout(t: number, fn: () -> ()) + local handle = { t = t, fn = fn, cancel = false } + table.insert(queue, handle) + return handle +end + +local function update_timeouts(dt: number) + for i = #queue, 1, -1 do + local handle = queue[i] + handle.t -= dt + + if handle.cancel or handle.t <= 0 then + queue[i] = queue[#queue] + queue[#queue] = nil + + if not handle.cancel then + handle.fn() + end + end + end +end + +return function() return timeout, update_timeouts end diff --git a/src/values.luau b/src/values.luau new file mode 100644 index 0000000..7edc4f4 --- /dev/null +++ b/src/values.luau @@ -0,0 +1,143 @@ +local flags = require "./flags" +local graph = require "./graph" +type Node = graph.Node +type SourceNode = graph.SourceNode +local create_node = graph.create_node +local create_source_node = graph.create_source_node +local push_scope_as_child_of = graph.push_scope_as_child_of +local update_descendants = graph.update_descendants +local assert_stable_scope = graph.assert_stable_scope +local push_scope = graph.push_scope +local pop_scope = graph.pop_scope +local evaluate_node = graph.evaluate_node +local destroy = graph.destroy + +type Map = { [K]: V } + +local function check_primitives(t: {}) + if not flags.strict then return end + + for _, v in t do + if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end + error("table source map cannot return primitives", 0) + end +end + +local function values(input: () -> Map, transform: (VI, () -> K) -> VO, delay: number?): () -> { VO } + local owner = assert_stable_scope() + local subowner = create_node(owner, false, false) + + local update_count = 0 + + local caches = {} :: Map, + index_source: SourceNode, + alive_source: SourceNode, + result: VO, + }> + + local function update_children(data: Map) + local count = update_count + update_count += 1 + + local children_need_update = false + + if flags.strict then + local cache = {} + for _, v in data do + if cache[v] ~= nil then + error "duplicate table value detected" + end + cache[v] = true + end + end + + push_scope(subowner) + + -- process data + for i, v in data do + local cache = caches[v] + + if cache == nil then -- create new scope and run transform + local scope = create_node(subowner, false, false) + local index_source = create_source_node(i) + + local new_cache = { + count = count, + index = i, + scope = scope :: Node, + index_source = index_source :: SourceNode, + alive_source = nil :: any, + result = false :: any, + } + -- must be set before transform is run so that the scope can + -- be destroyed if the transform itself updates the input + -- which lets the strict active scope destruction check work + caches[v] = new_cache + + push_scope(scope) + + local ok, result = xpcall(transform, debug.traceback, v, function() + push_scope_as_child_of(index_source) + return index_source.cache + end) + + pop_scope() + + if not ok then + pop_scope() -- subowner scope + error(result, 0) + end + + new_cache.result = result + children_need_update = true + else -- update source + cache.count = count + + if cache.index ~= i then + cache.index = i + cache.index_source.cache = i + update_descendants(cache.index_source) + end + end + end + + pop_scope() + + -- remove old values + for v, cache in caches do + if cache.count < count then + destroy(cache.scope) + caches[v] = nil + children_need_update = true + end + end + + if children_need_update then + local output_array = table.create(#data) + for _, cache in caches do + table.insert(output_array, cache.result) + end + check_primitives(output_array) + + return output_array + else + return nil + end + end + + local node = create_node(owner, function(pre_children) -- todo: add test + return update_children(input()) or pre_children + end, {}) + + evaluate_node(node) + + return function() + push_scope_as_child_of(node) + return node.cache + end +end + +return values diff --git a/test/tests.luau b/test/tests.luau index 14409bb..254e955 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -35,7 +35,7 @@ vide.strict = false TEST("graph", function() local create_node = graph.create_node - local push_child_to_scope = graph.push_child_to_scope + local push_scope_as_child_of = graph.push_scope_as_child_of local update_descendants = graph.update_descendants local push_child = graph.push_child local get_scope = graph.get_scope @@ -65,8 +65,8 @@ TEST("graph", function() push_scope(c) - push_child_to_scope(a) - push_child_to_scope(b) + push_scope_as_child_of(a) + push_scope_as_child_of(b) pop_scope() @@ -83,8 +83,8 @@ TEST("graph", function() local count = 0 local function effect(x) - push_child_to_scope(a) - push_child_to_scope(b) + push_scope_as_child_of(a) + push_scope_as_child_of(b) count += 1 return not x end @@ -115,9 +115,9 @@ TEST("graph", function() function c.effect(x) c_cnt += 1; return not x end function d.effect(x) d_cnt += 1; return not x end - push_scope(b); push_child_to_scope(a); pop_scope() - push_scope(c); push_child_to_scope(a); pop_scope() - push_scope(d); push_child_to_scope(b); push_child_to_scope(c); pop_scope() + push_scope(b); push_scope_as_child_of(a); pop_scope() + push_scope(c); push_scope_as_child_of(a); pop_scope() + push_scope(d); push_scope_as_child_of(b); push_scope_as_child_of(c); pop_scope() update_descendants(a) @@ -131,8 +131,8 @@ TEST("graph", function() local a, b, c = node(root), node(root), node(root) function c.effect(x) - push_child_to_scope(a) - push_child_to_scope(b) + push_scope_as_child_of(a) + push_scope_as_child_of(b) return not x end @@ -171,10 +171,10 @@ TEST("graph", function() do push_scope(root) clean "root" items_updated = node(root) - push_child_to_scope(items_updated) -- should not + push_scope_as_child_of(items_updated) -- should not do push_scope(items_updated) - push_child_to_scope(items) + push_scope_as_child_of(items) do push_scope(root) do push_scope(scope1) @@ -183,7 +183,7 @@ TEST("graph", function() do push_scope(bind1) clean "bind1" - push_child_to_scope(selected) + push_scope_as_child_of(selected) pop_scope() end pop_scope() end @@ -192,7 +192,7 @@ TEST("graph", function() bind2 = node(scope2) do push_scope(bind2) clean "bind2" - push_child_to_scope(selected) + push_scope_as_child_of(selected) pop_scope() end pop_scope() end pop_scope() end @@ -2345,7 +2345,7 @@ TEST("read()", wrap_root(function() CHECK(read(src) == 1) end - do CASE "push_child_to_scope source" + do CASE "push_scope_as_child_of source" local src = source(0) local count = 0 @@ -2750,16 +2750,6 @@ TEST("strict", wrap_root(function() CHECK(count == 4) end - do CASE "indexes() error if primitive" - local src = source { 1 } - - local ok = pcall(function() - indexes(src, function() return 1 end) - end) - - CHECK(not ok) - end - do CASE "values() error if duplicate" local src = source { 1, 2, 1 }