diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2024-03-06 09:56:45 -0500 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2024-03-06 09:56:45 -0500 |
commit | c621fc39fc4ac25482fbc1090b8067c4187de176 (patch) | |
tree | ad04aa2f0658cbf1b2ee0cce1b7aede826e7d2ed /indra | |
parent | b4bef56b20fbeaaea60b20bd84d1569ed76cf29b (diff) |
WIP: Unfinished Queue.lua, WaitQueue.lua, ErrorQueue.lua, leap.lua.
Also qtest.lua to exercise the queue classes and inspect.lua (from
https://github.com/kikito/inspect.lua) for debugging.
Diffstat (limited to 'indra')
-rw-r--r-- | indra/newview/scripts/lua/ErrorQueue.lua | 32 | ||||
-rw-r--r-- | indra/newview/scripts/lua/Queue.lua | 41 | ||||
-rw-r--r-- | indra/newview/scripts/lua/WaitQueue.lua | 68 | ||||
-rw-r--r-- | indra/newview/scripts/lua/inspect.lua | 371 | ||||
-rw-r--r-- | indra/newview/scripts/lua/leap.lua | 284 | ||||
-rw-r--r-- | indra/newview/scripts/lua/qtest.lua | 47 |
6 files changed, 823 insertions, 20 deletions
diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua new file mode 100644 index 0000000000..e6279f8411 --- /dev/null +++ b/indra/newview/scripts/lua/ErrorQueue.lua @@ -0,0 +1,32 @@ +-- ErrorQueue isa WaitQueue with the added feature that a producer can push an +-- error through the queue. When that error is dequeued, the consumer will +-- throw that error. + +local WaitQueue = require('WaitQueue') + +ErrorQueue = WaitQueue:new() + +function ErrorQueue:Enqueue(value) + -- normal value, not error + WaitQueue:Enqueue({ false, value }) +end + +function ErrorQueue:Error(message) + -- flag this entry as an error message + WaitQueue:Enqueue({ true, message }) +end + +function ErrorQueue:Dequeue() + local errflag, value = table.unpack(WaitQueue:Dequeue()) + if errflag == nil then + -- queue has been closed, tell caller + return nil + end + if errflag then + -- 'value' is a message pushed by Error() + error(value) + end + return value +end + +return ErrorQueue diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua index e178ad9969..b0a5a87f87 100644 --- a/indra/newview/scripts/lua/Queue.lua +++ b/indra/newview/scripts/lua/Queue.lua @@ -1,40 +1,41 @@ --- from https://create.roblox.com/docs/luau/queues#implementing-queues +-- from https://create.roblox.com/docs/luau/queues#implementing-queues, +-- amended per https://www.lua.org/pil/16.1.html local Queue = {} -Queue.__index = Queue -function Queue.new() - local self = setmetatable({}, Queue) +function Queue:new() + local obj = setmetatable({}, self) + self.__index = self - self._first = 0 - self._last = -1 - self._queue = {} + obj._first = 0 + obj._last = -1 + obj._queue = {} - return self + return obj end -- Check if the queue is empty function Queue:IsEmpty() - return self._first > self._last + return self._first > self._last end -- Add a value to the queue function Queue:Enqueue(value) - local last = self._last + 1 - self._last = last - self._queue[last] = value + local last = self._last + 1 + self._last = last + self._queue[last] = value end -- Remove a value from the queue function Queue:Dequeue() - local first = self._first - if self:IsEmpty() then - return nil - end - local value = self._queue[first] - self._queue[first] = nil - self._first = first + 1 - return value + if self:IsEmpty() then + return nil + end + local first = self._first + local value = self._queue[first] + self._queue[first] = nil + self._first = first + 1 + return value end return Queue diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua new file mode 100644 index 0000000000..05d2056085 --- /dev/null +++ b/indra/newview/scripts/lua/WaitQueue.lua @@ -0,0 +1,68 @@ +-- WaitQueue isa Queue with the added feature that when the queue is empty, +-- the Dequeue() operation blocks the calling coroutine until some other +-- coroutine Enqueue()s a new value. + +local Queue = require('Queue') + +WaitQueue = Queue:new() + +function WaitQueue:new() + local obj = setmetatable(Queue:new(), self) + self.__index = self + obj._waiters = {} + obj._closed = false + return obj +end + +function WaitQueue:Enqueue(value) + if self._closed then + error("can't Enqueue() on closed Queue") + end + Queue:Enqueue(value) + -- WaitQueue is designed to support multi-producer, multi-consumer use + -- cases. With multiple consumers, if more than one is trying to + -- Dequeue() from an empty WaitQueue, we'll have multiple waiters. + -- Unlike OS threads, with cooperative concurrency it doesn't make sense + -- to "notify all": we need resume only one of the waiting Dequeue() + -- callers. But since resuming that caller might entail either Enqueue() + -- or Dequeue() calls, recheck every time around to see if we must resume + -- another waiting coroutine. + while not self:IsEmpty() and #self._waiters do + -- pop the oldest waiting coroutine instead of the most recent, for + -- more-or-less round robin fairness + local waiter = table.remove(self._waiters, 1) + -- don't pass the head item: let the resumed coroutine retrieve it + local ok, message = coroutine.resume(waiter) + -- if resuming that waiter encountered an error, don't swallow it + if not ok then + error(message) + end + end +end + +function WaitQueue:Dequeue() + while self:IsEmpty() do + -- Don't check for closed until the queue is empty: producer can close + -- the queue while there are still items left, and we want the + -- consumer(s) to retrieve those last few items. + if self._closed then + return nil + end + local coro = coroutine.running() + if coro == nil then + error("WaitQueue:Dequeue() trying to suspend main coroutine") + end + -- add the running coroutine to the list of waiters + table.insert(self._waiters, coro) + -- then let somebody else run + coroutine.yield() + end + -- here we're sure this queue isn't empty + return Queue:Dequeue() +end + +function WaitQueue:close() + self._closed = true +end + +return WaitQueue diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/inspect.lua new file mode 100644 index 0000000000..9900a0b81b --- /dev/null +++ b/indra/newview/scripts/lua/inspect.lua @@ -0,0 +1,371 @@ +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table +local inspect = {Options = {}, } + + + + + + + + + + + + + + + + + +inspect._VERSION = 'inspect.lua 3.1.0' +inspect._URL = 'http://github.com/kikito/inspect.lua' +inspect._DESCRIPTION = 'human-readable representations of tables' +inspect._LICENSE = [[ + MIT LICENSE + + Copyright (c) 2022 Enrique GarcĂa Cota + + 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. +]] +inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end }) +inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end }) + +local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format + +local _rawget +if rawget then + _rawget = rawget +else + _rawget = function(t, k) return t[k] end +end + +local function rawpairs(t) + return next, t, nil +end + + + +local function smartQuote(str) + if match(str, '"') and not match(str, "'") then + return "'" .. str .. "'" + end + return '"' .. gsub(str, '"', '\\"') .. '"' +end + + +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127", +} +local longControlCharEscapes = { ["\127"] = "\127" } +for i = 0, 31 do + local ch = char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\" .. i + longControlCharEscapes[ch] = fmt("\\%03d", i) + end +end + +local function escape(str) + return (gsub(gsub(gsub(str, "\\", "\\\\"), + "(%c)%f[0-9]", longControlCharEscapes), + "%c", shortControlCharEscapes)) +end + +local luaKeywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['goto'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local function isIdentifier(str) + return type(str) == "string" and + not not str:match("^[_%a][_%a%d]*$") and + not luaKeywords[str] +end + +local flr = math.floor +local function isSequenceKey(k, sequenceLength) + return type(k) == "number" and + flr(k) == k and + 1 <= (k) and + k <= sequenceLength +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7, +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + + if ta == tb and (ta == 'string' or ta == 'number') then + return (a) < (b) + end + + local dta = defaultTypeOrders[ta] or 100 + local dtb = defaultTypeOrders[tb] or 100 + + + return dta == dtb and ta < tb or dta < dtb +end + +local function getKeys(t) + + local seqLen = 1 + while _rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 + end + seqLen = seqLen - 1 + + local keys, keysLen = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLen, seqLen +end + +local function countCycles(x, cycles) + if type(x) == "table" then + if cycles[x] then + cycles[x] = cycles[x] + 1 + else + cycles[x] = 1 + for k, v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) + end + end +end + +local function makePath(path, a, b) + local newPath = {} + local len = #path + for i = 1, len do newPath[i] = path[i] end + + newPath[len + 1] = a + newPath[len + 2] = b + + return newPath +end + + +local function processRecursive(process, + item, + path, + visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == "table" then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k, v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + +local function puts(buf, str) + buf.n = buf.n + 1 + buf[buf.n] = str +end + + + +local Inspector = {} + + + + + + + + + + +local Inspector_mt = { __index = Inspector } + +local function tabify(inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) +end + +function Inspector:getId(v) + local id = self.ids[v] + local ids = self.ids + if not id then + local tv = type(v) + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id + end + return tostring(id) +end + +function Inspector:putValue(v) + local buf = self.buf + local tv = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + puts(buf, tostring(v)) + elseif tv == 'table' and not self.ids[v] then + local t = v + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end + + local keys, keysLen, seqLen = getKeys(t) + + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then puts(buf, ',') end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k) + else + puts(buf, "[") + self:putValue(k) + puts(buf, "]") + end + puts(buf, ' = ') + self:putValue(t[k]) + end + end + + local mt = getmetatable(t) + if type(mt) == 'table' then + if seqLen + keysLen > 0 then puts(buf, ',') end + tabify(self) + puts(buf, '<metatable> = ') + self:putValue(mt) + end + + self.level = self.level - 1 + + if keysLen > 0 or type(mt) == 'table' then + tabify(self) + elseif seqLen > 0 then + puts(buf, ' ') + end + + puts(buf, '}') + end + + else + puts(buf, fmt('<%s %d>', tv, self:getId(v))) + end +end + + + + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or (math.huge) + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local cycles = {} + countCycles(root, cycles) + + local inspector = setmetatable({ + buf = { n = 0 }, + ids = {}, + cycles = cycles, + depth = depth, + level = 0, + newline = newline, + indent = indent, + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buf) +end + +setmetatable(inspect, { + __call = function(_, root, options) + return inspect.inspect(root, options) + end, +}) + +return inspect diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua new file mode 100644 index 0000000000..351e1bf007 --- /dev/null +++ b/indra/newview/scripts/lua/leap.lua @@ -0,0 +1,284 @@ +-- Lua implementation of LEAP (LLSD Event API Plugin) protocol +-- +-- This module supports Lua scripts run by the Second Life viewer. +-- +-- LEAP protocol passes LLSD objects, converted to/from Lua tables, in both +-- directions. A typical LLSD object is a map containing keys 'pump' and +-- 'data'. +-- +-- The viewer's Lua post_to(pump, data) function posts 'data' to the +-- LLEventPump 'pump'. This is typically used to engage an LLEventAPI method. +-- +-- Similarly, the viewer gives each Lua script its own LLEventPump with a +-- unique name. That name is returned by get_event_pumps(). Every event +-- received on that LLEventPump is queued for retrieval by get_event_next(), +-- which returns (pump, data): the name of the LLEventPump on which the event +-- was received and the received event data. When the queue is empty, +-- get_event_next() blocks the calling Lua script until the next event is +-- received. + +local leap = {} + +-- _reply: string name of reply LLEventPump. Any events the viewer posts to +-- this pump will be queued for get_event_next(). We usually specify it as the +-- reply pump for requests to internal viewer services. +-- _command: string name of command LLEventPump. post_to(_command, ...) +-- engages LLLeapListener operations such as listening on a specified other +-- LLEventPump, etc. +leap._reply, leap._command = get_event_pumps() +-- Dict of features added to the LEAP protocol since baseline implementation. +-- Before engaging a new feature that might break an older viewer, we can +-- check for the presence of that feature key. This table is solely about the +-- LEAP protocol itself, the way we communicate with the viewer. To discover +-- whether a given listener exists, or supports a particular operation, use +-- _command's "getAPI" operation. +-- For Lua, _command's "getFeatures" operation suffices? +-- leap._features = {} + +-- Each outstanding request() or generate() call has a corresponding +-- WaitForReqid object (later in this module) to handle the +-- response(s). If an incoming event contains an echoed ["reqid"] key, +-- we can look up the appropriate WaitForReqid object more efficiently +-- in a dict than by tossing such objects into the usual waitfors list. +-- Note: the ["reqid"] must be unique, otherwise we could end up +-- replacing an earlier WaitForReqid object in self.pending with a +-- later one. That means that no incoming event will ever be given to +-- the old WaitForReqid object. Any coroutine waiting on the discarded +-- WaitForReqid object would therefore wait forever. +leap._pending = {} +-- Our consumer will instantiate some number of WaitFor subclass objects. +-- As these are traversed in descending priority order, we must keep +-- them in a list. +leap._waitfors = {} +-- It has been suggested that we should use UUIDs as ["reqid"] values, +-- since UUIDs are guaranteed unique. However, as the "namespace" for +-- ["reqid"] values is our very own _reply pump, we can get away with +-- an integer. +leap._reqid = 0 + +-- get the name of the reply pump +function leap.replypump() + return leap._reply +end + +-- get the name of the command pump +function leap.cmdpump() + return leap._command +end + +-- Fire and forget. Send the specified request LLSD, expecting no reply. +-- In fact, should the request produce an eventual reply, it will be +-- treated as an unsolicited event. +-- +-- See also request(), generate(). +function leap.send(pump, data, reqid) + local data = data + if type(data) == 'table' then + data = table.clone(data) + data['reply'] = leap._reply + if reqid ~= nil then + data['reqid'] = reqid + end + end + post_to(pump, data) +end + +-- Send the specified request LLSD, expecting exactly one reply. Block +-- the calling coroutine until we receive that reply. +-- +-- Every request() (or generate()) LLSD block we send will get stamped +-- with a distinct ["reqid"] value. The requested event API must echo the +-- same ["reqid"] field in each reply associated with that request. This way +-- we can correctly dispatch interleaved replies from different requests. +-- +-- If the desired event API doesn't support the ["reqid"] echo convention, +-- you should use send() instead -- since request() or generate() would +-- wait forever for a reply stamped with that ["reqid"] -- and intercept +-- any replies using WaitFor. +-- +-- Unless the request data already contains a ["reply"] key, we insert +-- reply=self.replypump to try to ensure that the expected reply will be +-- returned over the socket. +function leap.request(pump, data) + local reqid = leap._requestSetup(pump, data) + local ok, response = pcall(leap._pending[reqid].wait) + -- kill off temporary WaitForReqid object, even if error + leap._pending[reqid] = nil + if ok then + return response + else + error(response) + end +end + +-- common setup code shared by request() and generate() +function leap._requestSetup(pump, data) + -- invent a new, unique reqid + local reqid = leap._reqid + leap._reqid += 1 + -- Instantiate a new WaitForReqid object. The priority is irrelevant + -- because, unlike the WaitFor base class, WaitForReqid does not + -- self-register on our leap._waitfors list. Instead, capture the new + -- WaitForReqid object in leap._pending so _dispatch() can find it. + leap._pending[reqid] = WaitForReqid.new(reqid) + -- Pass reqid to send() to stamp it into (a copy of) the request data. + leap.send(pump, data, reqid) + return reqid +end + +-- Send the specified request LLSD, expecting an arbitrary number of replies. +-- Each one is yielded on receipt. If you omit checklast, this is an infinite +-- generator; it's up to the caller to recognize when the last reply has been +-- received, and stop resuming for more. +-- +-- If you pass checklast=<callable accepting(event)>, each response event is +-- passed to that callable (after the yield). When the callable returns +-- True, the generator terminates in the usual way. +-- +-- See request() remarks about ["reqid"]. +function leap.generate(pump, data, checklast) + -- Invent a new, unique reqid. Arrange to handle incoming events + -- bearing that reqid. Stamp the outbound request with that reqid, and + -- send it. + local reqid = leap._requestSetup(pump, data) + local ok, response + repeat + ok, response = pcall(leap._pending[reqid].wait) + if not ok then + break + end + coroutine.yield(response) + until checklast and checklast(response) + -- If we break the above loop, whether or not due to error, clean up. + leap._pending[reqid] = nil + if not ok then + error(response) + end +end + +-- Kick off response processing. The calling script must create and resume one +-- or more coroutines to perform viewer requests using send(), request() or +-- generate() before calling this function to handle responses. +-- +-- While waiting for responses from the viewer, the C++ coroutine running the +-- calling Lua script is blocked: no other Lua coroutine is running. +function leap.process() + local ok, pump, data + while true do + ok, pump, data = pcall(get_event_next) + if not ok then + break + end + leap._dispatch(pump, data) + end + -- we're done: clean up all pending coroutines + for i, waitfor in pairs(leap._pending) do + waitfor._exception(pump) + end + for i, waitfor in pairs(leap._waitfors) do + waitfor._exception(pump) + end +end + +-- Route incoming (pump, data) event to the appropriate waiting coroutine. +function leap._dispatch(pump, data) + local reqid = data['reqid'] + -- if the response has no 'reqid', it's not from request() or generate() + if reqid == nil then + return leap._unsolicited(pump, data) + end + -- have reqid; do we have a WaitForReqid? + local waitfor = leap._pending[reqid] + if waitfor == nil then + return leap._unsolicited(pump, data) + end + -- found the right WaitForReqid object, let it handle the event + data['reqid'] = nil + waitfor._handle(pump, data) +end + +-- Handle an incoming (pump, data) event with no recognizable ['reqid'] +function leap._unsolicited(pump, data) + -- we maintain waitfors in descending priority order, so the first waitfor + -- to claim this event is the one with the highest priority + for i, waitfor in pairs(leap._waitfors) do + if waitfor._handle(pump, data) then + return + end + end + print_debug('_unsolicited(', pump, ', ', data, ') discarding unclaimed event') +end + +-- called by WaitFor.enable() +function leap._registerWaitFor(waitfor) + table.insert(leap._waitfors, waitfor) + -- keep waitfors sorted in descending order of specified priority + table.sort(leap._waitfors, + function (lhs, rhs) return lhs.priority > rhs.priority end) +end + +-- called by WaitFor.disable() +function leap._unregisterWaitFor(waitfor) + for i, w in pairs(leap._waitfors) do + if w == waitfor then + leap._waitfors[i] = nil + break + end + end +end + +-- ****************************************************************************** +-- WaitFor and friends +-- ****************************************************************************** + +-- An unsolicited event is handled by the highest-priority WaitFor subclass +-- object willing to accept it. If no such object is found, the unsolicited +-- event is discarded. +-- +-- - First, instantiate a WaitFor subclass object to register its interest in +-- some incoming event(s). WaitFor instances are self-registering; merely +-- instantiating the object suffices. +-- - Any coroutine may call a given WaitFor object's wait() method. This blocks +-- the calling coroutine until a suitable event arrives. +-- - WaitFor's constructor accepts a float priority. Every incoming event +-- (other than those claimed by request() or generate()) is passed to each +-- extant WaitFor.filter() method in descending priority order. The first +-- such filter() to return nontrivial data claims that event. +-- - At that point, the blocked wait() call on that WaitFor object returns the +-- item returned by filter(). +-- - WaitFor contains a queue. Multiple arriving events claimed by that WaitFor +-- object's filter() method are added to the queue. Naturally, until the +-- queue is empty, calling wait() immediately returns the front entry. +-- +-- It's reasonable to instantiate a WaitFor subclass whose filter() method +-- unconditionally returns the incoming event, and whose priority places it +-- last in the list. This object will enqueue every unsolicited event left +-- unclaimed by other WaitFor subclass objects. +-- +-- It's not strictly necessary to associate a WaitFor object with exactly one +-- coroutine. You might have multiple "worker" coroutines drawing from the same +-- WaitFor object, useful if the work being done per event might itself involve +-- "blocking" operations. Or a given coroutine might sample a number of WaitFor +-- objects in round-robin fashion... etc. etc. Nonetheless, it's +-- straightforward to designate one coroutine for each WaitFor object. +leap.WaitFor = {} + +function leap.WaitFor:new() + obj = setmetatable({}, self) + self.__index = self + + + + self._first = 0 + self._last = -1 + self._queue = {} + + return obj +end + +-- Check if the queue is empty +function Queue:IsEmpty() + return self._first > self._last +end + +return leap diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua new file mode 100644 index 0000000000..16a54be0d1 --- /dev/null +++ b/indra/newview/scripts/lua/qtest.lua @@ -0,0 +1,47 @@ +-- Exercise the Queue, WaitQueue, ErrorQueue family + +Queue = require('Queue') +WaitQueue = require('WaitQueue') +ErrorQueue = require('ErrorQueue') + +q1 = Queue:new() +q2 = Queue:new() + +q1:Enqueue(17) + +assert(not q1:IsEmpty()) +assert(q2:IsEmpty()) +assert(q1:Dequeue() == 17) +assert(q1:Dequeue() == nil) +assert(q2:Dequeue() == nil) + +q1 = WaitQueue:new() + +inspect = require('inspect') +print(inspect(q1)) + +q2 = WaitQueue:new() +result = {} + +values = { 1, 1, 2, 3, 5, 8, 13, 21 } +for i, value in pairs(values) do + q1:Enqueue(value) +end + +function consumer(desc, q) + print('consumer(', desc, ') start') + local value = q:Dequeue() + while value ~= nil do + table.insert(result, value) + value = q:Dequeue() + end + print('consumer(', desc, ') done') +end + +coa = coroutine.create(consumer) +cob = coroutine.create(consumer) +coroutine.resume(coa, 'a', q1) +coroutine.resume(cob, 'b', q1) + +assert(result == values) + |