From 841e19c1cb62341c10254e6f4bf992c0c19d27b8 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 15:07:14 -0400 Subject: Remove obsolete, unreferenced DESTRINGIZE(), DEWSTRINGIZE() macros. --- indra/llcommon/stringize.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h index 63d44a7272..9604d912b5 100644 --- a/indra/llcommon/stringize.h +++ b/indra/llcommon/stringize.h @@ -190,16 +190,4 @@ void destringize_f(std::basic_string const & str, Functor const & f) f(in); } -/** - * DESTRINGIZE(str, item1 >> item2 >> item3 ...) effectively expands to the - * following: - * @code - * std::istringstream in(str); - * in >> item1 >> item2 >> item3 ... ; - * @endcode - */ -#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](auto& in){in >> EXPRESSION;})) -// legacy name, just use DESTRINGIZE() going forward -#define DEWSTRINGIZE(STR, EXPRESSION) DESTRINGIZE(STR, EXPRESSION) - #endif /* ! defined(LL_STRINGIZE_H) */ -- cgit v1.2.3 From 14c8fc3768d978205bf17ffc1905c2772afbd434 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 16:47:38 -0400 Subject: Add `LL.setdtor()` function to add a "destructor" to any Lua object. `setdtor('description', object, function)` returns a proxy userdata object referencing object and function. When the proxy is garbage-collected, or at the end of the script, its destructor calls `function(object)`. The original object may be retrieved as `proxy._target`, e.g. to pass it to the `table` library. The proxy also has a metatable with metamethods supporting arithmetic operations, string concatenation, length and table indexing. For other operations, retrieve `proxy._target`. (But don't assign to `proxy._target`. It will appear to work, in that subsequent references to `proxy._target` will retrieve the replacement object -- however, the destructor will still call `function(original object)`.) Fix bugs in `lua_setfieldv()`, `lua_rawgetfield()` and `lua_rawsetfield()`. Add C++ functions `lua_destroyuserdata()` to explicitly destroy a `lua_emplace()` userdata object, plus `lua_destroybounduserdata()`. The latter can bind such a userdata object as an upvalue to pass to `LL.atexit()`. Make `LL.help()` and `LL.leaphelp()` help text include the `LL.` prefix. --- indra/llcommon/lua_function.cpp | 325 ++++++++++++++++++++++++++++- indra/llcommon/lua_function.h | 18 +- indra/newview/scripts/lua/test_setdtor.lua | 62 ++++++ 3 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 indra/newview/scripts/lua/test_setdtor.lua diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 880bc209f6..12cff89fbd 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -99,6 +99,34 @@ fsyspath source_path(lua_State* L) } // namespace lluau +/***************************************************************************** +* lua_destroyuserdata(), lua_destroybounduserdata() (see lua_emplace()) +*****************************************************************************/ +int lua_destroyuserdata(lua_State* L) +{ + // stack: lua_emplace() userdata to be destroyed + if (int tag; + lua_isuserdata(L, -1) && + (tag = lua_userdatatag(L, -1)) != 0) + { + auto dtor = lua_getuserdatadtor(L, tag); + // detach this userdata from the destructor with tag 'tag' + lua_setuserdatatag(L, -1, 0); + // now run the real destructor + dtor(L, lua_touserdata(L, -1)); + } + lua_settop(L, 0); + return 0; +} + +int lua_destroybounduserdata(lua_State *L) +{ + // called with no arguments -- push bound upvalue + lluau_checkstack(L, 1); + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_destroyuserdata(L); +} + /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ @@ -664,10 +692,10 @@ LuaListener& LuaState::obtainListener(lua_State* L) // At this point, one way or the other, the stack top should be (a Lua // userdata containing) our LuaListener. LuaListener* listener{ lua_toclass(L, -1) }; - // userdata objects created by lua_emplace() are bound on the atexit() - // queue, and are thus never garbage collected: they're destroyed only - // when ~LuaState() walks that queue. That's why we dare pop the userdata - // value off the stack while still depending on a pointer into its data. + // Since our LuaListener instance is stored in the Registry, it won't be + // garbage collected: it will be destroyed only when lua_close() clears + // out the Registry. That's why we dare pop the userdata value off the + // stack while still depending on a pointer into its data. lua_pop(L, 1); return *listener; } @@ -851,8 +879,8 @@ lua_function(check_stop, "check_stop(): ensure that a Lua script responds to vie * help() *****************************************************************************/ lua_function(help, - "help(): list viewer's Lua functions\n" - "help(function): show help string for specific function") + "LL.help(): list viewer's Lua functions\n" + "LL.help(function): show help string for specific function") { auto& luapump{ LLEventPumps::instance().obtain("lua output") }; const auto& [registry, lookup]{ LuaFunction::getRState() }; @@ -911,8 +939,8 @@ lua_function(help, *****************************************************************************/ lua_function( leaphelp, - "leaphelp(): list viewer's LEAP APIs\n" - "leaphelp(api): show help for specific api string name") + "LL.leaphelp(): list viewer's LEAP APIs\n" + "LL.leaphelp(api): show help for specific api string name") { LLSD request; int top{ lua_gettop(L) }; @@ -974,6 +1002,287 @@ lua_function( return 0; // void return } +/***************************************************************************** +* setdtor +*****************************************************************************/ +namespace { + +// proxy userdata object returned by setdtor() +struct setdtor_refs +{ + lua_State* L; + std::string desc; + // You can't directly store a Lua object in a C++ object, but you can + // create a Lua "reference" by storing the object in the Lua Registry and + // capturing its Registry index. + int objref; + int dtorref; + + setdtor_refs(lua_State* L, const std::string& desc, int objref, int dtorref): + L(L), + desc(desc), + objref(objref), + dtorref(dtorref) + {} + setdtor_refs(const setdtor_refs&) = delete; + setdtor_refs& operator=(const setdtor_refs&) = delete; + ~setdtor_refs(); + + static void push_metatable(lua_State* L); + static std::string binop(const std::string& name, const std::string& op); + static int meta__index(lua_State* L); +}; + +} // anonymous namespace + +lua_function( + setdtor, + "setdtor(desc, obj, dtorfunc) => proxy object referencing obj and dtorfunc.\n" + "When the returned proxy object is garbage-collected, or when the script\n" + "ends, call dtorfunc(obj). String desc is logged in the error message, if any.\n" + "Use the returned proxy object (or proxy._target) like obj.\n" + "obj won't be destroyed as long as the proxy exists; it's the proxy object's\n" + "lifespan that determines when dtorfunc(obj) will be called.") +{ + if (lua_gettop(L) != 3) + { + return lluau::error(L, "setdtor(desc, obj, dtor) requires exactly 3 arguments"); + } + // called with (desc, obj, dtor), returns proxy object + lua_checkdelta(L, -2); + lluau_checkstack(L, 3); // might get up to 6 stack entries + auto desc{ lua_tostdstring(L, 1) }; + // Get Lua "references" for each of the object and the dtor function. + int objref = lua_ref(L, 2); + int dtorref = lua_ref(L, 3); + // Having captured each of our parameters, discard them. + lua_settop(L, 0); + // Push our setdtor_refs userdata. Not only do we want to push it on L's + // stack, but setdtor_refs's constructor itself requires L. + lua_emplace(L, L, desc, objref, dtorref); + // stack: proxy (i.e. setdtor_refs userdata) + // have to set its metatable + lua_getfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, setdtor_meta (which might be nil) + if (lua_isnil(L, -1)) + { + // discard nil + lua_pop(L, 1); + // compile and push our forwarding metatable + setdtor_refs::push_metatable(L); + // stack: proxy, metatable + // duplicate metatable to save it + lua_pushvalue(L, -1); + // stack: proxy, metatable, metable + // save metatable for future calls + lua_setfield(L, LUA_REGISTRYINDEX, "setdtor_meta"); + // stack: proxy, metatable + } + // stack: proxy, metatable + lua_setmetatable(L, -2); + // stack: proxy + // Because ~setdtor_refs() necessarily uses the Lua stack, the Registry et + // al., we can't let a setdtor_refs instance be destroyed by lua_close(): + // the Lua environment will already be partially shut down. To destroy + // this new setdtor_refs instance BEFORE lua_close(), bind it with + // lua_destroybounduserdata() and register it with LL.atexit(). + // push (the entry point for) LL.atexit() + lua_pushcfunction(L, atexit_luasub::call, "LL.atexit()"); + // stack: proxy, atexit() + lua_pushvalue(L, -2); + // stack: proxy, atexit(), proxy + int tag = lua_userdatatag(L, -1); + // We don't have a lookup table to get from an int Lua userdata tag to the + // corresponding C++ typeinfo name string. We'll introduce one if we need + // it for debugging. But for this particular call, we happen to know it's + // always a setdtor_refs object. + lua_pushcclosure(L, lua_destroybounduserdata, + stringize("lua_destroybounduserdata<", tag, ">()").c_str(), + 1); + // stack: proxy, atexit(), lua_destroybounduserdata + // call atexit(): one argument, no results, let error propagate + lua_call(L, 1, 0); + // stack: proxy + return 1; +} + +namespace { + +void setdtor_refs::push_metatable(lua_State* L) +{ + lua_checkdelta(L, 1); + lluau_checkstack(L, 1); + // Ideally we want a metatable that forwards every operation on our + // setdtor_refs userdata proxy object to the original object. But the + // published C API doesn't include (e.g.) arithmetic operations on Lua + // objects, so in fact it's easier to express the desired metatable in Lua + // than in C++. We could make setdtor() depend on an external Lua module, + // but it seems less fragile to embed the Lua source code right here. + static const std::string setdtor_meta = stringize(R"-( + -- This metatable literal doesn't define __index() because that's + -- implemented in C++. We cannot, in Lua, peek into the setdtor_refs + -- userdata object to obtain objref, nor can we fetch Registry[objref]. + -- So our C++ __index() metamethod recognizes access to '_target' as a + -- reference to Registry[objref]. + -- The rest are defined per https://www.lua.org/manual/5.1/manual.html#2.8. + -- Luau supports destructors instead of __gc metamethod -- we rely on that! + -- We don't set __mode because our proxy is not a table. Real references + -- are stored in the wrapped table, so ITS __mode is what counts. + -- Initial definition of meta omits binary metamethods so they can bind the + -- metatable itself, as explained for binop() below. + local meta = { + __unm = function(arg) + return -arg._target + end, + __len = function(arg) + return #arg._target + end, + -- Comparison metamethods __eq(), __lt() and __le() are only called + -- when both operands have the same metamethod. For our purposes, that + -- means both operands are setdtor_refs userdata objects. + __eq = function(lhs, rhs) + return (lhs._target == rhs._target) + end, + __lt = function(lhs, rhs) + return (lhs._target < rhs._target) + end, + __le = function(lhs, rhs) + return (lhs._target <= rhs._target) + end, + __newindex = function(t, key, value) + t._target[key] = value + end, + __call = function(func, ...) + return func._target(...) + end, + __tostring = function(arg) + -- don't fret about arg._target's __tostring metamethod, + -- if any, because built-in tostring() deals with that + return tostring(arg._target) + end + } +)-", + binop("add", "+"), + binop("sub", "-"), + binop("mul", "*"), + binop("div", "/"), + binop("mod", "%"), + binop("pow", "^"), + binop("concat", ".."), +R"-( + return meta +)-"); + // only needed for debugging binop() +// LL_DEBUGS("Lua") << setdtor_meta << LL_ENDL; + + if (lluau::dostring(L, LL_PRETTY_FUNCTION, setdtor_meta) != LUA_OK) + { + // stack: error message string + lua_error(L); + } + llassert(lua_gettop(L) > 0); + llassert(lua_type(L, -1) == LUA_TTABLE); + // stack: Lua metatable compiled from setdtor_meta source + // Inject our C++ __index metamethod. + lua_rawsetfield(L, -1, "__index"sv, &setdtor_refs::meta__index); +} + +// In the definition of setdtor_meta above, binary arithmethic and +// concatenation metamethods are a little funny in that we don't know a +// priori which operand is the userdata with our metatable: the metamethod +// can be invoked either way. So every such metamethod must check, which +// leads to lots of redundancy. Hence this helper function. Call it a Lua +// macro. +std::string setdtor_refs::binop(const std::string& name, const std::string& op) +{ + return stringize( + " meta.__", name, " = function(lhs, rhs)\n" + " if getmetatable(lhs) == meta then\n" + " return lhs._target ", op, " rhs\n" + " else\n" + " return lhs ", op, " rhs._target\n" + " end\n" + " end\n"); +} + +// setdtor_refs __index() metamethod +int setdtor_refs::meta__index(lua_State* L) +{ + // called with (setdtor_refs userdata, key), returns retrieved object + lua_checkdelta(L, -1); + lluau_checkstack(L, 2); + // stack: proxy, key + // get ptr to the C++ struct data + auto ptr = lua_toclass(L, -2); + // meta__index() should NEVER be called with anything but setdtor_refs! + llassert(ptr); + // push the wrapped object + lua_getref(L, ptr->objref); + // stack: proxy, key, _target + // replace userdata with _target + lua_replace(L, -3); + // stack: _target, key + // Duplicate key because lua_tostring() converts number to string: + // if the key is (e.g.) 1, don't try to retrieve _target["1"]! + lua_pushvalue(L, -1); + // stack: _target, key, key + // recognize the special _target field + if (lua_tostdstring(L, -1) == "_target") + { + // okay, ditch both copies of "_target" string key + lua_pop(L, 2); + // stack: _target + } + else // any key but _target + { + // ditch stringized key + lua_pop(L, 1); + // stack: _target, key + // replace key with _target[key], invoking metamethod if any + lua_gettable(L, -2); + // stack: _target, _target[key] + // discard _target + lua_remove(L, -2); + // stack: _target[key] + } + return 1; +} + +// When Lua destroys a setdtor_refs userdata object, either from garbage +// collection or from LL.atexit(lua_destroybounduserdata), it's time to keep +// its promise to call the specified Lua destructor function with the +// specified Lua object. Of course we must also delete the captured +// "references" to both objects. +setdtor_refs::~setdtor_refs() +{ + lua_checkdelta(L); + lluau_checkstack(L, 2); + // push Registry[dtorref] + lua_getref(L, dtorref); + // push Registry[objref] + lua_getref(L, objref); + // free Registry[dtorref] + lua_unref(L, dtorref); + // free Registry[objref] + lua_unref(L, objref); + // call dtor(obj): one arg, no result, no error function + int rc = lua_pcall(L, 1, 0, 0); + if (rc != LUA_OK) + { + // TODO: we don't really want to propagate the error here. + // If this setdtor_refs instance is being destroyed by + // LL.atexit(), we want to continue cleanup. If it's being + // garbage-collected, the call is completely unpredictable from + // the consuming script's point of view. But what to do about this + // error?? For now, just log it. + LL_WARNS("Lua") << "setdtor(" << std::quoted(desc) << ") error: " + << lua_tostring(L, -1) << LL_ENDL; + lua_pop(L, 1); + } +} + +} // anonymous namespace + /***************************************************************************** * lua_what *****************************************************************************/ diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 6965e206ab..83abe8d71e 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -339,6 +339,7 @@ auto lua_getfieldv(lua_State* L, int index, const char* k) template auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 1); lua_push(L, value); @@ -349,6 +350,7 @@ auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value) template auto lua_rawgetfield(lua_State* L, int index, const std::string_view& k) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 1); lua_pushlstring(L, k.data(), k.length()); @@ -361,6 +363,7 @@ auto lua_rawgetfield(lua_State* L, int index, const std::string_view& k) template void lua_rawsetfield(lua_State* L, int index, const std::string_view& k, const T& value) { + index = lua_absindex(L, index); lua_checkdelta(L); lluau_checkstack(L, 2); lua_pushlstring(L, k.data(), k.length()); @@ -459,7 +462,7 @@ DistinctInt TypeTag::value; * containing a newly-constructed C++ object T(args...). The userdata has a * Luau destructor guaranteeing that the new T instance is destroyed when the * userdata is garbage-collected, no later than when the LuaState is - * destroyed. + * destroyed. It may be destroyed explicitly by calling lua_destroyuserdata(). * * Usage: * lua_emplace(L, T constructor args...); @@ -511,6 +514,19 @@ T* lua_toclass(lua_State* L, int index) return static_cast(ptr); } +/** + * Call lua_destroyuserdata() with the doomed userdata on the stack top. + * It must have been created by lua_emplace(). + */ +int lua_destroyuserdata(lua_State* L); + +/** + * Call lua_pushcclosure(L, lua_destroybounduserdata, 1) with the target + * userdata on the stack top. When the resulting C closure is called with no + * arguments, the bound userdata is destroyed by lua_destroyuserdata(). + */ +int lua_destroybounduserdata(lua_State *L); + /***************************************************************************** * lua_what() *****************************************************************************/ diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua new file mode 100644 index 0000000000..743c5168d0 --- /dev/null +++ b/indra/newview/scripts/lua/test_setdtor.lua @@ -0,0 +1,62 @@ +inspect = require 'inspect' + +print('initial setdtor') +bye = LL.setdtor('initial setdtor', 'Goodbye world!', print) + +print('arithmetic') +n = LL.setdtor('arithmetic', 11, print) +print("n =", n) +print("n._target =", n._target) +print("getmetatable(n) =", inspect(getmetatable(n))) +print("-n =", -n) +for i = 10, 12 do + -- Comparison metamethods are only called if both operands have the same + -- metamethod. + tempi = LL.setdtor('tempi', i, function(n) print('temp', i) end) + print(`n < {i}`, n < tempi) + print(`n <= {i}`, n <= tempi) + print(`n == {i}`, n == tempi) + print(`n ~= {i}`, n ~= tempi) + print(`n >= {i}`, n >= tempi) + print(`n > {i}`, n > tempi) +end +for i = 2, 3 do + print(`n + {i} =`, n + i) + print(`{i} + n =`, i + n) + print(`n - {i} =`, n - i) + print(`{i} - n =`, i - n) + print(`n * {i} =`, n * i) + print(`{i} * n =`, i * n) + print(`n / {i} =`, n / i) + print(`{i} / n =`, i / n) + print(`n % {i} =`, n % i) + print(`{i} % n =`, i % n) + print(`n ^ {i} =`, n ^ i) + print(`{i} ^ n =`, i ^ n) +end + +print('string') +s = LL.setdtor('string', 'hello', print) +print('s =', s) +print('#s =', #s) +print('s .. " world" =', s .. " world") +print('"world " .. s =', "world " .. s) + +print('table') +t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'}, + function(t) print(inspect(t)) end) +print('t =', inspect(t)) +print('t._target =', inspect(t._target)) +print('#t =', #t) +print('t[2] =', t[2]) +print('t.def =', t.def) +t[1] = 'new [1]' +print('t[1] =', t[1]) + +print('function') +f = LL.setdtor('function', function(a, b) return (a .. b) end, print) +print('f =', f) +print('f._target =', f._target) +print('f("Hello", " world") =', f("Hello", " world")) + +print('cleanup') -- cgit v1.2.3 From 364ea79ab3a4d48e0d10fbeabb9b8e88f226baac Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 28 Aug 2024 19:34:05 -0400 Subject: Prevent erroneous assignment to LL.setdtor() proxy._target field. Trim redundant output from test_setdtor.lua. --- indra/llcommon/lua_function.cpp | 4 +++- indra/newview/scripts/lua/test_setdtor.lua | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 12cff89fbd..f61cf3fe10 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -1050,7 +1050,7 @@ lua_function( } // called with (desc, obj, dtor), returns proxy object lua_checkdelta(L, -2); - lluau_checkstack(L, 3); // might get up to 6 stack entries +// lluau_checkstack(L, 0); // might get up to 3 stack entries auto desc{ lua_tostdstring(L, 1) }; // Get Lua "references" for each of the object and the dtor function. int objref = lua_ref(L, 2); @@ -1150,6 +1150,8 @@ void setdtor_refs::push_metatable(lua_State* L) return (lhs._target <= rhs._target) end, __newindex = function(t, key, value) + assert(key ~= '_target', + "Don't try to replace a setdtor() proxy's _target") t._target[key] = value end, __call = function(func, ...) diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua index 743c5168d0..61ed86dcc8 100644 --- a/indra/newview/scripts/lua/test_setdtor.lua +++ b/indra/newview/scripts/lua/test_setdtor.lua @@ -7,6 +7,7 @@ print('arithmetic') n = LL.setdtor('arithmetic', 11, print) print("n =", n) print("n._target =", n._target) +print(pcall(function() n._target = 12 end)) print("getmetatable(n) =", inspect(getmetatable(n))) print("-n =", -n) for i = 10, 12 do @@ -20,20 +21,19 @@ for i = 10, 12 do print(`n >= {i}`, n >= tempi) print(`n > {i}`, n > tempi) end -for i = 2, 3 do - print(`n + {i} =`, n + i) - print(`{i} + n =`, i + n) - print(`n - {i} =`, n - i) - print(`{i} - n =`, i - n) - print(`n * {i} =`, n * i) - print(`{i} * n =`, i * n) - print(`n / {i} =`, n / i) - print(`{i} / n =`, i / n) - print(`n % {i} =`, n % i) - print(`{i} % n =`, i % n) - print(`n ^ {i} =`, n ^ i) - print(`{i} ^ n =`, i ^ n) -end +i = 2 +print(`n + {i} =`, n + i) +print(`{i} + n =`, i + n) +print(`n - {i} =`, n - i) +print(`{i} - n =`, i - n) +print(`n * {i} =`, n * i) +print(`{i} * n =`, i * n) +print(`n / {i} =`, n / i) +print(`{i} / n =`, i / n) +print(`n % {i} =`, n % i) +print(`{i} % n =`, i % n) +print(`n ^ {i} =`, n ^ i) +print(`{i} ^ n =`, i ^ n) print('string') s = LL.setdtor('string', 'hello', print) -- cgit v1.2.3 From a098a3d42bf862e0e3789e21f6b8f3f0e71d60d0 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 29 Aug 2024 08:27:43 -0400 Subject: Add Lua script name to log messages. --- indra/llcommon/lua_function.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index f61cf3fe10..be39a7d095 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -26,6 +26,7 @@ // other Linden headers #include "fsyspath.h" #include "hexdump.h" +#include "llcoros.h" #include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" @@ -530,7 +531,8 @@ LuaState::~LuaState() // That's important because we walk the atexit table backwards, to // destroy last the things we created (passed to LL.atexit()) first. int len(lua_objlen(mState, -1)); - LL_DEBUGS("Lua") << "Registry.atexit is a table with " << len << " entries" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with " + << len << " entries" << LL_ENDL; // Push debug.traceback() onto the stack as lua_pcall()'s error // handler function. On error, lua_pcall() calls the specified error @@ -555,15 +557,17 @@ LuaState::~LuaState() // Use lua_pcall() because errors in any one atexit() function // shouldn't cancel the rest of them. Pass debug.traceback() as // the error handler function. - LL_DEBUGS("Lua") << "Calling atexit(" << i << ")" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() + << ": calling atexit(" << i << ")" << LL_ENDL; if (lua_pcall(mState, 0, 0, -2) != LUA_OK) { auto error{ lua_tostdstring(mState, -1) }; - LL_WARNS("Lua") << "atexit(" << i << ") error: " << error << LL_ENDL; + LL_WARNS("Lua") << LLCoros::getName() + << ": atexit(" << i << ") error: " << error << LL_ENDL; // pop error message lua_pop(mState, 1); } - LL_DEBUGS("Lua") << "atexit(" << i << ") done" << LL_ENDL; + LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL; // lua_pcall() has already popped atexit[i]: // stack contains atexit, debug.traceback() } @@ -1277,7 +1281,8 @@ setdtor_refs::~setdtor_refs() // garbage-collected, the call is completely unpredictable from // the consuming script's point of view. But what to do about this // error?? For now, just log it. - LL_WARNS("Lua") << "setdtor(" << std::quoted(desc) << ") error: " + LL_WARNS("Lua") << LLCoros::getName() + << ": setdtor(" << std::quoted(desc) << ") error: " << lua_tostring(L, -1) << LL_ENDL; lua_pop(L, 1); } @@ -1377,7 +1382,8 @@ LuaStackDelta::~LuaStackDelta() // instance to keep its contract wrt the Lua data stack. if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth) { - LL_ERRS("Lua") << mWhere << ": Lua stack went from " << mDepth << " to " << depth; + LL_ERRS("Lua") << LLCoros::getName() << ": " << mWhere + << ": Lua stack went from " << mDepth << " to " << depth; if (mDelta) { LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")"; -- cgit v1.2.3 From 2d5cf36be6e0e367efec2bfa01378146269f33db Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 29 Aug 2024 21:36:39 -0400 Subject: Support next(), pairs(), ipairs() for LL.setdtor() table proxies. Replace the global next(), pairs() and ipairs() functions with a C++ function that drills down through layers of setdtor() proxy objects and then forwards the updated arguments to the original global function. Add a Luau __iter() metamethod to setdtor() proxy objects that, like other proxy metamethods, drills down to the underlying _target object. __iter() recognizes the case of a _target table which itself has a __iter() metamethod. Also add __idiv() metamethod to support integer division. Add tests for proxy // division, next(proxy), next(proxy, key), pairs(proxy), ipairs(proxy) and 'for k, v in proxy'. Also test the case where the table wrapped in the proxy has an __iter() metamethod of its own. --- indra/llcommon/lua_function.cpp | 62 ++++++++++++++++++++++++++++++ indra/newview/scripts/lua/test_setdtor.lua | 29 ++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index be39a7d095..850dff1a33 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,6 +496,9 @@ namespace using LuaStateMap = std::unordered_map; static LuaStateMap sLuaStateMap; +// replacement next(), pairs(), ipairs() that understand setdtor() proxy args +int lua_proxydrill(lua_State* L); + } // anonymous namespace LuaState::LuaState(script_finished_fn cb): @@ -513,6 +516,28 @@ LuaState::LuaState(script_finished_fn cb): lua_register(mState, "print", LuaFunction::get("print_info")); // We don't want to have to prefix require(). lua_register(mState, "require", LuaFunction::get("require")); + + // Replace certain key global functions so they understand our + // LL.setdtor() proxy objects. + // (We could also do this for selected library functions as well, + // e.g. the table, string, math libraries... let's see if needed.) + for (const auto& func : { "next", "pairs", "ipairs" }) + { + // push the function's name string twice + lua_pushstring(mState, func); + lua_pushvalue(mState, -1); + // stack: name, name + // look up the existing global + lua_rawget(mState, LUA_GLOBALSINDEX); + // stack: name, global function + // bind original function as the upvalue for lua_proxydrill() + lua_pushcclosure(mState, lua_proxydrill, + stringize("lua_proxydrill(", func, ')').c_str(), + 1); + // stack: name, lua_proxydrill(func) + // global name = lua_proxydrill(func) + lua_rawset(mState, LUA_GLOBALSINDEX); + } } LuaState::~LuaState() @@ -1165,6 +1190,14 @@ void setdtor_refs::push_metatable(lua_State* L) -- don't fret about arg._target's __tostring metamethod, -- if any, because built-in tostring() deals with that return tostring(arg._target) + end, + __iter = function(arg) + local iter = (getmetatable(arg._target) or {}).__iter + if iter then + return iter(arg._target) + else + return next, arg._target + end end } )-", @@ -1172,6 +1205,7 @@ void setdtor_refs::push_metatable(lua_State* L) binop("sub", "-"), binop("mul", "*"), binop("div", "/"), + binop("idiv", "//"), binop("mod", "%"), binop("pow", "^"), binop("concat", ".."), @@ -1254,6 +1288,34 @@ int setdtor_refs::meta__index(lua_State* L) return 1; } +// replacement for global next(), pairs(), ipairs(): +// its lua_upvalueindex(1) is the original function it's replacing +int lua_proxydrill(lua_State* L) +{ + // Accept however many arguments the original function normally accepts. + // If our first arg is a userdata, check if it's a setdtor_refs proxy. + // Drill through as many levels of proxy wrapper as needed. + while (const setdtor_refs* ptr = lua_toclass(L, 1)) + { + // push original object + lua_getref(L, ptr->objref); + // replace first argument with that + lua_replace(L, 1); + } + // We've reached a first argument that's not a setdtor() proxy. + // How many arguments were we passed, anyway? + int args = lua_gettop(L); + // Push the original function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // Shift the stack so the original function is first. + lua_insert(L, 1); + // Call the original function with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); +} + // When Lua destroys a setdtor_refs userdata object, either from garbage // collection or from LL.atexit(lua_destroybounduserdata), it's time to keep // its promise to call the specified Lua destructor function with the diff --git a/indra/newview/scripts/lua/test_setdtor.lua b/indra/newview/scripts/lua/test_setdtor.lua index 61ed86dcc8..ec5cd47e93 100644 --- a/indra/newview/scripts/lua/test_setdtor.lua +++ b/indra/newview/scripts/lua/test_setdtor.lua @@ -30,6 +30,8 @@ print(`n * {i} =`, n * i) print(`{i} * n =`, i * n) print(`n / {i} =`, n / i) print(`{i} / n =`, i / n) +print(`n // {i} =`, n // i) +print(`{i} // n =`, i // n) print(`n % {i} =`, n % i) print(`{i} % n =`, i % n) print(`n ^ {i} =`, n ^ i) @@ -48,10 +50,37 @@ t = LL.setdtor('table', {'[1]', '[2]', abc='.abc', def='.def'}, print('t =', inspect(t)) print('t._target =', inspect(t._target)) print('#t =', #t) +print('next(t) =', next(t)) +print('next(t, 1) =', next(t, 1)) print('t[2] =', t[2]) print('t.def =', t.def) t[1] = 'new [1]' print('t[1] =', t[1]) +print('for k, v in pairs(t) do') +for k, v in pairs(t) do + print(`{k}: {v}`) +end +print('for k, v in ipairs(t) do') +for k, v in ipairs(t) do + print(`{k}: {v}`) +end +print('for k, v in t do') +for k, v in t do + print(`{k}: {v}`) +end +-- and now for something completely different +setmetatable( + t._target, + { + __iter = function(arg) + return next, {'alternate', '__iter'} + end + } +) +print('for k, v in t with __iter() metamethod do') +for k, v in t do + print(`{k}: {v}`) +end print('function') f = LL.setdtor('function', function(a, b) return (a .. b) end, print) -- cgit v1.2.3 From a662fea2840bd6e8b7856b5f3c8e2f43e706178e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 30 Aug 2024 17:07:58 -0400 Subject: Change LLInstanceTracker::destruct() to erase(). One could argue that LLInstanceTracker is a container of sorts, and erase() is more conventional. This affects no other code, as destruct() is not currently referenced. --- indra/llcommon/llinstancetracker.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h index aba9f1187b..722cb483fe 100644 --- a/indra/llcommon/llinstancetracker.h +++ b/indra/llcommon/llinstancetracker.h @@ -262,19 +262,19 @@ public: virtual const KEY& getKey() const { return mInstanceKey; } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const KEY& key) + static bool erase(const KEY& key) { - return destruct(getInstance(key)); + return erase(getInstance(key)); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const weak_t& ptr) + static bool erase(const weak_t& ptr) { - return destruct(ptr.lock()); + return erase(ptr.lock()); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const ptr_t& ptr) + static bool erase(const ptr_t& ptr) { if (! ptr) { @@ -480,13 +480,13 @@ public: using key_snapshot_of = instance_snapshot_of; /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const weak_t& ptr) + static bool erase(const weak_t& ptr) { - return destruct(ptr.lock()); + return erase(ptr.lock()); } /// for use ONLY for an object we're sure resides on the heap! - static bool destruct(const ptr_t& ptr) + static bool erase(const ptr_t& ptr) { if (! ptr) { -- cgit v1.2.3 From 2eb0d173c3d24997a70b5ca9e78562ba48fc2f51 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 30 Aug 2024 17:13:26 -0400 Subject: Add LLIntTracker, an LLInstanceTracker with generated keys. The point of LLIntTracker is to generate its keys implicitly, so that its int getKey() can be treated more or less like an instance pointer, with the added bonus that the key can be passed around via LLSD. LLIntTracker generates random int keys to try to make it a little harder for one script to mess with an LLIntTracker instance belonging to another. --- indra/llcommon/CMakeLists.txt | 1 + indra/llcommon/llinttracker.h | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 indra/llcommon/llinttracker.h diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 8472eac9f6..8577d94be1 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -189,6 +189,7 @@ set(llcommon_HEADER_FILES llinitparam.h llinstancetracker.h llinstancetrackersubclass.h + llinttracker.h llkeybind.h llkeythrottle.h llleap.h diff --git a/indra/llcommon/llinttracker.h b/indra/llcommon/llinttracker.h new file mode 100644 index 0000000000..86c30bc7aa --- /dev/null +++ b/indra/llcommon/llinttracker.h @@ -0,0 +1,43 @@ +/** + * @file llinttracker.h + * @author Nat Goodspeed + * @date 2024-08-30 + * @brief LLIntTracker isa LLInstanceTracker with generated int keys. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLINTTRACKER_H) +#define LL_LLINTTRACKER_H + +#include "llinstancetracker.h" + +template +class LLIntTracker: public LLInstanceTracker +{ + using super = LLInstanceTracker; +public: + LLIntTracker(): + super(getUniqueKey()) + {} + +private: + static int getUniqueKey() + { + // Find a random key that does NOT already correspond to an instance. + // Passing a duplicate key to LLInstanceTracker would do Bad Things. + int key; + do + { + key = std::rand(); + } while (super::getInstance(key)); + // This could be racy, if we were instantiating new LLIntTrackers + // on multiple threads. If we need that, have to lock between checking + // getInstance() and constructing the new super. + return key; + } +}; + +#endif /* ! defined(LL_LLINTTRACKER_H) */ -- cgit v1.2.3 From 23f0aafb74551a741de8c87d62d74e7c6cee8b01 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 31 Aug 2024 09:42:52 -0400 Subject: Give certain LLInventory queries an API based on result sets. Introduce abstract base class InvResultSet, derived from LLIntTracker so each instance has a unique int key. InvResultSet supports virtual getLength() and getSlice() operations. getSlice() returns an LLSD array limited to MAX_ITEM_LIMIT result set entries. It permits retrieving a "slice" of the contained result set starting at an arbitrary index. A sequence of getSlice() calls can eventually retrieve a whole result set. InvResultSet has subclasses CatResultSet containing cat_array_t, and ItemResultSet containing item_array_t. Each implements a virtual method that produces an LLSD map from a single array item. Make LLInventoryListener::getItemsInfo(), getDirectDescendants() and collectDescendantsIf() instantiate heap CatResultSet and ItemResultSet objects containing the resultant LLPointer arrays, and return their int keys for categories and items. Add LLInventoryListener::getSlice() and closeResult() methods that accept the int keys of result sets. getSlice() returns the requested LLSD array to its caller, while closeResult() is fire-and-forget. Because bulk data transfer is now performed by getSlice() rather than by collectDescendantsIf(), change the latter's "limit" default to unlimited. Allow the C++ code to collect an arbitrary number of LLPointer array entries, as long as getSlice() limits retrieval overhead. Spell "descendants" correctly, unlike the "descendents" spelling embedded in the rest of the viewer... sigh. Make the Lua module provide both spellings. Make MAX_ITEM_LIMIT a U32 instead of F32. In LLInventory.lua, store int result set keys from 'getItemsInfo', 'getDirectDescendants' and 'collectDescendantsIf' in a table with a close() function. The close() function invokes 'closeResult' with the bound int keys. Give that table an __index() metamethod that recognizes only 'categories' and 'items' keys: anything else returns nil. For either of the recognized keys, call 'getSlice' with the corresponding result set key to retrieve (the initial slice of) the actual result set. Cache that result. Lazy retrieval means that if the caller only cares about categories, or only about items, the other result set need never be retrieved at all. This is a first step: like the previous code, it still retrieves only up to the first 100 result set entries. But the C++ code now supports retrieval of additional slices, so extending result set retrieval is mostly Lua work. Finally, wrap the table-with-metamethod in an LL.setdtor() proxy whose destructor calls its close() method to tell LLInventoryListener to destroy the CatResultSet and ItemResultSet with the bound keys. --- indra/newview/llinventorylistener.cpp | 261 ++++++++++++++++++---- indra/newview/llinventorylistener.h | 9 +- indra/newview/scripts/lua/require/LLInventory.lua | 79 ++++++- 3 files changed, 289 insertions(+), 60 deletions(-) diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp index 157e04dce3..c330ef42a0 100644 --- a/indra/newview/llinventorylistener.cpp +++ b/indra/newview/llinventorylistener.cpp @@ -28,12 +28,13 @@ #include "llinventorylistener.h" #include "llappearancemgr.h" +#include "llinttracker.h" #include "llinventoryfunctions.h" #include "lltransutil.h" #include "llwearableitemslist.h" #include "stringize.h" -static const F32 MAX_ITEM_LIMIT = 100; +constexpr U32 MAX_ITEM_LIMIT = 100; LLInventoryListener::LLInventoryListener() : LLEventAPI("LLInventory", @@ -41,7 +42,7 @@ LLInventoryListener::LLInventoryListener() { add("getItemsInfo", "Return information about items or folders defined in [\"item_ids\"]:\n" - "reply will contain [\"items\"] and [\"categories\"] tables accordingly", + "reply will contain [\"items\"] and [\"categories\"] result set keys", &LLInventoryListener::getItemsInfo, llsd::map("item_ids", LLSD(), "reply", LLSD())); @@ -61,78 +62,190 @@ LLInventoryListener::LLInventoryListener() &LLInventoryListener::getBasicFolderID, llsd::map("ft_name", LLSD(), "reply", LLSD())); - add("getDirectDescendents", - "Return the direct descendents(both items and folders) of the [\"folder_id\"]", - &LLInventoryListener::getDirectDescendents, + add("getDirectDescendants", + "Return result set keys [\"categories\"] and [\"items\"] for the direct\n" + "descendants of the [\"folder_id\"]", + &LLInventoryListener::getDirectDescendants, llsd::map("folder_id", LLSD(), "reply", LLSD())); - add("collectDescendentsIf", - "Return the descendents(both items and folders) of the [\"folder_id\"], if it passes specified filters:\n" + add("collectDescendantsIf", + "Return result set keys [\"categories\"] and [\"items\"] for the descendants\n" + "of the [\"folder_id\"], if it passes specified filters:\n" "[\"name\"] is a substring of object's name,\n" "[\"desc\"] is a substring of object's description,\n" "asset [\"type\"] corresponds to the string name of the object's asset type\n" - "[\"limit\"] sets item count limit in reply, maximum and default is 100\n" + "[\"limit\"] sets item count limit in result set (default unlimited)\n" "[\"filter_links\"]: EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)", - &LLInventoryListener::collectDescendentsIf, + &LLInventoryListener::collectDescendantsIf, llsd::map("folder_id", LLSD(), "reply", LLSD())); - } - -void add_item_info(LLEventAPI::Response& response, LLViewerInventoryItem* item) -{ - response["items"].insert(item->getUUID().asString(), - llsd::map("name", item->getName(), - "parent_id", item->getParentUUID(), - "desc", item->getDescription(), - "inv_type", LLInventoryType::lookup(item->getInventoryType()), - "asset_type", LLAssetType::lookup(item->getType()), - "creation_date", (S32) item->getCreationDate(), - "asset_id", item->getAssetUUID(), - "is_link", item->getIsLinkType(), - "linked_id", item->getLinkedUUID())); +/*==========================================================================*| + add("getSingle", + "Return LLSD [\"single\"] for a single folder or item from the specified\n" + "[\"result\"] key at the specified 0-relative [\"index\"].", + &LLInventoryListener::getSingle, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); +|*==========================================================================*/ + + add("getSlice", + stringize( + "Return an LLSD array [\"slice\"] from the specified [\"result\"] key\n" + "starting at 0-relative [\"index\"] with (up to) [\"count\"] entries.\n" + "count is limited to ", MAX_ITEM_LIMIT, " (default and max)."), + &LLInventoryListener::getSlice, + llsd::map("result", LLSD::Integer(), "index", LLSD::Integer(), + "reply", LLSD::String())); + + add("closeResult", + "Release resources associated with specified [\"result\"] key,\n" + "or keys if [\"result\"] is an array.", + &LLInventoryListener::closeResult, + llsd::map("result", LLSD())); } -void add_cat_info(LLEventAPI::Response &response, LLViewerInventoryCategory *cat) +// This abstract base class defines the interface for CatResultSet and +// ItemResultSet. It isa LLIntTracker so we can pass its unique int key to a +// consuming script via LLSD. +struct InvResultSet: public LLIntTracker { - response["categories"].insert(cat->getUUID().asString(), - llsd::map("name", cat->getName(), - "parent_id", cat->getParentUUID(), - "type", LLFolderType::lookup(cat->getPreferredType()))); -} + // Get the length of the result set. Indexes are 0-relative. + virtual int getLength() const = 0; +/*==========================================================================*| + // Retrieve LLSD corresponding to a single entry from the result set, + // with index validation. + LLSD getSingle(int index) const + { + if (0 <= index && index < getLength()) + { + return getSingle_(index); + } + else + { + return {}; + } + } +|*==========================================================================*/ + // Retrieve LLSD corresponding to a single entry from the result set, + // once we're sure the index is valid. + virtual LLSD getSingle(int index) const = 0; + // Retrieve LLSD corresponding to a "slice" of the result set: a + // contiguous sub-array starting at index. The returned LLSD array might + // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the + // specified slice contains the end of the result set. + LLSD getSlice(int index, int count) const + { + // only call getLength() once + auto length = getLength(); + // Adjust bounds [start, end) to overlap the actual result set from + // [0, getLength()). Permit negative index; e.g. with a result set + // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and + // end to 3. + int start = llclamp(index, 0, length); + // Constrain count to MAX_ITEM_LIMIT even before clamping end. + int end = llclamp(index + llclamp(count, 0, MAX_ITEM_LIMIT), 0, length); + LLSD result{ LLSD::emptyArray() }; + // beware of count == 0, or an [index, count) range that doesn't even + // overlap [0, length) at all + if (end > start) + { + // right away expand the result array to the size we'll need + result[end - 1] = LLSD(); + for (int i = start; i < end; ++i) + { + result[i] = getSingle(i); + } + } + return result; + } + + /*---------------- the rest is solely for debug logging ----------------*/ + std::string mName; + + friend std::ostream& operator<<(std::ostream& out, const InvResultSet& self) + { + return out << "InvResultSet(" << self.mName << ", " << self.getKey() << ")"; + } + + InvResultSet(const std::string& name): + mName(name) + { + LL_DEBUGS("Lua") << *this << LL_ENDL; + } + virtual ~InvResultSet() + { + // We want to be able to observe that the consuming script uses + // LL.setdtor() to eventually destroy each of these InvResultSets. + LL_DEBUGS("Lua") << "~" << *this << LL_ENDL; + } +}; -void add_objects_info(LLEventAPI::Response& response, LLInventoryModel::cat_array_t cat_array, LLInventoryModel::item_array_t item_array) +// This struct captures (possibly large) category results from +// getDirectDescendants() and collectDescendantsIf(). +struct CatResultSet: public InvResultSet { - for (auto &p : item_array) + CatResultSet(): InvResultSet("categories") {} + LLInventoryModel::cat_array_t mCategories; + + int getLength() const override { return narrow(mCategories.size()); } + LLSD getSingle(int index) const override { - add_item_info(response, p); + auto cat = mCategories[index]; + return llsd::map("name", cat->getName(), + "parent_id", cat->getParentUUID(), + "type", LLFolderType::lookup(cat->getPreferredType())); } - for (auto &p : cat_array) +}; + +// This struct captures (possibly large) item results from +// getDirectDescendants() and collectDescendantsIf(). +struct ItemResultSet: public InvResultSet +{ + ItemResultSet(): InvResultSet("items") {} + LLInventoryModel::item_array_t mItems; + + int getLength() const override { return narrow(mItems.size()); } + LLSD getSingle(int index) const override { - add_cat_info(response, p); + auto item = mItems[index]; + return llsd::map("name", item->getName(), + "parent_id", item->getParentUUID(), + "desc", item->getDescription(), + "inv_type", LLInventoryType::lookup(item->getInventoryType()), + "asset_type", LLAssetType::lookup(item->getType()), + "creation_date", LLSD::Integer(item->getCreationDate()), + "asset_id", item->getAssetUUID(), + "is_link", item->getIsLinkType(), + "linked_id", item->getLinkedUUID()); } -} +}; void LLInventoryListener::getItemsInfo(LLSD const &data) { Response response(LLSD(), data); + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + uuid_vec_t ids = LLSDParam(data["item_ids"]); for (auto &it : ids) { LLViewerInventoryItem* item = gInventory.getItem(it); if (item) { - add_item_info(response, item); + itemresult->mItems.push_back(item); } else { LLViewerInventoryCategory *cat = gInventory.getCategory(it); if (cat) { - add_cat_info(response, cat); + catresult->mCategories.push_back(cat); } } } + response["categories"] = catresult->getKey(); + response["items"] = itemresult->getKey(); } void LLInventoryListener::getFolderTypeNames(LLSD const &data) @@ -151,14 +264,21 @@ void LLInventoryListener::getBasicFolderID(LLSD const &data) } -void LLInventoryListener::getDirectDescendents(LLSD const &data) +void LLInventoryListener::getDirectDescendants(LLSD const &data) { Response response(LLSD(), data); LLInventoryModel::cat_array_t* cats; LLInventoryModel::item_array_t* items; gInventory.getDirectDescendentsOf(data["folder_id"], cats, items); - add_objects_info(response, *cats, *items); + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; + + catresult->mCategories = *cats; + itemresult->mItems = *items; + + response["categories"] = catresult->getKey(); + response["items"] = itemresult->getKey(); } struct LLFilteredCollector : public LLInventoryCollectFunctor @@ -173,7 +293,11 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor LLFilteredCollector(LLSD const &data); virtual ~LLFilteredCollector() {} virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item) override; - virtual bool exceedsLimit() override { return (mItemLimit <= mItemCount); }; + virtual bool exceedsLimit() override + { + // mItemLimit == 0 means unlimited + return (mItemLimit && mItemLimit <= mItemCount); + } protected: bool checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item); @@ -189,7 +313,7 @@ struct LLFilteredCollector : public LLInventoryCollectFunctor S32 mItemCount; }; -void LLInventoryListener::collectDescendentsIf(LLSD const &data) +void LLInventoryListener::collectDescendantsIf(LLSD const &data) { Response response(LLSD(), data); LLUUID folder_id(data["folder_id"].asUUID()); @@ -198,20 +322,63 @@ void LLInventoryListener::collectDescendentsIf(LLSD const &data) { return response.error(stringize("Folder ", std::quoted(data["folder_id"].asString()), " was not found")); } - LLInventoryModel::cat_array_t cat_array; - LLInventoryModel::item_array_t item_array; + auto catresult = new CatResultSet; + auto itemresult = new ItemResultSet; LLFilteredCollector collector = LLFilteredCollector(data); - gInventory.collectDescendentsIf(folder_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector); + // Populate results directly into the catresult and itemresult arrays. + // TODO: sprinkle count-based coroutine yields into the real + // collectDescendentsIf() method so it doesn't steal too many cycles. + gInventory.collectDescendentsIf( + folder_id, + catresult->mCategories, + itemresult->mItems, + LLInventoryModel::EXCLUDE_TRASH, + collector); + + response["categories"] = catresult->getKey(); + response["items"] = itemresult->getKey(); +} + +/*==========================================================================*| +void LLInventoryListener::getSingle(LLSD const& data) +{ + auto result = InvResultSet::getInstance(data["result"]); + sendReply(llsd::map("single", result->getSingle(data["index"])), data); +} +|*==========================================================================*/ - add_objects_info(response, cat_array, item_array); +void LLInventoryListener::getSlice(LLSD const& data) +{ + auto result = InvResultSet::getInstance(data["result"]); + int count = data.has("count")? data["count"].asInteger() : MAX_ITEM_LIMIT; + LL_DEBUGS("Lua") << *result << ".getSlice(" << data["index"].asInteger() + << ", " << count << ')' << LL_ENDL; + sendReply(llsd::map("slice", result->getSlice(data["index"], count)), data); +} + +void LLInventoryListener::closeResult(LLSD const& data) +{ + LLSD results = data["result"]; + if (results.isInteger()) + { + results = llsd::array(results); + } + for (const auto& result : llsd::inArray(results)) + { + auto ptr = InvResultSet::getInstance(result); + if (ptr) + { + delete ptr.get(); + } + } } LLFilteredCollector::LLFilteredCollector(LLSD const &data) : mType(LLAssetType::EType::AT_UNKNOWN), mLinkFilter(INCLUDE_LINKS), - mItemLimit(MAX_ITEM_LIMIT), + mItemLimit(0), mItemCount(0) { @@ -235,7 +402,7 @@ LLFilteredCollector::LLFilteredCollector(LLSD const &data) : } if (data["limit"].isInteger()) { - mItemLimit = llclamp(data["limit"].asInteger(), 1, MAX_ITEM_LIMIT); + mItemLimit = std::max(data["limit"].asInteger(), 1); } } diff --git a/indra/newview/llinventorylistener.h b/indra/newview/llinventorylistener.h index 5cbac2ca32..a05385f2c8 100644 --- a/indra/newview/llinventorylistener.h +++ b/indra/newview/llinventorylistener.h @@ -40,8 +40,13 @@ private: void getFolderTypeNames(LLSD const &data); void getAssetTypeNames(LLSD const &data); void getBasicFolderID(LLSD const &data); - void getDirectDescendents(LLSD const &data); - void collectDescendentsIf(LLSD const &data); + void getDirectDescendants(LLSD const &data); + void collectDescendantsIf(LLSD const &data); +/*==========================================================================*| + void getSingle(LLSD const& data); +|*==========================================================================*/ + void getSlice(LLSD const& data); + void closeResult(LLSD const& data); }; #endif // LL_LLINVENTORYLISTENER_H diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua index dd1b910250..0ff6b9fb37 100644 --- a/indra/newview/scripts/lua/require/LLInventory.lua +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -1,12 +1,66 @@ local leap = require 'leap' local mapargs = require 'mapargs' +local function result(keys) + return LL.setdtor( + 'LLInventory result', + setmetatable( + -- the basic table wrapped by setmetatable just captures the int + -- result-set keys from 'keys', but with underscore prefixes + { + _categories=keys.categories, + _items=keys.items, + -- call result:close() to release result sets before garbage + -- collection or script completion + close = function(self) + leap.send('LLInventory', + {op='closeResult', + result={self._categories, self._items}}) + end + }, + -- The caller of one of our methods that returns a result set + -- isn't necessarily interested in both categories and items, so + -- don't proactively populate both. Instead, when caller references + -- either 'categories' or 'items', the __index() metamethod + -- populates that field. + { + __index = function(t, key) + -- we really don't care about references to any other field + if not table.find({'categories', 'items'}, key) then + return nil + end + -- We cleverly saved the int result set key in a field + -- with the same name but an underscore prefix. + local resultkey = t['_' .. key] + -- TODO: This only ever fetches the FIRST slice. What we + -- really want is to return a table with metamethods that + -- manage indexed access and table iteration. + -- Remember our C++ entry point uses 0-relative indexing. + local slice = leap.request( + 'LLInventory', + {op='getSlice', result=resultkey, index=0}).slice + print(`getSlice({resultkey}, 0) => {slice} ({#slice} entries)`) + -- cache this slice for future reference + t[key] = slice + return slice + end + } + ), + -- When the table-with-metatable above is destroyed, tell LLInventory + -- we're done with its result sets -- whether or not we ever fetched + -- either of them. + function(keys) + keys:close() + end + ) +end + local LLInventory = {} -- Get the items/folders info by provided IDs, -- reply will contain "items" and "categories" tables accordingly function LLInventory.getItemsInfo(item_ids) - return leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids}) + return result(leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids})) end -- Get the table of folder type names, which can be later used to get the ID of the basic folders @@ -19,30 +73,33 @@ function LLInventory.getBasicFolderID(ft_name) return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id end --- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendentsIf(...) +-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendantsIf(...) function LLInventory.getAssetTypeNames() return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names end --- Get the direct descendents of the 'folder_id' provided, +-- Get the direct descendants of the 'folder_id' provided, -- reply will contain "items" and "categories" tables accordingly -function LLInventory.getDirectDescendents(folder_id) - return leap.request('LLInventory', {op = 'getDirectDescendents', folder_id=folder_id}) +function LLInventory.getDirectDescendants(folder_id) + return result(leap.request('LLInventory', {op = 'getDirectDescendants', folder_id=folder_id})) end +-- backwards compatibility +LLInventory.getDirectDescendents = LLInventory.getDirectDescendants --- Get the descendents of the 'folder_id' provided, which pass specified filters +-- Get the descendants of the 'folder_id' provided, which pass specified filters -- reply will contain "items" and "categories" tables accordingly --- LLInventory.collectDescendentsIf{ folder_id -- parent folder ID +-- LLInventory.collectDescendantsIf{ folder_id -- parent folder ID -- [, name] -- name (substring) -- [, desc] -- description (substring) -- [, type] -- asset type -- [, limit] -- item count limit in reply, maximum and default is 100 -- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default) -function LLInventory.collectDescendentsIf(...) +function LLInventory.collectDescendantsIf(...) local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...) - args.op = 'collectDescendentsIf' - return leap.request('LLInventory', args) + args.op = 'collectDescendantsIf' + return result(leap.request('LLInventory', args)) end - +-- backwards compatibility +LLInventory.collectDescendentsIf = LLInventory.collectDescendantsIf return LLInventory -- cgit v1.2.3 From 15db5010a0330bcb2ca2d0e4125ac3374f22b9cf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 31 Aug 2024 12:37:57 -0400 Subject: Make global pairs(), ipairs() honor metamethods. Specifically, make pairs(obj) honor obj's __iter() metamethod if any. Make ipairs(obj) honor obj's __index() metamethod, if any. Given the semantics of the __index() metamethod, though, this only works for a proxy table if the proxy has no array entries (int keys) of its own. --- indra/llcommon/lua_function.cpp | 132 +++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 16 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 850dff1a33..e4758d25a4 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,8 +496,19 @@ namespace using LuaStateMap = std::unordered_map; static LuaStateMap sLuaStateMap; -// replacement next(), pairs(), ipairs() that understand setdtor() proxy args +// replace table-at-index[name] with passed func, +// binding the original table-at-index[name] as func's upvalue +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func); + +// replacement next() function that understands setdtor() proxy args int lua_proxydrill(lua_State* L); +// replacement pairs() function that supports __iter() metamethod +int lua_metapairs(lua_State* L); +// replacement ipairs() function that supports __index() metamethod +int lua_metaipairs(lua_State* L); +// helper for lua_metaipairs() (actual generator function) +int lua_metaipair(lua_State* L); } // anonymous namespace @@ -521,25 +532,114 @@ LuaState::LuaState(script_finished_fn cb): // LL.setdtor() proxy objects. // (We could also do this for selected library functions as well, // e.g. the table, string, math libraries... let's see if needed.) - for (const auto& func : { "next", "pairs", "ipairs" }) + replace_entry(mState, LUA_GLOBALSINDEX, "next", lua_proxydrill); + // Replacing pairs() with lua_metapairs() makes global pairs() honor + // objects with __iter() metamethods. + replace_entry(mState, LUA_GLOBALSINDEX, "pairs", lua_metapairs); + // Replacing ipairs() with lua_metaipairs() makes global ipairs() honor + // objects with __index() metamethods -- as long as the object in question + // has no array entries (int keys) of its own. (If it does, object[i] will + // retrieve table[i] instead of calling __index(table, i).) + replace_entry(mState, LUA_GLOBALSINDEX, "ipairs", lua_metaipairs); +} + +namespace +{ + +void replace_entry(lua_State* L, int index, + const std::string& name, lua_CFunction func) +{ + index = lua_absindex(L, index); + lua_checkdelta(L); + // push the function's name string twice + lua_pushlstring(L, name.data(), name.length()); + lua_pushvalue(L, -1); + // stack: name, name + // look up the existing table entry + lua_rawget(L, index); + // stack: name, original function + // bind original function as the upvalue for func() + lua_pushcclosure(L, func, (name + "()").c_str(), 1); + // stack: name, func-with-bound-original + // table[name] = func-with-bound-original + lua_rawset(L, index); +} + +int lua_metapairs(lua_State* L) +{ + // pairs(obj): object is at index 1 + // discard any erroneous surplus parameters + lua_settop(L, 1); + // stack: obj + if (luaL_getmetafield(L, 1, "__iter")) + { + // stack: obj, getmetatable(obj).__iter + lua_insert(L, 1); + // stack: __iter, obj + // We don't use the even nicer shorthand luaL_callmeta() because + // luaL_callmeta() only permits the metamethod to return a single + // value, and __iter() returns up to 3. Use lua_call() instead. + lua_call(L, 1, LUA_MULTRET); + // return as many values as __iter(obj) returned + return lua_gettop(L); + } + // otherwise, just return (next, obj) + lluau_checkstack(L, 1); + // stack: obj + lua_getglobal(L, "next"); + // stack: obj, next + lua_insert(L, 1); + // stack: next, obj + return 2; +} + +int lua_metaipairs(lua_State* L) +{ + lua_checkdelta(L, 2); + // ipairs(obj): object is at index 1 + // discard any erroneous surplus parameters + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; +} + +int lua_metaipair(lua_State* L) +{ + // called with (obj, previous-index) + // increment previous-index for this call + lua_Integer i = luaL_checkinteger(L, 2) + 1; + lua_pop(L, 1); + // stack: obj + lua_pushinteger(L, i); + // stack: obj, i + lua_pushvalue(L, -1); + // stack: obj, i, i + lua_insert(L, 1); + // stack: i, obj, i + lua_gettable(L, -2); + // stack: i, obj, obj[i] + lua_remove(L, -2); + // stack: i, obj[i] + if (! lua_isnil(L, -1)) { - // push the function's name string twice - lua_pushstring(mState, func); - lua_pushvalue(mState, -1); - // stack: name, name - // look up the existing global - lua_rawget(mState, LUA_GLOBALSINDEX); - // stack: name, global function - // bind original function as the upvalue for lua_proxydrill() - lua_pushcclosure(mState, lua_proxydrill, - stringize("lua_proxydrill(", func, ')').c_str(), - 1); - // stack: name, lua_proxydrill(func) - // global name = lua_proxydrill(func) - lua_rawset(mState, LUA_GLOBALSINDEX); + // great, obj[i] isn't nil: return (i, obj[i]) + return 2; } + // obj[i] is nil. ipairs() is documented to stop at the first hole, + // regardless of #obj. Clear the stack, i.e. return nil. + lua_settop(L, 0); + return 0; } +} // anonymous namespace + LuaState::~LuaState() { // We're just about to destroy this lua_State mState. Did this Lua chunk -- cgit v1.2.3 From 8c18cfd22583e981f93734ec0aa6ee0ead3f26c5 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 2 Sep 2024 15:26:34 -0400 Subject: Make `pairs()`, `ipairs()` forward to original funcs if no metamethods. That is, our replacement `pairs()` forwards the call to built-in `pairs()` when the passed object has no `__iter()` metamethod. Similarly, our replacement `ipairs()` forwards to built-in `ipairs()` when the passed object has no `__index()` metamethod. This allows for the possibility that the built-in `pairs()` and `ipairs()` functions engage more efficient implementations than the obvious ones. --- indra/llcommon/lua_function.cpp | 87 +++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e4758d25a4..da88f57a5b 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -568,46 +568,67 @@ void replace_entry(lua_State* L, int index, int lua_metapairs(lua_State* L) { // pairs(obj): object is at index 1 - // discard any erroneous surplus parameters - lua_settop(L, 1); - // stack: obj + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... if (luaL_getmetafield(L, 1, "__iter")) { - // stack: obj, getmetatable(obj).__iter - lua_insert(L, 1); - // stack: __iter, obj - // We don't use the even nicer shorthand luaL_callmeta() because - // luaL_callmeta() only permits the metamethod to return a single - // value, and __iter() returns up to 3. Use lua_call() instead. - lua_call(L, 1, LUA_MULTRET); - // return as many values as __iter(obj) returned - return lua_gettop(L); + // stack: obj, ..., getmetatable(obj).__iter + } + else + { + // Push the original pairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original pairs() } - // otherwise, just return (next, obj) - lluau_checkstack(L, 1); - // stack: obj - lua_getglobal(L, "next"); - // stack: obj, next lua_insert(L, 1); - // stack: next, obj - return 2; + // stack: (__iter() or pairs()), obj, ... + // call whichever function(obj, ...) (args args, up to 3 return values) + lua_call(L, args, LUA_MULTRET); + // return as many values as the selected function returned + return lua_gettop(L); } int lua_metaipairs(lua_State* L) { - lua_checkdelta(L, 2); // ipairs(obj): object is at index 1 - // discard any erroneous surplus parameters - lua_settop(L, 1); - // stack: obj - lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); - // stack: obj, lua_metaipair - lua_insert(L, 1); - // stack: lua_metaipair, obj - // push explicit 0 so lua_metaipair need not special-case nil - lua_pushinteger(L, 0); - // stack: lua_metaipair, obj, 0 - return 3; + // How many args were we passed? + int args = lua_gettop(L); + // stack: obj, ... + if (luaL_getmetafield(L, 1, "__index")) + { + // stack: obj, ..., getmetatable(obj).__index + // discard __index and everything but obj: + // we don't want to call __index(), just check its presence + lua_settop(L, 1); + // stack: obj + lua_pushcfunction(L, lua_metaipair, "lua_metaipair"); + // stack: obj, lua_metaipair + lua_insert(L, 1); + // stack: lua_metaipair, obj + // push explicit 0 so lua_metaipair need not special-case nil + lua_pushinteger(L, 0); + // stack: lua_metaipair, obj, 0 + return 3; + } + else // no __index() metamethod + { + // Although our lua_metaipair() function demonstrably works whether or + // not our object has an __index() metamethod, the code below assumes + // that the Lua engine may have a more efficient implementation for + // built-in ipairs() than our lua_metaipair(). + // Push the original ipairs() function, captured as our upvalue. + lua_pushvalue(L, lua_upvalueindex(1)); + // stack: obj, ..., original ipairs() + // Shift the stack so the original function is first. + lua_insert(L, 1); + // stack: original ipairs(), obj, ... + // Call original ipairs() with all original args, no error checking. + // Don't truncate however many values that function returns. + lua_call(L, args, LUA_MULTRET); + // Return as many values as the original function returned. + return lua_gettop(L); + } } int lua_metaipair(lua_State* L) @@ -624,7 +645,7 @@ int lua_metaipair(lua_State* L) lua_insert(L, 1); // stack: i, obj, i lua_gettable(L, -2); - // stack: i, obj, obj[i] + // stack: i, obj, obj[i] (honoring __index()) lua_remove(L, -2); // stack: i, obj[i] if (! lua_isnil(L, -1)) @@ -1388,7 +1409,7 @@ int setdtor_refs::meta__index(lua_State* L) return 1; } -// replacement for global next(), pairs(), ipairs(): +// replacement for global next(): // its lua_upvalueindex(1) is the original function it's replacing int lua_proxydrill(lua_State* L) { -- cgit v1.2.3 From 1101ed699a0c3c23c0bb11267d390febfdc02409 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 2 Sep 2024 16:41:09 -0400 Subject: Introduce result_view.lua, and use it in LLInventory.lua. result_view(key_length, fetch) returns a virtual view of a potentially-large C++ result set. Given the result-set key, its total length and a function fetch(key, start) => (slice, adjusted start), the read-only table returned by result_view() manages indexed access and table iteration over the entire result set, fetching a slice at a time as required. Change LLInventory to use result_view() instead of only ever fetching the first slice of a result set. TODO: This depends on the viewer's "LLInventory" listener returning the total result set length as well as the result set key. It does not yet return the length. --- indra/newview/scripts/lua/require/LLInventory.lua | 38 +++++++------- indra/newview/scripts/lua/require/result_view.lua | 62 +++++++++++++++++++++++ 2 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 indra/newview/scripts/lua/require/result_view.lua diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua index 0ff6b9fb37..ce501e75f3 100644 --- a/indra/newview/scripts/lua/require/LLInventory.lua +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -1,12 +1,14 @@ local leap = require 'leap' local mapargs = require 'mapargs' +local result_view = require 'result_view' local function result(keys) return LL.setdtor( 'LLInventory result', setmetatable( -- the basic table wrapped by setmetatable just captures the int - -- result-set keys from 'keys', but with underscore prefixes + -- result-set {key, length} pairs from 'keys', but with underscore + -- prefixes { _categories=keys.categories, _items=keys.items, @@ -15,7 +17,7 @@ local function result(keys) close = function(self) leap.send('LLInventory', {op='closeResult', - result={self._categories, self._items}}) + result={self._categories[1], self._items[1]}}) end }, -- The caller of one of our methods that returns a result set @@ -24,25 +26,25 @@ local function result(keys) -- either 'categories' or 'items', the __index() metamethod -- populates that field. { - __index = function(t, key) + __index = function(t, field) -- we really don't care about references to any other field - if not table.find({'categories', 'items'}, key) then + if not table.find({'categories', 'items'}, field) then return nil end - -- We cleverly saved the int result set key in a field - -- with the same name but an underscore prefix. - local resultkey = t['_' .. key] - -- TODO: This only ever fetches the FIRST slice. What we - -- really want is to return a table with metamethods that - -- manage indexed access and table iteration. - -- Remember our C++ entry point uses 0-relative indexing. - local slice = leap.request( - 'LLInventory', - {op='getSlice', result=resultkey, index=0}).slice - print(`getSlice({resultkey}, 0) => {slice} ({#slice} entries)`) - -- cache this slice for future reference - t[key] = slice - return slice + local view = result_view( + -- We cleverly saved the result set {key, length} pair in + -- a field with the same name but an underscore prefix. + t['_' .. field], + function(key, start) + local fetched = leap.request( + 'LLInventory', + {op='getSlice', result=key, index=start}) + return fetched.slice, fetched.start + end + ) + -- cache that view for future reference + t[field] = view + return view end } ), diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua new file mode 100644 index 0000000000..4a58636f2f --- /dev/null +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -0,0 +1,62 @@ +-- result_view(key_length, fetch) returns a table which stores only a slice +-- of a result set plus some control values, yet presents read-only virtual +-- access to the entire result set. +-- key_length: {result set key, total result set length} +-- fetch: function(key, start) that returns (slice, adjusted start) +local function result_view(key_length, fetch) + return setmetatable( + { + key=key_length[1], + length=key_length[2], + -- C++ result sets use 0-based indexing, so internally we do too + start=0, + -- start with a dummy array with length 0 + slice={} + }, + { + __len = function(this) + return this.length + end, + __index = function(this, i) + -- right away, convert to 0-relative indexing + i -= 1 + -- can we find this index within the current slice? + local reli = i - this.start + if 0 <= reli and reli < #this.slice then + return this.slice[reli] + end + -- is this index outside the overall result set? + if not (0 <= i and i < this.length) then + return nil + end + -- fetch a new slice starting at i, using provided fetch() + local start + this.slice, start = fetch(key, i) + -- It's possible that caller-provided fetch() function forgot + -- to return the adjusted start index of the new slice. In + -- Lua, 0 tests as true, so if fetch() returned (slice, 0), + -- we'll duly reset this.start to 0. + if start then + this.start = start + end + -- hopefully this slice contains the desired i + return this.slice[i - this.start] + end, + -- We purposely avoid putting any array entries (int keys) into + -- our table so that access to any int key will always call our + -- __index() metamethod. Moreover, we want any table iteration to + -- call __index(table, i) however many times; we do NOT want it to + -- retrieve key, length, start, slice. + -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'. + __iter = ipairs, + -- This result set provides read-only access. + -- We do not support pushing updates to individual items back to + -- C++; for the intended use cases, that makes no sense. + __newindex = function(this, i, value) + error("result_view is a read-only data structure", 2) + end + } + ) +end + +return result_view -- cgit v1.2.3 From 9dc916bfcafd43890be20623d359be82e84f73ac Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 12:28:25 -0400 Subject: In lua_what() and lua_stack(), try to report a function's name. --- indra/llcommon/lua_function.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index da88f57a5b..67ca29c689 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -567,6 +567,7 @@ void replace_entry(lua_State* L, int index, int lua_metapairs(lua_State* L) { +// LuaLog debug(L, "lua_metapairs()"); // pairs(obj): object is at index 1 // How many args were we passed? int args = lua_gettop(L); @@ -591,6 +592,7 @@ int lua_metapairs(lua_State* L) int lua_metaipairs(lua_State* L) { +// LuaLog debug(L, "lua_metaipairs()"); // ipairs(obj): object is at index 1 // How many args were we passed? int args = lua_gettop(L); @@ -633,9 +635,10 @@ int lua_metaipairs(lua_State* L) int lua_metaipair(lua_State* L) { +// LuaLog debug(L, "lua_metaipair()"); // called with (obj, previous-index) // increment previous-index for this call - lua_Integer i = luaL_checkinteger(L, 2) + 1; + lua_Integer i = luaL_optinteger(L, 2, 0) + 1; lua_pop(L, 1); // stack: obj lua_pushinteger(L, i); @@ -1523,6 +1526,31 @@ std::ostream& operator<<(std::ostream& out, const lua_what& self) out << lua_touserdata(self.L, self.index); break; + case LUA_TFUNCTION: + { + // Try for the function's name, at the cost of a few more stack + // entries. + lua_checkdelta(self.L); + lluau_checkstack(self.L, 3); + lua_getglobal(self.L, "debug"); + // stack: ..., debug + lua_getfield(self.L, -1, "info"); + // stack: ..., debug, debug.info + lua_remove(self.L, -2); + // stack: ..., debug.info + lua_pushvalue(self.L, self.index); + // stack: ..., debug.info, this function + lua_pushstring(self.L, "n"); + // stack: ..., debug.info, this function, "n" + // 2 arguments, 1 return value (or error message), no error handler + lua_pcall(self.L, 2, 1, 0); + // stack: ..., function name (or error) from debug.info() + out << "function " << lua_tostdstring(self.L, -1); + lua_pop(self.L, 1); + // stack: ... + break; + } + default: // anything else, don't bother trying to report value, just type out << lua_typename(self.L, lua_type(self.L, self.index)); -- cgit v1.2.3 From 6a4b9b1184c142ca1b317296fa12304bf231fc7d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 12:34:21 -0400 Subject: Add test_result_view.lua; fix minor bugs in result_view.lua. --- indra/newview/scripts/lua/require/result_view.lua | 18 ++++---- indra/newview/scripts/lua/test_result_view.lua | 55 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 indra/newview/scripts/lua/test_result_view.lua diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua index 4a58636f2f..d53d953c24 100644 --- a/indra/newview/scripts/lua/require/result_view.lua +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -23,7 +23,8 @@ local function result_view(key_length, fetch) -- can we find this index within the current slice? local reli = i - this.start if 0 <= reli and reli < #this.slice then - return this.slice[reli] + -- Lua 1-relative indexing + return this.slice[reli + 1] end -- is this index outside the overall result set? if not (0 <= i and i < this.length) then @@ -31,16 +32,17 @@ local function result_view(key_length, fetch) end -- fetch a new slice starting at i, using provided fetch() local start - this.slice, start = fetch(key, i) + this.slice, start = fetch(this.key, i) -- It's possible that caller-provided fetch() function forgot -- to return the adjusted start index of the new slice. In -- Lua, 0 tests as true, so if fetch() returned (slice, 0), - -- we'll duly reset this.start to 0. - if start then - this.start = start - end - -- hopefully this slice contains the desired i - return this.slice[i - this.start] + -- we'll duly reset this.start to 0. Otherwise, assume the + -- requested index was not adjusted: that the returned slice + -- really does start at i. + this.start = start or i + -- Hopefully this slice contains the desired i. + -- Back to 1-relative indexing. + return this.slice[i - this.start + 1] end, -- We purposely avoid putting any array entries (int keys) into -- our table so that access to any int key will always call our diff --git a/indra/newview/scripts/lua/test_result_view.lua b/indra/newview/scripts/lua/test_result_view.lua new file mode 100644 index 0000000000..304633a472 --- /dev/null +++ b/indra/newview/scripts/lua/test_result_view.lua @@ -0,0 +1,55 @@ +-- Verify the functionality of result_view. +result_view = require 'result_view' + +print('alphabet') +alphabet = "abcdefghijklmnopqrstuvwxyz" +assert(#alphabet == 26) +alphabits = string.split(alphabet, '') + +print('function slice()') +function slice(t, index, count) + return table.move(t, index, index + count - 1, 1, {}) +end + +print('verify slice()') +-- verify that slice() does what we expect +assert(table.concat(slice(alphabits, 4, 3)) == "def") +assert(table.concat(slice(alphabits, 14, 3)) == "nop") +assert(table.concat(slice(alphabits, 25, 3)) == "yz") + +print('function fetch()') +function fetch(key, index) + -- fetch function is defined to be 0-relative: fix for Lua data + -- constrain view of alphabits to slices of at most 3 elements + return slice(alphabits, index+1, 3), index +end + +print('result_view()') +-- for test purposes, key is irrelevant, so just 'key' +view = result_view({'key', #alphabits}, fetch) + +print('function check_iter()') +function check_iter(...) + result = {} + for k, v in ... do + table.insert(result, v) + end + assert(table.concat(result) == alphabet) +end + +print('check_iter(pairs(view))') +check_iter(pairs(view)) +print('check_iter(ipairs(view))') +check_iter(ipairs(view)) +print('check_iter(view)') +check_iter(view) + +print('raw index access') +assert(view[5] == 'e') +assert(view[10] == 'j') +assert(view[15] == 'o') +assert(view[20] == 't') +assert(view[25] == 'y') + +print('Success!') + -- cgit v1.2.3 From ab04d116cc47fa979018525fce4f11b379aeeec5 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 12:35:55 -0400 Subject: Break out llinventorylistener.cpp's InvResultSet as LL::ResultSet. We may well want to leverage that API for additional queries that could potentially return large datasets. --- indra/llcommon/CMakeLists.txt | 2 + indra/llcommon/resultset.cpp | 93 ++++++++++++++++++++++++++++ indra/llcommon/resultset.h | 61 +++++++++++++++++++ indra/newview/llinventorylistener.cpp | 111 ++++++---------------------------- 4 files changed, 175 insertions(+), 92 deletions(-) create mode 100644 indra/llcommon/resultset.cpp create mode 100644 indra/llcommon/resultset.h diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 8577d94be1..8c346ea6ce 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -112,6 +112,7 @@ set(llcommon_SOURCE_FILES lockstatic.cpp lua_function.cpp lualistener.cpp + resultset.cpp threadpool.cpp throttle.cpp u64.cpp @@ -261,6 +262,7 @@ set(llcommon_HEADER_FILES lockstatic.h lua_function.h lualistener.h + resultset.h stdtypes.h stringize.h tempset.h diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp new file mode 100644 index 0000000000..ee8cc68c6c --- /dev/null +++ b/indra/llcommon/resultset.cpp @@ -0,0 +1,93 @@ +/** + * @file resultset.cpp + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief Implementation for resultset. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "resultset.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llerror.h" +#include "llsdutil.h" + +namespace LL +{ + +LLSD ResultSet::getKeyLength() const +{ + return llsd::array(getKey(), getLength()); +} + +std::pair ResultSet::getSliceStart(int index, int count) const +{ + // only call getLength() once + auto length = getLength(); + // Adjust bounds [start, end) to overlap the actual result set from + // [0, getLength()). Permit negative index; e.g. with a result set + // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and + // end to 3. + int start = llclamp(index, 0, length); + int end = llclamp(index + count, 0, length); + LLSD result{ LLSD::emptyArray() }; + // beware of count <= 0, or an [index, count) range that doesn't even + // overlap [0, length) at all + if (end > start) + { + // right away expand the result array to the size we'll need + result[end - 1] = LLSD(); + for (int i = start; i < end; ++i) + { + result[i] = getSingle(i); + } + } + return { result, start }; +} + +LLSD ResultSet::getSlice(int index, int count) const +{ + return getSliceStart(index, count).first; +} + +/*==========================================================================*| +LLSD ResultSet::getSingle(int index) const +{ + if (0 <= index && index < getLength()) + { + return getSingle_(index); + } + else + { + return {}; + } +} +|*==========================================================================*/ + +ResultSet::ResultSet(const std::string& name): + mName(name) +{ + LL_DEBUGS("Lua") << *this << LL_ENDL; +} + +ResultSet::~ResultSet() +{ + // We want to be able to observe that the consuming script eventually + // destroys each of these ResultSets. + LL_DEBUGS("Lua") << "~" << *this << LL_ENDL; +} + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self) +{ + return out << "ResultSet(" << self.mName << ", " << self.getKey() << ")"; +} diff --git a/indra/llcommon/resultset.h b/indra/llcommon/resultset.h new file mode 100644 index 0000000000..9b9ecbb21e --- /dev/null +++ b/indra/llcommon/resultset.h @@ -0,0 +1,61 @@ +/** + * @file resultset.h + * @author Nat Goodspeed + * @date 2024-09-03 + * @brief ResultSet is an abstract base class to allow scripted access to + * potentially large collections representable as LLSD arrays. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_RESULTSET_H) +#define LL_RESULTSET_H + +#include "llinttracker.h" +#include "llsd.h" +#include // std::ostream +#include // std::pair + +namespace LL +{ + +// This abstract base class defines an interface by which a large collection +// of items representable as an LLSD array can be retrieved in slices. It isa +// LLIntTracker so we can pass its unique int key to a consuming script via +// LLSD. +struct ResultSet: public LLIntTracker +{ + // Get the length of the result set. Indexes are 0-relative. + virtual int getLength() const = 0; + // Get conventional LLSD { key, length } pair. + LLSD getKeyLength() const; + // Retrieve LLSD corresponding to a single entry from the result set, + // once we're sure the index is valid. + virtual LLSD getSingle(int index) const = 0; + // Retrieve LLSD corresponding to a "slice" of the result set: a + // contiguous sub-array starting at index. The returned LLSD array might + // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the + // specified slice contains the end of the result set. + LLSD getSlice(int index, int count) const; + // Like getSlice(), but also return adjusted start position. + std::pair getSliceStart(int index, int count) const; +/*==========================================================================*| + // Retrieve LLSD corresponding to a single entry from the result set, + // with index validation. + LLSD getSingle(int index) const; +|*==========================================================================*/ + + /*---------------- the rest is solely for debug logging ----------------*/ + std::string mName; + + ResultSet(const std::string& name); + virtual ~ResultSet(); +}; + +} // namespace LL + +std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self); + +#endif /* ! defined(LL_RESULTSET_H) */ diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp index c330ef42a0..6a8182c539 100644 --- a/indra/newview/llinventorylistener.cpp +++ b/indra/newview/llinventorylistener.cpp @@ -28,13 +28,14 @@ #include "llinventorylistener.h" #include "llappearancemgr.h" -#include "llinttracker.h" #include "llinventoryfunctions.h" #include "lltransutil.h" #include "llwearableitemslist.h" +#include "resultset.h" #include "stringize.h" +#include // std::min() -constexpr U32 MAX_ITEM_LIMIT = 100; +constexpr S32 MAX_ITEM_LIMIT = 100; LLInventoryListener::LLInventoryListener() : LLEventAPI("LLInventory", @@ -104,87 +105,11 @@ LLInventoryListener::LLInventoryListener() llsd::map("result", LLSD())); } -// This abstract base class defines the interface for CatResultSet and -// ItemResultSet. It isa LLIntTracker so we can pass its unique int key to a -// consuming script via LLSD. -struct InvResultSet: public LLIntTracker -{ - // Get the length of the result set. Indexes are 0-relative. - virtual int getLength() const = 0; -/*==========================================================================*| - // Retrieve LLSD corresponding to a single entry from the result set, - // with index validation. - LLSD getSingle(int index) const - { - if (0 <= index && index < getLength()) - { - return getSingle_(index); - } - else - { - return {}; - } - } -|*==========================================================================*/ - // Retrieve LLSD corresponding to a single entry from the result set, - // once we're sure the index is valid. - virtual LLSD getSingle(int index) const = 0; - // Retrieve LLSD corresponding to a "slice" of the result set: a - // contiguous sub-array starting at index. The returned LLSD array might - // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the - // specified slice contains the end of the result set. - LLSD getSlice(int index, int count) const - { - // only call getLength() once - auto length = getLength(); - // Adjust bounds [start, end) to overlap the actual result set from - // [0, getLength()). Permit negative index; e.g. with a result set - // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and - // end to 3. - int start = llclamp(index, 0, length); - // Constrain count to MAX_ITEM_LIMIT even before clamping end. - int end = llclamp(index + llclamp(count, 0, MAX_ITEM_LIMIT), 0, length); - LLSD result{ LLSD::emptyArray() }; - // beware of count == 0, or an [index, count) range that doesn't even - // overlap [0, length) at all - if (end > start) - { - // right away expand the result array to the size we'll need - result[end - 1] = LLSD(); - for (int i = start; i < end; ++i) - { - result[i] = getSingle(i); - } - } - return result; - } - - /*---------------- the rest is solely for debug logging ----------------*/ - std::string mName; - - friend std::ostream& operator<<(std::ostream& out, const InvResultSet& self) - { - return out << "InvResultSet(" << self.mName << ", " << self.getKey() << ")"; - } - - InvResultSet(const std::string& name): - mName(name) - { - LL_DEBUGS("Lua") << *this << LL_ENDL; - } - virtual ~InvResultSet() - { - // We want to be able to observe that the consuming script uses - // LL.setdtor() to eventually destroy each of these InvResultSets. - LL_DEBUGS("Lua") << "~" << *this << LL_ENDL; - } -}; - // This struct captures (possibly large) category results from // getDirectDescendants() and collectDescendantsIf(). -struct CatResultSet: public InvResultSet +struct CatResultSet: public LL::ResultSet { - CatResultSet(): InvResultSet("categories") {} + CatResultSet(): LL::ResultSet("categories") {} LLInventoryModel::cat_array_t mCategories; int getLength() const override { return narrow(mCategories.size()); } @@ -199,9 +124,9 @@ struct CatResultSet: public InvResultSet // This struct captures (possibly large) item results from // getDirectDescendants() and collectDescendantsIf(). -struct ItemResultSet: public InvResultSet +struct ItemResultSet: public LL::ResultSet { - ItemResultSet(): InvResultSet("items") {} + ItemResultSet(): LL::ResultSet("items") {} LLInventoryModel::item_array_t mItems; int getLength() const override { return narrow(mItems.size()); } @@ -244,8 +169,9 @@ void LLInventoryListener::getItemsInfo(LLSD const &data) } } } - response["categories"] = catresult->getKey(); - response["items"] = itemresult->getKey(); + // Each of categories and items is a { result set key, total length } pair. + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); } void LLInventoryListener::getFolderTypeNames(LLSD const &data) @@ -277,8 +203,8 @@ void LLInventoryListener::getDirectDescendants(LLSD const &data) catresult->mCategories = *cats; itemresult->mItems = *items; - response["categories"] = catresult->getKey(); - response["items"] = itemresult->getKey(); + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); } struct LLFilteredCollector : public LLInventoryCollectFunctor @@ -337,25 +263,26 @@ void LLInventoryListener::collectDescendantsIf(LLSD const &data) LLInventoryModel::EXCLUDE_TRASH, collector); - response["categories"] = catresult->getKey(); - response["items"] = itemresult->getKey(); + response["categories"] = catresult->getKeyLength(); + response["items"] = itemresult->getKeyLength(); } /*==========================================================================*| void LLInventoryListener::getSingle(LLSD const& data) { - auto result = InvResultSet::getInstance(data["result"]); + auto result = LL::ResultSet::getInstance(data["result"]); sendReply(llsd::map("single", result->getSingle(data["index"])), data); } |*==========================================================================*/ void LLInventoryListener::getSlice(LLSD const& data) { - auto result = InvResultSet::getInstance(data["result"]); + auto result = LL::ResultSet::getInstance(data["result"]); int count = data.has("count")? data["count"].asInteger() : MAX_ITEM_LIMIT; LL_DEBUGS("Lua") << *result << ".getSlice(" << data["index"].asInteger() << ", " << count << ')' << LL_ENDL; - sendReply(llsd::map("slice", result->getSlice(data["index"], count)), data); + auto pair{ result->getSliceStart(data["index"], std::min(count, MAX_ITEM_LIMIT)) }; + sendReply(llsd::map("slice", pair.first, "start", pair.second), data); } void LLInventoryListener::closeResult(LLSD const& data) @@ -367,7 +294,7 @@ void LLInventoryListener::closeResult(LLSD const& data) } for (const auto& result : llsd::inArray(results)) { - auto ptr = InvResultSet::getInstance(result); + auto ptr = LL::ResultSet::getInstance(result); if (ptr) { delete ptr.get(); -- cgit v1.2.3 From 2157fa4fa9acf4db6093b962569274105e4d1fb4 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 12:52:34 -0400 Subject: result_view() now reuses same metatable instance for every table. --- indra/newview/scripts/lua/require/result_view.lua | 97 ++++++++++++----------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua index d53d953c24..c719681c66 100644 --- a/indra/newview/scripts/lua/require/result_view.lua +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -1,3 +1,50 @@ +-- metatable for every result_view() table +local mt = { + __len = function(this) + return this.length + end, + __index = function(this, i) + -- right away, convert to 0-relative indexing + i -= 1 + -- can we find this index within the current slice? + local reli = i - this.start + if 0 <= reli and reli < #this.slice then + -- Lua 1-relative indexing + return this.slice[reli + 1] + end + -- is this index outside the overall result set? + if not (0 <= i and i < this.length) then + return nil + end + -- fetch a new slice starting at i, using provided fetch() + local start + this.slice, start = this.fetch(this.key, i) + -- It's possible that caller-provided fetch() function forgot + -- to return the adjusted start index of the new slice. In + -- Lua, 0 tests as true, so if fetch() returned (slice, 0), + -- we'll duly reset this.start to 0. Otherwise, assume the + -- requested index was not adjusted: that the returned slice + -- really does start at i. + this.start = start or i + -- Hopefully this slice contains the desired i. + -- Back to 1-relative indexing. + return this.slice[i - this.start + 1] + end, + -- We purposely avoid putting any array entries (int keys) into + -- our table so that access to any int key will always call our + -- __index() metamethod. Moreover, we want any table iteration to + -- call __index(table, i) however many times; we do NOT want it to + -- retrieve key, length, start, slice. + -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'. + __iter = ipairs, + -- This result set provides read-only access. + -- We do not support pushing updates to individual items back to + -- C++; for the intended use cases, that makes no sense. + __newindex = function(this, i, value) + error("result_view is a read-only data structure", 2) + end +} + -- result_view(key_length, fetch) returns a table which stores only a slice -- of a result set plus some control values, yet presents read-only virtual -- access to the entire result set. @@ -11,53 +58,11 @@ local function result_view(key_length, fetch) -- C++ result sets use 0-based indexing, so internally we do too start=0, -- start with a dummy array with length 0 - slice={} + slice={}, + fetch=fetch }, - { - __len = function(this) - return this.length - end, - __index = function(this, i) - -- right away, convert to 0-relative indexing - i -= 1 - -- can we find this index within the current slice? - local reli = i - this.start - if 0 <= reli and reli < #this.slice then - -- Lua 1-relative indexing - return this.slice[reli + 1] - end - -- is this index outside the overall result set? - if not (0 <= i and i < this.length) then - return nil - end - -- fetch a new slice starting at i, using provided fetch() - local start - this.slice, start = fetch(this.key, i) - -- It's possible that caller-provided fetch() function forgot - -- to return the adjusted start index of the new slice. In - -- Lua, 0 tests as true, so if fetch() returned (slice, 0), - -- we'll duly reset this.start to 0. Otherwise, assume the - -- requested index was not adjusted: that the returned slice - -- really does start at i. - this.start = start or i - -- Hopefully this slice contains the desired i. - -- Back to 1-relative indexing. - return this.slice[i - this.start + 1] - end, - -- We purposely avoid putting any array entries (int keys) into - -- our table so that access to any int key will always call our - -- __index() metamethod. Moreover, we want any table iteration to - -- call __index(table, i) however many times; we do NOT want it to - -- retrieve key, length, start, slice. - -- So turn 'for k, v in result' into 'for k, v in ipairs(result)'. - __iter = ipairs, - -- This result set provides read-only access. - -- We do not support pushing updates to individual items back to - -- C++; for the intended use cases, that makes no sense. - __newindex = function(this, i, value) - error("result_view is a read-only data structure", 2) - end - } + -- use our special metatable + mt ) end -- cgit v1.2.3 From 93c21b503abdf4f9530064f8c3f1df7ea0dc244f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 13:01:11 -0400 Subject: test_inv_resultset.lua exercises LLInventory's result-set functionality. --- indra/newview/scripts/lua/test_inv_resultset.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 indra/newview/scripts/lua/test_inv_resultset.lua diff --git a/indra/newview/scripts/lua/test_inv_resultset.lua b/indra/newview/scripts/lua/test_inv_resultset.lua new file mode 100644 index 0000000000..c31cfe3c67 --- /dev/null +++ b/indra/newview/scripts/lua/test_inv_resultset.lua @@ -0,0 +1,18 @@ +local LLInventory = require 'LLInventory' +local inspect = require 'inspect' + +print('basic folders:') +print(inspect(LLInventory.getFolderTypeNames())) + +local folder = LLInventory.getBasicFolderID('my_otfts') +print(`folder = {folder}`) +local result = LLInventory.getDirectDescendants(folder) +print(`type(result) = {type(result)}`) +print(#result.categories, 'categories:') +for i, cat in pairs(result.categories) do + print(`{i}: {cat.name}`) +end +print(#result.items, 'items') +for i, item in pairs(result.items) do + print(`{i}: {item.name}`) +end -- cgit v1.2.3 From 83eace32cfccc672e7a5a2841bd7d844dce0ea3e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 13:13:22 -0400 Subject: Iterate to print landmarks returned by LLInventory. At this point, inspect(landmarks) just returns "". --- indra/newview/scripts/lua/test_LLInventory.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua index 107b0791d4..918ca56a2e 100644 --- a/indra/newview/scripts/lua/test_LLInventory.lua +++ b/indra/newview/scripts/lua/test_LLInventory.lua @@ -5,7 +5,9 @@ LLInventory = require 'LLInventory' my_landmarks_id = LLInventory.getBasicFolderID('landmark') -- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames()) landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3} -print(inspect(landmarks)) +for _, landmark in pairs(landmarks.items) do + print(landmark.name) +end -- Get 'Calling Cards' folder id calling_cards_id = LLInventory.getBasicFolderID('callcard') -- cgit v1.2.3 From bf2b2eb01ca8680914d17dda713d9365e2ecc3eb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 16:26:03 -0400 Subject: Add Lua traceback to errors from calling lluau::expr(). That includes scripts run by LLLUAmanager::runScriptFile(), runScriptLine() et al. --- indra/llcommon/lua_function.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 67ca29c689..c3d336bfcb 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -65,10 +65,26 @@ int dostring(lua_State* L, const std::string& desc, const std::string& text) if (r != LUA_OK) return r; + // Push debug.traceback() onto the stack as lua_pcall()'s error + // handler function. On error, lua_pcall() calls the specified error + // handler function with the original error message; the message + // returned by the error handler is then returned by lua_pcall(). + // Luau's debug.traceback() is called with a message to prepend to the + // returned traceback string. Almost as if they'd been designed to + // work together... + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + // ditch "debug" + lua_remove(L, -2); + // stack: compiled chunk, debug.traceback() + lua_insert(L, -2); + // stack: debug.traceback(), compiled chunk + LuaRemover cleanup(L, -2); + // It's important to pass LUA_MULTRET as the expected number of return // values: if we pass any fixed number, we discard any returned values // beyond that number. - return lua_pcall(L, 0, LUA_MULTRET, 0); + return lua_pcall(L, 0, LUA_MULTRET, -2); } int loadstring(lua_State *L, const std::string &desc, const std::string &text) -- cgit v1.2.3 From 517163c126f7c0620f506532d67d9097083728d9 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 18:02:31 -0400 Subject: Fix a bug in ResultSet::getSliceStart(). When asked to retrieve a slice starting at an `index > 0`, `getSliceStart()` was returning an LLSD array whose first `index` entries were `isUndefined()`, followed by the desired data. Fix to omit those undefined entries. --- indra/llcommon/resultset.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp index ee8cc68c6c..cb46db04fa 100644 --- a/indra/llcommon/resultset.cpp +++ b/indra/llcommon/resultset.cpp @@ -43,11 +43,14 @@ std::pair ResultSet::getSliceStart(int index, int count) const // overlap [0, length) at all if (end > start) { - // right away expand the result array to the size we'll need - result[end - 1] = LLSD(); - for (int i = start; i < end; ++i) + // Right away expand the result array to the size we'll need. + // (end - start) is that size; (end - start - 1) is the index of the + // last entry in result. + result[end - start - 1] = LLSD(); + for (int i = 0; (start + i) < end; ++i) { - result[i] = getSingle(i); + // For this to be a slice, set result[0] = getSingle(start), etc. + result[i] = getSingle(start + i); } } return { result, start }; -- cgit v1.2.3 From f3896d37ca625a4f7060ee5139a8825c2f6e6a74 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 20:39:14 -0400 Subject: Generalize Lua-side result-set machinery for other use cases. Change `result_view()` from a simple function to a callable table so we can add conventional/default functions to it: `result_view.fetch()` is a generic `fetch()` function suitable for use with `result_view()`, and `result_view.close()` is a variadic function that closes result sets for whichever keys are passed. This arises from the fact that any `LL::ResultSet` subclass is accessed generically through its base class, therefore we don't need distinct "getSlice" and "closeResult" operations for different `LLEventAPI` listeners. (It might make sense to relocate those operations to a new generic listener, but for now "LLInventory" works.) That lets `result_view()`'s caller omit the `fetch` parameter unless it requires special behavior. Omitting it uses the generic `result_view.fetch()` function. Moreover, every view returned by `result_view()` now contains a close() function that closes that view's result set. The table returned by LLInventory.lua's `result()` function has a `close()` method; that method can now call `result_view.close()` with the two keys of interest. That table's `__index()` metamethod can now leverage `result_view()`'s default `fetch` function. --- indra/newview/scripts/lua/require/LLInventory.lua | 22 ++---- indra/newview/scripts/lua/require/result_view.lua | 83 +++++++++++++++-------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua index ce501e75f3..9cf72a8678 100644 --- a/indra/newview/scripts/lua/require/LLInventory.lua +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -15,9 +15,7 @@ local function result(keys) -- call result:close() to release result sets before garbage -- collection or script completion close = function(self) - leap.send('LLInventory', - {op='closeResult', - result={self._categories[1], self._items[1]}}) + result_view.close(self._categories[1], self._items[1]) end }, -- The caller of one of our methods that returns a result set @@ -31,17 +29,9 @@ local function result(keys) if not table.find({'categories', 'items'}, field) then return nil end - local view = result_view( - -- We cleverly saved the result set {key, length} pair in - -- a field with the same name but an underscore prefix. - t['_' .. field], - function(key, start) - local fetched = leap.request( - 'LLInventory', - {op='getSlice', result=key, index=start}) - return fetched.slice, fetched.start - end - ) + -- We cleverly saved the result set {key, length} pair in + -- a field with the same name but an underscore prefix. + local view = result_view(t['_' .. field]) -- cache that view for future reference t[field] = view return view @@ -51,8 +41,8 @@ local function result(keys) -- When the table-with-metatable above is destroyed, tell LLInventory -- we're done with its result sets -- whether or not we ever fetched -- either of them. - function(keys) - keys:close() + function(res) + res:close() end ) end diff --git a/indra/newview/scripts/lua/require/result_view.lua b/indra/newview/scripts/lua/require/result_view.lua index c719681c66..5301d7838c 100644 --- a/indra/newview/scripts/lua/require/result_view.lua +++ b/indra/newview/scripts/lua/require/result_view.lua @@ -1,34 +1,36 @@ +local leap = require 'leap' + -- metatable for every result_view() table local mt = { - __len = function(this) - return this.length + __len = function(self) + return self.length end, - __index = function(this, i) + __index = function(self, i) -- right away, convert to 0-relative indexing i -= 1 -- can we find this index within the current slice? - local reli = i - this.start - if 0 <= reli and reli < #this.slice then + local reli = i - self.start + if 0 <= reli and reli < #self.slice then -- Lua 1-relative indexing - return this.slice[reli + 1] + return self.slice[reli + 1] end -- is this index outside the overall result set? - if not (0 <= i and i < this.length) then + if not (0 <= i and i < self.length) then return nil end -- fetch a new slice starting at i, using provided fetch() local start - this.slice, start = this.fetch(this.key, i) + self.slice, start = self.fetch(self.key, i) -- It's possible that caller-provided fetch() function forgot -- to return the adjusted start index of the new slice. In -- Lua, 0 tests as true, so if fetch() returned (slice, 0), - -- we'll duly reset this.start to 0. Otherwise, assume the + -- we'll duly reset self.start to 0. Otherwise, assume the -- requested index was not adjusted: that the returned slice -- really does start at i. - this.start = start or i + self.start = start or i -- Hopefully this slice contains the desired i. -- Back to 1-relative indexing. - return this.slice[i - this.start + 1] + return self.slice[i - self.start + 1] end, -- We purposely avoid putting any array entries (int keys) into -- our table so that access to any int key will always call our @@ -40,7 +42,7 @@ local mt = { -- This result set provides read-only access. -- We do not support pushing updates to individual items back to -- C++; for the intended use cases, that makes no sense. - __newindex = function(this, i, value) + __newindex = function(self, i, value) error("result_view is a read-only data structure", 2) end } @@ -50,20 +52,47 @@ local mt = { -- access to the entire result set. -- key_length: {result set key, total result set length} -- fetch: function(key, start) that returns (slice, adjusted start) -local function result_view(key_length, fetch) - return setmetatable( - { - key=key_length[1], - length=key_length[2], - -- C++ result sets use 0-based indexing, so internally we do too - start=0, - -- start with a dummy array with length 0 - slice={}, - fetch=fetch - }, - -- use our special metatable - mt - ) -end +local result_view = setmetatable( + { + -- generic fetch() function + fetch = function(key, start) + local fetched = leap.request( + 'LLInventory', + {op='getSlice', result=key, index=start}) + return fetched.slice, fetched.start + end, + -- generic close() function accepting variadic result-set keys + close = function(...) + local keys = table.pack(...) + -- table.pack() produces a table with an array entry for every + -- parameter, PLUS an 'n' key with the count. Unfortunately that + -- 'n' key bollixes our conversion to LLSD, which requires either + -- all int keys (for an array) or all string keys (for a map). + keys.n = nil + leap.send('LLInventory', {op='closeResult', result=keys}) + end + }, + { + -- result_view(key_length, fetch) calls this + __call = function(class, key_length, fetch) + return setmetatable( + { + key=key_length[1], + length=key_length[2], + -- C++ result sets use 0-based indexing, so internally we do too + start=0, + -- start with a dummy array with length 0 + slice={}, + -- if caller didn't pass fetch() function, use generic + fetch=fetch or class.fetch, + -- returned view:close() will close result set with passed key + close=function(self) class.close(key_length[1]) end + }, + -- use our special metatable + mt + ) + end + } +) return result_view -- cgit v1.2.3 From a8dd7135f0423384dbbb1e3b98514149c6a69e6b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 3 Sep 2024 20:50:18 -0400 Subject: Use Lua result-set logic for "LLFloaterReg"s "getFloaterNames" op. This is the query that produced so many results that, before we lifted the infinite-loop interrupt limit, inspect(result) hit the limit and terminated. --- indra/llui/llfloaterreglistener.cpp | 20 ++++++++++++++++---- indra/newview/scripts/lua/require/UI.lua | 8 +++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp index e17f9f4dd6..bd8d87086e 100644 --- a/indra/llui/llfloaterreglistener.cpp +++ b/indra/llui/llfloaterreglistener.cpp @@ -38,6 +38,7 @@ #include "llfloater.h" #include "llbutton.h" #include "llluafloater.h" +#include "resultset.h" LLFloaterRegListener::LLFloaterRegListener(): LLEventAPI("LLFloaterReg", @@ -82,9 +83,9 @@ LLFloaterRegListener::LLFloaterRegListener(): &LLFloaterRegListener::getLuaFloaterEvents); add("getFloaterNames", - "Return the table of all registered floaters", + "Return result set key [\"floaters\"] for names of all registered floaters", &LLFloaterRegListener::getFloaterNames, - llsd::map("reply", LLSD())); + llsd::map("reply", LLSD::String())); } void LLFloaterRegListener::getBuildMap(const LLSD& event) const @@ -126,10 +127,22 @@ void LLFloaterRegListener::instanceVisible(const LLSD& event) const event); } +struct NameResultSet: public LL::ResultSet +{ + NameResultSet(): + LL::ResultSet("floaters"), + mNames(LLFloaterReg::getFloaterNames()) + {} + LLSD mNames; + + int getLength() const override { return narrow(mNames.size()); } + LLSD getSingle(int index) const override { return mNames[index]; } +}; void LLFloaterRegListener::getFloaterNames(const LLSD &event) const { - Response response(llsd::map("floaters", LLFloaterReg::getFloaterNames()), event); + auto nameresult = new NameResultSet; + sendReply(llsd::map("floaters", nameresult->getKeyLength()), event); } void LLFloaterRegListener::clickButton(const LLSD& event) const @@ -178,4 +191,3 @@ void LLFloaterRegListener::getLuaFloaterEvents(const LLSD &event) const { Response response(llsd::map("events", LLLuaFloater::getEventsData()), event); } - diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua index bbcae3514a..aa64c0c7f9 100644 --- a/indra/newview/scripts/lua/require/UI.lua +++ b/indra/newview/scripts/lua/require/UI.lua @@ -2,6 +2,7 @@ local leap = require 'leap' local mapargs = require 'mapargs' +local result_view = require 'result_view' local Timer = (require 'timers').Timer local util = require 'util' @@ -234,7 +235,12 @@ function UI.closeAllFloaters() end function UI.getFloaterNames() - return leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters + local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters + local view = result_view(key_length) + return LL.setdtor( + 'registered floater names', + view, + function(self) view:close() end) end return UI -- cgit v1.2.3 From 35c3f0227c334e059abdc36c36cc942a517d92ec Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 4 Sep 2024 07:43:48 -0400 Subject: Instead of traversing all calling cards, pick a selected few. Make test_LLInventory.lua directly select from the calling_cards result set, instead of first copying all names to a separate array. --- indra/newview/scripts/lua/test_LLInventory.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua index 918ca56a2e..de57484bcd 100644 --- a/indra/newview/scripts/lua/test_LLInventory.lua +++ b/indra/newview/scripts/lua/test_LLInventory.lua @@ -15,9 +15,10 @@ calling_cards_id = LLInventory.getBasicFolderID('callcard') calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items -- Print a random calling card name from 'Calling Cards' folder -local card_names = {} -for _, value in pairs(calling_cards) do - table.insert(card_names, value.name) -end +-- (because getDirectDescendents().items is a Lua result set, selecting +-- a random entry only fetches one slice containing that entry) math.randomseed(os.time()) -print("Random calling card: " .. inspect(card_names[math.random(#card_names)])) +for i = 1, 5 do + pick = math.random(#calling_cards) + print(`Random calling card (#{pick} of {#calling_cards}): {calling_cards[pick].name}`) +end -- cgit v1.2.3 From d67ad5da3b5a37f7b4cb78e686ae36f31c513153 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 4 Sep 2024 09:15:10 -0400 Subject: `result_view()`'s table's `close()` method need not be further wrapped. `LL.setdtor(desc, table, func)` eventually calls `func(table)`. So the `close()` method on the table returned by `result_view()` can be directly passed to `setdtor()`, instead of wrapped in a new anonymous function whose only job is to pass the table to it. Moreover, there's no need for the table returned by LLInventory.lua's `result()` function to lazily instantiate the `result_view()` for `categories` or `items`: neither `result_view` will fetch a slice unless asked. Just return `{categories=result_view(...), items=result_view(...), close=...}`. This dramatically simplifies the `result()` function. Since that table also defines a `close()` function, that too can be passed directly to `setdtor()` without being wrapped in a new anonymous function. --- indra/newview/scripts/lua/require/LLInventory.lua | 52 +++++------------------ indra/newview/scripts/lua/require/UI.lua | 5 +-- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua index 9cf72a8678..2c80a8602b 100644 --- a/indra/newview/scripts/lua/require/LLInventory.lua +++ b/indra/newview/scripts/lua/require/LLInventory.lua @@ -3,48 +3,18 @@ local mapargs = require 'mapargs' local result_view = require 'result_view' local function result(keys) - return LL.setdtor( - 'LLInventory result', - setmetatable( - -- the basic table wrapped by setmetatable just captures the int - -- result-set {key, length} pairs from 'keys', but with underscore - -- prefixes - { - _categories=keys.categories, - _items=keys.items, - -- call result:close() to release result sets before garbage - -- collection or script completion - close = function(self) - result_view.close(self._categories[1], self._items[1]) - end - }, - -- The caller of one of our methods that returns a result set - -- isn't necessarily interested in both categories and items, so - -- don't proactively populate both. Instead, when caller references - -- either 'categories' or 'items', the __index() metamethod - -- populates that field. - { - __index = function(t, field) - -- we really don't care about references to any other field - if not table.find({'categories', 'items'}, field) then - return nil - end - -- We cleverly saved the result set {key, length} pair in - -- a field with the same name but an underscore prefix. - local view = result_view(t['_' .. field]) - -- cache that view for future reference - t[field] = view - return view - end - } - ), - -- When the table-with-metatable above is destroyed, tell LLInventory - -- we're done with its result sets -- whether or not we ever fetched - -- either of them. - function(res) - res:close() + -- capture result_view() instances for both categories and items + local result_table = { + categories=result_view(keys.categories), + items=result_view(keys.items), + -- call result_table:close() to release result sets before garbage + -- collection or script completion + close = function(self) + result_view.close(keys.categories[1], keys.items[1]) end - ) + } + -- When the result_table is destroyed, close its result_views. + return LL.setdtor('LLInventory result', result_table, result_table.close) end local LLInventory = {} diff --git a/indra/newview/scripts/lua/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua index aa64c0c7f9..73a76fa6b8 100644 --- a/indra/newview/scripts/lua/require/UI.lua +++ b/indra/newview/scripts/lua/require/UI.lua @@ -237,10 +237,7 @@ end function UI.getFloaterNames() local key_length = leap.request("LLFloaterReg", {op = "getFloaterNames"}).floaters local view = result_view(key_length) - return LL.setdtor( - 'registered floater names', - view, - function(self) view:close() end) + return LL.setdtor('registered floater names', view, view.close) end return UI -- cgit v1.2.3 From a6b85244a6f943a4598ff9b7b8a3343eb1e0d11e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 4 Sep 2024 10:26:40 -0400 Subject: Fix test: new traceback info changed error message. --- indra/newview/tests/llluamanager_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 3209d93d39..c2abd27f96 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -463,8 +463,8 @@ namespace tut // but now we have to give the startScriptLine() coroutine a chance to run auto [count, result] = future.get(); ensure_equals("killed Lua script terminated normally", count, -1); - ensure_equals("unexpected killed Lua script error", - result.asString(), "viewer is stopping"); + ensure_contains("unexpected killed Lua script error", + result.asString(), "viewer is stopping"); } template<> template<> -- cgit v1.2.3 From 49bf86b52459b183d3988388dbb74d8888a71925 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 5 Sep 2024 08:45:24 -0400 Subject: Fix a few trailing whitespaces. --- indra/llcommon/llinttracker.h | 2 +- indra/llcommon/resultset.cpp | 2 +- indra/llcommon/resultset.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/indra/llcommon/llinttracker.h b/indra/llcommon/llinttracker.h index 86c30bc7aa..fd6d24d0fd 100644 --- a/indra/llcommon/llinttracker.h +++ b/indra/llcommon/llinttracker.h @@ -3,7 +3,7 @@ * @author Nat Goodspeed * @date 2024-08-30 * @brief LLIntTracker isa LLInstanceTracker with generated int keys. - * + * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Copyright (c) 2024, Linden Research, Inc. * $/LicenseInfo$ diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp index cb46db04fa..4d7b00eabd 100644 --- a/indra/llcommon/resultset.cpp +++ b/indra/llcommon/resultset.cpp @@ -3,7 +3,7 @@ * @author Nat Goodspeed * @date 2024-09-03 * @brief Implementation for resultset. - * + * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Copyright (c) 2024, Linden Research, Inc. * $/LicenseInfo$ diff --git a/indra/llcommon/resultset.h b/indra/llcommon/resultset.h index 9b9ecbb21e..90d52b6fe4 100644 --- a/indra/llcommon/resultset.h +++ b/indra/llcommon/resultset.h @@ -4,7 +4,7 @@ * @date 2024-09-03 * @brief ResultSet is an abstract base class to allow scripted access to * potentially large collections representable as LLSD arrays. - * + * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Copyright (c) 2024, Linden Research, Inc. * $/LicenseInfo$ -- cgit v1.2.3