Improve error reporting

This commit is contained in:
aaron 2024-07-15 17:37:26 +01:00
parent 14f8d38a35
commit 7bae2517cd
7 changed files with 50 additions and 119 deletions

View file

@ -1,35 +1,12 @@
if not game then script = require "test/relative-string" end 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) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create_node = graph.create_node local create_node = graph.create_node
local assert_stable_scope = graph.assert_stable_scope local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
function create_binding<T>(updater: (T) -> T, binding: T) function create_implicit_effect<T>(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
evaluate_node(create_node(assert_stable_scope(), updater, binding)) evaluate_node(create_node(assert_stable_scope(), updater, binding))
end end
@ -39,7 +16,7 @@ type PropertyBinding = {
source: () -> unknown source: () -> unknown
} }
local function update_property(p: PropertyBinding) local function update_property_effect(p: PropertyBinding)
(p.instance :: any)[p.property] = p.source() (p.instance :: any)[p.property] = p.source()
return p return p
end end
@ -49,7 +26,7 @@ type ParentBinding = {
parent: () -> Instance parent: () -> Instance
} }
local function update_parent(p: ParentBinding) local function update_parent_effect(p: ParentBinding)
p.instance.Parent = p.parent() p.instance.Parent = p.parent()
return p return p
end end
@ -61,7 +38,7 @@ type ChildrenBinding = {
children: () -> Instance | { Instance } 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 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 local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
@ -94,7 +71,7 @@ end
return { return {
property = function(instance, property, source) property = function(instance, property, source)
return create_binding(update_property, { return create_implicit_effect(update_property_effect, {
instance = instance, instance = instance,
property = property, property = property,
source = source source = source
@ -102,14 +79,14 @@ return {
end, end,
parent = function(instance, parent) parent = function(instance, parent)
return create_binding(update_parent, { return create_implicit_effect(update_parent_effect, {
instance = instance, instance = instance,
parent = parent parent = parent
}) })
end, end,
children = function(instance, children) children = function(instance, children)
return create_binding(update_children, { return create_implicit_effect(update_children_effect, {
instance = instance, instance = instance,
cur_children_set = {}, cur_children_set = {},
new_children_set = {}, new_children_set = {},

View file

@ -23,13 +23,14 @@ export type Node<T> = {
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U) local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
local thread = coroutine.create(pcall) local thread = coroutine.create(xpcall)
local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg) 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) assert(resume_ok)
if coroutine.status(thread) ~= "dead" then 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 end
return run_ok, result return run_ok, result
@ -129,42 +130,48 @@ end
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> } local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
local function evaluate_node<T>(node: Node<T>) local function evaluate_node<T>(node: Node<T>)
local cur_value = node.cache
if flags.strict then if flags.strict then
flush_cleanups(node) local ok, cur_value, new_value
destroy_owned(node) for i = 1, 2 do
cur_value = node.cache
push_scope(node)
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
pop_scope()
if not ok then throw(new_value :: string) end
node.cache = new_value :: T
end
flush_cleanups(node) flush_cleanups(node)
destroy_owned(node) destroy_owned(node)
push_scope(node) push_scope(node)
ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
pop_scope() pop_scope()
if not ok then if not ok then
table.clear(update_queue) table.clear(update_queue)
update_queue.n = 0 update_queue.n = 0
throw(`side-effect error from source update\n{new_value}`) 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 = pcall(node.effect :: (T) -> T, node.cache)
pop_scope()
if not ok then
table.clear(update_queue)
update_queue.n = 0
throw(`effect stacktrace:\n{new_value}\n`)
end end
node.cache = new_value node.cache = new_value
return cur_value ~= new_value return cur_value ~= new_value
end end
end
local function queue_children_for_update<T>(node: SourceNode<T>) local function queue_children_for_update<T>(node: SourceNode<T>)
local i = update_queue.n local i = update_queue.n

View file

@ -29,7 +29,7 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
if not result[1] then if not result[1] then
refs[node] = nil refs[node] = nil
throw(`mount error\n{result[2]}`) throw(`error while running root():\n\n{result[2]}`)
end end
return unpack(result :: any, 2) return unpack(result :: any, 2)

View file

@ -1,9 +1,7 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local trace = require(script.Parent.trace) local function VIDE_ASSERT(msg): any
error(msg, 0)
local function throw(msg): any
error(msg, trace() - 1)
end end
return throw return VIDE_ASSERT

View file

@ -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

View file

@ -26,7 +26,6 @@ end
local N = 2^18 -- 262144 local N = 2^18 -- 262144
TITLE "sources" TITLE "sources"
BENCH("create source", function() BENCH("create source", function()
@ -449,27 +448,6 @@ end)
N *= 1024 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" TITLE "aggregate"
do do

View file

@ -218,8 +218,8 @@ TEST("graph", function()
do do
local c = get_children(selected) local c = get_children(selected)
CHECK(#c == 2) CHECK(#c == 2)
CHECK(table.find(c, bind1)) CHECK(table.find(c, bind1 :: any))
CHECK(table.find(c, bind2)) CHECK(table.find(c, bind2 :: any))
end end
do do