From 7bae2517cd952815a7846c8c006da322cba9dbe0 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:37:26 +0100 Subject: [PATCH] Improve error reporting --- src/bind.luau | 37 +++++-------------------- src/graph.luau | 67 +++++++++++++++++++++++++-------------------- src/root.luau | 2 +- src/throw.luau | 8 ++---- src/trace.luau | 29 -------------------- test/benchmark.luau | 22 --------------- test/tests.luau | 4 +-- 7 files changed, 50 insertions(+), 119 deletions(-) delete mode 100644 src/trace.luau diff --git a/src/bind.luau b/src/bind.luau index c6e8ab7..dd34ce3 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -1,35 +1,12 @@ if not game then script = require "test/relative-string" end -local trace = require(script.Parent.trace) -local flags = require(script.Parent.flags) local graph = require(script.Parent.graph) type Node = graph.Node local create_node = graph.create_node local assert_stable_scope = graph.assert_stable_scope local evaluate_node = graph.evaluate_node -function create_binding(updater: (T) -> T, binding: T) - if flags.strict then - -- track bind creation trace - local fn = updater - local bind_trace = debug.traceback(nil, trace()-1) - updater = function(...) - local ok, result = xpcall(fn, function(err: string) - return err - end, ...) - - if not ok then - local btype = - if (binding :: any).property then (binding :: any).property - elseif (binding :: any).parent then "Parent" - else "children" - error(`PROPERTY BINDING ERROR: Property {btype}\n{result}\nBIND CREATION TRACE:\n{bind_trace}`, 0) - end - - return result - end - end - +function create_implicit_effect(updater: (T) -> T, binding: T) evaluate_node(create_node(assert_stable_scope(), updater, binding)) end @@ -39,7 +16,7 @@ type PropertyBinding = { source: () -> unknown } -local function update_property(p: PropertyBinding) +local function update_property_effect(p: PropertyBinding) (p.instance :: any)[p.property] = p.source() return p end @@ -49,7 +26,7 @@ type ParentBinding = { parent: () -> Instance } -local function update_parent(p: ParentBinding) +local function update_parent_effect(p: ParentBinding) p.instance.Parent = p.parent() return p end @@ -61,7 +38,7 @@ type ChildrenBinding = { children: () -> Instance | { Instance } } -local function update_children(p: ChildrenBinding) +local function update_children_effect(p: ChildrenBinding) local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update @@ -94,7 +71,7 @@ end return { property = function(instance, property, source) - return create_binding(update_property, { + return create_implicit_effect(update_property_effect, { instance = instance, property = property, source = source @@ -102,14 +79,14 @@ return { end, parent = function(instance, parent) - return create_binding(update_parent, { + return create_implicit_effect(update_parent_effect, { instance = instance, parent = parent }) end, children = function(instance, children) - return create_binding(update_children, { + return create_implicit_effect(update_children_effect, { instance = instance, cur_children_set = {}, new_children_set = {}, diff --git a/src/graph.luau b/src/graph.luau index 31ca2d9..a90dd6c 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -23,13 +23,14 @@ export type Node = { local scopes = { n = 0 } :: { [number]: Node, n: number } -- scopes stack local function ycall(fn: (T) -> U, arg: T): (boolean, string|U) - local thread = coroutine.create(pcall) - local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg) + local thread = coroutine.create(xpcall) + local function efn(err: string) return debug.traceback(err, 3) end + local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg) assert(resume_ok) if coroutine.status(thread) ~= "dead" then - return false, "attempt to yield in reactive scope" + return false, debug.traceback(thread, "attempt to yield in reactive scope") end return run_ok, result @@ -129,41 +130,47 @@ end local update_queue = { n = 0 } :: { n: number, [number]: Node } local function evaluate_node(node: Node) - local cur_value = node.cache - if flags.strict then + local ok, cur_value, new_value + for i = 1, 2 do + cur_value = node.cache + + flush_cleanups(node) + destroy_owned(node) + + push_scope(node) + ok, new_value = ycall(node.effect :: (T) -> T, cur_value) + pop_scope() + + if not ok then + table.clear(update_queue) + update_queue.n = 0 + throw(`effect stacktrace:\n{new_value :: string}`) + end + + node.cache = new_value :: T + end + + return cur_value ~= new_value + else + local cur_value = node.cache + flush_cleanups(node) destroy_owned(node) push_scope(node) - - local ok, new_value = ycall(node.effect :: (T) -> T, cur_value) - + local ok, new_value = pcall(node.effect :: (T) -> T, node.cache) pop_scope() - - if not ok then throw(new_value :: string) end - node.cache = new_value :: T + if not ok then + table.clear(update_queue) + update_queue.n = 0 + throw(`effect stacktrace:\n{new_value}\n`) + end + + node.cache = new_value + return cur_value ~= new_value end - - flush_cleanups(node) - destroy_owned(node) - - push_scope(node) - - local ok, new_value = pcall(node.effect :: (T) -> T, node.cache) - - pop_scope() - - if not ok then - table.clear(update_queue) - update_queue.n = 0 - throw(`side-effect error from source update\n{new_value}`) - end - - node.cache = new_value - - return cur_value ~= new_value end local function queue_children_for_update(node: SourceNode) diff --git a/src/root.luau b/src/root.luau index 33c9a0d..2a1eb92 100644 --- a/src/root.luau +++ b/src/root.luau @@ -29,7 +29,7 @@ local function root(fn: (destroy: () -> ()) -> T...): T... if not result[1] then refs[node] = nil - throw(`mount error\n{result[2]}`) + throw(`error while running root():\n\n{result[2]}`) end return unpack(result :: any, 2) diff --git a/src/throw.luau b/src/throw.luau index 70b7973..954f3e2 100644 --- a/src/throw.luau +++ b/src/throw.luau @@ -1,9 +1,7 @@ if not game then script = require "test/relative-string" end -local trace = require(script.Parent.trace) - -local function throw(msg): any - error(msg, trace() - 1) +local function VIDE_ASSERT(msg): any + error(msg, 0) end -return throw +return VIDE_ASSERT diff --git a/src/trace.luau b/src/trace.luau deleted file mode 100644 index 04672ff..0000000 --- a/src/trace.luau +++ /dev/null @@ -1,29 +0,0 @@ --- returns path to file as an array with each directory --- accounts for Roblox and Luau contexts -local function get_path(s) - if string.sub(s, #s - 4, #s) == ".luau" then - s = string.sub(s, 1, #s - 5) - end - - return string.split(s, string.match(s, "%w+/") and "/" or ".") -end - --- get directory of vide root -local root do - local path = get_path(debug.info(1, "s")) - root = path[#path - 1] -end - --- finds the first stack depth outside of any vide library function -return function(): number - local stack = 1 - - local path = get_path(debug.info(stack, "s")) - - while path[#path] == root or path[#path - 1] == root do - stack += 1 - path = get_path(debug.info(stack, "s")) - end - - return stack -end diff --git a/test/benchmark.luau b/test/benchmark.luau index bee8c68..c35b651 100644 --- a/test/benchmark.luau +++ b/test/benchmark.luau @@ -26,7 +26,6 @@ end local N = 2^18 -- 262144 - TITLE "sources" BENCH("create source", function() @@ -449,27 +448,6 @@ end) N *= 1024 -TITLE "cleanup" - -ROOT_BENCH("register new cleanup", function() - local cleanup = cleanup - - local cleaner = function() end - - local callers = {} - - for i = 1, N do - callers[i] = function(fn, v) - fn(v) - return i -- return unique upvalue to ensure unique closure - end - end - - for i = 1, START(N) do - callers[i](cleanup, cleaner) - end -end) - TITLE "aggregate" do diff --git a/test/tests.luau b/test/tests.luau index e874d78..02e2e6c 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -218,8 +218,8 @@ TEST("graph", function() do local c = get_children(selected) CHECK(#c == 2) - CHECK(table.find(c, bind1)) - CHECK(table.find(c, bind2)) + CHECK(table.find(c, bind1 :: any)) + CHECK(table.find(c, bind2 :: any)) end do