Try improve error reporting

This commit is contained in:
aaron 2024-12-27 22:43:01 +00:00
parent 3b22f6ccf9
commit 58a31a1b32
16 changed files with 164 additions and 55 deletions

View file

@ -23,6 +23,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
for this. for this.
- Implicit effects to set children now unparent all children when the effect is - Implicit effects to set children now unparent all children when the effect is
destroyed. destroyed.
- Error reporting should be improved with better formatting when effects invoke
other effects and no more loss of stack traces.
### Removed ### Removed

View file

@ -1,7 +1,6 @@
local typeof = game and typeof or require "../test/mock".typeof :: never local typeof = game and typeof or require "../test/mock".typeof :: never
local flags = require "./flags" local flags = require "./flags"
local throw = require "./throw"
local implicit_effect = require "./implicit_effect" local implicit_effect = require "./implicit_effect"
local _, is_action = require "./action"() local _, is_action = require "./action"()
local graph = require "./graph" local graph = require "./graph"
@ -67,7 +66,7 @@ local function process_properties(properties: Map<unknown, unknown>, instance: I
if type(property) == "string" then if type(property) == "string" then
if flags.strict then -- check for duplicate property assignment at nesting depth if flags.strict then -- check for duplicate property assignment at nesting depth
if cache.nested_debug[depth][property] then if cache.nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`) error(`duplicate property {property} at depth {depth}`, 0)
end end
cache.nested_debug[depth][property] = true cache.nested_debug[depth][property] = true
end end
@ -104,7 +103,7 @@ end
-- applies table of nested properties to an instance using full vide semantics -- applies table of nested properties to an instance using full vide semantics
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
if not properties then if not properties then
throw("attempt to call a constructor returned by create() with no properties") error "attempt to call a constructor returned by create() with no properties"
end end
-- queue parent assignment if any for last -- queue parent assignment if any for last

View file

@ -1,5 +1,4 @@
local flags = require "./flags" local flags = require "./flags"
local throw = require "./throw"
local graph = require "./graph" local graph = require "./graph"
local function batch(setter: () -> ()) local function batch(setter: () -> ())
@ -11,14 +10,14 @@ local function batch(setter: () -> ())
from = graph.get_update_queue_length() from = graph.get_update_queue_length()
end end
local ok, err: string? = pcall(setter) local ok, err: string? = xpcall(setter, debug.traceback)
if not already_batching then if not already_batching then
flags.batch = false flags.batch = false
graph.flush_update_queue(from) graph.flush_update_queue(from)
end end
if not ok then throw(`error occured while batching updates: {err}`) end if not ok then error(`error occured while batching updates: {err}`, 0) end
end end
return batch return batch

View file

@ -1,6 +1,5 @@
local typeof = game and typeof or require "../test/mock".typeof :: never local typeof = game and typeof or require "../test/mock".typeof :: never
local throw = require "./throw"
local graph = require "./graph" local graph = require "./graph"
local get_scope = graph.get_scope local get_scope = graph.get_scope
local push_cleanup = graph.push_cleanup local push_cleanup = graph.push_cleanup
@ -14,14 +13,14 @@ local function helper(obj: any)
elseif obj.disconnect then function() obj:disconnect() end elseif obj.disconnect then function() obj:disconnect() end
elseif obj.Destroy then function() obj:Destroy() end elseif obj.Destroy then function() obj:Destroy() end
elseif obj.Disconnect then function() obj:Disconnect() end elseif obj.Disconnect then function() obj:Disconnect() end
else throw("cannot cleanup given object") else error "cannot cleanup given object"
end end
local function cleanup(value: unknown) local function cleanup(value: unknown)
local scope = get_scope() local scope = get_scope()
if not scope then if not scope then
throw "cannot cleanup outside a stable or reactive scope" error "cannot cleanup outside a stable or reactive scope"
end; assert(scope) end; assert(scope)
if type(value) == "function" then if type(value) == "function" then

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local graph = require "./graph" local graph = require "./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
@ -44,10 +43,10 @@ local function context<T>(...: T): Context<T>
if has_default ~= nil then if has_default ~= nil then
return default_value return default_value
else else
throw("attempt to get context when no context is set and no default context is set") error("attempt to get context when no context is set and no default context is set", 0)
end end
else -- set else -- set
if not scope then return throw("attempt to set context outside of a vide scope") end if not scope then return error("attempt to set context outside of a vide scope", 0) end
local value, component = ... local value, component = ...
@ -62,7 +61,7 @@ local function context<T>(...: T): Context<T>
pop_scope() pop_scope()
if not ok then if not ok then
throw(`error while running context:\n\n{result}`) error(`error while running context:\n\n{result}`, 0)
end end
return result return result

View file

@ -1,7 +1,6 @@
local typeof = game and typeof or require "../test/mock".typeof :: never local typeof = game and typeof or require "../test/mock".typeof :: never
local Instance = game and Instance or require "../test/mock".Instance :: never local Instance = game and Instance or require "../test/mock".Instance :: never
local throw = require "./throw"
local defaults = require "./defaults" local defaults = require "./defaults"
local apply = require "./apply" local apply = require "./apply"
@ -10,7 +9,7 @@ local ctor_cache = {} :: { [string]: () -> Instance }
setmetatable(ctor_cache :: any, { setmetatable(ctor_cache :: any, {
__index = function(self, class) __index = function(self, class)
local ok, instance: Instance = pcall(Instance.new, class :: any) local ok, instance: Instance = pcall(Instance.new, class :: any)
if not ok then throw(`invalid class name, could not create instance of class { class }`) end if not ok then error(`invalid class name, could not create instance of class { class }`, 0) end
local default: { [string]: unknown }? = defaults[class] local default: { [string]: unknown }? = defaults[class]
if default then if default then
@ -35,7 +34,7 @@ end
local function clone_instance(instance: Instance) local function clone_instance(instance: Instance)
return function(properties: Props): Instance return function(properties: Props): Instance
local clone = instance:Clone() local clone = instance:Clone()
if not clone then throw "attempt to clone a non-archivable instance" end if not clone then error "attempt to clone a non-archivable instance" end
return apply(clone, properties) return apply(clone, properties)
end end
end end
@ -47,7 +46,7 @@ local function create(class_or_instance: string | Instance, props: Props?): ((Pr
elseif typeof(class_or_instance) == "Instance" then elseif typeof(class_or_instance) == "Instance" then
result = clone_instance(class_or_instance) result = clone_instance(class_or_instance)
else else
throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance)) error("bad argument #1, expected string or instance, got " .. typeof(class_or_instance), 0)
return nil :: never return nil :: never
end end
if props then if props then

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local flags = require "./flags" local flags = require "./flags"
export type SourceNode<T> = { export type SourceNode<T> = {
@ -22,9 +21,23 @@ 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 efn(err: string)
local trace = debug.traceback(err, 2)
if string.find(err, "^effect error stacktrace") then -- if effect error is nested
trace = string.gsub(" " .. trace, "\n", function() -- indent entire error
return "\n "
end)
end
trace ..= "\nsource update stacktrace:"
return trace
end
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(xpcall) local thread = coroutine.create(xpcall)
local function efn(err: string) return debug.traceback(err, 3) end --local function efn(err: string) return debug.traceback(err, 3) end
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg) local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
assert(resume_ok) assert(resume_ok)
@ -45,9 +58,9 @@ local function assert_stable_scope(): Node<unknown>
if not scope then if not scope then
local caller_name = debug.info(2, "n") local caller_name = debug.info(2, "n")
return throw(`cannot use {caller_name}() outside a stable or reactive scope`) return error(`cannot use {caller_name}() outside a stable or reactive scope`, 0)
elseif scope.effect then elseif scope.effect then
throw("cannot create a new reactive scope inside another reactive scope") error("cannot create a new reactive scope inside another reactive scope", 0)
end end
return scope return scope
@ -81,8 +94,8 @@ end
local function flush_cleanups<T>(node: Node<T>) local function flush_cleanups<T>(node: Node<T>)
if node.cleanups then if node.cleanups then
for _, fn in next, node.cleanups do for _, fn in next, node.cleanups do
local ok, err: string? = pcall(fn) local ok, err: string? = xpcall(fn, debug.traceback)
if not ok then throw(`cleanup error: {err}`) end if not ok then error(`cleanup error: {err}`, 0) end
end end
table.clear(node.cleanups) table.clear(node.cleanups)
@ -107,7 +120,7 @@ end
local function destroy<T>(node: Node<T>) local function destroy<T>(node: Node<T>)
if flags.strict and table.find(scopes, node) then if flags.strict and table.find(scopes, node) then
throw("attempt to destroy an active scope") error("attempt to destroy an active scope", 0)
end end
flush_cleanups(node) flush_cleanups(node)
@ -150,7 +163,7 @@ local function evaluate_node<T>(node: Node<T>)
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(`effect stacktrace:\n{new_value :: string}`) error(`effect error stacktrace\n{new_value :: string}`, 0)
end end
node.cache = new_value :: T node.cache = new_value :: T
@ -170,7 +183,7 @@ local function evaluate_node<T>(node: Node<T>)
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(`effect stacktrace:\n{new_value}\n`) error(`effect error:\n{new_value}\n`, 0)
end end
node.cache = new_value node.cache = new_value

View file

@ -18,7 +18,6 @@ local indexes, values = require "./maps"()
local spring, update_springs = require "./spring"() local spring, update_springs = require "./spring"()
local action = require "./action"() local action = require "./action"()
local changed = require "./changed" local changed = require "./changed"
local throw = require "./throw"
local flags = require "./flags" local flags = require "./flags"
export type Source<T> = source.Source<T> export type Source<T> = source.Source<T>
@ -98,7 +97,7 @@ local vide = {
setmetatable(vide :: any, { setmetatable(vide :: any, {
__index = function(_, index: unknown): () __index = function(_, index: unknown): ()
if flags[index] == nil then if flags[index] == nil then
throw(`{tostring(index)} is not a valid member of vide`) error(`{tostring(index)} is not a valid member of vide`, 0)
else else
return flags[index] return flags[index]
end end
@ -106,7 +105,7 @@ setmetatable(vide :: any, {
__newindex = function(_, index: unknown, value: unknown) __newindex = function(_, index: unknown, value: unknown)
if flags[index] == nil then if flags[index] == nil then
throw(`{tostring(index)} is not a valid member of vide`) error(`{tostring(index)} is not a valid member of vide, 0`)
else else
flags[index] = value flags[index] = value
end end

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local flags = require "./flags" local flags = require "./flags"
local graph = require "./graph" local graph = require "./graph"
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
@ -20,7 +19,7 @@ local function check_primitives(t: {})
for _, v in next, t do for _, v in next, t do
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
throw("table source map cannot return primitives") error("table source map cannot return primitives", 0)
end end
end end
@ -71,7 +70,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
push_scope(scope) push_scope(scope)
local ok, result = pcall(transform, function() local ok, result = xpcall(transform, debug.traceback, function()
push_child_to_scope(node) push_child_to_scope(node)
return node.cache return node.cache
end, i) end, i)
@ -132,7 +131,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local cache = {} local cache = {}
for _, v in next, data do for _, v in next, data do
if cache[v] ~= nil then if cache[v] ~= nil then
throw "duplicate table value detected" error "duplicate table value detected"
end end
cache[v] = true cache[v] = true
end end
@ -154,7 +153,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
push_scope(scope) push_scope(scope)
local ok, result = pcall(transform, v, function() local ok, result = xpcall(transform, debug.traceback, v, function()
push_child_to_scope(node) push_child_to_scope(node)
return node.cache return node.cache
end) end)

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local graph = require "./graph" local graph = require "./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
@ -14,21 +13,20 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): (() -> (), T...)
refs[node] = true -- prevent gc of root node refs[node] = true -- prevent gc of root node
local destroy = function() local destroy = function()
if not refs[node] then throw "root already destroyed" end if not refs[node] then error "root already destroyed" end
refs[node] = nil refs[node] = nil
destroy(node) destroy(node)
end end
push_scope(node) push_scope(node)
local function efn(err: string) return debug.traceback(err, 3) end local result = { xpcall(fn, debug.traceback, destroy) }
local result = { xpcall(fn, efn, destroy) }
pop_scope() pop_scope()
if not result[1] then if not result[1] then
destroy() destroy()
throw(`error while running root():\n\n{result[2]}`) error(`error while running root():\n\n{result[2]}`, 0)
end end
return destroy, unpack(result :: any, 2) return destroy, unpack(result :: any, 2)

View file

@ -9,7 +9,7 @@ export type Source<T> = (() -> T) & ((value: T) -> T)
local function source<T>(initial_value: T): Source<T> local function source<T>(initial_value: T): Source<T>
local node = create_source_node(initial_value) local node = create_source_node(initial_value)
return function(...): T local function update_source(...): T
if select("#", ...) == 0 then -- no args were given if select("#", ...) == 0 then -- no args were given
push_child_to_scope(node) push_child_to_scope(node)
return node.cache return node.cache
@ -24,6 +24,8 @@ local function source<T>(initial_value: T): Source<T>
update_descendants(node) update_descendants(node)
return v return v
end end
return update_source
end end
return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>) return source :: (<T>(initial_value: T) -> Source<T>) & (<T>() -> Source<T>)

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local graph = require "./graph" local graph = require "./graph"
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
type SourceNode<T> = graph.SourceNode<T> type SourceNode<T> = graph.SourceNode<T>
@ -114,7 +113,7 @@ local vec6_to_type = {
local invalid_type = { local invalid_type = {
__index = function(_, t: string) __index = function(_, t: string)
throw(`cannot spring type {t}`) error(`cannot spring type {t}`, 0)
end end
} }
@ -141,7 +140,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
-- todo: is there a solution other than reducing step size? -- todo: is there a solution other than reducing step size?
-- todo: this does not catch all solver exploding cases -- todo: this does not catch all solver exploding cases
if c > UPDATE_RATE*2 then -- solver will explode if this is true if c > UPDATE_RATE*2 then -- solver will explode if this is true
throw("spring damping too high, consider reducing damping or increasing period") error("spring damping too high, consider reducing damping or increasing period", 0)
end end
local data: SpringState<T> = { local data: SpringState<T> = {
@ -263,8 +262,6 @@ local function step_springs(dt: number)
end end
end end
local remove_queue = {}
local function update_spring_sources() local function update_spring_sources()
for data, output in springs do for data, output in springs do
local x0_123, x1_123, v_123, local x0_123, x1_123, v_123,

View file

@ -1,4 +1,3 @@
local throw = require "./throw"
local graph = require "./graph" local graph = require "./graph"
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
type SourceNode<T> = graph.SourceNode<T> type SourceNode<T> = graph.SourceNode<T>
@ -32,7 +31,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
if component == nil then return nil end if component == nil then return nil end
if type(component) ~= "function" then if type(component) ~= "function" then
throw "map must map a value to a function" error "map must map a value to a function"
end end
local new_scope = create_node(owner, false, false) local new_scope = create_node(owner, false, false)
@ -40,7 +39,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
push_scope(new_scope) push_scope(new_scope)
local ok, result = pcall(component) local ok, result = xpcall(component, debug.traceback)
pop_scope() pop_scope()

View file

@ -1,5 +0,0 @@
local function VIDE_ASSERT(msg): any
error(msg, 0)
end
return VIDE_ASSERT

View file

@ -10,13 +10,13 @@ local function untrack<T>(source: () -> T): T
local effect = scope.effect local effect = scope.effect
scope.effect = false scope.effect = false
local ok, result = pcall(source) local ok, result = xpcall(source, debug.traceback)
scope.effect = effect :: () -> () scope.effect = effect :: () -> ()
if not ok then error(result, 0) end if not ok then error(result, 0) end
return result return result :: T
else else
return source() return source()
end end

110
test/stacktrace-test.luau Normal file
View file

@ -0,0 +1,110 @@
local vide = require "../"
do
print "============================================================="
local a = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end
do
print "============================================================="
local a = vide.source(1)
local b = vide.source(1)
local c = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
b(vide.untrack(b) + 1)
end)
vide.effect(function()
b()
c(vide.untrack(c) + 1)
end)
vide.effect(function()
c()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end
do
print "============================================================="
local a = vide.source(1)
local b = vide.source(1)
local c = vide.source(1)
local cause_error = false
local function try_error()
if cause_error then error("uh oh") end
end
vide.root(function()
vide.effect(function()
a()
vide.untrack(function() -- todo: this trace appearing twice
b(b() + 1)
return nil
end)
end)
vide.effect(function()
b()
vide.batch(function()
c(vide.untrack(c) + 1)
end)
end)
vide.effect(function()
c()
try_error()
end)
end)
cause_error = true
local ok, result = pcall(function() a(2) end)
print(result)
print "============================================================="
end