Log In  

hey @zep, I've found a nasty coroutine(?)/multival bug. I'm on Linux + picotron 0.1.0d.

tl;dr: sometimes select("#",tostr(i)) is 2, possibly triggered by calling coresume() with extra args.


I ran into this initially because add({},3,nil) is a runtime error now (it used to work in PICO-8, but now it throws bad argument #2 to 'add' (position out of bounds)). I had some code: add(list,quote(arg)) that was crashing as if quote() was returning a second value for some reason, even though the code for quote() definitely returned just one value. (surrounding it in parens (to only keep the first return value) fixed my bug: add(list,(quote(arg))))

Version A of the code is very short, and trips the assert inside spin maybe 50% of the time? sometimes many runs in a row don't trigger the assert, but sometimes many runs in a row all trigger the assert. (maybe that's just statistics tho). Version B is a bit more complex but always trips the assert instantly for me.


Version A: (short, inconsistent)

printh"---"

function _init()
    local bad,good = cocreate_spin()
    fn = bad
end
function _draw()
    cls()
    local _,prog = fn(0.70)
    ?stat(1)
    ?prog or "done"
end

function cocreate_spin()
    local coro = cocreate(spin)
    return function( cpu_limit)
        if costatus(coro)=="suspended" then
            --this one breaks sometimes
            return assert(coresume(coro,cpu_limit or 0.90))
        end
    end, function()
        if costatus(coro)=="suspended" then
            --this one always works
            return assert(coresume(coro))
        end
    end
end

local total = 1000000
function spin()
    for i=1,total do
        if i%10000==0 --[[and stat(1)>cpu_limit]] then
            yield(i/total)
        end
        local n = select("#",quote(i))
        if n!=1 then
            assert(false,tostr(n).." retvals?! "..i)
        end
    end
end

function quote(t)
    return tostr(t)
end

Version B: (longer, very consistent)

printh"---"

function _init()
    poke(0x5f36,0x80) -- wrap text
    window{
        title="import png",
        width=160,
        height=64,
    }
    job = job_importpng()
end
function _update()
    job:work(0.70)
end
function _draw()
    cls()
    print(stat(1))
    print(costatus(job.coro))
end

function job_importpng()
    local coro = cocreate(pq_many)
    assert(coresume(coro))
    local job = {
        coro = coro,
        progress = 0.00,
    }
    -- returns true iff job has more work to do
    function job:work( cpu_limit)
        if costatus(self.coro)=="suspended" then
            local _,dat = assert(coresume(self.coro,cpu_limit or 0.90))
            if dat then
                self.progress = dat
                return true -- more work to do
            end
        end
    end
    return job
end

function pq_many()
    for i=1,1000000 do
        if i&2047==0 then
            yield()
        end
        pq(i)
    end
end

-- quotes all args and prints to host console
-- usage:
--   pq("handles nils", many_vars, {tables=1, work=11, too=111})
function pq(arg)
    local s= {}
        local n = select("#",quote(arg))
        if n!=1 then
            local second = select(2,quote(arg))
            assert(false,tostr(n).." retvals?: "..quote(arg).." "..tostr(second))
        end
        add(s,(quote(arg)))
        -- add(s,quote(arg))
    printh(table.concat(s," "))
end

-- quote a single thing
-- like tostr() but for tables
-- don't call this directly; call pq or qq instead
function quote(t)
    if type(t)~="table" then return tostr(t) end
    local s={}
    for k,v in pairs(t) do
        add(s,tostr(k).."="..quote(v))
    end
    return "{"..table.concat(s,",").."}"
end


As noted in Version A, the two coresume-wrapping-functions inside cocreate_spin() act differently -- I've never been able to trip the assert with the "good" version. I've tried versions of this code with no coroutines and haven't been able to trip the assert.

idk what else to say, this bug seems baffling -- sometimes select("#",quote(i)) is 2, despite quote() being a wrapper for tostr()

P#145091 2024-03-30 08:35

1

Fascinating! I was able to simplify the reproduction quite a bit and have it fail every single time (sometimes it takes 100,000+ iterations, sometimes it only takes 650 🤷‍♂️):

coro = cocreate(function()
    for i=1,10000000 do
        -- ceil() could be any function as long as it's called within a co-routine
        -- and it should ALWAYS return only one value
        local rets = pack(ceil(i))
        if #rets > 1 then
             assert(false, "\n"..table.concat(rets, "\n"))
        end
    end
end)

-- second param to coresume _sometimes_ causes errors
-- remove second param to coresume, then it works
assert(coresume(coro, "uh-oh"))

It looks like any function called within a coroutine could occasionally return the parameter(s) passed to coresume (try adding more args to the coresume(...) call - they call get returned from ceil())!

Edit: Reproduced in 0.1.0d on MacOS M1.


The equivalent code in lua 5.4.6 on my host machine (MacOS 12.4 M1) doesn't exhibit any errors:

coro = coroutine.create(function()
  for i = 1, 10000000 do
    -- ceil() could be any function as long as it's called within a co-routine
    local rets = table.pack(math.ceil(i))
    if #rets > 1 then
      assert(false, "\n" .. table.concat(rets, "\n"))
    end
  end
end)

-- never errors
assert(coroutine.resume(coro, "uh-oh"))
P#145108 2024-03-30 11:10 ( Edited 2024-03-30 11:20)

[Please log in to post a comment]