Browse Source

Initial commit

master
luk3yx 3 weeks ago
commit
0253561e51
9 changed files with 1121 additions and 0 deletions
  1. 22
    0
      LICENSE.md
  2. 302
    0
      README.md
  3. 260
    0
      core.lua
  4. 12
    0
      init.lua
  5. 110
    0
      locks.lua
  6. 69
    0
      lua51.lua
  7. 1
    0
      mod.conf
  8. 89
    0
      test.lua
  9. 256
    0
      transpile.lua

+ 22
- 0
LICENSE.md View File

@@ -0,0 +1,22 @@

# The MIT License (MIT)

Copyright © 2019 by luk3yx.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 302
- 0
README.md View File

@@ -0,0 +1,302 @@
# Minetest asyncio

A lua coroutine wrapper thing. Inspired by Python's
[asyncio](https://docs.python.org/3/library/asyncio.html). As with Python's
asyncio, coroutines run in one thread and must voluntarily give up control.

*Note that this doesn't come with I/O-related functions, it is just called
`asyncio` to not conflict with the similar-but-different async mod.*

## Example code

### "Plain" lua

```lua
local function iter(a)
local stage
return function()
if stage == 0 then
stage = 1
return 'Iteration started.'
elseif stage == 2 then
return
elseif a < 100 then
a = a * 2
return a
else
stage = 2
return 'Finished iterating.'
end
end
end

function itertest(a)
for i in iter(a) do
print('Iterator test: ' .. i)
end
end

print('Non-async code started, deferring...')
minetest.after(0, function()
print('Non-async code resumed, using iterator.')
itertest(1)
print('Finished iterating, waiting 1 second.')
minetest.after(1, function()
print('Waiting another two seconds.')
minetest.after(2, function()
print('Done!')
end)
end)
end)
```

### "Plain" lua syntax with asyncio

```lua
local iter = asyncio.coroutine(function(a)
asyncio.yield('Iteration started.')
while a < 100 do
a = a * 2
asyncio.yield(a)
end
asyncio.yield('Finished iterating.')
end)

itertest = asyncio.coroutine(function(a)
for i in asyncio.iter(iter, a) do
print('Iterator test: ' .. i)
end
end)

asyncio.run(function()
print('async code started, deferring...')
asyncio.defer()
print('async code resumed, using async iterator')
itertest(1)
print('Finished iterating, waiting 1 second.')
asyncio.sleep(1)
print('Waiting another two seconds without asyncio.sleep.')
asyncio.await(minetest.after(2, asyncio.resume))
print('Done!')
end)
```

### Entirely asyncio lua

```lua
local async function iter(a)
asyncio.yield('Iteration started.')
while a < 100 do
a = a * 2
asyncio.yield(a)
end
asyncio.yield('Finished iterating.')
end

async function itertest(a)
async for i in iter(a) do
print('Iterator test: ' .. i)
end
end

print('async code started, deferring...')
asyncio.defer()
print('async code resumed, using async iterator')
itertest(1)
print('Finished iterating, waiting 1 second.')
asyncio.sleep(1)
print('Waiting another two seconds without asyncio.sleep.')
asyncio.await(minetest.after(2, asyncio.resume))
print('Done!')
```

## How it works

This mod is a lightweight-ish wrapper around
[lua coroutines](https://www.lua.org/pil/9.1.html). The `async function` and
`async for` syntax can only be used with `asyncio.dofile` and is run through a
primitive transpiler(?) that converts the syntax into valid lua.

## API

The following functions are available both inside and outside of coroutines:
- `asyncio.run(function(...))`: Executes a function inside an asyncio
coroutine.
- `asyncio.coroutine(function(...))`: A function wrapper that runs the function
inside a coroutine.
- `asyncio.exec(...)`: Similar to `asyncio.loadstring(...)()`.

`asyncio.loadstring`, `asyncio.loadfile`, and `asyncio.dofile` work the same
way as their built-in counterparts, however these functions also accept
`async function` and `async for` syntax, and the code itself is executed inside
a coroutine.

The following functions only work inside coroutines:
- `asyncio.await(...)`: Suspends execution of the current coroutine until
`asyncio.resume` is called. If `asyncio.resume` is never called, the
coroutine will be garbage collected by lua.
Example: `asyncio.await(function_with_callback(params, asyncio.resume))`
- `asyncio.resume`: This should not be called inside the coroutine itself,
instead used as a callback function. `asyncio.resume` changes depending on
what coroutine is running, and is `nil` outside of coroutines.
- `asyncio.sleep(delay)`: Suspends execution of the current coroutine for
`delay` seconds. Internally, this just calls
`asyncio.await(minetest.after(delay, asyncio.resume))`.
- `asyncio.defer()`: Defers execution of this coroutine until the next
globalstep.
- `asyncio.optional_defer()`: Allows asyncio to defer, however leaves the
decision to actually defer up to asyncio. Currently, it will only call
`defer()` if it has been over 5ms since the previous "successful"
`optional_defer()`, however this may change in the future. Returns `true`
if `asyncio.defer()` was actually called, otherwise `false`.
- `asyncio.odefer()`: Alias for `asyncio.optional_defer`.
- `asyncio.iter(func, ...)`: Calls `func` as an asyncio iterator.
- `asyncio.pairs(obj, defer_every)`: Similar to `pairs()`, but calls
`asyncio.defer()` every `defer_every` iterations. Must be called in an
`async for` loop or with `asyncio.iter()`. If `defer_every` is unspecified,
`asyncio.optional_defer()` is called on every iteration instead.
- `asyncio.ipairs(obj, defer_every)`: The same as `asyncio.pairs`, but with
`ipairs`.
- `asyncio.yield(...)`: Used to yield values in asyncio iterators. This
function is slightly different from `coroutine.yield` and should not be
used interchangeably with it.
- `asyncio.running`: A variable, set to `true` when inside a coroutine. Do not
manually set this, or it will break.
- `asyncio.run_async(function(...))`: Runs `function(...)` in a separate
coroutine, so it does not block the current one.
- `asyncio.domodfile(file)`: Calls something similar to
`asyncio.dofile(current_modpath .. '/' .. file)`.

## Custom lua syntax

The custom lua syntax introduced by this mod is "transpiled" back into regular
lua, and can only be used with `asyncio.dofile` etc.

## Coroutines

asyncio coroutines can be created in one of two ways:

```lua
func1 = asyncio.coroutine(function(a, b, c))
return a + 1, b + 1, c + 1
end)

async function func2(a, b, c)
return a + 1, b + 1, c + 1
end
```

The `async function` sytnax will only work in lua code loaded with
`asyncio.dofile` etc.

The return value of coroutine functions will be lost if they are called from
other non-coroutine functions.

*You don't have to `await` coroutines from other coroutines, and calling
coroutines from non-asynchronous code will work correctly.*

## Iterators

asyncio iterators can only be used inside coroutines.

```lua
for value in asyncio.iter(iterable_function, parameters) do
print(value)
end

-- "async for" is recommended over asyncio.iter and should be used where
-- possible.
async for value in iterable_function(parameters) do
print(value)
end
```

Without LuaJIT, because of coroutine limitations in Lua 5.1, `asyncio.iter`
calculates and stores all iteration values before starting the loop. This
greatly reduces performance, especially when using `asyncio.pairs` or
`asyncio.ipairs`. `async for` does not have this limitation, however, and is
therefore recommended over `asyncio.iter`.

```lua
async for k, v in asyncio.pairs(minetest.registered_items) do
-- Some resource-intensive thing here.
end
```

*For another iterator example, see [here](#entirely-asyncio-lua).*

## HTTP(S) requests

As to not circumvent mod security, there are no asynchronous HTTP fetch
functions, and there are no helpers to create fetch functions.

To send an HTTP(S) request from an asyncio coroutine, you can call
`asyncio.await(http.fetch(req, asyncio.resume))`, where `http` is the name of
the [`HTTPApiTable`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt#L4746). This will return the response table.

Alternatively, if you are calling `http.fetch` multiple times, it may be easier
to define an asyncio HTTP fetch function:

```lua
local function http_fetch(req)
return asyncio.await(http.fetch(req, asyncio.resume))
end
```

## `wait until`

asyncio introduces a `wait until <condition>` statement, that checks
`condition` every globalstep until it evaluates to `true`, and then continues
executing the thread.

**Do not use `wait until` unless there is no other way, it can and will create
lag.**

## Locks and events

asyncio adds `asyncio.Lock`s and `asyncio.Event`s.

These are similar to
[Python's asyncio locks](https://docs.python.org/3/library/asyncio-sync.html).

Example:

```lua
local lock = asyncio.Lock()
async function do_something()
print('Acquiring lock...')
lock:acquire()
print('Acquired the lock, doing something...')
asyncio.sleep(5)
print('Releasing the lock...')
lock:release()
end

for i = 1, 5 do
asyncio.run_async(do_something)
end

-- Wait for the above functions to finish.
lock:acquire()
lock:release()

-- Events
local event = asyncio.Event()
async function do_something_else()
print('Waiting on the event...')
event:wait()
print('Event triggered, waiting 5 seconds.')
asyncio.sleep(5)
print('Done.')
end

for i = 1, 5 do
asyncio.run_async(do_something_else)
end

-- Set the event
asyncio.sleep(5)
print('Running event:set()...')
event:set()
print('Finished running event:set().')
```

+ 260
- 0
core.lua View File

@@ -0,0 +1,260 @@
--
-- 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

-- 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

+ 12
- 0
init.lua View File

@@ -0,0 +1,12 @@
--
-- Minetest asyncio-style thing
--

asyncio = {}

local modpath = minetest.get_modpath('asyncio')
dofile(modpath .. '/core.lua')
dofile(modpath .. '/transpile.lua')

asyncio.domodfile('locks.lua')
-- asyncio.domodfile('test.lua')

+ 110
- 0
locks.lua View File

@@ -0,0 +1,110 @@
--
-- asyncio synchronisation primitives.
--

--
-- Locks
--

-- Store lock metadata locally
local lock_states = {}
local lock_callbacks = {}

setmetatable(lock_states, {__mode = 'kv'})
setmetatable(lock_callbacks, {__mode = 'k'})

-- Create the functions
-- https://docs.python.org/3/library/asyncio-sync.html#lock
local lock = {}
function lock:acquire()
if not asyncio.running then
error('lock:acquire() called outside of a coroutine.')
elseif not lock_callbacks[self] then
lock_callbacks[self] = {}
end

if lock_states[self] then
asyncio.await(table.insert(lock_callbacks[self], asyncio.resume))
elseif #lock_callbacks[self] > 0 then
table.remove(lock_callbacks[self], 1)()
asyncio.await(table.insert(lock_callbacks[self], asyncio.resume))
end
lock_states[self] = coroutine.running()
end

function lock:release()
if not lock_states[self] then error('Lock is not acquired.', 2) end
lock_states[self] = nil

-- lock_callbacks[self] should never be nil.
assert(lock_callbacks[self])

-- Call the next function.
local func = table.remove(lock_callbacks[self], 1)
if func then func() end
end

function lock:locked()
return lock_states[self] and true or false
end

function asyncio.Lock()
return {
acquire = lock.acquire,
release = lock.release,
locked = lock.locked,
}
end

--
-- Events
--

local event_callbacks = {}
setmetatable(event_callbacks, {__mode = 'k'})

local event = {}

function event:wait()
if not asyncio.running then
error('event:wait() called outside of a coroutine.')
elseif event_callbacks[self] then
asyncio.await(table.insert(event_callbacks[self], asyncio.resume))
end
return true
end

-- Automatically defer execution when running event:set()
async function event:set()
local funcs = event_callbacks[self]
event_callbacks[self] = nil
if not funcs then return end

async for _, func in asyncio.ipairs(funcs) do
-- If event:set() is re-called, re-add any unhandled coroutines.
if event_callbacks[self] then
table.insert(event_callbacks[self], 1, func)
else
func()
end
end
end

function event:clear()
if not event_callbacks[self] then event_callbacks[self] = {} end
end

function event:is_set()
return not event_callbacks[self]
end

function asyncio.Event()
local res = {
wait = event.wait,
set = event.set,
clear = event.clear,
is_set = event.is_set,
}
event_callbacks[res] = {}
return res
end

+ 69
- 0
lua51.lua View File

@@ -0,0 +1,69 @@
--
-- Minetest asyncio: Lua 5.1 workarounds
--


local rawequal = rawequal
minetest.log('warning', '[asyncio] LuaJIT not installed, asyncio.iter() ' ..
'will be less efficient.')

-- Keep the original asyncio.iter accessible, this is used in the workaround
-- iter function and in the transpiler.
asyncio._iter_start = asyncio.iter

-- Helper functions
local function pack_len(...)
local res = {...}
res.length = select('#', ...)
return res
end

local function unpack_len(t, i)
i = i or 1
if i <= t.length then
return t[i], unpack_len(t, i + 1)
end
end

-- A workaround for non-LuaJIT Lua 5.1
-- Lua 5.1 does not allow coroutine.yield() inside iterators.
function asyncio.iter(func, ...)
local iter = asyncio._iter_start(func, ...)

-- Fetch all values first
local t = {}
while true do
local msg = pack_len(iter())
if msg[1] == nil then break end
table.insert(t, msg)
end
iter = nil

-- Get the values
return function()
local msg = table.remove(t, 1)
if type(msg) == 'table' then
return unpack_len(msg)
end
end
end

-- A pcall() workaround/hack.
local last_error = ...
assert(type(last_error) == 'table')
local function pcall_hack(func, ...)
asyncio.yield(last_error, pack_len(func(...)))
end

function last_error.cleanup() last_error.msg = nil end
local old_pcall = pcall
function pcall(func, ...)
if not asyncio.running then return old_pcall(func, ...) end
local l, msg = asyncio._iter_start(pcall_hack, func, ...)()
if l == last_error then
return true, unpack_len(msg)
elseif l ~= nil then
return false, 'asyncio.yield() called outside of async iterator.'
end
return false, last_error.msg
end

+ 1
- 0
mod.conf View File

@@ -0,0 +1 @@
name = asyncio

+ 89
- 0
test.lua View File

@@ -0,0 +1,89 @@
-- Testing
local async function iter(i)
asyncio.yield(0)
for i = 1, 5 do
print('Waiting 1 second because iterators')
asyncio.sleep(1)
print('Finished waiting, yield-ing ' .. i .. '...')
asyncio.yield('#' .. i)
end

print('Finished iterating!')
end

print('Hello from the test file, waiting for other mods to load...')
asyncio.defer()
print('Done, starting iteration.')

async for i, _ in iter(1) do
print('Hello from the asyncio code!')
print('Got ' .. i)
end

print('Iterating over all items')
async for k, _ in asyncio.pairs(minetest.registered_items) do
print('Heavy operation:', k)
end
print('Done')

minetest.register_chatcommand('/alua', {
privs = {server=true},
help = 'Executes asyncio lua commands.',

-- This function has to use minetest.chat_send_player as it is executed
-- asynchronously and return values aren't sent to non-asynchronous
-- functions.
func = async function(name, param)
local func, msg = asyncio.loadstring(param)
if not func then
minetest.chat_send_player(name, 'Load error: ' .. tostring(msg))
return
end
local good, msg = pcall(func)
if good then
minetest.chat_send_player(name, 'Code executed.')
else
minetest.chat_send_player(name, 'Error executing code: ' ..
tostring(msg))
end
end,
})

-- More testing
local lock = asyncio.Lock()
local async function do_something()
print('Acquiring lock...')
lock:acquire()
print('Acquired the lock, doing something...')
asyncio.sleep(5)
print('Releasing the lock...')
lock:release()
end

for i = 1, 5 do
asyncio.run_async(do_something)
end

-- Wait for the above functions to finish.
lock:acquire()
lock:release()

-- Events
local event = asyncio.Event()
local async function do_something_else()
print('Waiting on the event...')
event:wait()
print('Event triggered, waiting 5 seconds.')
asyncio.sleep(5)
print('Done.')
end

for i = 1, 5 do
asyncio.run_async(do_something_else)
end

-- Set the event
asyncio.sleep(5)
print('Running event:set()...')
event:set()
print('Finished running event:set().')

+ 256
- 0
transpile.lua View File

@@ -0,0 +1,256 @@
--
-- A primitive code transpiler
--

-- Find multiple patterns
local function find_multiple(text, ...)
local n = select('#', ...)
local s, e, pattern
for i = 1, n do
local p = select(i, ...)
local s2, e2 = text:find(p)
if s2 and (not s or s2 < s) then
s, e, pattern = s2, e2 or s2, p
end
end
return s, e, pattern
end

-- Matches
-- These take 2-3 arguments (code, res, char) and should return code and res.
local matches
matches = {
-- Handle multi-line strings and comments
['%[=*%['] = function(code, res, char)
res = res .. char
if char:sub(1, 2) == '--' then
char = char:sub(4, -2)
else
char = char:sub(2, -2)
end
local s, e = code:find(']' .. char .. ']', nil, true)
if not s or not e then return code, res end
return code:sub(e + 1), res .. code:sub(1, e)
end,

-- Handle regular comments
['--'] = function(code, res, char)
res = res .. char
local s, e = code:find('\n', nil, true)
if not s or not e then return code, res end
return code:sub(e + 1), res .. code:sub(1, e)
end,

-- Handle quoted text
['"'] = function(code, res, char)
res = res .. char

-- Handle backslashes
repeat
local s, e, pattern = find_multiple(code, '\\', char)
if pattern then
res = res .. code:sub(1, e + 1)
code = code:sub(e + 2)
end
until not pattern or pattern == char

return code, res
end,

-- Handle "async function" statements
['async function'] = function(code, res, char)
if not code:find('end', nil, true) then return code, res .. char end
local s = code:find(')', nil, true)
if not s then return code, res .. char end
if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end

local params = code:match('^%s*%(([^%)]*)%)')
if params then
res = res .. 'asyncio.coroutine(function(' .. params
else
-- Check for method functions
local name, params = code:match('^%s*([%w_%.]+)%s*%(([^%)]*)%)')
if not name or not params then
local n, n2, p = code:match(
'^%s*([%w_%.]+)%:([%w_]+)%s*%(([^%)]*)%)')
if not n or not n2 or not p then
return code, res .. char
end
name = n .. '.' .. n2
if p:find('%S') then
params = 'self, ' .. p
else
params = 'self'
end
end

-- Local functions
if res:sub(-6, -2) == 'local' then res = res .. name .. '; ' end

-- Add the first part
res = res .. name .. ' = asyncio.coroutine(function(' .. params
end
code = code:sub(s)

-- Search for the "end"
if code:sub(#code) == 'd' then code = code .. '\n' end
local lvl = 1
while lvl > 0 do
local s, e, pattern = find_multiple(code, '[\'"\\]', '%-%-%[=*%[',
'%-%-', '%[=*%[', '^async function ', '[%s;%[%(]async function',
'^async for ', '[%s;%[]async for ', '^wait until ',
'[%s;%[]wait until ', '%sdo%s', '[%s%)]then%s',
'[%s%(%)]function[%s%(]', '%send[%s%),]', '^end[%s%),]')
if not s then return code .. ')', res end

-- Add non-matching characters
res = res .. code:sub(1, math.max(s - 1, 0))

local char = code:sub(s, e)
if pattern == '%sdo%s' or pattern == '[%s%(%)]function[%s%(]' or
pattern == '[%s%)]then%s' then
lvl = lvl + 1
res = res .. char
code = code:sub(e + 1)
elseif pattern == '%send[%s%),]' or pattern == '^end[%s%),]' then
lvl = lvl - 1
res = res .. char
code = code:sub(e + 1)
else
-- Call the correct function
local func = matches[char] or matches[pattern]
assert(func, 'No function found for pattern!')
code, res = func(code:sub(e + 1), res, char)
end
end

return code, res:sub(1, -2) .. ')' .. res:sub(-1)
end,

-- Handle "async for" statements
['async for '] = function(code, res, char)
if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end
local var, func, n = code:match(
'^([%w_,%s]+)%s+in%s*([%w_%.]+)%s*%(%s*(%S)')
if not var or not func then return code, res .. 'for ' end
local s = code:find('(', nil, true)
code = code:sub(s + 1)
res = res .. 'for ' .. var .. ' in asyncio.iter(' .. func
if n ~= ')' then res = res .. ', ' end
return code, res
end,

['wait until '] = function(code, res, char)
if char:sub(1, 1) ~= 'w' then res = res .. char:sub(1, 1) end
return code, res .. 'repeat asyncio.defer() until '
end,
}

-- A workaround for Lua 5.1's iterator limitations.
if not asyncio or asyncio.lua51 then
local orig = matches['async for ']
matches['async for '] = function(code, res, char)
if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end
local var, func, params = code:match(
'^([%w_,%s]+)%s+in%s*([%w_%.]+)%s*%(%s*([^%(%)]*%))')

-- TODO: Handle everything
local s, e = code:find('%)%s*do')
if not var or not func or not params or params:sub(-1) ~= ')' or
not e then
print('Not handling', var, func, params)
return orig(code, res, 'async for ')
end

code = code:sub(e + 1)

-- Generate code
s = var:find(',', nil, true)
local i, v = '__asyncio_lua51_iter_shim', var:sub(1, (s or 2) - 1)
if params ~= ')' then params = ', ' .. params end
res = res .. 'local ' .. i .. ' = asyncio._iter_start(' .. func ..
params .. '; while true do local ' .. var .. ' = ' .. i ..
'(); if ' .. v .. ' == nil then break end;'

return code, res
end
end

-- Give the functions alternate names
matches['%-%-%[=*%['], matches["'"] = matches['%[=*%['], matches['"']
matches['[%s;%[%(]async function'] = matches['async function']
matches['[%s;%[]async for '] = matches['async for ']
matches['[%s;%[]wait until '] = matches['wait until ']

-- The actual transpiler
local function transpile(code)
assert(type(code) == 'string')

local res = ''

-- Split the code by "tokens"
while true do
-- Search for special characters
local s, e, pattern = find_multiple(code, '[\'"\\]', '%-%-%[=*%[',
'%-%-', '%[=*%[', '^async function', '[%s;%[%(]async function',
'^async for ', '[%s;%[]async for ', '^wait until ',
'[%s;%[]wait until ')
if not s then break end

-- Add non-matching characters
res = res .. code:sub(1, math.max(s - 1, 0))

-- Call the correct function
local char = code:sub(s, e)
local func = matches[char] or matches[pattern]
assert(func, 'No function found for pattern!')
code, res = func(code:sub(e + 1), res, char)
end

return res .. code
end

-- Testing
if not asyncio then
local f = io.open('test.lua')
print(transpile(f:read '*a'))

return transpile
end

-- Lua 5.2+ compatibility
local loadstring = assert(rawget(_G, 'loadstring') or load)

-- Load strings
function asyncio.loadstring(code, ...)
local f, msg = loadstring(transpile(code), ...)
return f and asyncio.coroutine(f), msg
end

-- Load strings from files
function asyncio.loadfile(file)
local f, msg = io.open(file, 'r')
if not f then return nil, msg end
return asyncio.loadstring(f:read('*a'), '@' .. file)
end

-- Execute files
function asyncio.dofile(file)
local func, msg = asyncio.loadfile(file)
if not func then error(msg, 0) end
return func()
end

-- Execute a string
function asyncio.exec(...)
local func, msg = asyncio.loadstring(...)
if not func then error(msg, 2) end
return func()
end

-- Load a file in the current mod
local DIR_DELIM = rawget(_G, 'DIR_DELIM') or '/'
function asyncio.domodfile(file)
local prefix = minetest.get_modpath(minetest.get_current_modname())
return asyncio.dofile(prefix .. DIR_DELIM .. file)
end

Loading…
Cancel
Save