forked from ggcrunchy/solar2d-snippets
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcoroutine_ex.lua
295 lines (239 loc) · 8.5 KB
/
coroutine_ex.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
--- An extended coroutine wrapper behaves like a function returned by @{coroutine.wrap},
-- though as a loop and not a one-shot call. Once the body has completed, it will "rewind"
-- and thus be back in its original state, excepting any side effects. It is also possible
-- to query if the body has just rewound.
--
-- In addition, a coroutine created with this function can be reset, i.e. the body function
-- is explicitly rewound while active. To accommodate this, reset logic can be attached to
-- clean up any important state.
--
-- 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.
--
-- [ MIT license: http://www.opensource.org/licenses/mit-license.php ]
--
-- Standard library imports --
local assert = assert
local create = coroutine.create
local error = error
local pcall = pcall
local rawequal = rawequal
local resume = coroutine.resume
local running = coroutine.running
local status = coroutine.status
local yield = coroutine.yield
-- Modules --
local errors = require("errors")
local iterators = require("iterators")
local var_ops = require("var_ops")
local var_preds = require("var_preds")
-- Imports --
local CollectArgsInto_IfAny = var_ops.CollectArgsInto_IfAny
local GetLastTraceback = errors.GetLastTraceback
local InstancedAutocacher = iterators.InstancedAutocacher
local IsCallable = var_preds.IsCallable
local StoreTraceback = errors.StoreTraceback
local UnpackAndWipe = var_ops.UnpackAndWipe
local WipeRange = var_ops.WipeRange
-- Cached module references --
local _IsIterationDone_
local _Reset_
local _Wrap_
-- Common weak metatable --
local WeakKV = { __mode = "kv" }
-- List of running extended coroutines --
local Running = setmetatable({}, WeakKV)
-- Coroutine wrappers --
local Wrappers = setmetatable({}, WeakKV)
-- Cookies --
local _is_done = {}
local _reset = {}
local _status = {}
-- Exports --
local M = {}
--- Builds an instanced autocaching coroutine-based iterator.
-- @callable func Iterator body.
-- @callable on_reset Function called on reset; if **nil**, this is a no-op.
-- @treturn iterator Instanced iterator.
-- @see Wrap, iterators.InstancedAutocacher
function M.Iterator (func, on_reset)
return InstancedAutocacher(function()
local args, count, is_clear
local coro = _Wrap_(function()
is_clear = true
return func(UnpackAndWipe(args, count))
end, on_reset)
-- Body --
return coro,
-- Done --
function()
return _IsIterationDone_(coro)
end,
-- Setup --
function(...)
is_clear = false
count, args = CollectArgsInto_IfAny(args, ...)
end,
-- Reclaim --
function()
if not is_clear then
WipeRange(args, 1, count)
end
if not _IsIterationDone_(coro) then
_Reset_(coro)
end
end
end)
end
--- Queries a coroutine made by @{Wrap} about whether its body just ended an iteration.
-- @tparam function coro Wrapper for coroutine to query.
-- @treturn boolean The body finished, and the wrapper has not since been resumed or reset?
function M.IsIterationDone (coro)
assert(Wrappers[coro] == true, "Argument was not made with Wrap")
return coro(_is_done)
end
--- Resets a coroutine made by @{Wrap}.
--
-- If the coroutine is already reset, this is a no-op.
-- @tparam function coro Optional wrapper for coroutine to reset; if absent, uses the
-- running coroutine.
-- @param ... Reset arguments.
function M.Reset (coro, ...)
-- Figure out how to perform the reset. If the wrapper was specified or it corresponds
-- to the running coroutine, the reset cookie is yielded to the wrapper. Otherwise, do
-- a dummy resume with the cookie, which will fall through to the same logic.
local running_coro = Running[running()]
local is_suspended = coro and coro ~= running_coro
local wrapper, call
if is_suspended then
wrapper, call = assert(Wrappers[coro] == true and coro, "Cannot reset argument not made with Wrap"), coro
else
wrapper, call = assert(running_coro, "Invalid reset"), yield
end
-- If it will have any effect, trigger the reset.
if not wrapper(_is_done) then
call(_reset, ...)
end
end
--- Reports the status of a coroutine made by @{Wrap}.
-- @tparam function coro Wrapper for coroutine to query.
-- @treturn string One of the results of @{coroutine.status}, **"resetting"** during a
-- reset, or **"failed_reset"** if an error was thrown by @{Wrap}'s _on\_reset_.
-- @see Reset
function M.Status (coro)
assert(Wrappers[coro] == true, "Argument was not made with Wrap")
return coro(_status)
end
-- Default reset: no-op
local function DefaultReset () end
--- Creates an extended coroutine, exposed by a wrapper function.
-- @callable func Coroutine body.
-- @callable on_reset Function called on reset; if **nil**, this is a no-op.
--
-- Note that this will be executed in a protected call, within the context of the resetter.
-- @treturn function Wrapper function.
-- @see Reset
function M.Wrap (func, on_reset)
on_reset = on_reset or DefaultReset
-- Validate arguments and options.
assert(IsCallable(func), "Uncallable producer")
assert(IsCallable(on_reset), "Uncallable reset response")
-- Wrapper loop
local return_count, return_results = -1
local function Func (func)
while true do
return_count, return_results = CollectArgsInto_IfAny(return_results, func(yield()))
end
end
-- Handles a coroutine resume, propagating any error
-- success: If true, resume was successful
-- res_: First result of resume, or error message
-- ...: Remaining resume results
-- Returns: On success, any results
local coro
local function Resume (success, res_, ...)
Running[coro] = nil
-- On a reset, invalidate the coroutine and trigger any response.
if rawequal(res_, _reset) then
coro = false
success, res_ = pcall(on_reset, ...)
coro = nil
end
-- Propagate any error.
if not success then
if coro then
StoreTraceback(coro, res_, 2)
res_ = GetLastTraceback(true)
end
error(res_, 3)
-- Otherwise, return results if the body returned anything.
elseif return_count > 0 then
return UnpackAndWipe(return_results, return_count)
-- Otherwise, return yield (or empty return) results if no reset occurred.
elseif coro then
return res_, ...
end
end
-- Supply a wrapped coroutine.
local function wrapper (arg_, ...)
-- If queried, indicate whether the body finished an iteration and no resume /
-- reset has since occurred.
if rawequal(arg_, _is_done) then
return return_count >= 0
-- Supply the status if requested.
elseif rawequal(arg_, _status) then
if coro then
return status(coro)
else
return coro ~= nil and "resetting" or "failed_reset"
end
end
-- Validate the coroutine.
assert(coro ~= false, "Cannot resume during reset")
assert(not coro or status(coro) ~= "dead", "Dead coroutine")
assert(not Running[coro], "Coroutine already running")
-- On the first run or after / on a reset, build a fresh coroutine and put it into
-- a ready-and-waiting state.
return_count = -1
local is_resetting = rawequal(arg_, _reset)
if coro == nil or is_resetting then
coro = create(Func)
resume(coro, func)
-- On a forced reset, bypass running.
if is_resetting then
return Resume(true, _reset, ...)
end
end
-- Run the coroutine and return its results.
Running[coro] = Wrappers[Func]
return Resume(resume(coro, arg_, ...))
end
-- Store the wrapper under another key so it may reference itself without upvalues
-- (where it would become uncollectable).
Wrappers[Func] = wrapper
-- Register and return the wrapper.
Wrappers[wrapper] = true
return wrapper
end
-- Cache module members.
_IsIterationDone_ = M.IsIterationDone
_Reset_ = M.Reset
_Wrap_ = M.Wrap
-- Export the module.
return M