From c621fc39fc4ac25482fbc1090b8067c4187de176 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 6 Mar 2024 09:56:45 -0500 Subject: 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. --- indra/newview/scripts/lua/qtest.lua | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 indra/newview/scripts/lua/qtest.lua (limited to 'indra/newview/scripts/lua/qtest.lua') 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) + -- cgit v1.2.3 From 63dcb3802c8139ff3b87b614cb275236cecea858 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 7 Mar 2024 13:46:24 -0500 Subject: Finish WaitQueue, ErrorQueue; add util.count(), join(); extend qtest. For WaitQueue, nail down the mechanism for declaring a subclass and for calling a base-class method from a subclass override. Break out new _wake_waiters() method from Enqueue(): we need to do the same from close(), in case there are waiting consumers. Also, in Lua, 0 is not false. Instead of bundling a normal/error flag with every queued value, make ErrorQueue overload its _closed attribute. Once you call ErrorQueue:Error(), every subsequent Dequeue() call by any consumer will re-raise the same error. util.count() literally counts entries in a table, since #t is documented to be unreliable. (If you create a list with 5 entries and delete the middle one, #t might return 2 or it might return 5, but it won't return 4.) util.join() fixes a curious omission from Luau's string library: like Python's str.join(), it concatenates all the strings from a list with an optional separator. We assume that incrementally building a list of strings and then doing a single allocation for the desired result string is cheaper than reallocating each of a sequence of partial concatenated results. Add qtest test that posts individual items to a WaitQueue, waking waiting consumers to retrieve the next available result. Add test proving that calling ErrorQueue:Error() propagates the error to all consumers. --- indra/newview/scripts/lua/qtest.lua | 100 +++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 8 deletions(-) (limited to 'indra/newview/scripts/lua/qtest.lua') diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua index 16a54be0d1..12e425b7b2 100644 --- a/indra/newview/scripts/lua/qtest.lua +++ b/indra/newview/scripts/lua/qtest.lua @@ -3,7 +3,11 @@ Queue = require('Queue') WaitQueue = require('WaitQueue') ErrorQueue = require('ErrorQueue') +util = require('util') +inspect = require('inspect') + +-- ------------------ Queue variables are instance-specific ------------------ q1 = Queue:new() q2 = Queue:new() @@ -15,33 +19,113 @@ assert(q1:Dequeue() == 17) assert(q1:Dequeue() == nil) assert(q2:Dequeue() == nil) +-- ----------------------------- test WaitQueue ------------------------------ 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 +-- close() while not empty tests that queue drains before reporting done +q1:close() +-- ensure that WaitQueue instance variables are in fact independent +assert(q2:IsEmpty()) + +-- consumer() coroutine to pull from the passed q until closed function consumer(desc, q) - print('consumer(', desc, ') start') + print(string.format('consumer(%s) start', desc)) local value = q:Dequeue() while value ~= nil do + print(string.format('consumer(%s) got %q', desc, value)) table.insert(result, value) value = q:Dequeue() end - print('consumer(', desc, ') done') + print(string.format('consumer(%s) done', desc)) end +-- run two consumers coa = coroutine.create(consumer) cob = coroutine.create(consumer) +-- Since consumer() doesn't yield while it can still retrieve values, +-- consumer(a) will dequeue all values from q1 and return when done. coroutine.resume(coa, 'a', q1) +-- consumer(b) will wake up to find the queue empty and closed. coroutine.resume(cob, 'b', q1) +coroutine.close(coa) +coroutine.close(cob) + +print('values:', inspect(values)) +print('result:', inspect(result)) + +assert(util.equal(values, result)) + +-- try incrementally enqueueing values +q3 = WaitQueue:new() +result = {} +values = { 'This', 'is', 'a', 'test', 'script' } + +coros = {} +for _, name in {'a', 'b'} do + local coro = coroutine.create(consumer) + table.insert(coros, coro) + -- Resuming both coroutines should leave them both waiting for a queue item. + coroutine.resume(coro, name, q3) +end + +for _, s in pairs(values) do + print(string.format('Enqueue(%q)', s)) + q3:Enqueue(s) +end +q3:close() + +function joinall(coros) + local running + local errors = 0 + repeat + running = false + for i, coro in pairs(coros) do + if coroutine.status(coro) == 'suspended' then + running = true + local ok, message = coroutine.resume(coro) + if not ok then + print('*** ' .. message) + errors += 1 + end + if coroutine.status(coro) == 'dead' then + coros[i] = nil + end + end + end + until not running + return errors +end + +joinall(coros) + +print(string.format('%q', util.join(result, ' '))) +assert(util.equal(values, result)) + +-- ----------------------------- test ErrorQueue ----------------------------- +q4 = ErrorQueue:new() +result = {} +values = { 'This', 'is', 'a', 'test', 'script' } + +coros = {} +for _, name in {'a', 'b'} do + local coro = coroutine.create(consumer) + table.insert(coros, coro) + -- Resuming both coroutines should leave them both waiting for a queue item. + coroutine.resume(coro, name, q4) +end + +for i = 1, 4 do + print(string.format('Enqueue(%q)', values[i])) + q4:Enqueue(values[i]) +end +q4:Error('something went wrong') -assert(result == values) +assert(joinall(coros) == 2) -- cgit v1.2.3 From 354d7b55c0a267b542dc51e3985f7d3739ffcdfd Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 13 Mar 2024 14:37:48 -0400 Subject: Introduce a resume() wrapper to surface coroutine errors. --- indra/newview/scripts/lua/qtest.lua | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) (limited to 'indra/newview/scripts/lua/qtest.lua') diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua index 12e425b7b2..009446d0c3 100644 --- a/indra/newview/scripts/lua/qtest.lua +++ b/indra/newview/scripts/lua/qtest.lua @@ -7,6 +7,19 @@ util = require('util') inspect = require('inspect') +-- resume() wrapper to propagate errors +function resume(co, ...) + -- if there's an idiom other than table.pack() to assign an arbitrary + -- number of return values, I don't yet know it + local ok_result = table.pack(coroutine.resume(co, ...)) + if not ok_result[1] then + -- if [1] is false, then [2] is the error message + error(ok_result[2]) + end + -- ok is true, whew, just return the rest of the values + return table.unpack(ok_result, 2) +end + -- ------------------ Queue variables are instance-specific ------------------ q1 = Queue:new() q2 = Queue:new() @@ -51,9 +64,9 @@ coa = coroutine.create(consumer) cob = coroutine.create(consumer) -- Since consumer() doesn't yield while it can still retrieve values, -- consumer(a) will dequeue all values from q1 and return when done. -coroutine.resume(coa, 'a', q1) +resume(coa, 'a', q1) -- consumer(b) will wake up to find the queue empty and closed. -coroutine.resume(cob, 'b', q1) +resume(cob, 'b', q1) coroutine.close(coa) coroutine.close(cob) @@ -72,7 +85,7 @@ for _, name in {'a', 'b'} do local coro = coroutine.create(consumer) table.insert(coros, coro) -- Resuming both coroutines should leave them both waiting for a queue item. - coroutine.resume(coro, name, q3) + resume(coro, name, q3) end for _, s in pairs(values) do @@ -89,6 +102,8 @@ function joinall(coros) for i, coro in pairs(coros) do if coroutine.status(coro) == 'suspended' then running = true + -- directly call coroutine.resume() instead of our resume() + -- wrapper because we explicitly check for errors here local ok, message = coroutine.resume(coro) if not ok then print('*** ' .. message) @@ -105,7 +120,7 @@ end joinall(coros) -print(string.format('%q', util.join(result, ' '))) +print(string.format('%q', table.concat(result, ' '))) assert(util.equal(values, result)) -- ----------------------------- test ErrorQueue ----------------------------- @@ -118,7 +133,7 @@ for _, name in {'a', 'b'} do local coro = coroutine.create(consumer) table.insert(coros, coro) -- Resuming both coroutines should leave them both waiting for a queue item. - coroutine.resume(coro, name, q4) + resume(coro, name, q4) end for i = 1, 4 do -- cgit v1.2.3 From 18c4dcc5998e061fe3ab54607665c775dd18c826 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 21:42:10 -0400 Subject: Allow Python-like 'object = ClassName(ctor args)' constructor calls. The discussions we've read about Lua classes conventionally use ClassName:new() as the constructor, and so far we've followed that convention. But setting metaclass(ClassName).__call = ClassName.new permits Lua to respond to calls of the form ClassName(ctor args) by implicitly calling ClassName:new(ctor args). Introduce util.classctor(). Calling util.classctor(ClassName) sets ClassName's metaclass's __call to ClassName's constructor method. If the constructor method is named something other than new(), pass ClassName.method as the second arg. Use util.classctor() on each of our classes that defines a new() method. Replace ClassName:new(args) calls with ClassName(args) calls throughout. --- indra/newview/scripts/lua/qtest.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'indra/newview/scripts/lua/qtest.lua') diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua index 009446d0c3..9526f58b04 100644 --- a/indra/newview/scripts/lua/qtest.lua +++ b/indra/newview/scripts/lua/qtest.lua @@ -21,8 +21,8 @@ function resume(co, ...) end -- ------------------ Queue variables are instance-specific ------------------ -q1 = Queue:new() -q2 = Queue:new() +q1 = Queue() +q2 = Queue() q1:Enqueue(17) @@ -33,8 +33,8 @@ assert(q1:Dequeue() == nil) assert(q2:Dequeue() == nil) -- ----------------------------- test WaitQueue ------------------------------ -q1 = WaitQueue:new() -q2 = WaitQueue:new() +q1 = WaitQueue() +q2 = WaitQueue() result = {} values = { 1, 1, 2, 3, 5, 8, 13, 21 } @@ -76,7 +76,7 @@ print('result:', inspect(result)) assert(util.equal(values, result)) -- try incrementally enqueueing values -q3 = WaitQueue:new() +q3 = WaitQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } @@ -124,7 +124,7 @@ print(string.format('%q', table.concat(result, ' '))) assert(util.equal(values, result)) -- ----------------------------- test ErrorQueue ----------------------------- -q4 = ErrorQueue:new() +q4 = ErrorQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } -- cgit v1.2.3