Warn on a yield in strict mode instead of cancelling the thread

This commit is contained in:
Kaan 2026-10-22 15:33:34 +03:00
parent 6b0bbf0cf4
commit d933fe8e9c
3 changed files with 33 additions and 24 deletions

View file

@ -15,7 +15,7 @@ and identifying improper usage.
Currently, strict mode will: Currently, strict mode will:
1. Run reactive scopes twice when a source updates. 1. Run reactive scopes twice when a source updates.
2. Throw an error if yields occur where they are not allowed. 2. Warn if yields occur where they are not allowed.
3. Checks for `values()` input having duplicate values. 3. Checks for `values()` input having duplicate values.
4. Checks for duplicate nested properties at same depth. 4. Checks for duplicate nested properties at same depth.
5. Checks for destruction of an active scope. 5. Checks for destruction of an active scope.

View file

@ -1,5 +1,7 @@
local flags = require "./flags" local flags = require "./flags"
local warn = warn or print
export type SourceNode<T> = { export type SourceNode<T> = {
cache: T, cache: T,
[number]: Node<T> [number]: Node<T>
@ -42,28 +44,33 @@ local function effect_traceback(err: unknown): string
return `effect error:\n{debug.traceback(message, 2)}` return `effect error:\n{debug.traceback(message, 2)}`
end end
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U) -- also returns whether fn yielded, its thread is then left to finish on its own
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U, boolean)
local n = scopes.n local n = scopes.n
local thread = coroutine.create(xpcall) local yielded = false
local thread = coroutine.create(function()
local ok, result = xpcall(fn, efn, arg)
if yielded and not ok then error(result, 0) end -- no caller left to return it to
return ok, result
end)
--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)
assert(resume_ok) assert(resume_ok)
if coroutine.status(thread) ~= "dead" then if coroutine.status(thread) ~= "dead" then
local trace = debug.traceback(thread, "attempt to yield in reactive scope") yielded = true
warn(debug.traceback(thread, "attempt to yield in reactive scope"))
-- the thread can still be resumed (e.g. by task.wait), so stop it and -- the thread pops the scopes it pushed once resumed, so move the
-- pop the scopes it pushed before yielding -- caller's scope above them for the caller to pop
if task then task.cancel(thread) else coroutine.close(thread) end table.insert(scopes, table.remove(scopes, n) :: Node<any>)
for i = scopes.n, n + 1, -1 do scopes[i] = nil end
scopes.n = n
return false, trace return true, result, true
end end
return run_ok, result return run_ok, result, false
end end
local function get_scope(): Node<unknown>? local function get_scope(): Node<unknown>?
@ -199,8 +206,10 @@ local function try_evaluate_node<T>(node: Node<T>): (boolean, string?)
err = err or cleanup_err or owned_err err = err or cleanup_err or owned_err
push_scope(node) push_scope(node)
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value) local ok, new_value, yielded = ycall(node.effect :: (T) -> T, cur_value)
pop_scope() pop_scope()
if yielded then break end
if not ok then if not ok then
return false, `effect error stacktrace\n{new_value :: string}` return false, `effect error stacktrace\n{new_value :: string}`

View file

@ -3146,7 +3146,7 @@ end))
TEST("strict", wrap_root(function() TEST("strict", wrap_root(function()
vide.strict = true vide.strict = true
do CASE "error on derived callback yield" do CASE "warn on derived callback yield"
local src = source(1) local src = source(1)
local ok = pcall(function() local ok = pcall(function()
@ -3156,10 +3156,10 @@ TEST("strict", wrap_root(function()
end) end)
end) end)
CHECK(not ok) CHECK(ok)
end end
do CASE "error on effecter callback yield" do CASE "warn on effecter callback yield"
local src = source(1) local src = source(1)
local ok = pcall(function() local ok = pcall(function()
@ -3169,26 +3169,26 @@ TEST("strict", wrap_root(function()
end) end)
end) end)
CHECK(not ok) CHECK(ok)
end end
do CASE "yield in a component" do CASE "yield in a component"
local input = source(false) local input = source(false)
local thread local thread
local resumed = false local output
local destroy = root(function() local destroy = root(function()
show(input, function() output = show(input, function()
thread = coroutine.running() thread = coroutine.running()
coroutine.yield() coroutine.yield()
resumed = true return "component"
return {}
end) end)
end) end)
CHECK(not pcall(input, true)) CHECK((pcall(input, true)))
CHECK(not coroutine.resume(thread)) CHECK(output() == nil)
CHECK(not resumed) CHECK((coroutine.resume(thread)))
CHECK(output() == "component")
CHECK(pcall(input, false)) CHECK(pcall(input, false))
CHECK(pcall(destroy)) CHECK(pcall(destroy))
end end