Example
Controls
Up/Down - change number of pixels drawn per frame by 20
Steps to reproduce
Write a program that runs nested coroutines when the frame end takes place.
Resulting and expected behavior
All coroutines except from the top one yield().
It's expected that coroutines resume normal work after frame end.
Example explanation
It's a program supposed to color the whole screen generating coordinates to color with a recursive coroutine (tab 2, "coroutine itself"). It is wrapped by a function that ensures all errors will be propagated and unwraps the output when no error is encountered (tab 1, "coroutine wrapper").
In pseudo code:
function coroutine(depth) if (depth == 0) yield{0,0} return c = cocreate(coroutine) x,y = wrap(c, depth - 1) -- a wrapper around coresume while x do -- this assumes that if x is nil then the coroutine yield{x,y} -- has reached a return or end. x,y = wrap(c) end c = cocreate(coroutine) x,y = wrap(c, depth - 1) while x do yield shifted {x,y} x,y = wrap(c) end end |
Info written on screen:
- Number of pixels coloured/number of pixels on screen
- Number of pixels coloured per frame (adjustable with Up/Down)
- Number of calls to subroutines of certain depth/expected number of calls
Failed workarounds
Adding a global table to hold recursive coroutines changes nothing.
Working workarounds
Limit the number of coroutine calls per frame with flip() outside the coroutines.
A wrapper that catches the nil yield and checks if the coroutine is dead. Doesn't work on coroutines that are supposed to yeld() or yield(nil)
WARNING! Skipps some coroutine outputs if flip() is called.
old_coresume = coresume function coresume(c, ...) flag, out = old_coresume(c,...) if not flag or out != nil or costatus(c) == 'dead' then return flag, out -- coroutine raised an error, yielded a non-nil value or finished else return coresume(c) -- resume from the unexpected yield() end end |
The example using an equivalent wrapper:
Maybe garbage collector deletes the coroutines before they are finished because they are not referenced to from the main loop but from inside other coroutines?
It seems like the "coroutines interrupted by garbage collection" bug that was supposed to be fixed in 0.2.0e
After some digging I found the original 0.2.0d bug report post which had the exact problem I had (though I thought my problem was different). In my previous code I used to check if the coroutine yielded nil, indicating it reached its end or a return, and if my coroutines randomly yield nil it would be interpreted as a coroutine closing and would tell the higher coroutine to proceed. I checked it by replacing
while x do |
with
while costatus(coroutine)!='dead' do |
and the whole thing broke, because coroutines yielded nil even though I never told them to. I may update the example cart if I come up with an example that better shows the problem, but till then take a look at the old post.
After even more digging and reading through @zep 's explanations I can guess what's going wrong:
At the end of the frame superyielding takes place, which correctly suspends all nested coroutines by yielding nil and processes the end of the frame.
At the start of the next frame only the top coroutine gets resumed like it never suspended, but the others still consider the forced yield() to be the correct one and break because nil is not the expected yield value.
I'll add a workaround to the post, but it's SO ANNOYING PLEASE FIX THIS THANKS.
There shouldn't be concurrent coroutines. Only one runs at a time. They're basically subroutines with embedded state info in the form of a hidden context that holds their stack and their pc. PICO-8 only has one hardware thread, after all.
Am I misunderstanding?
That being said, I could see the problem as @zep going through the entire list of known coroutines and yielding them all, even though only one of them is (possibly) running.
Ideally he should be checking to see if a coroutine is currently running and fake the yield on that one so he can force a flip(). Otherwise there should be no yielding at all.
(Also, tbh, I don't really understand why anything needs to be yielded. The automatic flip() should be happening at the virtual hardware level, not in Lua.)
@Felice Of course coroutines shold be concurrent! The best source about coroutines in Python is called A Curious Course on Coroutines and Concurrency because these two aspects of programming are so deeply related to each other.
My programs don't use concurrent coroutines though. The main usage of coroutines is to build a pipeline that works through the input instead of running each function on the whole input at once or generators that modify data yielded from other generators on the fly as data is requested from them.
As for the other things you said, you're probably right, but I've got no idea how PICO-8 works inside and I'm too afraid to go around guessing how to fix this.
Apropos nested coroutines:
LUA directly supports and expects nested coroutines.
In 5.2 (and PICO-8 as it turns out) there are FOUR statuses that coroutine.status (or costatus) can return. Three are documented in PICO-8 manual, but there is also a "normal" costatus: a coroutine is active, but not running, because it resumed another coroutine.
So hey, an undocumented PICO-8 feature! Cool!
no they are not concurrent, see: http://lua-users.org/wiki/CoroutinesTutorial
only a single coroutine runs at any given time.
@freds72 You're right, I messed up my English. I meant "recursive", and a better word would be "nested". I will edit my previous comments.
Concurrency can be achieved using coroutines without actually running two threads at the same time. The idea is to use yielding to suspend one thread and run another and switch between many threads this way making them run "concurrently" without actualy doing many things at once. What is described in the Lua wiki link you posted is actually called concurrent programming.
Anyway, my coroutines are not concurrent but recursive and that's where the problem comes from.
Recurrent means something that comes again, like a recurrent meeting every Monday. A function that calls itself is recursive, but this does not really apply here, so nested is probably appropriate (a coroutine creates other coroutines).
Anoter note about pico8 coroutines: if coro_a calls coro_b that calls coro_c, when coro_c yields it will automatically go up through coro_b and coro_a up to the site that coresumed coro_a.
@merwok my coroutines are recursive, although the requirement for reproducing the bug are nested coroutines.
Again sorry for my English. The mistake comes from the fact that a Polish word for recursion sounds similar to "recurrent".
@merwok what you said about coroutines yielding is not true. If coro_a runs coro_b as a coroutine, and similarly with coro_b calling coro_c and coro_c calling coro_d then when coro_d yields it resumes coro_c which can then process the data it got and yield it to coro_b and so on.
What you are describing is when coro_a calls func_a as a normal function. Then when func_a yields it will suspend both func_a and coro_a and resume main thread.
If what you said was true then my example program would work very differently (or break at all), because it depends heavily on the fact that a coroutine can create another coroutine and process the data it yields before yielding it further.
Yes, I did mean «coro_a calls coro_b» as a normal function, not coresume!
[Please log in to post a comment]