Skip to content

Commit 48f0ad5

Browse files
oberblastmeisterbfredl
authored andcommitted
async await using libuv (nvim-lua#83)
* started branch * added bench * added plenary. ahead * changed naming * added work future and test * fixed await_all, added more benches and tests * ntoes * more notes * added doc * added M * added some more uv functions * start of counting semaphore * more docs * use join in run_all * started branch * fixed tests * removed unneeded * small changes * async: refactor futures without object * maded naming more consistent * added argc * added argc for wrap * added argc for all functions * put in main loop * made timeout work * added runned * removed convert * added nvim future to be able to call api * added select * fixed wrong argc in select function * added block on * updated waiting time for blockon * added protect and block_on * added api helper * updated benchs for api * fixed protected * validate sender * add in_fast_event check * removed unneeded asset file * removed comment * change name to scheduler * removed idle and work related stuff for now * removed work tests and changed name to util * added scope and void * added check to condvar * removed unnecesary concats * removed long bench file * added better errors * added many docs * moved block_on and fixed oneshot channel * added async tests * updated tests and added describe it * fixed channel and added more tests * more tests * added counter channel * changed counter api and added tests * added more deque methods and tests * added mspc channel * woops forgot to commit * remove runned Co-authored-by: Björn Linse <bjorn.linse@gmail.com>
1 parent fdcb306 commit 48f0ad5

16 files changed

+1371
-0
lines changed

lua/plenary/async_lib/api.lua

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
local a = require('plenary.async_lib.async')
2+
local async, await = a.async, a.await
3+
4+
return setmetatable({}, {__index = function(t, k)
5+
return async(function(...)
6+
-- if we are in a fast event await the scheduler
7+
if vim.in_fast_event() then
8+
await(a.scheduler())
9+
end
10+
11+
vim.api[k](...)
12+
end)
13+
end})

lua/plenary/async_lib/async.lua

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
local co = coroutine
2+
local errors = require('plenary.errors')
3+
local traceback_error = errors.traceback_error
4+
5+
local M = {}
6+
7+
---@class Future
8+
---Something that will give a value when run
9+
10+
---Executes a future with a callback when it is done
11+
---@param future Future: the future to execute
12+
---@param callback function: the callback to call when done
13+
local execute = function(future, callback)
14+
assert(type(future) == "function", "type error :: expected func")
15+
local thread = co.create(future)
16+
17+
local step
18+
step = function(...)
19+
local res = {co.resume(thread, ...)}
20+
local stat = res[1]
21+
local ret = {select(2, unpack(res))}
22+
23+
if not stat then
24+
error(string.format("The coroutine failed with this message: %s", ret[1]))
25+
end
26+
27+
if co.status(thread) == "dead" then
28+
(callback or function() end)(unpack(ret))
29+
else
30+
assert(#ret == 1, "expected a single return value")
31+
local returned_future = ret[1]
32+
assert(type(returned_future) == "function", "type error :: expected func")
33+
returned_future(step)
34+
end
35+
end
36+
37+
step()
38+
end
39+
40+
---Creates an async function with a callback style function.
41+
---@param func function: A callback style function to be converted. The last argument must be the callback.
42+
---@param argc number: The number of arguments of func. Must be included.
43+
---@return function: Returns an async function
44+
M.wrap = function(func, argc)
45+
if type(func) ~= "function" then
46+
traceback_error("type error :: expected func, got " .. type(func))
47+
end
48+
49+
if type(argc) ~= "number" and argc ~= "vararg" then
50+
traceback_error("expected argc to be a number or string literal 'vararg'")
51+
end
52+
53+
return function(...)
54+
local params = {...}
55+
56+
local function future(step)
57+
if step then
58+
if type(argc) == "number" then
59+
params[argc] = step
60+
else
61+
table.insert(params, step) -- change once not optional
62+
end
63+
return func(unpack(params))
64+
else
65+
return co.yield(future)
66+
end
67+
end
68+
return future
69+
end
70+
end
71+
72+
---Return a new future that when run will run all futures concurrently.
73+
---@param futures table: the futures that you want to join
74+
---@return Future: returns a future
75+
M.join = M.wrap(function(futures, step)
76+
local len = #futures
77+
local results = {}
78+
local done = 0
79+
80+
if len == 0 then
81+
return step(results)
82+
end
83+
84+
for i, future in ipairs(futures) do
85+
assert(type(future) == "function", "type error :: future must be function")
86+
87+
local callback = function(...)
88+
results[i] = {...}
89+
done = done + 1
90+
if done == len then
91+
step(results)
92+
end
93+
end
94+
95+
future(callback)
96+
end
97+
end, 2)
98+
99+
---Returns a future that when run will select the first future that finishes
100+
---@param futures table: The future that you want to select
101+
---@return Future
102+
M.select = M.wrap(function(futures, step)
103+
local selected = false
104+
105+
for _, future in ipairs(futures) do
106+
assert(type(future) == "function", "type error :: future must be function")
107+
108+
local callback = function(...)
109+
if not selected then
110+
selected = true
111+
step(...)
112+
end
113+
end
114+
115+
future(callback)
116+
end
117+
end, 2)
118+
119+
---Use this to either run a future concurrently and then do something else
120+
---or use it to run a future with a callback in a non async context
121+
---@param future Future
122+
---@param callback function
123+
M.run = function(future, callback)
124+
future(callback or function() end)
125+
end
126+
127+
---Same as run but runs multiple futures
128+
---@param futures table
129+
---@param callback function
130+
M.run_all = function(futures, callback)
131+
M.run(M.join(futures), callback)
132+
end
133+
134+
---Await a future, yielding the current function
135+
---@param future Future
136+
---@return any: returns the result of the future when it is done
137+
M.await = function(future)
138+
assert(type(future) == "function", "type error :: expected function to await")
139+
return future(nil)
140+
end
141+
142+
---Same as await but can await multiple futures.
143+
---If the futures have libuv leaf futures they will be run concurrently
144+
---@param futures table
145+
---@return table: returns a table of results that each future returned. Note that if the future returns multiple values they will be packed into a table.
146+
M.await_all = function(futures)
147+
assert(type(futures) == "table", "type error :: expected table")
148+
return M.await(M.join(futures))
149+
end
150+
151+
---suspend a coroutine
152+
M.suspend = co.yield
153+
154+
---create a async scope
155+
M.scope = function(func)
156+
M.run(M.future(func))
157+
end
158+
159+
--- Future a :: a -> (a -> ())
160+
--- turns this signature
161+
--- ... -> Future a
162+
--- into this signature
163+
--- ... -> ()
164+
M.void = function(async_func)
165+
return function(...)
166+
async_func(...)(function() end)
167+
end
168+
end
169+
170+
---creates an async function
171+
---@param func function
172+
---@return function: returns an async function
173+
M.async = function(func)
174+
if type(func) ~= "function" then
175+
traceback_error("type error :: expected func, got " .. type(func))
176+
end
177+
178+
return function(...)
179+
local args = {...}
180+
local function future(step)
181+
if step == nil then
182+
return func(unpack(args))
183+
else
184+
execute(future, step)
185+
end
186+
end
187+
return future
188+
end
189+
end
190+
191+
---creates a future
192+
---@param func function
193+
---@return Future
194+
M.future = function(func)
195+
return M.async(func)()
196+
end
197+
198+
---An async function that when awaited will await the scheduler to be able to call the api.
199+
M.scheduler = M.wrap(vim.schedule, 1)
200+
201+
return M

lua/plenary/async_lib/init.lua

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
local exports = require('plenary.async_lib.async')
2+
exports.uv = require('plenary.async_lib.uv_async')
3+
exports.util = require('plenary.async_lib.util')
4+
exports.lsp = require('plenary.async_lib.lsp')
5+
exports.api = require('plenary.async_lib.api')
6+
exports.tests = require('plenary.async_lib.tests')
7+
8+
exports.tests.add_globals = function()
9+
a = exports
10+
async = exports.async
11+
await = exports.await
12+
await_all = exports.await_all
13+
14+
-- must prefix with a or stack overflow, plenary.test harness already added it
15+
a.describe = exports.tests.describe
16+
-- must prefix with a or stack overflow
17+
a.it = exports.tests.it
18+
end
19+
20+
exports.tests.add_to_env = function()
21+
local env = getfenv(2)
22+
23+
env.a = exports
24+
env.async = exports.async
25+
env.await = exports.await
26+
env.await_all = exports.await_all
27+
28+
-- must prefix with a or stack overflow, plenary.test harness already added it
29+
env.a.describe = exports.tests.describe
30+
-- must prefix with a or stack overflow
31+
env.a.it = exports.tests.it
32+
33+
setfenv(2, env)
34+
end
35+
36+
return exports

lua/plenary/async_lib/lsp.lua

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
local a = require('plenary.async_lib.async')
2+
3+
local M = {}
4+
5+
---Same as vim.lsp.buf_request but works with async await
6+
M.buf_request = a.wrap(vim.lsp.buf_request, 4)
7+
8+
return M

lua/plenary/async_lib/structs.lua

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
local M = {}
2+
3+
Deque = {}
4+
Deque.__index = Deque
5+
6+
---@class Deque
7+
---A double ended queue
8+
---
9+
---@return Deque
10+
function Deque.new()
11+
-- the indexes are created with an offset so that the indices are consequtive
12+
-- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
13+
return setmetatable({first = 0, last = -1}, Deque)
14+
end
15+
16+
---push to the left of the deque
17+
---@param value any
18+
function Deque:pushleft(value)
19+
local first = self.first - 1
20+
self.first = first
21+
self[first] = value
22+
end
23+
24+
---push to the right of the deque
25+
---@param value any
26+
function Deque:pushright(value)
27+
local last = self.last + 1
28+
self.last = last
29+
self[last] = value
30+
end
31+
32+
---pop from the left of the deque
33+
---@return any
34+
function Deque:popleft()
35+
local first = self.first
36+
if first > self.last then return nil end
37+
local value = self[first]
38+
self[first] = nil -- to allow garbage collection
39+
self.first = first + 1
40+
return value
41+
end
42+
43+
---pops from the right of the deque
44+
---@return any
45+
function Deque:popright()
46+
local last = self.last
47+
if self.first > last then return nil end
48+
local value = self[last]
49+
self[last] = nil -- to allow garbage collection
50+
self.last = last - 1
51+
return value
52+
end
53+
54+
---checks if the deque is empty
55+
---@return boolean
56+
function Deque:is_empty()
57+
return self:len() == 0
58+
end
59+
60+
---returns the number of elements of the deque
61+
---@return number
62+
function Deque:len()
63+
return self.last - self.first + 1
64+
end
65+
66+
---returns and iterator of the indices and values starting from the left
67+
---@return function
68+
function Deque:ipairs_left()
69+
local i = self.first
70+
71+
return function()
72+
local res = self[i]
73+
local idx = i
74+
75+
if res then
76+
i = i + 1
77+
78+
return idx, res
79+
end
80+
end
81+
end
82+
83+
---returns and iterator of the indices and values starting from the right
84+
---@return function
85+
function Deque:ipairs_right()
86+
local i = self.last
87+
88+
return function()
89+
local res = self[i]
90+
local idx = i
91+
92+
if res then
93+
i = i - 1 -- advance the iterator before we return
94+
95+
return idx, res
96+
end
97+
end
98+
end
99+
100+
---removes all values from the deque
101+
---@return nil
102+
function Deque:clear()
103+
for i, _ in self:ipairs_left() do
104+
self[i] = nil
105+
end
106+
self.first = 0
107+
self.last = -1
108+
end
109+
110+
M.Deque = Deque
111+
112+
return M

lua/plenary/async_lib/tests.lua

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
local a = require('plenary.async_lib.async')
2+
local util = require('plenary.async_lib.util')
3+
4+
local M = {}
5+
6+
M.describe = function(s, func)
7+
describe(s, util.will_block(a.future(func)))
8+
end
9+
10+
M.it = function(s, func)
11+
it(s, util.will_block(a.future(func)))
12+
end
13+
14+
return M

0 commit comments

Comments
 (0)