262 lines
6.9 KiB
Lua
262 lines
6.9 KiB
Lua
--
|
|
-- Minetest asyncio: Core
|
|
--
|
|
|
|
local coroutines = {}
|
|
local iterators = {}
|
|
|
|
-- Make coroutines and iterators "weak tables".
|
|
setmetatable(coroutines, {__mode = 'k'})
|
|
setmetatable(iterators, {__mode = 'kv'})
|
|
|
|
-- Resume a coroutine
|
|
local last_error
|
|
local function resume(co, ...)
|
|
local good, msg = coroutine.resume(co, ...)
|
|
local status = coroutine.status(co)
|
|
|
|
if status == 'dead' then coroutines[co], iterators[co] = nil, nil end
|
|
|
|
if not good then
|
|
assert(status == 'dead')
|
|
if last_error then
|
|
last_error.msg = msg
|
|
minetest.after(0, last_error.cleanup)
|
|
end
|
|
minetest.log('error', 'Error in coroutine: ' .. tostring(msg))
|
|
for _, line in ipairs(debug.traceback(co):split('\n')) do
|
|
minetest.log('error', line)
|
|
end
|
|
elseif iterators[co] then
|
|
if msg == nil then return false end
|
|
return msg
|
|
end
|
|
end
|
|
|
|
-- Run coroutines directly
|
|
local function run(func, ...)
|
|
local thread = coroutine.running()
|
|
if coroutines[thread] then
|
|
return func(...)
|
|
end
|
|
local co = coroutine.create(func)
|
|
coroutines[co] = true
|
|
resume(co, ...)
|
|
end
|
|
|
|
function asyncio.run(func, ...)
|
|
if type(func) ~= 'function' then
|
|
error('Invalid function passed to asyncio.run.', 2)
|
|
end
|
|
return run(func, ...)
|
|
end
|
|
|
|
-- Wrap a function
|
|
function asyncio.coroutine(func)
|
|
if type(func) ~= 'function' then
|
|
error('Invalid function passed to asyncio.coroutine.', 2)
|
|
end
|
|
|
|
return function(...)
|
|
return run(func, ...)
|
|
end
|
|
end
|
|
|
|
-- Run a coroutine in a separate "thread"
|
|
function asyncio.run_async(func, ...)
|
|
local co = coroutine.create(func)
|
|
coroutines[co] = true
|
|
resume(co, ...)
|
|
end
|
|
|
|
-- Make sure functions are being run inside coroutines.
|
|
local function get_coroutine()
|
|
local co = coroutine.running()
|
|
if not coroutines[co] then
|
|
error('asyncio function called outside coroutine.', 3)
|
|
end
|
|
return co
|
|
end
|
|
|
|
-- This table is used internally for iterators
|
|
local suspend = {}
|
|
function asyncio.await()
|
|
get_coroutine()
|
|
return coroutine.yield(suspend)
|
|
end
|
|
|
|
-- Create asyncio.resume on-the-fly when required.
|
|
setmetatable(asyncio, {__index = function(_, name)
|
|
if name == 'resume' then
|
|
local co = coroutine.running()
|
|
if not coroutines[co] then return end
|
|
while iterators[co] do co = iterators[co] end
|
|
return function(...)
|
|
if coroutines[co] then
|
|
resume(co, ...)
|
|
end
|
|
end
|
|
elseif name == 'running' then
|
|
return coroutines[coroutine.running()] and true or false
|
|
end
|
|
end})
|
|
|
|
-- Wait a specified amount of seconds before continuing.
|
|
function asyncio.sleep(delay)
|
|
if type(delay) ~= 'number' or delay ~= delay or delay >= math.huge then
|
|
error('Invalid asyncio.sleep() invocation.', 2)
|
|
end
|
|
|
|
asyncio.await(minetest.after(delay, asyncio.resume))
|
|
end
|
|
|
|
-- Defer execution until the next globalstep.
|
|
function asyncio.defer()
|
|
asyncio.await(minetest.after(0, asyncio.resume))
|
|
end
|
|
|
|
-- Only defer if it has been 5ms since the last optional_defer()
|
|
local next_defer = minetest.get_us_time()
|
|
function asyncio.optional_defer()
|
|
if minetest.get_us_time() > next_defer then
|
|
asyncio.defer()
|
|
next_defer = minetest.get_us_time() + 5000
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
asyncio.odefer = asyncio.optional_defer
|
|
|
|
-- Pack and unpack using nil
|
|
local function pack_len(...)
|
|
return select('#', ...), {...}
|
|
end
|
|
|
|
local function unpack_len(length, t, i)
|
|
i = i or 1
|
|
if i <= length then
|
|
return t[i], unpack_len(length, t, i + 1)
|
|
end
|
|
end
|
|
|
|
-- asyncio.yield returns a table
|
|
function asyncio.yield(...)
|
|
if not iterators[get_coroutine()] then
|
|
error('asyncio.yield() called outside of async iterator.', 2)
|
|
end
|
|
|
|
local l, t = pack_len(...)
|
|
t.length = l
|
|
coroutine.yield(t)
|
|
end
|
|
|
|
-- Iterate over a string
|
|
local function str_iter(s)
|
|
for n = 1, #s do
|
|
asyncio.yield(n, s:sub(n, n))
|
|
end
|
|
end
|
|
|
|
-- The standard iteration function (LuaJIT, Lua 5.2+)
|
|
local rawequal = rawequal
|
|
local function iter(func, ...)
|
|
local co = get_coroutine()
|
|
if type(func) == 'string' and select('#', ...) == 0 then
|
|
return iter(str_iter, func)
|
|
elseif type(func) ~= 'function' then
|
|
error('Invalid asyncio.iter() invocation.')
|
|
end
|
|
|
|
local iterco = coroutine.create(func)
|
|
coroutines[iterco], iterators[iterco] = true, co
|
|
local l, m = pack_len(...)
|
|
|
|
return function()
|
|
if iterco == nil then return end
|
|
assert(get_coroutine() == co)
|
|
local msg = resume(iterco, unpack_len(l, m))
|
|
l, m = 0, nil
|
|
|
|
-- Suspend the parent coroutine if required
|
|
while msg and rawequal(msg, suspend) do
|
|
msg = resume(iterco, coroutine.yield(suspend))
|
|
end
|
|
|
|
-- Unpack asyncio.yield()s
|
|
if type(msg) == 'table' and type(msg.length) == 'number' then
|
|
if msg[1] == nil then
|
|
coroutines[iterco], iterators[iterco] = nil, nil
|
|
iterco = nil
|
|
return
|
|
end
|
|
return unpack_len(msg.length, msg)
|
|
end
|
|
|
|
-- Return the result
|
|
if msg == nil then
|
|
coroutines[iterco], iterators[iterco], iterco = nil, nil, nil
|
|
end
|
|
return msg
|
|
end
|
|
end
|
|
asyncio.iter = iter
|
|
asyncio._iter_start = iter
|
|
|
|
-- Lua 5.1 (excluding LuaJIT) compatibility
|
|
asyncio.lua51 = _VERSION < 'Lua 5.2'
|
|
if asyncio.lua51 then
|
|
asyncio.lua51 = not pcall(coroutine.wrap(function()
|
|
for i in coroutine.yield do return end
|
|
end))
|
|
end
|
|
|
|
if asyncio.lua51 then
|
|
last_error = {}
|
|
assert(loadfile((minetest.get_modpath('asyncio') ..
|
|
'/lua51.lua')))(last_error)
|
|
end
|
|
|
|
-- Defer pairs() and ipairs() until the next globalstep
|
|
function asyncio.pairs(n, defer_every)
|
|
if not iterators[get_coroutine()] then
|
|
error('asyncio.pairs() called outside of an async iterator.', 2)
|
|
end
|
|
|
|
if type(defer_every) == 'number' and defer_every == defer_every and
|
|
defer_every > 1 then
|
|
local i = 0
|
|
for k, v in pairs(n) do
|
|
i = i + 1
|
|
if i >= defer_every then asyncio.defer(); i = 0 end
|
|
asyncio.yield(k, v)
|
|
end
|
|
else
|
|
for k, v in pairs(n) do
|
|
asyncio.optional_defer()
|
|
asyncio.yield(k, v)
|
|
end
|
|
end
|
|
end
|
|
|
|
function asyncio.ipairs(n, defer_every)
|
|
if not iterators[get_coroutine()] then
|
|
error('asyncio.ipairs() called outside of an async iterator.', 2)
|
|
end
|
|
|
|
if type(defer_every) == 'number' and defer_every == defer_every and
|
|
defer_every > 1 then
|
|
asyncio.defer()
|
|
defer_every = math.ceil(defer_every)
|
|
for k, v in ipairs(n) do
|
|
if k % defer_every == 0 then asyncio.defer() end
|
|
asyncio.yield(k, v)
|
|
end
|
|
else
|
|
-- Defer every 5ms
|
|
for k, v in ipairs(n) do
|
|
asyncio.optional_defer()
|
|
asyncio.yield(k, v)
|
|
end
|
|
end
|
|
end
|