hi @zep! This cart should not error, but it does (pico8 0.2.6b / linux)
-- wrap everything in func _init to fix it --function _init() --uncomment this to fix it --poke(0,0) --make this local to fix it coro = cocreate(function() -- uncomment this to fix it -- local x=1 for i=0,32000 do -- flr() is arbitary here; it's -- just a func that should -- return only one value local rets = pack(flr(i)) if #rets~=1 then local str="flr rvals:" for i,v in ipairs(rets) do str ..= "\n\t"..i..": "..tostr(v) end str ..="\nstat(1): "..stat(1) print(str.."\n") assert(false) end -- uncomment this to fix it -- if flr==flr then end end end) cls() --remove second param to fix it assert(coresume(coro, "uh-oh")) print"no errors" |
It seems like the string "uh oh" is being popped from the stack in a completely wrong place.
See also https://www.lexaloffle.com/bbs/?tid=141267 , a picotron coroutine bug. This cart is a modified version of @jesstelford's code there. I'm unsure if the root cause is related, but the result is the same.
Notably, this cart consistently crashes every single time (unlike the picotron version), and seems directly related to stat(1) going over 1.0
I marked a bunch of things you can uncomment to make this test case pass. (there are some very weird things you can do to make it suddenly work)
I'm fuzzy on the details, but it seems like something like this is happening:
coresume(coro, "uh-oh")
pushes "uh-oh" to the internal lua/C/lua_State stack, where it stays for a while (because there isn't ayield()
that pops that value)- the coroutine takes a long time (
stat(1)>1
), and pico8 pauses my cart to do its own thing (flip, etc) - pico8 prepares to hand control back to my cart, by restoring the state my cart was in when it got forcibly paused. it restores the stack with "uh-oh" on it, but it does it wrong somehow
- my code resumes running, maybe right at the point where
flr()
returns? (that would be quite a coincidence, but it might explain why irrelevant changes likelocal x=1
would squash the bug) flr
returns the correct value, but also the rest of the stack, which is "uh oh".
I was having a similar problem in some code of mine. I think I've figured out what's going on and that you've related it to stat(1)
going above 1 sort of backs up my suspicions.
This is pure speculation but here's what I think it happening:
Typically we'll have an _update
and _draw
function in our program and if our performance is bad we start dropping to a lower frame rate. That's important because it means the program isn't grinding to a halt completely, it's just slowing down. Which sounds a whole lot like coroutines to me. So behind the scenes @zep must be taking _update
and _draw
and turning them into coroutines when the game runs. Nothing too shocking there.
But most Pico-8 games don't use coroutines explicitly. So if performance in _update
is tanking there needs to be some way to kick us out of _update
so we can do a _draw_
cycle even though most games will never include a yield
anywhere. There's all sorts of ways you could do that but one way would be add a bunch of "sneaky" yields into API functions. Functions which everyone will use. Then every API function can check the current value of stat(1)
and if it's taking too long it can cut the frame rate, yield and, after the draw cycle, pick up right where it left off. (This isn't quite right as you'll see but that was my thinking.)
If you're not using coroutines in your code then that all works seamlessly and you never notice. But if you are using corountines the trouble can start to occur. Since coroutines can call other coroutines—or functions which call yield at some point—we're not necessarily calling coresume
on the coroutine we think we are.
I've modified your example a bit to show what I mean.
db = {} my_co = cocreate( function (a) local count = 0 while true do count = (count + 1) % 15000 x = pack(split(a)) -- for instance. if #x > 1 then add(db, 'extra arg: '..x[2]) add(db, 'caught after '..count..' iterations') end if count == 0 then yield() end end end ) count_draw = 0 function _draw() count_draw += 1 cls() print(count_draw) for d in all(db) do print(d) end end function _update() assert(coresume(my_co, '1,2,3')) end |
There is an explicit yield
there but it doesn't happen for a long time. Now I'm missing something in my explanation because if you remove the yield then you never get kicked out of _update
and never hit _draw
, which is what I thought would happen, but pack
is getting the extra argument so this seems to be where the coroutine machinery is getting mixed up.
My guess:
- my_co gets called with the string
'1,2,3'
- the loop takes too long and
split
(or maybepack
?) does a
"sneaky" yield which is then bound tomy_co
- which is then resumed also with the string
'1,2,3'
causing it to
be appended to the output ofsplit
and packed up bypack
.
The thing I'm missing might have to do with how Pico-8 (and presumably Picotron if to a lesser extent) throttles commands. Unless I'm remembering wrong, Pico-8 code runs slower than it technically needs to because the API commands are artificially throttled to take a consistent amount of time on the virtual CPU. Sneaky yields could be a mechanism to force commands to waste time and could be why they don't actually kick us out of _update
like I thought they would because zep is handling them slightly differently behind the scenes. But when one of those yields happens into our own coroutine code we catch it instead of Pico-8 catching it and if we give it extra arguments it happily takes them. (Having now read the last part of your post, this would also explain the "coincidence" of just happening to pick up right before the function returns. It's not a coincidence if that's where the sneaky yield is happening.)
Like I said that's all pure speculation.
The fix that's worked for me in my situation though is pretty simple. A function make_co
is basically a wrapper around a wrapper around cocreate
. It takes a function and returns a new function. That function when called (with arguments) wraps the orignal function in yet another function which does not take arguments and it's that function (with no arguments) from which we create the coroutine.
db = {} co_q = {} function make_co(f) return function(...) local args = {...} local co = cocreate( function() f(args) end ) add(co_q, co) end end -- In this version, my_co is a function that you call with whatever -- arguments you need. It creates a coroutine that takes no arguments -- and adds it to a queue. my_co = make_co( function (a) local count = 0 while true do count = (count + 1) % 15000 x = pack(split(a)) -- for instance. if #x > 1 then add(db, 'extra arg: '..x[2]) add(db, 'caught after '..count..' iterations') end if count == 0 then yield() end end end ) function _init() my_co('1,2,3') end count_draw = 0 function _draw() count_draw += 1 cls() print(count_draw) for d in all(db) do print(d) end end function _update() -- process all the coroutines in the queue. for co in all(co_q) do assert(coresume(co)) end end |
No more errors!
[Please log in to post a comment]