diff --git a/CLAUDE.md b/CLAUDE.md index 6aac4584d39..7c1a6356e20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,7 @@ CMake with vcpkg for dependency management. The source root for CMake is `indra/ - CMake 4.0+, Visual Studio 2022/2026 (Windows) or Xcode (macOS) or GCC/Clang+Ninja (Linux) - Rust and the .NET SDK (`dotnet tool restore`) only for Velopack installers - Python 3 with `llsd` (`pip install -r requirements.txt`) only for the four tests that spawn a Python peer; without it they are registered disabled +- Python 3 with `PyYAML` and `llsd`, plus bison and flex, only for LL's LSL compiler (`indra/lscript`, a test-only reference); without them it is left out CMake files are formatted with gersemi (`.gersemirc` at the repo root): run `gersemi -i ` on any CMake file you edit. diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 46bb39ff5d5..f4bae96a5a2 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -34,6 +34,8 @@ - **llwebrtc** - WebRTC voice integration (conditional) - **llphysicsextensionsos** - Physics extensions stub (VHACD convex decomposition) - **media_plugins** - Out-of-process media plugin implementations (CEF, VLC, GStreamer) +- **alscript** - Script Studio's language services, free of UI (depends on llcommon, llmath, Tailslide and SLua's Luau). One folder a part, each on the include path: `core/` (problems, symbols, the lexicon, messages, weights, the formatter), `lint/` (the studio's lints and fixes), `lsl/` (the LSL analyzer over Tailslide, the SLua converter), `lsl/optimizer/` (the LSL optimizer, a pass a file, sharing `allsloptimizerpass.h`), `luau/` (the SLua analyzer) and `preprocessor/` (the preprocessor and its includes) +- **lscript** - LL's LSL compiler and LSO VM, restored as a test-only reference for the optimizer and the weigher; built with tests, never linked into the viewer - **newview** - The main viewer application. Contains all viewer-specific logic: the rendering pipeline, scene graph, UI floaters/panels, settings, object/avatar systems, and the application entry point. Links against all libraries above plus third-party: Tracy, OpenXR, Discord SDK, NVAPI, TinyEXR, Velopack ### Application Lifecycle diff --git a/doc/BUILD.md b/doc/BUILD.md index 1cce67139bf..d3f973fdc6b 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -404,7 +404,9 @@ Enable tests at configure time: cmake -S indra --preset -DAL_BUILD_TESTS=ON ``` -Four tests drive a Python peer (`llleap`, `llprocess`, `llsdserialize`, `llcorehttp`); they need a Python 3 interpreter with the `llsd` package (`pip install -r requirements.txt`, in a venv if you like) and are registered disabled when configure finds none. Nothing else in the build runs Python. +Four tests drive a Python peer (`llleap`, `llprocess`, `llsdserialize`, `llcorehttp`); they need a Python 3 interpreter with the `llsd` package (`pip install -r requirements.txt`, in a venv if you like) and are registered disabled when configure finds none. + +LL's LSL compiler (`indra/lscript`), restored as a reference for the script tests, is built only with tests on. Its lexer, grammar, newer event nodes and library table are written at build time by lsl-definitions' own generator, which needs Python with `PyYAML` and `llsd` (both in `requirements.txt`), and then put through `bison` and `flex`. Where any of them is missing, configure leaves the library out and the configuration report says what it needs. Nothing else in the build runs Python. The `llrender` suites render on a hidden SDL window with the platform's own GL -- WGL on Windows, EGL on Linux, and where Linux has no display SDL's offscreen driver over Mesa (set `LIBGL_ALWAYS_SOFTWARE=1` for llvmpipe on a machine with no GPU). A host with no GL 4.1 to give, such as a CI runner without a graphics driver, configures with `-DAL_ENABLE_GL_TESTS=OFF`: those suites still build, and CTest reports them as not run rather than failed. They carry the label `gl`, so `ctest -LE gl` skips them for one run. diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt index ded4854c1d8..1654ead62a8 100644 --- a/indra/CMakeLists.txt +++ b/indra/CMakeLists.txt @@ -432,6 +432,10 @@ add_subdirectory(template_verifier) add_subdirectory(dullahan) add_subdirectory(llprecompiled) add_subdirectory(alscript) +if(AL_BUILD_TESTS) + # LL's LSL compiler, restored as a reference for the script tests. + add_subdirectory(lscript) +endif() add_subdirectory(llaudio) add_subdirectory(llappearance) add_subdirectory(llcharacter) diff --git a/indra/alscript/CMakeLists.txt b/indra/alscript/CMakeLists.txt index 4c35d94137f..5ab5a95e6a3 100644 --- a/indra/alscript/CMakeLists.txt +++ b/indra/alscript/CMakeLists.txt @@ -6,45 +6,68 @@ add_library(alscript STATIC) +# Each folder is a part of the library, and each is on the include path, +# so that a header is named alone wherever it is used: +# core/ what both languages share: problems, spans and symbols, the +# lexicon, the messages, a script's weight, the formatter; +# lint/ the studio's own lints over both languages, and the fixes; +# lsl/ the LSL analyzer over Tailslide, and what reads LSL; +# lsl/optimizer/ the LSL optimizer, a pass a file; +# luau/ the SLua analyzer over Second Life's fork of Luau; +# preprocessor/ the preprocessor, its includes and what it keeps. target_sources( alscript PRIVATE - aldiskcache.cpp - aldiskincludes.cpp - alincludeidentity.cpp - alincludesearch.cpp - almodulelook.cpp - allslconsts.cpp - allslcosts.cpp - allsleffects.cpp - allsllintpass.cpp - allslinliner.cpp - almessagemap.cpp - allsloptimizer.cpp - alluauconfig.cpp - alluauexports.cpp - alluauservice.cpp - allslexports.cpp - allslservice.cpp - allsltoslua.cpp - allsltraits.cpp - allslvalues.cpp - alpreprocessor.cpp - alscriptenvelope.cpp - alscriptfixes.cpp - alscriptformatter.cpp - alscriptlexicon.cpp - alscriptlintpass.cpp - alscriptmessages.cpp - alscriptnameindex.cpp - alscriptsnapshot.cpp - alnotecardformat.cpp - alscriptweight.cpp - alscriptstack.cpp - alscripttempfiles.cpp - alscripttextcache.cpp - alselenefilters.cpp - alsourcemap.cpp + core/almessagemap.cpp + core/alnotecardformat.cpp + core/alscriptformatter.cpp + core/alscriptlexicon.cpp + core/alscriptmessages.cpp + core/alscriptnameindex.cpp + core/alscriptstack.cpp + core/alscripttempfiles.cpp + core/alscriptweight.cpp + lint/allsllintpass.cpp + lint/alscriptfixes.cpp + lint/alscriptlintpass.cpp + lint/alselenefilters.cpp + lsl/allslconsts.cpp + lsl/allsleffects.cpp + lsl/allslexports.cpp + lsl/allslservice.cpp + lsl/allsltoslua.cpp + lsl/allsltraits.cpp + lsl/allslvalues.cpp + lsl/optimizer/allslconstantglobals.cpp + lsl/optimizer/allslcosts.cpp + lsl/optimizer/allslbranches.cpp + lsl/optimizer/allsldeadcode.cpp + lsl/optimizer/allslflowvalues.cpp + lsl/optimizer/allslfolder.cpp + lsl/optimizer/allslglobals.cpp + lsl/optimizer/allslinliner.cpp + lsl/optimizer/allsllibraryfold.cpp + lsl/optimizer/allslnames.cpp + lsl/optimizer/allsloptimizer.cpp + lsl/optimizer/allsloptimizerpass.cpp + lsl/optimizer/allslprinter.cpp + lsl/optimizer/allslrepeatedcalls.cpp + lsl/optimizer/allslshapes.cpp + lsl/optimizer/allslsignatures.cpp + lsl/optimizer/allslsimplifier.cpp + luau/alluauconfig.cpp + luau/alluauexports.cpp + luau/alluauservice.cpp + preprocessor/aldiskcache.cpp + preprocessor/aldiskincludes.cpp + preprocessor/alincludeidentity.cpp + preprocessor/alincludesearch.cpp + preprocessor/almodulelook.cpp + preprocessor/alpreprocessor.cpp + preprocessor/alscriptenvelope.cpp + preprocessor/alscriptsnapshot.cpp + preprocessor/alscripttextcache.cpp + preprocessor/alsourcemap.cpp ) target_sources( @@ -52,55 +75,110 @@ target_sources( PUBLIC FILE_SET HEADERS FILES - aldiskcache.h - aldiskincludes.h - alincludeidentity.h - alincludesearch.h - almodulelook.h - allslconsts.h - allslcosts.h - allsleffects.h - allsllintpass.h - allslinliner.h - almessagemap.h - allsloptimizer.h - allsltraits.inc - allsluuids.inc - allsleventtext.inc - alluauconfig.h - alluauexports.h - alluauservice.h - allslexports.h - allslservice.h - allsltoslua.h - allsltraits.h - allslvalues.h - alpreprocessor.h - alscriptengine.h - alscriptenvelope.h - alscriptfixes.h - alscriptformatter.h - alscriptjoblane.h - alscriptlexicon.h - alscriptlintpass.h - alscriptmessages.h - alscriptnameindex.h - alscriptsnapshot.h - alscriptspan.h - alnotecardformat.h - alscriptproblem.h - alscriptstack.h - alscriptsymbol.h - alscripttempfiles.h - alscripttextcache.h - alscriptweight.h - alselenefilters.h - alsourcemap.h + core/almessagemap.h + core/alnotecardformat.h + core/alscriptengine.h + core/alscriptformatter.h + core/alscriptlexicon.h + core/alscriptmessages.h + core/alscriptnameindex.h + core/alscriptproblem.h + core/alscriptspan.h + core/alscriptstack.h + core/alscriptsymbol.h + core/alscripttempfiles.h + core/alscriptweight.h + lint/allsllintpass.h + lint/alscriptfixes.h + lint/alscriptlintpass.h + lint/alselenefilters.h + lsl/allslconsts.h + lsl/allsleffects.h + lsl/allsleventtext.inc + lsl/allslexports.h + lsl/allslservice.h + lsl/allsltoslua.h + lsl/allsltraits.h + lsl/allsltraits.inc + lsl/allsluuids.inc + lsl/allslvalues.h + lsl/optimizer/allslconstantglobals.h + lsl/optimizer/allslcosts.h + lsl/optimizer/allslbranches.h + lsl/optimizer/allsldeadcode.h + lsl/optimizer/allslflowvalues.h + lsl/optimizer/allslfolder.h + lsl/optimizer/allslglobals.h + lsl/optimizer/allslinliner.h + lsl/optimizer/allsllibraryfold.h + lsl/optimizer/allslnames.h + lsl/optimizer/allsloptimizer.h + lsl/optimizer/allsloptimizerpass.h + lsl/optimizer/allslprinter.h + lsl/optimizer/allslrepeatedcalls.h + lsl/optimizer/allslshapes.h + lsl/optimizer/allslsignatures.h + lsl/optimizer/allslsimplifier.h + luau/alluauconfig.h + luau/alluauexports.h + luau/alluauservice.h + preprocessor/aldiskcache.h + preprocessor/aldiskincludes.h + preprocessor/alincludeidentity.h + preprocessor/alincludesearch.h + preprocessor/almodulelook.h + preprocessor/alpreprocessor.h + preprocessor/alscriptenvelope.h + preprocessor/alscriptjoblane.h + preprocessor/alscriptsnapshot.h + preprocessor/alscripttextcache.h + preprocessor/alsourcemap.h +) + +target_include_directories( + alscript + PUBLIC core lint lsl lsl/optimizer luau preprocessor ) target_link_libraries(alscript PRIVATE al::flags ll::slua ll::tailslide llmath PUBLIC llcommon) target_precompile_headers(alscript REUSE_FROM llprecompiled) +# The optimizer folds LSL's floats to what the VM gives and prints them so +# that they read back as themselves: IEEE's arithmetic a step at a time, and +# a zero's sign kept. The viewer's float flags (00-Common.cmake) let the +# compiler reassociate, fuse a product into a sum, take a reciprocal for a +# division and forget a zero's sign -- a vector normalised as a product with +# 1 / mag where the code divides -- so these are built without them, and +# without the precompiled header those flags were part of. +if(WINDOWS) + set(alscript_ieee_options /fp:precise) +else() + set( + alscript_ieee_options + -ffp-contract=off + -fno-associative-math + -fno-reciprocal-math + -fsigned-zeros + ) +endif() +set_source_files_properties( + lsl/allslvalues.cpp + lsl/optimizer/allslbranches.cpp + lsl/optimizer/allslconstantglobals.cpp + lsl/optimizer/allsldeadcode.cpp + lsl/optimizer/allslflowvalues.cpp + lsl/optimizer/allslfolder.cpp + lsl/optimizer/allsllibraryfold.cpp + lsl/optimizer/allslnames.cpp + lsl/optimizer/allsloptimizer.cpp + lsl/optimizer/allsloptimizerpass.cpp + lsl/optimizer/allslprinter.cpp + lsl/optimizer/allslrepeatedcalls.cpp + lsl/optimizer/allslshapes.cpp + lsl/optimizer/allslsimplifier.cpp + PROPERTIES COMPILE_OPTIONS "${alscript_ieee_options}" SKIP_PRECOMPILE_HEADERS TRUE +) + # tests if(AL_BUILD_TESTS) # The tests check real scripts against the grid's own definitions, which diff --git a/indra/alscript/allsloptimizer.cpp b/indra/alscript/allsloptimizer.cpp deleted file mode 100644 index 8f7a20ef533..00000000000 --- a/indra/alscript/allsloptimizer.cpp +++ /dev/null @@ -1,6075 +0,0 @@ -/** - * @file allsloptimizer.cpp - * @brief The LSL optimizer over Tailslide's tree. - * - * $LicenseInfo:firstyear=2026&license=viewerlgpl$ - * Alchemy Viewer Source Code - * Copyright (C) 2026, Rye - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * $/LicenseInfo$ - */ - - -#include "linden_common.h" - -#include "allsloptimizer.h" - -#include "alscriptengine.h" - -#include "allslcosts.h" -#include "allsleffects.h" -#include "allslinliner.h" -#include "allslservice.h" -#include "allsltraits.h" -#include "allslvalues.h" -#include "alscriptlexicon.h" -#include "llmath.h" -#include "llmd5.h" -#include "llstl.h" -#include "llquaternion.h" -#include "v3math.h" - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include - -namespace -{ - using namespace Tailslide; - - // ---- what the definitions say of the library ------------------------------------- - - bool isPure(const char* name) { return ALLSLTraits::pure(name); } - - S32 zeroBased(int one) { return std::max(0, one - 1); } - - // ---- numbers as the printer writes them ---------------------------------------------- - - using ALLSLValues::floatText; - using ALLSLValues::integral; - - bool ascii(const char* s) - { - for (; *s; ++s) - { - if (static_cast(*s) >= 0x80 || (*s < 0x20 && *s != '\n' && *s != '\t')) - { - return false; - } - } - return true; - } - - // ---- notes --------------------------------------------------------------------------- - - class Report - { - public: - Report(ALScriptProblems& problems, bool wanted) : mProblems(problems), mWanted(wanted) {} - - // Whether anybody reads the notes: a pass that would have to - // print a subtree to say what it did asks first. - bool wanted() const { return mWanted; } - - // A note with a key a translation may be found under, and the - // words -- [1], [2] ... in the text -- it is built from. - void note(const Tailslide::YYLTYPE* loc, const char* key, std::string_view text, std::vector args = {}) - { - if (!mWanted) - { - return; - } - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Note; - p.source = ALScriptProblem::Source::Optimizer; - if (loc) - { - p.line = zeroBased(loc->first_line); - p.column = zeroBased(loc->first_column); - p.endLine = zeroBased(loc->last_line); - // Tailslide's last column is one past the end, counted from - // one: the end as the studio counts it, as the LSL service - // reads it. - p.endColumn = zeroBased(loc->last_column); - } - p.message = ALScriptProblem::fill(text, args); - p.key = key; - p.args = std::move(args); - mProblems.push_back(std::move(p)); - } - - private: - ALScriptProblems& mProblems; - bool mWanted = true; - }; - - std::string render(LSLASTNode* node) - { - PrettyPrintOpts opts{}; - PrettyPrintVisitor printer(opts); - node->visit(&printer); - std::string text = printer.mStream.str(); - while (!text.empty() && (text.back() == '\n' || text.back() == ' ')) - { - text.pop_back(); - } - // A statement's rendering starts with its indentation. - const size_t start = text.find_first_not_of(" \t"); - return start == std::string::npos ? text : text.substr(start); - } - - bool sideEffectFree(LSLASTNode* node) { return ALLSLTraits::sideEffectFree(node); } - // What may be dropped, though not moved: a read of the world or the - // clock changes nothing. - bool changesNothing(LSLASTNode* node) { return ALLSLTraits::changesNothing(node); } - - // Skips the parentheses around an expression. - LSLExpression* bare(LSLExpression* expr) - { - while (expr && expr->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION) - { - expr = static_cast(expr)->getChildExpr(); - } - return expr; - } - - bool isInteger(LSLASTNode* node, int value) - { - LSLConstant* cv = node ? node->getConstantValue() : nullptr; - return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && static_cast(cv)->getValue() == value; - } - - bool isFloat(LSLASTNode* node, double value) - { - LSLConstant* cv = node ? node->getConstantValue() : nullptr; - return cv && cv->getNodeSubType() == NODE_FLOAT_CONSTANT && static_cast(cv)->getValue() == value; - } - - bool isEmptyString(LSLASTNode* node) - { - LSLConstant* cv = node ? node->getConstantValue() : nullptr; - return cv && cv->getNodeSubType() == NODE_STRING_CONSTANT && !*static_cast(cv)->getValue(); - } - - bool isEmptyList(LSLASTNode* node) - { - LSLConstant* cv = node ? node->getConstantValue() : nullptr; - return cv && cv->getNodeSubType() == NODE_LIST_CONSTANT && static_cast(cv)->getLength() == 0; - } - - // Whether the parent of an expression is another expression that could - // bind a replacement differently: parentheses go around it then. The - // right side of an assignment binds nothing. - bool wantsParens(LSLASTNode* parent, LSLASTNode* old) - { - if (!parent || parent->getNodeType() != NODE_EXPRESSION) - { - return false; - } - switch (parent->getNodeSubType()) - { - case NODE_BINARY_EXPRESSION: - if (operation_mutates(static_cast(parent)->getOperation()) && parent->getChild(1) == old) - { - return false; - } - return true; - // A cast's own printing puts its operand in parentheses where - // it needs them, and around parentheses too. - case NODE_TYPECAST_EXPRESSION: - return false; - case NODE_UNARY_EXPRESSION: - case NODE_PRINT_EXPRESSION: - case NODE_BOOL_CONVERSION_EXPRESSION: - return true; - default: - return false; - } - } - - // Puts `replacement` where `old` stands, in parentheses if the place - // could bind it differently. - void putInPlace(LSLASTNode* old, LSLExpression* replacement, ScriptAllocator* allocator) - { - LSLASTNode* parent = old->getParent(); - replacement->setLoc(old->getLoc()); - if (wantsParens(parent, old) && replacement->getNodeSubType() != NODE_CONSTANT_EXPRESSION && - replacement->getNodeSubType() != NODE_LVALUE_EXPRESSION && replacement->getNodeSubType() != NODE_PARENTHESIS_EXPRESSION && - replacement->getNodeSubType() != NODE_FUNCTION_EXPRESSION) - { - auto* parens = allocator->newTracked(replacement); - parens->setType(replacement->getType()); - parens->setLoc(old->getLoc()); - LSLASTNode::replaceNode(old, parens); - return; - } - LSLASTNode::replaceNode(old, replacement); - } - - // Nodes put together off the script -- a sum built a term at a time - // -- with Tailslide's counting of the references under each held off - // while they are: every node made counts its children's whole - // subtrees again, so a sum of n terms, each the left side of the - // next, is n squared. The whole is counted once as it is put in - // place. Tailslide holds it off the same way while it parses; a node - // made meanwhile is marked made by us, as it would have been. - class Uncounted - { - public: - explicit Uncounted(ScriptContext& context) : mContext(context), mWas(context.parsing) { mContext.parsing = true; } - ~Uncounted() { mContext.parsing = mWas; } - Uncounted(const Uncounted&) = delete; - Uncounted& operator=(const Uncounted&) = delete; - - template T* made(T* node) const - { - node->setSynthesized(true); - return node; - } - - private: - ScriptContext& mContext; - const bool mWas; - }; - - // ---- the library, evaluated ----------------------------------------------------------------- - - ALScriptWeight weigh(ALLSLOptimizer::Target target, std::string_view text); - - struct Ctx - { - ScriptAllocator* allocator = nullptr; - ScriptContext* context = nullptr; - ALLSLOptimizer::Target target = ALLSLOptimizer::Target::Mono; - bool foldtabs = false; - // Whether a call is larger than the string that answers it, by the - // call as written and whether the code holds the answer already: - // weighed once for the run (Folder::smallerAnswered). - boost::unordered_flat_map> answers; - // What each function may write, found once for the run: the passes - // only take code away, so what it says stays true, if more than - // is left. - const ALLSLEffects* effects = nullptr; - - // A builtin constant as an answer: the JSON_* names, whose values - // are characters no literal may carry, are folded to the name. - // The value handed back is the builtin's own, which the call - // recognises and puts the name in for. - std::map namedValues; - LSLConstant* builtin(const char* name) - { - if (!context || !context->builtins) - { - return nullptr; - } - LSLSymbol* sym = context->builtins->lookup(name, SYM_VARIABLE); - if (!sym || !sym->getConstantValue()) - { - return nullptr; - } - namedValues.emplace(sym->getConstantValue(), name); - return sym->getConstantValue(); - } - const std::string* nameOf(LSLConstant* value) const - { - const auto found = namedValues.find(value); - return found == namedValues.end() ? nullptr : &found->second; - } - - LSLConstant* integer(int v) { return allocator->newTracked(v); } - LSLConstant* number(double v) - { - const bool single = target != ALLSLOptimizer::Target::Luau; - if (single) - { - const float f = static_cast(v); - if (!std::isfinite(f)) - { - return nullptr; - } - return allocator->newTracked(static_cast(f)); - } - return std::isfinite(v) ? allocator->newTracked(v) : nullptr; - } - LSLConstant* string(const std::string& s) - { - if (!ascii(s.c_str()) || (!foldtabs && s.find('\t') != std::string::npos)) - { - return nullptr; - } - return allocator->newTracked(allocator->copyStr(s.c_str())); - } - LSLConstant* key(const std::string& s) - { - if (!ascii(s.c_str())) - { - return nullptr; - } - return allocator->newTracked(allocator->copyStr(s.c_str())); - } - LSLConstant* vector(double x, double y, double z) - { - const float fx = static_cast(x), fy = static_cast(y), fz = static_cast(z); - if (!std::isfinite(fx) || !std::isfinite(fy) || !std::isfinite(fz)) - { - return nullptr; - } - return allocator->newTracked(fx, fy, fz); - } - LSLConstant* rotation(float x, float y, float z, float s) - { - if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || !std::isfinite(s)) - { - return nullptr; - } - return allocator->newTracked(x, y, z, s); - } - }; - - typedef std::vector Args; - - bool argInt(const Args& a, size_t i, int& out) - { - if (i >= a.size() || a[i]->getNodeSubType() != NODE_INTEGER_CONSTANT) - { - return false; - } - out = static_cast(a[i])->getValue(); - return true; - } - - // A float argument, or an integer where LSL would promote one. - bool argFloat(const Args& a, size_t i, double& out) - { - if (i >= a.size()) - { - return false; - } - if (a[i]->getNodeSubType() == NODE_FLOAT_CONSTANT) - { - out = static_cast(static_cast(static_cast(a[i])->getValue())); - return true; - } - if (a[i]->getNodeSubType() == NODE_INTEGER_CONSTANT) - { - out = static_cast(static_cast(static_cast(a[i])->getValue())); - return true; - } - return false; - } - - // A string argument, plain ASCII, so that no VM's idea of a character - // differs from ours. - bool argString(const Args& a, size_t i, std::string& out) - { - if (i >= a.size() || a[i]->getNodeSubType() != NODE_STRING_CONSTANT) - { - return false; - } - const char* s = static_cast(a[i])->getValue(); - if (!ascii(s)) - { - return false; - } - out = s; - return true; - } - - bool argList(const Args& a, size_t i, LSLListConstant*& out) - { - if (i >= a.size() || a[i]->getNodeSubType() != NODE_LIST_CONSTANT) - { - return false; - } - out = static_cast(a[i]); - return true; - } - - bool argVector(const Args& a, size_t i, Vector3& out) - { - if (i >= a.size() || a[i]->getNodeSubType() != NODE_VECTOR_CONSTANT) - { - return false; - } - out = *static_cast(a[i])->getValue(); - return true; - } - - // A rotation argument that is a unit quaternion, near enough that the - // VM's normalising of it changes nothing: what the rotation functions - // are folded over, since what a VM does with the rest is its own. - bool argUnitRotation(const Args& a, size_t i, LLQuaternion& out) - { - if (i >= a.size() || a[i]->getNodeSubType() != NODE_QUATERNION_CONSTANT) - { - return false; - } - const Quaternion* q = static_cast(a[i])->getValue(); - const float mag = std::sqrt(q->x * q->x + q->y * q->y + q->z * q->z + q->s * q->s); - if (!std::isfinite(mag) || std::fabs(mag - 1.0f) >= 1e-6f) - { - return false; - } - // set() normalises, which changes nothing of a unit rotation but - // the last bit; the components are taken as they are. - out.mQ[VX] = q->x; - out.mQ[VY] = q->y; - out.mQ[VZ] = q->z; - out.mQ[VW] = q->s; - return true; - } - - std::vector elements(LSLListConstant* list) - { - std::vector out; - for (LSLASTNode* child : *list) - { - out.push_back(static_cast(child)); - } - return out; - } - - LSLConstant* listOf(Ctx& ctx, const std::vector& items) - { - auto* list = ctx.allocator->newTracked(nullptr); - for (LSLConstant* item : items) - { - list->pushChild(item->copy(ctx.allocator)); - } - return list; - } - - // LSL's indices: negative from the end. `start > end` names the - // outside of the range, for a get, and the inside for a delete. - void normalise(int length, int& start, int& end) - { - if (start < 0) - { - start += length; - } - if (end < 0) - { - end += length; - } - } - - template std::vector subRange(const std::vector& v, int start, int end) - { - const int length = static_cast(v.size()); - normalise(length, start, end); - std::vector out; - if (start <= end) - { - for (int i = std::max(0, start); i <= std::min(length - 1, end); ++i) - { - out.push_back(v[i]); - } - return out; - } - for (int i = 0; i <= std::min(length - 1, end); ++i) - { - out.push_back(v[i]); - } - for (int i = std::max(0, start); i < length; ++i) - { - out.push_back(v[i]); - } - return out; - } - - template std::vector deleteRange(const std::vector& v, int start, int end) - { - const int length = static_cast(v.size()); - normalise(length, start, end); - std::vector out; - for (int i = 0; i < length; ++i) - { - const bool inside = start <= end ? (i >= start && i <= end) : (i <= end || i >= start); - if (!inside) - { - out.push_back(v[i]); - } - } - return out; - } - - std::vector chars(const std::string& s) { return std::vector(s.begin(), s.end()); } - std::string fromChars(const std::vector& v) { return std::string(v.begin(), v.end()); } - - // What (string) makes of a float: six places, as both VMs write it. - std::string floatString(double v) - { - char buffer[64]; - snprintf(buffer, sizeof(buffer), "%f", static_cast(static_cast(v))); - return buffer; - } - - // An element as llList2String and llList2CSV write it, or nothing for - // what neither should fold. - bool elementText(LSLConstant* c, std::string& out) - { - switch (c->getNodeSubType()) - { - case NODE_INTEGER_CONSTANT: - out = std::to_string(static_cast(c)->getValue()); - return true; - case NODE_FLOAT_CONSTANT: - out = floatString(static_cast(c)->getValue()); - return true; - case NODE_STRING_CONSTANT: - case NODE_KEY_CONSTANT: - out = static_cast(c)->getValue(); - return ascii(out.c_str()); - case NODE_VECTOR_CONSTANT: - { - const Vector3* v = static_cast(c)->getValue(); - out = "<" + floatString(v->x) + ", " + floatString(v->y) + ", " + floatString(v->z) + ">"; - return true; - } - case NODE_QUATERNION_CONSTANT: - { - const Quaternion* q = static_cast(c)->getValue(); - out = "<" + floatString(q->x) + ", " + floatString(q->y) + ", " + floatString(q->z) + ", " + floatString(q->s) + ">"; - return true; - } - default: - return false; - } - } - - const char* const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - std::string toBase64(const std::string& in) - { - std::string out; - size_t i = 0; - while (i + 2 < in.size()) - { - const uint32_t n = (static_cast(in[i]) << 16) | (static_cast(in[i + 1]) << 8) | static_cast(in[i + 2]); - out += BASE64[(n >> 18) & 63]; - out += BASE64[(n >> 12) & 63]; - out += BASE64[(n >> 6) & 63]; - out += BASE64[n & 63]; - i += 3; - } - if (i + 1 == in.size()) - { - const uint32_t n = static_cast(in[i]) << 16; - out += BASE64[(n >> 18) & 63]; - out += BASE64[(n >> 12) & 63]; - out += "=="; - } - else if (i + 2 == in.size()) - { - const uint32_t n = (static_cast(in[i]) << 16) | (static_cast(in[i + 1]) << 8); - out += BASE64[(n >> 18) & 63]; - out += BASE64[(n >> 12) & 63]; - out += BASE64[(n >> 6) & 63]; - out += '='; - } - return out; - } - - // Strictly formed input only; anything else is the VM's to answer. - bool fromBase64(const std::string& in, std::string& out) - { - if (in.size() % 4 != 0) - { - return false; - } - out.clear(); - for (size_t i = 0; i < in.size(); i += 4) - { - uint32_t n = 0; - int pad = 0; - for (int k = 0; k < 4; ++k) - { - const char c = in[i + k]; - if (c == '=') - { - if (i + 4 != in.size() || (k < 2)) - { - return false; - } - ++pad; - n <<= 6; - continue; - } - if (pad) - { - return false; - } - const char* p = strchr(BASE64, c); - if (!p || !c) - { - return false; - } - n = (n << 6) | static_cast(p - BASE64); - } - out += static_cast((n >> 16) & 255); - if (pad < 2) - { - out += static_cast((n >> 8) & 255); - } - if (pad < 1) - { - out += static_cast(n & 255); - } - } - return true; - } - - // A strict reader of JSON, for llJsonGetValue: only what RFC 8259 - // allows, in ASCII, so that what is folded is what every reader - // agrees on; anything the simulator's own reader might take another - // way -- duplicate keys, numbers with a fraction or an exponent, - // escapes outside ASCII, and the JSON_* answers, which are - // characters no string literal here may hold -- is left to it. - struct JsonField; - - struct JsonValue - { - enum class Kind : U8 - { - Null, - True, - False, - Number, - String, - Array, - Object - }; - Kind kind = Kind::Null; - std::string text; // a number as written, a string unescaped - std::vector items; - std::vector fields; - }; - - struct JsonField - { - std::string first; - JsonValue second; - }; - - struct JsonReader - { - const std::string& in; - size_t at = 0; - bool ok = true; - - explicit JsonReader(const std::string& text) : in(text) {} - - void space() - { - while (at < in.size() && (in[at] == ' ' || in[at] == '\t' || in[at] == '\n' || in[at] == '\r')) - { - ++at; - } - } - bool take(char c) - { - if (at < in.size() && in[at] == c) - { - ++at; - return true; - } - return false; - } - bool word(const char* w) - { - const size_t n = strlen(w); - if (in.compare(at, n, w) == 0) - { - at += n; - return true; - } - return false; - } - bool string(std::string& out) - { - if (!take('"')) - { - return false; - } - out.clear(); - while (at < in.size()) - { - const char c = in[at++]; - if (c == '"') - { - return true; - } - if (static_cast(c) < 0x20) - { - return false; - } - if (c != '\\') - { - out += c; - continue; - } - if (at >= in.size()) - { - return false; - } - const char e = in[at++]; - switch (e) - { - case '"': out += '"'; break; - case '\\': out += '\\'; break; - case '/': out += '/'; break; - case 'b': out += '\b'; break; - case 'f': out += '\f'; break; - case 'n': out += '\n'; break; - case 'r': out += '\r'; break; - case 't': out += '\t'; break; - case 'u': - { - if (at + 4 > in.size()) - { - return false; - } - unsigned code = 0; - for (int k = 0; k < 4; ++k) - { - const char h = in[at++]; - if (!isxdigit(static_cast(h))) - { - return false; - } - code = code * 16 + static_cast(isdigit(static_cast(h)) ? h - '0' : tolower(h) - 'a' + 10); - } - if (code == 0 || code > 0x7F) - { - return false; - } - out += static_cast(code); - break; - } - default: return false; - } - } - return false; - } - bool value(JsonValue& out) - { - space(); - if (at >= in.size()) - { - return false; - } - const char c = in[at]; - if (c == '{') - { - ++at; - out.kind = JsonValue::Kind::Object; - space(); - if (take('}')) - { - return true; - } - while (true) - { - space(); - std::string key; - if (!string(key)) - { - return false; - } - for (const auto& field : out.fields) - { - if (field.first == key) - { - return false; - } - } - space(); - if (!take(':')) - { - return false; - } - JsonValue v; - if (!value(v)) - { - return false; - } - out.fields.emplace_back(std::move(key), std::move(v)); - space(); - if (take(',')) - { - continue; - } - return take('}'); - } - } - if (c == '[') - { - ++at; - out.kind = JsonValue::Kind::Array; - space(); - if (take(']')) - { - return true; - } - while (true) - { - JsonValue v; - if (!value(v)) - { - return false; - } - out.items.push_back(std::move(v)); - space(); - if (take(',')) - { - continue; - } - return take(']'); - } - } - if (c == '"') - { - out.kind = JsonValue::Kind::String; - return string(out.text); - } - if (word("true")) - { - out.kind = JsonValue::Kind::True; - return true; - } - if (word("false")) - { - out.kind = JsonValue::Kind::False; - return true; - } - if (word("null")) - { - out.kind = JsonValue::Kind::Null; - return true; - } - // A number: only a plain integer is taken; a fraction or an - // exponent is left to the simulator's own formatting. - const size_t start = at; - take('-'); - if (at < in.size() && in[at] == '0') - { - ++at; - } - else if (at < in.size() && in[at] >= '1' && in[at] <= '9') - { - while (at < in.size() && isdigit(static_cast(in[at]))) - { - ++at; - } - } - else - { - return false; - } - if (at < in.size() && (in[at] == '.' || in[at] == 'e' || in[at] == 'E')) - { - ok = false; - return false; - } - out.kind = JsonValue::Kind::Number; - out.text = in.substr(start, at - start); - return at > start + (in[start] == '-' ? 1 : 0); - } - bool whole(JsonValue& out) - { - if (!value(out)) - { - return false; - } - space(); - return at == in.size(); - } - }; - - // The value a path of keys and indexes reaches, or null. - const JsonValue* jsonAt(const JsonValue& root, const std::vector& path) - { - const JsonValue* at = &root; - for (LSLConstant* step : path) - { - if (step->getNodeSubType() == NODE_STRING_CONSTANT && at->kind == JsonValue::Kind::Object) - { - const char* key = static_cast(step)->getValue(); - const JsonValue* found = nullptr; - for (const auto& field : at->fields) - { - if (field.first == key) - { - found = &field.second; - } - } - if (!found) - { - return nullptr; - } - at = found; - } - else if (step->getNodeSubType() == NODE_INTEGER_CONSTANT && at->kind == JsonValue::Kind::Array) - { - const int index = static_cast(step)->getValue(); - if (index < 0 || static_cast(index) >= at->items.size()) - { - return nullptr; - } - at = &at->items[static_cast(index)]; - } - else - { - return nullptr; - } - } - return at; - } - - // A string that reads as a string to every JSON writer and reader: - // starts with a letter, holds letters, digits, spaces and plain - // punctuation, and is not a word JSON has a meaning for. - bool plainJsonString(const std::string& s) - { - if (s.empty() || !isalpha(static_cast(s[0]))) - { - return false; - } - for (const char ch : s) - { - if (!(isalnum(static_cast(ch)) || ch == ' ' || ch == '_' || ch == '-' || ch == '.' || ch == ',' || ch == ':' || ch == ';' || - ch == '!' || ch == '?' || ch == '\'' || ch == '(' || ch == ')' || ch == '#' || ch == '@' || ch == '%' || ch == '&' || ch == '*' || - ch == '+' || ch == '=' || ch == '/' || ch == '|' || ch == '~' || ch == '^' || ch == '$')) - { - return false; - } - } - return s != "true" && s != "false" && s != "null"; - } - - // Whether a value is one every writer spells alike: integers, plain - // strings, true, false and null, and arrays and objects of those. - bool jsonPlain(const JsonValue& v) - { - switch (v.kind) - { - case JsonValue::Kind::Number: return v.text != "-0" && v.text.size() <= 10; - case JsonValue::Kind::String: return plainJsonString(v.text); - case JsonValue::Kind::Array: - for (const JsonValue& item : v.items) - { - if (!jsonPlain(item)) return false; - } - return true; - case JsonValue::Kind::Object: - for (const auto& field : v.fields) - { - if (!plainJsonString(field.first) || !jsonPlain(field.second)) return false; - } - return true; - default: return true; - } - } - - // A plain value written the compact way, which is the simulator's. - std::string jsonWrite(const JsonValue& v) - { - switch (v.kind) - { - case JsonValue::Kind::Null: return "null"; - case JsonValue::Kind::True: return "true"; - case JsonValue::Kind::False: return "false"; - case JsonValue::Kind::Number: return v.text; - case JsonValue::Kind::String: return "\"" + v.text + "\""; - case JsonValue::Kind::Array: - { - std::string out = "["; - for (size_t i = 0; i < v.items.size(); ++i) - { - out += (i ? "," : "") + jsonWrite(v.items[i]); - } - return out + "]"; - } - case JsonValue::Kind::Object: - { - std::string out = "{"; - for (size_t i = 0; i < v.fields.size(); ++i) - { - out += (i ? "," : "") + ("\"" + v.fields[i].first + "\":") + jsonWrite(v.fields[i].second); - } - return out + "}"; - } - } - return std::string(); - } - - typedef std::function Evaluator; - - const boost::unordered_flat_map>& evaluators() - { - static const boost::unordered_flat_map> table = { - { "llAbs", - [](Ctx& c, const Args& a) -> LSLConstant* { - int v; - if (!argInt(a, 0, v)) return nullptr; - return c.integer(v < 0 ? static_cast(0u - static_cast(v)) : v); - } }, - { "llFabs", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) ? c.number(std::fabs(v)) : nullptr; - } }, - { "llFloor", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - if (!argFloat(a, 0, v)) return nullptr; - const double r = std::floor(v); - return (r >= -2147483648.0 && r < 2147483648.0) ? c.integer(static_cast(r)) : nullptr; - } }, - { "llCeil", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - if (!argFloat(a, 0, v)) return nullptr; - const double r = std::ceil(v); - return (r >= -2147483648.0 && r < 2147483648.0) ? c.integer(static_cast(r)) : nullptr; - } }, - { "llSqrt", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) && v >= 0.0 ? c.number(std::sqrt(v)) : nullptr; - } }, - { "llPow", - [](Ctx& c, const Args& a) -> LSLConstant* { - double b, e; - return argFloat(a, 0, b) && argFloat(a, 1, e) ? c.number(std::pow(b, e)) : nullptr; - } }, - { "llSin", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) ? c.number(std::sin(v)) : nullptr; - } }, - { "llCos", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) ? c.number(std::cos(v)) : nullptr; - } }, - { "llTan", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) ? c.number(std::tan(v)) : nullptr; - } }, - { "llAtan2", - [](Ctx& c, const Args& a) -> LSLConstant* { - double y, x; - return argFloat(a, 0, y) && argFloat(a, 1, x) ? c.number(std::atan2(y, x)) : nullptr; - } }, - { "llLog", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) && v > 0.0 ? c.number(std::log(v)) : nullptr; - } }, - { "llLog10", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) && v > 0.0 ? c.number(std::log10(v)) : nullptr; - } }, - { "llModPow", - [](Ctx& c, const Args& a) -> LSLConstant* { - int base, exponent, modulus; - if (!argInt(a, 0, base) || !argInt(a, 1, exponent) || !argInt(a, 2, modulus) || base < 0 || exponent < 0 || modulus <= 0) - { - return nullptr; - } - uint64_t result = 1 % static_cast(modulus); - uint64_t b = static_cast(base) % static_cast(modulus); - for (uint32_t e = static_cast(exponent); e; e >>= 1) - { - if (e & 1) - { - result = result * b % static_cast(modulus); - } - b = b * b % static_cast(modulus); - } - return c.integer(static_cast(result)); - } }, - { "llVecMag", - [](Ctx& c, const Args& a) -> LSLConstant* { - Vector3 v; - return argVector(a, 0, v) ? c.number(std::sqrt(double(v.x) * v.x + double(v.y) * v.y + double(v.z) * v.z)) : nullptr; - } }, - { "llVecDist", - [](Ctx& c, const Args& a) -> LSLConstant* { - Vector3 p, q; - if (!argVector(a, 0, p) || !argVector(a, 1, q)) return nullptr; - const float dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z; - return c.number(std::sqrt(double(dx) * dx + double(dy) * dy + double(dz) * dz)); - } }, - { "llVecNorm", - [](Ctx& c, const Args& a) -> LSLConstant* { - Vector3 v; - if (!argVector(a, 0, v)) return nullptr; - const float mag = static_cast(std::sqrt(double(v.x) * v.x + double(v.y) * v.y + double(v.z) * v.z)); - if (mag == 0.0f) return c.vector(0, 0, 0); - return c.vector(v.x / mag, v.y / mag, v.z / mag); - } }, - // The rotations, by the viewer's own quaternion, which is the - // lineage of the simulator's: Euler angles through the - // matrix, axis and angle, and the axes of a rotation as the - // unit vectors turned by it. Only unit rotations are folded. - { "llEuler2Rot", - [](Ctx& c, const Args& a) -> LSLConstant* { - Vector3 v; - if (!argVector(a, 0, v)) return nullptr; - LLQuaternion q; - q.setEulerAngles(v.x, v.y, v.z); - return c.rotation(q.mQ[VX], q.mQ[VY], q.mQ[VZ], q.mQ[VW]); - } }, - { "llRot2Euler", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - F32 roll, pitch, yaw; - q.getEulerAngles(&roll, &pitch, &yaw); - return c.vector(roll, pitch, yaw); - } }, - { "llAxisAngle2Rot", - [](Ctx& c, const Args& a) -> LSLConstant* { - Vector3 axis; - double angle; - if (!argVector(a, 0, axis) || !argFloat(a, 1, angle)) return nullptr; - const LLQuaternion q(static_cast(angle), LLVector3(axis.x, axis.y, axis.z)); - return c.rotation(q.mQ[VX], q.mQ[VY], q.mQ[VZ], q.mQ[VW]); - } }, - { "llRot2Axis", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - F32 angle; - LLVector3 axis; - q.getAngleAxis(&angle, axis); - // No rotation has no axis to speak of: the VM's to say. - if (angle == 0.0f) return nullptr; - return c.vector(axis.mV[VX], axis.mV[VY], axis.mV[VZ]); - } }, - { "llRot2Angle", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - F32 angle; - LLVector3 axis; - q.getAngleAxis(&angle, axis); - return c.number(angle); - } }, - { "llRot2Fwd", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - const LLVector3 v = LLVector3(1.f, 0.f, 0.f) * q; - return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); - } }, - { "llRot2Left", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - const LLVector3 v = LLVector3(0.f, 1.f, 0.f) * q; - return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); - } }, - { "llRot2Up", - [](Ctx& c, const Args& a) -> LSLConstant* { - LLQuaternion q; - if (!argUnitRotation(a, 0, q)) return nullptr; - const LLVector3 v = LLVector3(0.f, 0.f, 1.f) * q; - return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); - } }, - { "llStringLength", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - return argString(a, 0, s) ? c.integer(static_cast(s.size())) : nullptr; - } }, - { "llGetSubString", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - int start, end; - if (!argString(a, 0, s) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; - return c.string(fromChars(subRange(chars(s), start, end))); - } }, - { "llDeleteSubString", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - int start, end; - if (!argString(a, 0, s) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; - return c.string(fromChars(deleteRange(chars(s), start, end))); - } }, - { "llInsertString", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s, ins; - int at; - if (!argString(a, 0, s) || !argInt(a, 1, at) || !argString(a, 2, ins) || at < 0) return nullptr; - const size_t pos = std::min(s.size(), static_cast(at)); - return c.string(s.substr(0, pos) + ins + s.substr(pos)); - } }, - { "llSubStringIndex", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s, p; - if (!argString(a, 0, s) || !argString(a, 1, p)) return nullptr; - const size_t at = s.find(p); - return c.integer(at == std::string::npos ? -1 : static_cast(at)); - } }, - { "llToUpper", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - if (!argString(a, 0, s)) return nullptr; - for (char& ch : s) ch = static_cast(toupper(static_cast(ch))); - return c.string(s); - } }, - { "llToLower", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - if (!argString(a, 0, s)) return nullptr; - for (char& ch : s) ch = static_cast(tolower(static_cast(ch))); - return c.string(s); - } }, - { "llStringTrim", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - int how; - if (!argString(a, 0, s) || !argInt(a, 1, how)) return nullptr; - const auto blank = [](char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; }; - size_t from = 0; - size_t to = s.size(); - if (how & 1) - { - while (from < to && blank(s[from])) ++from; - } - if (how & 2) - { - while (to > from && blank(s[to - 1])) --to; - } - return c.string(s.substr(from, to - from)); - } }, - { "llEscapeURL", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - if (!argString(a, 0, s)) return nullptr; - std::string out; - for (char ch : s) - { - if (isalnum(static_cast(ch))) - { - out += ch; - } - else - { - char buffer[8]; - snprintf(buffer, sizeof(buffer), "%%%02X", static_cast(ch)); - out += buffer; - } - } - return c.string(out); - } }, - { "llUnescapeURL", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - if (!argString(a, 0, s)) return nullptr; - std::string out; - for (size_t i = 0; i < s.size(); ++i) - { - if (s[i] != '%') - { - out += s[i]; - continue; - } - if (i + 2 >= s.size() || !isxdigit(static_cast(s[i + 1])) || !isxdigit(static_cast(s[i + 2]))) - { - return nullptr; - } - const int byte = static_cast(strtol(s.substr(i + 1, 2).c_str(), nullptr, 16)); - if (byte == 0 || byte >= 0x80) - { - return nullptr; - } - out += static_cast(byte); - i += 2; - } - return c.string(out); - } }, - { "llStringToBase64", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - return argString(a, 0, s) ? c.string(toBase64(s)) : nullptr; - } }, - { "llBase64ToString", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s, out; - if (!argString(a, 0, s) || !fromBase64(s, out) || out.find('\0') != std::string::npos) return nullptr; - return c.string(out); - } }, - { "llIntegerToBase64", - [](Ctx& c, const Args& a) -> LSLConstant* { - int v; - if (!argInt(a, 0, v)) return nullptr; - const uint32_t u = static_cast(v); - std::string bytes{ static_cast(u >> 24), static_cast(u >> 16), static_cast(u >> 8), static_cast(u) }; - return c.string(toBase64(bytes)); - } }, - { "llBase64ToInteger", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s, out; - if (!argString(a, 0, s) || s.size() != 8 || !fromBase64(s, out) || out.size() != 4) return nullptr; - const uint32_t u = (static_cast(out[0]) << 24) | (static_cast(out[1]) << 16) | - (static_cast(out[2]) << 8) | static_cast(out[3]); - return c.integer(static_cast(u)); - } }, - { "llChar", - [](Ctx& c, const Args& a) -> LSLConstant* { - int v; - if (!argInt(a, 0, v)) return nullptr; - if (v == 0) return c.string(std::string()); - return (v > 0 && v < 128) ? c.string(std::string(1, static_cast(v))) : nullptr; - } }, - { "llOrd", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s; - int at; - if (!argString(a, 0, s) || !argInt(a, 1, at)) return nullptr; - const int length = static_cast(s.size()); - if (at < 0) at += length; - return (at >= 0 && at < length) ? c.integer(static_cast(s[at])) : c.integer(0); - } }, - { "llReplaceSubString", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string s, pattern, with; - int count; - if (!argString(a, 0, s) || !argString(a, 1, pattern) || !argString(a, 2, with) || !argInt(a, 3, count) || pattern.empty() || count < 0) - { - return nullptr; - } - std::string out; - size_t from = 0; - int done = 0; - while (true) - { - const size_t at = s.find(pattern, from); - if (at == std::string::npos || (count > 0 && done == count)) - { - out += s.substr(from); - break; - } - out += s.substr(from, at - from) + with; - from = at + pattern.size(); - ++done; - } - return c.string(out); - } }, - { "llGetListLength", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - return argList(a, 0, l) ? c.integer(static_cast(l->getLength())) : nullptr; - } }, - { "llList2List", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - int start, end; - if (!argList(a, 0, l) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; - return listOf(c, subRange(elements(l), start, end)); - } }, - { "llDeleteSubList", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - int start, end; - if (!argList(a, 0, l) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; - return listOf(c, deleteRange(elements(l), start, end)); - } }, - { "llListInsertList", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant *l, *ins; - int at; - if (!argList(a, 0, l) || !argList(a, 1, ins) || !argInt(a, 2, at)) return nullptr; - std::vector items = elements(l); - const int length = static_cast(items.size()); - if (at < 0) at += length; - if (at < 0) return nullptr; - const size_t pos = std::min(items.size(), static_cast(at)); - std::vector more = elements(ins); - items.insert(items.begin() + pos, more.begin(), more.end()); - return listOf(c, items); - } }, - { "llListReplaceList", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant *l, *with; - int start, end; - if (!argList(a, 0, l) || !argList(a, 1, with) || !argInt(a, 2, start) || !argInt(a, 3, end)) return nullptr; - std::vector items = elements(l); - const int length = static_cast(items.size()); - normalise(length, start, end); - if (start > end || start < 0 || start > length) return nullptr; - std::vector out(items.begin(), items.begin() + std::min(length, start)); - std::vector more = elements(with); - out.insert(out.end(), more.begin(), more.end()); - if (end + 1 < length) - { - out.insert(out.end(), items.begin() + end + 1, items.end()); - } - return listOf(c, out); - } }, - { "llGetListEntryType", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - int at; - if (!argList(a, 0, l) || !argInt(a, 1, at)) return nullptr; - std::vector items = elements(l); - const int length = static_cast(items.size()); - if (at < 0) at += length; - if (at < 0 || at >= length) return c.integer(0); - switch (items[at]->getNodeSubType()) - { - case NODE_INTEGER_CONSTANT: return c.integer(1); - case NODE_FLOAT_CONSTANT: return c.integer(2); - case NODE_STRING_CONSTANT: return c.integer(3); - case NODE_KEY_CONSTANT: return c.integer(4); - case NODE_VECTOR_CONSTANT: return c.integer(5); - case NODE_QUATERNION_CONSTANT: return c.integer(6); - default: return nullptr; - } - } }, - { "llJsonGetValue", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string json; - LSLListConstant* path; - if (!argString(a, 0, json) || !argList(a, 1, path)) return nullptr; - JsonValue root; - JsonReader reader(json); - if (!reader.whole(root)) return nullptr; - const JsonValue* at = jsonAt(root, elements(path)); - if (!at) return c.builtin("JSON_INVALID"); - // A string or a plain number: what is written is what - // is answered; true, false and null are their constants. - // A nested object or array the simulator spells its own - // way. - switch (at->kind) - { - case JsonValue::Kind::String: - case JsonValue::Kind::Number: return c.string(at->text); - case JsonValue::Kind::True: return c.builtin("JSON_TRUE"); - case JsonValue::Kind::False: return c.builtin("JSON_FALSE"); - case JsonValue::Kind::Null: return c.builtin("JSON_NULL"); - default: - // A nested value as the simulator writes it, where - // every writer would write it alike. - return jsonPlain(*at) ? c.string(jsonWrite(*at)) : nullptr; - } - } }, - { "llJsonSetValue", - [](Ctx& c, const Args& a) -> LSLConstant* { - // Over a plain document, down a path that is there but - // for its last step, which may be a new key or the next - // index; the value a plain string, an integer, or true, - // false or null by its constant. Anything the simulator - // might make more of -- a path to create, a deletion, a - // value that reads as JSON -- is left to it. - std::string json; - LSLListConstant* path; - if (!argString(a, 0, json) || !argList(a, 1, path) || a.size() < 3 || a[2]->getNodeSubType() != NODE_STRING_CONSTANT) return nullptr; - JsonValue root; - JsonReader reader(json); - if (!reader.whole(root) || !jsonPlain(root)) return nullptr; - const std::string value = static_cast(a[2])->getValue(); - JsonValue put; - auto same = [&](const char* name) { - LSLConstant* builtin = c.builtin(name); - return builtin && value == static_cast(builtin)->getValue(); - }; - // JSON_DELETE takes out what is there: a key from an - // object, an index from an array; what is not there the - // simulator answers for. - const bool deleting = same("JSON_DELETE"); - if (same("JSON_TRUE")) put.kind = JsonValue::Kind::True; - else if (same("JSON_FALSE")) put.kind = JsonValue::Kind::False; - else if (same("JSON_NULL")) put.kind = JsonValue::Kind::Null; - else if (deleting) - { - } - else if (plainJsonString(value)) - { - put.kind = JsonValue::Kind::String; - put.text = value; - } - else - { - // An integer, as JSON writes one. - size_t i = value.size() > 1 && value[0] == '-' ? 1 : 0; - if (i >= value.size() || value.size() > 10 || (value[i] == '0' && value.size() > i + 1)) return nullptr; - for (; i < value.size(); ++i) - { - if (!isdigit(static_cast(value[i]))) return nullptr; - } - put.kind = JsonValue::Kind::Number; - put.text = value; - } - const std::vector steps = elements(path); - if (steps.empty()) return nullptr; - JsonValue* at = &root; - for (size_t i = 0; i + 1 < steps.size(); ++i) - { - const std::vector one(steps.begin() + static_cast(i), steps.begin() + static_cast(i) + 1); - const JsonValue* next = jsonAt(*at, one); - if (!next) return nullptr; - at = const_cast(next); - } - LSLConstant* last = steps.back(); - if (last->getNodeSubType() == NODE_STRING_CONSTANT && at->kind == JsonValue::Kind::Object) - { - std::string key; - if (!argString({ last }, 0, key) || !plainJsonString(key)) return nullptr; - bool found = false; - for (size_t i = 0; i < at->fields.size(); ++i) - { - if (at->fields[i].first == key) - { - found = true; - if (deleting) at->fields.erase(at->fields.begin() + static_cast(i)); - else at->fields[i].second = put; - break; - } - } - if (!found && deleting) return nullptr; - if (!found) at->fields.emplace_back(key, put); - } - else if (last->getNodeSubType() == NODE_INTEGER_CONSTANT && at->kind == JsonValue::Kind::Array) - { - const int index = static_cast(last)->getValue(); - if (deleting) - { - if (index < 0 || static_cast(index) >= at->items.size()) return nullptr; - at->items.erase(at->items.begin() + index); - } - else if (index == -1 || static_cast(index) == at->items.size()) at->items.push_back(put); - else if (index >= 0 && static_cast(index) < at->items.size()) at->items[static_cast(index)] = put; - else if (index > 0) return c.builtin("JSON_INVALID"); - else return nullptr; - } - else - { - return nullptr; - } - return c.string(jsonWrite(root)); - } }, - { "llJsonValueType", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string json; - LSLListConstant* path; - if (!argString(a, 0, json) || !argList(a, 1, path)) return nullptr; - JsonValue root; - JsonReader reader(json); - if (!reader.whole(root)) return nullptr; - const JsonValue* at = jsonAt(root, elements(path)); - if (!at) return c.builtin("JSON_INVALID"); - switch (at->kind) - { - case JsonValue::Kind::Object: return c.builtin("JSON_OBJECT"); - case JsonValue::Kind::Array: return c.builtin("JSON_ARRAY"); - case JsonValue::Kind::String: return c.builtin("JSON_STRING"); - case JsonValue::Kind::Number: return c.builtin("JSON_NUMBER"); - case JsonValue::Kind::True: return c.builtin("JSON_TRUE"); - case JsonValue::Kind::False: return c.builtin("JSON_FALSE"); - case JsonValue::Kind::Null: return c.builtin("JSON_NULL"); - } - return nullptr; - } }, - { "llList2Json", - [](Ctx& c, const Args& a) -> LSLConstant* { - // Only what every writer spells alike: integers and - // plain strings -- letters, digits after the first, - // spaces and the punctuation that needs no escape -- - // that read as nothing else. Floats, JSON values as - // strings, and anything wanting an escape are left. - LSLListConstant* items; - if (a.empty() || a[0]->getNodeSubType() != NODE_STRING_CONSTANT || !argList(a, 1, items)) return nullptr; - // The kind is a JSON_* value, which is no ASCII string. - const std::string kind = static_cast(a[0])->getValue(); - LSLConstant* array = c.builtin("JSON_ARRAY"); - LSLConstant* object = c.builtin("JSON_OBJECT"); - const bool is_array = array && kind == static_cast(array)->getValue(); - const bool is_object = object && kind == static_cast(object)->getValue(); - if (!is_array && !is_object) return nullptr; - const std::vector list = elements(items); - if (is_object && list.size() % 2 != 0) return nullptr; - std::string out = is_array ? "[" : "{"; - for (size_t i = 0; i < list.size(); ++i) - { - // A comma before each item, or before each pair. - if (i > 0 && (is_array || i % 2 == 0)) out += ","; - LSLConstant* item = list[i]; - if (is_object && i % 2 == 0) - { - std::string key; - if (!argString({ item }, 0, key) || !plainJsonString(key)) return nullptr; - out += "\"" + key + "\":"; - continue; - } - if (item->getNodeSubType() == NODE_INTEGER_CONSTANT) - { - out += std::to_string(static_cast(item)->getValue()); - continue; - } - std::string text; - if (!argString({ item }, 0, text) || !plainJsonString(text)) return nullptr; - out += "\"" + text + "\""; - } - out += is_array ? "]" : "}"; - return c.string(out); - } }, - { "llJson2List", - [](Ctx& c, const Args& a) -> LSLConstant* { - // An array's items or an object's keys and values, where - // every one is an integer or a plain string; anything - // else the simulator types its own way. - std::string json; - if (!argString(a, 0, json)) return nullptr; - JsonValue root; - JsonReader reader(json); - if (!reader.whole(root)) return nullptr; - std::vector values; - std::vector out; - auto push = [&](const JsonValue& v) { - if (v.kind == JsonValue::Kind::Number) - { - if (v.text.size() > 10) return false; - out.push_back(c.integer(static_cast(std::strtol(v.text.c_str(), nullptr, 10)))); - return true; - } - if (v.kind == JsonValue::Kind::String && plainJsonString(v.text)) - { - out.push_back(c.string(v.text)); - return true; - } - return false; - }; - if (root.kind == JsonValue::Kind::Array) - { - for (const JsonValue& v : root.items) - { - if (!push(v)) return nullptr; - } - } - else if (root.kind == JsonValue::Kind::Object) - { - for (const auto& field : root.fields) - { - if (!plainJsonString(field.first)) return nullptr; - out.push_back(c.string(field.first)); - if (!push(field.second)) return nullptr; - } - } - else - { - return nullptr; - } - return listOf(c, out); - } }, - { "llAcos", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) && std::fabs(v) <= 1.0 ? c.number(std::acos(v)) : nullptr; - } }, - { "llAsin", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - return argFloat(a, 0, v) && std::fabs(v) <= 1.0 ? c.number(std::asin(v)) : nullptr; - } }, - { "llRound", - [](Ctx& c, const Args& a) -> LSLConstant* { - double v; - if (!argFloat(a, 0, v)) return nullptr; - // LSO adds the half in single precision, Mono in double: - // folded only where the two come to the same. - double rounded = std::floor(v + 0.5); - if (c.target != ALLSLOptimizer::Target::Luau) - { - const float f = static_cast(v); - rounded = std::floor(static_cast(f) + 0.5); - if (static_cast(std::floor(f + 0.5f)) != rounded) return nullptr; - } - return std::fabs(rounded) < 2147483647.0 ? c.integer(static_cast(rounded)) : nullptr; - } }, - { "llDumpList2String", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - std::string between; - if (!argList(a, 0, l) || !argString(a, 1, between) || !ascii(between.c_str())) return nullptr; - std::string out; - bool first = true; - for (LSLConstant* item : elements(l)) - { - std::string text; - if (!elementText(item, text)) return nullptr; - out += (first ? "" : between) + text; - first = false; - } - return c.string(out); - } }, - { "llListFindList", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* in; - LSLListConstant* sought; - // An empty list to find is found where LSO and Mono - // disagree: not folded. - if (!argList(a, 0, in) || !argList(a, 1, sought)) return nullptr; - const std::vector hay = elements(in); - const std::vector find = elements(sought); - if (find.empty()) return nullptr; - // The same type and the same value. - const auto same = [](LSLConstant* x, LSLConstant* y) { - if (x->getNodeSubType() != y->getNodeSubType()) return false; - switch (x->getNodeSubType()) - { - case NODE_INTEGER_CONSTANT: - return static_cast(x)->getValue() == static_cast(y)->getValue(); - case NODE_FLOAT_CONSTANT: - return static_cast(x)->getValue() == static_cast(y)->getValue(); - case NODE_STRING_CONSTANT: - case NODE_KEY_CONSTANT: - return !strcmp(static_cast(x)->getValue(), static_cast(y)->getValue()); - case NODE_VECTOR_CONSTANT: - { - const Vector3* p = static_cast(x)->getValue(); - const Vector3* q = static_cast(y)->getValue(); - return p->x == q->x && p->y == q->y && p->z == q->z; - } - case NODE_QUATERNION_CONSTANT: - { - const Quaternion* p = static_cast(x)->getValue(); - const Quaternion* q = static_cast(y)->getValue(); - return p->x == q->x && p->y == q->y && p->z == q->z && p->s == q->s; - } - default: - return false; - } - }; - for (size_t i = 0; i + find.size() <= hay.size(); ++i) - { - bool all = true; - for (size_t j = 0; j < find.size() && all; ++j) - { - all = same(hay[i + j], find[j]); - } - if (all) return c.integer(static_cast(i)); - } - return c.integer(-1); - } }, - { "llMD5String", - [](Ctx& c, const Args& a) -> LSLConstant* { - std::string text; - int nonce; - if (!argString(a, 0, text) || !argInt(a, 1, nonce)) return nullptr; - LLMD5 md5; - md5.update(text + ":" + std::to_string(nonce)); - md5.finalize(); - char hex[33]; - md5.hex_digest(hex); - return c.string(hex); - } }, - { "llList2CSV", - [](Ctx& c, const Args& a) -> LSLConstant* { - LSLListConstant* l; - if (!argList(a, 0, l)) return nullptr; - std::string out; - bool first = true; - for (LSLConstant* item : elements(l)) - { - std::string text; - if (!elementText(item, text)) return nullptr; - out += (first ? "" : ", ") + text; - first = false; - } - return c.string(out); - } }, - }; - return table; - } - - // llList2Integer and its kin: the element where it is the type asked - // for, the type's nothing where the index is off the end, and the - // VM's own answer otherwise. - LSLConstant* listGet(Ctx& c, const Args& a, LSLNodeSubType wanted) - { - LSLListConstant* l; - int at; - if (!argList(a, 0, l) || !argInt(a, 1, at)) - { - return nullptr; - } - std::vector items = elements(l); - const int length = static_cast(items.size()); - if (at < 0) - { - at += length; - } - if (at < 0 || at >= length) - { - switch (wanted) - { - case NODE_INTEGER_CONSTANT: return c.integer(0); - case NODE_FLOAT_CONSTANT: return c.number(0.0); - case NODE_STRING_CONSTANT: return c.string(std::string()); - // What LSO gives for a key out of range is NULL_KEY, which - // the call is folded to by name; Mono gives an empty key. - case NODE_KEY_CONSTANT: return c.target == ALLSLOptimizer::Target::LSO ? c.builtin("NULL_KEY") : c.key(std::string()); - case NODE_VECTOR_CONSTANT: return c.vector(0, 0, 0); - case NODE_QUATERNION_CONSTANT: return c.allocator->newTracked(0.f, 0.f, 0.f, 1.f); - default: return nullptr; - } - } - LSLConstant* item = items[at]; - if (wanted == NODE_STRING_CONSTANT) - { - std::string text; - return elementText(item, text) ? c.string(text) : nullptr; - } - if (wanted == NODE_KEY_CONSTANT && item->getNodeSubType() == NODE_STRING_CONSTANT) - { - return c.key(static_cast(item)->getValue()); - } - return item->getNodeSubType() == wanted ? item->copy(c.allocator) : nullptr; - } - - LSLConstant* evaluate(Ctx& ctx, const char* name, const Args& args) - { - // The list getters are in the table with the rest; nothing is - // compared before it is looked up. - static const boost::unordered_flat_map> GETTERS = { - { "llList2Integer", NODE_INTEGER_CONSTANT }, { "llList2Float", NODE_FLOAT_CONSTANT }, - { "llList2String", NODE_STRING_CONSTANT }, { "llList2Key", NODE_KEY_CONSTANT }, - { "llList2Vector", NODE_VECTOR_CONSTANT }, { "llList2Rot", NODE_QUATERNION_CONSTANT }, - }; - if (const auto getter = GETTERS.find(name); getter != GETTERS.end()) - { - return listGet(ctx, args, getter->second); - } - auto it = evaluators().find(name); - return it == evaluators().end() ? nullptr : it->second(ctx, args); - } - - // ---- the passes ----------------------------------------------------------------------------- - - // How many nodes a script is, for the budget: what one walk of it - // visits. - struct Gather : public ASTVisitor - { - size_t& count; - explicit Gather(size_t& into) : count(into) {} - bool visit(LSLASTNode* node) override - { - ++count; - return true; - } - }; - - using Counted = boost::unordered_flat_map>; - - // The string literals in what it visits, keys' too. - struct Strings : public ASTVisitor - { - Counted found; - - bool visit(LSLStringConstant* c) override - { - ++found[c->getValue()]; - return false; - } - bool visit(LSLKeyConstant* c) override - { - ++found[c->getValue()]; - return false; - } - }; - - struct Pass - { - Ctx& ctx; - Report& report; - const ALLSLOptimizer::Options& options; - int changes = 0; - - Pass(Ctx& c, Report& r, const ALLSLOptimizer::Options& o) : ctx(c), report(r), options(o) {} - - LSLConstantExpression* constant(LSLConstant* cv, LSLASTNode* at) - { - auto* expr = ctx.allocator->newTracked(cv); - expr->setLoc(at->getLoc()); - return expr; - } - - // Whether a value may be written into the script as a literal. A - // list literal is no smaller than the list expression it came - // from, but it is smaller than a call. - bool inlineable(LSLConstant* cv, bool listsToo = false) - { - if (!cv || cv->containsNaN()) - { - return false; - } - switch (cv->getIType()) - { - case LST_LIST: - if (!listsToo) - { - return false; - } - for (LSLASTNode* item : *cv) - { - if (!inlineable(static_cast(item)) && item->getIType() != LST_KEY) - { - return false; - } - } - return true; - case LST_KEY: - // A key literal is a string. - return false; - case LST_STRING: - { - const char* s = static_cast(cv)->getValue(); - return options.foldtabs || !strchr(s, '\t'); - } - case LST_FLOATINGPOINT: - return std::isfinite(static_cast(cv)->getValue()); - default: - return true; - } - } - - void fold(LSLASTNode* node, LSLConstant* cv, const char* key, const char* what) - { - // What it was and what it became, printed only where the - // notes are read: printing is a visit of the whole subtree, - // and a fold is what a pass does most. - LSLConstantExpression* expr = nullptr; - if (report.wanted()) - { - const std::string was = render(node); - expr = constant(cv, node); - const std::string now = render(expr); - if (was != now) - { - report.note(node->getLoc(), key, std::string(what) + " [1] to [2]", { was, now }); - } - } - else - { - expr = constant(cv, node); - } - LSLASTNode::replaceNode(node, expr); - ++changes; - } - }; - - // Constant expressions and never-assigned variables become their - // values; a call to a pure library function with its arguments in - // hand becomes its answer. - class Folder : public ASTVisitor, public Pass - { - public: - using Pass::Pass; - - bool visit(LSLExpression* expr) override - { - switch (expr->getNodeSubType()) - { - case NODE_CONSTANT_EXPRESSION: - return false; - case NODE_FUNCTION_EXPRESSION: - return !call(static_cast(expr)); - default: - break; - } - LSLConstant* cv = expr->getConstantValue(); - if (cv && inlineable(cv)) - { - fold(expr, cv, "OptimizerFolded", "folded"); - return false; - } - return true; - } - - bool visit(LSLLValueExpression* lvalue) override - { - LSLSymbol* sym = lvalue->getSymbol(); - if (!sym || sym->getSubType() == SYM_BUILTIN || sym->getIType() == LST_LIST) - { - // A builtin constant is a token the compiler already has. - return false; - } - if (sym->getIType() == LST_KEY && !keyMayInline(lvalue)) - { - return false; - } - LSLConstant* cv = lvalue->getConstantValue(); - if (cv && inlineable(cv) && writeOut(sym, cv)) - { - fold(lvalue, cv, "OptimizerInlinedConstant", "inlined"); - } - return false; - } - - private: - // A global's value goes where it is read only where writing it at - // every place it is read costs less than the global does: a vector - // read ten times is ten vectors. What goes where is the target's - // (ALLSLCosts). A value a larger expression folds into is not a - // read of it, and folds whatever this says. - bool writeOut(LSLSymbol* sym, LSLConstant* cv) const - { - if (sym->getSubType() != SYM_GLOBAL) - { - return true; - } - const ALLSLCosts& costs = ALLSLCosts::of(ctx.target); - const auto whole = [](std::initializer_list parts) { - return std::all_of(parts.begin(), parts.end(), [](double v) { return integral(v); }); - }; - ALLSLCosts::Held held; - switch (cv->getIType()) - { - case LST_INTEGER: - held = costs.integer; - break; - case LST_FLOATINGPOINT: - held = whole({ static_cast(cv)->getValue() }) ? costs.wholeFloating : costs.floating; - break; - case LST_VECTOR: - { - const Vector3* v = static_cast(cv)->getValue(); - held = whole({ v->x, v->y, v->z }) ? costs.wholeVector : costs.vector; - break; - } - case LST_QUATERNION: - { - const Quaternion* q = static_cast(cv)->getValue(); - held = whole({ q->x, q->y, q->z, q->s }) ? costs.wholeRotation : costs.rotation; - break; - } - case LST_STRING: - held = costs.stringOf(static_cast(strlen(static_cast(cv)->getValue()))); - break; - default: - return true; - } - return held.writeOut(sym->getReferences() - 1 - sym->getAssignments()); - } - - // Tailslide's rules: a key's key-ness must not be lost to a list, - // a print, a condition or a boolean. - static bool keyMayInline(LSLLValueExpression* lvalue) - { - LSLASTNode* ancestor = lvalue->getParent(); - LSLASTNode* top = lvalue; - while (ancestor && ancestor->getNodeType() == NODE_EXPRESSION) - { - switch (ancestor->getNodeSubType()) - { - case NODE_LIST_EXPRESSION: - case NODE_PRINT_EXPRESSION: - case NODE_BOOL_CONVERSION_EXPRESSION: - return false; - case NODE_TYPECAST_EXPRESSION: - if (ancestor->getIType() == LST_LIST) - { - return false; - } - break; - default: - break; - } - top = ancestor; - ancestor = ancestor->getParent(); - } - if (top->getIType() == LST_KEY) - { - if (ancestor && ancestor->getNodeType() == NODE_AST_NODE_LIST) - { - ancestor = ancestor->getParent(); - } - if (ancestor && ancestor->getNodeType() == NODE_STATEMENT) - { - switch (ancestor->getNodeSubType()) - { - case NODE_WHILE_STATEMENT: - case NODE_IF_STATEMENT: - case NODE_DO_STATEMENT: - case NODE_FOR_STATEMENT: - return false; - default: - break; - } - } - } - return true; - } - - bool call(LSLFunctionExpression* expr) - { - LSLSymbol* sym = expr->getSymbol(); - if (!sym || sym->getSubType() != SYM_BUILTIN || !isPure(sym->getName())) - { - return false; - } - Args args; - for (LSLASTNode* arg : *expr->getArguments()) - { - LSLConstant* cv = arg->getConstantValue(); - if (!cv && arg->getNodeSubType() == NODE_LVALUE_EXPRESSION) - { - // A list is never written into the script in place of - // its name, but a call may still be answered from it. - auto* lvalue = static_cast(arg); - LSLSymbol* sym = lvalue->getSymbol(); - if (sym && !lvalue->getMember() && sym->getAssignments() == 0 && sym->getSubType() != SYM_BUILTIN) - { - cv = sym->getConstantValue(); - } - } - if (!cv) - { - return false; - } - args.push_back(cv); - } - LSLConstant* value = evaluate(ctx, sym->getName(), args); - if (!value) - { - return false; - } - if (const std::string* name = ctx.nameOf(value)) - { - // The builtin's name in place of the call: a token the - // compiler already has. - LSLSymbol* builtin = ctx.context->builtins->lookup(name->c_str(), SYM_VARIABLE); - if (!builtin) - { - return false; - } - // newTracked passes the context itself. - auto* id = ctx.allocator->newTracked(ctx.allocator->copyStr(name->c_str())); - id->setSymbol(builtin); - auto* lvalue = ctx.allocator->newTracked(id, static_cast(nullptr)); - lvalue->setLoc(expr->getLoc()); - id->setLoc(expr->getLoc()); - report.note(expr->getLoc(), "OptimizerEvaluated", "evaluated [1] to [2]", { render(expr), *name }); - LSLASTNode::replaceNode(expr, lvalue); - ++changes; - return true; - } - if (!inlineable(value, true)) - { - return false; - } - // An answer no longer than the strings the call was given takes - // no more room than they did; a longer one can be larger than - // the call, on a target that writes a string at every place it - // is used, and on one that holds each string once where the - // answer is new to it. - if (value->getIType() == LST_STRING && strlen(static_cast(value)->getValue()) > givenChars(expr) && - !smallerAnswered(expr, args, static_cast(value))) - { - return false; - } - fold(expr, value, "OptimizerEvaluated", "evaluated"); - return true; - } - - // Whether a string is smaller than the call it answers, as the - // target's compiler counts: each compiled in a script of its own, - // beside what stays either way -- the function called with its - // strings emptied, for the reference to it a script may keep, and - // each string of the call's and the answer that the code holds - // elsewhere. - bool smallerAnswered(LSLFunctionExpression* expr, const Args& args, LSLStringConstant* value) - { - const bool wide = ctx.target == ALLSLOptimizer::Target::Luau; - const std::optional answer = ALLSLValues::literal(value, wide); - if (!answer) - { - return false; - } - const std::string name = expr->getSymbol()->getName(); - std::string call = name + "("; - std::string again = call; - Counted own; - for (size_t i = 0; i < args.size(); ++i) - { - const std::optional arg = ALLSLValues::literal(args[i], wide); - const std::optional empty = emptied(args[i], wide); - if (!arg || !empty) - { - return false; - } - call += (i ? ", " : "") + *arg; - again += (i ? ", " : "") + *empty; - count(args[i], own); - } - call += ")"; - again += ")"; - std::string before = "default { state_entry() { llOwnerSay(" + again + ");"; - const Counted held = holds(expr); - const auto elsewhere = [&](const std::string& text) { - const auto in = held.find(text); - const auto mine = own.find(text); - return in != held.end() && in->second > (mine == own.end() ? 0 : mine->second); - }; - for (const auto& [text, times] : own) - { - if (elsewhere(text)) - { - const std::optional written = ALLSLValues::literal(ctx.allocator->newTracked(ctx.allocator->copyStr(text.c_str())), wide); - if (!written) - { - return false; - } - before += " llOwnerSay(" + *written + ");"; - } - } - if (elsewhere(value->getValue())) - { - before += " llOwnerSay(" + *answer + ");"; - } - const std::string key = before + call; - if (const auto found = ctx.answers.find(key); found != ctx.answers.end()) - { - return found->second; - } - const ALScriptWeight asCall = weigh(ctx.target, before + " llOwnerSay(" + call + "); } }"); - const ALScriptWeight asAnswer = weigh(ctx.target, before + " llOwnerSay(" + *answer + "); } }"); - const bool smaller = asCall.compiled && asAnswer.compiled && asAnswer.total <= asCall.total; - ctx.answers.emplace(key, smaller); - return smaller; - } - - // The strings in a value, each as many times as it is there. - static void count(LSLConstant* cv, Counted& out) - { - switch (cv->getIType()) - { - case LST_STRING: - ++out[static_cast(cv)->getValue()]; - break; - case LST_KEY: - ++out[static_cast(cv)->getValue()]; - break; - case LST_LIST: - for (LSLASTNode* item : *cv) - { - count(static_cast(item), out); - } - break; - default: - break; - } - } - - // A value as a literal of its shape, its strings empty. - static std::optional emptied(LSLConstant* cv, bool wide) - { - switch (cv->getIType()) - { - case LST_STRING: - case LST_KEY: - return "\"\""; - case LST_LIST: - { - std::string out = "["; - for (LSLASTNode* item : *cv) - { - const std::optional element = emptied(static_cast(item), wide); - if (!element) - { - return std::nullopt; - } - out += (out.size() > 1 ? ", " : "") + *element; - } - return out + "]"; - } - default: - return ALLSLValues::literal(cv, wide); - } - } - - // The strings the code holds where a place would share them, each - // as many times as it is written, as the code is now: Mono's are - // the whole script's, Luau's each function's; LSO writes one - // wherever it is used. - Counted holds(LSLASTNode* at) const - { - if (ctx.target == ALLSLOptimizer::Target::LSO) - { - return {}; - } - LSLASTNode* scope = at; - while (scope->getParent() && (ctx.target == ALLSLOptimizer::Target::Mono || (scope->getNodeType() != NODE_GLOBAL_FUNCTION && - scope->getNodeType() != NODE_EVENT_HANDLER))) - { - scope = scope->getParent(); - } - Strings strings; - scope->visit(&strings); - return std::move(strings.found); - } - - // The characters of the strings written in a call's arguments, - // which go with it: a variable's value stays where it is. - static size_t givenChars(LSLFunctionExpression* expr) - { - Counted given; - for (LSLASTNode* arg : *expr->getArguments()) - { - if (arg->getNodeSubType() != NODE_LVALUE_EXPRESSION && arg->getConstantValue()) - { - count(arg->getConstantValue(), given); - } - } - size_t chars = 0; - for (const auto& [text, times] : given) - { - chars += text.size() * times; - } - return chars; - } - }; - - // The identities, the shorter spellings, and the reshaped conditions. - class Simplifier : public ASTVisitor, public Pass - { - public: - using Pass::Pass; - - bool visit(LSLBinaryExpression* expr) override - { - visitChildren(expr); - LSLExpression* left = expr->getLHS(); - LSLExpression* right = expr->getRHS(); - if (!left || !right || expr->getIType() == LST_ERROR) - { - return false; - } - const LSLOperator op = expr->getOperation(); - const LSLIType type = expr->getIType(); - const bool keepLeft = left->getIType() == type; - const bool keepRight = right->getIType() == type; - const bool numeric = type == LST_INTEGER || type == LST_FLOATINGPOINT; - const auto zero = [](LSLASTNode* n) { return isInteger(n, 0) || isFloat(n, 0.0); }; - const auto one = [](LSLASTNode* n) { return isInteger(n, 1) || isFloat(n, 1.0); }; - switch (op) - { - case OP_PLUS: - // What adds nothing, by what the sum is: an empty - // string to a string, an empty list to a list, and a - // zero to an integer. A list plus anything else is that - // list with one more element -- `l + 0` appends a zero, - // and so does `(list)a + 0`, which is what a list's - // literal is written as -- and a float plus zero is not - // the float where the float is -0.0. - if (keepLeft && ((type == LST_INTEGER && zero(right)) || (type == LST_STRING && isEmptyString(right)) || - (type == LST_LIST && isEmptyList(right)))) - { - return keep(expr, 0); - } - if (keepRight && ((type == LST_INTEGER && zero(left)) || (type == LST_STRING && isEmptyString(left)) || - (type == LST_LIST && isEmptyList(left)))) - { - return keep(expr, 1); - } - if (numeric && keepLeft && negative(right)) return resign(expr, OP_MINUS); - break; - case OP_MINUS: - // Taking zero away leaves a number as it was, -0.0 too. - if (numeric && keepLeft && zero(right)) return keep(expr, 0); - if (numeric && keepLeft && negative(right)) return resign(expr, OP_PLUS); - break; - case OP_MUL: - if (keepLeft && one(right)) return keep(expr, 0); - if (keepRight && one(left)) return keep(expr, 1); - if (type == LST_INTEGER && isInteger(right, 0) && changesNothing(left)) return become(expr, ctx.integer(0)); - if (type == LST_INTEGER && isInteger(left, 0) && changesNothing(right)) return become(expr, ctx.integer(0)); - break; - case OP_DIV: - if (keepLeft && one(right)) return keep(expr, 0); - break; - case OP_BIT_OR: - case OP_BIT_XOR: - case OP_SHIFT_LEFT: - case OP_SHIFT_RIGHT: - if (isInteger(right, 0)) return keep(expr, 0); - if ((op == OP_BIT_OR || op == OP_BIT_XOR) && isInteger(left, 0)) return keep(expr, 1); - break; - case OP_BIT_AND: - if (isInteger(right, -1)) return keep(expr, 0); - if (isInteger(left, -1)) return keep(expr, 1); - if (isInteger(right, 0) && changesNothing(left)) return become(expr, ctx.integer(0)); - if (isInteger(left, 0) && changesNothing(right)) return become(expr, ctx.integer(0)); - break; - case OP_EQ: - // `x == 0` is `!x`, and one token shorter. - if (left->getIType() == LST_INTEGER && isInteger(right, 0)) return negate(expr, 0); - if (right->getIType() == LST_INTEGER && isInteger(left, 0)) return negate(expr, 1); - break; - case OP_BOOLEAN_AND: - case OP_BOOLEAN_OR: - condition(expr, 0); - condition(expr, 1); - break; - default: - break; - } - return false; - } - - bool visit(LSLUnaryExpression* expr) override - { - visitChildren(expr); - LSLExpression* child = expr->getChildExpr(); - if (!child || expr->getIType() == LST_ERROR) - { - return false; - } - LSLExpression* inner = bare(child); - if (expr->getOperation() == OP_MINUS && inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_MINUS) - { - // -(-x) - LSLExpression* x = static_cast(inner)->getChildExpr(); - if (x && x->getIType() == expr->getIType()) - { - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { render(expr), render(x) }); - inner->setChild(0, nullptr); - putInPlace(expr, x, ctx.allocator); - ++changes; - } - return false; - } - if (expr->getOperation() == OP_BOOLEAN_NOT) - { - condition(expr, 0); - child = expr->getChildExpr(); - inner = bare(child); - if (inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getIType() == LST_INTEGER) - { - // !(a != b) is a == b, whatever they are: both are - // true or false. !(a == b) is a != b only for numbers -- - // a list's != is the difference of the lengths, and - // LSO's for a string is not only one or zero -- and the - // orderings only for integers, which have no NaN. - auto* bin = static_cast(inner); - LSLOperator opposite = OP_NONE; - const auto number = [](LSLExpression* e) { return e->getIType() == LST_INTEGER || e->getIType() == LST_FLOATINGPOINT; }; - const bool ints = bin->getLHS()->getIType() == LST_INTEGER && bin->getRHS()->getIType() == LST_INTEGER; - const bool numbers = number(bin->getLHS()) && number(bin->getRHS()); - switch (bin->getOperation()) - { - case OP_EQ: opposite = numbers ? OP_NEQ : OP_NONE; break; - case OP_NEQ: opposite = OP_EQ; break; - case OP_LESS: opposite = ints ? OP_GEQ : OP_NONE; break; - case OP_GREATER: opposite = ints ? OP_LEQ : OP_NONE; break; - case OP_LEQ: opposite = ints ? OP_GREATER : OP_NONE; break; - case OP_GEQ: opposite = ints ? OP_LESS : OP_NONE; break; - default: break; - } - if (opposite != OP_NONE) - { - const std::string was = render(expr); - bin->setOperation(opposite); - inner->getParent()->takeChild(inner->getParentSlot()); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(bin) }); - putInPlace(expr, bin, ctx.allocator); - ++changes; - } - } - } - return false; - } - - bool visit(LSLTypecastExpression* expr) override - { - visitChildren(expr); - LSLExpression* child = expr->getChildExpr(); - if (child && child->getIType() == expr->getIType() && expr->getIType() != LST_ERROR) - { - report.note(expr->getLoc(), "OptimizerDroppedCast", "dropped the cast in [1]", { render(expr) }); - expr->setChild(0, nullptr); - putInPlace(expr, child, ctx.allocator); - ++changes; - return false; - } - return false; - } - - bool visit(LSLFunctionExpression* expr) override - { - visitChildren(expr); - LSLSymbol* sym = expr->getSymbol(); - if (options.listlength && ALLSLCosts::of(options.target).lengthAsNotEqual && sym && sym->getSubType() == SYM_BUILTIN && - !strcmp(sym->getName(), "llGetListLength") && - expr->getArguments()->getNumChildren() == 1) - { - // llGetListLength(x) is x != [], which is the length. - auto* arg = static_cast(expr->getArguments()->takeChild(0)); - auto* empty = ctx.allocator->newTracked(ctx.allocator->newTracked(nullptr)); - auto* test = ctx.allocator->newTracked(arg, OP_NEQ, static_cast(empty)); - test->setType(TYPE(LST_INTEGER)); - report.note(expr->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { render(expr), render(test) }); - putInPlace(expr, test, ctx.allocator); - ++changes; - } - return false; - } - - bool visit(LSLIfStatement* stmt) override - { - visitChildren(stmt); - condition(stmt, 0); - if (!options.ifelseswap) - { - return false; - } - LSLExpression* cond = stmt->getCheckExpr(); - LSLStatement* yes = stmt->getTrueBranch(); - LSLStatement* no = stmt->getFalseBranch(); - LSLExpression* inner = bare(cond); - if (no && inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) - { - // if (!c) A else B is if (c) B else A. - LSLExpression* c = static_cast(inner)->getChildExpr(); - if (c) - { - inner->setChild(0, nullptr); - stmt->setCheckExpr(c); - swapBranches(stmt); - report.note(stmt->getLoc(), "OptimizerSwappedBranches", "swapped the branches of if ([1]) and dropped the !", { render(c) }); - ++changes; - } - return false; - } - if (no && empty(yes) && cond->getIType() == LST_INTEGER) - { - // if (c) ; else B is if (!c) B: !(c) where c is an - // operation, which the ! would take only the first operand - // of otherwise. - stmt->setCheckExpr(nullptr); - LSLExpression* operand = cond; - if (cond->getNodeSubType() == NODE_BINARY_EXPRESSION) - { - auto* parens = ctx.allocator->newTracked(cond); - parens->setType(cond->getType()); - parens->setLoc(cond->getLoc()); - operand = parens; - } - auto* negated = ctx.allocator->newTracked(operand, OP_BOOLEAN_NOT); - negated->setType(TYPE(LST_INTEGER)); - negated->setLoc(cond->getLoc()); - stmt->setCheckExpr(negated); - stmt->setTrueBranch(nullptr); - stmt->setFalseBranch(nullptr); - stmt->setTrueBranch(no); - report.note(stmt->getLoc(), "OptimizerTurnedEmptyBranch", "turned an empty if branch around"); - ++changes; - } - return false; - } - - bool visit(LSLWhileStatement* stmt) override - { - visitChildren(stmt); - condition(stmt, 0); - return false; - } - - bool visit(LSLDoStatement* stmt) override - { - visitChildren(stmt); - condition(stmt, 1); - return false; - } - - bool visit(LSLForStatement* stmt) override - { - visitChildren(stmt); - condition(stmt, 1); - return false; - } - - private: - static bool empty(LSLStatement* s) - { - if (!s) - { - return true; - } - if (s->getNodeSubType() == NODE_NOP_STATEMENT) - { - return true; - } - return s->getNodeSubType() == NODE_COMPOUND_STATEMENT && !s->hasChildren(); - } - - static void swapBranches(LSLIfStatement* stmt) - { - LSLStatement* yes = stmt->getTrueBranch(); - LSLStatement* no = stmt->getFalseBranch(); - stmt->setTrueBranch(nullptr); - stmt->setFalseBranch(nullptr); - stmt->setTrueBranch(no); - stmt->setFalseBranch(yes); - } - - // A constant below zero, whose sign can move onto the operator. - static bool negative(LSLASTNode* n) - { - LSLConstant* cv = n->getConstantValue(); - if (!cv) - { - return false; - } - if (cv->getNodeSubType() == NODE_INTEGER_CONSTANT) - { - const int v = static_cast(cv)->getValue(); - return v < 0 && v != INT32_MIN; - } - if (cv->getNodeSubType() == NODE_FLOAT_CONSTANT) - { - return static_cast(cv)->getValue() < 0.0; - } - return false; - } - - // The expression becomes one of its operands. - bool keep(LSLBinaryExpression* expr, int slot) - { - const std::string was = render(expr); - auto* kept = static_cast(expr->takeChild(slot)); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(kept) }); - putInPlace(expr, kept, ctx.allocator); - ++changes; - return false; - } - - bool become(LSLBinaryExpression* expr, LSLConstant* cv) - { - fold(expr, cv, "OptimizerSimplified", "simplified"); - return false; - } - - // x + -c as x - c, x - -c as x + c. - bool resign(LSLBinaryExpression* expr, LSLOperator op) - { - const std::string was = render(expr); - LSLConstant* cv = expr->getRHS()->getConstantValue(); - LSLConstant* pos = cv->getNodeSubType() == NODE_INTEGER_CONSTANT - ? ctx.integer(-static_cast(cv)->getValue()) - : ctx.allocator->newTracked(-static_cast(cv)->getValue()); - expr->setOperation(op); - expr->setRHS(constant(pos, expr->getRHS())); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(expr) }); - ++changes; - return false; - } - - // x == 0 as !x. - bool negate(LSLBinaryExpression* expr, int slot) - { - const std::string was = render(expr); - auto* x = static_cast(expr->takeChild(slot)); - LSLExpression* operand = x; - if (x->getNodeSubType() == NODE_BINARY_EXPRESSION) - { - operand = ctx.allocator->newTracked(x); - operand->setType(x->getType()); - } - auto* negated = ctx.allocator->newTracked(operand, OP_BOOLEAN_NOT); - negated->setType(TYPE(LST_INTEGER)); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(negated) }); - putInPlace(expr, negated, ctx.allocator); - ++changes; - return false; - } - - // A child that is only ever true or false: `x != 0` is `x`, and - // `!!x` is `x`. - void condition(LSLASTNode* parent, int slot) - { - LSLASTNode* child = parent->getChild(slot); - if (!child || child->getNodeType() != NODE_EXPRESSION) - { - return; - } - auto* expr = static_cast(child); - LSLExpression* inner = bare(expr); - if (inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getOperation() == OP_NEQ) - { - auto* bin = static_cast(inner); - int keepSlot = -1; - if (bin->getLHS()->getIType() == LST_INTEGER && isInteger(bin->getRHS(), 0)) keepSlot = 0; - else if (bin->getRHS()->getIType() == LST_INTEGER && isInteger(bin->getLHS(), 0)) keepSlot = 1; - if (keepSlot >= 0) - { - const std::string was = render(expr); - auto* kept = static_cast(bin->takeChild(keepSlot)); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(kept) }); - LSLASTNode::replaceNode(expr, kept); - ++changes; - return; - } - } - if (inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) - { - LSLExpression* once = bare(static_cast(inner)->getChildExpr()); - if (once && once->getNodeSubType() == NODE_UNARY_EXPRESSION && once->getOperation() == OP_BOOLEAN_NOT) - { - LSLExpression* x = static_cast(once)->getChildExpr(); - if (x && x->getIType() == LST_INTEGER) - { - const std::string was = render(expr); - once->setChild(0, nullptr); - report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(x) }); - LSLASTNode::replaceNode(expr, x); - ++changes; - } - } - } - } - }; - - // Which of Mono's list helpers a script calls, each by what Tailslide's - // Mono compiler calls one for: a literal, an empty list, a cast to a - // list and one of a list to a string, a list's literal among the - // globals, something added to a list -- a helper for each type added - // -- and something added before one. - // The assembly references each once, which is what a list's shapes can - // cost that they do not save at each place: a helper nothing in the - // script called before. - class ListHelpers : public ASTVisitor - { - public: - boost::unordered_flat_set used; - // Lists dumped with nothing between their elements, each a cast to - // a string it may be. - S32 dumps = 0; - - bool castsToString() const { return used.contains(TO_STRING); } - - bool visit(LSLFunctionExpression* expr) override - { - LSLSymbol* sym = expr->getSymbol(); - LSLASTNodeList* args = expr->getArguments(); - if (sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llDumpList2String") && args && args->getNumChildren() == 2) - { - LSLConstant* cv = args->getChild(1)->getConstantValue(); - dumps += cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue() ? 1 : 0; - } - return true; - } - - // How many of `after`'s helpers are not among these. - S32 freshIn(const ListHelpers& after) const - { - return static_cast(std::count_if(after.used.begin(), after.used.end(), [this](U32 helper) { return !used.contains(helper); })); - } - - bool visit(LSLGlobalVariable* global) override - { - mGlobal = true; - visitChildren(global); - mGlobal = false; - return false; - } - bool visit(LSLListExpression* expr) override - { - used.insert(mGlobal ? GLOBAL_LITERAL : expr->hasChildren() ? LITERAL : EMPTY); - return true; - } - bool visit(LSLTypecastExpression* expr) override - { - if (expr->getIType() == LST_LIST) - { - used.insert(CAST); - } - else if (expr->getIType() == LST_STRING && expr->getChildExpr() && expr->getChildExpr()->getIType() == LST_LIST) - { - used.insert(TO_STRING); - } - return true; - } - bool visit(LSLBinaryExpression* expr) override - { - const LSLOperator op = expr->getOperation(); - LSLExpression* left = expr->getLHS(); - LSLExpression* right = expr->getRHS(); - if ((op == OP_PLUS || op == OP_ADD_ASSIGN) && expr->getIType() == LST_LIST && left && right) - { - used.insert(left->getIType() == LST_LIST ? APPEND | static_cast(right->getIType()) : PREPEND); - } - return true; - } - - private: - enum : U32 - { - LITERAL = 1u << 8, - EMPTY = 2u << 8, - GLOBAL_LITERAL = 3u << 8, - CAST = 4u << 8, - PREPEND = 5u << 8, - APPEND = 6u << 8, - TO_STRING = 7u << 8 - }; - bool mGlobal = false; - }; - - // Rewrites made for size alone, once the rounds are done: each hides a - // value the folder could have used -- a list's literal made a sum is - // no longer a constant -- so they wait until nothing more will fold. - class Shapes : public ASTVisitor, public Pass - { - public: - // Integers' and conditions' shapes, and increments', which the - // code makes of plain instructions; and lists', which Mono makes - // of helpers its assembly references once each, so that what they - // save at each place a helper new to the script can cost more than - // -- weighed, where it can (once()). - enum class Stage : U8 - { - Values, - Lists - }; - Shapes(Ctx& c, Report& r, const ALLSLOptimizer::Options& o, Stage stage) : Pass(c, r, o), mCosts(ALLSLCosts::of(o.target)), mStage(stage) {} - - // An increment whose value nothing reads, before rather than after. - bool visit(LSLExpressionStatement* stmt) override - { - visitChildren(stmt); - if (mStage == Stage::Values) - { - pre(stmt->getExpr()); - } - return false; - } - - // Where only whether it is true counts; and an if whose condition - // is a == with an else, turned around over a ^. - bool visit(LSLIfStatement* stmt) override - { - visitChildren(stmt); - if (mStage != Stage::Values) - { - return false; - } - truth(stmt, 0); - LSLExpression* cond = bare(stmt->getCheckExpr()); - LSLStatement* yes = stmt->getTrueBranch(); - LSLStatement* no = stmt->getFalseBranch(); - if (mCosts.xorForNotEqual && no && yes && cond && cond->getNodeSubType() == NODE_BINARY_EXPRESSION && cond->getOperation() == OP_EQ && - integerOperands(static_cast(cond))) - { - const std::string before = report.wanted() ? render(cond) : std::string(); - cond->setOperation(OP_BIT_XOR); - stmt->setTrueBranch(nullptr); - stmt->setFalseBranch(nullptr); - stmt->setTrueBranch(no); - stmt->setFalseBranch(yes); - if (report.wanted()) - { - report.note(stmt->getLoc(), "OptimizerSwappedOnXor", "wrote if ([1]) as if ([2]), its branches swapped", { before, render(cond) }); - } - ++changes; - } - return false; - } - - // The ! of a comparison as the comparison the other way. - bool visit(LSLUnaryExpression* expr) override - { - visitChildren(expr); - if (mStage == Stage::Values && !inGlobal(expr)) - { - notComparison(expr); - } - return false; - } - bool visit(LSLWhileStatement* stmt) override - { - visitChildren(stmt); - if (mStage == Stage::Values) - { - truth(stmt, 0); - } - return false; - } - bool visit(LSLDoStatement* stmt) override - { - visitChildren(stmt); - if (mStage == Stage::Values) - { - truth(stmt, 1); - } - return false; - } - bool visit(LSLForStatement* stmt) override - { - visitChildren(stmt); - if (mStage != Stage::Values) - { - return false; - } - truth(stmt, 1); - if (LSLASTNode* steps = stmt->getIncrExprs()) - { - for (LSLASTNode* step = steps->getChild(0); step; step = step->getNext()) - { - pre(step); - } - } - return false; - } - - bool visit(LSLFunctionExpression* expr) override - { - visitChildren(expr); - if (!inGlobal(expr)) - { - if (mStage == Stage::Values) - { - nullKeys(expr); - } - else - { - // A list cast to a string, a list's helper on Mono. - libraryCast(expr); - } - } - return false; - } - - bool visit(LSLBinaryExpression* expr) override - { - if (inGlobal(expr)) - { - return false; - } - // A list's elements bare, seen as the author wrote them, before - // what is left of a literal is made a sum below. - if (mStage == Stage::Lists) - { - if (LSLExpression* made = elements(expr)) - { - made->visit(this); - return false; - } - visitChildren(expr); - return false; - } - visitChildren(expr); - integers(expr); - return false; - } - - bool visit(LSLListExpression* expr) override - { - visitChildren(expr); - if (mStage != Stage::Lists || !options.listadd || !mCosts.listAsSum || expr->getNumChildren() == 0) - { - return false; - } - // Not in a global's initializer, which must stay simple. - if (inGlobal(expr)) - { - return false; - } - // A sum takes its right side first, so its elements are taken - // last to first, where a literal's are taken first to last: - // where one of them changes something, and another is anything - // but a constant, that one could see the change the other way - // round. - size_t changing = 0; - size_t varying = 0; - for (LSLASTNode* child : *expr) - { - if (child->getIType() == LST_LIST || child->getIType() == LST_ERROR) - { - return false; - } - // Read at another time than it was: a read of the clock - // counts as a change here. - changing += sideEffectFree(child) ? 0 : 1; - varying += child->getConstantValue() ? 0 : 1; - } - if (changing > 0 && varying > 1) - { - return false; - } - // [a, b, c] as (list)a + b + c. - const std::string was = render(expr); - std::vector terms; - terms.reserve(expr->getNumChildren()); - while (expr->hasChildren()) - { - // takeChild leaves a null in the slot, dropped each time. - terms.push_back(static_cast(expr->takeChild(0))); - expr->removeChild(expr->getChild(0)); - } - LSLExpression* sum = nullptr; - { - const Uncounted uncounted(*ctx.context); - sum = uncounted.made(ctx.allocator->newTracked(TYPE(LST_LIST), terms.front())); - sum->setLoc(terms.front()->getLoc()); - for (size_t i = 1; i < terms.size(); ++i) - { - sum = uncounted.made(ctx.allocator->newTracked(sum, OP_PLUS, uncounted.made(bracketed(terms[i])))); - sum->setType(TYPE(LST_LIST)); - sum->setLoc(terms[i]->getLoc()); - } - } - report.note(expr->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { was, render(sum) }); - putInPlace(expr, sum, ctx.allocator); - ++changes; - return false; - } - - public: - // What list helpers the script called before a list's shapes, where - // they are held once for the script (Mono): a list dumped with - // nothing between is cast to a string only where the script casts - // one already, or the dumps are enough to pay for the helper. - void setHelpers(const ListHelpers* had) { mHad = had; } - - private: - const ALLSLCosts& mCosts; - const Stage mStage; - const ListHelpers* mHad = nullptr; - - static bool inGlobal(LSLASTNode* node) - { - for (LSLASTNode* up = node->getParent(); up; up = up->getParent()) - { - if (up->getNodeType() == NODE_GLOBAL_VARIABLE) - { - return true; - } - } - return false; - } - - void wrote(LSLASTNode* was, LSLExpression* now, const std::string& before) - { - if (report.wanted()) - { - report.note(was->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { before, render(now) }); - } - ++changes; - } - - // `op` over `x`, which is bracketed where what it is could read - // otherwise after the operator: a sum, or -- after a minus -- a - // minus of its own, which would read as a decrement. - LSLExpression* unary(const Uncounted& uncounted, LSLOperator op, LSLExpression* x) - { - bool tight = false; - switch (x->getNodeSubType()) - { - case NODE_LVALUE_EXPRESSION: - case NODE_FUNCTION_EXPRESSION: - case NODE_PARENTHESIS_EXPRESSION: - case NODE_TYPECAST_EXPRESSION: - tight = true; - break; - case NODE_UNARY_EXPRESSION: - tight = op != OP_MINUS || (x->getOperation() != OP_MINUS && x->getOperation() != OP_PRE_DECR); - break; - default: - break; - } - if (!tight) - { - auto* parens = uncounted.made(ctx.allocator->newTracked(x)); - parens->setType(x->getType()); - parens->setLoc(x->getLoc()); - x = parens; - } - auto* made = uncounted.made(ctx.allocator->newTracked(x, op)); - made->setType(TYPE(LST_INTEGER)); - made->setLoc(x->getLoc()); - return made; - } - - // `expr`, a binary expression, made `ops` over its operand in - // `slot`, the last of them outermost: `-~x` is { OP_BIT_NOT, - // OP_MINUS }. - void over(LSLBinaryExpression* expr, int slot, std::initializer_list ops) - { - const std::string before = report.wanted() ? render(expr) : std::string(); - auto* x = static_cast(expr->takeChild(slot)); - LSLExpression* made = x; - { - const Uncounted uncounted(*ctx.context); - for (LSLOperator op : ops) - { - made = unary(uncounted, op, made); - } - } - made->setLoc(expr->getLoc()); - // Unary binds tighter than anything around it. - LSLASTNode::replaceNode(expr, made); - wrote(expr, made, before); - } - - // Whether a value is never below -1: a find's, or a local's that - // is set to one where it is declared and never after. - static bool atLeastMinusOne(LSLExpression* x) - { - x = bare(x); - if (x && x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember()) - { - LSLSymbol* sym = x->getSymbol(); - LSLASTNode* decl = sym && sym->getSubType() == SYM_LOCAL && sym->getAssignments() == 0 ? sym->getVarDecl() : nullptr; - LSLASTNode* init = decl ? decl->getChild(1) : nullptr; - return init && init->getNodeType() == NODE_EXPRESSION && atLeastMinusOne(static_cast(init)); - } - if (!x || x->getNodeSubType() != NODE_FUNCTION_EXPRESSION) - { - return false; - } - LSLSymbol* sym = static_cast(x)->getSymbol(); - return sym && sym->getSubType() == SYM_BUILTIN && ALLSLTraits::atLeastMinusOne(sym->getName()); - } - - static bool integerOperands(LSLBinaryExpression* expr) - { - return expr->getLHS() && expr->getRHS() && expr->getLHS()->getIType() == LST_INTEGER && expr->getRHS()->getIType() == LST_INTEGER; - } - - // !(a < b) as a >= b, and each comparison's so: of integers, where - // the order of any two is known; and of anything but lists for == - // and !=, which are each other's opposite whatever the values -- - // a list's != is how much longer one is. - void notComparison(LSLUnaryExpression* expr) - { - LSLExpression* inner = bare(expr->getChildExpr()); - if (!mCosts.comparisonForNot || expr->getOperation() != OP_BOOLEAN_NOT || !inner || inner->getNodeSubType() != NODE_BINARY_EXPRESSION) - { - return; - } - auto* cmp = static_cast(inner); - LSLOperator to = OP_NONE; - const LSLOperator op = cmp->getOperation(); - switch (op) - { - case OP_LESS: to = OP_GEQ; break; - case OP_GEQ: to = OP_LESS; break; - case OP_GREATER: to = OP_LEQ; break; - case OP_LEQ: to = OP_GREATER; break; - case OP_EQ: to = OP_NEQ; break; - case OP_NEQ: to = OP_EQ; break; - default: return; - } - LSLExpression* left = cmp->getLHS(); - LSLExpression* right = cmp->getRHS(); - if (!left || !right) - { - return; - } - const bool equality = op == OP_EQ || op == OP_NEQ; - const bool fits = integerOperands(cmp) || (equality && left->getIType() != LST_LIST && right->getIType() != LST_LIST && - left->getIType() != LST_ERROR && right->getIType() != LST_ERROR); - if (!fits) - { - return; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - cmp->getParent()->takeChild(cmp->getParentSlot()); - cmp->setOperation(to); - putInPlace(expr, cmp, ctx.allocator); - wrote(expr, cmp, before); - } - - // a >= 5 as a > 4, and a <= 5 as a < 6, either way round, where the - // constant one either side of it is an integer too. - void strict(LSLBinaryExpression* expr) - { - const LSLOperator op = expr->getOperation(); - if (!mCosts.strictForInclusive || (op != OP_GEQ && op != OP_LEQ) || !integerOperands(expr)) - { - return; - } - for (int slot = 0; slot < 2; ++slot) - { - LSLASTNode* side = expr->getChild(slot); - LSLConstant* cv = side->getConstantValue(); - if (!cv || cv->getNodeSubType() != NODE_INTEGER_CONSTANT || expr->getChild(1 - slot)->getConstantValue()) - { - continue; - } - // Whether the constant is on the greater side: a >= C, C <= a. - const bool below = (op == OP_GEQ) == (slot == 1); - const int32_t c = static_cast(cv)->getValue(); - if ((below && c == INT32_MIN) || (!below && c == INT32_MAX)) - { - return; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - LSLASTNode::replaceNode(side, constant(ctx.integer(below ? c - 1 : c + 1), side)); - expr->setOperation(op == OP_GEQ ? OP_GREATER : OP_LESS); - wrote(expr, expr, before); - return; - } - } - - // What an integer comparison or sum comes to in fewer bytes, as a - // value: x == -1 as !~x, x < 0 as !~x of a find, x + 1 as -~x, - // x - 1 as ~-x, and two either way. - void integers(LSLBinaryExpression* expr) - { - strict(expr); - LSLExpression* left = expr->getLHS(); - LSLExpression* right = expr->getRHS(); - if (!left || !right || expr->getIType() != LST_INTEGER || left->getIType() != LST_INTEGER || right->getIType() != LST_INTEGER) - { - return; - } - switch (expr->getOperation()) - { - case OP_EQ: - if (mCosts.notComplementForMinusOne && (isInteger(right, -1) || isInteger(left, -1))) - { - over(expr, isInteger(right, -1) ? 0 : 1, { OP_BIT_NOT, OP_BOOLEAN_NOT }); - } - return; - case OP_LESS: - if (mCosts.notComplementForMinusOne && isInteger(right, 0) && atLeastMinusOne(left)) - { - over(expr, 0, { OP_BIT_NOT, OP_BOOLEAN_NOT }); - } - return; - case OP_GREATER: - if (mCosts.notComplementForMinusOne && isInteger(left, 0) && atLeastMinusOne(right)) - { - over(expr, 1, { OP_BIT_NOT, OP_BOOLEAN_NOT }); - } - return; - case OP_PLUS: - for (int slot = 0; slot < 2 && mCosts.negateComplementForIncrement; ++slot) - { - LSLExpression* other = slot == 0 ? right : left; - if (isInteger(other, 1)) - { - over(expr, slot, { OP_BIT_NOT, OP_MINUS }); - return; - } - if (isInteger(other, 2)) - { - over(expr, slot, { OP_BIT_NOT, OP_MINUS, OP_BIT_NOT, OP_MINUS }); - return; - } - } - return; - case OP_MINUS: - if (mCosts.complementNegateForDecrement && isInteger(right, 1)) - { - over(expr, 0, { OP_MINUS, OP_BIT_NOT }); - } - else if (mCosts.complementNegateForDecrement && isInteger(right, 2)) - { - over(expr, 0, { OP_MINUS, OP_BIT_NOT, OP_MINUS, OP_BIT_NOT }); - } - return; - default: - return; - } - } - - // What only counts as true or false -- a condition, an operand of - // !, && or || in one -- in fewer bytes: x != -1 as ~x, and x > -1 - // and x >= 0 as ~x of a find. - void truth(LSLASTNode* parent, int slot) - { - LSLASTNode* child = parent->getChild(slot); - if (!child || child->getNodeType() != NODE_EXPRESSION) - { - return; - } - LSLExpression* inner = bare(static_cast(child)); - if (!inner || inGlobal(inner)) - { - return; - } - if (inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) - { - truth(inner, 0); - return; - } - // A string's length is nothing where the string is. - if (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && mCosts.emptyForLength) - { - auto* call = static_cast(inner); - LSLSymbol* sym = call->getSymbol(); - LSLASTNodeList* args = call->getArguments(); - if (sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llStringLength") && args && args->getNumChildren() == 1) - { - const std::string before = report.wanted() ? render(call) : std::string(); - auto* text = static_cast(args->takeChild(0)); - auto* made = ctx.allocator->newTracked(text, OP_NEQ, constant(ctx.string(std::string()), call)); - made->setType(TYPE(LST_INTEGER)); - made->setLoc(call->getLoc()); - putInPlace(call, made, ctx.allocator); - wrote(call, made, before); - } - return; - } - if (inner->getNodeSubType() != NODE_BINARY_EXPRESSION) - { - return; - } - auto* expr = static_cast(inner); - LSLExpression* left = expr->getLHS(); - LSLExpression* right = expr->getRHS(); - if (expr->getOperation() == OP_BOOLEAN_AND || expr->getOperation() == OP_BOOLEAN_OR) - { - truth(expr, 0); - truth(expr, 1); - // Either true is the two bits or'd true; LSL runs both - // sides of || either way. - if (expr->getOperation() == OP_BOOLEAN_OR && mCosts.bitOrForOr && integerOperands(expr)) - { - const std::string before = report.wanted() ? render(expr) : std::string(); - expr->setOperation(OP_BIT_OR); - wrote(expr, expr, before); - } - return; - } - if (!left || !right || left->getIType() != LST_INTEGER || right->getIType() != LST_INTEGER) - { - return; - } - if (expr->getOperation() == OP_NEQ && !isInteger(right, -1) && !isInteger(left, -1)) - { - // Different is the bits differing. - if (mCosts.xorForNotEqual) - { - const std::string before = report.wanted() ? render(expr) : std::string(); - expr->setOperation(OP_BIT_XOR); - wrote(expr, expr, before); - } - return; - } - if (!mCosts.complementForNotMinusOne) - { - return; - } - switch (expr->getOperation()) - { - case OP_NEQ: - if (isInteger(right, -1) || isInteger(left, -1)) - { - over(expr, isInteger(right, -1) ? 0 : 1, { OP_BIT_NOT }); - } - return; - case OP_GREATER: - if (isInteger(right, -1) && atLeastMinusOne(left)) - { - over(expr, 0, { OP_BIT_NOT }); - } - return; - case OP_GEQ: - if (isInteger(right, 0) && atLeastMinusOne(left)) - { - over(expr, 0, { OP_BIT_NOT }); - } - return; - case OP_LESS: - if (isInteger(left, -1) && atLeastMinusOne(right)) - { - over(expr, 1, { OP_BIT_NOT }); - } - return; - case OP_LEQ: - if (isInteger(left, 0) && atLeastMinusOne(right)) - { - over(expr, 1, { OP_BIT_NOT }); - } - return; - default: - return; - } - } - - // A library call that a cast to a string says the same as: - // llDumpList2String(l, "") as (string)l, and the one detail - // llGetObjectDetails gives as a string -- or "", as both say where - // there is no such object. - void libraryCast(LSLFunctionExpression* expr) - { - LSLSymbol* sym = expr->getSymbol(); - LSLASTNodeList* args = expr->getArguments(); - if (!sym || sym->getSubType() != SYM_BUILTIN || !args || args->getNumChildren() != 2) - { - return; - } - auto* first = static_cast(args->getChild(0)); - LSLASTNode* second = args->getChild(1); - bool cast = false; - if (mCosts.castForDump && !strcmp(sym->getName(), "llDumpList2String")) - { - LSLConstant* cv = second->getConstantValue(); - const bool pays = !mHad || mHad->castsToString() || mHad->dumps * mCosts.listShapeLeast > mCosts.listHelperMost; - cast = pays && cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue(); - } - else if (mCosts.castForDetail && !strcmp(sym->getName(), "llList2String") && isInteger(second, 0)) - { - LSLExpression* details = bare(first); - LSLSymbol* inner = details && details->getNodeSubType() == NODE_FUNCTION_EXPRESSION ? details->getSymbol() : nullptr; - LSLASTNodeList* asked = inner ? static_cast(details)->getArguments() : nullptr; - LSLASTNode* which = asked && asked->getNumChildren() == 2 ? asked->getChild(1) : nullptr; - LSLExpression* named = which && which->getNodeType() == NODE_EXPRESSION ? bare(static_cast(which)) : nullptr; - // One detail asked for: a literal of one, or that literal as - // the cast listadd makes of it. - const bool one = named && ((named->getNodeSubType() == NODE_LIST_EXPRESSION && named->getNumChildren() == 1) || - (named->getNodeSubType() == NODE_TYPECAST_EXPRESSION && named->getIType() == LST_LIST && - static_cast(named)->getChildExpr() && - static_cast(named)->getChildExpr()->getIType() != LST_LIST)); - cast = inner && inner->getSubType() == SYM_BUILTIN && !strcmp(inner->getName(), "llGetObjectDetails") && one; - } - if (!cast) - { - return; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - args->takeChild(0); - auto* made = ctx.allocator->newTracked(TYPE(LST_STRING), first); - made->setType(TYPE(LST_STRING)); - made->setLoc(expr->getLoc()); - putInPlace(expr, made, ctx.allocator); - wrote(expr, made, before); - } - - // Whether a constant is no key a library function would find - // anything by: NULL_KEY, or anything that is not a key's form. - static bool nullKey(LSLConstant* cv) - { - if (!cv || (cv->getIType() != LST_STRING && cv->getIType() != LST_KEY)) - { - return false; - } - const std::string_view text = cv->getIType() == LST_STRING ? static_cast(cv)->getValue() - : static_cast(cv)->getValue(); - if (text.size() != 36) - { - return true; - } - bool zero = true; - for (size_t i = 0; i < text.size(); ++i) - { - const bool dash = i == 8 || i == 13 || i == 18 || i == 23; - if (dash ? text[i] != '-' : !isxdigit(static_cast(text[i]))) - { - return true; - } - zero = zero && (dash || text[i] == '0'); - } - return zero; - } - - // A library function given NULL_KEY, or no key at all, for a key - // it looks something up by, given "" instead, which it takes the - // same way; not the two that pass a key on as it came. - void nullKeys(LSLFunctionExpression* expr) - { - LSLSymbol* sym = expr->getSymbol(); - if (!mCosts.emptyForNullKey || !sym || sym->getSubType() != SYM_BUILTIN || !sym->getFunctionDecl() || !expr->getArguments() || - !strcmp(sym->getName(), "llMessageLinked") || !strcmp(sym->getName(), "llRemoteDataReply")) - { - return; - } - std::vector nulls; - LSLASTNode* param = sym->getFunctionDecl()->getChild(0); - for (LSLASTNode* arg = expr->getArguments()->getChild(0); arg && param; arg = arg->getNext(), param = param->getNext()) - { - LSLConstant* cv = arg->getConstantValue(); - const bool empty = cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue(); - if (param->getIType() == LST_KEY && !empty && nullKey(cv)) - { - nulls.push_back(arg); - } - } - for (LSLASTNode* arg : nulls) - { - const std::string before = report.wanted() ? render(arg) : std::string(); - auto* made = constant(ctx.string(std::string()), arg); - LSLASTNode::replaceNode(arg, made); - wrote(arg, made, before); - } - } - - // x++ and x-- whose value nothing reads, as ++x and --x. - void pre(LSLASTNode* node) - { - if (!mCosts.preForPost || !node || node->getNodeType() != NODE_EXPRESSION || node->getNodeSubType() != NODE_UNARY_EXPRESSION) - { - return; - } - auto* expr = static_cast(node); - const LSLOperator op = expr->getOperation(); - if (op != OP_POST_INCR && op != OP_POST_DECR) - { - return; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - expr->setOperation(op == OP_POST_INCR ? OP_PRE_INCR : OP_PRE_DECR); - wrote(expr, expr, before); - } - - // A list's element added bare: l + [a, b] as l + a + b, [a] + l as - // a + l, l + (list)x as l + x, and l += [a] as l += a. What was - // made, where anything was. - LSLExpression* elements(LSLBinaryExpression* expr) - { - if (!mCosts.elementForList || expr->getIType() != LST_LIST) - { - return nullptr; - } - LSLExpression* left = expr->getLHS(); - LSLExpression* right = expr->getRHS(); - const LSLOperator op = expr->getOperation(); - if (!left || !right || (op != OP_PLUS && op != OP_ADD_ASSIGN) || left->getIType() != LST_LIST) - { - return nullptr; - } - if (LSLExpression* made = appended(expr, left, right, op)) - { - return made; - } - // [a] + l as a + l. - LSLExpression* literal = bare(left); - if (op == OP_PLUS && right->getIType() == LST_LIST && literal->getNodeSubType() == NODE_LIST_EXPRESSION && - literal->getNumChildren() == 1 && literal->getChild(0)->getIType() != LST_LIST) - { - const std::string before = report.wanted() ? render(expr) : std::string(); - LSLASTNode* a = literal->takeChild(0); - literal->removeChild(literal->getChild(0)); - LSLASTNode::replaceNode(left, a); - wrote(expr, expr, before); - return expr; - } - return nullptr; - } - - // A list with elements added to it: those elements bare. - LSLExpression* appended(LSLBinaryExpression* expr, LSLExpression* left, LSLExpression* right, LSLOperator op) - { - LSLExpression* added = bare(right); - // l + (list)x as l + x. - if (op == OP_PLUS && added->getNodeSubType() == NODE_TYPECAST_EXPRESSION && added->getIType() == LST_LIST) - { - LSLExpression* x = static_cast(added)->getChildExpr(); - if (!x || x->getIType() == LST_LIST) - { - return nullptr; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - added->takeChild(0); - LSLASTNode::replaceNode(right, x); - wrote(expr, expr, before); - return expr; - } - if (added->getNodeSubType() != NODE_LIST_EXPRESSION || added->getNumChildren() == 0) - { - return nullptr; - } - const size_t count = added->getNumChildren(); - if ((op == OP_ADD_ASSIGN && count != 1) || (mCosts.elementsForListMost > 0 && count > size_t(mCosts.elementsForListMost))) - { - return nullptr; - } - // Each element's order as a sum's -- last first -- against a - // literal's, which is known to nobody: as for listadd. - size_t changing = 0; - size_t varying = 0; - for (LSLASTNode* child : *added) - { - if (child->getIType() == LST_LIST || child->getIType() == LST_ERROR) - { - return nullptr; - } - changing += sideEffectFree(child) ? 0 : 1; - varying += child->getConstantValue() ? 0 : 1; - } - if (changing > 0 && varying > 1) - { - return nullptr; - } - const std::string before = report.wanted() ? render(expr) : std::string(); - std::vector terms; - while (added->hasChildren()) - { - terms.push_back(static_cast(added->takeChild(0))); - added->removeChild(added->getChild(0)); - } - if (op == OP_ADD_ASSIGN) - { - LSLASTNode::replaceNode(right, terms.front()); - wrote(expr, expr, before); - return expr; - } - // l + a + b, each on the right of its own +. - auto* l = static_cast(expr->takeChild(0)); - LSLExpression* sum = l; - { - const Uncounted uncounted(*ctx.context); - for (LSLExpression* term : terms) - { - sum = uncounted.made(ctx.allocator->newTracked(sum, OP_PLUS, uncounted.made(bracketed(term)))); - sum->setType(TYPE(LST_LIST)); - sum->setLoc(term->getLoc()); - } - } - sum->setLoc(expr->getLoc()); - putInPlace(expr, sum, ctx.allocator); - wrote(expr, sum, before); - return sum; - } - - // An element of a list as a sum has it, after the first: on the - // right of a +, which binds tighter than every operator but a - // product and what is unary, and so bracketed where it is any - // other. (The first goes under the cast, which the printer - // brackets for itself.) - LSLExpression* bracketed(LSLExpression* element) - { - bool bare = false; - switch (element->getNodeSubType()) - { - case NODE_CONSTANT_EXPRESSION: - case NODE_LVALUE_EXPRESSION: - case NODE_PARENTHESIS_EXPRESSION: - case NODE_FUNCTION_EXPRESSION: - case NODE_PRINT_EXPRESSION: - case NODE_VECTOR_EXPRESSION: - case NODE_QUATERNION_EXPRESSION: - case NODE_LIST_EXPRESSION: - bare = true; - break; - case NODE_UNARY_EXPRESSION: - case NODE_TYPECAST_EXPRESSION: - bare = true; - break; - case NODE_BINARY_EXPRESSION: - bare = element->getOperation() == OP_MUL || element->getOperation() == OP_DIV || element->getOperation() == OP_MOD; - break; - default: - break; - } - if (bare) - { - return element; - } - auto* parens = ctx.allocator->newTracked(element); - parens->setType(element->getType()); - parens->setLoc(element->getLoc()); - return parens; - } - }; - - // What can never run, and what is never used. - class DeadCode : public ASTVisitor, public Pass - { - public: - using Pass::Pass; - - int run(LSLScript* script) - { - for (LSLASTNode* global : *script->getGlobals()) - { - if (global->getNodeType() == NODE_GLOBAL_FUNCTION && global->getSymbol()) - { - mFunctions[global->getSymbol()] = static_cast(global); - } - } - script->visit(this); - globals(script); - states(script); - unreached(script); - writeOnly(script); - emptyHandlers(script); - return changes; - } - - bool visit(LSLCompoundStatement* block) override - { - visitChildren(block); - bool dead = false; - std::vector going; - // Removed for a reason, said; or silently, with nothing. - const auto go = [&](LSLASTNode* stmt, const char* key, const char* why) { - if (key) - { - // What went, as far as its first line and a few words - // of it: a note is read beside the line, not in place - // of the code. - std::string said = render(stmt); - const size_t cut = std::min(said.find('\n'), size_t(60)); - if (cut < said.size()) - { - said = said.substr(0, cut) + "\xE2\x80\xA6"; - } - report.note(stmt->getLoc(), key, std::string("removed [1]") + why, { said }); - } - going.push_back(stmt); - }; - for (LSLASTNode* stmt : *block) - { - if (stmt->getNodeSubType() == NODE_LABEL) - { - dead = false; - if (unusedLabel(stmt)) - { - go(stmt, nullptr, ""); - } - continue; - } - if (dead) - { - // A label in it that a jump goes to makes it reachable, - // and what follows it: a jump in SL goes to the last - // label of its name in the function, whatever block - // that is in. - if (!holdsLiveLabel(stmt)) - { - go(stmt, "OptimizerRemovedUnreachable", ", which can never run"); - continue; - } - dead = false; - } - switch (stmt->getNodeSubType()) - { - case NODE_JUMP_STATEMENT: - if (jumpsToNext(stmt)) - { - go(stmt, "OptimizerRemovedJump", ", which goes where running on goes"); - break; - } - dead = true; - break; - case NODE_RETURN_STATEMENT: - case NODE_STATE_STATEMENT: - dead = true; - break; - case NODE_IF_STATEMENT: - case NODE_WHILE_STATEMENT: - case NODE_DO_STATEMENT: - case NODE_FOR_STATEMENT: - // An if none of whose ways goes on, a loop that - // never ends -- but for a jump out, whose label - // keeps what follows it (above). - dead = stops(stmt); - break; - case NODE_NOP_STATEMENT: - go(stmt, nullptr, ""); - break; - case NODE_COMPOUND_STATEMENT: - if (!stmt->hasChildren()) - { - go(stmt, nullptr, ""); - } - break; - case NODE_EXPRESSION_STATEMENT: - { - LSLExpression* expr = static_cast(stmt)->getExpr(); - if (changesNothing(expr) || callsNothing(expr)) - { - go(stmt, "OptimizerRemovedNoEffect", ", which does nothing"); - } - dead = stops(stmt); - break; - } - case NODE_DECLARATION: - if (unusedLocal(static_cast(stmt))) - { - go(stmt, nullptr, ""); - } - break; - default: - break; - } - } - for (LSLASTNode* stmt : going) - { - block->removeChild(stmt); - ++changes; - } - flattenBlocks(block); - sinkDeclarations(block); - substituteLocals(block); - return false; - } - - // How many identifiers of each name a function or event has, - // counted once a run: what the pass takes away only makes a count - // more than there are, never fewer. - const boost::unordered_flat_map& namesIn(LSLASTNode* node) - { - LSLASTNode* callable = node; - while (callable && callable->getNodeType() != NODE_GLOBAL_FUNCTION && callable->getNodeType() != NODE_EVENT_HANDLER) - { - callable = callable->getParent(); - } - auto& counted = mNames[callable]; - if (counted.empty() && callable) - { - std::vector stack{ callable }; - while (!stack.empty()) - { - LSLASTNode* n = stack.back(); - stack.pop_back(); - if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) - { - ++counted[static_cast(n)->getName()]; - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - } - return counted; - } - - // A block standing as a statement of a block, its statements put - // in its place: where it declares nothing and holds no label, or - // only names no identifier outside it has -- LSL has no shadowing, - // and a name it declared would otherwise be one a later declaration - // shadows, or a global a later read meant; and two labels of a name - // may not stand in one block. - void flattenBlocks(LSLCompoundStatement* block) - { - const auto flat = [&](LSLASTNode* inner) { - if (inner->getNodeSubType() != NODE_COMPOUND_STATEMENT) - { - return false; - } - boost::unordered_flat_map own; - std::vector declared; - std::vector stack{ inner }; - while (!stack.empty()) - { - LSLASTNode* n = stack.back(); - stack.pop_back(); - if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) - { - ++own[static_cast(n)->getName()]; - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - // What it declares, and the labels it holds: two labels of a - // name may not stand in one block. - for (LSLASTNode* stmt = inner->getChild(0); stmt; stmt = stmt->getNext()) - { - if (stmt->getNodeSubType() == NODE_DECLARATION) - { - declared.push_back(static_cast(stmt)->getIdentifier()->getName()); - } - else if (stmt->getNodeSubType() == NODE_LABEL) - { - declared.push_back(static_cast(stmt)->getIdentifier()->getName()); - } - } - const auto& all = namesIn(block); - for (const char* name : declared) - { - const auto total = all.find(std::string_view(name)); - if (!name || total == all.end() || total->second != own[std::string_view(name)]) - { - return false; - } - } - return true; - }; - std::vector statements; - bool any = false; - for (LSLASTNode* stmt : *block) - { - statements.push_back(stmt); - any = any || flat(stmt); - } - if (!any) - { - return; - } - std::vector flatten(statements.size()); - for (size_t i = 0; i < statements.size(); ++i) - { - flatten[i] = flat(statements[i]); - } - // The statements are the ones they were, every reference in - // them still in the script: counted neither out nor in again. - const Uncounted uncounted(*ctx.context); - for (LSLASTNode* stmt : statements) - { - block->removeChild(stmt); - } - for (size_t i = 0; i < statements.size(); ++i) - { - if (!flatten[i]) - { - block->pushChild(statements[i]); - continue; - } - std::vector inner; - for (LSLASTNode* stmt = statements[i]->getChild(0); stmt; stmt = stmt->getNext()) - { - inner.push_back(stmt); - } - for (LSLASTNode* stmt : inner) - { - statements[i]->removeChild(stmt); - block->pushChild(stmt); - } - ++changes; - } - } - - // `T x;` and, later in the block, `x = e;` with nothing between that - // names x, jumps or is jumped to: the declaration where the - // assignment was, `T x = e;` -- which the local's value may then - // go further from. - void sinkDeclarations(LSLCompoundStatement* block) - { - std::vector statements; - for (LSLASTNode* stmt : *block) - { - statements.push_back(stmt); - } - const auto mentions = [](LSLASTNode* root, LSLSymbol* sym) { - std::vector stack{ root }; - while (!stack.empty()) - { - LSLASTNode* n = stack.back(); - stack.pop_back(); - if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym) - { - return true; - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - return false; - }; - const auto jumpy = [](LSLASTNode* root) { - std::vector stack{ root }; - while (!stack.empty()) - { - LSLASTNode* n = stack.back(); - stack.pop_back(); - if (n->getNodeType() == NODE_STATEMENT && (n->getNodeSubType() == NODE_LABEL || n->getNodeSubType() == NODE_JUMP_STATEMENT)) - { - return true; - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - return false; - }; - for (size_t i = 0; i < statements.size(); ++i) - { - if (statements[i]->getNodeSubType() != NODE_DECLARATION) - { - continue; - } - auto* decl = static_cast(statements[i]); - LSLSymbol* sym = decl->getSymbol(); - LSLASTNode* init = decl->getChild(1); - if (!sym || sym->getSubType() != SYM_LOCAL || (init && init->getNodeType() != NODE_NULL)) - { - continue; - } - for (size_t j = i + 1; j < statements.size(); ++j) - { - LSLASTNode* stmt = statements[j]; - if (!mentions(stmt, sym)) - { - if (jumpy(stmt)) - { - break; - } - continue; - } - auto* expr = stmt->getNodeSubType() == NODE_EXPRESSION_STATEMENT ? static_cast(stmt)->getExpr() : nullptr; - if (!expr || expr->getNodeSubType() != NODE_BINARY_EXPRESSION || expr->getOperation() != '=' || - expr->getChild(0)->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(expr->getChild(0))->getMember() || - static_cast(expr->getChild(0))->getSymbol() != sym || mentions(expr->getChild(1), sym)) - { - break; - } - decl->setChild(1, static_cast(expr->takeChild(1))); - block->removeChild(decl); - LSLASTNode::replaceNode(stmt, decl); - statements[j] = decl; - ++changes; - break; - } - } - } - - // How many labels of a name the function or event a node is in has. - // A jump goes to the last of them; the compiler finds it among - // those in scope. - static int labelsNamed(LSLASTNode* node, const char* name) - { - LSLASTNode* callable = node; - while (callable && callable->getNodeType() != NODE_GLOBAL_FUNCTION && callable->getNodeType() != NODE_EVENT_HANDLER) - { - callable = callable->getParent(); - } - int named = 0; - const std::function count = [&](LSLASTNode* n) { - if (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL && !strcmp(static_cast(n)->getIdentifier()->getName(), name)) - { - ++named; - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - count(child); - } - }; - if (callable) - { - count(callable); - } - return named; - } - - // Whether a jump goes where running on would go anyway: its label - // is the next statement to run -- after the jump, or after the - // blocks and the if branches it is the end of -- and the only - // label of its name in the function, a jump going to the last of - // a name. - static bool jumpsToNext(LSLASTNode* jump) - { - LSLIdentifier* id = static_cast(jump)->getIdentifier(); - const char* target = id ? id->getName() : nullptr; - if (!target) - { - return false; - } - const auto empty = [](LSLASTNode* n) { - return n->getNodeSubType() == NODE_NOP_STATEMENT || (n->getNodeSubType() == NODE_COMPOUND_STATEMENT && !n->hasChildren()); - }; - LSLASTNode* at = jump; - while (true) - { - LSLASTNode* parent = at->getParent(); - if (!parent || parent->getNodeType() != NODE_STATEMENT) - { - return false; - } - if (parent->getNodeSubType() == NODE_IF_STATEMENT && at != static_cast(parent)->getCheckExpr()) - { - at = parent; - continue; - } - if (parent->getNodeSubType() != NODE_COMPOUND_STATEMENT) - { - return false; - } - LSLASTNode* next = at->getNext(); - while (next && empty(next)) - { - next = next->getNext(); - } - if (!next) - { - at = parent; - continue; - } - if (next->getNodeSubType() != NODE_LABEL || strcmp(static_cast(next)->getIdentifier()->getName(), target)) - { - return false; - } - // The only label of the name in the function or event. - return labelsNamed(next, target) == 1; - } - } - - // A local set once from what changes nothing and is read the same - // at any time, then read once in the expression of a later - // statement of the same block: its value put where it is read, and - // the local gone -- where no loop reads it again and no label - // could bring the run in between, and nothing that runs between - // writes what the value reads. A parameter a function was put in - // place with, or the variable its value was set in, most often. - void substituteLocals(LSLCompoundStatement* block) - { - // The block's locals that may go -- declared here from what - // changes nothing, never written, read once -- and, in one walk - // of what follows, where each is read. - std::vector statements; - boost::unordered_flat_map> reads; - for (LSLASTNode* stmt : *block) - { - if (stmt->getNodeSubType() == NODE_DECLARATION) - { - auto* decl = static_cast(stmt); - LSLSymbol* sym = decl->getSymbol(); - if (sym && sym->getSubType() == SYM_LOCAL && sym->getAssignments() == 0 && sym->getReferences() == 2 && decl->getInitializer() && - decl->getInitializer()->getNodeType() == NODE_EXPRESSION) - { - reads.emplace(sym, std::make_pair(size_t(-1), nullptr)); - } - } - statements.push_back(stmt); - } - if (reads.empty()) - { - return; - } - std::vector stack; - for (size_t k = 0; k < statements.size(); ++k) - { - stack.assign(1, statements[k]); - while (!stack.empty()) - { - LSLASTNode* n = stack.back(); - stack.pop_back(); - if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION) - { - const auto found = reads.find(static_cast(n)->getSymbol()); - if (found != reads.end() && !found->second.second) - { - found->second = { k, static_cast(n) }; - } - } - // A statement's own expressions only: a read in one it - // holds is not one a value may be put in, and that - // statement's own block walks it. - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - if (child->getNodeType() != NODE_STATEMENT) - { - stack.push_back(child); - } - } - } - } - const auto holdsLabel = [](LSLASTNode* root) { - bool found = false; - const std::function look = [&](LSLASTNode* n) { - found = found || (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL); - for (LSLASTNode* child = n->getChild(0); child && !found; child = child->getNext()) - { - look(child); - } - }; - look(root); - return found; - }; - std::vector gone; - for (size_t i = 0; i < statements.size(); ++i) - { - if (statements[i]->getNodeSubType() != NODE_DECLARATION) - { - continue; - } - auto* decl = static_cast(statements[i]); - LSLSymbol* sym = decl->getSymbol(); - LSLExpression* value = decl->getInitializer(); - const auto read = sym ? reads.find(sym) : reads.end(); - if (read == reads.end() || !value || !sideEffectFree(value) || value->getIType() != sym->getIType()) - { - continue; - } - // The read, and the statement it is in: one after this. - const size_t at = read->second.first; - LSLLValueExpression* use = read->second.second; - if (!use || at <= i || use->getMember()) - { - continue; - } - // In the statement's own expression: no statement, and so - // no loop or branch, between it and the read. - bool direct = true; - for (LSLASTNode* up = use->getParent(); up && up != statements[at]; up = up->getParent()) - { - direct = direct && up->getNodeType() != NODE_STATEMENT; - } - const LSLNodeSubType shape = statements[at]->getNodeSubType(); - if (!direct || shape == NODE_WHILE_STATEMENT || shape == NODE_DO_STATEMENT || shape == NODE_FOR_STATEMENT) - { - continue; - } - bool labelled = false; - ALLSLEffects::Writes between; - for (size_t k = i + 1; k < at; ++k) - { - labelled = labelled || holdsLabel(statements[k]); - between.add(ctx.effects->of(statements[k])); - } - for (LSLASTNode* earlier : ALLSLEffects::before(statements[at], use)) - { - between.add(ctx.effects->of(earlier)); - } - bool written = false; - const std::function reads = [&](LSLASTNode* n) { - if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION) - { - written = written || between.writes(static_cast(n)->getSymbol()); - } - for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) - { - reads(child); - } - }; - reads(value); - if (labelled || written) - { - continue; - } - report.note(decl->getLoc(), "OptimizerSubstitutedLocal", "put the value of [1] where it is read, and removed it", { sym->getName() }); - putInPlace(use, static_cast(decl->takeChild(1)), ctx.allocator); - gone.push_back(decl); - ++changes; - } - for (LSLDeclaration* decl : gone) - { - block->removeChild(decl); - } - } - - bool visit(LSLIfStatement* stmt) override - { - visitChildren(stmt); - LSLConstant* cv = stmt->getCheckExpr() ? stmt->getCheckExpr()->getConstantValue() : nullptr; - if (!cv || cv->getNodeSubType() != NODE_INTEGER_CONSTANT) - { - return false; - } - const bool taken = static_cast(cv)->getValue() != 0; - LSLStatement* branch = taken ? stmt->getTrueBranch() : stmt->getFalseBranch(); - // A branch never taken is reached all the same by a jump to a - // label in it. - if (LSLStatement* other = taken ? stmt->getFalseBranch() : stmt->getTrueBranch(); other && holdsLiveLabel(other)) - { - return false; - } - if (branch && lastIfOfStateChange(stmt, branch)) - { - return false; - } - report.note(stmt->getLoc(), taken ? "OptimizerIfAlwaysTrue" : "OptimizerIfAlwaysFalse", - taken ? "the condition of this if is always true; kept only what runs" : "the condition of this if is always false; kept only what runs"); - replaceStatement(stmt, branch ? static_cast(stmt->takeChild(taken ? 1 : 2)) : nullptr); - return false; - } - - bool visit(LSLWhileStatement* stmt) override - { - visitChildren(stmt); - if (isInteger(stmt->getCheckExpr(), 0) && !holdsLiveLabel(stmt)) - { - report.note(stmt->getLoc(), "OptimizerRemovedWhile", "removed a while loop whose condition is always false"); - replaceStatement(stmt, nullptr); - } - return false; - } - - bool visit(LSLDoStatement* stmt) override - { - visitChildren(stmt); - if (isInteger(stmt->getCheckExpr(), 0)) - { - report.note(stmt->getLoc(), "OptimizerDoRunsOnce", "a do loop whose condition is always false runs once; kept its body"); - replaceStatement(stmt, static_cast(stmt->takeChild(0))); - } - return false; - } - - bool visit(LSLForStatement* stmt) override - { - visitChildren(stmt); - if (isInteger(stmt->getCheckExpr(), 0) && !holdsLiveLabel(stmt)) - { - // The initialisers still run, once. - auto* block = ctx.allocator->newTracked(nullptr); - block->setLoc(stmt->getLoc()); - LSLASTNode* init = stmt->getInitExprs(); - while (init && init->hasChildren()) - { - auto* expr = static_cast(init->takeChild(0)); - init->removeChild(init->getChild(0)); - auto* es = ctx.allocator->newTracked(expr); - es->setLoc(expr->getLoc()); - block->pushChild(es); - } - report.note(stmt->getLoc(), "OptimizerRemovedFor", "removed a for loop whose condition is always false; its initialisers stay"); - replaceStatement(stmt, block->hasChildren() ? block : nullptr); - } - return false; - } - - private: - // A statement becomes another, or nothing: nothing is an empty - // statement where a branch or a body needs one. - void replaceStatement(LSLStatement* old, LSLStatement* now) - { - LSLASTNode* parent = old->getParent(); - if (!now) - { - if (parent && parent->getNodeSubType() == NODE_COMPOUND_STATEMENT) - { - parent->removeChild(old); - ++changes; - return; - } - now = ctx.allocator->newTracked(); - now->setLoc(old->getLoc()); - } - LSLASTNode::replaceNode(old, now); - ++changes; - } - - // A call to a function of the script's own whose body is empty, - // with arguments that do nothing on their own. - bool callsNothing(LSLExpression* expr) const - { - if (!expr || expr->getNodeSubType() != NODE_FUNCTION_EXPRESSION) - { - return false; - } - LSLSymbol* sym = expr->getSymbol(); - auto function = sym ? mFunctions.find(sym) : mFunctions.end(); - if (function == mFunctions.end()) - { - return false; - } - LSLASTNode* body = function->second->getStatements(); - if (!body || body->getNodeSubType() != NODE_COMPOUND_STATEMENT || body->hasChildren()) - { - return false; - } - for (LSLASTNode* arg : *static_cast(expr)->getArguments()) - { - if (!changesNothing(arg)) - { - return false; - } - } - return true; - } - - // Whether a label a jump from outside a statement goes to is in - // it, however far down: the one way in to a statement nothing - // before it reaches. A jump from inside to a label inside -- a - // loop made of labels -- is no way in. - static bool holdsLiveLabel(LSLASTNode* statement) - { - // Each label's references are its own name and every jump to - // it; the jumps inside the statement are taken off. - boost::unordered_flat_map inside; - std::vector labels; - const std::function gather = [&](LSLASTNode* node) { - if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_LABEL) - { - if (LSLSymbol* sym = node->getSymbol()) - { - labels.push_back(sym); - } - } - else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_JUMP_STATEMENT) - { - if (LSLSymbol* sym = static_cast(node)->getIdentifier()->getSymbol()) - { - ++inside[sym]; - } - } - for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) - { - gather(child); - } - }; - gather(statement); - for (LSLSymbol* label : labels) - { - if (label->getReferences() - 1 > inside[label]) - { - return true; - } - } - return false; - } - - // Whether taking `stmt` away for its branch would leave a state - // change in a function with no `if` around it: a function may - // change state only under an `if`, which the compiler checks - // (Tailslide's E_CHANGE_STATE_IN_FUNCTION), whatever the `if` - // tests. An event may change state anywhere. - static bool lastIfOfStateChange(LSLIfStatement* stmt, LSLStatement* branch) - { - for (LSLASTNode* up = stmt->getParent(); up; up = up->getParent()) - { - if (up->getNodeType() == NODE_STATE || (up->getNodeType() == NODE_STATEMENT && up->getNodeSubType() == NODE_IF_STATEMENT)) - { - return false; - } - if (up->getNodeType() == NODE_GLOBAL_FUNCTION) - { - break; - } - } - bool orphaned = false; - const std::function look = [&](LSLASTNode* node, bool underIf) { - if (orphaned || !node) - { - return; - } - if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_STATE_STATEMENT && !underIf) - { - orphaned = true; - return; - } - const bool isIf = node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_IF_STATEMENT; - for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) - { - look(child, underIf || isIf); - } - }; - look(branch, false); - return orphaned; - } - - bool unusedLabel(LSLASTNode* label) - { - LSLSymbol* sym = label->getSymbol(); - if (!sym || sym->getReferences() > 1) - { - return false; - } - // Another label of its name, and a jump the compiler finds this - // one for, runs to the last: the count of this one's own jumps - // does not say it is unused. - if (labelsNamed(label, static_cast(label)->getIdentifier()->getName()) > 1) - { - return false; - } - report.note(label->getLoc(), "OptimizerRemovedLabel", "removed the label [1], which nothing jumps to", { render(label) }); - return true; - } - - bool unusedLocal(LSLDeclaration* decl) - { - LSLSymbol* sym = decl->getSymbol(); - if (!sym || sym->getReferences() != 1 || sym->getAssignments() != 0) - { - return false; - } - LSLASTNode* init = decl->getInitializer(); - if (init && !changesNothing(init)) - { - return false; - } - forget(decl, sym); - report.note(decl->getLoc(), "OptimizerRemovedLocal", "removed the unused local [1]", { sym->getName() }); - return true; - } - - static void forget(LSLASTNode* node, LSLSymbol* sym) - { - for (LSLASTNode* up = node; up; up = up->getParent()) - { - if (up->getSymbolTable() && up->getSymbolTable()->remove(sym)) - { - return; - } - } - } - - void globals(LSLScript* script) - { - std::vector going; - for (LSLASTNode* global : *script->getGlobals()) - { - LSLSymbol* sym = global->getSymbol(); - if (!sym || sym->getReferences() != 1) - { - continue; - } - if (global->getNodeType() == NODE_GLOBAL_VARIABLE) - { - report.note(global->getLoc(), "OptimizerRemovedGlobal", "removed the unused global [1]", { sym->getName() }); - going.push_back(global); - } - else if (global->getNodeType() == NODE_GLOBAL_FUNCTION) - { - report.note(global->getLoc(), "OptimizerRemovedFunction", "removed the unused function [1]", { sym->getName() }); - going.push_back(global); - } - } - for (LSLASTNode* global : going) - { - script->getSymbolTable()->remove(global->getSymbol()); - script->getGlobals()->removeChild(global); - ++changes; - } - } - - boost::unordered_flat_map mFunctions; - boost::unordered_flat_map> mNames; - - // Whether what follows a statement never runs by running on from it: - // it returns, changes state or jumps; resets the script; is an if - // every way of which does so, or a loop that never ends. A label - // jumped to after it is the dead code's own business (above). - static bool stops(LSLASTNode* stmt) - { - if (!stmt || stmt->getNodeType() != NODE_STATEMENT) - { - return false; - } - const auto forever = [](LSLExpression* check) { - LSLConstant* cv = check ? check->getConstantValue() : nullptr; - return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && static_cast(cv)->getValue() != 0; - }; - switch (stmt->getNodeSubType()) - { - case NODE_RETURN_STATEMENT: - case NODE_STATE_STATEMENT: - case NODE_JUMP_STATEMENT: - return true; - case NODE_EXPRESSION_STATEMENT: - { - LSLExpression* expr = bare(static_cast(stmt)->getExpr()); - LSLSymbol* sym = expr && expr->getNodeSubType() == NODE_FUNCTION_EXPRESSION ? expr->getSymbol() : nullptr; - return sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llResetScript"); - } - case NODE_COMPOUND_STATEMENT: - { - LSLASTNode* last = nullptr; - for (LSLASTNode* child = stmt->getChild(0); child; child = child->getNext()) - { - last = child; - } - return stops(last); - } - case NODE_IF_STATEMENT: - { - auto* branch = static_cast(stmt); - return branch->getFalseBranch() && stops(branch->getTrueBranch()) && stops(branch->getFalseBranch()); - } - case NODE_WHILE_STATEMENT: - return forever(static_cast(stmt)->getCheckExpr()); - case NODE_DO_STATEMENT: - return forever(static_cast(stmt)->getCheckExpr()); - case NODE_FOR_STATEMENT: - return forever(static_cast(stmt)->getCheckExpr()); - default: - return false; - } - } - - // Every node of a subtree, the root first. - template static void each(LSLASTNode* root, const F& f) - { - std::vector stack{ root }; - while (!stack.empty()) - { - LSLASTNode* node = stack.back(); - stack.pop_back(); - if (!node) - { - continue; - } - f(node); - for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - } - - // What can run: the default state's handlers, each function they - // call and each state they enter, and on through those. A function - // or a state nothing that runs reaches goes, though others that do - // not run reach it -- functions that call only each other. - void unreached(LSLScript* script) - { - boost::unordered_flat_map states; - LSLASTNode* start = nullptr; - for (LSLASTNode* state : *script->getStates()) - { - if (LSLSymbol* sym = state->getSymbol()) - { - states[sym] = state; - start = !strcmp(sym->getName(), "default") ? state : start; - } - } - if (!start) - { - return; - } - boost::unordered_flat_set reached{ start }; - std::vector todo{ start }; - const auto reach = [&](LSLASTNode* node) { - if (node && reached.insert(node).second) - { - todo.push_back(node); - } - }; - while (!todo.empty()) - { - LSLASTNode* from = todo.back(); - todo.pop_back(); - each(from, [&](LSLASTNode* node) { - if (node->getNodeType() == NODE_EXPRESSION && node->getNodeSubType() == NODE_FUNCTION_EXPRESSION) - { - LSLSymbol* sym = static_cast(node)->getSymbol(); - const auto found = sym ? mFunctions.find(sym) : mFunctions.end(); - reach(found == mFunctions.end() ? nullptr : found->second); - } - else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_STATE_STATEMENT) - { - LSLIdentifier* id = static_cast(node)->getIdentifier(); - const auto found = id && id->getSymbol() ? states.find(id->getSymbol()) : states.end(); - reach(found == states.end() ? nullptr : found->second); - } - }); - } - for (LSLASTNode* list : { static_cast(script->getGlobals()), static_cast(script->getStates()) }) - { - std::vector going; - for (LSLASTNode* node = list->getChild(0); node; node = node->getNext()) - { - const bool function = node->getNodeType() == NODE_GLOBAL_FUNCTION; - if ((function || node->getNodeType() == NODE_STATE) && node->getSymbol() && !reached.contains(node)) - { - report.note(node->getLoc(), function ? "OptimizerRemovedUncalled" : "OptimizerRemovedUnentered", - function ? "removed the function [1], which nothing that runs calls" : "removed the state [1], which nothing that runs enters", - { node->getSymbol()->getName() }); - going.push_back(node); - } - } - for (LSLASTNode* node : going) - { - script->getSymbolTable()->remove(node->getSymbol()); - list->removeChild(node); - ++changes; - } - } - } - - // A handler with nothing in it goes where having it changes - // nothing: not a touch's, which makes the object touchable, nor a - // collision's, money's or control's, whose handler changes what the - // object does. A state keeps one handler, which it must have. - void emptyHandlers(LSLScript* script) - { - static constexpr std::string_view QUIET[] = { "state_entry", "state_exit", "on_rez", "attach", "changed", "timer", "listen", "link_message", - "dataserver", "http_response", "sensor", "no_sensor", "at_target", "not_at_target", - "at_rot_target", "not_at_rot_target", "moving_start", "moving_end", "object_rez", - "email", "remote_data", "run_time_permissions", "linkset_data", "transaction_result", - "path_update", "experience_permissions", "experience_permissions_denied" }; - for (LSLASTNode* state = script->getStates()->getChild(0); state; state = state->getNext()) - { - LSLASTNode* handlers = state->getChild(1); - if (!handlers) - { - continue; - } - std::vector going; - size_t count = 0; - for (LSLASTNode* handler = handlers->getChild(0); handler; handler = handler->getNext()) - { - ++count; - auto* event = static_cast(handler); - LSLStatement* body = event->getStatements(); - LSLSymbol* sym = event->getSymbol(); - if (sym && body && body->getNodeSubType() == NODE_COMPOUND_STATEMENT && !body->hasChildren() && - std::find(std::begin(QUIET), std::end(QUIET), std::string_view(sym->getName())) != std::end(QUIET)) - { - going.push_back(handler); - } - } - if (going.size() == count) - { - going.pop_back(); - } - for (LSLASTNode* handler : going) - { - report.note(handler->getLoc(), "OptimizerRemovedEmptyHandler", "removed the empty handler of [1], which having changes nothing", - { handler->getSymbol()->getName() }); - handlers->removeChild(handler); - ++changes; - } - } - } - - // Whether an assignment's or an increment's value goes unread: it is - // a statement of its own, or a for's first part or step. - static bool unread(LSLASTNode* expr) - { - LSLASTNode* up = expr->getParent(); - if (up && up->getNodeType() == NODE_STATEMENT && up->getNodeSubType() == NODE_EXPRESSION_STATEMENT) - { - return true; - } - LSLASTNode* loop = up ? up->getParent() : nullptr; - return up && up->getNodeType() == NODE_AST_NODE_LIST && loop && loop->getNodeType() == NODE_STATEMENT && - loop->getNodeSubType() == NODE_FOR_STATEMENT && - (up == static_cast(loop)->getInitExprs() || up == static_cast(loop)->getIncrExprs()); - } - - // A statement taken out: from its block or its for, or made nothing - // where it is the whole of an if's or a loop's body. - void takeOut(LSLASTNode* node) - { - LSLASTNode* up = node->getParent(); - if (up && (up->getNodeSubType() == NODE_COMPOUND_STATEMENT || up->getNodeType() == NODE_AST_NODE_LIST)) - { - up->removeChild(node); - return; - } - LSLASTNode::replaceNode(node, ctx.allocator->newTracked()); - } - - // A variable -- a local, a global -- set and never read goes, and - // each place it is set keeps what setting it ran: `x = f();` is - // `f();`, `x++;` nothing, and `integer h = llListen(...);` the - // call. Only where every place that names it but its declaration - // sets it, and nothing reads what was set. - void writeOnly(LSLScript* script) - { - struct Uses - { - LSLASTNode* declaration = nullptr; - std::vector writes; - bool read = false; - }; - boost::unordered_flat_map uses; - std::vector order; - const auto of = [&](LSLSymbol* sym) -> Uses& { - auto [it, fresh] = uses.try_emplace(sym); - if (fresh) - { - order.push_back(sym); - } - return it->second; - }; - each(script, [&](LSLASTNode* node) { - if (node->getNodeType() == NODE_GLOBAL_VARIABLE && node->getSymbol()) - { - of(node->getSymbol()).declaration = node; - } - else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_DECLARATION && node->getSymbol()) - { - of(node->getSymbol()).declaration = node; - } - else if (node->getNodeType() == NODE_EXPRESSION && node->getNodeSubType() == NODE_LVALUE_EXPRESSION) - { - LSLSymbol* sym = node->getSymbol(); - if (!sym || (sym->getSubType() != SYM_LOCAL && sym->getSubType() != SYM_GLOBAL)) - { - return; - } - LSLASTNode* parent = node->getParent(); - const bool write = parent && parent->getNodeType() == NODE_EXPRESSION && - operation_mutates(static_cast(parent)->getOperation()) && parent->getChild(0) == node; - if (write && unread(parent)) - { - of(sym).writes.push_back(parent); - } - else - { - of(sym).read = true; - } - } - }); - for (LSLSymbol* sym : order) - { - // A global never used at all goes with the unused (globals()), - // and a local set to nothing that changes anything goes with - // those (unusedLocal); one set to what does is this pass's. - Uses& use = uses[sym]; - const bool global = use.declaration && use.declaration->getNodeType() == NODE_GLOBAL_VARIABLE; - if (use.read || !use.declaration || (global && use.writes.empty())) - { - continue; - } - report.note(use.declaration->getLoc(), "OptimizerRemovedWriteOnly", "removed [1], which is set and never read", { sym->getName() }); - for (LSLASTNode* write : use.writes) - { - LSLASTNode* value = write->getNodeSubType() == NODE_BINARY_EXPRESSION ? write->getChild(1) : nullptr; - LSLASTNode* holder = write->getParent()->getNodeType() == NODE_AST_NODE_LIST ? write : write->getParent(); - if (value && !changesNothing(value)) - { - // What it was set to, still worked out. - LSLASTNode::replaceNode(write, write->takeChild(1)); - } - else - { - takeOut(holder); - } - } - if (global) - { - script->getSymbolTable()->remove(sym); - script->getGlobals()->removeChild(use.declaration); - } - else - { - auto* decl = static_cast(use.declaration); - LSLASTNode* init = decl->getInitializer(); - forget(decl, sym); - if (init && !changesNothing(init)) - { - LSLASTNode::replaceNode(decl, ctx.allocator->newTracked(static_cast(decl->takeChild(1)))); - } - else - { - takeOut(decl); - } - } - ++changes; - } - } - - void states(LSLScript* script) - { - std::vector going; - for (LSLASTNode* state : *script->getStates()) - { - LSLSymbol* sym = state->getSymbol(); - if (!sym || sym->getReferences() != 1 || !strcmp(sym->getName(), "default")) - { - continue; - } - report.note(state->getLoc(), "OptimizerRemovedState", "removed the state [1], which nothing enters", { sym->getName() }); - going.push_back(state); - } - for (LSLASTNode* state : going) - { - script->getSymbolTable()->remove(state->getSymbol()); - script->getStates()->removeChild(state); - ++changes; - } - } - }; - - // ---- names ------------------------------------------------------------------------------- - - class TableCollector : public ASTVisitor - { - public: - bool visit(LSLASTNode* node) override - { - if (LSLSymbolTable* table = node->getSymbolTable()) - { - if (mSeen.insert(table).second) - { - tables.push_back(table); - } - } - return true; - } - std::vector tables; - - private: - std::set mSeen; - }; - - // Each label's function or handler. LSL takes a jump to whichever - // label of its name the function declares last, seen from the jump or - // not, so labels of one name in one function are one name still. - class LabelScopes : public ASTVisitor - { - public: - bool visit(LSLLabel* label) override - { - LSLASTNode* scope = label; - while (scope && scope->getNodeType() != NODE_GLOBAL_FUNCTION && scope->getNodeType() != NODE_EVENT_HANDLER) - { - scope = scope->getParent(); - } - if (LSLSymbol* sym = label->getSymbol()) - { - of[sym] = scope; - } - return false; - } - std::map of; - }; - - // What each character of a name costs the target's code, where the - // compiled script keeps it. Mono keeps a global's as its field's, and a - // function's as its method's, and each once more where the code - // reaches it; and a state's in the name of each of its handlers' - // methods, and in the string a change to it loads, whose characters - // are two bytes. Luau keeps a global's and a function's once. Nothing - // else keeps a name: LSO none, and a local's, a parameter's or a - // label's no target. - S32 nameCost(LSLSymbol* sym, ALLSLOptimizer::Target target, const std::map& handlers) - { - const bool global = sym->getSymbolType() == SYM_VARIABLE && sym->getSubType() == SYM_GLOBAL; - const bool used = sym->getReferences() > 1; - switch (target) - { - case ALLSLOptimizer::Target::Mono: - if (global || sym->getSymbolType() == SYM_FUNCTION) - { - return used ? 2 : 1; - } - if (sym->getSymbolType() == SYM_STATE) - { - const auto found = handlers.find(sym); - return (found == handlers.end() ? 0 : found->second) + (used ? 2 : 0); - } - return 0; - case ALLSLOptimizer::Target::Luau: - return global || sym->getSymbolType() == SYM_FUNCTION ? 1 : 0; - default: - return 0; - } - } - - // Every name the script owns, shortest first for those whose characters - // cost the compiled script the most, then for the most used. - void shrink(LSLScript* script, ScriptContext& context, ScriptAllocator& allocator, Report& report, ALLSLOptimizer::Result& result, - ALLSLOptimizer::Target target) - { - TableCollector collector; - script->visit(&collector); - std::vector symbols; - for (LSLSymbolTable* table : collector.tables) - { - if (table->getTableType() == SYMTAB_BUILTINS) - { - continue; - } - for (auto& entry : table->getMap()) - { - LSLSymbol* sym = entry.second; - if (sym->getSymbolType() == SYM_EVENT || sym->getSubType() == SYM_BUILTIN) - { - continue; - } - if (sym->getSymbolType() == SYM_STATE && !strcmp(sym->getName(), "default")) - { - continue; - } - symbols.push_back(sym); - } - } - std::map handlers; - for (LSLState* state : *script->getStates()) - { - if (LSLSymbol* sym = state->getSymbol()) - { - handlers[sym] = static_cast(state->getEventHandlers()->getNumChildren()); - } - } - std::map costs; - for (LSLSymbol* sym : symbols) - { - costs[sym] = nameCost(sym, target, handlers); - } - // Equally used, the first declared first, then by name: the tables - // are hash maps, whose order is the standard library's own. - std::stable_sort(symbols.begin(), symbols.end(), [&costs](LSLSymbol* a, LSLSymbol* b) { - if (costs[a] != costs[b]) - { - return costs[a] > costs[b]; - } - if (a->getReferences() != b->getReferences()) - { - return a->getReferences() > b->getReferences(); - } - const Tailslide::YYLTYPE& at = *a->getLoc(); - const Tailslide::YYLTYPE& bt = *b->getLoc(); - if (at < bt || bt < at) - { - return at < bt; - } - return strcmp(a->getName(), b->getName()) < 0; - }); - const std::string first = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; - const std::string rest = first + "0123456789"; - // Every name given so far, which none is given again but a label's. - boost::unordered_flat_set> given; - const auto usable = [&](const std::string& name) { - // No word of the language's, the preprocessor's extensions - // included, whether or not they are on. - if (ALScriptLexicon::lslWord(name) != ALScriptLexicon::LSL_NAME) - { - return false; - } - return !given.contains(name) && !(context.builtins && context.builtins->lookup(name.c_str(), SYM_ANY)); - }; - std::vector digits; - // The next name, counting up in a mixed radix: the first digit over - // `first`, the rest over `rest`. Looked at, or taken. - const auto next = [&](bool take) { - std::vector looked = digits; - std::vector& at = take ? digits : looked; - std::string name; - while (true) - { - if (at.empty()) - { - at.push_back(0); - } - else - { - size_t i = at.size(); - while (i > 0) - { - --i; - const int radix = i == 0 ? static_cast(first.size()) : static_cast(rest.size()); - if (++at[i] < radix) - { - break; - } - at[i] = 0; - if (i == 0) - { - at.insert(at.begin(), 0); - break; - } - } - } - name.clear(); - for (size_t i = 0; i < at.size(); ++i) - { - name += (i == 0 ? first : rest)[at[i]]; - } - if (usable(name)) - { - return name; - } - } - }; - // Mono loads a state's name as a string where the script changes to - // it, and holds each string once: such a state is named for a string - // the script holds already, where its characters in each handler's - // name cost less than the next name's and a string of that. - Strings strings; - std::vector held; - if (target == ALLSLOptimizer::Target::Mono) - { - script->visit(&strings); - for (const auto& [text, times] : strings.found) - { - const bool shaped = !text.empty() && first.find(text[0]) != std::string::npos && - text.find_first_not_of(rest) == std::string::npos; - if (shaped) - { - held.push_back(text); - } - } - std::sort(held.begin(), held.end(), [](const std::string& a, const std::string& b) { return a.size() != b.size() ? a.size() < b.size() : a < b; }); - } - const auto heldName = [&](LSLSymbol* sym) -> std::optional { - const auto count = handlers.find(sym); - if (held.empty() || sym->getSymbolType() != SYM_STATE || sym->getReferences() < 2 || count == handlers.end()) - { - return std::nullopt; - } - const std::string ours = next(false); - const S32 each = count->second; - const S32 cost = each * S32(ours.size()) + (strings.found.contains(ours) ? 0 : 2 * S32(ours.size()) + 2); - for (const std::string& text : held) - { - if (each * S32(text.size()) >= cost) - { - break; - } - if (usable(text)) - { - return text; - } - } - return std::nullopt; - }; - LabelScopes labels; - script->visit(&labels); - std::map, std::string> labelNames; - for (LSLSymbol* sym : symbols) - { - std::string name; - const auto scope = labels.of.find(sym); - if (scope != labels.of.end()) - { - auto [named, fresh] = labelNames.try_emplace({ scope->second, sym->getName() }); - if (fresh) - { - named->second = next(true); - } - name = named->second; - } - else if (const std::optional text = heldName(sym)) - { - name = *text; - } - else - { - name = next(true); - } - given.insert(name); - sym->setMangledName(allocator.copyStr(name.c_str())); - result.renamed.emplace(sym->getName(), name); - report.note(sym->getLoc(), "OptimizerRenamed", "renamed the [1] [2] to [3]", { LSLSymbol::getTypeName(sym->getSymbolType()), sym->getName(), name }); - } - } - - // ---- the printer ---------------------------------------------------------------------------- - - // Tailslide's printer with floats that read back exactly, integers - // where a float may be one, and a note of where every name, number - // and statement came from. - class Printer : public PrettyPrintVisitor - { - public: - Printer(const PrettyPrintOpts& opts, const ALLSLOptimizer::Options& options) : PrettyPrintVisitor(opts), mOptions(options) {} - - bool visit(LSLIdentifier* id) override { return marked(id, [&] { return PrettyPrintVisitor::visit(id); }); } - bool visit(LSLIntegerConstant* c) override { return marked(c, [&] { return PrettyPrintVisitor::visit(c); }); } - bool visit(LSLStringConstant* c) override { return marked(c, [&] { return PrettyPrintVisitor::visit(c); }); } - bool visit(LSLKeyConstant* c) override { return marked(c, [&] { return PrettyPrintVisitor::visit(c); }); } - bool visit(LSLFloatConstant* c) override - { - return marked(c, [&] { - const double v = c->getValue(); - const bool integer = integerAllowed(c); - // A whole float where nothing converts an integer for it, - // as the cast of one where that is smaller; not in a - // global's value, which takes no cast, nor a negative - // zero, which the cast would lose the sign of. - if (!integer && mOptions.optfloats && ALLSLCosts::of(mOptions.target).castForWholeFloat && std::isfinite(v) && std::floor(v) == v && - std::fabs(v) < 2147483648.0 && !(v == 0.0 && std::signbit(v)) && !inGlobal(c)) - { - mStream << "((float)" << static_cast(v) << ")"; - return false; - } - mStream << number(v, integer); - return false; - }); - } - bool visit(LSLVectorConstant* c) override - { - return marked(c, [&] { - const Vector3* v = c->getValue(); - mStream << '<' << number(v->x, true) << ", " << number(v->y, true) << ", " << number(v->z, true) << '>'; - return false; - }); - } - bool visit(LSLQuaternionConstant* c) override - { - return marked(c, [&] { - const Quaternion* q = c->getValue(); - mStream << '<' << number(q->x, true) << ", " << number(q->y, true) << ", " << number(q->z, true) << ", " << number(q->s, true) << '>'; - return false; - }); - } - // Tailslide calls the type quaternion, which LSL takes, but the - // word everyone writes is rotation. - bool visit(LSLType* type) override - { - if (type->getIType() == LST_QUATERNION) - { - mStream << "rotation"; - return false; - } - return PrettyPrintVisitor::visit(type); - } - bool visit(LSLGlobalVariable* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLGlobalFunction* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLState* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLEventHandler* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLExpressionStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLDeclaration* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLIfStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLWhileStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLDoStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLForStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLReturnStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLJumpStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLLabel* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - bool visit(LSLStateStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } - - // The map, once everything is printed. - ALSourceMap map(const std::string& name) - { - ALSourceMap out; - out.addFile(name, std::string()); - const std::string text = mStream.str(); - // Marks that start together -- a statement and the name it - // begins with -- kept in the order they were printed, so that - // the map, which answers with the last at a place, answers the - // same for the same script every time. - std::stable_sort(mMarks.begin(), mMarks.end(), [](const Mark& a, const Mark& b) { return a.offset < b.offset; }); - S32 line = 0, column = 0; - size_t pos = 0; - for (const Mark& mark : mMarks) - { - for (; pos < mark.offset && pos < text.size(); ++pos) - { - if (text[pos] == '\n') - { - ++line; - column = 0; - } - else - { - ++column; - } - } - ALSourceMap::Segment s; - s.outLine = line; - s.outColumn = column; - s.length = static_cast(mark.length); - s.line = zeroBased(mark.loc.first_line); - s.column = zeroBased(mark.loc.first_column); - s.verbatim = false; - out.add(s); - } - out.finish(); - return out; - } - - private: - struct Mark - { - size_t offset = 0; - size_t length = 0; - Tailslide::YYLTYPE loc{}; - }; - - template bool marked(LSLASTNode* node, F print) - { - const size_t start = static_cast(mStream.tellp()); - const bool r = print(); - const size_t end = static_cast(mStream.tellp()); - if (node->getLoc()->first_line > 0) - { - mMarks.push_back(Mark{ start, end - start, *node->getLoc() }); - } - return r; - } - - template bool at(LSLASTNode* node, F print) - { - if (node->getLoc()->first_line > 0) - { - mMarks.push_back(Mark{ static_cast(mStream.tellp()), 0, *node->getLoc() }); - } - return print(); - } - - std::string number(double v, bool asInteger) - { - if (mOptions.optfloats && asInteger && integral(v) && ALLSLCosts::of(mOptions.target).integerForFloat) - { - return std::to_string(static_cast(v)); - } - return floatText(v, mOptions.target == ALLSLOptimizer::Target::Luau); - } - - // Whether an integer literal may stand where this float does: where - // LSL converts one on its own, and the operation does not change - // with it. - static bool inGlobal(LSLASTNode* c) - { - for (LSLASTNode* up = c->getParent(); up; up = up->getParent()) - { - if (up->getNodeType() == NODE_GLOBAL_VARIABLE) - { - return true; - } - } - return false; - } - - static bool integerAllowed(LSLASTNode* c) - { - LSLASTNode* node = c->getParent(); - LSLASTNode* parent = node ? node->getParent() : nullptr; - // Up through parentheses and a leading minus. - while (parent && parent->getNodeType() == NODE_EXPRESSION && - (parent->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION || - (parent->getNodeSubType() == NODE_UNARY_EXPRESSION && static_cast(parent)->getOperation() == OP_MINUS))) - { - node = parent; - parent = parent->getParent(); - } - if (!parent || !node) - { - return false; - } - const int slot = node->getParentSlot(); - switch (parent->getNodeType()) - { - case NODE_GLOBAL_VARIABLE: - return slot == 1 && parent->getChild(0)->getIType() == LST_FLOATINGPOINT; - case NODE_STATEMENT: - if (parent->getNodeSubType() == NODE_DECLARATION) - { - return slot == 1 && parent->getChild(0)->getIType() == LST_FLOATINGPOINT; - } - if (parent->getNodeSubType() == NODE_RETURN_STATEMENT) - { - for (LSLASTNode* up = parent; up; up = up->getParent()) - { - if (up->getNodeType() == NODE_GLOBAL_FUNCTION) - { - return up->getChild(0)->getIType() == LST_FLOATINGPOINT; - } - } - } - return false; - case NODE_AST_NODE_LIST: - { - // An argument: the parameter's type decides. - LSLASTNode* call = parent->getParent(); - if (!call || call->getNodeSubType() != NODE_FUNCTION_EXPRESSION) - { - return false; - } - LSLSymbol* sym = call->getSymbol(); - if (!sym || !sym->getFunctionDecl()) - { - return false; - } - LSLASTNode* param = sym->getFunctionDecl()->getChild(slot); - return param && param->getIType() == LST_FLOATINGPOINT; - } - case NODE_EXPRESSION: - break; - default: - return false; - } - auto* expr = static_cast(parent); - switch (expr->getNodeSubType()) - { - case NODE_VECTOR_EXPRESSION: - case NODE_QUATERNION_EXPRESSION: - return true; - case NODE_BINARY_EXPRESSION: - { - const LSLOperator op = expr->getOperation(); - LSLASTNode* other = expr->getChild(slot == 0 ? 1 : 0); - if (op == OP_ASSIGN || op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN || op == OP_MUL_ASSIGN || op == OP_DIV_ASSIGN) - { - return slot == 1 && expr->getChild(0)->getIType() == LST_FLOATINGPOINT; - } - if (op == OP_MOD || op == OP_MOD_ASSIGN) - { - return false; - } - // The other operand keeps the operation in floats. - return other && (other->getIType() == LST_FLOATINGPOINT || other->getIType() == LST_VECTOR || other->getIType() == LST_QUATERNION); - } - default: - return false; - } - } - - const ALLSLOptimizer::Options& mOptions; - std::vector mMarks; - }; - - void collectMessages(Logger& logger, ALScriptProblems& problems) - { - for (LogMessage* message : logger.getMessages()) - { - if (message->getType() != LOG_ERROR && message->getType() != LOG_INTERNAL_ERROR) - { - continue; - } - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Error; - p.source = message->getError() == E_SYNTAX_ERROR || message->getError() == E_PARSER_STACK_DEPTH ? ALScriptProblem::Source::Parser - : ALScriptProblem::Source::Types; - p.line = zeroBased(message->getLoc()->first_line); - p.column = zeroBased(message->getLoc()->first_column); - p.endLine = zeroBased(message->getLoc()->last_line); - p.endColumn = std::max(0, message->getLoc()->last_column); - p.code = std::to_string(static_cast(message->getError())); - p.message = message->getMessage(); - problems.push_back(std::move(p)); - } - } - - // Whether a statement standing as an if's true branch, with an else - // after it, would take that else for its own once printed: an if with - // no else, or a loop or an else that ends in one. - bool endsInBareIf(LSLASTNode* statement) - { - LSLASTNode* s = statement; - while (s && s->getNodeType() == NODE_STATEMENT) - { - switch (s->getNodeSubType()) - { - case NODE_IF_STATEMENT: - { - LSLASTNode* otherwise = s->getChild(2); - if (!otherwise || otherwise->getNodeType() == NODE_NULL) - { - return true; - } - s = otherwise; - break; - } - case NODE_WHILE_STATEMENT: - s = s->getChild(1); - break; - case NODE_FOR_STATEMENT: - s = s->getChild(3); - break; - default: - return false; - } - } - return false; - } - - // Braces round each true branch that would otherwise take the else - // after it. The passes move branches about -- `if (!c) A else B` swapped - // round, an empty branch turned into a negated condition -- and the - // printer, as the grammar reads it back, gives an else to the nearest - // if before it. - void braceDanglingElses(LSLASTNode* node, ScriptAllocator* allocator) - { - for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) - { - braceDanglingElses(child, allocator); - } - if (node->getNodeType() != NODE_STATEMENT || node->getNodeSubType() != NODE_IF_STATEMENT) - { - return; - } - LSLASTNode* otherwise = node->getChild(2); - if (!otherwise || otherwise->getNodeType() == NODE_NULL || !endsInBareIf(node->getChild(1))) - { - return; - } - LSLASTNode* yes = node->takeChild(1); - auto* block = allocator->newTracked(nullptr); - block->setLoc(yes->getLoc()); - block->pushChild(yes); - node->setChild(1, block); - } - // A user function a run left standing, with what putting it in place - // would take but for its bytes (ALLSLCosts), and where it is in the - // source. - struct Standing - { - std::string name; - ALLSLCosts::Inlining shape; - S32 line = 0; - S32 column = 0; - S32 endLine = 0; - S32 endColumn = 0; - }; - - // Each user function left in the script, as the inliner would put it - // in place: one whose body is `return e;` as its expression, with its - // arguments where its parameters were; any other as a block, each - // parameter a local and its value another, each return but a last one - // a jump. - void standingFunctions(LSLScript* script, const ALSourceMap& inlinedMap, std::vector& out) - { - if (!script->getGlobals()) - { - return; - } - for (LSLASTNode* global : *script->getGlobals()) - { - if (global->getNodeType() != NODE_GLOBAL_FUNCTION) - { - continue; - } - auto* function = static_cast(global); - LSLSymbol* sym = function->getSymbol(); - LSLStatement* body = function->getStatements(); - if (!sym || !body) - { - continue; - } - Standing f; - f.name = sym->getName(); - f.shape.name = f.name.size(); - f.shape.calls = sym->getReferences() - 1; - if (LSLFunctionDec* dec = function->getArguments()) - { - f.shape.params = static_cast(dec->getNumChildren()); - } - const bool value = sym->getType() && sym->getType()->getIType() != LST_NULL; - LSLASTNode* last = nullptr; - for (LSLASTNode* statement = body->getChild(0); statement; statement = statement->getNext()) - { - last = statement; - } - const bool returnsLast = last && last->getNodeType() == NODE_STATEMENT && last->getNodeSubType() == NODE_RETURN_STATEMENT; - const bool expression = value && returnsLast && body->getNumChildren() == 1; - f.shape.locals = expression ? 0 : f.shape.params + (value ? 1 : 0); - boost::unordered_flat_set> strings; - std::vector stack{ body }; - while (!stack.empty()) - { - LSLASTNode* node = stack.back(); - stack.pop_back(); - if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_RETURN_STATEMENT && node != last) - { - ++f.shape.jumps; - } - else if (node->getNodeType() == NODE_CONSTANT && node->getNodeSubType() == NODE_STRING_CONSTANT) - { - strings.insert(static_cast(node)->getValue()); - } - for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) - { - stack.push_back(child); - } - } - f.shape.strings = static_cast(strings.size()); - for (const std::string& text : strings) - { - f.shape.chars += static_cast(text.size()); - } - if (const Tailslide::YYLTYPE* loc = function->getLoc()) - { - f.line = zeroBased(loc->first_line); - f.column = zeroBased(loc->first_column); - f.endLine = zeroBased(loc->last_line); - f.endColumn = zeroBased(loc->last_column); - if (!inlinedMap.empty()) - { - const ALSourceMap::Loc from = inlinedMap.toSource(f.line, f.column); - const ALSourceMap::Loc to = inlinedMap.toSource(f.endLine, f.endColumn); - if (from.found() && to.found()) - { - f.line = from.line; - f.column = from.column; - f.endLine = to.line; - f.endColumn = to.column; - } - } - } - out.push_back(std::move(f)); - } - } - - ALScriptWeight::Target weightTarget(ALLSLOptimizer::Target target) - { - return target == ALLSLOptimizer::Target::LSO ? ALScriptWeight::Target::LSO - : target == ALLSLOptimizer::Target::Luau ? ALScriptWeight::Target::LSLLuau - : ALScriptWeight::Target::Mono; - } - - ALScriptWeight weigh(ALLSLOptimizer::Target target, std::string_view text) - { - switch (weightTarget(target)) - { - case ALScriptWeight::Target::LSO: - return ALScriptWeigh::lso(text); - case ALScriptWeight::Target::LSLLuau: - return ALScriptWeigh::lslLuau(text); - default: - return ALScriptWeigh::mono(text); - } - } - - - // One run over a script: put in place what is to be, then the rounds, - // then the printing, and what was printed checked. The functions it - // leaves standing into `standing`, where that is asked for; a list's - // shapes made where `lists` says so. - ALLSLOptimizer::Result once(std::string_view source, const ALLSLOptimizer::Options& options, std::vector* standing, bool lists = true) - { - using Result = ALLSLOptimizer::Result; - Result result; - result.text = std::string(source); - result.sizeBefore = source.size(); - result.sizeAfter = source.size(); - result.map = ALSourceMap::identity(result.text); - if (!ALLSLService::builtinsLoaded()) - { - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Error; - p.source = ALScriptProblem::Source::Optimizer; - p.message = "the LSL definitions are not loaded, so nothing was optimized"; - p.key = "OptimizerNoDefinitions"; - result.problems.push_back(std::move(p)); - return result; - } - - // The functions called once put in place first, in the text, so that - // what is parsed below is an ordinary script; its map is under the - // printer's. Before the engine is taken for the rest, so that the - // inliner's hold is its only one and it can let go between rounds. - std::string inlined; - ALSourceMap inlinedMap; - size_t spent = 0; - if (options.inlining || !options.constFunctions.empty()) - { - ALLSLInliner::Asked asked; - asked.marked = options.inlineNames; - asked.constant = options.constFunctions; - asked.others = options.inlining; - ALLSLInliner::Result put = ALLSLInliner::run(source, asked, options.visitBudget); - // One budget for the two: what the inliner visited is spent. - spent = put.visited; - if (put.stoppedEarly) - { - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Note; - p.source = ALScriptProblem::Source::Optimizer; - p.key = "InlinerStoppedEarly"; - p.message = "stopped putting functions in place part way: there may be more to put in place"; - result.problems.push_back(std::move(p)); - } - if (put.inlined > 0) - { - inlined = std::move(put.text); - inlinedMap = std::move(put.map); - source = inlined; - for (ALScriptProblem& note : put.notes) - { - result.problems.push_back(std::move(note)); - } - } - } - AL_SCRIPT_ENGINE_HELD; - // What is said from here on is said of the inlined text, and brought - // back to the source on the way out. - const size_t saidOfInlined = result.problems.size(); - const auto bringBack = [&]() { - if (inlinedMap.empty()) - { - return; - } - for (size_t i = saidOfInlined; i < result.problems.size(); ++i) - { - ALScriptProblem& p = result.problems[i]; - const ALSourceMap::Loc from = inlinedMap.toSource(p.line, p.column); - const ALSourceMap::Loc to = inlinedMap.toSource(p.endLine, p.endColumn); - if (from.found()) - { - p.line = from.line; - p.column = from.column; - } - if (to.found()) - { - p.endLine = to.line; - p.endColumn = to.column; - } - } - }; - - ScopedScriptParser parser(nullptr); - LSLScript* script = parser.parseLSLBytes(source.data(), static_cast(source.size())); - if (!script || parser.logger.getErrors()) - { - collectMessages(parser.logger, result.problems); - bringBack(); - result.uncompiled = inlinedMap.empty(); - return result; - } - script->collectSymbols(); - script->determineTypes(); - script->recalculateReferenceData(); - ALLSLArithmetic behavior(&parser.allocator, options.addstrings, options.target); - const auto propagate = [&]() { - ConstantDeterminingVisitor values(&behavior, &parser.allocator); - script->visit(&values); - }; - propagate(); - script->finalPass(); - if (parser.logger.getErrors()) - { - collectMessages(parser.logger, result.problems); - bringBack(); - result.uncompiled = inlinedMap.empty(); - return result; - } - - const ALLSLEffects effects(script); - Ctx ctx; - ctx.allocator = &parser.allocator; - ctx.context = &parser.context; - ctx.target = options.target; - ctx.foldtabs = options.foldtabs; - ctx.effects = &effects; - Report report(result.problems, options.notes); - // Each pass opens the way for the others; round and round until a - // round changes nothing -- or until the run has visited as much as - // its budget allows, since a large script whose passes keep finding - // work would otherwise hold whoever asked for as long as it liked. - // Every walk of the script counted as it is made -- a pass, and the - // references and values found again after a pass that changed - // something -- from what the inliner left of the budget. - size_t visited = spent; - const auto nodes = [&]() { - size_t count = 0; - Gather gather(count); - script->visit(&gather); - return count; - }; - const size_t perWalk = std::max(1, nodes()); - const auto walks = [&](size_t count) { visited += perWalk * count; }; - const auto refresh = [&]() { - script->recalculateReferenceData(); - propagate(); - walks(2); - }; - for (int round = 0; round < 64; ++round) - { - // No round begun that the budget cannot see through its passes. - if (visited + perWalk * 3 > options.visitBudget) - { - result.stoppedEarly = true; - report.note(nullptr, "OptimizerStoppedEarly", "stopped after [1] rounds: there may be more to do", { std::to_string(round) }); - break; - } - // The references and values found again after the folder and the - // simplifier together, not after each: what the folder makes are - // constants, whose values the simplifier reads off them, and what - // counts it leaves behind can only be too high -- which a pass - // takes as a reason to do less, never as leave to do wrong. Dead - // code is looked for with everything found again, since what - // runs is told from the values. - int changes = 0; - if (options.constfold) - { - Folder folder(ctx, report, options); - script->visit(&folder); - walks(1); - Simplifier simplifier(ctx, report, options); - script->visit(&simplifier); - walks(1); - if (folder.changes + simplifier.changes) - { - changes += folder.changes + simplifier.changes; - refresh(); - } - } - if (options.dcr) - { - DeadCode dead(ctx, report, options); - const int removed = dead.run(script); - walks(1); - if (removed) - { - changes += removed; - refresh(); - } - } - if (!changes) - { - break; - } - } - // What is made for size alone, once nothing more will fold. - bool weighLists = false; - if (options.constfold) - { - Shapes values(ctx, report, options, Shapes::Stage::Values); - script->visit(&values); - walks(1); - // A list's shapes on Mono, which references a list's helpers - // once for the whole script -- one to add a string, another to - // add an integer, one for a literal. At each place they are - // smaller (ALLSLCosts), which is all they could cost but for a - // helper nothing in the script called before: where they bring - // one in, and the places they save at are too few to pay for it - // at the most a helper costs, the script is weighed with them - // and without (below), and the smaller kept. - const bool mono = lists && options.target == ALLSLOptimizer::Target::Mono; - ListHelpers had; - if (mono) - { - script->visit(&had); - } - if (lists) - { - Shapes shaped(ctx, report, options, Shapes::Stage::Lists); - if (mono) - { - shaped.setHelpers(&had); - } - script->visit(&shaped); - walks(1); - if (mono && shaped.changes) - { - ListHelpers has; - script->visit(&has); - const ALLSLCosts& costs = ALLSLCosts::of(options.target); - weighLists = shaped.changes * costs.listShapeLeast <= had.freshIn(has) * costs.listHelperMost; - } - } - } - if (standing) - { - standingFunctions(script, inlinedMap, *standing); - } - if (options.shrinknames) - { - shrink(script, parser.context, parser.allocator, report, result, options.target); - } - braceDanglingElses(script, &parser.allocator); - - PrettyPrintOpts opts{}; - opts.mangle_local_names = options.shrinknames; - opts.mangle_func_names = options.shrinknames; - opts.mangle_global_names = options.shrinknames; - opts.show_unmangled = false; - Printer printer(opts, options); - script->visit(&printer); - std::string written = printer.mStream.str(); - // What was written must check as what it was made from did: a script - // the optimizer made unable to compile is the optimizer's fault, and - // the source goes as it was, said so, rather than that. - if (std::optional refused = ALLSLOptimizer::checkWritten(written)) - { - result.problems.clear(); - result.problems.push_back(std::move(*refused)); - return result; - } - result.text = std::move(written); - result.map = printer.map(std::string()); - if (!inlinedMap.empty()) - { - result.map = result.map.composed(inlinedMap); - } - bringBack(); - result.sizeAfter = result.text.size(); - result.optimized = true; - if (weighLists) - { - // Made again without a list's shapes, both weighed, the smaller - // kept with its weight. - std::vector alone; - ALLSLOptimizer::Result plain = once(source, options, standing ? &alone : nullptr, false); - result.weight = weigh(options.target, result.text); - plain.weight = weigh(options.target, plain.text); - if (plain.optimized && plain.weight.compiled && result.weight.compiled && plain.weight.total <= result.weight.total) - { - if (standing) - { - *standing = std::move(alone); - } - return plain; - } - } - return result; - } -} // namespace - -ALLSLOptimizer::Result ALLSLOptimizer::run(std::string_view source, const Options& options) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - const bool byCost = options.inlining && options.inlineByCost; - std::vector standing; - Result kept = once(source, options, byCost ? &standing : nullptr); - // Of the functions left standing, those called from more than one - // place and not marked -- a marked one went wherever it could -- are - // what the cost may put in place. - std::erase_if(standing, [&options](const Standing& f) { - return f.shape.calls < 2 || std::find(options.inlineNames.begin(), options.inlineNames.end(), f.name) != options.inlineNames.end(); - }); - if (!byCost || !kept.optimized || standing.empty()) - { - return kept; - } - if (!kept.weight.compiled) - { - kept.weight = weigh(options.target, kept.text); - } - if (!kept.weight.compiled) - { - return kept; - } - // Each estimated from what it weighs as the run left it. - const ALLSLCosts& costs = ALLSLCosts::of(options.target); - std::vector> estimated; - for (const Standing& f : standing) - { - const auto renamed = kept.renamed.find(f.name); - const std::string shown = renamed == kept.renamed.end() ? f.name : renamed->second; - for (const ALScriptWeight::Part& part : kept.weight.parts) - { - if (part.kind != ALScriptWeight::Part::Kind::Function || part.name != shown) - { - continue; - } - ALLSLCosts::Inlining shape = f.shape; - shape.bytes = static_cast(part.bytes); - shape.name = shown.size(); - estimated.emplace_back(&f, costs.inlined(shape)); - break; - } - } - // Tried, and kept only where it is smaller as the compiler counts it: - // those estimated within what an estimate is good to -- a function near - // nothing either way is often worth it beside the others -- and those - // estimated to save alone, where they are fewer; the smallest kept. - constexpr S32 MARGIN = 16; - size_t last = 0; - std::optional best; - for (const S32 under : { MARGIN, 0 }) - { - Options more = options; - std::vector chosen; - for (const auto& [f, estimate] : estimated) - { - if (estimate < under) - { - more.inlineNames.push_back(f->name); - chosen.push_back(f->name); - } - } - if (chosen.empty() || chosen.size() == last) - { - continue; - } - last = chosen.size(); - std::vector left; - Result tried = once(source, more, &left); - // What the inliner could not take is the same text. - if (!tried.optimized || tried.text == kept.text) - { - continue; - } - if (!tried.weight.compiled) - { - tried.weight = weigh(options.target, tried.text); - } - if (!tried.weight.compiled || tried.weight.total >= (best ? best->weight.total : kept.weight.total)) - { - continue; - } - const size_t saved = kept.weight.total - tried.weight.total; - for (const auto& [f, estimate] : estimated) - { - if (std::find(chosen.begin(), chosen.end(), f->name) == chosen.end() || !options.notes || - std::any_of(left.begin(), left.end(), [f](const Standing& l) { return l.name == f->name; })) - { - continue; - } - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Note; - p.source = ALScriptProblem::Source::Optimizer; - p.key = "InlinerChoseFunction"; - p.line = f->line; - p.column = f->column; - p.endLine = f->endLine; - p.endColumn = f->endColumn; - p.args = { f->name, std::to_string(f->shape.calls), std::to_string(saved), ALScriptWeight::nameOf(weightTarget(options.target)) }; - p.message = ALScriptProblem::fill("put the function [1] in place at its [2] calls, of what put in place so made the code [3] bytes smaller on [4]", - p.args); - tried.problems.push_back(std::move(p)); - } - best = std::move(tried); - } - return best ? std::move(*best) : std::move(kept); -} - -// static -void ALLSLOptimizer::foldGlobals(LSLScript* script, ScriptAllocator* allocator, ScriptContext* context, Target target) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - if (!script || !script->getGlobals()) - { - return; - } - Options options; - options.target = target; - options.addstrings = true; - options.notes = false; - ALLSLArithmetic behavior(allocator, options.addstrings, target); - Ctx ctx; - ctx.allocator = allocator; - ctx.context = context; - ctx.target = target; - ALScriptProblems unsaid; - Report report(unsaid, false); - script->recalculateReferenceData(); - // A value folded may be what another is folded from: round and round, - // as a run does, until a round folds nothing. - for (int round = 0; round < 16; ++round) - { - ConstantDeterminingVisitor values(&behavior, allocator); - script->visit(&values); - Folder folder(ctx, report, options); - for (LSLASTNode* global : *script->getGlobals()) - { - if (global->getNodeType() == NODE_GLOBAL_VARIABLE) - { - global->visit(&folder); - } - } - if (!folder.changes) - { - break; - } - script->recalculateReferenceData(); - } -} - -// static -std::optional ALLSLOptimizer::checkWritten(std::string_view written) -{ - AL_SCRIPT_ENGINE_HELD; - ScopedScriptParser check(nullptr); - const std::string text(written); - LSLScript* again = check.parseLSLBytes(text.data(), static_cast(text.size())); - if (again && !check.logger.getErrors()) - { - again->collectSymbols(); - again->determineTypes(); - } - if (again && !check.logger.getErrors()) - { - return std::nullopt; - } - ALScriptProblems said; - collectMessages(check.logger, said); - ALScriptProblem p; - p.severity = ALScriptProblem::Severity::Warning; - p.source = ALScriptProblem::Source::Optimizer; - p.key = "OptimizerWroteUncompilable"; - p.args = { said.empty() ? std::string() : said.front().message }; - p.message = ALScriptProblem::fill("not optimized: what the optimizer made of this script did not compile ([1]), so it goes as it was; " - "please report it", - p.args); - return p; -} diff --git a/indra/alscript/almessagemap.cpp b/indra/alscript/core/almessagemap.cpp similarity index 100% rename from indra/alscript/almessagemap.cpp rename to indra/alscript/core/almessagemap.cpp diff --git a/indra/alscript/almessagemap.h b/indra/alscript/core/almessagemap.h similarity index 100% rename from indra/alscript/almessagemap.h rename to indra/alscript/core/almessagemap.h diff --git a/indra/alscript/alnotecardformat.cpp b/indra/alscript/core/alnotecardformat.cpp similarity index 100% rename from indra/alscript/alnotecardformat.cpp rename to indra/alscript/core/alnotecardformat.cpp diff --git a/indra/alscript/alnotecardformat.h b/indra/alscript/core/alnotecardformat.h similarity index 100% rename from indra/alscript/alnotecardformat.h rename to indra/alscript/core/alnotecardformat.h diff --git a/indra/alscript/alscriptengine.h b/indra/alscript/core/alscriptengine.h similarity index 100% rename from indra/alscript/alscriptengine.h rename to indra/alscript/core/alscriptengine.h diff --git a/indra/alscript/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp similarity index 100% rename from indra/alscript/alscriptformatter.cpp rename to indra/alscript/core/alscriptformatter.cpp diff --git a/indra/alscript/alscriptformatter.h b/indra/alscript/core/alscriptformatter.h similarity index 100% rename from indra/alscript/alscriptformatter.h rename to indra/alscript/core/alscriptformatter.h diff --git a/indra/alscript/alscriptlexicon.cpp b/indra/alscript/core/alscriptlexicon.cpp similarity index 100% rename from indra/alscript/alscriptlexicon.cpp rename to indra/alscript/core/alscriptlexicon.cpp diff --git a/indra/alscript/alscriptlexicon.h b/indra/alscript/core/alscriptlexicon.h similarity index 100% rename from indra/alscript/alscriptlexicon.h rename to indra/alscript/core/alscriptlexicon.h diff --git a/indra/alscript/alscriptmessages.cpp b/indra/alscript/core/alscriptmessages.cpp similarity index 100% rename from indra/alscript/alscriptmessages.cpp rename to indra/alscript/core/alscriptmessages.cpp diff --git a/indra/alscript/alscriptmessages.h b/indra/alscript/core/alscriptmessages.h similarity index 100% rename from indra/alscript/alscriptmessages.h rename to indra/alscript/core/alscriptmessages.h diff --git a/indra/alscript/alscriptnameindex.cpp b/indra/alscript/core/alscriptnameindex.cpp similarity index 100% rename from indra/alscript/alscriptnameindex.cpp rename to indra/alscript/core/alscriptnameindex.cpp diff --git a/indra/alscript/alscriptnameindex.h b/indra/alscript/core/alscriptnameindex.h similarity index 100% rename from indra/alscript/alscriptnameindex.h rename to indra/alscript/core/alscriptnameindex.h diff --git a/indra/alscript/alscriptproblem.h b/indra/alscript/core/alscriptproblem.h similarity index 100% rename from indra/alscript/alscriptproblem.h rename to indra/alscript/core/alscriptproblem.h diff --git a/indra/alscript/alscriptspan.h b/indra/alscript/core/alscriptspan.h similarity index 100% rename from indra/alscript/alscriptspan.h rename to indra/alscript/core/alscriptspan.h diff --git a/indra/alscript/alscriptstack.cpp b/indra/alscript/core/alscriptstack.cpp similarity index 100% rename from indra/alscript/alscriptstack.cpp rename to indra/alscript/core/alscriptstack.cpp diff --git a/indra/alscript/alscriptstack.h b/indra/alscript/core/alscriptstack.h similarity index 100% rename from indra/alscript/alscriptstack.h rename to indra/alscript/core/alscriptstack.h diff --git a/indra/alscript/alscriptsymbol.h b/indra/alscript/core/alscriptsymbol.h similarity index 100% rename from indra/alscript/alscriptsymbol.h rename to indra/alscript/core/alscriptsymbol.h diff --git a/indra/alscript/alscripttempfiles.cpp b/indra/alscript/core/alscripttempfiles.cpp similarity index 100% rename from indra/alscript/alscripttempfiles.cpp rename to indra/alscript/core/alscripttempfiles.cpp diff --git a/indra/alscript/alscripttempfiles.h b/indra/alscript/core/alscripttempfiles.h similarity index 100% rename from indra/alscript/alscripttempfiles.h rename to indra/alscript/core/alscripttempfiles.h diff --git a/indra/alscript/alscriptweight.cpp b/indra/alscript/core/alscriptweight.cpp similarity index 87% rename from indra/alscript/alscriptweight.cpp rename to indra/alscript/core/alscriptweight.cpp index dd845529751..98af8140b02 100644 --- a/indra/alscript/alscriptweight.cpp +++ b/indra/alscript/core/alscriptweight.cpp @@ -1422,61 +1422,99 @@ namespace }; } -namespace ALScriptWeigh +namespace { - ALScriptWeight mono(std::string_view source) + // An assembly's text a line to an instruction, as Tailslide writes it + // and monoOf() reads it: LL's compiler writes `cil managed` on a line of + // its own, and runs the instruction after a print onto the call's line. + // Each instruction keeps the form it was written in, and Tailslide's + // own text comes through as it was. + std::string instructionLines(std::string_view text) { - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - ALScriptWeight weight; - weight.target = ALScriptWeight::Target::Mono; - weight.limit = ALScriptWeight::limitOf(weight.target); - weight.estimate = true; - if (!ALLSLService::builtinsLoaded()) - { - weight.error = "the LSL builtins are not loaded"; - return weight; + std::vector lines; + const auto add = [&lines](std::string line) { + const size_t lead = line.find_first_not_of(" \t\r"); + line = lead == std::string::npos ? std::string() : line.substr(lead, line.find_last_not_of(" \t\r") - lead + 1); + if (line == "cil managed" && !lines.empty()) + { + lines.back() += " cil managed"; + return; + } + lines.push_back(std::move(line)); + }; + for (size_t from = 0; from <= text.size();) + { + const size_t cut = std::min(text.find('\n', from), text.size()); + std::string line(text.substr(from, cut - from)); + from = cut + 1; + // What follows a call's signature on its line, an instruction of + // its own. + const size_t lead = line.find_first_not_of(" \t"); + const bool call = lead != std::string::npos && (line.compare(lead, 5, "call ") == 0 || line.compare(lead, 9, "callvirt ") == 0 || + line.compare(lead, 7, "newobj ") == 0); + const size_t colons = call ? line.find("::") : std::string::npos; + if (colons == std::string::npos) + { + add(std::move(line)); + continue; + } + const size_t name = line.find("::") + 2; + size_t close = line.find('(', name); + for (S32 depth = 0; close != std::string::npos && close < line.size(); ++close) + { + depth += line[close] == '(' ? 1 : line[close] == ')' ? -1 : 0; + if (depth == 0) + { + break; + } + } + if (close != std::string::npos && close + 1 < line.size() && line.find_first_not_of(" \t\r", close + 1) != std::string::npos) + { + std::string rest = line.substr(close + 1); + line.erase(close + 1); + add(std::move(line)); + add(std::move(rest)); + continue; + } + add(std::move(line)); } - AL_SCRIPT_ENGINE_HELD; - Tailslide::ScopedScriptParser parser(nullptr); - const std::string text(source); - Tailslide::LSLScript* script = parser.parseLSLBytes(text.data(), static_cast(text.size())); - if (!script) + std::string out; + for (const std::string& line : lines) { - weight.error = "it does not parse"; - return weight; + out += line + "\n"; } - script->collectSymbols(); - script->determineTypes(); - script->recalculateReferenceData(); - script->propagateValues(); - script->finalPass(); - script->validateGlobals(true); - script->checkSymbols(); - if (parser.logger.getErrors()) - { - weight.error = "it has errors"; - return weight; - } - Places places; - script->visit(&places); - LinedMono compiler(&parser.allocator); - script->visit(&compiler); + return out; + } + // An assembly's text sized as mono() sizes Tailslide's: a method at a + // time, over the base. `places` names each method as the script does, + // where its tree is to hand; `at_lines` gives the text's places the + // source lines they were written for. Without them, each method is a + // function of the assembly's name, and no line is anybody's. + ALScriptWeight monoOf(std::string_view text, const Places* places, const std::vector>* at_lines) + { + ALScriptWeight weight; + weight.target = ALScriptWeight::Target::Mono; + weight.limit = ALScriptWeight::limitOf(weight.target); + weight.estimate = true; // A method at a time: its header, its code, its locals, and what // declaring it takes; with the fields, the strings and what it calls // over the whole. Each instruction, with its string, is the line it // was written for as well. - std::istringstream cil(compiler.mCIL.str()); + // Another compiler's text a line to an instruction; Tailslide's own + // as it is, where the marks count its bytes. + std::istringstream cil{ places || at_lines ? std::string(text) : instructionLines(text) }; std::set referenced; std::set strings; - size_t shared = MONO_BASE_BYTES; + size_t shared = ALScriptWeigh::MONO_BASE_BYTES; ALScriptWeight::Part* method = nullptr; // Heavy strings, parts of their own once every method is in. std::vector heavy; ALScriptWeight::Part globals; globals.kind = ALScriptWeight::Part::Kind::Frame; globals.name = "globals"; - const std::vector>& marks = compiler.marks.marks(); + const std::vector> none; + const std::vector>& marks = at_lines ? *at_lines : none; size_t mark = 0; size_t next = 0; std::map by_line; @@ -1518,14 +1556,19 @@ namespace ALScriptWeigh globals.bytes += head; continue; } - const auto known = places.methods.find(name); + const auto known = places ? places->methods.find(name) : decltype(places->methods.find(name)){}; + const bool found = places && known != places->methods.end(); ALScriptWeight::Part part; - part.kind = known != places.methods.end() && !known->second.second.second.empty() ? ALScriptWeight::Part::Kind::Handler - : ALScriptWeight::Part::Kind::Function; - part.name = known != places.methods.end() ? known->second.second.first : name; - part.within = known != places.methods.end() ? known->second.second.second : std::string(); + part.kind = found && !known->second.second.second.empty() ? ALScriptWeight::Part::Kind::Handler + : ALScriptWeight::Part::Kind::Function; + // Without the tree, a function by its name in the script: + // the assembly's, quoted and after a g. + part.name = found ? known->second.second.first + : name.size() > 3 && name.front() == '\'' && name[1] == 'g' ? name.substr(2, name.size() - 3) + : name; + part.within = found ? known->second.second.second : std::string(); part.bytes = head; - if (known != places.methods.end()) + if (found) { if (const Tailslide::YYLTYPE* at = known->second.first->getLoc(); at && at->first_line > 0) { @@ -1579,14 +1622,36 @@ namespace ALScriptWeigh { by_line[source] += bytes; } - if ((op == "call" || op == "callvirt" || op == "newobj" || op == "ldfld" || op == "stfld" || op == "ldflda") && - referenced.insert(rest).second) + if (op == "call" || op == "callvirt" || op == "newobj" || op == "ldfld" || op == "stfld" || op == "ldflda") { - // What it names outside itself, once however often: a row - // with a name and a signature. - const size_t colons = rest.find("::"); - const size_t paren = rest.find('(', colons == std::string::npos ? 0 : colons); - shared += 6 + (colons == std::string::npos ? 8 : (paren == std::string::npos ? rest.size() : paren) - colons) + 8; + // What it names outside itself, once however often, as the + // metadata holds it: a row with a name and a signature, the + // name without the quotes the assembler reads it by, and the + // member the same whether its owner is written `class` or + // `valuetype` or bare. + std::string member = rest; + for (const char* spelt : { "class [", "valuetype [" }) + { + for (size_t at; (at = member.find(spelt)) != std::string::npos;) + { + member.erase(at, strlen(spelt) - 1); + } + } + const size_t colons = member.find("::"); + if (colons != std::string::npos && colons + 2 < member.size() && member[colons + 2] == '\'') + { + const size_t close = member.find('\'', colons + 3); + if (close != std::string::npos) + { + member.erase(close, 1); + member.erase(colons + 2, 1); + } + } + if (referenced.insert(member).second) + { + const size_t paren = member.find('(', colons == std::string::npos ? 0 : colons); + shared += 6 + (colons == std::string::npos ? 8 : (paren == std::string::npos ? member.size() : paren) - colons) + 8; + } } } weight.parts.insert(weight.parts.begin(), globals); @@ -1605,6 +1670,124 @@ namespace ALScriptWeigh } } +namespace ALScriptWeigh +{ + ALScriptWeight mono(std::string_view source) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + ALScriptWeight weight; + weight.target = ALScriptWeight::Target::Mono; + weight.limit = ALScriptWeight::limitOf(weight.target); + weight.estimate = true; + if (!ALLSLService::builtinsLoaded()) + { + weight.error = "the LSL builtins are not loaded"; + return weight; + } + AL_SCRIPT_ENGINE_HELD; + Tailslide::ScopedScriptParser parser(nullptr); + const std::string text(source); + Tailslide::LSLScript* script = parser.parseLSLBytes(text.data(), static_cast(text.size())); + if (!script) + { + weight.error = "it does not parse"; + return weight; + } + script->collectSymbols(); + script->determineTypes(); + script->recalculateReferenceData(); + script->propagateValues(); + script->finalPass(); + script->validateGlobals(true); + script->checkSymbols(); + if (parser.logger.getErrors()) + { + weight.error = "it has errors"; + return weight; + } + Places places; + script->visit(&places); + LinedMono compiler(&parser.allocator); + script->visit(&compiler); + return monoOf(compiler.mCIL.str(), &places, &compiler.marks.marks()); + } + + ALScriptWeight monoOfCIL(std::string_view cil) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + return monoOf(cil, nullptr, nullptr); + } + + namespace + { + // Parsed and through the passes Tailslide's own tool runs before it + // compiles, but its optimizer, as lso() and mono() run them; null + // with why where it does not get that far. + Tailslide::LSLScript* readied(Tailslide::ScopedScriptParser& parser, const std::string& text, bool mono, std::string& error) + { + if (!ALLSLService::builtinsLoaded()) + { + error = "the LSL builtins are not loaded"; + return nullptr; + } + Tailslide::LSLScript* script = parser.parseLSLBytes(text.data(), static_cast(text.size())); + if (!script) + { + error = "it does not parse"; + return nullptr; + } + script->collectSymbols(); + script->determineTypes(); + script->recalculateReferenceData(); + script->propagateValues(); + script->finalPass(); + script->validateGlobals(mono); + script->checkSymbols(); + if (parser.logger.getErrors()) + { + error = "it has errors"; + return nullptr; + } + return script; + } + } + + bool tailslideLSO(std::string_view source, std::vector& image, std::string& error) + { + AL_SCRIPT_ENGINE_HELD; + Tailslide::ScopedScriptParser parser(nullptr); + Tailslide::LSLScript* script = readied(parser, std::string(source), false, error); + if (!script) + { + return false; + } + Tailslide::LSOScriptCompiler compiler(&parser.allocator); + script->visit(&compiler); + if (parser.logger.getErrors()) + { + error = "it does not fit in 16 KB"; + return false; + } + image.assign(compiler.mScriptBS.data(), compiler.mScriptBS.data() + compiler.mScriptBS.size()); + return true; + } + + bool tailslideCIL(std::string_view source, std::string& cil, std::string& error) + { + AL_SCRIPT_ENGINE_HELD; + Tailslide::ScopedScriptParser parser(nullptr); + Tailslide::LSLScript* script = readied(parser, std::string(source), true, error); + if (!script) + { + return false; + } + Tailslide::MonoScriptCompiler compiler(&parser.allocator); + script->visit(&compiler); + cil = compiler.mCIL.str(); + return true; + } +} + namespace { void nameLSLParts(ALScriptWeight& weight, std::string_view source) diff --git a/indra/alscript/alscriptweight.h b/indra/alscript/core/alscriptweight.h similarity index 91% rename from indra/alscript/alscriptweight.h rename to indra/alscript/core/alscriptweight.h index 0edd72e9ad7..7cf6a652ec8 100644 --- a/indra/alscript/alscriptweight.h +++ b/indra/alscript/core/alscriptweight.h @@ -164,4 +164,14 @@ namespace ALScriptWeigh // counts against 64 KB is the runtime's business rather than the IL's. // As lso() for the builtins and the lock. ALScriptWeight mono(std::string_view source); + // Any assembly's text sized as mono() sizes Tailslide's -- LL's + // compiler's, to hold the two against each other -- each method a part + // by the assembly's name, and no line anybody's. + ALScriptWeight monoOfCIL(std::string_view cil); + + // What Tailslide makes of a script, as lso() and mono() have it before + // they weigh it: the 16 KB LSO image, and the CIL text. False, with why, + // where it makes nothing. As lso() for the builtins and the lock. + bool tailslideLSO(std::string_view source, std::vector& image, std::string& error); + bool tailslideCIL(std::string_view source, std::string& cil, std::string& error); } diff --git a/indra/alscript/allsllintpass.cpp b/indra/alscript/lint/allsllintpass.cpp similarity index 89% rename from indra/alscript/allsllintpass.cpp rename to indra/alscript/lint/allsllintpass.cpp index 0bca64f9632..cc3fe385f66 100644 --- a/indra/alscript/allsllintpass.cpp +++ b/indra/alscript/lint/allsllintpass.cpp @@ -36,6 +36,8 @@ #include #include +#include +#include #include #include #include @@ -851,6 +853,108 @@ namespace }); }); } + + // SlIntegerPast32Bits: a whole number written past 0xFFFFFFFF, which the + // grid's compilers read as -1 -- their 32-bit hosts' strtoul stops + // there -- and as 1 with a minus before it. Read from the text, past + // strings, comments and floats, since Tailslide's tree keeps only what + // it read. A warning: no script means what it says. + void integersPast32Bits(const Text& text, ALScriptProblems& out) + { + const std::string_view source = text.source(); + for (size_t at = 0; at < source.size();) + { + const char c = source[at]; + if (c == '"') + { + for (++at; at < source.size() && source[at] != '"'; ++at) + { + at += source[at] == '\\'; + } + ++at; + continue; + } + if (c == '/' && at + 1 < source.size() && source[at + 1] == '/') + { + at = std::min(source.find('\n', at), source.size()); + continue; + } + if (c == '/' && at + 1 < source.size() && source[at + 1] == '*') + { + const size_t end = source.find("*/", at + 2); + at = end == std::string_view::npos ? source.size() : end + 2; + continue; + } + const bool starts = std::isdigit(static_cast(c)) && + (at == 0 || !(ALScriptLexicon::isNameByte(source[at - 1]) || source[at - 1] == '.')); + if (!starts) + { + ++at; + continue; + } + const bool hex = c == '0' && at + 2 < source.size() && (source[at + 1] == 'x' || source[at + 1] == 'X') && + std::isxdigit(static_cast(source[at + 2])); + size_t end = hex ? at + 2 : at; + while (end < source.size() && (hex ? std::isxdigit(static_cast(source[end])) : std::isdigit(static_cast(source[end])))) + { + ++end; + } + // A float, or a name that starts with digits: not a whole number. + if (end < source.size() && (source[end] == '.' || ALScriptLexicon::isNameByte(source[end]))) + { + while (end < source.size() && (source[end] == '.' || ALScriptLexicon::isNameByte(source[end]))) + { + ++end; + } + at = end; + continue; + } + const std::string written(source.substr(at, end - at)); + errno = 0; + const unsigned long long value = std::strtoull(written.c_str(), nullptr, hex ? 16 : 10); + if (errno == ERANGE || value > 0xFFFFFFFFull) + { + // A minus before it that is not taking it from something: + // -1 made 1. + size_t minus = at; + while (minus > 0 && (source[minus - 1] == ' ' || source[minus - 1] == '\t')) + { + --minus; + } + size_t before = minus > 0 && source[minus - 1] == '-' ? minus - 1 : std::string_view::npos; + if (before != std::string_view::npos) + { + size_t prior = before; + while (prior > 0 && std::isspace(static_cast(source[prior - 1]))) + { + --prior; + } + const char last = prior > 0 ? source[prior - 1] : '\0'; + if (ALScriptLexicon::isNameByte(last) || last == ')' || last == ']' || last == '.') + { + before = std::string_view::npos; + } + } + const size_t from = before != std::string_view::npos ? before : at; + const auto [line, column] = text.place(from); + const auto [end_line, end_col] = text.place(end); + ALScriptProblem p; + p.severity = ALScriptLintPass::rule("SlIntegerPast32Bits")->severity; + p.source = ALScriptProblem::Source::Lint; + p.line = line; + p.column = column; + p.endLine = end_line; + p.endColumn = end_col; + p.code = "SlIntegerPast32Bits"; + p.key = "LSLSlIntegerPast32Bits"; + p.args = { std::string(source.substr(from, end - from)), before != std::string_view::npos ? "1" : "-1" }; + p.message = ALScriptProblem::fill("LSL reads [1] as [2]: past 0xFFFFFFFF, the most its 32 bits hold, a number stops there, which is -1", + p.args); + out.push_back(std::move(p)); + } + at = end; + } + } } // static @@ -868,4 +972,5 @@ void ALLSLLintPass::check(std::string_view source, LSLScript* script, ALScriptPr costlyEvents(script, out); stringBuilds(script, out); repeatedCalls(text, script, effects, out); + integersPast32Bits(text, out); } diff --git a/indra/alscript/allsllintpass.h b/indra/alscript/lint/allsllintpass.h similarity index 100% rename from indra/alscript/allsllintpass.h rename to indra/alscript/lint/allsllintpass.h diff --git a/indra/alscript/alscriptfixes.cpp b/indra/alscript/lint/alscriptfixes.cpp similarity index 100% rename from indra/alscript/alscriptfixes.cpp rename to indra/alscript/lint/alscriptfixes.cpp diff --git a/indra/alscript/alscriptfixes.h b/indra/alscript/lint/alscriptfixes.h similarity index 100% rename from indra/alscript/alscriptfixes.h rename to indra/alscript/lint/alscriptfixes.h diff --git a/indra/alscript/alscriptlintpass.cpp b/indra/alscript/lint/alscriptlintpass.cpp similarity index 99% rename from indra/alscript/alscriptlintpass.cpp rename to indra/alscript/lint/alscriptlintpass.cpp index 4fc50a74703..0308a6d57d1 100644 --- a/indra/alscript/alscriptlintpass.cpp +++ b/indra/alscript/lint/alscriptlintpass.cpp @@ -112,6 +112,9 @@ namespace // for i = 1, n do ... i = j ... end: LSL's for went on from what i was // set to, Luau's makes i afresh each time round. { "SlForIndexAssign", Rule::SLua, Severity::Warning, true, false, nullptr, true }, + // integer x = 4294967296: past 0xFFFFFFFF, which the grid's compilers + // read as -1. + { "SlIntegerPast32Bits", Rule::LSL, Severity::Warning, true, false, nullptr }, }; const ALScriptLintPass::PrimParams PRIM_PARAMS[] = { diff --git a/indra/alscript/alscriptlintpass.h b/indra/alscript/lint/alscriptlintpass.h similarity index 100% rename from indra/alscript/alscriptlintpass.h rename to indra/alscript/lint/alscriptlintpass.h diff --git a/indra/alscript/alselenefilters.cpp b/indra/alscript/lint/alselenefilters.cpp similarity index 100% rename from indra/alscript/alselenefilters.cpp rename to indra/alscript/lint/alselenefilters.cpp diff --git a/indra/alscript/alselenefilters.h b/indra/alscript/lint/alselenefilters.h similarity index 100% rename from indra/alscript/alselenefilters.h rename to indra/alscript/lint/alselenefilters.h diff --git a/indra/alscript/allslconsts.cpp b/indra/alscript/lsl/allslconsts.cpp similarity index 100% rename from indra/alscript/allslconsts.cpp rename to indra/alscript/lsl/allslconsts.cpp diff --git a/indra/alscript/allslconsts.h b/indra/alscript/lsl/allslconsts.h similarity index 100% rename from indra/alscript/allslconsts.h rename to indra/alscript/lsl/allslconsts.h diff --git a/indra/alscript/allsleffects.cpp b/indra/alscript/lsl/allsleffects.cpp similarity index 100% rename from indra/alscript/allsleffects.cpp rename to indra/alscript/lsl/allsleffects.cpp diff --git a/indra/alscript/allsleffects.h b/indra/alscript/lsl/allsleffects.h similarity index 100% rename from indra/alscript/allsleffects.h rename to indra/alscript/lsl/allsleffects.h diff --git a/indra/alscript/allsleventtext.inc b/indra/alscript/lsl/allsleventtext.inc similarity index 100% rename from indra/alscript/allsleventtext.inc rename to indra/alscript/lsl/allsleventtext.inc diff --git a/indra/alscript/allslexports.cpp b/indra/alscript/lsl/allslexports.cpp similarity index 100% rename from indra/alscript/allslexports.cpp rename to indra/alscript/lsl/allslexports.cpp diff --git a/indra/alscript/allslexports.h b/indra/alscript/lsl/allslexports.h similarity index 100% rename from indra/alscript/allslexports.h rename to indra/alscript/lsl/allslexports.h diff --git a/indra/alscript/allslservice.cpp b/indra/alscript/lsl/allslservice.cpp similarity index 99% rename from indra/alscript/allslservice.cpp rename to indra/alscript/lsl/allslservice.cpp index 6ca73c44694..fc94e62e7f4 100644 --- a/indra/alscript/allslservice.cpp +++ b/indra/alscript/lsl/allslservice.cpp @@ -49,6 +49,7 @@ #include #include #include +#include namespace { @@ -1354,13 +1355,7 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) { return true; } - const std::string more = path + ".added"; - { - llofstream out(more, std::ios::out | std::ios::binary | std::ios::trunc); - out << added; - } - Tailslide::tailslide_init_builtins(more.c_str()); - LLFile::remove(more); + Tailslide::tailslide_init_builtins_from_data(added.c_str()); // An event new here numbered as the runtime numbers it, or after // every event there already was: those numbered before keep theirs. int last = 0; @@ -1388,19 +1383,26 @@ bool ALLSLService::loadBuiltins(const std::string& path, std::string& error) mImpl->builtins = true; return true; } - // A file that is not there has nothing loaded, which is said here: the - // file is opened first. A line Tailslide cannot read -- a type it does - // not know, a constant it cannot parse, blanks -- it says so on stderr - // and skips, since the tailslide port's builtins-skip-unreadable.patch; - // before that it ended the process, over a file the grid sends. - LLFILE* file = LLFile::fopen(path, LLFILE_MODE("rb")); - if (!file) + // Read here and handed over as text: a path Tailslide opened itself + // went through the narrow fopen(), which Windows reads in the ANSI code + // page. A file that is not there has nothing loaded, which is said here. + // A line Tailslide cannot read -- a type it does not know, a constant it + // cannot parse -- it says so on stderr and skips, since the tailslide + // port's builtins-skip-unreadable.patch; before that it ended the + // process, over a file the grid sends. + std::string text; { - error = "cannot open " + path; - return false; + llifstream in(path, std::ios::in | std::ios::binary); + if (!in.is_open()) + { + error = "cannot open " + path; + return false; + } + std::stringstream all; + all << in.rdbuf(); + text = all.str(); } - fclose(file); - Tailslide::tailslide_init_builtins(path.c_str()); + Tailslide::tailslide_init_builtins_from_data(text.c_str()); // Every event numbered as the runtime numbers it (Luau::lslEventIndex): // the grid's definitions list them in an order of their own, and what an // event's number goes into -- LSO's handled-events bits, the dispatch of diff --git a/indra/alscript/allslservice.h b/indra/alscript/lsl/allslservice.h similarity index 100% rename from indra/alscript/allslservice.h rename to indra/alscript/lsl/allslservice.h diff --git a/indra/alscript/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp similarity index 79% rename from indra/alscript/allsltoslua.cpp rename to indra/alscript/lsl/allsltoslua.cpp index 4fb8f81e453..59223c42c8b 100644 --- a/indra/alscript/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -41,7 +41,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -250,7 +253,7 @@ namespace case LST_INTEGER: case LST_FLOATINGPOINT: return "0"; case LST_STRING: return "\"\""; - case LST_KEY: return "uuid(\"\")"; + case LST_KEY: return "NULL_KEY"; case LST_VECTOR: return "ZERO_VECTOR"; case LST_QUATERNION: return "ZERO_ROTATION"; case LST_LIST: return "{}"; @@ -309,10 +312,14 @@ namespace class Writer { public: - Writer(LSLScript* script, const ALLSLToSLua::Options& options) : mScript(script), mOptions(options), mEffects(script) {} + Writer(LSLScript* script, std::string_view source, const ALLSLToSLua::Options& options) + : mScript(script), mSource(source), mOptions(options), mEffects(script) + { + } std::string write(); ALScriptProblems& notes() { return mNotes; } + const std::vector>& anchors() const { return mAnchors; } private: // --- where the two languages differ ----------------------------------------- @@ -338,6 +345,62 @@ namespace // --- the text --------------------------------------------------------------- void line(const std::string& text); + + // --- the script's own comments ----------------------------------------------- + + // Where a comment begins, or a node begins or ends, by line and + // column from one; a node's end is one past it. + struct Pos + { + S32 line = 0; + S32 column = 0; + bool operator<(const Pos& o) const { return line != o.line ? line < o.line : column < o.column; } + }; + struct Comment + { + Pos at; + S32 endLine = 0; + // As SLua writes it: a block comment's lines past its first as + // they were. + std::string text; + // A blank line over it in the LSL, which it keeps. + bool apart = false; + bool written = false; + }; + // Each comment in the LSL, given to the node it is written over -- + // a global, a function, a state, a handler, a statement -- or to + // the end of what holds it, where nothing after it in there does. + void findComments(); + void place(size_t k, const std::vector& children, LSLASTNode* holder); + bool placeInside(size_t k, LSLASTNode* node); + void placeIn(size_t k, LSLASTNode* compound); + // Those given to a node, written over it; those at a holder's end, + // with any of what it held that nothing wrote, at its end. + void commentsBefore(LSLASTNode* node); + void commentsAtEnd(LSLASTNode* holder); + void writeComment(Comment& c); + // Those after a node on the line it ends, where it is all on one + // line or no if or loop: at the end of the first line of code + // written of it from `from` in the text, as the LSL had them; or + // where nothing was, on a line of their own. + void commentsAfter(LSLASTNode* node, size_t from); + // How long what commentsAfter put after each node's line was. + boost::unordered_flat_map mTrailed; + + // Declarations on lines one after another that the LSL lined up -- + // each one line, given a value, and their = at one column, or the + // comments after them -- lined up again, as what each is written as + // is as long as it is: each as it is written from `from`, the run + // ended by anything else. + void lined(LSLASTNode* node, size_t from); + void lineUp(); + struct Lined + { + LSLASTNode* node; + LSLIdentifier* id; + size_t at; + }; + std::vector mRun; std::string indent() const { return std::string(static_cast(mDepth) * 4, ' '); } // --- expressions ------------------------------------------------------------ @@ -347,12 +410,44 @@ namespace Expr value(LSLExpression* e); // As a boolean, as LSL reads a value in a condition. Expr condition(LSLExpression* e); + // An & asked only whether it is nought, as bit32.btest asks it: an + // & of &s one call of them all. None for anything else. + std::optional bitTest(LSLExpression* e); // As a value of another LSL type, where LSL converts on its own. Expr coerced(LSLExpression* e, LSLIType to); Expr constant(LSLConstant* c); + // How the LSL wrote a number in hexadecimal, on the line Tailslide + // puts its constant at or the one before: where it did, and wrote + // it no other way there. Tailslide's constants are where the + // parser's next token was, not where they were. + std::optional hexAsWritten(LSLConstant* c, int v); + // The numbers written on a constant's line, or the one before where + // its own has none: each as written, and its value. + std::vector> numbersNear(LSLConstant* c); + // The variables declared hexadecimal past 0x7FFFFFFF and never set + // that nothing but bit32 reads, which takes 0xF0000000 as the + // -268435456 LSL wrapped it to: their declarations, whose + // hexadecimal stays. + void findBitsOnly(); + boost::unordered_flat_set mBitsOnly; + // Where each of the LSL's lines starts, from the first. + const std::vector& lineStarts(); + std::vector mLineStarts; Expr lvalue(LSLLValueExpression* e); Expr call(LSLFunctionExpression* e); Expr binary(LSLBinaryExpression* e); + // A bit32 call as LSL's signed integer, but where `same` says its + // answer is that already, or nothing asks for more. + Expr signedUnless(bool same, const std::string& call, LSLASTNode* at); + // What a scripter from LSL is told once of each, over the first + // place it is written: hexadecimal past 0x7FFFFFFF, which SLua + // reads as another number than LSL did; and bit32.s32, which makes + // bit32's answer LSL's. + bool mSaidHexSign = false; + bool mSaidSigned = false; + // A number not below nought: as nonNegative says, or a global the + // script never sets, declared one. + bool notBelowZero(LSLExpression* e) const; Expr unary(LSLUnaryExpression* e); Expr typecast(LSLTypecastExpression* e); // An assignment or a step inside an expression, which SLua makes a @@ -398,7 +493,12 @@ namespace // --- statements ------------------------------------------------------------- void statement(LSLASTNode* s, bool last); + void statementBody(LSLASTNode* s, bool last); void block(LSLASTNode* s); + // An if written from `at` in the text, `lines` lines down, its + // anchors from `anchors`, put on one line where it is one statement + // and nothing said over it: if c then s end. + void onOneLine(size_t at, S32 lines, size_t anchors); // An expression standing as a statement: an assignment, a step, a // call. void effect(LSLExpression* e); @@ -418,8 +518,13 @@ namespace LSLExpression* limit = nullptr; LSLOperator check = OP_NONE; int step = 0; + // A step by a constant's name, which the step is written by. + LSLExpression* by = nullptr; }; std::optional counting(LSLForStatement* f) const; + // A whole number written out, or the name of one that never + // changes: a global nothing sets, given one, or one of LSL's own. + bool steadyWhole(LSLExpression* e, int& v) const; // Whether an expression reads the same on every turn of a loop: // nothing it reads is set in the loop, and it calls only what the // definitions call pure. @@ -601,10 +706,20 @@ namespace void helpers(std::string& out); LSLScript* mScript; + std::string_view mSource; const ALLSLToSLua::Options& mOptions; // What each part of the script may change, for LSL's order. ALLSLEffects mEffects; std::string mText; + // The LSL's lines beside the SLua's first lines made of them, the + // SLua's counted in mText until the head goes over it; the LSL line + // of the node whose first line is written next, once its comments + // are; and how many lines mText was counted to hold, and to where. + std::vector> mAnchors; + std::optional mAnchorPending; + size_t mCounted = 0; + S32 mCountedLines = 0; + S32 linesWritten(); int mDepth = 0; std::vector mPending; boost::unordered_flat_set mOnce; @@ -612,6 +727,17 @@ namespace boost::unordered_flat_map mNames; boost::unordered_flat_set mTaken; // What the text needs of its own, defined once over it. + // The script's comments; those over its first global, written at the + // top; and by what each is given to. + std::vector mComments; + std::vector mLeadComments; + boost::unordered_flat_map> mCommentsBefore; + boost::unordered_flat_map> mCommentsAfter; + boost::unordered_flat_map> mCommentsWithin; + // The globals the script never sets, declared a number not below + // nought; and declared any whole number, by what it is. + boost::unordered_flat_set mSteadyNonNegative; + boost::unordered_flat_map mSteadyWhole; bool mJoinLists = false; bool mLslInteger = false; bool mLslFloat = false; @@ -786,50 +912,735 @@ namespace } mPending.clear(); } + if (mAnchorPending && !mInline && !text.empty()) + { + mAnchors.emplace_back(*mAnchorPending, linesWritten()); + mAnchorPending.reset(); + } mText += text.empty() ? std::string("\n") : indent() + text + "\n"; } + S32 Writer::linesWritten() + { + // Appended to but for an assignment in an expression, written apart + // and put back as it was. + if (mCounted > mText.size()) + { + mCounted = 0; + mCountedLines = 0; + } + mCountedLines += static_cast(std::count(mText.begin() + static_cast(mCounted), mText.end(), '\n')); + mCounted = mText.size(); + return mCountedLines; + } + + // --- the script's own comments ----------------------------------------------------- + + namespace + { + // The runs of a mark a comment's text begins and ends with, as + // dashes: a rule, or the rule a heading stands in. + void dashed(std::string& text, char mark) + { + for (size_t i = 0; i < text.size() && text[i] == mark; ++i) + { + text[i] = '-'; + } + for (size_t i = text.size(); i > 0 && text[i - 1] == mark; --i) + { + text[i - 1] = '-'; + } + } + } + + void Writer::findComments() + { + if (!mOptions.keepComments) + { + return; + } + // Each comment, past strings, where it begins. + const std::string_view src = mSource; + Pos at = { 1, 1 }; + const auto step = [&](char c) { + if (c == '\n') + { + ++at.line; + at.column = 1; + } + else + { + ++at.column; + } + }; + for (size_t i = 0; i < src.size();) + { + const char c = src[i]; + if (c == '"') + { + step(src[i++]); + while (i < src.size() && src[i] != '"') + { + if (src[i] == '\\' && i + 1 < src.size()) + { + step(src[i++]); + } + step(src[i++]); + } + if (i < src.size()) + { + step(src[i++]); + } + continue; + } + if (c != '/' || i + 1 >= src.size() || (src[i + 1] != '/' && src[i + 1] != '*')) + { + step(src[i++]); + continue; + } + Comment k; + k.at = at; + size_t end; + if (src[i + 1] == '/') + { + end = std::min(src.find('\n', i), src.size()); + std::string body = std::string(src.substr(i + 2, end - i - 2)); + while (!body.empty() && (body.back() == '\r' || body.back() == ' ' || body.back() == '\t')) + { + body.pop_back(); + } + // Not a block comment of Luau's, which --[[ would begin. + if (!body.empty() && body[0] == '[') + { + body = " " + body; + } + k.text = "--" + body; + if (body == "*" || body == "*/") + { + // //* over code and //*/ under it, which a / taken out of + // the first makes a block comment: ---[[ and --]], which a + // - taken out of the first makes one. + k.text = body == "*" ? "---[[" : "--]]"; + } + else if (!body.empty() && body[0] == '/') + { + // A rule of slashes, a heading in it or not: of dashes. + dashed(body, '/'); + k.text = "--" + body; + } + } + else + { + const size_t close = src.find("*/", i + 2); + end = close == std::string_view::npos ? src.size() : close + 2; + std::string body = std::string(src.substr(i + 2, (close == std::string_view::npos ? src.size() : close) - i - 2)); + // The toggle's other way, /* over code and //*/ under it: --]] + // under it, which ---[[ over it would leave a line comment. + const bool toggled = body.size() >= 2 && body.compare(body.size() - 2, 2, "//") == 0 && + body.find('\n') != std::string::npos; + if (toggled) + { + body.resize(body.size() - 2); + } + // Brackets of a level nothing in it closes. + std::string level; + while (body.find("]" + level + "]") != std::string::npos) + { + level += "="; + } + if (!body.empty() && body.back() == ']') + { + body += " "; + } + k.text = "--[" + level + "[" + body + (toggled ? "--" : "") + "]" + level + "]"; + if (body.size() > 1 && body.front() == '*' && body.back() == '*' && body.find('\n') == std::string::npos) + { + // A rule of stars on its line, a heading in it or not: of + // dashes, as long. + dashed(body, '*'); + k.text = "--" + body + "--"; + } + } + // A comment that reads as one of the notes this writes is not one. + if (k.text.rfind(NOTE_MARK, 0) == 0) + { + k.text.insert(2, " "); + } + while (i < end) + { + step(src[i++]); + } + k.endLine = at.line; + mComments.push_back(std::move(k)); + } + // Whether a line of the LSL, from one, is blank. + std::vector starts{ 0 }; + for (size_t i = 0; i < src.size(); ++i) + { + if (src[i] == '\n') + { + starts.push_back(i + 1); + } + } + const auto blank = [&](S32 line) { + if (line < 1 || static_cast(line) > starts.size()) + { + return false; + } + const size_t from = starts[static_cast(line) - 1]; + const size_t to = std::min(src.find('\n', from), src.size()); + return src.substr(from, to - from).find_first_not_of(" \t\r") == std::string_view::npos; + }; + for (Comment& c : mComments) + { + c.apart = blank(c.at.line - 1); + } + if (mComments.empty()) + { + return; + } + std::vector top; + for (LSLASTNode* g = mScript->getGlobals()->getChild(0); g; g = g->getNext()) + { + top.push_back(g); + } + for (LSLASTNode* st = mScript->getStates()->getChild(0); st; st = st->getNext()) + { + top.push_back(st); + } + // Over the first global, those with no blank line between them and it + // are about it; those before, the script's own, go at the top. + size_t before = 0; + while (!top.empty() && before < mComments.size() && + mComments[before].at < Pos{ top.front()->getLoc()->first_line, top.front()->getLoc()->first_column }) + { + ++before; + } + size_t about = before; + S32 next = top.empty() ? 0 : top.front()->getLoc()->first_line; + while (about > 0 && mComments[about - 1].endLine + 1 >= next && !blank(next - 1)) + { + --about; + next = mComments[about].at.line; + } + for (size_t k = 0; k < mComments.size(); ++k) + { + if (k < about) + { + // Written at the top as the text is put together. + mLeadComments.push_back(k); + mComments[k].written = true; + continue; + } + place(k, top, nullptr); + } + } + + namespace + { + // Whether what a node ends with is a statement of its own, not a + // block's brace: a comment after it on its line is about it. + bool endsBare(LSLASTNode* node) + { + switch (node->getNodeType()) + { + case NODE_GLOBAL_VARIABLE: return true; + case NODE_STATEMENT: break; + default: return false; + } + switch (node->getNodeSubType()) + { + case NODE_COMPOUND_STATEMENT: return false; + case NODE_IF_STATEMENT: + { + LSLASTNode* last = node->getChild(2); + return endsBare(isNull(last) ? node->getChild(1) : last); + } + case NODE_WHILE_STATEMENT: return endsBare(node->getChild(1)); + case NODE_FOR_STATEMENT: return endsBare(node->getChild(3)); + default: return true; + } + } + } + + void Writer::place(size_t k, const std::vector& children, LSLASTNode* holder) + { + const Pos at = mComments[k].at; + LSLASTNode* prev = nullptr; + mCommentsWithin[holder].push_back(k); + for (LSLASTNode* child : children) + { + const Pos begins = { child->getLoc()->first_line, child->getLoc()->first_column }; + const Pos ends = { child->getLoc()->last_line, child->getLoc()->last_column }; + if (!(at < ends)) + { + prev = child; + continue; + } + if (!(at < begins)) + { + // Inside it: in what it holds, or over it. + if (!placeInside(k, child)) + { + mCommentsBefore[child].push_back(k); + } + return; + } + break; + } + // On the line a bare statement ends, after it: about that statement, + // and after it too, where it is one line of the comment's and the + // statement one line, or no if or loop, whose first line is not + // where the comment was. + if (prev && endsBare(prev) && at.line == prev->getLoc()->last_line) + { + const LSLNodeSubType type = prev->getNodeSubType(); + const bool control = type == NODE_IF_STATEMENT || type == NODE_WHILE_STATEMENT || type == NODE_FOR_STATEMENT || + type == NODE_DO_STATEMENT; + if (mComments[k].endLine == at.line && (!control || prev->getLoc()->first_line == prev->getLoc()->last_line)) + { + mCommentsAfter[prev].push_back(k); + } + else + { + mCommentsBefore[prev].push_back(k); + } + return; + } + // Over what follows it in here; or, where nothing does, at the end. + for (LSLASTNode* child : children) + { + if (at < Pos{ child->getLoc()->first_line, child->getLoc()->first_column }) + { + mCommentsBefore[child].push_back(k); + return; + } + } + } + + void Writer::placeIn(size_t k, LSLASTNode* compound) + { + std::vector children; + for (LSLASTNode* child = compound->getChild(0); child; child = child->getNext()) + { + children.push_back(child); + } + place(k, children, compound); + } + + bool Writer::placeInside(size_t k, LSLASTNode* node) + { + const Pos at = mComments[k].at; + const auto inside = [&at](LSLASTNode* n) { + return !isNull(n) && !(at < Pos{ n->getLoc()->first_line, n->getLoc()->first_column }) && + at < Pos{ n->getLoc()->last_line, n->getLoc()->last_column }; + }; + // A statement standing where one goes -- a body, a branch -- in what + // it holds, or over it. + const auto within = [&](LSLASTNode* body) { + if (!inside(body)) + { + return false; + } + if (body->getNodeSubType() == NODE_COMPOUND_STATEMENT) + { + // On the line its brace opens, before anything in it: about + // what the block is the body of. + LSLASTNode* first = body->getChild(0); + if (at.line == body->getLoc()->first_line && + (!first || at < Pos{ first->getLoc()->first_line, first->getLoc()->first_column })) + { + return false; + } + placeIn(k, body); + } + else if (!placeInside(k, body)) + { + mCommentsBefore[body].push_back(k); + } + return true; + }; + switch (node->getNodeType()) + { + case NODE_GLOBAL_FUNCTION: + return within(static_cast(node)->getStatements()); + case NODE_EVENT_HANDLER: + return within(static_cast(node)->getStatements()); + case NODE_STATE: + { + std::vector handlers; + for (LSLASTNode* h = static_cast(node)->getEventHandlers()->getChild(0); h; h = h->getNext()) + { + handlers.push_back(h); + } + place(k, handlers, node); + return true; + } + case NODE_STATEMENT: + switch (node->getNodeSubType()) + { + case NODE_COMPOUND_STATEMENT: placeIn(k, node); return true; + case NODE_IF_STATEMENT: + { + if (within(node->getChild(1)) || within(node->getChild(2))) + { + return true; + } + // Between the branches: over what the else runs -- an + // elseif's line, or the else's first statement. + LSLASTNode* no = node->getChild(2); + if (isNull(no) || !(at < Pos{ no->getLoc()->first_line, no->getLoc()->first_column }) || + at < Pos{ node->getChild(1)->getLoc()->last_line, node->getChild(1)->getLoc()->last_column }) + { + return false; + } + if (no->getNodeSubType() == NODE_COMPOUND_STATEMENT) + { + placeIn(k, no); + } + else + { + mCommentsBefore[no].push_back(k); + } + return true; + } + case NODE_WHILE_STATEMENT: return within(node->getChild(1)); + case NODE_DO_STATEMENT: return within(node->getChild(0)); + case NODE_FOR_STATEMENT: return within(node->getChild(3)); + default: return false; + } + default: + return false; + } + } + + void Writer::writeComment(Comment& c) + { + if (c.written) + { + return; + } + c.written = true; + if (c.apart && !mText.empty() && mText.compare(mText.size() - std::min(mText.size(), 2), 2, "\n\n") != 0) + { + mText += "\n"; + } + mText += indent() + c.text + "\n"; + } + + void Writer::commentsBefore(LSLASTNode* node) + { + // Every node written is through here first: the first line of code + // written after its comments is the one made of it. + mAnchorPending.reset(); + if (const auto found = mCommentsBefore.find(node); found != mCommentsBefore.end()) + { + for (size_t k : found->second) + { + writeComment(mComments[k]); + } + } + if (!isNull(node) && node->getLoc()) + { + mAnchorPending = zeroBased(node->getLoc()->first_line); + } + } + + void Writer::commentsAfter(LSLASTNode* node, size_t from) + { + const auto found = mCommentsAfter.find(node); + if (found == mCommentsAfter.end()) + { + return; + } + std::string after; + for (size_t k : found->second) + { + Comment& c = mComments[k]; + if (!c.written) + { + c.written = true; + after += " " + c.text; + } + } + if (after.empty()) + { + return; + } + // Past what was said over it, and the comments over what it holds. + for (size_t at = from; at < mText.size();) + { + const size_t end = mText.find('\n', at); + if (end == std::string::npos) + { + break; + } + const size_t first = mText.find_first_not_of(' ', at); + if (first < end && mText.compare(first, 2, "--") != 0) + { + mText.insert(end, after); + // Counted past it: as many lines, further on. + if (end < mCounted) + { + mCounted += after.size(); + } + mTrailed[node] = after.size(); + return; + } + at = end + 1; + } + mText += indent() + after.substr(1) + "\n"; + } + + void Writer::commentsAtEnd(LSLASTNode* holder) + { + const auto found = mCommentsWithin.find(holder); + if (found == mCommentsWithin.end()) + { + return; + } + for (size_t k : found->second) + { + writeComment(mComments[k]); + } + } + // --- expressions ------------------------------------------------------------------ - Expr Writer::constant(LSLConstant* c) + bool intoBit32(LSLASTNode* e); + bool wholeNumber(LSLExpression* e, int& v); + namespace + { + LSLSymbol* setBy(LSLASTNode* node); + } + + Expr Writer::constant(LSLConstant* c) + { + switch (c->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + { + // Hexadecimal as the LSL wrote it, where SLua reads the same + // number, or where bit32 takes it, which takes one past + // 0x7FFFFFFF as the number below nought LSL wrapped it to: + // one constant either way. Elsewhere such a one as LSL had + // it, which bit32.s32 of the hexadecimal would make a call + // that SLua's compiler does not fold. + const int v = static_cast(c)->getValue(); + // What it is the value of: a declaration of a variable only + // bit32 reads, or what bit32 takes. + LSLASTNode* holder = c->getParent(); + while (holder && (holder->getNodeSubType() == NODE_CONSTANT_EXPRESSION || holder->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION)) + { + holder = holder->getParent(); + } + const std::optional hex = hexAsWritten(c, v); + // Past 0x7FFFFFFF: what SLua reads it as, said once. + if (hex && v < 0 && !mSaidHexSign) + { + mSaidHexSign = true; + note(c, "SluaHexSign", + "SLua reads [1] as [2], where LSL wrapped it to [3]: bit32 takes either the same, so it stays as written where only " + "bit32 reads it, and is [3] where it is read as a number.", + { *hex, std::to_string(static_cast(v)), std::to_string(v) }); + } + if (hex && (v >= 0 || mBitsOnly.contains(holder) || (c->getParent() && intoBit32(c->getParent())))) + { + return { *hex, PRIMARY }; + } + // A number past 0xFFFFFFFF, which the grid's 32-bit hosts + // stop at: -1 once signed, as written here. + if (v == -1 && !static_cast(c)->wasNegated()) + { + for (const auto& [word, value] : numbersNear(c)) + { + if (value > 0xFFFFFFFFull) + { + noteOnce(c, "SluaIntegerPast32Bits", "LSL reads [1] as -1: past 0xFFFFFFFF, the most its 32 bits hold, a number stops there.", + { word }); + break; + } + } + } + return { std::to_string(v), v < 0 ? UNARY : PRIMARY }; + } + case NODE_FLOAT_CONSTANT: + { + const double v = static_cast(c)->getValue(); + return { number(v), std::signbit(v) ? UNARY : PRIMARY }; + } + case NODE_KEY_CONSTANT: + return { "uuid(" + luaString(static_cast(c)->getValue()) + ")" }; + case NODE_STRING_CONSTANT: + return { luaString(static_cast(c)->getValue()) }; + case NODE_VECTOR_CONSTANT: + { + const Vector3* v = static_cast(c)->getValue(); + return { "vector(" + number(v->x) + ", " + number(v->y) + ", " + number(v->z) + ")" }; + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* q = static_cast(c)->getValue(); + return { "quaternion(" + number(q->x) + ", " + number(q->y) + ", " + number(q->z) + ", " + number(q->s) + ")" }; + } + case NODE_LIST_CONSTANT: + { + std::string out; + for (LSLConstant* item = static_cast(c)->getValue(); item; item = static_cast(item->getNext())) + { + out += (out.empty() ? "" : ", ") + constant(item).text; + } + return { "{" + out + "}" }; + } + default: + return { "nil" }; + } + } + + const std::vector& Writer::lineStarts() + { + if (mLineStarts.empty()) + { + mLineStarts.push_back(0); + for (size_t i = 0; i < mSource.size(); ++i) + { + if (mSource[i] == '\n') + { + mLineStarts.push_back(i + 1); + } + } + } + return mLineStarts; + } + + std::vector> Writer::numbersNear(LSLConstant* c) + { + std::vector> found; + const auto* loc = c->getLoc(); + if (!loc || loc->first_line < 1) + { + return found; + } + lineStarts(); + // Each number on a line, past strings and comments. + const auto onLine = [&](S32 line) { + if (line < 1 || static_cast(line) > mLineStarts.size()) + { + return; + } + const size_t from = mLineStarts[static_cast(line) - 1]; + const std::string_view text = mSource.substr(from, std::min(mSource.find('\n', from), mSource.size()) - from); + for (size_t i = 0; i < text.size();) + { + const char ch = text[i]; + if (ch == '"') + { + for (++i; i < text.size() && text[i] != '"'; ++i) + { + i += text[i] == '\\' ? 1 : 0; + } + ++i; + continue; + } + if (ch == '/' && i + 1 < text.size() && (text[i + 1] == '/' || text[i + 1] == '*')) + { + break; + } + const bool starts = std::isdigit(static_cast(ch)) && + (i == 0 || !(std::isalnum(static_cast(text[i - 1])) || text[i - 1] == '_' || text[i - 1] == '.')); + if (!starts) + { + ++i; + continue; + } + size_t end = i; + while (end < text.size() && (std::isalnum(static_cast(text[end])) || text[end] == '.' || text[end] == '_')) + { + ++end; + } + const std::string word(text.substr(i, end - i)); + const bool hex = word.size() > 2 && word[0] == '0' && (word[1] == 'x' || word[1] == 'X') && + word.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos; + if (hex || word.find_first_not_of("0123456789") == std::string::npos) + { + // Past what strtoull holds: past 32 bits all the same. + errno = 0; + const unsigned long long value = std::strtoull(word.c_str(), nullptr, hex ? 16 : 10); + found.emplace_back(word, errno == ERANGE ? ~0ull : value); + } + i = end; + } + }; + onLine(loc->first_line); + if (found.empty()) + { + onLine(loc->first_line - 1); + } + return found; + } + + std::optional Writer::hexAsWritten(LSLConstant* c, int v) + { + // Hexadecimal of 32 bits at most whose bits are the value, LSL's + // own reading of it, where no number written another way is. + std::optional hex; + for (const auto& [word, value] : numbersNear(c)) + { + const bool is_hex = word.size() > 2 && (word[1] == 'x' || word[1] == 'X'); + if (is_hex && value <= 0xFFFFFFFFull && static_cast(value) == static_cast(v)) + { + hex = word; + } + else if (!is_hex && (value == static_cast(std::llabs(static_cast(v))))) + { + return std::nullopt; + } + } + return hex; + } + + void Writer::findBitsOnly() { - switch (c->getNodeSubType()) - { - case NODE_INTEGER_CONSTANT: + // The declarations of integers given such a number. + boost::unordered_flat_map given; + walk(mScript, [&](LSLASTNode* node) { + LSLIdentifier* id = nullptr; + LSLExpression* init = nullptr; + if (node->getNodeType() == NODE_GLOBAL_VARIABLE) { - const int v = static_cast(c)->getValue(); - return { std::to_string(v), v < 0 ? UNARY : PRIMARY }; + id = static_cast(node)->getIdentifier(); + init = static_cast(node)->getInitializer(); } - case NODE_FLOAT_CONSTANT: + else if (node->getNodeSubType() == NODE_DECLARATION) { - const double v = static_cast(c)->getValue(); - return { number(v), std::signbit(v) ? UNARY : PRIMARY }; + id = static_cast(node)->getIdentifier(); + init = static_cast(node)->getInitializer(); } - case NODE_KEY_CONSTANT: - return { "uuid(" + luaString(static_cast(c)->getValue()) + ")" }; - case NODE_STRING_CONSTANT: - return { luaString(static_cast(c)->getValue()) }; - case NODE_VECTOR_CONSTANT: + int v = 0; + if (id && id->getSymbol() && id->getIType() == LST_INTEGER && wholeNumber(init, v) && v < 0) { - const Vector3* v = static_cast(c)->getValue(); - return { "vector(" + number(v->x) + ", " + number(v->y) + ", " + number(v->z) + ")" }; + given.emplace(id->getSymbol(), node); } - case NODE_QUATERNION_CONSTANT: + }); + if (given.empty()) + { + return; + } + // Each read of one by anything but bit32, and each that sets one, + // takes it out. + walk(mScript, [&](LSLASTNode* node) { + if (LSLSymbol* set = setBy(node)) { - const Quaternion* q = static_cast(c)->getValue(); - return { "quaternion(" + number(q->x) + ", " + number(q->y) + ", " + number(q->z) + ", " + number(q->s) + ")" }; + given.erase(set); } - case NODE_LIST_CONSTANT: + if (node->getNodeSubType() == NODE_LVALUE_EXPRESSION && !intoBit32(node)) { - std::string out; - for (LSLConstant* item = static_cast(c)->getValue(); item; item = static_cast(item->getNext())) - { - out += (out.empty() ? "" : ", ") + constant(item).text; - } - return { "{" + out + "}" }; + given.erase(static_cast(node)->getIdentifier()->getSymbol()); } - default: - return { "nil" }; + }); + for (const auto& [symbol, declaration] : given) + { + mBitsOnly.insert(declaration); } } @@ -900,6 +1711,10 @@ namespace return *test; } } + if (std::optional test = bitTest(inner)) + { + return *test; + } Expr out = expr(e); if (out.boolean) { @@ -926,6 +1741,35 @@ namespace } } + std::optional Writer::bitTest(LSLExpression* e) + { + const auto bare = [](LSLExpression* one) { + while (one && one->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION) + { + one = static_cast(one)->getChildExpr(); + } + return one; + }; + LSLExpression* inner = bare(e); + if (!inner || inner->getNodeSubType() != NODE_BINARY_EXPRESSION || inner->getOperation() != OP_BIT_AND) + { + return std::nullopt; + } + std::string all; + const std::function gather = [&](LSLExpression* one) { + LSLExpression* b = bare(one); + if (b->getNodeSubType() == NODE_BINARY_EXPRESSION && b->getOperation() == OP_BIT_AND) + { + gather(static_cast(b)->getLHS()); + gather(static_cast(b)->getRHS()); + return; + } + all += (all.empty() ? "" : ", ") + value(one).text; + }; + gather(inner); + return Expr{ "bit32.btest(" + all + ")", PRIMARY, true }; + } + Expr Writer::coerced(LSLExpression* e, LSLIType to) { Expr out = value(e); @@ -1043,6 +1887,37 @@ namespace } } + // Whether what an expression gives is bit32's to take again, which takes + // a number below nought as it takes the same past 2147483647: an operand + // of &, | or ^, what ~ turns over, or what a shift moves. + bool intoBit32(LSLASTNode* e) + { + LSLASTNode* node = e; + LSLASTNode* parent = node->getParent(); + while (parent && parent->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION) + { + node = parent; + parent = parent->getParent(); + } + if (!parent || parent->getNodeType() != NODE_EXPRESSION) + { + return false; + } + switch (static_cast(parent)->getOperation()) + { + case OP_BIT_AND: + case OP_BIT_OR: + case OP_BIT_XOR: + case OP_BIT_NOT: + return true; + case OP_SHIFT_LEFT: + case OP_SHIFT_RIGHT: + return node->getParentSlot() == 0; + default: + return false; + } + } + // A whole number written out, and what it is: a constant, or one with a // minus before it. bool wholeNumber(LSLExpression* e, int& v) @@ -1461,6 +2336,49 @@ namespace return { "llcompat." + bare + "(" + args(e->getArguments(), params) + ")" }; } + Expr Writer::signedUnless(bool same, const std::string& call, LSLASTNode* at) + { + if (same) + { + return { call }; + } + if (!mSaidSigned) + { + mSaidSigned = true; + note(at, "SluaBit32Signed", + "bit32 answers 0 to 4294967295, where LSL's integers were signed: bit32.s32 makes the answer LSL's where it could pass " + "2147483647 and is read as a number."); + } + return { "bit32.s32(" + call + ")" }; + } + + bool Writer::notBelowZero(LSLExpression* e) const + { + if (nonNegative(e)) + { + return true; + } + while (e && e->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION) + { + e = static_cast(e)->getChildExpr(); + } + if (e && e->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + // An & with one, an | or ^ of two, a >> of one. + auto* b = static_cast(e); + switch (b->getOperation()) + { + case OP_BIT_AND: return notBelowZero(b->getLHS()) || notBelowZero(b->getRHS()); + case OP_BIT_OR: + case OP_BIT_XOR: return notBelowZero(b->getLHS()) && notBelowZero(b->getRHS()); + case OP_SHIFT_RIGHT: return notBelowZero(b->getLHS()); + default: return false; + } + } + return e && e->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(e)->getMember() && + mSteadyNonNegative.contains(static_cast(e)->getIdentifier()->getSymbol()); + } + Expr Writer::binary(LSLBinaryExpression* e) { // == and ~= the other way round under a not (unary). @@ -1520,15 +2438,14 @@ namespace "LSL ran the right side of this before the left, and Luau runs the left first: one side changes what the other reads."); } const auto bit = [&](const char* fn) -> Expr { - // Not where the answer is the same number: an & with a number not - // below nought, or an | or ^ of two; nor where it is only asked - // whether it is nought. - const bool same = (op == OP_BIT_AND && (nonNegative(lhs) || nonNegative(rhs))) || - ((op == OP_BIT_OR || op == OP_BIT_XOR) && nonNegative(lhs) && nonNegative(rhs)) || truthOnly(e); - if (!same) - { - noteOnce(e, "SluaBit32", "bit32 answers 0 to 4294967295; LSL's integers were signed, from -2147483648."); - } + // bit32 answers 0 to 4294967295, where LSL's integers were signed: + // made LSL's by bit32.s32, but where the answer is the same + // number -- an & with a number not below nought, an | or ^ of + // two, a >> of one -- or where it is only asked whether it is + // nought, or bit32 takes it again. + const bool same = (op == OP_BIT_AND && (notBelowZero(lhs) || notBelowZero(rhs))) || + ((op == OP_BIT_OR || op == OP_BIT_XOR) && notBelowZero(lhs) && notBelowZero(rhs)) || + (op == OP_SHIFT_RIGHT && notBelowZero(lhs)) || truthOnly(e) || intoBit32(e); // An &, | or ^ of the same again, one call of them all: bit32's // take as many as are given. if (op == OP_BIT_AND || op == OP_BIT_OR || op == OP_BIT_XOR) @@ -1550,9 +2467,9 @@ namespace }; gather(lhs); gather(rhs); - return { std::string("bit32.") + fn + "(" + all + ")" }; + return signedUnless(same, std::string("bit32.") + fn + "(" + all + ")", e); } - return { std::string("bit32.") + fn + "(" + value(lhs).text + ", " + value(rhs).text + ")" }; + return signedUnless(same, std::string("bit32.") + fn + "(" + value(lhs).text + ", " + value(rhs).text + ")", e); }; switch (op) { @@ -1609,6 +2526,17 @@ namespace return *asked; } const bool eq = op == OP_EQ; + // An & against nought: whether it is. + { + int zero = 1; + std::optional test = wholeNumber(rhs, zero) && zero == 0 ? bitTest(lhs) + : wholeNumber(lhs, zero) && zero == 0 ? bitTest(rhs) + : std::nullopt; + if (test) + { + return eq ? Expr{ "not " + test->text, UNARY, true } : *test; + } + } if (lt == LST_LIST && rt == LST_LIST) { // LSL compares two lists by their lengths alone; != says @@ -1700,6 +2628,10 @@ namespace static_cast(inner)->getIdentifier()->getSymbol()->getSubType() == SYM_BUILTIN); if (plain && inner->getIType() == LST_INTEGER) { + if (std::optional test = bitTest(inner)) + { + return { "not " + test->text, UNARY, true }; + } const Expr v = expr(inner); if (v.boolean) { @@ -1710,8 +2642,7 @@ namespace return { "not " + bracketed(condition(child), UNARY), UNARY, true }; } case OP_BIT_NOT: - noteOnce(e, "SluaBit32", "bit32 answers 0 to 4294967295; LSL's integers were signed, from -2147483648."); - return { "bit32.bnot(" + value(child).text + ")" }; + return signedUnless(truthOnly(e) || intoBit32(e), "bit32.bnot(" + value(child).text + ")", e); case OP_PRE_INCR: case OP_PRE_DECR: case OP_POST_INCR: @@ -2126,9 +3057,12 @@ namespace { statement(child, !child->getNext()); } + lineUp(); + commentsAtEnd(s); return; } statement(s, true); + lineUp(); } void Writer::statement(LSLASTNode* s, bool last) @@ -2137,6 +3071,224 @@ namespace { return; } + // Lined up before anything else is written, which notes where in + // the text it was. + if (s->getNodeSubType() != NODE_DECLARATION) + { + lineUp(); + } + commentsBefore(s); + const size_t from = mText.size(); + statementBody(s, last); + commentsAfter(s, from); + lined(s, from); + } + + void Writer::lined(LSLASTNode* node, size_t from) + { + // Its one line of code, past what was said over it. + LSLIdentifier* id = nullptr; + LSLASTNode* init = nullptr; + if (node->getNodeType() == NODE_GLOBAL_VARIABLE) + { + id = static_cast(node)->getIdentifier(); + init = static_cast(node)->getInitializer(); + } + else if (node->getNodeSubType() == NODE_DECLARATION) + { + id = static_cast(node)->getIdentifier(); + init = static_cast(node)->getInitializer(); + } + size_t at = std::string::npos; + int lines = 0; + for (size_t i = from; id && i < mText.size();) + { + const size_t end = mText.find('\n', i); + const size_t first = mText.find_first_not_of(' ', i); + if (end == std::string::npos) + { + break; + } + if (first < end && mText.compare(first, 2, "--") != 0) + { + at = lines++ == 0 ? i : at; + } + i = end + 1; + } + const bool fits = id && !isNull(init) && lines == 1 && node->getLoc()->first_line == node->getLoc()->last_line; + if (!fits || (!mRun.empty() && (mRun.back().node->getParent() != node->getParent() || + mRun.back().node->getLoc()->last_line + 1 != node->getLoc()->first_line))) + { + // What it pads is all before this one's line, which moves on. + const size_t before = mText.size(); + lineUp(); + at += mText.size() - before; + } + if (fits) + { + mRun.push_back({ node, id, at }); + } + } + + void Writer::lineUp() + { + std::vector run; + run.swap(mRun); + if (run.size() < 2) + { + return; + } + lineStarts(); + // Where the LSL put each one's =, and the comment after it. + const auto lslLine = [&](S32 line) { + const size_t from = mLineStarts[static_cast(line) - 1]; + return mSource.substr(from, std::min(mSource.find('\n', from), mSource.size()) - from); + }; + bool padded = false; + std::optional equals; + std::optional comment; + bool sameEquals = true; + bool sameComment = true; + int comments = 0; + for (const Lined& one : run) + { + const auto* loc = one.id->getLoc(); + const std::string_view text = loc->first_line >= 1 && static_cast(loc->first_line) <= mLineStarts.size() + ? lslLine(loc->first_line) + : std::string_view(); + size_t i = static_cast(std::max(1, loc->first_column) - 1); + while (i < text.size() && (std::isalnum(static_cast(text[i])) || text[i] == '_')) + { + ++i; + } + const size_t name_end = i; + while (i < text.size() && (text[i] == ' ' || text[i] == '\t')) + { + ++i; + } + if (i >= text.size() || text[i] != '=') + { + sameEquals = false; + } + else + { + padded = padded || i - name_end > 1; + sameEquals = sameEquals && (!equals || *equals == i); + equals = i; + } + if (const auto after = mCommentsAfter.find(one.node); after != mCommentsAfter.end() && mTrailed.contains(one.node)) + { + const S32 column = mComments[after->second.front()].at.column; + sameComment = sameComment && (!comment || *comment == column); + comment = column; + ++comments; + } + } + const auto lineEnd = [&](size_t at) { return mText.find('\n', at); }; + // Spaces put in, and where each line after them starts moved on. + const auto pad = [&](size_t at, size_t count) { + mText.insert(at, count, ' '); + if (at < mCounted) + { + mCounted += count; + } + for (Lined& one : run) + { + one.at += one.at > at ? count : 0; + } + }; + // Each =, at the column of the one furthest on. + if (sameEquals && padded) + { + size_t most = 0; + for (const Lined& one : run) + { + most = std::max(most, mText.find(" = ", one.at) - one.at); + } + for (size_t n = run.size(); n > 0; --n) + { + const size_t at = mText.find(" = ", run[n - 1].at); + if (at < lineEnd(run[n - 1].at)) + { + pad(at, most - (at - run[n - 1].at)); + } + } + } + // And each comment after them, past the longest line of code that + // has one. + if (sameComment && comments > 1) + { + size_t most = 0; + for (const Lined& one : run) + { + if (const auto trailed = mTrailed.find(one.node); trailed != mTrailed.end()) + { + most = std::max(most, lineEnd(one.at) - trailed->second - one.at); + } + } + for (size_t n = run.size(); n > 0; --n) + { + if (const auto trailed = mTrailed.find(run[n - 1].node); trailed != mTrailed.end()) + { + const size_t code = lineEnd(run[n - 1].at) - trailed->second; + pad(code, most - (code - run[n - 1].at)); + } + } + } + } + + void Writer::onOneLine(size_t at, S32 lines, size_t anchors) + { + // Its lines, the last three if c then, s, end; any before them what + // was said over it. + std::vector starts; + for (size_t i = at; i < mText.size(); i = mText.find('\n', i) + 1) + { + starts.push_back(i); + if (mText.find('\n', i) == std::string::npos) + { + return; + } + } + if (starts.size() < 3) + { + return; + } + const auto row = [&](size_t n) { + const size_t begin = starts[n]; + const size_t first = std::min(mText.find_first_not_of(' ', begin), mText.find('\n', begin)); + return std::string_view(mText).substr(first, mText.find('\n', begin) - first); + }; + const size_t head = starts.size() - 3; + for (size_t n = 0; n < head; ++n) + { + if (row(n).substr(0, 2) != "--") + { + return; + } + } + const std::string_view body = row(head + 1); + if (row(head).substr(0, 3) != "if " || body.empty() || body.substr(0, 2) == "--" || row(head + 2) != "end") + { + return; + } + const std::string joined = mText.substr(starts[head], mText.find('\n', starts[head]) - starts[head]) + " " + std::string(body) + " end\n"; + mText.replace(starts[head], std::string::npos, joined); + // What is anchored past its first line, on it. + const S32 first = lines + static_cast(head); + for (size_t n = mAnchors.size(); n > anchors; --n) + { + if (mAnchors[n - 1].second > first) + { + mAnchors.erase(mAnchors.begin() + static_cast(n - 1)); + } + } + mCounted = starts[head]; + mCountedLines = first; + } + + void Writer::statementBody(LSLASTNode* s, bool last) + { if (mBuilds.contains(s)) { buildingLoop(s, last); @@ -2217,6 +3369,11 @@ namespace line(nameOf(chosen) + " = " + (when_true ? check.text : "not " + bracketed(check, UNARY))); return; } + // On one line where the LSL wrote it on one, with no else. + const bool one_line = s->getLoc()->first_line == s->getLoc()->last_line && isNull(i->getFalseBranch()); + const S32 lines_at = one_line ? linesWritten() : 0; + const size_t text_at = mText.size(); + const size_t anchors = mAnchors.size(); line("if " + condition(i->getCheckExpr()).text + " then"); for (;;) { @@ -2231,6 +3388,7 @@ namespace if (otherwise->getNodeSubType() == NODE_IF_STATEMENT) { i = static_cast(otherwise); + commentsBefore(i); line("elseif " + condition(i->getCheckExpr()).text + " then"); continue; } @@ -2241,6 +3399,10 @@ namespace break; } line("end"); + if (one_line) + { + onOneLine(text_at, lines_at, anchors); + } return; } case NODE_WHILE_STATEMENT: @@ -2445,9 +3607,11 @@ namespace { c.step = -1; } - else if ((op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN) && wholeNumber(static_cast(incr)->getRHS(), by) && by > 0) + else if ((op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN) && steadyWhole(static_cast(incr)->getRHS(), by) && by > 0) { c.step = op == OP_ADD_ASSIGN ? by : -by; + LSLExpression* step = unwrapped(static_cast(incr)->getRHS()); + c.by = step->getNodeSubType() == NODE_LVALUE_EXPRESSION ? step : nullptr; } const bool up = c.check == OP_LESS || c.check == OP_LEQ; if (c.step == 0 || (c.step > 0) != up) @@ -2465,6 +3629,32 @@ namespace return c; } + bool Writer::steadyWhole(LSLExpression* e, int& v) const + { + if (wholeNumber(e, v)) + { + return true; + } + e = unwrapped(e); + if (!e || e->getNodeSubType() != NODE_LVALUE_EXPRESSION || !isNull(static_cast(e)->getMember())) + { + return false; + } + LSLSymbol* symbol = static_cast(e)->getIdentifier()->getSymbol(); + if (const auto found = mSteadyWhole.find(symbol); found != mSteadyWhole.end()) + { + v = found->second; + return true; + } + LSLConstant* value = symbol && symbol->getSubType() == SYM_BUILTIN ? symbol->getConstantValue() : nullptr; + if (value && value->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + v = static_cast(value)->getValue(); + return true; + } + return false; + } + bool Writer::steadyIn(LSLExpression* e, LSLASTNode* loop) const { // What the loop sets, and whether it calls anything of the script's @@ -2573,7 +3763,9 @@ namespace const Expr bound = value(c.limit); to = strict ? bracketed(bound, ADD) + (shift < 0 ? " - 1" : " + 1") : bound.text; } - const std::string step = c.step == 1 ? std::string() : ", " + std::to_string(c.step); + const std::string step = c.by ? ", " + std::string(c.step < 0 ? "-" : "") + value(c.by).text + : c.step == 1 ? std::string() + : ", " + std::to_string(c.step); // A counter never below nought -- up from a whole number that is // not, or down to one -- is an index ll may be given moved on by // one; and where the body reads it as nothing else, it counts from 1. @@ -3561,7 +4753,6 @@ namespace Kind kind; LSLExpression* e; std::vector items; - bool apart = true; }; std::vector shaped; for (LSLExpression* e : parts) @@ -3584,7 +4775,6 @@ namespace for (LSLASTNode* item = m->getChild(0); !isNull(item); item = item->getNext()) { part.items.push_back(item); - part.apart = part.apart && apart(item); } } else if (m->getNodeSubType() == NODE_CONSTANT_EXPRESSION && m->getChild(0)->getNodeSubType() == NODE_LIST_CONSTANT) @@ -3613,23 +4803,61 @@ namespace return item->getNodeType() == NODE_CONSTANT ? constant(static_cast(item)).text : value(static_cast(item)).text; }; + // The values added at the end one after another, one table.append + // of them all, which has each before it adds any, as a list written + // out had -- and of several parts, each runs apart -- but one alone, + // which table.insert, a builtin of Luau's, adds faster; and more than + // a call takes well, a table of them moved on. + std::vector values; + const auto append = [&]() { + std::string all; + for (const std::string& v : values) + { + all += (all.empty() ? "" : ", ") + v; + } + if (values.size() == 1) + { + line("table.insert(" + name + ", " + all + ")"); + } + else if (values.size() > 32) + { + line("table.move({" + all + "}, 1, " + std::to_string(values.size()) + ", #" + name + " + 1, " + name + ")"); + } + else if (!values.empty()) + { + line("table.append(" + name + ", " + all + ")"); + } + values.clear(); + }; for (const Part& part : shaped) { + if (part.kind != Kind::Value && part.kind != Kind::Items) + { + append(); + } switch (part.kind) { - case Kind::Value: line("table.insert(" + name + at + value(part.e).text + ")"); break; + case Kind::Value: + if (before) + { + line("table.insert(" + name + at + value(part.e).text + ")"); + } + else + { + values.push_back(value(part.e).text); + } + break; case Kind::Items: - if (part.items.size() == 1 || part.apart) + for (LSLASTNode* item : part.items) { - for (LSLASTNode* item : part.items) + if (before) { line("table.insert(" + name + at + text(item) + ")"); } - } - else - { - line("table.move(" + expr(part.e).text + ", 1, " + std::to_string(part.items.size()) + ", #" + name + " + 1, " + - name + ")"); + else + { + values.push_back(text(item)); + } } break; case Kind::Named: @@ -3651,6 +4879,7 @@ namespace } } } + append(); return true; } @@ -4378,6 +5607,8 @@ namespace auto* global = static_cast(g); LSLIdentifier* id = global->getIdentifier(); LSLExpression* init = global->getInitializer(); + commentsBefore(global); + const size_t from = mText.size(); if (boolean(id->getSymbol())) { line("local " + nameOf(id) + (mOptions.types ? ": boolean" : "") + " = " + (isNull(init) ? std::string("false") : truthOf(init))); @@ -4387,8 +5618,11 @@ namespace const LSLIType type = varType(id->getSymbol(), id->getIType()); line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : coerced(init, type).text)); } + commentsAfter(global, from); + lined(global, from); any = true; } + lineUp(); if (any) { line(""); @@ -4397,8 +5631,9 @@ namespace void Writer::functions() { - // Declared together first where one calls another written after - // it, which LSL allows and a Luau local does not. + // Declared first, together, those a function written before them + // calls, which LSL allows and a Luau local function does not; the + // rest local functions. std::vector all; for (LSLASTNode* g = mScript->getGlobals()->getChild(0); g; g = g->getNext()) { @@ -4407,8 +5642,8 @@ namespace all.push_back(static_cast(g)); } } - bool forward = false; - for (size_t i = 0; i < all.size() && !forward; ++i) + boost::unordered_flat_set forward; + for (size_t i = 0; i < all.size(); ++i) { walk(all[i]->getStatements(), [&](LSLASTNode* node) { if (node->getNodeSubType() != NODE_FUNCTION_EXPRESSION) @@ -4420,16 +5655,22 @@ namespace LSLSymbol* callee = static_cast(node)->getIdentifier()->getSymbol(); for (size_t j = i + 1; j < all.size(); ++j) { - forward = forward || all[j]->getSymbol() == callee; + if (all[j]->getSymbol() == callee) + { + forward.insert(callee); + } } }); } - if (forward) + if (!forward.empty()) { std::string names; for (LSLGlobalFunction* f : all) { - names += (names.empty() ? "" : ", ") + nameOf(f->getIdentifier()); + if (forward.contains(f->getSymbol())) + { + names += (names.empty() ? "" : ", ") + nameOf(f->getIdentifier()); + } } line("local " + names); line(""); @@ -4444,7 +5685,8 @@ namespace (boolean(param) ? std::string(mOptions.types ? ": boolean" : "") : typed(varType(param, p->getIType()))); } mFunction = f->getSymbol(); - line(std::string(forward ? "function " : "local function ") + nameOf(f->getIdentifier()) + "(" + params + ")" + + commentsBefore(f); + line(std::string(forward.contains(mFunction) ? "function " : "local function ") + nameOf(f->getIdentifier()) + "(" + params + ")" + (boolean(mFunction) ? std::string(mOptions.types ? ": boolean" : "") : typed(f->getIdentifier()->getIType()))); ++mDepth; prepareBody(f->getStatements()); @@ -4489,6 +5731,7 @@ namespace void Writer::singleState(LSLState* state) { LSLEventHandler* entry = nullptr; + commentsBefore(state); for (LSLASTNode* h = state->getEventHandlers()->getChild(0); h; h = h->getNext()) { auto* handler = static_cast(h); @@ -4498,6 +5741,7 @@ namespace entry = handler; continue; } + commentsBefore(handler); if (event == "state_exit") { // Not an event SLua has, and one that never came: said, over @@ -4534,11 +5778,13 @@ namespace { // What LSL ran as the script started, run as SLua runs a script: // once, from the top. + commentsBefore(entry); line("-- state_entry"); mTopLevel = true; handlerBody(entry); mTopLevel = false; } + commentsAtEnd(state); } void Writer::statesPreamble() @@ -4557,7 +5803,10 @@ namespace // runs, and the timer where LLTimers calls it. const std::string own = mTimers ? "event ~= \"state_entry\" and event ~= \"state_exit\" and event ~= \"timer\"" : "event ~= \"state_entry\" and event ~= \"state_exit\""; - const std::string off = field ? "(LLEvents :: any)[event] = nil" : "LLEvents:off(event :: any, handler)"; + // Taken off by LLEvents:off either way: SLua puts on a handler + // assigned to a field as LLEvents:on does, and stops the script at + // a field assigned nil. + const std::string off = "LLEvents:off(event :: any, handler)"; const std::string on = field ? "(LLEvents :: any)[event] = handler" : "LLEvents:on(event :: any, handler)"; mText += " if name == currentState then\n" " return\n" @@ -4625,6 +5874,7 @@ namespace { auto* state = static_cast(s); const std::string name = state->getIdentifier()->getName(); + commentsBefore(state); line("states" + stateKey(name) + " = {"); ++mDepth; for (LSLASTNode* h = state->getEventHandlers()->getChild(0); h; h = h->getNext()) @@ -4633,6 +5883,7 @@ namespace const std::string event = handler->getIdentifier()->getName(); std::string lead; const std::string params = handlerParams(handler, lead); + commentsBefore(handler); if (event == "timer" && !mTimers) { noteOnce(handler, "SluaTimerStates", @@ -4649,6 +5900,7 @@ namespace --mDepth; line("end,"); } + commentsAtEnd(state); --mDepth; line("}"); line(""); @@ -4682,17 +5934,14 @@ end )LUA"; } + // LSL's casts of a string, as LSL converts a list's item, which is + // the same: llcompat's, as tonumber reads text its own way. if (mLslInteger) { out += R"LUA(-- LSL's (integer) of a string: the whole number it starts with, in -- decimal or 0x hexadecimal, after any spaces; 0 where it starts with none. local function lslInteger(s: string): number - local sign, hex = string.match(s, "^%s*([+-]?)0[xX](%x+)") - if hex then - local v = tonumber(hex, 16) or 0 - return if sign == "-" then -v else v - end - return tonumber(string.match(s, "^%s*([+-]?%d+)") or "0") or 0 + return llcompat.List2Integer({ s }, 0) end )LUA"; @@ -4702,8 +5951,7 @@ end out += R"LUA(-- LSL's (float) of a string: the number it starts with; 0 where it -- starts with none. local function lslFloat(s: string): number - local found = string.match(s, "^%s*([+-]?%d*%.?%d+[eE][+-]?%d+)") or string.match(s, "^%s*([+-]?%d*%.?%d*)") - return tonumber(found or "0") or 0 + return llcompat.List2Float({ s }, 0) end )LUA"; @@ -4748,9 +5996,33 @@ end { timersPreamble(); } + findComments(); + // The globals never set, declared a number not below nought: what an & + // with one of them gives needs no sign. + const ALLSLEffects::Writes written = mEffects.of(mScript); + for (LSLASTNode* g = mScript->getGlobals()->getChild(0); g; g = g->getNext()) + { + int v = 0; + if (g->getNodeType() == NODE_GLOBAL_VARIABLE && !written.writes(g->getSymbol()) && + wholeNumber(static_cast(g)->getInitializer(), v)) + { + mSteadyWhole.emplace(g->getSymbol(), v); + if (v >= 0) + { + mSteadyNonNegative.insert(g->getSymbol()); + } + } + } + findBitsOnly(); globals(); functions(); states(); + // Those after everything, and any of what nothing wrote, last. + commentsAtEnd(nullptr); + for (Comment& c : mComments) + { + writeComment(c); + } // Each of its lines a comment, whatever language the studio says // it in. const std::string head = mOptions.comments @@ -4768,7 +6040,24 @@ end from = cut + 1; } out += "\n"; + // The script's own first comments, over everything: what it is, and + // whose. + if (!mLeadComments.empty()) + { + for (size_t k : mLeadComments) + { + out += mComments[k].text + "\n"; + } + out += "\n"; + } helpers(out); + // The head over what was written: the SLua's lines counted from the + // top. + const S32 above = static_cast(std::count(out.begin(), out.end(), '\n')); + for (std::pair& anchor : mAnchors) + { + anchor.second += above; + } return out + mText; } } @@ -4953,9 +6242,10 @@ ALLSLToSLua::Result ALLSLToSLua::convert(std::string_view lsl, const Options& op } return; } - Writer writer(script, options); + Writer writer(script, lsl, options); result.text = writer.write(); result.notes = std::move(writer.notes()); + result.anchors = writer.anchors(); result.converted = true; }); return result; diff --git a/indra/alscript/allsltoslua.h b/indra/alscript/lsl/allsltoslua.h similarity index 86% rename from indra/alscript/allsltoslua.h rename to indra/alscript/lsl/allsltoslua.h index ffc64de4fd3..605e3283113 100644 --- a/indra/alscript/allsltoslua.h +++ b/indra/alscript/lsl/allsltoslua.h @@ -54,8 +54,9 @@ class ALLSLToSLua // every one off. struct Options { - // A handler put on with LLEvents:on, which lets others listen too; - // or assigned, LLEvents.touch_start = ..., one for each event. + // A handler put on with LLEvents:on; or assigned, LLEvents.touch_start + // = ..., which SLua puts on as LLEvents:on does, beside any others, + // but gives back nothing for LLEvents:off to take off. enum class Handlers : U8 { On, @@ -82,6 +83,11 @@ class ALLSLToSLua // Each place noted as a "-- LSL:" comment over its line, as well as // in the notes. bool comments = true; + // The script's own comments, each over what it stood over, after it + // where it was after it on its line, or at the end of what held it: + // // as --, and /* */ as --[[ ]]; a rule of stars or slashes as one of + // dashes, and the //* ... //*/ toggle as ---[[ ... --]]. + bool keepComments = true; // What a note says, by its key -- the studio's own words for it, // in its skin's language -- where given; else the English, its // marks filled from the args. @@ -109,6 +115,12 @@ class ALLSLToSLua // Why nothing was converted: the LSL does not parse, or the // definitions are not loaded. ALScriptProblems problems; + // Lines of the LSL beside the lines of the SLua written of them, each + // counted from nought: where each global, function, state, handler + // and statement begins, and the first line of code made of it. In + // the order written, which is not always the LSL's: what a diff of + // the two lines up (ALTextDiff's anchors). + std::vector> anchors; }; static Result convert(std::string_view lsl, const Options& options); diff --git a/indra/alscript/allsltraits.cpp b/indra/alscript/lsl/allsltraits.cpp similarity index 79% rename from indra/alscript/allsltraits.cpp rename to indra/alscript/lsl/allsltraits.cpp index 064a6fb0af1..dd0301cc8b6 100644 --- a/indra/alscript/allsltraits.cpp +++ b/indra/alscript/lsl/allsltraits.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -194,6 +195,53 @@ bool ALLSLTraits::changesNothing(Tailslide::LSLASTNode* node) return quiet(node, true); } +// static +bool ALLSLTraits::bounds(const char* name, S32& least, S32& most) +{ + if (!name) + { + return false; + } + if (atLeastMinusOne(name)) + { + least = -1; + most = INT32_MAX; + return true; + } + struct Row + { + std::string_view name; + S32 least; + S32 most; + }; + // Not llGetUnixTime, whose answer wraps below nought in 2038. + static constexpr Row ROWS[] = { + { "llGetAttached", 0, INT32_MAX }, { "llGetFreeMemory", 0, INT32_MAX }, { "llGetFreeURLs", 0, INT32_MAX }, + { "llGetInventoryNumber", 0, INT32_MAX }, { "llGetLinkNumber", 0, INT32_MAX }, { "llGetLinkNumberOfSides", 0, INT32_MAX }, + { "llGetListEntryType", 0, 6 }, { "llGetListLength", 0, INT32_MAX }, { "llGetMemoryLimit", 0, INT32_MAX }, + { "llGetNumberOfPrims", 0, INT32_MAX }, { "llGetNumberOfSides", 0, INT32_MAX }, { "llGetObjectPrimCount", 0, INT32_MAX }, + { "llGetRegionAgentCount", 0, INT32_MAX }, { "llGetSPMaxMemory", 0, INT32_MAX }, { "llGetUsedMemory", 0, INT32_MAX }, + { "llStringLength", 0, INT32_MAX }, + }; + for (const Row& row : ROWS) + { + if (row.name == name) + { + least = row.least; + most = row.most; + return true; + } + } + const Trait* t = of(name); + if (t && (t->slua & SluaBool)) + { + least = 0; + most = 1; + return true; + } + return false; +} + // static bool ALLSLTraits::atLeastMinusOne(const char* name) { diff --git a/indra/alscript/allsltraits.h b/indra/alscript/lsl/allsltraits.h similarity index 91% rename from indra/alscript/allsltraits.h rename to indra/alscript/lsl/allsltraits.h index ba62fe46f48..932b556608e 100644 --- a/indra/alscript/allsltraits.h +++ b/indra/alscript/lsl/allsltraits.h @@ -106,4 +106,10 @@ class ALLSLTraits // something, and INVENTORY_NONE. What the definitions do not say, so // kept by hand. static bool atLeastMinusOne(const char* name); + // The least and the most an integer function answers, where either is + // narrower than any integer: a count's nought or more, a find's -1 or + // more, 1 or 0 where the definitions say it answers a truth, a type's + // TYPE_INVALID to TYPE_ROTATION. Kept by hand beside the definitions' + // truths; false for a function it says nothing of. + static bool bounds(const char* name, S32& least, S32& most); }; diff --git a/indra/alscript/allsltraits.inc b/indra/alscript/lsl/allsltraits.inc similarity index 100% rename from indra/alscript/allsltraits.inc rename to indra/alscript/lsl/allsltraits.inc diff --git a/indra/alscript/allsluuids.inc b/indra/alscript/lsl/allsluuids.inc similarity index 100% rename from indra/alscript/allsluuids.inc rename to indra/alscript/lsl/allsluuids.inc diff --git a/indra/alscript/allslvalues.cpp b/indra/alscript/lsl/allslvalues.cpp similarity index 82% rename from indra/alscript/allslvalues.cpp rename to indra/alscript/lsl/allslvalues.cpp index 95e2ff861f5..57502e5dd4b 100644 --- a/indra/alscript/allslvalues.cpp +++ b/indra/alscript/lsl/allslvalues.cpp @@ -106,6 +106,49 @@ LSLConstant* ALLSLArithmetic::operation(LSLOperator op, LSLConstant* cv, LSLCons break; } } + // Two vectors' dot and cross products, worked out as a single does it, a + // step at a time, and as a double rounded once, and folded only where the + // two agree: LSO works in singles, and Mono's sums are not known to. The + // tailslide port's arithmetic has them as singles now (its + // vector-products.patch: the dot took x by the other's z, and the cross + // let a compiler fuse each product into its difference); that alone says + // nothing of where the two VMs part. + if (other && cv->getIType() == LST_VECTOR && other->getIType() == LST_VECTOR && (op == OP_MUL || op == OP_MOD)) + { + const Vector3* u = static_cast(cv)->getValue(); + const Vector3* v = static_cast(other)->getValue(); + if (!u || !v) + { + return nullptr; + } + // One operation of singles in double, which is exact enough, rounded + // once: what a single's own operation gives. + const auto mul = [](double a, double b) { return static_cast(static_cast(a * b)); }; + const auto add = [](double a, double b) { return static_cast(static_cast(a + b)); }; + const auto sub = [](double a, double b) { return static_cast(static_cast(a - b)); }; + const auto agreed = [](double stepped, double once, double& out) { + out = stepped; + return std::isfinite(stepped) && static_cast(static_cast(once)) == stepped; + }; + const double ux = u->x, uy = u->y, uz = u->z; + const double vx = v->x, vy = v->y, vz = v->z; + if (op == OP_MUL) + { + double dot = 0.0; + if (!agreed(add(add(mul(ux, vx), mul(uy, vy)), mul(uz, vz)), ux * vx + uy * vy + uz * vz, dot)) + { + return nullptr; + } + return _mAllocator->newTracked(dot); + } + double x = 0.0, y = 0.0, z = 0.0; + if (!agreed(sub(mul(uy, vz), mul(uz, vy)), uy * vz - uz * vy, x) || !agreed(sub(mul(uz, vx), mul(ux, vz)), uz * vx - ux * vz, y) || + !agreed(sub(mul(ux, vy), mul(uy, vx)), ux * vy - uy * vx, z)) + { + return nullptr; + } + return _mAllocator->newTracked(static_cast(x), static_cast(y), static_cast(z)); + } return rounded(TailslideOperationBehavior::operation(op, cv, other, lloc)); } diff --git a/indra/alscript/allslvalues.h b/indra/alscript/lsl/allslvalues.h similarity index 100% rename from indra/alscript/allslvalues.h rename to indra/alscript/lsl/allslvalues.h diff --git a/indra/alscript/lsl/optimizer/allslbranches.cpp b/indra/alscript/lsl/optimizer/allslbranches.cpp new file mode 100644 index 00000000000..68699697e4d --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslbranches.cpp @@ -0,0 +1,1369 @@ +/** + * @file allslbranches.cpp + * @brief The LSL optimizer's branches and loops in fewer jumps. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslbranches.h" + +#include "allslcosts.h" +#include "allsleffects.h" +#include "alscriptlexicon.h" + +#include + +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // Each rewrite is made a block at a time, the block's statements read + // into a list, changed there and put back: a statement moved is the + // one it was, counted neither out nor in (setStatements), so that what + // the references say holds until the run finds them again after the + // pass. Each rewrite takes away a jump, an else or a statement, so the + // block's rewrites come to an end. + class Branches : public ASTVisitor, public Pass + { + public: + Branches(Ctx& c, Report& r, const ALLSLOptimizer::Options& o, bool last) : Pass(c, r, o), mCosts(ALLSLCosts::of(o.target)), mLast(last) {} + + bool visit(LSLCompoundStatement* block) override + { + visitChildren(block); + while (rewrite(block)) + { + } + return false; + } + + private: + typedef std::vector Statements; + + const ALLSLCosts& mCosts; + // The last run, once the rounds are done: only what hides a loop + // from the dead code, which must have seen it whole first. + const bool mLast; + + static bool present(LSLASTNode* n) { return n && n->getNodeType() != NODE_NULL; } + + static bool isStatement(LSLASTNode* n, LSLNodeSubType type) + { + return n && n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == type; + } + + // A branch's statements: a block's, or the one it is; none of nothing. + static Statements statementsOf(LSLASTNode* branch) + { + Statements out; + if (!present(branch) || isStatement(branch, NODE_NOP_STATEMENT)) + { + return out; + } + if (isStatement(branch, NODE_COMPOUND_STATEMENT)) + { + for (LSLASTNode* stmt = branch->getChild(0); stmt; stmt = stmt->getNext()) + { + out.push_back(stmt); + } + return out; + } + out.push_back(branch); + return out; + } + + // The one statement a branch is, a block of one included. + static LSLASTNode* single(LSLASTNode* branch) + { + const Statements stmts = statementsOf(branch); + return stmts.size() == 1 ? stmts.front() : nullptr; + } + + // `x = value;`, of a variable whole: x and the value. + static std::optional> assignment(LSLASTNode* stmt) + { + if (!isStatement(stmt, NODE_EXPRESSION_STATEMENT)) + { + return std::nullopt; + } + LSLExpression* expr = static_cast(stmt)->getExpr(); + if (!expr || expr->getNodeSubType() != NODE_BINARY_EXPRESSION || expr->getOperation() != OP_ASSIGN) + { + return std::nullopt; + } + auto* target = static_cast(expr)->getLHS(); + if (!target || target->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(target)->getMember() || + !target->getSymbol()) + { + return std::nullopt; + } + return std::make_pair(static_cast(target), static_cast(expr)->getRHS()); + } + + // `return value;`: the value. + static LSLExpression* returned(LSLASTNode* stmt) + { + if (!isStatement(stmt, NODE_RETURN_STATEMENT) || !present(stmt->getChild(0)) || stmt->getChild(0)->getNodeType() != NODE_EXPRESSION) + { + return nullptr; + } + return static_cast(stmt->getChild(0)); + } + + // Whether a subtree names a symbol. + static bool mentions(LSLASTNode* root, LSLSymbol* sym) + { + bool found = false; + eachNode(root, [&](LSLASTNode* n) { found = found || (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym); }); + return found; + } + + // How many times a subtree names a symbol. + static int mentionsOf(LSLASTNode* root, LSLSymbol* sym) + { + int count = 0; + eachNode(root, [&](LSLASTNode* n) { count += n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym ? 1 : 0; }); + return count; + } + + static LSLASTNode* callable(LSLASTNode* node) + { + while (node && node->getNodeType() != NODE_GLOBAL_FUNCTION && node->getNodeType() != NODE_EVENT_HANDLER) + { + node = node->getParent(); + } + return node; + } + + // Whether every name a statement reads or writes is declared outside + // `outside`: what may move out of it and mean the same. + static bool namesFromOutside(LSLASTNode* stmt, LSLASTNode* outside) + { + bool inside = false; + eachNode(stmt, [&](LSLASTNode* n) { + if (inside || n->getNodeType() != NODE_IDENTIFIER) + { + return; + } + LSLSymbol* sym = static_cast(n)->getSymbol(); + if (!sym || sym->getSymbolType() != SYM_VARIABLE || sym->getSubType() != SYM_LOCAL) + { + return; + } + for (LSLASTNode* up = sym->getVarDecl(); up; up = up->getParent()) + { + if (up == outside) + { + inside = true; + return; + } + } + }); + return !inside; + } + + // Whether a state change in `branch` stands under no if of its own: + // in a function, which may change state only under an if, it must + // stay under the one it is in. + static bool bareStateChange(LSLASTNode* branch) + { + bool bare = false; + const std::function look = [&](LSLASTNode* n) { + if (bare || !n) + { + return; + } + if (isStatement(n, NODE_STATE_STATEMENT)) + { + bare = true; + return; + } + if (isStatement(n, NODE_IF_STATEMENT)) + { + return; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + look(child); + } + }; + look(branch); + return bare; + } + + bool inFunction(LSLASTNode* node) const + { + LSLASTNode* owner = callable(node); + return owner && owner->getNodeType() == NODE_GLOBAL_FUNCTION; + } + + // A branch made of statements: nothing, the one, or a block of them. + LSLStatement* branchOf(const Statements& stmts, LSLASTNode* at) + { + if (stmts.empty()) + { + auto* nop = ctx.allocator->newTracked(); + nop->setLoc(at->getLoc()); + return nop; + } + if (stmts.size() == 1 && !isStatement(stmts.front(), NODE_DECLARATION)) + { + return static_cast(stmts.front()); + } + auto* block = ctx.allocator->newTracked(nullptr); + block->setLoc(stmts.front()->getLoc()); + for (LSLASTNode* stmt : stmts) + { + block->pushChild(stmt); + } + return block; + } + + // A statement's place left, as its parent sees it: taken from a + // block, or from an if's or a loop's slot, where a null is put. + static void detach(LSLASTNode* stmt) + { + LSLASTNode* parent = stmt->getParent(); + if (!parent) + { + return; + } + if (isStatement(parent, NODE_COMPOUND_STATEMENT)) + { + parent->removeChild(stmt); + return; + } + parent->takeChild(stmt->getParentSlot()); + } + + // A branch's statements taken out of it, in order. + static Statements takeStatements(LSLASTNode* branch) + { + Statements stmts = statementsOf(branch); + for (LSLASTNode* stmt : stmts) + { + detach(stmt); + } + return stmts; + } + + // !(c), bracketed where c is an operation. + LSLExpression* negation(LSLExpression* c) + { + LSLExpression* operand = c; + if (c->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* parens = ctx.allocator->newTracked(c); + parens->setType(c->getType()); + parens->setLoc(c->getLoc()); + operand = parens; + } + auto* negated = ctx.allocator->newTracked(operand, OP_BOOLEAN_NOT); + negated->setType(TYPE(LST_INTEGER)); + negated->setLoc(c->getLoc()); + return negated; + } + + // The opposite of a check: a comparison of integers the other way, + // == and != of anything but lists each other; else !(c). + LSLExpression* notOf(LSLExpression* c) + { + LSLExpression* inner = bare(c); + if (inner && inner->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* cmp = static_cast(inner); + const bool ints = cmp->getLHS() && cmp->getRHS() && cmp->getLHS()->getIType() == LST_INTEGER && cmp->getRHS()->getIType() == LST_INTEGER; + const bool lists = cmp->getLHS() && cmp->getRHS() && (cmp->getLHS()->getIType() == LST_LIST || cmp->getRHS()->getIType() == LST_LIST); + LSLOperator to = OP_NONE; + switch (cmp->getOperation()) + { + case OP_LESS: to = ints ? OP_GEQ : OP_NONE; break; + case OP_GEQ: to = ints ? OP_LESS : OP_NONE; break; + case OP_GREATER: to = ints ? OP_LEQ : OP_NONE; break; + case OP_LEQ: to = ints ? OP_GREATER : OP_NONE; break; + case OP_EQ: to = lists ? OP_NONE : OP_NEQ; break; + case OP_NEQ: to = lists ? OP_NONE : OP_EQ; break; + default: break; + } + // != of floats as == is the one of them NaN tells apart. + if (to != OP_NONE && (ints || (cmp->getLHS()->getIType() != LST_FLOATINGPOINT && cmp->getRHS()->getIType() != LST_FLOATINGPOINT))) + { + cmp->setOperation(to); + return c; + } + } + return negation(c); + } + + // Whether a value is only ever 1 or 0. + static bool truthValue(LSLExpression* x) + { + const std::optional r = x && x->getIType() == LST_INTEGER ? rangeOf(x) : std::nullopt; + return r && r->least >= 0.0 && r->most <= 1.0; + } + + static std::optional integerValue(LSLASTNode* n) + { + LSLConstant* cv = n ? n->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT ? std::optional(static_cast(cv)->getValue()) + : std::nullopt; + } + + // c as the truth of itself, 1 or 0: as it is where it is one; !c, + // or !!c, where it is not. + LSLExpression* truthOf(LSLExpression* c, bool asTrue) + { + if (asTrue) + { + return truthValue(c) ? c : negation(negation(c)); + } + return negation(c); + } + + void noteAt(LSLASTNode* at, const char* key, const char* text, const std::string& before) + { + if (report.wanted()) + { + report.note(at->getLoc(), key, text, { before }); + } + ++changes; + } + + // ---- one rewrite of a block, the first there is --------------------------------- + + bool rewrite(LSLCompoundStatement* block) + { + Statements stmts; + for (LSLASTNode* stmt = block->getChild(0); stmt; stmt = stmt->getNext()) + { + stmts.push_back(stmt); + } + for (size_t i = 0; i < stmts.size(); ++i) + { + LSLASTNode* stmt = stmts[i]; + if (mLast) + { + // The loop made a jump back, and a jump out of it after + // that made one jump back where the check fails. + if ((isStatement(stmt, NODE_DO_STATEMENT) && foreverAsJump(block, stmts, i, static_cast(stmt))) || + (isStatement(stmt, NODE_IF_STATEMENT) && jumpOver(block, stmts, i, static_cast(stmt)))) + { + return true; + } + continue; + } + if (isStatement(stmt, NODE_IF_STATEMENT)) + { + auto* branch = static_cast(stmt); + if (emptyIf(block, stmts, i, branch) || truthChosen(block, stmts, i, branch) || sameWays(block, stmts, i, branch) || + elseAfterStop(block, stmts, i, branch) || jumpOver(block, stmts, i, branch) || setBeforeIf(block, stmts, i, branch)) + { + return true; + } + } + else if (isStatement(stmt, NODE_FOR_STATEMENT)) + { + auto* loop = static_cast(stmt); + if (countDown(block, stmts, i, loop) || forAsDo(block, stmts, i, loop)) + { + return true; + } + } + else if (isStatement(stmt, NODE_WHILE_STATEMENT)) + { + if (whileAsDo(block, stmts, i, static_cast(stmt))) + { + return true; + } + } + } + return false; + } + + // ---- an if with nothing in it ---------------------------------------------------- + + // An if with nothing to do either way: its check, where that does + // something, or nothing. + bool emptyIf(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + LSLExpression* c = stmt->getCheckExpr(); + if (!c || !statementsOf(stmt->getTrueBranch()).empty() || !statementsOf(stmt->getFalseBranch()).empty() || holdsLabel(stmt)) + { + return false; + } + const std::string before = report.wanted() ? render(c) : std::string(); + const Uncounted uncounted(*ctx.context); + if (changesNothing(c)) + { + stmts.erase(stmts.begin() + static_cast(i)); + } + else + { + auto* run = ctx.allocator->newTracked(static_cast(stmt->takeChild(0))); + run->setLoc(stmt->getLoc()); + stmts[i] = run; + } + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerRemovedEmptyIf", "removed if ([1]), which has nothing to do either way", before); + return true; + } + + // ---- a truth chosen by an if ------------------------------------------------------ + + // `if (c) return 1; return 0;` as `return c;`, and `if (c) x = 1; + // else x = 0;` as `x = c;` -- the other way round, `!c`; c as its + // truth, `!!c`, where it is not only ever 1 or 0. + bool truthChosen(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + LSLExpression* c = stmt->getCheckExpr(); + if (!c || c->getIType() != LST_INTEGER) + { + return false; + } + LSLASTNode* yes = single(stmt->getTrueBranch()); + LSLASTNode* no = present(stmt->getFalseBranch()) ? single(stmt->getFalseBranch()) : nullptr; + // Returns: the else's, or the statement after a lone if. + const bool lone = !present(stmt->getFalseBranch()) && i + 1 < stmts.size(); + LSLASTNode* other = no ? no : lone ? stmts[i + 1] : nullptr; + LSLASTNode* owner = callable(stmt); + const bool ints = owner && owner->getNodeType() == NODE_GLOBAL_FUNCTION && + static_cast(owner)->getIdentifier()->getIType() == LST_INTEGER; + if (LSLExpression* a = returned(yes); a && other && ints) + { + LSLExpression* b = returned(other); + const std::optional va = integerValue(a); + const std::optional vb = b ? integerValue(b) : std::nullopt; + if (!va || !vb || *va == *vb || (*va != 0 && *va != 1) || (*vb != 0 && *vb != 1) || a->getIType() != LST_INTEGER) + { + return false; + } + const std::string before = report.wanted() ? render(stmt) : std::string(); + const Uncounted uncounted(*ctx.context); + stmt->takeChild(0); + auto* made = ctx.allocator->newTracked(truthOf(c, *va == 1)); + made->setLoc(stmt->getLoc()); + stmts[i] = made; + if (!no) + { + stmts.erase(stmts.begin() + static_cast(i) + 1); + } + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerReturnedTruth", "returned the truth of what [1] chose", before); + return true; + } + const auto set = assignment(yes); + const auto unset = no ? assignment(no) : std::nullopt; + if (!set || !unset || set->first->getSymbol() != unset->first->getSymbol() || set->first->getIType() != LST_INTEGER) + { + return false; + } + const std::optional va = integerValue(set->second); + const std::optional vb = integerValue(unset->second); + if (!va || !vb || *va == *vb || (*va != 0 && *va != 1) || (*vb != 0 && *vb != 1)) + { + return false; + } + const std::string before = report.wanted() ? render(stmt) : std::string(); + const Uncounted uncounted(*ctx.context); + stmt->takeChild(0); + auto* target = static_cast(set->first->getParent()->takeChild(0)); + auto* value = truthOf(c, *va == 1); + auto* made = ctx.allocator->newTracked(target, OP_ASSIGN, value); + made->setType(target->getType()); + made->setLoc(stmt->getLoc()); + auto* line = ctx.allocator->newTracked(made); + line->setLoc(stmt->getLoc()); + stmts[i] = line; + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerReturnedTruth", "returned the truth of what [1] chose", before); + return true; + } + + // ---- ways the same --------------------------------------------------------------- + + // An if whose ways are the same as one way, its check run first where + // it does something; and the statements both ways end or begin + // with, once after or before it -- before only where the check reads + // nothing they change and could be read at another time. + bool sameWays(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + LSLExpression* c = stmt->getCheckExpr(); + if (!c || !present(stmt->getFalseBranch()) || holdsLabel(stmt)) + { + return false; + } + LSLStatement* yes = stmt->getTrueBranch(); + LSLStatement* no = stmt->getFalseBranch(); + const std::string was = report.wanted() ? render(stmt) : std::string(); + if (render(yes) == render(no)) + { + const Uncounted uncounted(*ctx.context); + Statements made; + if (!changesNothing(c)) + { + auto* run = ctx.allocator->newTracked(static_cast(stmt->takeChild(0))); + run->setLoc(stmt->getLoc()); + made.push_back(run); + } + // A block stays one, its locals its own. + made.push_back(stmt->takeChild(1)); + stmts.erase(stmts.begin() + static_cast(i)); + stmts.insert(stmts.begin() + static_cast(i), made.begin(), made.end()); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerMergedWays", "wrote the if [1] once, its ways being the same", was); + return true; + } + Statements a = statementsOf(yes); + Statements b = statementsOf(no); + if (a.empty() || b.empty()) + { + return false; + } + const auto movable = [&](LSLASTNode* x) { + return !isStatement(x, NODE_DECLARATION) && !isStatement(x, NODE_LABEL) && namesFromOutside(x, stmt); + }; + // The last of each, after the if. + if (render(a.back()) == render(b.back()) && movable(a.back())) + { + const Uncounted uncounted(*ctx.context); + LSLASTNode* kept = a.back(); + detach(kept); + detach(b.back()); + tidy(stmt); + stmts.insert(stmts.begin() + static_cast(i) + 1, kept); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerMergedEnds", "wrote what both ways of the if [1] end with once, after it", was); + return true; + } + // The first of each, before the if. + if (render(a.front()) == render(b.front()) && movable(a.front()) && sideEffectFree(c)) + { + const ALLSLEffects::Writes writes = ctx.effects->of(a.front()); + bool read = false; + eachNode(c, [&](LSLASTNode* n) { + read = read || (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION && writes.writes(n->getSymbol())); + }); + if (read) + { + return false; + } + const Uncounted uncounted(*ctx.context); + LSLASTNode* kept = a.front(); + detach(kept); + detach(b.front()); + tidy(stmt); + stmts.insert(stmts.begin() + static_cast(i), kept); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerMergedStarts", "wrote what both ways of the if [1] begin with once, before it", was); + return true; + } + return false; + } + + // An if's ways after statements were taken from them: a true way + // left null made an empty statement, and an else left empty gone. + void tidy(LSLIfStatement* stmt) + { + if (!present(stmt->getChild(1))) + { + auto* nop = ctx.allocator->newTracked(); + nop->setLoc(stmt->getLoc()); + stmt->setChild(1, nop); + } + if (present(stmt->getChild(2)) && statementsOf(stmt->getChild(2)).empty()) + { + stmt->setChild(2, stmt->newNullNode()); + } + } + + // ---- an else after a way that never goes on -------------------------------------- + + // `if (c) { ...; return; } else B` as `if (c) { ...; return; } B`: + // B runs where c is false either way, without the jump over it. + bool elseAfterStop(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + LSLStatement* no = stmt->getFalseBranch(); + if (!present(no) || !stops(stmt->getTrueBranch()) || (inFunction(stmt) && bareStateChange(no))) + { + return false; + } + const std::string before = report.wanted() ? render(stmt->getCheckExpr()) : std::string(); + const Uncounted uncounted(*ctx.context); + LSLASTNode* otherwise = stmt->takeChild(2); + stmts.insert(stmts.begin() + static_cast(i) + 1, otherwise); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerDroppedElse", "dropped the else of if ([1]), whose way never goes on", before); + return true; + } + + // ---- a jump over statements ------------------------------------------------------ + + // `if (c) { A; jump L; } S; @L;` as `if (c) A; else S;`: where L + // is jumped to from there alone, the only label of its name, and + // nothing S declares is named after it. + bool jumpOver(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + if (present(stmt->getFalseBranch())) + { + return false; + } + Statements way = statementsOf(stmt->getTrueBranch()); + if (way.empty() || !isStatement(way.back(), NODE_JUMP_STATEMENT)) + { + return false; + } + auto* jump = static_cast(way.back()); + LSLIdentifier* id = jump->getIdentifier(); + LSLSymbol* target = id ? id->getSymbol() : nullptr; + if (!target || !id->getName()) + { + return false; + } + size_t at = i + 1; + while (at < stmts.size() && !(isStatement(stmts[at], NODE_LABEL) && stmts[at]->getSymbol() == target)) + { + ++at; + } + if (at >= stmts.size() || at == i + 1 || labelsNamed(stmts[at], id->getName()) != 1) + { + return false; + } + // Named by the label and this jump alone. + if (mentionsOf(callable(stmt), target) != 2) + { + return false; + } + Statements over(stmts.begin() + static_cast(i) + 1, stmts.begin() + static_cast(at)); + for (LSLASTNode* s : over) + { + if (holdsLabel(s) || (inFunction(stmt) && bareStateChange(s))) + { + return false; + } + } + // What S declares, named only in S. + for (LSLASTNode* s : over) + { + if (!isStatement(s, NODE_DECLARATION)) + { + continue; + } + LSLSymbol* sym = s->getSymbol(); + int inside = 0; + for (LSLASTNode* t : over) + { + inside += mentionsOf(t, sym); + } + if (!sym || inside != mentionsOf(callable(stmt), sym)) + { + return false; + } + } + const std::string before = report.wanted() ? render(id) : std::string(); + const Uncounted uncounted(*ctx.context); + LSLASTNode* label = stmts[at]; + way.pop_back(); + takeStatements(stmt->getTrueBranch()); + for (LSLASTNode* s : over) + { + detach(s); + } + detach(label); + LSLExpression* c = stmt->getCheckExpr(); + if (way.empty() && c && c->getIType() == LST_INTEGER) + { + // Nothing else that way: if (!c) S. + stmt->setChild(0, notOf(static_cast(stmt->takeChild(0)))); + stmt->setChild(1, branchOf(over, stmt)); + } + else + { + stmt->setChild(1, branchOf(way, stmt)); + stmt->setChild(2, branchOf(over, stmt)); + } + stmts.erase(stmts.begin() + static_cast(i) + 1, stmts.begin() + static_cast(at) + 1); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerJumpAsIf", "wrote the jump to [1] over what follows as an else", before); + return true; + } + + // ---- a value set before an if ---------------------------------------------------- + + // `if (c) x = a; else x = B;` as `x = B; if (c) x = a;`, B a + // constant, of a local or a parameter neither c nor a reads; or, + // both constant integers, as `x = (c) * (A - B) + B`, c as its truth. + bool setBeforeIf(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLIfStatement* stmt) + { + LSLExpression* c = stmt->getCheckExpr(); + const auto set = assignment(single(stmt->getTrueBranch())); + const auto unset = present(stmt->getFalseBranch()) ? assignment(single(stmt->getFalseBranch())) : std::nullopt; + if (!c || !set || !unset || set->first->getSymbol() != unset->first->getSymbol()) + { + return false; + } + LSLSymbol* sym = set->first->getSymbol(); + if (!unset->second->getConstantValue()) + { + return false; + } + const std::string before = report.wanted() ? render(stmt) : std::string(); + const std::optional va = integerValue(set->second); + const std::optional vb = integerValue(unset->second); + const Uncounted uncounted(*ctx.context); + // (Both ways set what c and a read nothing of, below; the arithmetic + // sets it once, after c runs, as the if did.) + // Of a truth smaller on every target; of anything else, as !!c, + // where the target has it so. + if (va && vb && c->getIType() == LST_INTEGER && sym->getIType() == LST_INTEGER && (truthValue(c) || mCosts.arithmeticForSelect)) + { + // truth * (A - B) + B, in 32 bits as the VM has it. + const S32 step = static_cast(static_cast(*va) - static_cast(*vb)); + LSLExpression* value = truthOf(static_cast(stmt->takeChild(0)), true); + if (value->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* parens = ctx.allocator->newTracked(value); + parens->setType(value->getType()); + value = parens; + } + if (step != 1) + { + auto* product = ctx.allocator->newTracked(value, OP_MUL, constant(ctx.integer(step), stmt)); + product->setType(TYPE(LST_INTEGER)); + value = product; + } + if (*vb != 0) + { + auto* sum = ctx.allocator->newTracked(value, OP_PLUS, constant(ctx.integer(*vb), stmt)); + sum->setType(TYPE(LST_INTEGER)); + value = sum; + } + auto* target = static_cast(set->first->getParent()->takeChild(0)); + auto* made = ctx.allocator->newTracked(target, OP_ASSIGN, value); + made->setType(target->getType()); + made->setLoc(stmt->getLoc()); + auto* line = ctx.allocator->newTracked(made); + line->setLoc(stmt->getLoc()); + stmts[i] = line; + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerSelectAsArithmetic", "wrote [1] as arithmetic", before); + return true; + } + // Set before the check, only where nothing could see it early. + if ((sym->getSubType() != SYM_LOCAL && sym->getSubType() != SYM_FUNCTION_PARAMETER && sym->getSubType() != SYM_EVENT_PARAMETER) || + mentions(c, sym) || mentions(set->second, sym)) + { + return false; + } + LSLASTNode* first = single(stmt->getFalseBranch()); + detach(first); + stmt->setChild(2, stmt->newNullNode()); + stmts.insert(stmts.begin() + static_cast(i), first); + setStatements(block, stmts, *ctx.context); + noteAt(stmt, "OptimizerSetBeforeIf", "set the value of [1]'s else before it", before); + return true; + } + + // ---- loops ----------------------------------------------------------------------- + + typedef boost::unordered_flat_map Known; + + // An integer expression worked out from what is known of the locals + // it reads, where everything it reads is known. + static std::optional worked(LSLExpression* e, const Known& known) + { + e = bare(e); + if (!e || e->getIType() != LST_INTEGER) + { + return std::nullopt; + } + if (const std::optional v = integerValue(e)) + { + return v; + } + switch (e->getNodeSubType()) + { + case NODE_LVALUE_EXPRESSION: + { + const auto found = static_cast(e)->getMember() ? known.end() : known.find(e->getSymbol()); + return found == known.end() ? std::nullopt : std::optional(found->second); + } + case NODE_UNARY_EXPRESSION: + { + const std::optional x = worked(static_cast(e)->getChildExpr(), known); + if (!x) + { + return std::nullopt; + } + switch (e->getOperation()) + { + case OP_BOOLEAN_NOT: return *x ? 0 : 1; + case OP_BIT_NOT: return ~*x; + case OP_MINUS: return static_cast(0u - static_cast(*x)); + default: return std::nullopt; + } + } + case NODE_BINARY_EXPRESSION: + { + auto* b = static_cast(e); + if (!b->getLHS() || !b->getRHS() || b->getLHS()->getIType() != LST_INTEGER || b->getRHS()->getIType() != LST_INTEGER) + { + return std::nullopt; + } + const std::optional l = worked(b->getLHS(), known); + const std::optional r = worked(b->getRHS(), known); + if (!l || !r) + { + return std::nullopt; + } + switch (b->getOperation()) + { + case OP_LESS: return *l < *r; + case OP_LEQ: return *l <= *r; + case OP_GREATER: return *l > *r; + case OP_GEQ: return *l >= *r; + case OP_EQ: return *l == *r; + case OP_NEQ: return *l != *r; + case OP_BOOLEAN_AND: return *l && *r; + case OP_BOOLEAN_OR: return *l || *r; + case OP_BIT_AND: return *l & *r; + case OP_BIT_OR: return *l | *r; + case OP_BIT_XOR: return *l ^ *r; + case OP_PLUS: return static_cast(static_cast(*l) + static_cast(*r)); + case OP_MINUS: return static_cast(static_cast(*l) - static_cast(*r)); + default: return std::nullopt; + } + } + default: + return std::nullopt; + } + } + + // What a local integer is set to by `x = C`, the constant. + static std::optional> setTo(LSLASTNode* expr) + { + if (!expr || expr->getNodeType() != NODE_EXPRESSION || expr->getNodeSubType() != NODE_BINARY_EXPRESSION || + static_cast(expr)->getOperation() != OP_ASSIGN) + { + return std::nullopt; + } + auto* b = static_cast(expr); + LSLSymbol* sym = b->getLHS() && b->getLHS()->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(b->getLHS())->getMember() + ? b->getLHS()->getSymbol() + : nullptr; + const std::optional v = integerValue(b->getRHS()); + if (!sym || !v || sym->getIType() != LST_INTEGER || sym->getSubType() != SYM_LOCAL) + { + return std::nullopt; + } + return std::make_pair(sym, *v); + } + + // What a for's first part leaves known: each `x = C` in turn, and + // anything else it writes forgotten. + Known knownAfter(LSLASTNode* inits) const + { + Known known; + for (LSLASTNode* init = inits ? inits->getChild(0) : nullptr; init; init = init->getNext()) + { + if (const auto set = setTo(init)) + { + known[set->first] = set->second; + continue; + } + for (LSLSymbol* sym : ctx.effects->of(init).variables) + { + known.erase(sym); + } + } + return known; + } + + // The one step of a for that adds one to, or takes one from, a + // local integer: the local and which way. + static std::optional> stepOf(LSLASTNode* steps) + { + LSLASTNode* step = steps ? steps->getChild(0) : nullptr; + if (!step || step->getNext() || step->getNodeType() != NODE_EXPRESSION) + { + return std::nullopt; + } + auto* e = static_cast(step); + LSLSymbol* sym = nullptr; + int way = 0; + const auto var = [](LSLExpression* x) -> LSLSymbol* { + x = bare(x); + return x && x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember() ? x->getSymbol() : nullptr; + }; + if (e->getNodeSubType() == NODE_UNARY_EXPRESSION) + { + const LSLOperator op = e->getOperation(); + way = op == OP_PRE_INCR || op == OP_POST_INCR ? 1 : op == OP_PRE_DECR || op == OP_POST_DECR ? -1 : 0; + sym = var(static_cast(e)->getChildExpr()); + } + else if (e->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* b = static_cast(e); + const LSLOperator op = b->getOperation(); + sym = var(b->getLHS()); + if ((op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN) && isInteger(b->getRHS(), 1)) + { + way = op == OP_ADD_ASSIGN ? 1 : -1; + } + else if (op == OP_ASSIGN && bare(b->getRHS())->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* sum = static_cast(bare(b->getRHS())); + if (sum->getOperation() == OP_PLUS && var(sum->getLHS()) == sym && isInteger(sum->getRHS(), 1)) + { + way = 1; + } + else if (sum->getOperation() == OP_MINUS && var(sum->getLHS()) == sym && isInteger(sum->getRHS(), 1)) + { + way = -1; + } + } + } + if (!sym || way == 0 || sym->getIType() != LST_INTEGER || sym->getSubType() != SYM_LOCAL) + { + return std::nullopt; + } + return std::make_pair(sym, way); + } + + // The for's parts taken apart: its first part as statements, its + // body and its check. + struct Parts + { + Statements inits; + LSLStatement* body = nullptr; + LSLExpression* check = nullptr; + }; + Parts takeApart(LSLForStatement* loop) + { + Parts parts; + LSLASTNode* inits = loop->getInitExprs(); + while (inits && inits->hasChildren()) + { + auto* expr = static_cast(inits->takeChild(0)); + inits->removeChild(inits->getChild(0)); + auto* line = ctx.allocator->newTracked(expr); + line->setLoc(expr->getLoc()); + parts.inits.push_back(line); + } + parts.body = static_cast(loop->takeChild(3)); + parts.check = static_cast(loop->takeChild(1)); + return parts; + } + + // A statement made a body's last: the body a block where it was not. + LSLStatement* ending(LSLStatement* body, LSLStatement* last) + { + if (!isStatement(body, NODE_COMPOUND_STATEMENT)) + { + auto* block = ctx.allocator->newTracked(nullptr); + block->setLoc(body->getLoc()); + if (!isStatement(body, NODE_NOP_STATEMENT)) + { + block->pushChild(body); + } + body = block; + } + body->pushChild(last); + return body; + } + + LSLDoStatement* doOf(LSLStatement* body, LSLExpression* check, LSLASTNode* at) + { + auto* made = ctx.allocator->newTracked(body, check); + made->setLoc(at->getLoc()); + return made; + } + + // `--x` or `++x` of a local, new. + LSLExpression* stepped(LSLSymbol* sym, int way, LSLASTNode* at) + { + auto* id = ctx.allocator->newTracked(sym->getType(), sym->getName()); + id->setSymbol(sym); + id->setLoc(at->getLoc()); + auto* read = ctx.allocator->newTracked(id, static_cast(nullptr)); + read->setType(sym->getType()); + read->setLoc(at->getLoc()); + auto* made = ctx.allocator->newTracked(read, way > 0 ? OP_PRE_INCR : OP_PRE_DECR); + made->setType(TYPE(LST_INTEGER)); + made->setLoc(at->getLoc()); + return made; + } + + // `for (i = A; i < B; ++i) S`, S never naming i, nor anything after + // the loop: S run B - A times by `i = B - A; do S while (--i);`; + // or, B not known but never below nought and A nought, `i = B; + // while (i--) S` -- B read once, where nothing S does changes it. + bool countDown(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLForStatement* loop) + { + LSLASTNode* inits = loop->getInitExprs(); + LSLASTNode* init = inits ? inits->getChild(0) : nullptr; + const auto set = init && !init->getNext() ? setTo(init) : std::nullopt; + const auto step = stepOf(loop->getIncrExprs()); + LSLExpression* check = bare(loop->getCheckExpr()); + if (!set || !step || set->first != step->first || !check || check->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return false; + } + LSLSymbol* sym = set->first; + auto* cmp = static_cast(check); + const auto isSym = [sym](LSLExpression* x) { + x = bare(x); + return x && x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember() && x->getSymbol() == sym; + }; + // i < B, i <= B, i > B, i >= B, either way round. + LSLOperator op = cmp->getOperation(); + LSLExpression* bound = nullptr; + if (isSym(cmp->getLHS())) + { + bound = cmp->getRHS(); + } + else if (isSym(cmp->getRHS())) + { + bound = cmp->getLHS(); + op = op == OP_LESS ? OP_GREATER : op == OP_GREATER ? OP_LESS : op == OP_LEQ ? OP_GEQ : op == OP_GEQ ? OP_LEQ : op; + } + const bool up = step->second > 0; + if (!bound || mentions(bound, sym) || !((up && (op == OP_LESS || op == OP_LEQ)) || (!up && (op == OP_GREATER || op == OP_GEQ)))) + { + return false; + } + // i named in the loop's head and its declaration, nowhere else. + LSLASTNode* body = loop->getBody(); + if (mentions(body, sym) || mentionsOf(callable(loop), sym) != mentionsOf(loop, sym) + 1) + { + return false; + } + const std::optional b = integerValue(bound); + const std::string before = report.wanted() ? render(loop->getCheckExpr()) : std::string(); + if (b) + { + const int64_t count = (up ? int64_t(*b) - int64_t(set->second) : int64_t(set->second) - int64_t(*b)) + + (op == OP_LEQ || op == OP_GEQ ? 1 : 0); + if (count < 1 || count > INT32_MAX) + { + return false; + } + const Uncounted uncounted(*ctx.context); + Parts parts = takeApart(loop); + // i = count, where the first part set i = A. + auto* first = static_cast(parts.inits.front()); + renumber(static_cast(first->getExpr()), ctx.integer(static_cast(count))); + Statements made = parts.inits; + made.push_back(doOf(parts.body, stepped(sym, -1, loop), loop)); + stmts.erase(stmts.begin() + static_cast(i)); + stmts.insert(stmts.begin() + static_cast(i), made.begin(), made.end()); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerCountedDown", "counted the loop while ([1]) down to nought", before); + return true; + } + const std::optional r = rangeOf(bound); + bool moved = false; + for (LSLSymbol* written : ctx.effects->of(body).variables) + { + moved = moved || mentions(bound, written); + } + if (op != OP_LESS || set->second != 0 || !r || r->least < 0.0 || !sideEffectFree(bound) || moved) + { + return false; + } + const Uncounted uncounted(*ctx.context); + Parts parts = takeApart(loop); + auto* first = static_cast(parts.inits.front()); + auto* sets = static_cast(first->getExpr()); + // i = B, B taken from the check. + LSLASTNode* boundParent = bound->getParent(); + boundParent->takeChild(bound->getParentSlot()); + sets->setChild(1, bound); + auto* id = ctx.allocator->newTracked(sym->getType(), sym->getName()); + id->setSymbol(sym); + id->setLoc(loop->getLoc()); + auto* read = ctx.allocator->newTracked(id, static_cast(nullptr)); + read->setType(sym->getType()); + read->setLoc(loop->getLoc()); + auto* down = ctx.allocator->newTracked(read, OP_POST_DECR); + down->setType(TYPE(LST_INTEGER)); + down->setLoc(loop->getLoc()); + auto* made = ctx.allocator->newTracked(down, parts.body); + made->setLoc(loop->getLoc()); + Statements all = parts.inits; + all.push_back(made); + stmts.erase(stmts.begin() + static_cast(i)); + stmts.insert(stmts.begin() + static_cast(i), all.begin(), all.end()); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerCountedDown", "counted the loop while ([1]) down to nought", before); + return true; + } + + void renumber(LSLBinaryExpression* assignment, LSLConstant* cv) + { + LSLASTNode* value = assignment->getChild(1); + LSLASTNode::replaceNode(value, constant(cv, value)); + } + + // A for whose first check is known to fail, from what its first part + // sets, gone but for that part; and one known to pass run as a do: its step made the check's read of what it steps + // where that is the check's one read of it -- `while (++i < 10)` -- + // or put at the body's end. Where the target has a do the smaller. + bool forAsDo(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLForStatement* loop) + { + LSLExpression* check = loop->getCheckExpr(); + if (!check || holdsLabel(loop)) + { + return false; + } + const std::optional first = worked(check, knownAfter(loop->getInitExprs())); + if (!first) + { + return false; + } + if (!*first) + { + // Never run: its first part alone. + const std::string before = report.wanted() ? render(check) : std::string(); + const Uncounted uncounted(*ctx.context); + Parts parts = takeApart(loop); + stmts.erase(stmts.begin() + static_cast(i)); + stmts.insert(stmts.begin() + static_cast(i), parts.inits.begin(), parts.inits.end()); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerLoopNeverRuns", "removed the loop on ([1]), its first check known to fail", before); + return true; + } + if (!mCosts.doForKnownFirst) + { + return false; + } + const std::string before = report.wanted() ? render(check) : std::string(); + const auto step = stepOf(loop->getIncrExprs()); + // The check's one read of the stepped local. + LSLLValueExpression* read = nullptr; + int reads = 0; + if (step) + { + eachNode(check, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == step->first) + { + ++reads; + LSLASTNode* parent = n->getParent(); + read = parent && parent->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(parent)->getMember() + ? static_cast(parent) + : nullptr; + } + }); + } + const Uncounted uncounted(*ctx.context); + Parts parts = takeApart(loop); + LSLStatement* body = parts.body; + if (step && reads == 1 && read) + { + LSLASTNode* parent = read == parts.check ? nullptr : read->getParent(); + const int slot = read->getParentSlot(); + if (parent) + { + parent->takeChild(slot); + } + auto* made = ctx.allocator->newTracked(read, step->second > 0 ? OP_PRE_INCR : OP_PRE_DECR); + made->setType(TYPE(LST_INTEGER)); + made->setLoc(read->getLoc()); + if (parent) + { + parent->setChild(slot, made); + } + else + { + parts.check = made; + } + } + else + { + LSLASTNode* steps = loop->getIncrExprs(); + while (steps && steps->hasChildren()) + { + auto* expr = static_cast(steps->takeChild(0)); + steps->removeChild(steps->getChild(0)); + auto* line = ctx.allocator->newTracked(expr); + line->setLoc(expr->getLoc()); + body = ending(body, line); + } + } + Statements made = parts.inits; + made.push_back(doOf(body, parts.check, loop)); + stmts.erase(stmts.begin() + static_cast(i)); + stmts.insert(stmts.begin() + static_cast(i), made.begin(), made.end()); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerLoopAsDo", "ran the loop on ([1]) as a do, its first check known to pass", before); + return true; + } + + // A do whose check is a constant other than nought, a loop that + // never ends: `@l; S jump l;`, l a label the function has no name + // like. Where the target has the jump the smaller; in the last run + // alone, since what follows such a loop holds a label the dead code + // would then take for one a jump comes back to. + bool foreverAsJump(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLDoStatement* loop) + { + const std::optional check = integerValue(loop->getCheckExpr()); + if (!mCosts.jumpForForever || !check || !*check) + { + return false; + } + // A name nothing in the function has. + boost::unordered_flat_map names; + eachNode(callable(loop), [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) + { + names.emplace(static_cast(n)->getName(), true); + } + }); + std::string name = "loop"; + for (int k = 2; names.contains(name) || ALScriptLexicon::lslWord(name) != ALScriptLexicon::LSL_NAME || + (ctx.context->builtins && ctx.context->builtins->lookup(name.c_str(), SYM_ANY)); + ++k) + { + name = "loop" + std::to_string(k); + } + const std::string before = report.wanted() ? render(loop->getCheckExpr()) : std::string(); + const char* id = ctx.allocator->copyStr(name.c_str()); + auto* at = ctx.allocator->newTracked(TYPE(LST_NULL), id); + at->setLoc(loop->getLoc()); + auto* label = ctx.allocator->newTracked(at); + label->setLoc(loop->getLoc()); + auto* sym = ctx.allocator->newTracked(id, TYPE(LST_NULL), SYM_LABEL, SYM_LOCAL, loop->getLoc(), nullptr, nullptr, label); + at->setSymbol(sym); + auto* to = ctx.allocator->newTracked(TYPE(LST_NULL), id); + to->setSymbol(sym); + to->setLoc(loop->getLoc()); + auto* jump = ctx.allocator->newTracked(to); + jump->setLoc(loop->getLoc()); + const Uncounted uncounted(*ctx.context); + LSLASTNode* body = loop->takeChild(0); + stmts.erase(stmts.begin() + static_cast(i)); + Statements made{ label }; + // The body's statements in the block where they declare and + // label nothing, so that a jump out of the loop stands beside + // the jump back. + bool flat = isStatement(body, NODE_COMPOUND_STATEMENT); + for (LSLASTNode* stmt = flat ? body->getChild(0) : nullptr; stmt; stmt = stmt->getNext()) + { + flat = flat && !isStatement(stmt, NODE_DECLARATION) && !isStatement(stmt, NODE_LABEL); + } + if (flat) + { + for (LSLASTNode* stmt : takeStatements(body)) + { + made.push_back(stmt); + } + } + else + { + made.push_back(body); + } + made.push_back(jump); + stmts.insert(stmts.begin() + static_cast(i), made.begin(), made.end()); + setStatements(block, stmts, *ctx.context); + label->defineSymbol(sym); + noteAt(loop, "OptimizerForeverAsJump", "ran the loop on ([1]), which never ends, by a jump back", before); + return true; + } + + // A while whose first check is known, from the constants the + // statements just before it set: gone where it fails, run as a do + // where it passes. + bool whileAsDo(LSLCompoundStatement* block, Statements& stmts, size_t i, LSLWhileStatement* loop) + { + LSLExpression* check = loop->getCheckExpr(); + if (!check || holdsLabel(loop)) + { + return false; + } + Known known; + for (size_t k = i; k-- > 0;) + { + LSLASTNode* stmt = stmts[k]; + if (isStatement(stmt, NODE_EXPRESSION_STATEMENT)) + { + const auto set = setTo(static_cast(stmt)->getExpr()); + if (!set) + { + break; + } + known.try_emplace(set->first, set->second); + continue; + } + if (isStatement(stmt, NODE_DECLARATION)) + { + LSLSymbol* sym = stmt->getSymbol(); + LSLASTNode* init = static_cast(stmt)->getInitializer(); + const std::optional v = present(init) ? integerValue(init) : std::optional(0); + if (!sym || !v || sym->getIType() != LST_INTEGER) + { + break; + } + known.try_emplace(sym, *v); + continue; + } + break; + } + const std::optional first = worked(check, known); + if (!first) + { + return false; + } + const std::string before = report.wanted() ? render(check) : std::string(); + if (!*first) + { + const Uncounted uncounted(*ctx.context); + stmts.erase(stmts.begin() + static_cast(i)); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerLoopNeverRuns", "removed the loop on ([1]), its first check known to fail", before); + return true; + } + if (!mCosts.doForKnownFirst) + { + return false; + } + const Uncounted uncounted(*ctx.context); + auto* body = static_cast(loop->takeChild(1)); + auto* condition = static_cast(loop->takeChild(0)); + stmts[i] = doOf(body, condition, loop); + setStatements(block, stmts, *ctx.context); + noteAt(loop, "OptimizerLoopAsDo", "ran the loop on ([1]) as a do, its first check known to pass", before); + return true; + } + }; +} + + int restructure(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, bool last) + { + Branches branches(ctx, report, options, last); + script->visit(&branches); + return branches.changes; + } +} diff --git a/indra/alscript/lsl/optimizer/allslbranches.h b/indra/alscript/lsl/optimizer/allslbranches.h new file mode 100644 index 00000000000..6722a51466b --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslbranches.h @@ -0,0 +1,41 @@ +/** + * @file allslbranches.h + * @brief The LSL optimizer's branches and loops in fewer jumps. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // Branches and loops written with fewer jumps, each the same code run + // the same way: a truth returned or set where an if chose 1 or 0; an + // if whose ways are the same, or begin or end the same, written once; + // an else after a way that never goes on made what follows; a jump over + // statements made an if; a value set before an if rather than in its + // else; and a loop whose first check is known to pass run as a do, or + // counted down where its counter is read nowhere else. How many + // changes it made. `last`, once the rounds are done: a loop that never + // ends run by a jump back, which the dead code must not see. + int restructure(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, bool last = false); +} diff --git a/indra/alscript/lsl/optimizer/allslconstantglobals.cpp b/indra/alscript/lsl/optimizer/allslconstantglobals.cpp new file mode 100644 index 00000000000..43bb6cab2f1 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslconstantglobals.cpp @@ -0,0 +1,165 @@ +/** + * @file allslconstantglobals.cpp + * @brief The LSL optimizer's constants written at many places kept once in a global. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslconstantglobals.h" + +#include "alscriptlexicon.h" + +#include +#include + +#include + +namespace ALLSLPasses +{ +namespace +{ + class ConstantGlobals : public Pass + { + public: + using Pass::Pass; + + int run(LSLScript* script) + { + boost::unordered_flat_set> taken; + eachNode(script, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) + { + taken.insert(static_cast(n)->getName()); + } + }); + // Each constant's places in the functions and handlers, by its + // type and value, in the order they come. + struct Places + { + std::vector at; + }; + std::vector order; + boost::unordered_flat_map> places; + const auto gather = [&](LSLASTNode* body) { + eachNode(body, [&](LSLASTNode* n) { + if (n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_CONSTANT_EXPRESSION || !n->getConstantValue() || + n->getIType() == LST_LIST || n->getConstantValue()->containsNaN()) + { + return; + } + const std::string key = std::to_string(static_cast(n->getIType())) + "\n" + render(n); + auto [it, fresh] = places.try_emplace(key); + if (fresh) + { + order.push_back(key); + } + it->second.at.push_back(static_cast(n)); + }); + }; + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_FUNCTION) + { + gather(global); + } + } + for (LSLASTNode* state : *script->getStates()) + { + gather(state); + } + std::vector made; + for (const std::string& key : order) + { + const std::vector& at = places[key].at; + LSLConstant* cv = at.front()->getConstantValue(); + const std::optional held = heldFor(ctx.target, cv); + const S32 n = static_cast(at.size()); + // Kept only where the global is the smaller, not where it is + // only as large. + if (n < 2 || !held || held->first + (n - 1) * held->each <= held->global) + { + continue; + } + const std::string name = fresh(cv->getIType(), taken); + LSLType* type = TYPE(cv->getIType()); + const char* id = ctx.allocator->copyStr(name.c_str()); + auto* ident = ctx.allocator->newTracked(type, id); + ident->setLoc(at.front()->getLoc()); + auto* global = ctx.allocator->newTracked(ident, static_cast(constant(cv->copy(ctx.allocator), at.front()))); + global->setLoc(at.front()->getLoc()); + auto* sym = ctx.allocator->newTracked(id, type, SYM_VARIABLE, SYM_GLOBAL, at.front()->getLoc(), nullptr, global); + ident->setSymbol(sym); + script->getSymbolTable()->define(sym); + for (LSLExpression* place : at) + { + auto* read = ctx.allocator->newTracked(type, id); + read->setSymbol(sym); + read->setLoc(place->getLoc()); + auto* lvalue = ctx.allocator->newTracked(read, static_cast(nullptr)); + lvalue->setType(type); + lvalue->setLoc(place->getLoc()); + LSLASTNode::replaceNode(place, lvalue); + } + report.note(at.front()->getLoc(), "OptimizerKeptConstant", "kept [1] in the global [2], which its [3] places read", + { render(global->getChild(1)), name, std::to_string(n) }); + made.push_back(global); + ++changes; + } + if (!made.empty()) + { + // Ahead of the rest, in the order they were made. + std::vector globals = made; + for (LSLASTNode* g = script->getGlobals()->getChild(0); g; g = g->getNext()) + { + globals.push_back(g); + } + setStatements(script->getGlobals(), globals, *ctx.context); + } + return changes; + } + + private: + // A short name of the script's own, by the type's first letter. + std::string fresh(LSLIType type, boost::unordered_flat_set>& taken) const + { + const char* base = type == LST_INTEGER ? "n" : type == LST_FLOATINGPOINT ? "f" : type == LST_STRING ? "s" : type == LST_KEY ? "k" : type == LST_VECTOR ? "v" : "r"; + const auto usable = [&](const std::string& name) { + return !taken.contains(name) && ALScriptLexicon::lslWord(name) == ALScriptLexicon::LSL_NAME && + !(ctx.context->builtins && ctx.context->builtins->lookup(name.c_str(), SYM_ANY)); + }; + std::string name = base; + for (int k = 2; !usable(name); ++k) + { + name = base + std::to_string(k); + } + taken.insert(name); + return name; + } + }; +} + + int poolConstants(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + ConstantGlobals pooled(ctx, report, options); + return pooled.run(script); + } +} diff --git a/indra/alscript/lsl/optimizer/allslconstantglobals.h b/indra/alscript/lsl/optimizer/allslconstantglobals.h new file mode 100644 index 00000000000..2b7c21ad611 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslconstantglobals.h @@ -0,0 +1,37 @@ +/** + * @file allslconstantglobals.h + * @brief The LSL optimizer's constants written at many places kept once in a global. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // A constant the script writes at enough places kept once in a global + // of its own, each place reading it, where the target holds it smaller + // so (ALLSLCosts::Held, which the folder writes globals out by): LSO's + // long strings, keys and vectors, Mono's and LSO's rotations. Once + // nothing more will fold, which would write them back. How many it kept. + int poolConstants(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/allslcosts.cpp b/indra/alscript/lsl/optimizer/allslcosts.cpp similarity index 58% rename from indra/alscript/allslcosts.cpp rename to indra/alscript/lsl/optimizer/allslcosts.cpp index e658b78941b..579b6dbdc47 100644 --- a/indra/alscript/allslcosts.cpp +++ b/indra/alscript/lsl/optimizer/allslcosts.cpp @@ -44,6 +44,16 @@ namespace c.negateComplementForIncrement = true; c.complementNegateForDecrement = true; c.preForPost = true; + // A float's, a key's, a vector's and a rotation's default loads + // smaller than what writes it. + c.incrementForAssign = false; + c.dropIntegerDefault = false; + c.dropFloatDefault = true; + c.dropKeyDefault = true; + c.dropVectorDefault = true; + c.dropRotationDefault = true; + c.dropIntegerGlobalDefault = false; + c.castForNegative = false; c.emptyForNullKey = true; c.comparisonForNot = true; c.xorForNotEqual = true; @@ -53,8 +63,26 @@ namespace c.castForDump = true; c.castForDetail = true; c.castForWholeFloat = false; + c.bitTestsMerged = true; + // An && costs what an & does; two ifs one jump more than one; and + // a do's check one jump less than a for's. + c.bitAndForAnd = false; + c.plusForMinus = false; + c.productForShift = false; + c.complementForMinusOneLess = true; + c.sumForDouble = false; + c.productForQuotient = false; + c.remainderForOddTest = false; + c.differenceForNotEqual = false; + c.quotientForShift = false; + c.andForNestedIf = true; + c.bitAndForNestedTruths = true; + c.arithmeticForSelect = true; + c.doForKnownFirst = true; + c.jumpForForever = true; c.local = 11; + c.localFrame = 0; c.jump = 5; c.function = 16; c.functionChar = 0; @@ -82,7 +110,8 @@ namespace ALLSLCosts mono() { ALLSLCosts c; - // A small integer loads in a byte or two, and a float in nine. + // An integer the script writes loads in five bytes, as LL's compiler + // writes every one and the grid keeps it, and a float in nine. c.integerForFloat = true; c.lengthAsNotEqual = false; c.listAsSum = true; @@ -91,10 +120,22 @@ namespace c.listHelperMost = 26; c.complementForNotMinusOne = true; - c.notComplementForMinusOne = false; - c.negateComplementForIncrement = false; + c.notComplementForMinusOne = true; + c.negateComplementForIncrement = true; c.complementNegateForDecrement = true; c.preForPost = true; + // Mono loads an integer the script writes by ldc.i4, five bytes: + // ++x adds the one it makes up itself in one; an integer's default + // is ldc.i4.0; and the cast of a negative number is one constant, + // where the number negated is two instructions. + c.incrementForAssign = true; + c.dropIntegerDefault = true; + c.dropFloatDefault = false; + c.dropKeyDefault = false; + c.dropVectorDefault = false; + c.dropRotationDefault = false; + c.dropIntegerGlobalDefault = true; + c.castForNegative = true; c.emptyForNullKey = true; c.comparisonForNot = true; c.xorForNotEqual = true; @@ -106,27 +147,47 @@ namespace // script may not have. c.castForDetail = false; c.castForWholeFloat = true; + c.bitTestsMerged = true; + // An && is a branch, where & is one instruction; a subtraction, a + // shift and a division by a float each a call into the helpers, + // where an add, a product and a product are one; a constant five + // bytes, a local's read two; and a do's check one jump less. + c.bitAndForAnd = true; + c.plusForMinus = true; + c.productForShift = true; + c.complementForMinusOneLess = true; + c.sumForDouble = true; + c.productForQuotient = true; + c.remainderForOddTest = false; + c.differenceForNotEqual = false; + c.quotientForShift = false; + c.andForNestedIf = false; + c.bitAndForNestedTruths = true; + c.arithmeticForSelect = true; + c.doForKnownFirst = true; + c.jumpForForever = true; c.local = 6; + c.localFrame = 31; c.jump = 5; c.function = 35; c.functionChar = 1; c.parameter = 14; c.call = 6; c.argument = 2; - c.reference = 19; + c.reference = 17; c.referenceChar = 1; // A global is a field, set in the constructor; a string is held // once, however many places load it. - c.integer = { 45, -1, -1 }; - c.floating = { 49, 3, 3 }; - c.wholeFloating = { 42, -4, -4 }; - c.vector = { 72, 26, 26 }; - c.wholeVector = { 51, 5, 5 }; - c.rotation = { 115, 35, 35 }; - c.wholeRotation = { 87, 7, 7 }; - c.string = { 47, -1, -1 }; + c.integer = { 43, -1, -1 }; + c.floating = { 47, 3, 3 }; + c.wholeFloating = { 44, 0, 0 }; + c.vector = { 70, 26, 26 }; + c.wholeVector = { 61, 17, 17 }; + c.rotation = { 111, 35, 35 }; + c.wholeRotation = { 99, 23, 23 }; + c.string = { 45, -1, -1 }; c.stringChar = { 2, 0, 0 }; c.stringHeld = 2; c.stringHeldChar = 2; @@ -149,6 +210,15 @@ namespace c.negateComplementForIncrement = false; c.complementNegateForDecrement = false; c.preForPost = false; + // A key's default loads smaller than the empty string it is. + c.incrementForAssign = false; + c.dropIntegerDefault = false; + c.dropFloatDefault = false; + c.dropKeyDefault = true; + c.dropVectorDefault = false; + c.dropRotationDefault = false; + c.dropIntegerGlobalDefault = false; + c.castForNegative = false; c.emptyForNullKey = true; c.comparisonForNot = true; c.xorForNotEqual = false; @@ -160,9 +230,27 @@ namespace c.castForDump = false; c.castForDetail = false; c.castForWholeFloat = false; + c.bitTestsMerged = true; + // Its bitwise operators are calls into bit32, its arithmetic one + // instruction each. + c.bitAndForAnd = false; + c.plusForMinus = false; + c.productForShift = true; + c.complementForMinusOneLess = false; + c.sumForDouble = true; + c.productForQuotient = false; + c.remainderForOddTest = true; + c.differenceForNotEqual = true; + c.quotientForShift = true; + c.andForNestedIf = false; + c.bitAndForNestedTruths = false; + c.arithmeticForSelect = false; + c.doForKnownFirst = false; + c.jumpForForever = false; c.local = 4; - c.jump = 93; + c.localFrame = 0; + c.jump = 92; c.function = 37; c.functionChar = 1; c.parameter = 0; diff --git a/indra/alscript/allslcosts.h b/indra/alscript/lsl/optimizer/allslcosts.h similarity index 67% rename from indra/alscript/allslcosts.h rename to indra/alscript/lsl/optimizer/allslcosts.h index f509abd9b17..4df0de78c68 100644 --- a/indra/alscript/allslcosts.h +++ b/indra/alscript/lsl/optimizer/allslcosts.h @@ -76,6 +76,24 @@ struct ALLSLCosts bool complementNegateForDecrement = false; // `x++` as `++x`, and `x--` as `--x`, where the value goes unused. bool preForPost = false; + // `x = x + 1`, `x = 1 + x` and `x += 1` as `++x`, and `x = x - 1` and + // `x -= 1` as `--x`, of an integer variable: a float's are larger. + bool incrementForAssign = false; + // A local's initializer that is its type's default left out -- + // `integer x = 0;` as `integer x;` -- each type on its own, since each + // target loads each type's default its own way: a string's and a list's + // are never smaller. And an integer global's. + bool dropIntegerDefault = false; + bool dropFloatDefault = false; + bool dropKeyDefault = false; + bool dropVectorDefault = false; + bool dropRotationDefault = false; + bool dropIntegerGlobalDefault = false; + // A negative number in an expression as the cast of one -- `-5` as + // `((integer)-5)`, `-5.5` as `((float)-5.5)` -- which is one constant, + // where `-5` is 5 negated. Never in a global's initializer, which takes + // no cast and reads `-5` as one constant already. + bool castForNegative = false; // A key a library function is given that is NULL_KEY, or no key at // all, as "": the function takes either as it takes the other. bool emptyForNullKey = false; @@ -100,12 +118,64 @@ struct ALLSLCosts // A float whose value is whole, where LSL converts nothing for it -- a // list's element, what is cast -- as `((float)2)` rather than `2.0`. bool castForWholeFloat = false; + // Bit tests of one value as one: `(x & 4) && (x & 8)` as + // `!~(x | -13)`, `!(x & 4) && !(x & 8)` as `!(x & 12)`, `(x & 4) | (x & 8)` + // as `x & 12`, and an || of them as that where only truth counts. + bool bitTestsMerged = false; + // `a && b` as `a & b` where each side is only ever 1 or 0: a + // comparison, a !, or an && or || of its own. LSL runs both sides of + // either, the right first. + bool bitAndForAnd = false; + // `a - b` as `a + -b`, and `a - 5` as `a + -5`, of integers and floats: + // the same value, the operands run in the same order -- the right + // first -- and on Mono an add in place of a call to subtract. + bool plusForMinus = false; + // `x << 3` as `x * 8`, a shift by 0 to 30: the two wrap alike. + bool productForShift = false; + // `-1 - x` as `~x`, which two's complement makes the same. + bool complementForMinusOneLess = false; + // `x * 2` as `x + x`, where x is a local's or a parameter's read. + bool sumForDouble = false; + // `f / 4` as `f * 0.25`, a float divided by a power of two, whose + // reciprocal is exact: the same value to the last bit. + bool productForQuotient = false; + // Where only truth counts, `x & 1` as `x % 2`, and `a != b` as `a - b`; + // and `a == b` as `!(a - b)`: Luau's bitwise operators are calls, its + // arithmetic one instruction. + bool remainderForOddTest = false; + bool differenceForNotEqual = false; + // Of a value never below nought, `x >> 2` as `x / 4`, and `x & 7` as + // `x % 8`. + bool quotientForShift = false; + // `if (a) if (b) S`, neither with an else, as `if (a && b) S` -- where b + // is safe to run whatever a is -- and as `if (a & b) S` where both are + // only ever 1 or 0. + bool andForNestedIf = false; + bool bitAndForNestedTruths = false; + // `if (c) x = 5; else x = 7;` as `x = !!c * -2 + 7`, of a c that is not + // only ever 1 or 0: integer constants either way. Of one that is, the + // arithmetic is smaller on every target, and made wherever it can be. + bool arithmeticForSelect = false; + // A for or a while whose first check is known to pass run as a do: + // `for (i = 0; i < 10; ++i) S` as `i = 0; do S while (++i < 10);`. + bool doForKnownFirst = false; + // A loop that never ends, `do S while (TRUE);`, as `@l; S jump l;`: a + // jump back in place of the check's constant and its jump. + bool jumpForForever = false; // ---- functions, for whether putting one in place saves // A local declared and set, then read, against the value used where it // is read. S32 local = 0; + // What a local costs past its code where the target keeps each in a + // frame it saves and restores: the grid microthreads Mono, each function + // a frame class with a field for every local, filled by its constructor, + // read back by a restore and pushed by a save -- some 31 bytes a local, + // from the grid's IL (the SL wiki's sample), where the weigher sizes + // Tailslide's IL, which has none of it. An estimate, so not measured + // here; what a new local must save past what the weigher says. + S32 localFrame = 0; // A jump and the label it goes to. S32 jump = 0; // A function taking and returning nothing, with an empty body, and what diff --git a/indra/alscript/lsl/optimizer/allsldeadcode.cpp b/indra/alscript/lsl/optimizer/allsldeadcode.cpp new file mode 100644 index 00000000000..1d4f6030c20 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsldeadcode.cpp @@ -0,0 +1,1292 @@ +/** + * @file allsldeadcode.cpp + * @brief The LSL optimizer's dead code removal. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allsldeadcode.h" + +#include "allsleffects.h" + +#include +#include + +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // What can never run, and what is never used. + class DeadCode : public ASTVisitor, public Pass + { + public: + using Pass::Pass; + + int run(LSLScript* script) + { + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_FUNCTION && global->getSymbol()) + { + mFunctions[global->getSymbol()] = static_cast(global); + } + } + script->visit(this); + globals(script); + states(script); + unreached(script); + writeOnly(script); + emptyHandlers(script); + return changes; + } + + bool visit(LSLCompoundStatement* block) override + { + visitChildren(block); + bool dead = false; + std::vector going; + // Removed for a reason, said; or silently, with nothing. + const auto go = [&](LSLASTNode* stmt, const char* key, const char* why) { + if (key) + { + // What went, as far as its first line and a few words + // of it: a note is read beside the line, not in place + // of the code. + std::string said = render(stmt); + const size_t cut = std::min(said.find('\n'), size_t(60)); + if (cut < said.size()) + { + said = said.substr(0, cut) + "\xE2\x80\xA6"; + } + report.note(stmt->getLoc(), key, std::string("removed [1]") + why, { said }); + } + going.push_back(stmt); + }; + for (LSLASTNode* stmt : *block) + { + if (stmt->getNodeSubType() == NODE_LABEL) + { + dead = false; + if (unusedLabel(stmt)) + { + go(stmt, nullptr, ""); + } + continue; + } + if (dead) + { + // A label in it that a jump goes to makes it reachable, + // and what follows it: a jump in SL goes to the last + // label of its name in the function, whatever block + // that is in. + if (!holdsLiveLabel(stmt)) + { + go(stmt, "OptimizerRemovedUnreachable", ", which can never run"); + continue; + } + dead = false; + } + switch (stmt->getNodeSubType()) + { + case NODE_JUMP_STATEMENT: + if (jumpsToNext(stmt)) + { + go(stmt, "OptimizerRemovedJump", ", which goes where running on goes"); + break; + } + dead = true; + break; + case NODE_RETURN_STATEMENT: + case NODE_STATE_STATEMENT: + dead = true; + break; + case NODE_IF_STATEMENT: + case NODE_WHILE_STATEMENT: + case NODE_DO_STATEMENT: + case NODE_FOR_STATEMENT: + // An if none of whose ways goes on, a loop that + // never ends -- but for a jump out, whose label + // keeps what follows it (above). + dead = stops(stmt); + break; + case NODE_NOP_STATEMENT: + go(stmt, nullptr, ""); + break; + case NODE_COMPOUND_STATEMENT: + if (!stmt->hasChildren()) + { + go(stmt, nullptr, ""); + } + break; + case NODE_EXPRESSION_STATEMENT: + { + LSLExpression* expr = static_cast(stmt)->getExpr(); + if (changesNothing(expr) || callsNothing(expr)) + { + go(stmt, "OptimizerRemovedNoEffect", ", which does nothing"); + } + dead = stops(stmt); + break; + } + case NODE_DECLARATION: + if (unusedLocal(static_cast(stmt))) + { + go(stmt, nullptr, ""); + } + break; + default: + break; + } + } + for (LSLASTNode* stmt : going) + { + block->removeChild(stmt); + ++changes; + } + flattenBlocks(block); + sinkDeclarations(block); + substituteLocals(block); + deadStores(block); + return false; + } + + // How many identifiers of each name a function or event has, + // counted once a run: what the pass takes away only makes a count + // more than there are, never fewer. + const boost::unordered_flat_map& namesIn(LSLASTNode* node) + { + LSLASTNode* callable = node; + while (callable && callable->getNodeType() != NODE_GLOBAL_FUNCTION && callable->getNodeType() != NODE_EVENT_HANDLER) + { + callable = callable->getParent(); + } + auto& counted = mNames[callable]; + if (counted.empty() && callable) + { + std::vector stack{ callable }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) + { + ++counted[static_cast(n)->getName()]; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + } + return counted; + } + + // A block standing as a statement of a block, its statements put + // in its place: where it declares nothing and holds no label, or + // only names no identifier outside it has -- LSL has no shadowing, + // and a name it declared would otherwise be one a later declaration + // shadows, or a global a later read meant; and two labels of a name + // may not stand in one block. + void flattenBlocks(LSLCompoundStatement* block) + { + const auto flat = [&](LSLASTNode* inner) { + if (inner->getNodeSubType() != NODE_COMPOUND_STATEMENT) + { + return false; + } + boost::unordered_flat_map own; + std::vector declared; + std::vector stack{ inner }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) + { + ++own[static_cast(n)->getName()]; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + // What it declares, and the labels it holds: two labels of a + // name may not stand in one block. + for (LSLASTNode* stmt = inner->getChild(0); stmt; stmt = stmt->getNext()) + { + if (stmt->getNodeSubType() == NODE_DECLARATION) + { + declared.push_back(static_cast(stmt)->getIdentifier()->getName()); + } + else if (stmt->getNodeSubType() == NODE_LABEL) + { + declared.push_back(static_cast(stmt)->getIdentifier()->getName()); + } + } + const auto& all = namesIn(block); + for (const char* name : declared) + { + const auto total = all.find(std::string_view(name)); + if (!name || total == all.end() || total->second != own[std::string_view(name)]) + { + return false; + } + } + return true; + }; + std::vector statements; + bool any = false; + for (LSLASTNode* stmt : *block) + { + statements.push_back(stmt); + any = any || flat(stmt); + } + if (!any) + { + return; + } + std::vector flatten(statements.size()); + for (size_t i = 0; i < statements.size(); ++i) + { + flatten[i] = flat(statements[i]); + } + // The statements are the ones they were, every reference in + // them still in the script: counted neither out nor in again. + const Uncounted uncounted(*ctx.context); + for (LSLASTNode* stmt : statements) + { + block->removeChild(stmt); + } + for (size_t i = 0; i < statements.size(); ++i) + { + if (!flatten[i]) + { + block->pushChild(statements[i]); + continue; + } + std::vector inner; + for (LSLASTNode* stmt = statements[i]->getChild(0); stmt; stmt = stmt->getNext()) + { + inner.push_back(stmt); + } + for (LSLASTNode* stmt : inner) + { + statements[i]->removeChild(stmt); + block->pushChild(stmt); + } + ++changes; + } + } + + // `T x;` and, later in the block, `x = e;` with nothing between that + // names x, jumps or is jumped to: the declaration where the + // assignment was, `T x = e;` -- which the local's value may then + // go further from. + void sinkDeclarations(LSLCompoundStatement* block) + { + std::vector statements; + for (LSLASTNode* stmt : *block) + { + statements.push_back(stmt); + } + const auto mentions = [](LSLASTNode* root, LSLSymbol* sym) { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym) + { + return true; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + return false; + }; + const auto jumpy = [](LSLASTNode* root) { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_STATEMENT && (n->getNodeSubType() == NODE_LABEL || n->getNodeSubType() == NODE_JUMP_STATEMENT)) + { + return true; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + return false; + }; + for (size_t i = 0; i < statements.size(); ++i) + { + if (statements[i]->getNodeSubType() != NODE_DECLARATION) + { + continue; + } + auto* decl = static_cast(statements[i]); + LSLSymbol* sym = decl->getSymbol(); + LSLASTNode* init = decl->getChild(1); + if (!sym || sym->getSubType() != SYM_LOCAL || (init && init->getNodeType() != NODE_NULL)) + { + continue; + } + for (size_t j = i + 1; j < statements.size(); ++j) + { + LSLASTNode* stmt = statements[j]; + if (!mentions(stmt, sym)) + { + if (jumpy(stmt)) + { + break; + } + continue; + } + auto* expr = stmt->getNodeSubType() == NODE_EXPRESSION_STATEMENT ? static_cast(stmt)->getExpr() : nullptr; + if (!expr || expr->getNodeSubType() != NODE_BINARY_EXPRESSION || expr->getOperation() != '=' || + expr->getChild(0)->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(expr->getChild(0))->getMember() || + static_cast(expr->getChild(0))->getSymbol() != sym || mentions(expr->getChild(1), sym)) + { + break; + } + decl->setChild(1, static_cast(expr->takeChild(1))); + block->removeChild(decl); + LSLASTNode::replaceNode(stmt, decl); + statements[j] = decl; + ++changes; + break; + } + } + } + + // Whether a jump goes where running on would go anyway: its label + // is the next statement to run -- after the jump, or after the + // blocks and the if branches it is the end of -- and the only + // label of its name in the function, a jump going to the last of + // a name. + static bool jumpsToNext(LSLASTNode* jump) + { + LSLIdentifier* id = static_cast(jump)->getIdentifier(); + const char* target = id ? id->getName() : nullptr; + if (!target) + { + return false; + } + const auto empty = [](LSLASTNode* n) { + return n->getNodeSubType() == NODE_NOP_STATEMENT || (n->getNodeSubType() == NODE_COMPOUND_STATEMENT && !n->hasChildren()); + }; + LSLASTNode* at = jump; + while (true) + { + LSLASTNode* parent = at->getParent(); + if (!parent || parent->getNodeType() != NODE_STATEMENT) + { + return false; + } + if (parent->getNodeSubType() == NODE_IF_STATEMENT && at != static_cast(parent)->getCheckExpr()) + { + at = parent; + continue; + } + if (parent->getNodeSubType() != NODE_COMPOUND_STATEMENT) + { + return false; + } + LSLASTNode* next = at->getNext(); + while (next && empty(next)) + { + next = next->getNext(); + } + if (!next) + { + at = parent; + continue; + } + if (next->getNodeSubType() != NODE_LABEL || strcmp(static_cast(next)->getIdentifier()->getName(), target)) + { + return false; + } + // The only label of the name in the function or event. + return labelsNamed(next, target) == 1; + } + } + + // A value set and set again, in later statements of the same block, + // before anything reads it: `x = 3; ... x = 4;`, `integer x = 3; + // ... for (x = 0; ...)`. The first goes -- what it ran still run -- + // where nothing between names x, jumps or is jumped to. Of a local + // or a parameter, which nothing outside the code here reads. + void deadStores(LSLCompoundStatement* block) + { + std::vector statements; + for (LSLASTNode* stmt : *block) + { + statements.push_back(stmt); + } + const auto names = [](LSLASTNode* root, LSLSymbol* sym) { + bool found = false; + each(root, [&](LSLASTNode* n) { found = found || (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym); }); + return found; + }; + const auto jumpy = [](LSLASTNode* root) { + bool found = false; + each(root, [&](LSLASTNode* n) { + found = found || (n->getNodeType() == NODE_STATEMENT && (n->getNodeSubType() == NODE_LABEL || n->getNodeSubType() == NODE_JUMP_STATEMENT)); + }); + return found; + }; + // `x = e`, whole, e naming nothing of x: what it sets. + const auto setsWhole = [&](LSLASTNode* expr) -> LSLSymbol* { + if (!expr || expr->getNodeType() != NODE_EXPRESSION || expr->getNodeSubType() != NODE_BINARY_EXPRESSION || + static_cast(expr)->getOperation() != OP_ASSIGN) + { + return nullptr; + } + LSLASTNode* target = expr->getChild(0); + LSLSymbol* sym = target->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(target)->getMember() + ? target->getSymbol() + : nullptr; + return sym && !names(expr->getChild(1), sym) ? sym : nullptr; + }; + // Whether a statement sets x again before it reads it. + const auto overwrites = [&](LSLASTNode* stmt, LSLSymbol* sym) { + if (stmt->getNodeSubType() == NODE_EXPRESSION_STATEMENT) + { + return setsWhole(stmt->getChild(0)) == sym; + } + if (stmt->getNodeSubType() == NODE_FOR_STATEMENT) + { + // Its first part, the first that names x setting it. + for (LSLASTNode* init = stmt->getChild(0)->getChild(0); init; init = init->getNext()) + { + if (names(init, sym)) + { + return setsWhole(init) == sym; + } + } + } + return false; + }; + bool changed = false; + for (size_t i = 0; i < statements.size(); ++i) + { + LSLASTNode* stmt = statements[i]; + LSLASTNode* value = nullptr; + LSLSymbol* sym = nullptr; + if (stmt->getNodeSubType() == NODE_DECLARATION) + { + value = stmt->getChild(1); + sym = value && value->getNodeType() == NODE_EXPRESSION ? stmt->getSymbol() : nullptr; + } + else if (stmt->getNodeSubType() == NODE_EXPRESSION_STATEMENT) + { + sym = setsWhole(stmt->getChild(0)); + value = sym ? stmt->getChild(0)->getChild(1) : nullptr; + } + if (!sym || (sym->getSubType() != SYM_LOCAL && !parameter(sym)) || sym->getIType() == LST_LIST) + { + continue; + } + bool dead = false; + for (size_t j = i + 1; j < statements.size(); ++j) + { + if (overwrites(statements[j], sym)) + { + dead = true; + break; + } + if (names(statements[j], sym) || jumpy(statements[j])) + { + break; + } + } + if (!dead) + { + continue; + } + report.note(stmt->getLoc(), "OptimizerDeadStore", "removed the value [1] is set to here, which it is set again before anything reads", + { sym->getName() }); + const Uncounted uncounted(*ctx.context); + if (stmt->getNodeSubType() == NODE_DECLARATION) + { + auto* was = static_cast(stmt->takeChild(1)); + if (!changesNothing(was)) + { + auto* run = ctx.allocator->newTracked(was); + run->setLoc(stmt->getLoc()); + statements.insert(statements.begin() + static_cast(i) + 1, run); + } + } + else if (changesNothing(value)) + { + statements.erase(statements.begin() + static_cast(i)); + --i; + } + else + { + LSLASTNode* expr = stmt->getChild(0); + stmt->setChild(0, expr->takeChild(1)); + } + changed = true; + ++changes; + } + if (changed) + { + setStatements(block, statements, *ctx.context); + } + } + + // A local set once from what changes nothing and is read the same + // at any time, then read once in the expression of a later + // statement of the same block: its value put where it is read, and + // the local gone -- where no loop reads it again and no label + // could bring the run in between, and nothing that runs between + // writes what the value reads. A parameter a function was put in + // place with, or the variable its value was set in, most often. + void substituteLocals(LSLCompoundStatement* block) + { + // The block's locals that may go -- declared here from what + // changes nothing, never written, read once -- and, in one walk + // of what follows, where each is read. + std::vector statements; + boost::unordered_flat_map> reads; + for (LSLASTNode* stmt : *block) + { + if (stmt->getNodeSubType() == NODE_DECLARATION) + { + auto* decl = static_cast(stmt); + LSLSymbol* sym = decl->getSymbol(); + if (sym && sym->getSubType() == SYM_LOCAL && sym->getAssignments() == 0 && sym->getReferences() == 2 && decl->getInitializer() && + decl->getInitializer()->getNodeType() == NODE_EXPRESSION) + { + reads.emplace(sym, std::make_pair(size_t(-1), nullptr)); + } + } + statements.push_back(stmt); + } + if (reads.empty()) + { + return; + } + std::vector stack; + for (size_t k = 0; k < statements.size(); ++k) + { + stack.assign(1, statements[k]); + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + const auto found = reads.find(static_cast(n)->getSymbol()); + if (found != reads.end() && !found->second.second) + { + found->second = { k, static_cast(n) }; + } + } + // A statement's own expressions only: a read in one it + // holds is not one a value may be put in, and that + // statement's own block walks it. + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + if (child->getNodeType() != NODE_STATEMENT) + { + stack.push_back(child); + } + } + } + } + const auto holdsLabel = [](LSLASTNode* root) { + bool found = false; + const std::function look = [&](LSLASTNode* n) { + found = found || (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL); + for (LSLASTNode* child = n->getChild(0); child && !found; child = child->getNext()) + { + look(child); + } + }; + look(root); + return found; + }; + std::vector gone; + for (size_t i = 0; i < statements.size(); ++i) + { + if (statements[i]->getNodeSubType() != NODE_DECLARATION) + { + continue; + } + auto* decl = static_cast(statements[i]); + LSLSymbol* sym = decl->getSymbol(); + LSLExpression* value = decl->getInitializer(); + const auto read = sym ? reads.find(sym) : reads.end(); + if (read == reads.end() || !value || !sideEffectFree(value) || value->getIType() != sym->getIType()) + { + continue; + } + // The read, and the statement it is in: one after this. + const size_t at = read->second.first; + LSLLValueExpression* use = read->second.second; + if (!use || at <= i || use->getMember()) + { + continue; + } + // In the statement's own expression: no statement, and so + // no loop or branch, between it and the read. + bool direct = true; + for (LSLASTNode* up = use->getParent(); up && up != statements[at]; up = up->getParent()) + { + direct = direct && up->getNodeType() != NODE_STATEMENT; + } + const LSLNodeSubType shape = statements[at]->getNodeSubType(); + if (!direct || shape == NODE_WHILE_STATEMENT || shape == NODE_DO_STATEMENT || shape == NODE_FOR_STATEMENT) + { + continue; + } + bool labelled = false; + ALLSLEffects::Writes between; + for (size_t k = i + 1; k < at; ++k) + { + labelled = labelled || holdsLabel(statements[k]); + between.add(ctx.effects->of(statements[k])); + } + for (LSLASTNode* earlier : ALLSLEffects::before(statements[at], use)) + { + between.add(ctx.effects->of(earlier)); + } + bool written = false; + const std::function reads = [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + written = written || between.writes(static_cast(n)->getSymbol()); + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + reads(child); + } + }; + reads(value); + if (labelled || written) + { + continue; + } + report.note(decl->getLoc(), "OptimizerSubstitutedLocal", "put the value of [1] where it is read, and removed it", { sym->getName() }); + putInPlace(use, static_cast(decl->takeChild(1)), ctx.allocator); + gone.push_back(decl); + ++changes; + } + for (LSLDeclaration* decl : gone) + { + block->removeChild(decl); + } + } + + bool visit(LSLIfStatement* stmt) override + { + visitChildren(stmt); + LSLConstant* cv = stmt->getCheckExpr() ? stmt->getCheckExpr()->getConstantValue() : nullptr; + if (!cv || cv->getNodeSubType() != NODE_INTEGER_CONSTANT) + { + return false; + } + const bool taken = static_cast(cv)->getValue() != 0; + LSLStatement* branch = taken ? stmt->getTrueBranch() : stmt->getFalseBranch(); + // A branch never taken is reached all the same by a jump to a + // label in it. + if (LSLStatement* other = taken ? stmt->getFalseBranch() : stmt->getTrueBranch(); other && holdsLiveLabel(other)) + { + return false; + } + if (branch && lastIfOfStateChange(stmt, branch)) + { + return false; + } + report.note(stmt->getLoc(), taken ? "OptimizerIfAlwaysTrue" : "OptimizerIfAlwaysFalse", + taken ? "the condition of this if is always true; kept only what runs" : "the condition of this if is always false; kept only what runs"); + replaceStatement(stmt, branch ? static_cast(stmt->takeChild(taken ? 1 : 2)) : nullptr); + return false; + } + + bool visit(LSLWhileStatement* stmt) override + { + visitChildren(stmt); + if (isInteger(stmt->getCheckExpr(), 0) && !holdsLiveLabel(stmt)) + { + report.note(stmt->getLoc(), "OptimizerRemovedWhile", "removed a while loop whose condition is always false"); + replaceStatement(stmt, nullptr); + } + return false; + } + + bool visit(LSLDoStatement* stmt) override + { + visitChildren(stmt); + if (isInteger(stmt->getCheckExpr(), 0)) + { + report.note(stmt->getLoc(), "OptimizerDoRunsOnce", "a do loop whose condition is always false runs once; kept its body"); + replaceStatement(stmt, static_cast(stmt->takeChild(0))); + } + return false; + } + + bool visit(LSLForStatement* stmt) override + { + visitChildren(stmt); + if (isInteger(stmt->getCheckExpr(), 0) && !holdsLiveLabel(stmt)) + { + // The initialisers still run, once. + auto* block = ctx.allocator->newTracked(nullptr); + block->setLoc(stmt->getLoc()); + LSLASTNode* init = stmt->getInitExprs(); + while (init && init->hasChildren()) + { + auto* expr = static_cast(init->takeChild(0)); + init->removeChild(init->getChild(0)); + auto* es = ctx.allocator->newTracked(expr); + es->setLoc(expr->getLoc()); + block->pushChild(es); + } + report.note(stmt->getLoc(), "OptimizerRemovedFor", "removed a for loop whose condition is always false; its initialisers stay"); + replaceStatement(stmt, block->hasChildren() ? block : nullptr); + } + return false; + } + + private: + // A statement becomes another, or nothing: nothing is an empty + // statement where a branch or a body needs one. + void replaceStatement(LSLStatement* old, LSLStatement* now) + { + LSLASTNode* parent = old->getParent(); + if (!now) + { + if (parent && parent->getNodeSubType() == NODE_COMPOUND_STATEMENT) + { + parent->removeChild(old); + ++changes; + return; + } + now = ctx.allocator->newTracked(); + now->setLoc(old->getLoc()); + } + LSLASTNode::replaceNode(old, now); + ++changes; + } + + // A call to a function of the script's own whose body is empty, + // with arguments that do nothing on their own. + bool callsNothing(LSLExpression* expr) const + { + if (!expr || expr->getNodeSubType() != NODE_FUNCTION_EXPRESSION) + { + return false; + } + LSLSymbol* sym = expr->getSymbol(); + auto function = sym ? mFunctions.find(sym) : mFunctions.end(); + if (function == mFunctions.end()) + { + return false; + } + LSLASTNode* body = function->second->getStatements(); + if (!body || body->getNodeSubType() != NODE_COMPOUND_STATEMENT || body->hasChildren()) + { + return false; + } + for (LSLASTNode* arg : *static_cast(expr)->getArguments()) + { + if (!changesNothing(arg)) + { + return false; + } + } + return true; + } + + // Whether a label a jump from outside a statement goes to is in + // it, however far down: the one way in to a statement nothing + // before it reaches. A jump from inside to a label inside -- a + // loop made of labels -- is no way in. + static bool holdsLiveLabel(LSLASTNode* statement) + { + // Each label's references are its own name and every jump to + // it; the jumps inside the statement are taken off. + boost::unordered_flat_map inside; + std::vector labels; + const std::function gather = [&](LSLASTNode* node) { + if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_LABEL) + { + if (LSLSymbol* sym = node->getSymbol()) + { + labels.push_back(sym); + } + } + else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_JUMP_STATEMENT) + { + if (LSLSymbol* sym = static_cast(node)->getIdentifier()->getSymbol()) + { + ++inside[sym]; + } + } + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + gather(child); + } + }; + gather(statement); + for (LSLSymbol* label : labels) + { + if (label->getReferences() - 1 > inside[label]) + { + return true; + } + } + return false; + } + + // Whether taking `stmt` away for its branch would leave a state + // change in a function with no `if` around it: a function may + // change state only under an `if`, which the compiler checks + // (Tailslide's E_CHANGE_STATE_IN_FUNCTION), whatever the `if` + // tests. An event may change state anywhere. + static bool lastIfOfStateChange(LSLIfStatement* stmt, LSLStatement* branch) + { + for (LSLASTNode* up = stmt->getParent(); up; up = up->getParent()) + { + if (up->getNodeType() == NODE_STATE || (up->getNodeType() == NODE_STATEMENT && up->getNodeSubType() == NODE_IF_STATEMENT)) + { + return false; + } + if (up->getNodeType() == NODE_GLOBAL_FUNCTION) + { + break; + } + } + bool orphaned = false; + const std::function look = [&](LSLASTNode* node, bool underIf) { + if (orphaned || !node) + { + return; + } + if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_STATE_STATEMENT && !underIf) + { + orphaned = true; + return; + } + const bool isIf = node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_IF_STATEMENT; + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + look(child, underIf || isIf); + } + }; + look(branch, false); + return orphaned; + } + + bool unusedLabel(LSLASTNode* label) + { + LSLSymbol* sym = label->getSymbol(); + if (!sym || sym->getReferences() > 1) + { + return false; + } + // Another label of its name, and a jump the compiler finds this + // one for, runs to the last: the count of this one's own jumps + // does not say it is unused. + if (labelsNamed(label, static_cast(label)->getIdentifier()->getName()) > 1) + { + return false; + } + report.note(label->getLoc(), "OptimizerRemovedLabel", "removed the label [1], which nothing jumps to", { render(label) }); + return true; + } + + bool unusedLocal(LSLDeclaration* decl) + { + LSLSymbol* sym = decl->getSymbol(); + if (!sym || sym->getReferences() != 1 || sym->getAssignments() != 0) + { + return false; + } + LSLASTNode* init = decl->getInitializer(); + if (init && !changesNothing(init)) + { + return false; + } + forget(decl, sym); + report.note(decl->getLoc(), "OptimizerRemovedLocal", "removed the unused local [1]", { sym->getName() }); + return true; + } + + static void forget(LSLASTNode* node, LSLSymbol* sym) + { + for (LSLASTNode* up = node; up; up = up->getParent()) + { + if (up->getSymbolTable() && up->getSymbolTable()->remove(sym)) + { + return; + } + } + } + + void globals(LSLScript* script) + { + std::vector going; + for (LSLASTNode* global : *script->getGlobals()) + { + LSLSymbol* sym = global->getSymbol(); + if (!sym || sym->getReferences() != 1) + { + continue; + } + if (global->getNodeType() == NODE_GLOBAL_VARIABLE) + { + report.note(global->getLoc(), "OptimizerRemovedGlobal", "removed the unused global [1]", { sym->getName() }); + going.push_back(global); + } + else if (global->getNodeType() == NODE_GLOBAL_FUNCTION) + { + report.note(global->getLoc(), "OptimizerRemovedFunction", "removed the unused function [1]", { sym->getName() }); + going.push_back(global); + } + } + for (LSLASTNode* global : going) + { + script->getSymbolTable()->remove(global->getSymbol()); + script->getGlobals()->removeChild(global); + ++changes; + } + } + + boost::unordered_flat_map mFunctions; + boost::unordered_flat_map> mNames; + + // Every node of a subtree, the root first. + template static void each(LSLASTNode* root, const F& f) + { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* node = stack.back(); + stack.pop_back(); + if (!node) + { + continue; + } + f(node); + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + } + + // What can run: the default state's handlers, each function they + // call and each state they enter, and on through those. A function + // or a state nothing that runs reaches goes, though others that do + // not run reach it -- functions that call only each other. + void unreached(LSLScript* script) + { + boost::unordered_flat_map states; + LSLASTNode* start = nullptr; + for (LSLASTNode* state : *script->getStates()) + { + if (LSLSymbol* sym = state->getSymbol()) + { + states[sym] = state; + start = !strcmp(sym->getName(), "default") ? state : start; + } + } + if (!start) + { + return; + } + boost::unordered_flat_set reached{ start }; + std::vector todo{ start }; + const auto reach = [&](LSLASTNode* node) { + if (node && reached.insert(node).second) + { + todo.push_back(node); + } + }; + while (!todo.empty()) + { + LSLASTNode* from = todo.back(); + todo.pop_back(); + each(from, [&](LSLASTNode* node) { + if (node->getNodeType() == NODE_EXPRESSION && node->getNodeSubType() == NODE_FUNCTION_EXPRESSION) + { + LSLSymbol* sym = static_cast(node)->getSymbol(); + const auto found = sym ? mFunctions.find(sym) : mFunctions.end(); + reach(found == mFunctions.end() ? nullptr : found->second); + } + else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_STATE_STATEMENT) + { + LSLIdentifier* id = static_cast(node)->getIdentifier(); + const auto found = id && id->getSymbol() ? states.find(id->getSymbol()) : states.end(); + reach(found == states.end() ? nullptr : found->second); + } + }); + } + for (LSLASTNode* list : { static_cast(script->getGlobals()), static_cast(script->getStates()) }) + { + std::vector going; + for (LSLASTNode* node = list->getChild(0); node; node = node->getNext()) + { + const bool function = node->getNodeType() == NODE_GLOBAL_FUNCTION; + if ((function || node->getNodeType() == NODE_STATE) && node->getSymbol() && !reached.contains(node)) + { + report.note(node->getLoc(), function ? "OptimizerRemovedUncalled" : "OptimizerRemovedUnentered", + function ? "removed the function [1], which nothing that runs calls" : "removed the state [1], which nothing that runs enters", + { node->getSymbol()->getName() }); + going.push_back(node); + } + } + for (LSLASTNode* node : going) + { + script->getSymbolTable()->remove(node->getSymbol()); + list->removeChild(node); + ++changes; + } + } + } + + // A handler with nothing in it goes where having it changes + // nothing: not a touch's, which makes the object touchable, nor a + // collision's, money's or control's, whose handler changes what the + // object does. A state keeps one handler, which it must have. + void emptyHandlers(LSLScript* script) + { + static constexpr std::string_view QUIET[] = { "state_entry", "state_exit", "on_rez", "attach", "changed", "timer", "listen", "link_message", + "dataserver", "http_response", "sensor", "no_sensor", "at_target", "not_at_target", + "at_rot_target", "not_at_rot_target", "moving_start", "moving_end", "object_rez", + "email", "remote_data", "run_time_permissions", "linkset_data", "transaction_result", + "path_update", "experience_permissions", "experience_permissions_denied" }; + for (LSLASTNode* state = script->getStates()->getChild(0); state; state = state->getNext()) + { + LSLASTNode* handlers = state->getChild(1); + if (!handlers) + { + continue; + } + std::vector going; + size_t count = 0; + for (LSLASTNode* handler = handlers->getChild(0); handler; handler = handler->getNext()) + { + ++count; + auto* event = static_cast(handler); + LSLStatement* body = event->getStatements(); + LSLSymbol* sym = event->getSymbol(); + if (sym && body && body->getNodeSubType() == NODE_COMPOUND_STATEMENT && !body->hasChildren() && + std::find(std::begin(QUIET), std::end(QUIET), std::string_view(sym->getName())) != std::end(QUIET)) + { + going.push_back(handler); + } + } + if (going.size() == count) + { + going.pop_back(); + } + for (LSLASTNode* handler : going) + { + report.note(handler->getLoc(), "OptimizerRemovedEmptyHandler", "removed the empty handler of [1], which having changes nothing", + { handler->getSymbol()->getName() }); + handlers->removeChild(handler); + ++changes; + } + } + } + + // Whether an assignment's or an increment's value goes unread: it is + // a statement of its own, or a for's first part or step. + static bool unread(LSLASTNode* expr) + { + LSLASTNode* up = expr->getParent(); + if (up && up->getNodeType() == NODE_STATEMENT && up->getNodeSubType() == NODE_EXPRESSION_STATEMENT) + { + return true; + } + LSLASTNode* loop = up ? up->getParent() : nullptr; + return up && up->getNodeType() == NODE_AST_NODE_LIST && loop && loop->getNodeType() == NODE_STATEMENT && + loop->getNodeSubType() == NODE_FOR_STATEMENT && + (up == static_cast(loop)->getInitExprs() || up == static_cast(loop)->getIncrExprs()); + } + + // A statement taken out: from its block or its for, or made nothing + // where it is the whole of an if's or a loop's body. + void takeOut(LSLASTNode* node) + { + LSLASTNode* up = node->getParent(); + if (up && (up->getNodeSubType() == NODE_COMPOUND_STATEMENT || up->getNodeType() == NODE_AST_NODE_LIST)) + { + up->removeChild(node); + return; + } + LSLASTNode::replaceNode(node, ctx.allocator->newTracked()); + } + + static bool parameter(LSLSymbol* sym) + { + return sym->getSubType() == SYM_FUNCTION_PARAMETER || sym->getSubType() == SYM_EVENT_PARAMETER; + } + + // A variable -- a local, a global -- set and never read goes, and + // each place it is set keeps what setting it ran: `x = f();` is + // `f();`, `x++;` nothing, and `integer h = llListen(...);` the + // call. Only where every place that names it but its declaration + // sets it, and nothing reads what was set. A parameter so set + // loses its writes the same way, and stays. + void writeOnly(LSLScript* script) + { + struct Uses + { + LSLASTNode* declaration = nullptr; + std::vector writes; + bool read = false; + }; + boost::unordered_flat_map uses; + std::vector order; + const auto of = [&](LSLSymbol* sym) -> Uses& { + auto [it, fresh] = uses.try_emplace(sym); + if (fresh) + { + order.push_back(sym); + } + return it->second; + }; + each(script, [&](LSLASTNode* node) { + if (node->getNodeType() == NODE_GLOBAL_VARIABLE && node->getSymbol()) + { + of(node->getSymbol()).declaration = node; + } + else if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_DECLARATION && node->getSymbol()) + { + of(node->getSymbol()).declaration = node; + } + else if (node->getNodeType() == NODE_EXPRESSION && node->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + LSLSymbol* sym = node->getSymbol(); + if (!sym || (sym->getSubType() != SYM_LOCAL && sym->getSubType() != SYM_GLOBAL && !parameter(sym))) + { + return; + } + LSLASTNode* parent = node->getParent(); + const bool write = parent && parent->getNodeType() == NODE_EXPRESSION && + operation_mutates(static_cast(parent)->getOperation()) && parent->getChild(0) == node; + if (write && unread(parent)) + { + of(sym).writes.push_back(parent); + } + else + { + of(sym).read = true; + } + } + }); + for (LSLSymbol* sym : order) + { + // A global never used at all goes with the unused (globals()), + // and a local set to nothing that changes anything goes with + // those (unusedLocal); one set to what does is this pass's. + // A parameter's writes go, and the parameter stays. + Uses& use = uses[sym]; + const bool global = use.declaration && use.declaration->getNodeType() == NODE_GLOBAL_VARIABLE; + const bool given = parameter(sym); + if (use.read || (!use.declaration && !given) || ((global || given) && use.writes.empty())) + { + continue; + } + report.note(given ? use.writes.front()->getLoc() : use.declaration->getLoc(), "OptimizerRemovedWriteOnly", + "removed [1], which is set and never read", { sym->getName() }); + for (LSLASTNode* write : use.writes) + { + LSLASTNode* value = write->getNodeSubType() == NODE_BINARY_EXPRESSION ? write->getChild(1) : nullptr; + LSLASTNode* holder = write->getParent()->getNodeType() == NODE_AST_NODE_LIST ? write : write->getParent(); + if (value && !changesNothing(value)) + { + // What it was set to, still worked out. + LSLASTNode::replaceNode(write, write->takeChild(1)); + } + else + { + takeOut(holder); + } + } + if (global) + { + script->getSymbolTable()->remove(sym); + script->getGlobals()->removeChild(use.declaration); + } + else if (!given) + { + auto* decl = static_cast(use.declaration); + LSLASTNode* init = decl->getInitializer(); + forget(decl, sym); + if (init && !changesNothing(init)) + { + LSLASTNode::replaceNode(decl, ctx.allocator->newTracked(static_cast(decl->takeChild(1)))); + } + else + { + takeOut(decl); + } + } + ++changes; + } + } + + void states(LSLScript* script) + { + std::vector going; + for (LSLASTNode* state : *script->getStates()) + { + LSLSymbol* sym = state->getSymbol(); + if (!sym || sym->getReferences() != 1 || !strcmp(sym->getName(), "default")) + { + continue; + } + report.note(state->getLoc(), "OptimizerRemovedState", "removed the state [1], which nothing enters", { sym->getName() }); + going.push_back(state); + } + for (LSLASTNode* state : going) + { + script->getSymbolTable()->remove(state->getSymbol()); + script->getStates()->removeChild(state); + ++changes; + } + } + }; +} + + int removeDead(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + DeadCode dead(ctx, report, options); + return dead.run(script); + } +} diff --git a/indra/alscript/lsl/optimizer/allsldeadcode.h b/indra/alscript/lsl/optimizer/allsldeadcode.h new file mode 100644 index 00000000000..829690ea0bd --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsldeadcode.h @@ -0,0 +1,34 @@ +/** + * @file allsldeadcode.h + * @brief The LSL optimizer's dead code removal. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // What can never run, and what is never used, taken out. How many + // changes it made. + int removeDead(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/lsl/optimizer/allslflowvalues.cpp b/indra/alscript/lsl/optimizer/allslflowvalues.cpp new file mode 100644 index 00000000000..09cd39b17a7 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslflowvalues.cpp @@ -0,0 +1,460 @@ +/** + * @file allslflowvalues.cpp + * @brief What a local holds as the code runs, for the LSL optimizer. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslflowvalues.h" + +#include "allsleffects.h" + +#include + +#include + +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // Tailslide's values, and with them a local's or a parameter's through + // the code that runs on from where it was set to a constant: read + // before it is set again, on every way there, it is that constant -- + // where Tailslide gives one only to a variable never set after it is + // declared. What is known goes with the code as it runs: a statement's + // reads see it as the statement begins and its writes change it as it + // ends, and a read that something in the same statement may write + // first -- LSL runs a binary operator's right side before its left -- + // sees nothing. Both ways of an if are followed, and what they agree on + // goes on; a loop forgets what it writes before it begins, since it may + // come round again; and a label forgets everything, since a jump may + // come in from anywhere -- a loop holding one, everything before it + // begins. Lists are left alone, as Tailslide leaves them; and a local + // declared with no value is known only once it is set. + class FlowValues : public ConstantDeterminingVisitor + { + public: + // `vectors` where a vector's or a rotation's literal is no larger + // than what it is worked out from: not on Luau, which builds one + // where LSO and Mono load it. + FlowValues(AOperationBehavior* behavior, ScriptAllocator* allocator, const ALLSLEffects& effects, bool vectors) + : ConstantDeterminingVisitor(behavior, allocator), mEffects(effects), mVectors(vectors) + { + } + + bool beforeDescend(LSLASTNode* node) override + { + if (!ConstantDeterminingVisitor::beforeDescend(node)) + { + if (node->getNodeType() == NODE_STATEMENT || node->getNodeType() == NODE_EXPRESSION) + { + forget(mEffects.of(node)); + } + return false; + } + switch (node->getNodeType()) + { + case NODE_GLOBAL_FUNCTION: + case NODE_EVENT_HANDLER: + mState = State{}; + return true; + case NODE_STATEMENT: + statement(node); + return false; + default: + return true; + } + } + + bool visit(LSLLValueExpression* lvalue) override + { + ConstantDeterminingVisitor::visit(lvalue); + LSLSymbol* sym = lvalue->getSymbol(); + if (!sym || sym->getAssignments() == 0 || !tracked(sym) || !mState.reachable || written(lvalue)) + { + return true; + } + const auto found = mState.known.find(sym); + if (found == mState.known.end() || (mHot && mHot->writes(sym) && !readFirst(lvalue, sym))) + { + return true; + } + LSLConstant* cv = found->second; + if (LSLIdentifier* member = lvalue->getMember()) + { + cv = part(cv, member->getName()); + } + lvalue->setConstantValue(cv); + return true; + } + + bool visit(LSLDeclaration* decl) override + { + ConstantDeterminingVisitor::visit(decl); + LSLSymbol* sym = decl->getSymbol(); + LSLASTNode* init = decl->getChild(1); + if (!sym || !tracked(sym)) + { + return false; + } + // The value Tailslide gave the symbol from what it was declared + // with, made the symbol's type. + LSLConstant* cv = init && init->getNodeType() != NODE_NULL ? sym->getConstantValue() : nullptr; + if (cv && mState.reachable) + { + mState.known[sym] = cv; + } + else + { + mState.known.erase(sym); + } + return false; + } + + private: + struct State + { + boost::unordered_flat_map known; + // False after a return, a jump or a change of state, until a label. + bool reachable = true; + }; + + bool tracked(LSLSymbol* sym) const + { + if (sym->getSymbolType() != SYM_VARIABLE || + (sym->getSubType() != SYM_LOCAL && sym->getSubType() != SYM_FUNCTION_PARAMETER && sym->getSubType() != SYM_EVENT_PARAMETER)) + { + return false; + } + switch (sym->getIType()) + { + case LST_INTEGER: + case LST_FLOATINGPOINT: + case LST_STRING: + case LST_KEY: + return true; + case LST_VECTOR: + case LST_QUATERNION: + return mVectors; + default: + return false; + } + } + + // What an assignment or an increment writes, not a read. + static bool written(LSLLValueExpression* lvalue) + { + LSLASTNode* parent = lvalue->getParent(); + return parent && parent->getNodeType() == NODE_EXPRESSION && operation_mutates(static_cast(parent)->getOperation()) && + parent->getChild(0) == lvalue; + } + + // Whether nothing of the expression a read is in that runs before it + // writes what it reads. + bool readFirst(LSLLValueExpression* lvalue, LSLSymbol* sym) const + { + for (LSLASTNode* earlier : ALLSLEffects::before(mRoot, lvalue)) + { + if (mEffects.of(earlier).writes(sym)) + { + return false; + } + } + return true; + } + + // A vector's or a rotation's part, as Tailslide reads one. + LSLConstant* part(LSLConstant* cv, const char* member) const + { + if (!member) + { + return nullptr; + } + if (cv->getIType() == LST_VECTOR) + { + const Vector3* v = static_cast(cv)->getValue(); + switch (member[0]) + { + case 'x': return _mAllocator->newTracked(v->x); + case 'y': return _mAllocator->newTracked(v->y); + case 'z': return _mAllocator->newTracked(v->z); + default: return nullptr; + } + } + if (cv->getIType() == LST_QUATERNION) + { + const Quaternion* q = static_cast(cv)->getValue(); + switch (member[0]) + { + case 'x': return _mAllocator->newTracked(q->x); + case 'y': return _mAllocator->newTracked(q->y); + case 'z': return _mAllocator->newTracked(q->z); + case 's': return _mAllocator->newTracked(q->s); + default: return nullptr; + } + } + return nullptr; + } + + static bool same(LSLConstant* a, LSLConstant* b) + { + if (a == b) + { + return true; + } + if (a->getNodeSubType() != b->getNodeSubType()) + { + return false; + } + // Bit for bit: -0.0 is not 0.0, and NaN is not itself. + const auto bits = [](double x, double y) { return x == y && std::signbit(x) == std::signbit(y); }; + switch (a->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + return static_cast(a)->getValue() == static_cast(b)->getValue(); + case NODE_FLOAT_CONSTANT: + return bits(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_STRING_CONSTANT: + return !strcmp(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_KEY_CONSTANT: + return !strcmp(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_VECTOR_CONSTANT: + { + const Vector3* u = static_cast(a)->getValue(); + const Vector3* v = static_cast(b)->getValue(); + return bits(u->x, v->x) && bits(u->y, v->y) && bits(u->z, v->z); + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* p = static_cast(a)->getValue(); + const Quaternion* q = static_cast(b)->getValue(); + return bits(p->x, q->x) && bits(p->y, q->y) && bits(p->z, q->z) && bits(p->s, q->s); + } + default: + return false; + } + } + + // What two ways that meet both know. + static State merged(const State& a, const State& b) + { + if (!a.reachable) + { + return b; + } + if (!b.reachable) + { + return a; + } + State out; + for (const auto& [sym, cv] : a.known) + { + const auto there = b.known.find(sym); + if (there != b.known.end() && same(cv, there->second)) + { + out.known.emplace(sym, cv); + } + } + return out; + } + + void forget(const ALLSLEffects::Writes& writes) + { + for (LSLSymbol* sym : writes.variables) + { + mState.known.erase(sym); + } + } + + static bool holdsLabel(LSLASTNode* root) + { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL) + { + return true; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + return false; + } + + // A loop about to begin: what it writes forgotten, or everything + // where a jump could come into it. + void entering(LSLASTNode* loop, const ALLSLEffects::Writes& writes) + { + if (holdsLabel(loop)) + { + mState.known.clear(); + return; + } + forget(writes); + } + + // An expression run as one: its reads see what is known as it + // begins, and what it writes is known as it ends -- a variable set + // by the whole of it to a constant, that constant. + void expression(LSLASTNode* expr) + { + if (!expr || expr->getNodeType() == NODE_NULL) + { + return; + } + const ALLSLEffects::Writes writes = mEffects.of(expr); + LSLASTNode* const root = mRoot; + const ALLSLEffects::Writes* hot = mHot; + mRoot = expr; + mHot = &writes; + expr->visit(this); + mRoot = root; + mHot = hot; + LSLSymbol* set = nullptr; + LSLConstant* to = nullptr; + if (expr->getNodeType() == NODE_EXPRESSION && static_cast(expr)->getOperation() == OP_ASSIGN && + expr->getChild(0)->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(expr->getChild(0))->getMember()) + { + set = expr->getChild(0)->getSymbol(); + LSLASTNode* value = expr->getChild(1); + to = set && !mEffects.of(value).writes(set) ? value->getConstantValue() : nullptr; + } + forget(writes); + if (set && to && tracked(set) && mState.reachable) + { + if (to->getType() != set->getType()) + { + to = to->getType()->canCoerce(set->getType()) ? _mOperationBehavior->cast(set->getType(), to, to->getLoc()) : nullptr; + } + if (to) + { + mState.known[set] = to; + } + } + } + + void statement(LSLASTNode* stmt) + { + switch (stmt->getNodeSubType()) + { + case NODE_COMPOUND_STATEMENT: + visitChildren(stmt); + return; + case NODE_EXPRESSION_STATEMENT: + expression(stmt->getChild(0)); + return; + case NODE_RETURN_STATEMENT: + expression(stmt->getChild(0)); + mState.reachable = false; + return; + case NODE_DECLARATION: + // Known as visit(LSLDeclaration*) says, once its value has run. + stmt->getChild(0)->visit(this); + expression(stmt->getChild(1)); + return; + case NODE_STATE_STATEMENT: + case NODE_JUMP_STATEMENT: + visitChildren(stmt); + mState.reachable = false; + return; + case NODE_LABEL: + visitChildren(stmt); + mState = State{}; + return; + case NODE_IF_STATEMENT: + { + expression(stmt->getChild(0)); + const State before = mState; + stmt->getChild(1)->visit(this); + const State yes = std::move(mState); + mState = before; + stmt->getChild(2)->visit(this); + mState = merged(yes, mState); + return; + } + case NODE_WHILE_STATEMENT: + { + entering(stmt, mEffects.of(stmt)); + expression(stmt->getChild(0)); + const State checked = mState; + stmt->getChild(1)->visit(this); + mState = merged(checked, mState); + return; + } + case NODE_DO_STATEMENT: + { + entering(stmt, mEffects.of(stmt)); + const State begun = mState; + stmt->getChild(0)->visit(this); + expression(stmt->getChild(1)); + mState = merged(begun, mState); + return; + } + case NODE_FOR_STATEMENT: + { + for (LSLASTNode* init = stmt->getChild(0)->getChild(0); init; init = init->getNext()) + { + expression(init); + } + ALLSLEffects::Writes loops = mEffects.of(stmt->getChild(1)); + loops.add(mEffects.of(stmt->getChild(2))); + loops.add(mEffects.of(stmt->getChild(3))); + entering(stmt, loops); + expression(stmt->getChild(1)); + const State checked = mState; + stmt->getChild(3)->visit(this); + for (LSLASTNode* step = stmt->getChild(2)->getChild(0); step; step = step->getNext()) + { + expression(step); + } + mState = merged(checked, mState); + return; + } + default: + visitChildren(stmt); + forget(mEffects.of(stmt)); + return; + } + } + + const ALLSLEffects& mEffects; + const bool mVectors; + State mState; + // The expression being run, and what it writes. + LSLASTNode* mRoot = nullptr; + const ALLSLEffects::Writes* mHot = nullptr; + }; +} + + void flowValues(LSLScript* script, AOperationBehavior* behavior, ScriptAllocator* allocator, const ALLSLEffects& effects, bool vectors) + { + FlowValues values(behavior, allocator, effects, vectors); + script->visit(&values); + } +} diff --git a/indra/alscript/lsl/optimizer/allslflowvalues.h b/indra/alscript/lsl/optimizer/allslflowvalues.h new file mode 100644 index 00000000000..7d7c0fd2bd1 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslflowvalues.h @@ -0,0 +1,36 @@ +/** + * @file allslflowvalues.h + * @brief What a local holds as the code runs, for the LSL optimizer. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // Tailslide's values, and with them a local's or a parameter's through + // the code that runs on from where it was set to a constant: each read + // given the value it has on every way there. `vectors` where a vector's + // or a rotation's literal is no larger than what it is worked out from. + void flowValues(LSLScript* script, AOperationBehavior* behavior, ScriptAllocator* allocator, const ALLSLEffects& effects, bool vectors); +} diff --git a/indra/alscript/lsl/optimizer/allslfolder.cpp b/indra/alscript/lsl/optimizer/allslfolder.cpp new file mode 100644 index 00000000000..4b082e7857d --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslfolder.cpp @@ -0,0 +1,461 @@ +/** + * @file allslfolder.cpp + * @brief The LSL optimizer's folder. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslfolder.h" + +#include "allslcosts.h" +#include "allsllibraryfold.h" + +#include + +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // Constant expressions and never-assigned variables become their + // values; a call to a pure library function with its arguments in + // hand becomes its answer. + class Folder : public ASTVisitor, public Pass + { + public: + using Pass::Pass; + + bool visit(LSLExpression* expr) override + { + switch (expr->getNodeSubType()) + { + case NODE_CONSTANT_EXPRESSION: + return false; + case NODE_FUNCTION_EXPRESSION: + return !call(static_cast(expr)); + default: + break; + } + LSLConstant* cv = expr->getConstantValue(); + if (cv && inlineable(cv)) + { + fold(expr, cv, "OptimizerFolded", "folded"); + return false; + } + return true; + } + + bool visit(LSLLValueExpression* lvalue) override + { + LSLSymbol* sym = lvalue->getSymbol(); + if (!sym || sym->getSubType() == SYM_BUILTIN || sym->getIType() == LST_LIST) + { + // A builtin constant is a token the compiler already has. + return false; + } + if (sym->getIType() == LST_KEY && !keyMayInline(lvalue)) + { + return false; + } + LSLConstant* cv = lvalue->getConstantValue(); + if (cv && inlineable(cv) && writeOut(sym, cv) && (sym->getAssignments() == 0 || sym->getSubType() == SYM_GLOBAL || allKnown(lvalue, sym))) + { + fold(lvalue, cv, "OptimizerInlinedConstant", "inlined"); + } + return false; + } + + private: + // Of a variable set more than once, whose reads have values where + // the code that runs to them set it to a constant (FlowValues): + // whether every read has one, so that with all of them written out + // the variable goes. Where one has none the variable stays, and a + // value written at a read alone is a literal in place of a read -- + // larger, for most -- unless what it is read in folds with it, + // which is that expression's fold, not this. + bool allKnown(LSLLValueExpression* lvalue, LSLSymbol* sym) + { + if (!mCounted) + { + mCounted = true; + std::vector stack{ lvalue->getRoot() }; + while (!stack.empty()) + { + LSLASTNode* n = stack.back(); + stack.pop_back(); + if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION && n->getSymbol() && + n->getSymbol()->getAssignments() > 0) + { + auto* read = static_cast(n); + LSLASTNode* parent = n->getParent(); + const bool set = parent && parent->getNodeType() == NODE_EXPRESSION && + static_cast(parent)->getOperation() == OP_ASSIGN && parent->getChild(0) == n; + if (!set) + { + // A compound assignment or an increment reads it + // too, with nothing to write in its place. + const bool mutated = parent && parent->getNodeType() == NODE_EXPRESSION && + operation_mutates(static_cast(parent)->getOperation()) && parent->getChild(0) == n; + LSLConstant* known = mutated ? nullptr : read->getConstantValue(); + auto& count = mReads[n->getSymbol()]; + ++count.first; + if (known && inlineable(known) && (n->getSymbol()->getIType() != LST_KEY || keyMayInline(read))) + { + ++count.second; + } + } + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + } + const auto found = mReads.find(sym); + return found != mReads.end() && found->second.first == found->second.second; + } + + // Each such variable's reads, and those of them with a value. + boost::unordered_flat_map> mReads; + bool mCounted = false; + + // A global's value goes where it is read only where writing it at + // every place it is read costs less than the global does: a vector + // read ten times is ten vectors. What goes where is the target's + // (ALLSLCosts). A value a larger expression folds into is not a + // read of it, and folds whatever this says. + bool writeOut(LSLSymbol* sym, LSLConstant* cv) const + { + if (sym->getSubType() != SYM_GLOBAL) + { + return true; + } + const std::optional held = cv->getIType() == LST_KEY ? std::nullopt : heldFor(ctx.target, cv); + if (!held) + { + return true; + } + return held->writeOut(sym->getReferences() - 1 - sym->getAssignments()); + } + + // Tailslide's rules: a key's key-ness must not be lost to a list, + // a print, a condition or a boolean. + static bool keyMayInline(LSLLValueExpression* lvalue) + { + LSLASTNode* ancestor = lvalue->getParent(); + LSLASTNode* top = lvalue; + while (ancestor && ancestor->getNodeType() == NODE_EXPRESSION) + { + switch (ancestor->getNodeSubType()) + { + case NODE_LIST_EXPRESSION: + case NODE_PRINT_EXPRESSION: + case NODE_BOOL_CONVERSION_EXPRESSION: + return false; + case NODE_TYPECAST_EXPRESSION: + if (ancestor->getIType() == LST_LIST) + { + return false; + } + break; + default: + break; + } + top = ancestor; + ancestor = ancestor->getParent(); + } + if (top->getIType() == LST_KEY) + { + if (ancestor && ancestor->getNodeType() == NODE_AST_NODE_LIST) + { + ancestor = ancestor->getParent(); + } + if (ancestor && ancestor->getNodeType() == NODE_STATEMENT) + { + switch (ancestor->getNodeSubType()) + { + case NODE_WHILE_STATEMENT: + case NODE_IF_STATEMENT: + case NODE_DO_STATEMENT: + case NODE_FOR_STATEMENT: + return false; + default: + break; + } + } + } + return true; + } + + bool call(LSLFunctionExpression* expr) + { + LSLSymbol* sym = expr->getSymbol(); + if (!sym || sym->getSubType() != SYM_BUILTIN || !isPure(sym->getName())) + { + return false; + } + Args args; + for (LSLASTNode* arg : *expr->getArguments()) + { + LSLConstant* cv = arg->getConstantValue(); + if (!cv && arg->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + // A list is never written into the script in place of + // its name, but a call may still be answered from it. + auto* lvalue = static_cast(arg); + LSLSymbol* sym = lvalue->getSymbol(); + if (sym && !lvalue->getMember() && sym->getAssignments() == 0 && sym->getSubType() != SYM_BUILTIN) + { + cv = sym->getConstantValue(); + } + } + if (!cv) + { + return false; + } + args.push_back(cv); + } + LSLConstant* value = evaluate(ctx, sym->getName(), args); + if (!value) + { + return false; + } + if (const std::string* name = ctx.nameOf(value)) + { + // The builtin's name in place of the call: a token the + // compiler already has. + LSLSymbol* builtin = ctx.context->builtins->lookup(name->c_str(), SYM_VARIABLE); + if (!builtin) + { + return false; + } + // newTracked passes the context itself. + auto* id = ctx.allocator->newTracked(ctx.allocator->copyStr(name->c_str())); + id->setSymbol(builtin); + auto* lvalue = ctx.allocator->newTracked(id, static_cast(nullptr)); + lvalue->setLoc(expr->getLoc()); + id->setLoc(expr->getLoc()); + report.note(expr->getLoc(), "OptimizerEvaluated", "evaluated [1] to [2]", { render(expr), *name }); + LSLASTNode::replaceNode(expr, lvalue); + ++changes; + return true; + } + if (!inlineable(value, true)) + { + return false; + } + // An answer no longer than the strings the call was given takes + // no more room than they did; a longer one can be larger than + // the call, on a target that writes a string at every place it + // is used, and on one that holds each string once where the + // answer is new to it. + if (value->getIType() == LST_STRING && strlen(static_cast(value)->getValue()) > givenChars(expr) && + !smallerAnswered(expr, args, static_cast(value))) + { + return false; + } + fold(expr, value, "OptimizerEvaluated", "evaluated"); + return true; + } + + // Whether a string is smaller than the call it answers, as the + // target's compiler counts: each compiled in a script of its own, + // beside what stays either way -- the function called with its + // strings emptied, for the reference to it a script may keep, and + // each string of the call's and the answer that the code holds + // elsewhere. + bool smallerAnswered(LSLFunctionExpression* expr, const Args& args, LSLStringConstant* value) + { + const bool wide = ctx.target == ALLSLOptimizer::Target::Luau; + const std::optional answer = ALLSLValues::literal(value, wide); + if (!answer) + { + return false; + } + const std::string name = expr->getSymbol()->getName(); + std::string call = name + "("; + std::string again = call; + Counted own; + for (size_t i = 0; i < args.size(); ++i) + { + const std::optional arg = ALLSLValues::literal(args[i], wide); + const std::optional empty = emptied(args[i], wide); + if (!arg || !empty) + { + return false; + } + call += (i ? ", " : "") + *arg; + again += (i ? ", " : "") + *empty; + count(args[i], own); + } + call += ")"; + again += ")"; + std::string before = "default { state_entry() { llOwnerSay(" + again + ");"; + const Counted held = holds(expr); + const auto elsewhere = [&](const std::string& text) { + const auto in = held.find(text); + const auto mine = own.find(text); + return in != held.end() && in->second > (mine == own.end() ? 0 : mine->second); + }; + for (const auto& [text, times] : own) + { + if (elsewhere(text)) + { + const std::optional written = ALLSLValues::literal(ctx.allocator->newTracked(ctx.allocator->copyStr(text.c_str())), wide); + if (!written) + { + return false; + } + before += " llOwnerSay(" + *written + ");"; + } + } + if (elsewhere(value->getValue())) + { + before += " llOwnerSay(" + *answer + ");"; + } + const std::string key = before + call; + if (const auto found = ctx.answers.find(key); found != ctx.answers.end()) + { + return found->second; + } + const ALScriptWeight asCall = weigh(ctx.target, before + " llOwnerSay(" + call + "); } }"); + const ALScriptWeight asAnswer = weigh(ctx.target, before + " llOwnerSay(" + *answer + "); } }"); + const bool smaller = asCall.compiled && asAnswer.compiled && asAnswer.total <= asCall.total; + ctx.answers.emplace(key, smaller); + return smaller; + } + + // The strings in a value, each as many times as it is there. + static void count(LSLConstant* cv, Counted& out) + { + switch (cv->getIType()) + { + case LST_STRING: + ++out[static_cast(cv)->getValue()]; + break; + case LST_KEY: + ++out[static_cast(cv)->getValue()]; + break; + case LST_LIST: + for (LSLASTNode* item : *cv) + { + count(static_cast(item), out); + } + break; + default: + break; + } + } + + // A value as a literal of its shape, its strings empty. + static std::optional emptied(LSLConstant* cv, bool wide) + { + switch (cv->getIType()) + { + case LST_STRING: + case LST_KEY: + return "\"\""; + case LST_LIST: + { + std::string out = "["; + for (LSLASTNode* item : *cv) + { + const std::optional element = emptied(static_cast(item), wide); + if (!element) + { + return std::nullopt; + } + out += (out.size() > 1 ? ", " : "") + *element; + } + return out + "]"; + } + default: + return ALLSLValues::literal(cv, wide); + } + } + + // The strings the code holds where a place would share them, each + // as many times as it is written, as the code is now: Mono's are + // the whole script's, Luau's each function's; LSO writes one + // wherever it is used. + Counted holds(LSLASTNode* at) const + { + if (ctx.target == ALLSLOptimizer::Target::LSO) + { + return {}; + } + LSLASTNode* scope = at; + while (scope->getParent() && (ctx.target == ALLSLOptimizer::Target::Mono || (scope->getNodeType() != NODE_GLOBAL_FUNCTION && + scope->getNodeType() != NODE_EVENT_HANDLER))) + { + scope = scope->getParent(); + } + Strings strings; + scope->visit(&strings); + return std::move(strings.found); + } + + // The characters of the strings written in a call's arguments, + // which go with it: a variable's value stays where it is. + static size_t givenChars(LSLFunctionExpression* expr) + { + Counted given; + for (LSLASTNode* arg : *expr->getArguments()) + { + if (arg->getNodeSubType() != NODE_LVALUE_EXPRESSION && arg->getConstantValue()) + { + count(arg->getConstantValue(), given); + } + } + size_t chars = 0; + for (const auto& [text, times] : given) + { + chars += text.size() * times; + } + return chars; + } + }; +} + + int fold(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + Folder folder(ctx, report, options); + script->visit(&folder); + return folder.changes; + } + + int foldGlobalValues(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + Folder folder(ctx, report, options); + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_VARIABLE) + { + global->visit(&folder); + } + } + return folder.changes; + } +} diff --git a/indra/alscript/lsl/optimizer/allslfolder.h b/indra/alscript/lsl/optimizer/allslfolder.h new file mode 100644 index 00000000000..929f3d1683b --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslfolder.h @@ -0,0 +1,37 @@ +/** + * @file allslfolder.h + * @brief The LSL optimizer's folder. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // Constant expressions and never-assigned variables made their values, + // and a pure library call with its arguments in hand its answer: over + // the whole script, and over its globals' values alone. How many + // changes each made. + int fold(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); + int foldGlobalValues(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/lsl/optimizer/allslglobals.cpp b/indra/alscript/lsl/optimizer/allslglobals.cpp new file mode 100644 index 00000000000..3518b8a9613 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslglobals.cpp @@ -0,0 +1,361 @@ +/** + * @file allslglobals.cpp + * @brief The LSL optimizer's globals that are only scratch made locals. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslglobals.h" + +#include +#include + +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + class ScratchGlobals : public Pass + { + public: + using Pass::Pass; + + int run(LSLScript* script) + { + // Each function and handler, and the script's functions each + // calls. + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_FUNCTION && global->getSymbol()) + { + mFunctions[global->getSymbol()] = global; + mOwners.push_back(global); + } + } + for (LSLASTNode* state : *script->getStates()) + { + for (LSLASTNode* handler = state->getChild(1) ? state->getChild(1)->getChild(0) : nullptr; handler; handler = handler->getNext()) + { + mOwners.push_back(handler); + } + } + for (LSLASTNode* owner : mOwners) + { + auto& calls = mCalls[owner]; + eachNode(owner, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_FUNCTION_EXPRESSION) + { + const auto found = mFunctions.find(n->getSymbol()); + if (found != mFunctions.end()) + { + calls.insert(found->second); + } + } + }); + } + std::vector globals; + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_VARIABLE && global->getSymbol()) + { + globals.push_back(global); + } + } + for (LSLASTNode* global : globals) + { + localize(script, global); + } + return changes; + } + + private: + boost::unordered_flat_map mFunctions; + std::vector mOwners; + boost::unordered_flat_map> mCalls; + + static bool isStatement(LSLASTNode* n, LSLNodeSubType type) + { + return n && n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == type; + } + + // What a global of a type costs the script, its default's value. + static S32 globalCost(const ALLSLCosts& costs, LSLIType type) + { + switch (type) + { + case LST_INTEGER: return costs.integer.global; + case LST_FLOATINGPOINT: return costs.wholeFloating.global; + case LST_VECTOR: return costs.wholeVector.global; + case LST_QUATERNION: return costs.wholeRotation.global; + default: return costs.string.global; + } + } + + static bool mentions(LSLASTNode* root, LSLSymbol* sym) + { + bool found = false; + eachNode(root, [&](LSLASTNode* n) { found = found || (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym); }); + return found; + } + + // The functions a function or handler calls, however far on. + boost::unordered_flat_set reached(LSLASTNode* from) + { + boost::unordered_flat_set seen; + std::vector todo(mCalls[from].begin(), mCalls[from].end()); + while (!todo.empty()) + { + LSLASTNode* f = todo.back(); + todo.pop_back(); + if (seen.insert(f).second) + { + todo.insert(todo.end(), mCalls[f].begin(), mCalls[f].end()); + } + } + return seen; + } + + // `G = e`, whole, e naming nothing of G. + static bool setsWhole(LSLASTNode* expr, LSLSymbol* sym) + { + if (!expr || expr->getNodeType() != NODE_EXPRESSION || static_cast(expr)->getOperation() != OP_ASSIGN || + expr->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return false; + } + LSLASTNode* target = expr->getChild(0); + return target->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(target)->getMember() && + target->getSymbol() == sym && !mentions(expr->getChild(1), sym); + } + + // Whether every read of G in a statement comes after G is set, on + // every way there, from `set` (whether it is set as the statement + // begins); where it does, `set` as it ends. + static bool setBeforeRead(LSLASTNode* stmt, LSLSymbol* sym, bool& set) + { + if (!stmt || stmt->getNodeType() == NODE_NULL) + { + return true; + } + // An expression: set whole, or read only once set. + const auto expression = [&](LSLASTNode* expr) { + if (!expr || expr->getNodeType() == NODE_NULL) + { + return true; + } + if (setsWhole(expr, sym)) + { + set = true; + return true; + } + return set || !mentions(expr, sym); + }; + if (stmt->getNodeType() != NODE_STATEMENT) + { + return expression(stmt); + } + switch (stmt->getNodeSubType()) + { + case NODE_COMPOUND_STATEMENT: + for (LSLASTNode* s = stmt->getChild(0); s; s = s->getNext()) + { + if (!setBeforeRead(s, sym, set)) + { + return false; + } + } + return true; + case NODE_EXPRESSION_STATEMENT: + case NODE_RETURN_STATEMENT: + return expression(stmt->getChild(0)); + case NODE_DECLARATION: + return expression(stmt->getChild(1)); + case NODE_LABEL: + // A jump may come in from anywhere. + set = false; + return true; + case NODE_IF_STATEMENT: + { + if (!expression(stmt->getChild(0))) + { + return false; + } + bool yes = set; + bool no = set; + if (!setBeforeRead(stmt->getChild(1), sym, yes) || !setBeforeRead(stmt->getChild(2), sym, no)) + { + return false; + } + set = yes && no; + return true; + } + case NODE_WHILE_STATEMENT: + { + // The body may not run, and runs again as set as it was. + if (!expression(stmt->getChild(0))) + { + return false; + } + bool body = set; + return setBeforeRead(stmt->getChild(1), sym, body); + } + case NODE_DO_STATEMENT: + return setBeforeRead(stmt->getChild(0), sym, set) && expression(stmt->getChild(1)); + case NODE_FOR_STATEMENT: + { + for (LSLASTNode* init = stmt->getChild(0)->getChild(0); init; init = init->getNext()) + { + if (!expression(init)) + { + return false; + } + } + if (!expression(stmt->getChild(1))) + { + return false; + } + bool body = set; + if (!setBeforeRead(stmt->getChild(3), sym, body)) + { + return false; + } + for (LSLASTNode* step = stmt->getChild(2)->getChild(0); step; step = step->getNext()) + { + if (!(body || !mentions(step, sym))) + { + return false; + } + } + return true; + } + default: + return !mentions(stmt, sym) || set; + } + } + + // The global made a local of each function and handler that names it. + void localize(LSLScript* script, LSLASTNode* global) + { + LSLSymbol* sym = global->getSymbol(); + LSLASTNode* init = global->getChild(1); + if (init && init->getNodeType() == NODE_EXPRESSION && !changesNothing(init)) + { + return; + } + std::vector owners; + for (LSLASTNode* owner : mOwners) + { + if (mentions(owner, sym)) + { + owners.push_back(owner); + } + } + if (owners.empty()) + { + return; + } + // A local in each, on a target that keeps a frame for every one, + // against the global it replaces. + const ALLSLCosts& costs = ALLSLCosts::of(ctx.target); + if (costs.localFrame > 0 && static_cast(owners.size()) * costs.localFrame >= globalCost(costs, sym->getIType())) + { + return; + } + const boost::unordered_flat_set named(owners.begin(), owners.end()); + for (LSLASTNode* owner : owners) + { + // Set before read in its body; no other name the same; and no + // call on from it into one that names the global. + LSLStatement* body = owner->getNodeType() == NODE_GLOBAL_FUNCTION ? static_cast(owner)->getStatements() + : static_cast(owner)->getStatements(); + bool set = false; + if (!isStatement(body, NODE_COMPOUND_STATEMENT) || !setBeforeRead(body, sym, set)) + { + return; + } + bool clash = false; + eachNode(owner, [&](LSLASTNode* n) { + clash = clash || (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() != sym && + static_cast(n)->getName() && !strcmp(static_cast(n)->getName(), sym->getName())); + }); + if (clash) + { + return; + } + for (LSLASTNode* f : reached(owner)) + { + if (named.contains(f)) + { + return; + } + } + } + report.note(global->getLoc(), "OptimizerLocalizedGlobal", "made the global [1] a local where it is used, each setting it before it reads it", + { sym->getName() }); + const Uncounted uncounted(*ctx.context); + for (LSLASTNode* owner : owners) + { + LSLStatement* body = owner->getNodeType() == NODE_GLOBAL_FUNCTION ? static_cast(owner)->getStatements() + : static_cast(owner)->getStatements(); + LSLType* type = sym->getType(); + auto* id = ctx.allocator->newTracked(type, sym->getName()); + id->setLoc(body->getLoc()); + auto* decl = ctx.allocator->newTracked(id, static_cast(nullptr)); + decl->setLoc(body->getLoc()); + auto* local = ctx.allocator->newTracked(sym->getName(), type, SYM_VARIABLE, SYM_LOCAL, body->getLoc(), nullptr, decl); + id->setSymbol(local); + eachNode(body, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym) + { + static_cast(n)->setSymbol(local); + } + }); + std::vector stmts{ decl }; + for (LSLASTNode* stmt = body->getChild(0); stmt; stmt = stmt->getNext()) + { + stmts.push_back(stmt); + } + setStatements(body, stmts, *ctx.context); + if (LSLSymbolTable* table = body->getSymbolTable()) + { + table->define(local); + } + else + { + decl->defineSymbol(local); + } + } + script->getSymbolTable()->remove(sym); + script->getGlobals()->removeChild(global); + ++changes; + } + }; +} + + int localizeGlobals(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + ScratchGlobals scratch(ctx, report, options); + return scratch.run(script); + } +} diff --git a/indra/alscript/lsl/optimizer/allslglobals.h b/indra/alscript/lsl/optimizer/allslglobals.h new file mode 100644 index 00000000000..a03ce7063f8 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslglobals.h @@ -0,0 +1,37 @@ +/** + * @file allslglobals.h + * @brief The LSL optimizer's globals that are only scratch made locals. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // A global no function or handler that names it reads before setting + // it -- each sets it, on every way, before any read -- and none of them + // calls, however far on, another that names it: scratch, whose value + // never outlives the run of the code that set it, made a local of each. + // How many it made so. + int localizeGlobals(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/allslinliner.cpp b/indra/alscript/lsl/optimizer/allslinliner.cpp similarity index 100% rename from indra/alscript/allslinliner.cpp rename to indra/alscript/lsl/optimizer/allslinliner.cpp diff --git a/indra/alscript/allslinliner.h b/indra/alscript/lsl/optimizer/allslinliner.h similarity index 100% rename from indra/alscript/allslinliner.h rename to indra/alscript/lsl/optimizer/allslinliner.h diff --git a/indra/alscript/lsl/optimizer/allsllibraryfold.cpp b/indra/alscript/lsl/optimizer/allsllibraryfold.cpp new file mode 100644 index 00000000000..66e768ebb38 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsllibraryfold.cpp @@ -0,0 +1,2088 @@ +/** + * @file allsllibraryfold.cpp + * @brief The LSL library, answered for constant arguments. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allsllibraryfold.h" + +#include "llmath.h" +#include "llmd5.h" +#include "llquaternion.h" +#include "v3math.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + bool argInt(const Args& a, size_t i, int& out) + { + if (i >= a.size() || a[i]->getNodeSubType() != NODE_INTEGER_CONSTANT) + { + return false; + } + out = static_cast(a[i])->getValue(); + return true; + } + + // A float argument, or an integer where LSL would promote one. + bool argFloat(const Args& a, size_t i, double& out) + { + if (i >= a.size()) + { + return false; + } + if (a[i]->getNodeSubType() == NODE_FLOAT_CONSTANT) + { + out = static_cast(static_cast(static_cast(a[i])->getValue())); + return true; + } + if (a[i]->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + out = static_cast(static_cast(static_cast(a[i])->getValue())); + return true; + } + return false; + } + + // A string argument, plain ASCII, so that no VM's idea of a character + // differs from ours. + bool argString(const Args& a, size_t i, std::string& out) + { + if (i >= a.size() || a[i]->getNodeSubType() != NODE_STRING_CONSTANT) + { + return false; + } + const char* s = static_cast(a[i])->getValue(); + if (!ascii(s)) + { + return false; + } + out = s; + return true; + } + + bool argList(const Args& a, size_t i, LSLListConstant*& out) + { + if (i >= a.size() || a[i]->getNodeSubType() != NODE_LIST_CONSTANT) + { + return false; + } + out = static_cast(a[i]); + return true; + } + + bool argVector(const Args& a, size_t i, Vector3& out) + { + if (i >= a.size() || a[i]->getNodeSubType() != NODE_VECTOR_CONSTANT) + { + return false; + } + out = *static_cast(a[i])->getValue(); + return true; + } + + // A rotation argument that is a unit quaternion, near enough that the + // VM's normalising of it changes nothing: what the rotation functions + // are folded over, since what a VM does with the rest is its own. + bool argUnitRotation(const Args& a, size_t i, LLQuaternion& out) + { + if (i >= a.size() || a[i]->getNodeSubType() != NODE_QUATERNION_CONSTANT) + { + return false; + } + const Quaternion* q = static_cast(a[i])->getValue(); + const float mag = std::sqrt(q->x * q->x + q->y * q->y + q->z * q->z + q->s * q->s); + if (!std::isfinite(mag) || std::fabs(mag - 1.0f) >= 1e-6f) + { + return false; + } + // set() normalises, which changes nothing of a unit rotation but + // the last bit; the components are taken as they are. + out.mQ[VX] = q->x; + out.mQ[VY] = q->y; + out.mQ[VZ] = q->z; + out.mQ[VW] = q->s; + return true; + } + + std::vector elements(LSLListConstant* list) + { + std::vector out; + for (LSLASTNode* child : *list) + { + out.push_back(static_cast(child)); + } + return out; + } + + LSLConstant* listOf(Ctx& ctx, const std::vector& items) + { + auto* list = ctx.allocator->newTracked(nullptr); + for (LSLConstant* item : items) + { + list->pushChild(item->copy(ctx.allocator)); + } + return list; + } + + // LSL's indices: negative from the end. `start > end` names the + // outside of the range, for a get, and the inside for a delete. + void normalise(int length, int& start, int& end) + { + if (start < 0) + { + start += length; + } + if (end < 0) + { + end += length; + } + } + + template std::vector subRange(const std::vector& v, int start, int end) + { + const int length = static_cast(v.size()); + normalise(length, start, end); + std::vector out; + if (start <= end) + { + for (int i = std::max(0, start); i <= std::min(length - 1, end); ++i) + { + out.push_back(v[i]); + } + return out; + } + for (int i = 0; i <= std::min(length - 1, end); ++i) + { + out.push_back(v[i]); + } + for (int i = std::max(0, start); i < length; ++i) + { + out.push_back(v[i]); + } + return out; + } + + template std::vector deleteRange(const std::vector& v, int start, int end) + { + const int length = static_cast(v.size()); + normalise(length, start, end); + std::vector out; + for (int i = 0; i < length; ++i) + { + const bool inside = start <= end ? (i >= start && i <= end) : (i <= end || i >= start); + if (!inside) + { + out.push_back(v[i]); + } + } + return out; + } + + std::vector chars(const std::string& s) { return std::vector(s.begin(), s.end()); } + std::string fromChars(const std::vector& v) { return std::string(v.begin(), v.end()); } + + // What (string) makes of a float: six places, as both VMs write it. + std::string floatString(double v) + { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", static_cast(static_cast(v))); + return buffer; + } + + // An element as llList2String and llList2CSV write it, or nothing for + // what neither should fold. + bool elementText(LSLConstant* c, std::string& out) + { + switch (c->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + out = std::to_string(static_cast(c)->getValue()); + return true; + case NODE_FLOAT_CONSTANT: + out = floatString(static_cast(c)->getValue()); + return true; + case NODE_STRING_CONSTANT: + case NODE_KEY_CONSTANT: + out = static_cast(c)->getValue(); + return ascii(out.c_str()); + case NODE_VECTOR_CONSTANT: + { + const Vector3* v = static_cast(c)->getValue(); + out = "<" + floatString(v->x) + ", " + floatString(v->y) + ", " + floatString(v->z) + ">"; + return true; + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* q = static_cast(c)->getValue(); + out = "<" + floatString(q->x) + ", " + floatString(q->y) + ", " + floatString(q->z) + ", " + floatString(q->s) + ">"; + return true; + } + default: + return false; + } + } + + const char* const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string toBase64(const std::string& in) + { + std::string out; + size_t i = 0; + while (i + 2 < in.size()) + { + const uint32_t n = (static_cast(in[i]) << 16) | (static_cast(in[i + 1]) << 8) | static_cast(in[i + 2]); + out += BASE64[(n >> 18) & 63]; + out += BASE64[(n >> 12) & 63]; + out += BASE64[(n >> 6) & 63]; + out += BASE64[n & 63]; + i += 3; + } + if (i + 1 == in.size()) + { + const uint32_t n = static_cast(in[i]) << 16; + out += BASE64[(n >> 18) & 63]; + out += BASE64[(n >> 12) & 63]; + out += "=="; + } + else if (i + 2 == in.size()) + { + const uint32_t n = (static_cast(in[i]) << 16) | (static_cast(in[i + 1]) << 8); + out += BASE64[(n >> 18) & 63]; + out += BASE64[(n >> 12) & 63]; + out += BASE64[(n >> 6) & 63]; + out += '='; + } + return out; + } + + // Strictly formed input only; anything else is the VM's to answer. + bool fromBase64(const std::string& in, std::string& out) + { + if (in.size() % 4 != 0) + { + return false; + } + out.clear(); + for (size_t i = 0; i < in.size(); i += 4) + { + uint32_t n = 0; + int pad = 0; + for (int k = 0; k < 4; ++k) + { + const char c = in[i + k]; + if (c == '=') + { + if (i + 4 != in.size() || (k < 2)) + { + return false; + } + ++pad; + n <<= 6; + continue; + } + if (pad) + { + return false; + } + const char* p = strchr(BASE64, c); + if (!p || !c) + { + return false; + } + n = (n << 6) | static_cast(p - BASE64); + } + out += static_cast((n >> 16) & 255); + if (pad < 2) + { + out += static_cast((n >> 8) & 255); + } + if (pad < 1) + { + out += static_cast(n & 255); + } + } + return true; + } + + // A strict reader of JSON, for llJsonGetValue: only what RFC 8259 + // allows, in ASCII, so that what is folded is what every reader + // agrees on; anything the simulator's own reader might take another + // way -- duplicate keys, numbers with a fraction or an exponent, + // escapes outside ASCII, and the JSON_* answers, which are + // characters no string literal here may hold -- is left to it. + struct JsonField; + + struct JsonValue + { + enum class Kind : U8 + { + Null, + True, + False, + Number, + String, + Array, + Object + }; + Kind kind = Kind::Null; + std::string text; // a number as written, a string unescaped + std::vector items; + std::vector fields; + }; + + struct JsonField + { + std::string first; + JsonValue second; + }; + + struct JsonReader + { + const std::string& in; + size_t at = 0; + bool ok = true; + + explicit JsonReader(const std::string& text) : in(text) {} + + void space() + { + while (at < in.size() && (in[at] == ' ' || in[at] == '\t' || in[at] == '\n' || in[at] == '\r')) + { + ++at; + } + } + bool take(char c) + { + if (at < in.size() && in[at] == c) + { + ++at; + return true; + } + return false; + } + bool word(const char* w) + { + const size_t n = strlen(w); + if (in.compare(at, n, w) == 0) + { + at += n; + return true; + } + return false; + } + bool string(std::string& out) + { + if (!take('"')) + { + return false; + } + out.clear(); + while (at < in.size()) + { + const char c = in[at++]; + if (c == '"') + { + return true; + } + if (static_cast(c) < 0x20) + { + return false; + } + if (c != '\\') + { + out += c; + continue; + } + if (at >= in.size()) + { + return false; + } + const char e = in[at++]; + switch (e) + { + case '"': out += '"'; break; + case '\\': out += '\\'; break; + case '/': out += '/'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'u': + { + if (at + 4 > in.size()) + { + return false; + } + unsigned code = 0; + for (int k = 0; k < 4; ++k) + { + const char h = in[at++]; + if (!isxdigit(static_cast(h))) + { + return false; + } + code = code * 16 + static_cast(isdigit(static_cast(h)) ? h - '0' : tolower(h) - 'a' + 10); + } + if (code == 0 || code > 0x7F) + { + return false; + } + out += static_cast(code); + break; + } + default: return false; + } + } + return false; + } + bool value(JsonValue& out) + { + space(); + if (at >= in.size()) + { + return false; + } + const char c = in[at]; + if (c == '{') + { + ++at; + out.kind = JsonValue::Kind::Object; + space(); + if (take('}')) + { + return true; + } + while (true) + { + space(); + std::string key; + if (!string(key)) + { + return false; + } + for (const auto& field : out.fields) + { + if (field.first == key) + { + return false; + } + } + space(); + if (!take(':')) + { + return false; + } + JsonValue v; + if (!value(v)) + { + return false; + } + out.fields.emplace_back(std::move(key), std::move(v)); + space(); + if (take(',')) + { + continue; + } + return take('}'); + } + } + if (c == '[') + { + ++at; + out.kind = JsonValue::Kind::Array; + space(); + if (take(']')) + { + return true; + } + while (true) + { + JsonValue v; + if (!value(v)) + { + return false; + } + out.items.push_back(std::move(v)); + space(); + if (take(',')) + { + continue; + } + return take(']'); + } + } + if (c == '"') + { + out.kind = JsonValue::Kind::String; + return string(out.text); + } + if (word("true")) + { + out.kind = JsonValue::Kind::True; + return true; + } + if (word("false")) + { + out.kind = JsonValue::Kind::False; + return true; + } + if (word("null")) + { + out.kind = JsonValue::Kind::Null; + return true; + } + // A number: only a plain integer is taken; a fraction or an + // exponent is left to the simulator's own formatting. + const size_t start = at; + take('-'); + if (at < in.size() && in[at] == '0') + { + ++at; + } + else if (at < in.size() && in[at] >= '1' && in[at] <= '9') + { + while (at < in.size() && isdigit(static_cast(in[at]))) + { + ++at; + } + } + else + { + return false; + } + if (at < in.size() && (in[at] == '.' || in[at] == 'e' || in[at] == 'E')) + { + ok = false; + return false; + } + out.kind = JsonValue::Kind::Number; + out.text = in.substr(start, at - start); + return at > start + (in[start] == '-' ? 1 : 0); + } + bool whole(JsonValue& out) + { + if (!value(out)) + { + return false; + } + space(); + return at == in.size(); + } + }; + + // The value a path of keys and indexes reaches, or null. + const JsonValue* jsonAt(const JsonValue& root, const std::vector& path) + { + const JsonValue* at = &root; + for (LSLConstant* step : path) + { + if (step->getNodeSubType() == NODE_STRING_CONSTANT && at->kind == JsonValue::Kind::Object) + { + const char* key = static_cast(step)->getValue(); + const JsonValue* found = nullptr; + for (const auto& field : at->fields) + { + if (field.first == key) + { + found = &field.second; + } + } + if (!found) + { + return nullptr; + } + at = found; + } + else if (step->getNodeSubType() == NODE_INTEGER_CONSTANT && at->kind == JsonValue::Kind::Array) + { + const int index = static_cast(step)->getValue(); + if (index < 0 || static_cast(index) >= at->items.size()) + { + return nullptr; + } + at = &at->items[static_cast(index)]; + } + else + { + return nullptr; + } + } + return at; + } + + // A string that reads as a string to every JSON writer and reader: + // starts with a letter, holds letters, digits, spaces and plain + // punctuation, and is not a word JSON has a meaning for. + bool plainJsonString(const std::string& s) + { + if (s.empty() || !isalpha(static_cast(s[0]))) + { + return false; + } + for (const char ch : s) + { + if (!(isalnum(static_cast(ch)) || ch == ' ' || ch == '_' || ch == '-' || ch == '.' || ch == ',' || ch == ':' || ch == ';' || + ch == '!' || ch == '?' || ch == '\'' || ch == '(' || ch == ')' || ch == '#' || ch == '@' || ch == '%' || ch == '&' || ch == '*' || + ch == '+' || ch == '=' || ch == '/' || ch == '|' || ch == '~' || ch == '^' || ch == '$')) + { + return false; + } + } + return s != "true" && s != "false" && s != "null"; + } + + // Whether a value is one every writer spells alike: integers, plain + // strings, true, false and null, and arrays and objects of those. + bool jsonPlain(const JsonValue& v) + { + switch (v.kind) + { + case JsonValue::Kind::Number: return v.text != "-0" && v.text.size() <= 10; + case JsonValue::Kind::String: return plainJsonString(v.text); + case JsonValue::Kind::Array: + for (const JsonValue& item : v.items) + { + if (!jsonPlain(item)) return false; + } + return true; + case JsonValue::Kind::Object: + for (const auto& field : v.fields) + { + if (!plainJsonString(field.first) || !jsonPlain(field.second)) return false; + } + return true; + default: return true; + } + } + + // A plain value written the compact way, which is the simulator's. + std::string jsonWrite(const JsonValue& v) + { + switch (v.kind) + { + case JsonValue::Kind::Null: return "null"; + case JsonValue::Kind::True: return "true"; + case JsonValue::Kind::False: return "false"; + case JsonValue::Kind::Number: return v.text; + case JsonValue::Kind::String: return "\"" + v.text + "\""; + case JsonValue::Kind::Array: + { + std::string out = "["; + for (size_t i = 0; i < v.items.size(); ++i) + { + out += (i ? "," : "") + jsonWrite(v.items[i]); + } + return out + "]"; + } + case JsonValue::Kind::Object: + { + std::string out = "{"; + for (size_t i = 0; i < v.fields.size(); ++i) + { + out += (i ? "," : "") + ("\"" + v.fields[i].first + "\":") + jsonWrite(v.fields[i].second); + } + return out + "}"; + } + } + return std::string(); + } + + // Rotations worked out more than one way, where the VM's own way is not + // known: LLQuaternion's of today and of 2007, in singles a step at a + // time and in doubles. What every way comes to as singles, where they + // all come to the same; nothing where they part. + template struct Quat + { + T x, y, z, s; + }; + + std::optional> agreed(std::initializer_list>> ways) + { + std::optional> out; + for (const std::optional>& way : ways) + { + if (!way) + { + return std::nullopt; + } + const std::array q = { static_cast(way->x), static_cast(way->y), static_cast(way->z), static_cast(way->s) }; + for (float part : q) + { + if (!std::isfinite(part)) + { + return std::nullopt; + } + } + if (out && *out != q) + { + return std::nullopt; + } + out = q; + } + return out; + } + + template std::optional> widened(const std::optional>& q) + { + if (!q) + { + return std::nullopt; + } + return Quat{ double(q->x), double(q->y), double(q->z), double(q->s) }; + } + + // A quaternion made of unit length: today's, always; 2007's only where + // it is more than a part in a million away. + template Quat unitQuat(Quat q, bool nearlyLeft) + { + const T mag = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.s * q.s); + if (mag > T(0.0000001) && (!nearlyLeft || std::fabs(mag - T(1)) > T(0.000001))) + { + const T over = T(1) / mag; + q = { q.x * over, q.y * over, q.z * over, q.s * over }; + } + return q; + } + + // llRotBetween as LLQuaternion::shortestArc has it today: for vectors + // neither parallel nor opposed. + template std::optional> arcToday(const Vector3& u, const Vector3& v) + { + const T ax = u.x, ay = u.y, az = u.z, bx = v.x, by = v.y, bz = v.z; + const T ab = ax * bx + ay * by + az * bz; + const T cx = ay * bz - az * by, cy = az * bx - ax * bz, cz = ax * by - ay * bx; + const T cc = cx * cx + cy * cy + cz * cz; + if (!(ab * ab + cc > T(0)) || !(cc > T(0))) + { + return std::nullopt; + } + const T sum = std::sqrt(ab * ab + cc) + ab; + const T over = T(1) / std::sqrt(cc + sum * sum); + return Quat{ cx * over, cy * over, cz * over, sum * over }; + } + + // And as it had it in 2007: each vector made unit, then the angle from + // its cosine about the cross product made unit. + template std::optional> arc2007(const Vector3& u, const Vector3& v) + { + const auto unit = [](T& x, T& y, T& z) { + const T mag = std::sqrt(x * x + y * y + z * z); + if (mag > T(0.0000001)) + { + const T over = T(1) / mag; + x *= over; + y *= over; + z *= over; + } + return mag; + }; + T ax = u.x, ay = u.y, az = u.z, bx = v.x, by = v.y, bz = v.z; + if (unit(ax, ay, az) < T(0.00001) || unit(bx, by, bz) < T(0.00001)) + { + return std::nullopt; + } + T cx = ay * bz - az * by, cy = az * bx - ax * bz, cz = ax * by - ay * bx; + const T cosine = ax * bx + ay * by + az * bz; + if (cosine > T(1.0 - 0.00001) || cosine < T(-1.0 + 0.00001)) + { + return std::nullopt; + } + const T theta = static_cast(std::acos(double(cosine))); + unit(cx, cy, cz); + const T half = theta * T(0.5); + const T c = std::cos(half), sn = std::sin(half); + return unitQuat(Quat{ cx * sn, cy * sn, cz * sn, c }, true); + } + + // llAngleBetween: twice the arccosine of the rotations' dot product over + // their lengths; and, a way apart, twice the angle of the rotation from + // one to the other, by its arctangent. + template std::optional angleByCosine(const Quaternion& p, const Quaternion& q) + { + const T ax = p.x, ay = p.y, az = p.z, as = p.s, bx = q.x, by = q.y, bz = q.z, bs = q.s; + const T aa = ax * ax + ay * ay + az * az + as * as, bb = bx * bx + by * by + bz * bz + bs * bs; + const T aabb = std::sqrt(aa * bb); + if (!(aabb > T(0))) + { + return std::nullopt; + } + T ab = std::fabs((ax * bx + ay * by + az * bz + as * bs) / aabb); + if (ab > T(1)) + { + ab = T(1); + } + return double(T(2) * std::acos(ab)); + } + + std::optional angleByTangent(const Quaternion& p, const Quaternion& q) + { + const double ax = p.x, ay = p.y, az = p.z, as = p.s, bx = q.x, by = q.y, bz = q.z, bs = q.s; + const double w = as * bs + ax * bx + ay * by + az * bz; + const double vx = as * bx - bs * ax - (ay * bz - az * by); + const double vy = as * by - bs * ay - (az * bx - ax * bz); + const double vz = as * bz - bs * az - (ax * by - ay * bx); + const double v = std::sqrt(vx * vx + vy * vy + vz * vz); + if (!(v > 0.0 || w != 0.0)) + { + return std::nullopt; + } + return 2.0 * std::atan2(v, std::fabs(w)); + } + + // llAxes2Rot as LLMatrix3::quaternion has it where the trace is above + // nought, then made unit as today or as in 2007; and, a way apart, each + // part from its own square root, signed by the matrix. + template std::optional> axesByTrace(const Vector3& f, const Vector3& l, const Vector3& u, bool nearlyLeft) + { + const T m[3][3] = { { f.x, f.y, f.z }, { l.x, l.y, l.z }, { u.x, u.y, u.z } }; + const T tr = m[0][0] + m[1][1] + m[2][2]; + if (!(tr > T(0))) + { + return std::nullopt; + } + T root = std::sqrt(tr + T(1)); + const T w = root * T(0.5); + root = T(0.5) / root; + return unitQuat(Quat{ (m[1][2] - m[2][1]) * root, (m[2][0] - m[0][2]) * root, (m[0][1] - m[1][0]) * root, w }, nearlyLeft); + } + + std::optional> axesByParts(const Vector3& f, const Vector3& l, const Vector3& u) + { + const double m[3][3] = { { f.x, f.y, f.z }, { l.x, l.y, l.z }, { u.x, u.y, u.z } }; + const auto part = [](double square) { return 0.5 * std::sqrt(std::max(0.0, square)); }; + const double w = part(1.0 + m[0][0] + m[1][1] + m[2][2]); + if (!(w > 0.0)) + { + return std::nullopt; + } + return Quat{ std::copysign(part(1.0 + m[0][0] - m[1][1] - m[2][2]), m[1][2] - m[2][1]), + std::copysign(part(1.0 - m[0][0] + m[1][1] - m[2][2]), m[2][0] - m[0][2]), + std::copysign(part(1.0 - m[0][0] - m[1][1] + m[2][2]), m[0][1] - m[1][0]), w }; + } + + // A list's strings, where every element is one: what llParseString2List + // is given to split at. + std::optional> strings(LSLListConstant* list) + { + std::vector out; + for (LSLConstant* item : elements(list)) + { + if (item->getNodeSubType() != NODE_STRING_CONSTANT) + { + return std::nullopt; + } + out.emplace_back(static_cast(item)->getValue()); + } + return out; + } + + // llParseString2List's and llParseStringKeepNulls' splitting: at each + // place the separator or spacer found there, the separator dropped and + // the spacer kept; what is between, empty or not as `nulls` says. Only + // where which one is found cannot be in question -- at most eight of + // each, which is as many as the VM reads, none empty and none the start + // of another -- and where everything is ASCII. + std::optional> parsed(const std::string& src, const std::vector& separators, + const std::vector& spacers, bool nulls) + { + if (src.empty() || separators.size() > 8 || spacers.size() > 8 || !ascii(src.c_str())) + { + return std::nullopt; + } + std::vector> marks; // the mark, and whether it is kept + for (const std::string& sep : separators) + { + marks.emplace_back(sep, false); + } + for (const std::string& spacer : spacers) + { + marks.emplace_back(spacer, true); + } + for (const auto& [mark, kept] : marks) + { + if (mark.empty() || !ascii(mark.c_str())) + { + return std::nullopt; + } + for (const auto& [other, also] : marks) + { + if (&other != &mark && other.size() >= mark.size() && other.compare(0, mark.size(), mark) == 0) + { + return std::nullopt; + } + } + } + std::vector out; + const auto piece = [&](const std::string& text) { + if (nulls || !text.empty()) + { + out.push_back(text); + } + }; + size_t from = 0; + for (size_t at = 0; at < src.size();) + { + const auto found = std::find_if(marks.begin(), marks.end(), [&](const auto& m) { return src.compare(at, m.first.size(), m.first) == 0; }); + if (found == marks.end()) + { + ++at; + continue; + } + piece(src.substr(from, at - from)); + if (found->second) + { + out.push_back(found->first); + } + at += found->first.size(); + from = at; + } + piece(src.substr(from)); + return out; + } + + // Strings llListSort orders the same by their code points as by any + // culture's rules: letters and digits only, and the letters of all of + // them of one case -- "B" comes before "a" by code point, after it by a + // culture's. What letters a string has is told into `lower` and + // `upper`; false for one that has anything else. + bool plainSortKey(const char* s, bool& lower, bool& upper) + { + for (; *s; ++s) + { + const unsigned char ch = static_cast(*s); + if (ch >= 'a' && ch <= 'z') + { + lower = true; + } + else if (ch >= 'A' && ch <= 'Z') + { + upper = true; + } + else if (ch < '0' || ch > '9') + { + return false; + } + } + return !(lower && upper); + } + + // A vector's length, in singles a step at a time, as LLVector3 works it + // out, and as a double rounded once to a single: whether the two agree, + // and what they come to where they do. On Luau, whose answer the + // simulator gives by a way not known, a single or a double, only where + // the single is the double too. + bool magnitude(const Ctx& c, float x, float y, float z, double& out) + { + const double once = std::sqrt(double(x) * x + double(y) * y + double(z) * z); + const float xx = x * x, yy = y * y, zz = z * z; + const float sum = (xx + yy) + zz; + const float stepped = std::sqrt(sum); + out = stepped; + const bool agree = std::isfinite(stepped) && static_cast(static_cast(once)) == static_cast(stepped); + return agree && (c.target != ALLSLOptimizer::Target::Luau || static_cast(stepped) == once); + } + + // An ll function of floats, whose answer is a single on every target, + // folded only where two ways of working it out agree: in double and + // rounded once, as the VMs do it, and in single precision. This host's + // library is not the grid's -- glibc on 32-bit Linux, and .NET's under + // Mono -- and where the answer lies so near a single's rounding that two + // libraries could round it apart, the two ways here part too, and the VM + // is left to say. Its sign kept: -0 is not 0 to a single's text. + template + LSLConstant* agreedSingle(Ctx& c, Double inDouble, Single inSingle) + { + const float once = static_cast(inDouble()); + const float each = inSingle(); + if (!std::isfinite(once) || once != each || std::signbit(once) != std::signbit(each)) + { + return nullptr; + } + return c.single(once); + } + + // Whether an argument is exactly a single: what sin, cos and tan take on + // Luau, whose SLua takes its double as it is where the others round it. + bool exactSingle(const Args& a, size_t i) + { + if (i >= a.size()) + { + return false; + } + if (a[i]->getNodeSubType() == NODE_FLOAT_CONSTANT) + { + const double v = static_cast(a[i])->getValue(); + return static_cast(static_cast(v)) == v; + } + if (a[i]->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + const int v = static_cast(a[i])->getValue(); + return static_cast(static_cast(v)) == v; + } + return false; + } + + typedef std::function Evaluator; + + const boost::unordered_flat_map>& evaluators() + { + static const boost::unordered_flat_map> table = { + { "llAbs", + [](Ctx& c, const Args& a) -> LSLConstant* { + int v; + if (!argInt(a, 0, v)) return nullptr; + return c.integer(v < 0 ? static_cast(0u - static_cast(v)) : v); + } }, + { "llFabs", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + return argFloat(a, 0, v) ? c.single(static_cast(std::fabs(v))) : nullptr; + } }, + { "llFloor", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v)) return nullptr; + const double r = std::floor(v); + return (r >= -2147483648.0 && r < 2147483648.0) ? c.integer(static_cast(r)) : nullptr; + } }, + { "llCeil", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v)) return nullptr; + const double r = std::ceil(v); + return (r >= -2147483648.0 && r < 2147483648.0) ? c.integer(static_cast(r)) : nullptr; + } }, + // The functions of floats, each where its two ways agree + // (agreedSingle), and with what SLua's own make of their edges, + // which say they are Mono's: a pow or an atan2 of -0 is 0. + { "llSqrt", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || v < 0.0) return nullptr; + return agreedSingle(c, [v] { return std::sqrt(v); }, [v] { return std::sqrt(static_cast(v)); }); + } }, + { "llPow", + [](Ctx& c, const Args& a) -> LSLConstant* { + double b, e; + if (!argFloat(a, 0, b) || !argFloat(a, 1, e) || !std::isfinite(b) || !std::isfinite(e)) return nullptr; + const auto zero = [](auto r) { return r == 0 ? decltype(r)(0) : r; }; + return agreedSingle(c, [&] { return zero(std::pow(b, e)); }, [&] { return zero(std::pow(static_cast(b), static_cast(e))); }); + } }, + { "llSin", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || (c.target == ALLSLOptimizer::Target::Luau && !exactSingle(a, 0))) return nullptr; + return agreedSingle(c, [v] { return std::sin(v); }, [v] { return std::sin(static_cast(v)); }); + } }, + { "llCos", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || (c.target == ALLSLOptimizer::Target::Luau && !exactSingle(a, 0))) return nullptr; + return agreedSingle(c, [v] { return std::cos(v); }, [v] { return std::cos(static_cast(v)); }); + } }, + { "llTan", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || (c.target == ALLSLOptimizer::Target::Luau && !exactSingle(a, 0))) return nullptr; + return agreedSingle(c, [v] { return std::tan(v); }, [v] { return std::tan(static_cast(v)); }); + } }, + { "llAtan2", + [](Ctx& c, const Args& a) -> LSLConstant* { + double y, x; + if (!argFloat(a, 0, y) || !argFloat(a, 1, x) || !std::isfinite(y) || !std::isfinite(x)) return nullptr; + const auto zero = [](auto r) { return r == 0 ? decltype(r)(0) : r; }; + return agreedSingle(c, [&] { return zero(std::atan2(y, x)); }, + [&] { return zero(std::atan2(static_cast(y), static_cast(x))); }); + } }, + { "llLog", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || v <= 0.0) return nullptr; + return agreedSingle(c, [v] { return std::log(v); }, [v] { return std::log(static_cast(v)); }); + } }, + { "llLog10", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || v <= 0.0) return nullptr; + return agreedSingle(c, [v] { return std::log10(v); }, [v] { return std::log10(static_cast(v)); }); + } }, + { "llModPow", + [](Ctx& c, const Args& a) -> LSLConstant* { + int base, exponent, modulus; + if (!argInt(a, 0, base) || !argInt(a, 1, exponent) || !argInt(a, 2, modulus) || base < 0 || exponent < 0 || modulus <= 0) + { + return nullptr; + } + uint64_t result = 1 % static_cast(modulus); + uint64_t b = static_cast(base) % static_cast(modulus); + for (uint32_t e = static_cast(exponent); e; e >>= 1) + { + if (e & 1) + { + result = result * b % static_cast(modulus); + } + b = b * b % static_cast(modulus); + } + return c.integer(static_cast(result)); + } }, + // A vector's length as LLVector3 has it, in singles a step at a + // time, and as a double rounded once: which the VM does is not + // known, so folded only where the two agree (magnitude). And a + // vector normalised by dividing each part by the length, and by + // the product with 1 / length that LLVector3 makes of a division, + // likewise. + { "llVecMag", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 v; + double mag = 0.0; + return argVector(a, 0, v) && magnitude(c, v.x, v.y, v.z, mag) ? c.number(mag) : nullptr; + } }, + { "llVecDist", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 p, q; + double mag = 0.0; + if (!argVector(a, 0, p) || !argVector(a, 1, q)) return nullptr; + const float dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z; + return magnitude(c, dx, dy, dz, mag) ? c.number(mag) : nullptr; + } }, + { "llVecNorm", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 v; + double length = 0.0; + if (!argVector(a, 0, v) || !magnitude(c, v.x, v.y, v.z, length)) return nullptr; + const float mag = static_cast(length); + if (mag == 0.0f) return c.vector(0, 0, 0); + const float x = v.x / mag, y = v.y / mag, z = v.z / mag; + const float over = 1.0f / mag; + if (x != v.x * over || y != v.y * over || z != v.z * over) return nullptr; + return c.vector(x, y, z); + } }, + // The rotations, by the viewer's own quaternion, which is the + // lineage of the simulator's: Euler angles through the + // matrix, axis and angle, and the axes of a rotation as the + // unit vectors turned by it. Only unit rotations are folded. + { "llEuler2Rot", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 v; + if (!argVector(a, 0, v)) return nullptr; + LLQuaternion q; + q.setEulerAngles(v.x, v.y, v.z); + return c.rotation(q.mQ[VX], q.mQ[VY], q.mQ[VZ], q.mQ[VW]); + } }, + { "llRot2Euler", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + F32 roll, pitch, yaw; + q.getEulerAngles(&roll, &pitch, &yaw); + return c.vector(roll, pitch, yaw); + } }, + { "llAxisAngle2Rot", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 axis; + double angle; + if (!argVector(a, 0, axis) || !argFloat(a, 1, angle)) return nullptr; + const LLQuaternion q(static_cast(angle), LLVector3(axis.x, axis.y, axis.z)); + return c.rotation(q.mQ[VX], q.mQ[VY], q.mQ[VZ], q.mQ[VW]); + } }, + { "llRot2Axis", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + F32 angle; + LLVector3 axis; + q.getAngleAxis(&angle, axis); + // No rotation has no axis to speak of: the VM's to say. + if (angle == 0.0f) return nullptr; + return c.vector(axis.mV[VX], axis.mV[VY], axis.mV[VZ]); + } }, + { "llRot2Angle", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + F32 angle; + LLVector3 axis; + q.getAngleAxis(&angle, axis); + return c.number(angle); + } }, + { "llRot2Fwd", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + const LLVector3 v = LLVector3(1.f, 0.f, 0.f) * q; + return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); + } }, + // Three more by more than one way each (agreed): left where the + // ways part, which an answer near a parallel, a half turn or + // nothing does. + { "llRotBetween", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 u, v; + if (!argVector(a, 0, u) || !argVector(a, 1, v)) return nullptr; + const auto q = agreed({ widened(arcToday(u, v)), arcToday(u, v), widened(arc2007(u, v)), arc2007(u, v) }); + return q ? c.rotation((*q)[0], (*q)[1], (*q)[2], (*q)[3]) : nullptr; + } }, + { "llAngleBetween", + [](Ctx& c, const Args& a) -> LSLConstant* { + // Luau's answer is a double, by a way not known. + if (c.target == ALLSLOptimizer::Target::Luau || a.size() != 2 || a[0]->getNodeSubType() != NODE_QUATERNION_CONSTANT || + a[1]->getNodeSubType() != NODE_QUATERNION_CONSTANT) + { + return nullptr; + } + const Quaternion& p = *static_cast(a[0])->getValue(); + const Quaternion& q = *static_cast(a[1])->getValue(); + std::optional out; + for (const std::optional& way : { angleByCosine(p, q), angleByCosine(p, q), angleByTangent(p, q) }) + { + if (!way || !std::isfinite(static_cast(*way)) || (out && *out != static_cast(*way))) return nullptr; + out = static_cast(*way); + } + return c.number(*out); + } }, + { "llAxes2Rot", + [](Ctx& c, const Args& a) -> LSLConstant* { + Vector3 f, l, u; + if (!argVector(a, 0, f) || !argVector(a, 1, l) || !argVector(a, 2, u)) return nullptr; + const auto q = agreed({ widened(axesByTrace(f, l, u, false)), widened(axesByTrace(f, l, u, true)), + axesByTrace(f, l, u, false), axesByParts(f, l, u) }); + return q ? c.rotation((*q)[0], (*q)[1], (*q)[2], (*q)[3]) : nullptr; + } }, + { "llRot2Left", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + const LLVector3 v = LLVector3(0.f, 1.f, 0.f) * q; + return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); + } }, + { "llRot2Up", + [](Ctx& c, const Args& a) -> LSLConstant* { + LLQuaternion q; + if (!argUnitRotation(a, 0, q)) return nullptr; + const LLVector3 v = LLVector3(0.f, 0.f, 1.f) * q; + return c.vector(v.mV[VX], v.mV[VY], v.mV[VZ]); + } }, + { "llStringLength", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + return argString(a, 0, s) ? c.integer(static_cast(s.size())) : nullptr; + } }, + { "llGetSubString", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + int start, end; + if (!argString(a, 0, s) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; + return c.string(fromChars(subRange(chars(s), start, end))); + } }, + { "llDeleteSubString", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + int start, end; + if (!argString(a, 0, s) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; + return c.string(fromChars(deleteRange(chars(s), start, end))); + } }, + { "llInsertString", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s, ins; + int at; + if (!argString(a, 0, s) || !argInt(a, 1, at) || !argString(a, 2, ins) || at < 0) return nullptr; + const size_t pos = std::min(s.size(), static_cast(at)); + return c.string(s.substr(0, pos) + ins + s.substr(pos)); + } }, + { "llSubStringIndex", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s, p; + if (!argString(a, 0, s) || !argString(a, 1, p)) return nullptr; + const size_t at = s.find(p); + return c.integer(at == std::string::npos ? -1 : static_cast(at)); + } }, + { "llToUpper", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + if (!argString(a, 0, s)) return nullptr; + for (char& ch : s) ch = static_cast(toupper(static_cast(ch))); + return c.string(s); + } }, + { "llToLower", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + if (!argString(a, 0, s)) return nullptr; + for (char& ch : s) ch = static_cast(tolower(static_cast(ch))); + return c.string(s); + } }, + { "llStringTrim", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + int how; + if (!argString(a, 0, s) || !argInt(a, 1, how)) return nullptr; + const auto blank = [](char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; }; + size_t from = 0; + size_t to = s.size(); + if (how & 1) + { + while (from < to && blank(s[from])) ++from; + } + if (how & 2) + { + while (to > from && blank(s[to - 1])) --to; + } + return c.string(s.substr(from, to - from)); + } }, + { "llEscapeURL", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + if (!argString(a, 0, s)) return nullptr; + std::string out; + for (char ch : s) + { + if (isalnum(static_cast(ch))) + { + out += ch; + } + else + { + char buffer[8]; + snprintf(buffer, sizeof(buffer), "%%%02X", static_cast(ch)); + out += buffer; + } + } + return c.string(out); + } }, + { "llUnescapeURL", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + if (!argString(a, 0, s)) return nullptr; + std::string out; + for (size_t i = 0; i < s.size(); ++i) + { + if (s[i] != '%') + { + out += s[i]; + continue; + } + if (i + 2 >= s.size() || !isxdigit(static_cast(s[i + 1])) || !isxdigit(static_cast(s[i + 2]))) + { + return nullptr; + } + const int byte = static_cast(strtol(s.substr(i + 1, 2).c_str(), nullptr, 16)); + if (byte == 0 || byte >= 0x80) + { + return nullptr; + } + out += static_cast(byte); + i += 2; + } + return c.string(out); + } }, + { "llStringToBase64", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + return argString(a, 0, s) ? c.string(toBase64(s)) : nullptr; + } }, + { "llBase64ToString", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s, out; + if (!argString(a, 0, s) || !fromBase64(s, out) || out.find('\0') != std::string::npos) return nullptr; + return c.string(out); + } }, + { "llIntegerToBase64", + [](Ctx& c, const Args& a) -> LSLConstant* { + int v; + if (!argInt(a, 0, v)) return nullptr; + const uint32_t u = static_cast(v); + std::string bytes{ static_cast(u >> 24), static_cast(u >> 16), static_cast(u >> 8), static_cast(u) }; + return c.string(toBase64(bytes)); + } }, + { "llBase64ToInteger", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s, out; + if (!argString(a, 0, s) || s.size() != 8 || !fromBase64(s, out) || out.size() != 4) return nullptr; + const uint32_t u = (static_cast(out[0]) << 24) | (static_cast(out[1]) << 16) | + (static_cast(out[2]) << 8) | static_cast(out[3]); + return c.integer(static_cast(u)); + } }, + { "llChar", + [](Ctx& c, const Args& a) -> LSLConstant* { + int v; + if (!argInt(a, 0, v)) return nullptr; + if (v == 0) return c.string(std::string()); + return (v > 0 && v < 128) ? c.string(std::string(1, static_cast(v))) : nullptr; + } }, + { "llOrd", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s; + int at; + if (!argString(a, 0, s) || !argInt(a, 1, at)) return nullptr; + const int length = static_cast(s.size()); + if (at < 0) at += length; + return (at >= 0 && at < length) ? c.integer(static_cast(s[at])) : c.integer(0); + } }, + { "llReplaceSubString", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string s, pattern, with; + int count; + if (!argString(a, 0, s) || !argString(a, 1, pattern) || !argString(a, 2, with) || !argInt(a, 3, count) || pattern.empty() || count < 0) + { + return nullptr; + } + std::string out; + size_t from = 0; + int done = 0; + while (true) + { + const size_t at = s.find(pattern, from); + if (at == std::string::npos || (count > 0 && done == count)) + { + out += s.substr(from); + break; + } + out += s.substr(from, at - from) + with; + from = at + pattern.size(); + ++done; + } + return c.string(out); + } }, + { "llGetListLength", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + return argList(a, 0, l) ? c.integer(static_cast(l->getLength())) : nullptr; + } }, + { "llList2List", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + int start, end; + if (!argList(a, 0, l) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; + return listOf(c, subRange(elements(l), start, end)); + } }, + // A list split, as LSL splits it, where nothing could make the + // two VMs split it otherwise (parsed): no spacers for the one + // that keeps what is empty, whose empties beside a spacer are + // not known. + { "llParseString2List", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string src; + LSLListConstant *seps, *spacers; + if (!argString(a, 0, src) || !argList(a, 1, seps) || !argList(a, 2, spacers)) return nullptr; + const auto sepText = strings(seps), spacerText = strings(spacers); + if (!sepText || !spacerText) return nullptr; + const auto pieces = parsed(src, *sepText, *spacerText, false); + if (!pieces) return nullptr; + std::vector out; + for (const std::string& piece : *pieces) + { + LSLConstant* item = c.string(piece); + if (!item) return nullptr; + out.push_back(item); + } + return listOf(c, out); + } }, + { "llParseStringKeepNulls", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string src; + LSLListConstant *seps, *spacers; + if (!argString(a, 0, src) || !argList(a, 1, seps) || !argList(a, 2, spacers) || spacers->getLength() != 0) return nullptr; + const auto sepText = strings(seps); + if (!sepText) return nullptr; + const auto pieces = parsed(src, *sepText, {}, true); + if (!pieces) return nullptr; + std::vector out; + for (const std::string& piece : *pieces) + { + LSLConstant* item = c.string(piece); + if (!item) return nullptr; + out.push_back(item); + } + return listOf(c, out); + } }, + // Comma-separated values as strings: only where no value is + // empty, starts or ends with a space, or holds the angle + // brackets that keep a vector's commas in -- what the two VMs + // are not known to treat alike. + { "llCSV2List", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string src; + if (!argString(a, 0, src) || src.empty() || src.find_first_of("<>") != std::string::npos) return nullptr; + const auto pieces = parsed(src, { "," }, {}, true); + if (!pieces) return nullptr; + std::vector out; + for (const std::string& piece : *pieces) + { + if (piece.empty() || std::isspace(static_cast(piece.front())) || + std::isspace(static_cast(piece.back()))) + { + return nullptr; + } + LSLConstant* item = c.string(piece); + if (!item) return nullptr; + out.push_back(item); + } + return listOf(c, out); + } }, + // A list sorted by every stride-th element, ascending or not: + // only where the order cannot be the sort's to choose -- the + // keys all of one type, integers, floats or strings a culture + // orders as their code points do (plainSortKey), and no two the + // same, so that a sort that is not stable sorts them alike -- + // and the stride divides the list. + { "llListSort", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + int stride, ascending; + if (!argList(a, 0, l) || !argInt(a, 1, stride) || !argInt(a, 2, ascending) || (ascending != 0 && ascending != 1)) return nullptr; + const std::vector items = elements(l); + if (stride < 1 || items.size() % static_cast(stride) != 0 || items.empty()) return nullptr; + const LSLNodeSubType kind = items.front()->getNodeSubType(); + if (kind != NODE_INTEGER_CONSTANT && kind != NODE_FLOAT_CONSTANT && kind != NODE_STRING_CONSTANT) return nullptr; + std::vector groups; + bool lower = false, upper = false; + for (size_t g = 0; g < items.size(); g += static_cast(stride)) + { + LSLConstant* key = items[g]; + if (key->getNodeSubType() != kind) return nullptr; + if (kind == NODE_FLOAT_CONSTANT && !std::isfinite(static_cast(key)->getValue())) return nullptr; + if (kind == NODE_STRING_CONSTANT && !plainSortKey(static_cast(key)->getValue(), lower, upper)) return nullptr; + groups.push_back(g); + } + const auto less = [&](size_t x, size_t y) { + LSLConstant* p = items[x]; + LSLConstant* q = items[y]; + switch (kind) + { + case NODE_INTEGER_CONSTANT: + return static_cast(p)->getValue() < static_cast(q)->getValue(); + case NODE_FLOAT_CONSTANT: + return static_cast(p)->getValue() < static_cast(q)->getValue(); + default: + return strcmp(static_cast(p)->getValue(), static_cast(q)->getValue()) < 0; + } + }; + std::sort(groups.begin(), groups.end(), [&](size_t x, size_t y) { return ascending ? less(x, y) : less(y, x); }); + for (size_t i = 1; i < groups.size(); ++i) + { + if (!less(groups[i - 1], groups[i]) && !less(groups[i], groups[i - 1])) return nullptr; + } + std::vector out; + for (size_t g : groups) + { + out.insert(out.end(), items.begin() + static_cast(g), items.begin() + static_cast(g) + stride); + } + return listOf(c, out); + } }, + { "llDeleteSubList", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + int start, end; + if (!argList(a, 0, l) || !argInt(a, 1, start) || !argInt(a, 2, end)) return nullptr; + return listOf(c, deleteRange(elements(l), start, end)); + } }, + { "llListInsertList", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant *l, *ins; + int at; + if (!argList(a, 0, l) || !argList(a, 1, ins) || !argInt(a, 2, at)) return nullptr; + std::vector items = elements(l); + const int length = static_cast(items.size()); + if (at < 0) at += length; + if (at < 0) return nullptr; + const size_t pos = std::min(items.size(), static_cast(at)); + std::vector more = elements(ins); + items.insert(items.begin() + pos, more.begin(), more.end()); + return listOf(c, items); + } }, + { "llListReplaceList", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant *l, *with; + int start, end; + if (!argList(a, 0, l) || !argList(a, 1, with) || !argInt(a, 2, start) || !argInt(a, 3, end)) return nullptr; + std::vector items = elements(l); + const int length = static_cast(items.size()); + normalise(length, start, end); + if (start > end || start < 0 || start > length) return nullptr; + std::vector out(items.begin(), items.begin() + std::min(length, start)); + std::vector more = elements(with); + out.insert(out.end(), more.begin(), more.end()); + if (end + 1 < length) + { + out.insert(out.end(), items.begin() + end + 1, items.end()); + } + return listOf(c, out); + } }, + { "llGetListEntryType", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + int at; + if (!argList(a, 0, l) || !argInt(a, 1, at)) return nullptr; + std::vector items = elements(l); + const int length = static_cast(items.size()); + if (at < 0) at += length; + if (at < 0 || at >= length) return c.integer(0); + switch (items[at]->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: return c.integer(1); + case NODE_FLOAT_CONSTANT: return c.integer(2); + case NODE_STRING_CONSTANT: return c.integer(3); + case NODE_KEY_CONSTANT: return c.integer(4); + case NODE_VECTOR_CONSTANT: return c.integer(5); + case NODE_QUATERNION_CONSTANT: return c.integer(6); + default: return nullptr; + } + } }, + { "llJsonGetValue", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string json; + LSLListConstant* path; + if (!argString(a, 0, json) || !argList(a, 1, path)) return nullptr; + JsonValue root; + JsonReader reader(json); + if (!reader.whole(root)) return nullptr; + const JsonValue* at = jsonAt(root, elements(path)); + if (!at) return c.builtin("JSON_INVALID"); + // A string or a plain number: what is written is what + // is answered; true, false and null are their constants. + // A nested object or array the simulator spells its own + // way. + switch (at->kind) + { + case JsonValue::Kind::String: + case JsonValue::Kind::Number: return c.string(at->text); + case JsonValue::Kind::True: return c.builtin("JSON_TRUE"); + case JsonValue::Kind::False: return c.builtin("JSON_FALSE"); + case JsonValue::Kind::Null: return c.builtin("JSON_NULL"); + default: + // A nested value as the simulator writes it, where + // every writer would write it alike. + return jsonPlain(*at) ? c.string(jsonWrite(*at)) : nullptr; + } + } }, + { "llJsonSetValue", + [](Ctx& c, const Args& a) -> LSLConstant* { + // Over a plain document, down a path that is there but + // for its last step, which may be a new key or the next + // index; the value a plain string, an integer, or true, + // false or null by its constant. Anything the simulator + // might make more of -- a path to create, a deletion, a + // value that reads as JSON -- is left to it. + std::string json; + LSLListConstant* path; + if (!argString(a, 0, json) || !argList(a, 1, path) || a.size() < 3 || a[2]->getNodeSubType() != NODE_STRING_CONSTANT) return nullptr; + JsonValue root; + JsonReader reader(json); + if (!reader.whole(root) || !jsonPlain(root)) return nullptr; + const std::string value = static_cast(a[2])->getValue(); + JsonValue put; + auto same = [&](const char* name) { + LSLConstant* builtin = c.builtin(name); + return builtin && value == static_cast(builtin)->getValue(); + }; + // JSON_DELETE takes out what is there: a key from an + // object, an index from an array; what is not there the + // simulator answers for. + const bool deleting = same("JSON_DELETE"); + if (same("JSON_TRUE")) put.kind = JsonValue::Kind::True; + else if (same("JSON_FALSE")) put.kind = JsonValue::Kind::False; + else if (same("JSON_NULL")) put.kind = JsonValue::Kind::Null; + else if (deleting) + { + } + else if (plainJsonString(value)) + { + put.kind = JsonValue::Kind::String; + put.text = value; + } + else + { + // An integer, as JSON writes one. + size_t i = value.size() > 1 && value[0] == '-' ? 1 : 0; + if (i >= value.size() || value.size() > 10 || (value[i] == '0' && value.size() > i + 1)) return nullptr; + for (; i < value.size(); ++i) + { + if (!isdigit(static_cast(value[i]))) return nullptr; + } + put.kind = JsonValue::Kind::Number; + put.text = value; + } + const std::vector steps = elements(path); + if (steps.empty()) return nullptr; + JsonValue* at = &root; + for (size_t i = 0; i + 1 < steps.size(); ++i) + { + const std::vector one(steps.begin() + static_cast(i), steps.begin() + static_cast(i) + 1); + const JsonValue* next = jsonAt(*at, one); + if (!next) return nullptr; + at = const_cast(next); + } + LSLConstant* last = steps.back(); + if (last->getNodeSubType() == NODE_STRING_CONSTANT && at->kind == JsonValue::Kind::Object) + { + std::string key; + if (!argString({ last }, 0, key) || !plainJsonString(key)) return nullptr; + bool found = false; + for (size_t i = 0; i < at->fields.size(); ++i) + { + if (at->fields[i].first == key) + { + found = true; + if (deleting) at->fields.erase(at->fields.begin() + static_cast(i)); + else at->fields[i].second = put; + break; + } + } + if (!found && deleting) return nullptr; + if (!found) at->fields.emplace_back(key, put); + } + else if (last->getNodeSubType() == NODE_INTEGER_CONSTANT && at->kind == JsonValue::Kind::Array) + { + const int index = static_cast(last)->getValue(); + if (deleting) + { + if (index < 0 || static_cast(index) >= at->items.size()) return nullptr; + at->items.erase(at->items.begin() + index); + } + else if (index == -1 || static_cast(index) == at->items.size()) at->items.push_back(put); + else if (index >= 0 && static_cast(index) < at->items.size()) at->items[static_cast(index)] = put; + else if (index > 0) return c.builtin("JSON_INVALID"); + else return nullptr; + } + else + { + return nullptr; + } + return c.string(jsonWrite(root)); + } }, + { "llJsonValueType", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string json; + LSLListConstant* path; + if (!argString(a, 0, json) || !argList(a, 1, path)) return nullptr; + JsonValue root; + JsonReader reader(json); + if (!reader.whole(root)) return nullptr; + const JsonValue* at = jsonAt(root, elements(path)); + if (!at) return c.builtin("JSON_INVALID"); + switch (at->kind) + { + case JsonValue::Kind::Object: return c.builtin("JSON_OBJECT"); + case JsonValue::Kind::Array: return c.builtin("JSON_ARRAY"); + case JsonValue::Kind::String: return c.builtin("JSON_STRING"); + case JsonValue::Kind::Number: return c.builtin("JSON_NUMBER"); + case JsonValue::Kind::True: return c.builtin("JSON_TRUE"); + case JsonValue::Kind::False: return c.builtin("JSON_FALSE"); + case JsonValue::Kind::Null: return c.builtin("JSON_NULL"); + } + return nullptr; + } }, + { "llList2Json", + [](Ctx& c, const Args& a) -> LSLConstant* { + // Only what every writer spells alike: integers and + // plain strings -- letters, digits after the first, + // spaces and the punctuation that needs no escape -- + // that read as nothing else. Floats, JSON values as + // strings, and anything wanting an escape are left. + LSLListConstant* items; + if (a.empty() || a[0]->getNodeSubType() != NODE_STRING_CONSTANT || !argList(a, 1, items)) return nullptr; + // The kind is a JSON_* value, which is no ASCII string. + const std::string kind = static_cast(a[0])->getValue(); + LSLConstant* array = c.builtin("JSON_ARRAY"); + LSLConstant* object = c.builtin("JSON_OBJECT"); + const bool is_array = array && kind == static_cast(array)->getValue(); + const bool is_object = object && kind == static_cast(object)->getValue(); + if (!is_array && !is_object) return nullptr; + const std::vector list = elements(items); + if (is_object && list.size() % 2 != 0) return nullptr; + std::string out = is_array ? "[" : "{"; + for (size_t i = 0; i < list.size(); ++i) + { + // A comma before each item, or before each pair. + if (i > 0 && (is_array || i % 2 == 0)) out += ","; + LSLConstant* item = list[i]; + if (is_object && i % 2 == 0) + { + std::string key; + if (!argString({ item }, 0, key) || !plainJsonString(key)) return nullptr; + out += "\"" + key + "\":"; + continue; + } + if (item->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + out += std::to_string(static_cast(item)->getValue()); + continue; + } + std::string text; + if (!argString({ item }, 0, text) || !plainJsonString(text)) return nullptr; + out += "\"" + text + "\""; + } + out += is_array ? "]" : "}"; + return c.string(out); + } }, + { "llJson2List", + [](Ctx& c, const Args& a) -> LSLConstant* { + // An array's items or an object's keys and values, where + // every one is an integer or a plain string; anything + // else the simulator types its own way. + std::string json; + if (!argString(a, 0, json)) return nullptr; + JsonValue root; + JsonReader reader(json); + if (!reader.whole(root)) return nullptr; + std::vector values; + std::vector out; + auto push = [&](const JsonValue& v) { + if (v.kind == JsonValue::Kind::Number) + { + // Within 32 bits: past them, what the simulator + // makes of one is its 32-bit host's business, and + // a long is 64 bits here, and 32 on Windows. + const long long n = v.text.size() <= 11 ? std::strtoll(v.text.c_str(), nullptr, 10) : INT64_MAX; + if (n < INT32_MIN || n > INT32_MAX) return false; + out.push_back(c.integer(static_cast(n))); + return true; + } + if (v.kind == JsonValue::Kind::String && plainJsonString(v.text)) + { + out.push_back(c.string(v.text)); + return true; + } + return false; + }; + if (root.kind == JsonValue::Kind::Array) + { + for (const JsonValue& v : root.items) + { + if (!push(v)) return nullptr; + } + } + else if (root.kind == JsonValue::Kind::Object) + { + for (const auto& field : root.fields) + { + if (!plainJsonString(field.first)) return nullptr; + out.push_back(c.string(field.first)); + if (!push(field.second)) return nullptr; + } + } + else + { + return nullptr; + } + return listOf(c, out); + } }, + { "llAcos", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || std::fabs(v) > 1.0) return nullptr; + return agreedSingle(c, [v] { return std::acos(v); }, [v] { return std::acos(static_cast(v)); }); + } }, + { "llAsin", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v) || std::fabs(v) > 1.0) return nullptr; + return agreedSingle(c, [v] { return std::asin(v); }, [v] { return std::asin(static_cast(v)); }); + } }, + { "llRound", + [](Ctx& c, const Args& a) -> LSLConstant* { + double v; + if (!argFloat(a, 0, v)) return nullptr; + // LSO adds the half in single precision, Mono in double: + // folded only where the two come to the same. + double rounded = std::floor(v + 0.5); + if (c.target != ALLSLOptimizer::Target::Luau) + { + const float f = static_cast(v); + rounded = std::floor(static_cast(f) + 0.5); + if (static_cast(std::floor(f + 0.5f)) != rounded) return nullptr; + } + return std::fabs(rounded) < 2147483647.0 ? c.integer(static_cast(rounded)) : nullptr; + } }, + { "llDumpList2String", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + std::string between; + if (!argList(a, 0, l) || !argString(a, 1, between) || !ascii(between.c_str())) return nullptr; + std::string out; + bool first = true; + for (LSLConstant* item : elements(l)) + { + std::string text; + if (!elementText(item, text)) return nullptr; + out += (first ? "" : between) + text; + first = false; + } + return c.string(out); + } }, + { "llListFindList", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* in; + LSLListConstant* sought; + // An empty list to find is found where LSO and Mono + // disagree: not folded. + if (!argList(a, 0, in) || !argList(a, 1, sought)) return nullptr; + const std::vector hay = elements(in); + const std::vector find = elements(sought); + if (find.empty()) return nullptr; + // The same type and the same value. + const auto same = [](LSLConstant* x, LSLConstant* y) { + if (x->getNodeSubType() != y->getNodeSubType()) return false; + switch (x->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + return static_cast(x)->getValue() == static_cast(y)->getValue(); + case NODE_FLOAT_CONSTANT: + return static_cast(x)->getValue() == static_cast(y)->getValue(); + case NODE_STRING_CONSTANT: + case NODE_KEY_CONSTANT: + return !strcmp(static_cast(x)->getValue(), static_cast(y)->getValue()); + case NODE_VECTOR_CONSTANT: + { + const Vector3* p = static_cast(x)->getValue(); + const Vector3* q = static_cast(y)->getValue(); + return p->x == q->x && p->y == q->y && p->z == q->z; + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* p = static_cast(x)->getValue(); + const Quaternion* q = static_cast(y)->getValue(); + return p->x == q->x && p->y == q->y && p->z == q->z && p->s == q->s; + } + default: + return false; + } + }; + for (size_t i = 0; i + find.size() <= hay.size(); ++i) + { + bool all = true; + for (size_t j = 0; j < find.size() && all; ++j) + { + all = same(hay[i + j], find[j]); + } + if (all) return c.integer(static_cast(i)); + } + return c.integer(-1); + } }, + { "llMD5String", + [](Ctx& c, const Args& a) -> LSLConstant* { + std::string text; + int nonce; + if (!argString(a, 0, text) || !argInt(a, 1, nonce)) return nullptr; + LLMD5 md5; + md5.update(text + ":" + std::to_string(nonce)); + md5.finalize(); + char hex[33]; + md5.hex_digest(hex); + return c.string(hex); + } }, + { "llList2CSV", + [](Ctx& c, const Args& a) -> LSLConstant* { + LSLListConstant* l; + if (!argList(a, 0, l)) return nullptr; + std::string out; + bool first = true; + for (LSLConstant* item : elements(l)) + { + std::string text; + if (!elementText(item, text)) return nullptr; + out += (first ? "" : ", ") + text; + first = false; + } + return c.string(out); + } }, + }; + return table; + } + + // llList2Integer and its kin: the element where it is the type asked + // for, the type's nothing where the index is off the end, and the + // VM's own answer otherwise. + LSLConstant* listGet(Ctx& c, const Args& a, LSLNodeSubType wanted) + { + LSLListConstant* l; + int at; + if (!argList(a, 0, l) || !argInt(a, 1, at)) + { + return nullptr; + } + std::vector items = elements(l); + const int length = static_cast(items.size()); + if (at < 0) + { + at += length; + } + if (at < 0 || at >= length) + { + switch (wanted) + { + case NODE_INTEGER_CONSTANT: return c.integer(0); + case NODE_FLOAT_CONSTANT: return c.number(0.0); + case NODE_STRING_CONSTANT: return c.string(std::string()); + // What LSO gives for a key out of range is NULL_KEY, which + // the call is folded to by name; Mono gives an empty key. + case NODE_KEY_CONSTANT: return c.target == ALLSLOptimizer::Target::LSO ? c.builtin("NULL_KEY") : c.key(std::string()); + case NODE_VECTOR_CONSTANT: return c.vector(0, 0, 0); + case NODE_QUATERNION_CONSTANT: return c.allocator->newTracked(0.f, 0.f, 0.f, 1.f); + default: return nullptr; + } + } + LSLConstant* item = items[at]; + if (wanted == NODE_STRING_CONSTANT) + { + std::string text; + return elementText(item, text) ? c.string(text) : nullptr; + } + if (wanted == NODE_KEY_CONSTANT && item->getNodeSubType() == NODE_STRING_CONSTANT) + { + return c.key(static_cast(item)->getValue()); + } + return item->getNodeSubType() == wanted ? item->copy(c.allocator) : nullptr; + } +} + + LSLConstant* evaluate(Ctx& ctx, const char* name, const Args& args) + { + // The list getters are in the table with the rest; nothing is + // compared before it is looked up. + static const boost::unordered_flat_map> GETTERS = { + { "llList2Integer", NODE_INTEGER_CONSTANT }, { "llList2Float", NODE_FLOAT_CONSTANT }, + { "llList2String", NODE_STRING_CONSTANT }, { "llList2Key", NODE_KEY_CONSTANT }, + { "llList2Vector", NODE_VECTOR_CONSTANT }, { "llList2Rot", NODE_QUATERNION_CONSTANT }, + }; + if (const auto getter = GETTERS.find(name); getter != GETTERS.end()) + { + return listGet(ctx, args, getter->second); + } + auto it = evaluators().find(name); + return it == evaluators().end() ? nullptr : it->second(ctx, args); + } +} diff --git a/indra/alscript/lsl/optimizer/allsllibraryfold.h b/indra/alscript/lsl/optimizer/allsllibraryfold.h new file mode 100644 index 00000000000..3ffb7624fde --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsllibraryfold.h @@ -0,0 +1,40 @@ +/** + * @file allsllibraryfold.h + * @brief The LSL library, answered for constant arguments. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +#include + +namespace ALLSLPasses +{ + typedef std::vector Args; + + // A pure library function's answer to constant arguments, as the + // target's VM gives it, or nothing where it is the VM's to answer: a + // list's getters, the maths, the strings, the parsing, JSON, base64, + // the rotations -- each only where every VM agrees. + LSLConstant* evaluate(Ctx& ctx, const char* name, const Args& args); +} diff --git a/indra/alscript/lsl/optimizer/allslnames.cpp b/indra/alscript/lsl/optimizer/allslnames.cpp new file mode 100644 index 00000000000..d629a2c38d5 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslnames.cpp @@ -0,0 +1,312 @@ +/** + * @file allslnames.cpp + * @brief The LSL optimizer's shortened names. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslnames.h" + +#include "alscriptlexicon.h" + +#include + +#include +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + class TableCollector : public ASTVisitor + { + public: + bool visit(LSLASTNode* node) override + { + if (LSLSymbolTable* table = node->getSymbolTable()) + { + if (mSeen.insert(table).second) + { + tables.push_back(table); + } + } + return true; + } + std::vector tables; + + private: + std::set mSeen; + }; + + // Each label's function or handler. LSL takes a jump to whichever + // label of its name the function declares last, seen from the jump or + // not, so labels of one name in one function are one name still. + class LabelScopes : public ASTVisitor + { + public: + bool visit(LSLLabel* label) override + { + LSLASTNode* scope = label; + while (scope && scope->getNodeType() != NODE_GLOBAL_FUNCTION && scope->getNodeType() != NODE_EVENT_HANDLER) + { + scope = scope->getParent(); + } + if (LSLSymbol* sym = label->getSymbol()) + { + of[sym] = scope; + } + return false; + } + std::map of; + }; + + // What each character of a name costs the target's code, where the + // compiled script keeps it. Mono keeps a global's as its field's, and a + // function's as its method's, and each once more where the code + // reaches it; and a state's in the name of each of its handlers' + // methods, and in the string a change to it loads, whose characters + // are two bytes. Luau keeps a global's and a function's once. Nothing + // else keeps a name: LSO none, and a local's, a parameter's or a + // label's no target. + S32 nameCost(LSLSymbol* sym, ALLSLOptimizer::Target target, const std::map& handlers) + { + const bool global = sym->getSymbolType() == SYM_VARIABLE && sym->getSubType() == SYM_GLOBAL; + const bool used = sym->getReferences() > 1; + switch (target) + { + case ALLSLOptimizer::Target::Mono: + if (global || sym->getSymbolType() == SYM_FUNCTION) + { + return used ? 2 : 1; + } + if (sym->getSymbolType() == SYM_STATE) + { + const auto found = handlers.find(sym); + return (found == handlers.end() ? 0 : found->second) + (used ? 2 : 0); + } + return 0; + case ALLSLOptimizer::Target::Luau: + return global || sym->getSymbolType() == SYM_FUNCTION ? 1 : 0; + default: + return 0; + } + } + + // Every name the script owns, shortest first for those whose characters + // cost the compiled script the most, then for the most used. + void shrink(LSLScript* script, ScriptContext& context, ScriptAllocator& allocator, Report& report, ALLSLOptimizer::Result& result, + ALLSLOptimizer::Target target) + { + TableCollector collector; + script->visit(&collector); + std::vector symbols; + for (LSLSymbolTable* table : collector.tables) + { + if (table->getTableType() == SYMTAB_BUILTINS) + { + continue; + } + for (auto& entry : table->getMap()) + { + LSLSymbol* sym = entry.second; + if (sym->getSymbolType() == SYM_EVENT || sym->getSubType() == SYM_BUILTIN) + { + continue; + } + if (sym->getSymbolType() == SYM_STATE && !strcmp(sym->getName(), "default")) + { + continue; + } + symbols.push_back(sym); + } + } + std::map handlers; + for (LSLState* state : *script->getStates()) + { + if (LSLSymbol* sym = state->getSymbol()) + { + handlers[sym] = static_cast(state->getEventHandlers()->getNumChildren()); + } + } + std::map costs; + for (LSLSymbol* sym : symbols) + { + costs[sym] = nameCost(sym, target, handlers); + } + // Equally used, the first declared first, then by name: the tables + // are hash maps, whose order is the standard library's own. + std::stable_sort(symbols.begin(), symbols.end(), [&costs](LSLSymbol* a, LSLSymbol* b) { + if (costs[a] != costs[b]) + { + return costs[a] > costs[b]; + } + if (a->getReferences() != b->getReferences()) + { + return a->getReferences() > b->getReferences(); + } + const Tailslide::YYLTYPE& at = *a->getLoc(); + const Tailslide::YYLTYPE& bt = *b->getLoc(); + if (at < bt || bt < at) + { + return at < bt; + } + return strcmp(a->getName(), b->getName()) < 0; + }); + const std::string first = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; + const std::string rest = first + "0123456789"; + // Every name given so far, which none is given again but a label's. + boost::unordered_flat_set> given; + const auto usable = [&](const std::string& name) { + // No word of the language's, the preprocessor's extensions + // included, whether or not they are on. + if (ALScriptLexicon::lslWord(name) != ALScriptLexicon::LSL_NAME) + { + return false; + } + return !given.contains(name) && !(context.builtins && context.builtins->lookup(name.c_str(), SYM_ANY)); + }; + std::vector digits; + // The next name, counting up in a mixed radix: the first digit over + // `first`, the rest over `rest`. Looked at, or taken. + const auto next = [&](bool take) { + std::vector looked = digits; + std::vector& at = take ? digits : looked; + std::string name; + while (true) + { + if (at.empty()) + { + at.push_back(0); + } + else + { + size_t i = at.size(); + while (i > 0) + { + --i; + const int radix = i == 0 ? static_cast(first.size()) : static_cast(rest.size()); + if (++at[i] < radix) + { + break; + } + at[i] = 0; + if (i == 0) + { + at.insert(at.begin(), 0); + break; + } + } + } + name.clear(); + for (size_t i = 0; i < at.size(); ++i) + { + name += (i == 0 ? first : rest)[at[i]]; + } + if (usable(name)) + { + return name; + } + } + }; + // Mono loads a state's name as a string where the script changes to + // it, and holds each string once: such a state is named for a string + // the script holds already, where its characters in each handler's + // name cost less than the next name's and a string of that. + Strings strings; + std::vector held; + if (target == ALLSLOptimizer::Target::Mono) + { + script->visit(&strings); + for (const auto& [text, times] : strings.found) + { + const bool shaped = !text.empty() && first.find(text[0]) != std::string::npos && + text.find_first_not_of(rest) == std::string::npos; + if (shaped) + { + held.push_back(text); + } + } + std::sort(held.begin(), held.end(), [](const std::string& a, const std::string& b) { return a.size() != b.size() ? a.size() < b.size() : a < b; }); + } + const auto heldName = [&](LSLSymbol* sym) -> std::optional { + const auto count = handlers.find(sym); + if (held.empty() || sym->getSymbolType() != SYM_STATE || sym->getReferences() < 2 || count == handlers.end()) + { + return std::nullopt; + } + const std::string ours = next(false); + const S32 each = count->second; + const S32 cost = each * S32(ours.size()) + (strings.found.contains(ours) ? 0 : 2 * S32(ours.size()) + 2); + for (const std::string& text : held) + { + if (each * S32(text.size()) >= cost) + { + break; + } + if (usable(text)) + { + return text; + } + } + return std::nullopt; + }; + LabelScopes labels; + script->visit(&labels); + std::map, std::string> labelNames; + for (LSLSymbol* sym : symbols) + { + std::string name; + const auto scope = labels.of.find(sym); + if (scope != labels.of.end()) + { + auto [named, fresh] = labelNames.try_emplace({ scope->second, sym->getName() }); + if (fresh) + { + named->second = next(true); + } + name = named->second; + } + else if (const std::optional text = heldName(sym)) + { + name = *text; + } + else + { + name = next(true); + } + given.insert(name); + sym->setMangledName(allocator.copyStr(name.c_str())); + result.renamed.emplace(sym->getName(), name); + report.note(sym->getLoc(), "OptimizerRenamed", "renamed the [1] [2] to [3]", { LSLSymbol::getTypeName(sym->getSymbolType()), sym->getName(), name }); + } + } +} + + void shrinkNames(LSLScript* script, ScriptContext& context, ScriptAllocator& allocator, Report& report, ALLSLOptimizer::Result& result, + ALLSLOptimizer::Target target) + { + shrink(script, context, allocator, report, result, target); + } +} diff --git a/indra/alscript/lsl/optimizer/allslnames.h b/indra/alscript/lsl/optimizer/allslnames.h new file mode 100644 index 00000000000..df739ff17ab --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslnames.h @@ -0,0 +1,36 @@ +/** + * @file allslnames.h + * @brief The LSL optimizer's shortened names. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // Every name the script owns shortened, the shortest for those whose + // characters cost the compiled script the most, then the most used: + // each symbol's mangled name, and the result's renamed. + void shrinkNames(LSLScript* script, ScriptContext& context, ScriptAllocator& allocator, Report& report, ALLSLOptimizer::Result& result, + ALLSLOptimizer::Target target); +} diff --git a/indra/alscript/lsl/optimizer/allsloptimizer.cpp b/indra/alscript/lsl/optimizer/allsloptimizer.cpp new file mode 100644 index 00000000000..59041271121 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsloptimizer.cpp @@ -0,0 +1,775 @@ +/** + * @file allsloptimizer.cpp + * @brief The LSL optimizer over Tailslide's tree. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +/** + * @file allsloptimizer.cpp + * @brief The LSL optimizer over Tailslide's tree. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + + +#include "linden_common.h" + +#include "allsloptimizer.h" + +#include "alscriptengine.h" + +#include "allslbranches.h" +#include "allslconstantglobals.h" +#include "allslcosts.h" +#include "allsldeadcode.h" +#include "allsleffects.h" +#include "allslflowvalues.h" +#include "allslfolder.h" +#include "allslglobals.h" +#include "allslinliner.h" +#include "allslnames.h" +#include "allsloptimizerpass.h" +#include "allslprinter.h" +#include "allslrepeatedcalls.h" +#include "allslservice.h" +#include "allslshapes.h" +#include "allslsignatures.h" +#include "allslsimplifier.h" + +#include + +#include + +#include +#include + +namespace +{ + using namespace ALLSLPasses; + + // How many nodes a script is, for the budget: what one walk of it + // visits. + struct Gather : public ASTVisitor + { + size_t& count; + explicit Gather(size_t& into) : count(into) {} + bool visit(LSLASTNode* node) override + { + ++count; + return true; + } + }; + + void collectMessages(Logger& logger, ALScriptProblems& problems) + { + for (LogMessage* message : logger.getMessages()) + { + if (message->getType() != LOG_ERROR && message->getType() != LOG_INTERNAL_ERROR) + { + continue; + } + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Error; + p.source = message->getError() == E_SYNTAX_ERROR || message->getError() == E_PARSER_STACK_DEPTH ? ALScriptProblem::Source::Parser + : ALScriptProblem::Source::Types; + p.line = zeroBased(message->getLoc()->first_line); + p.column = zeroBased(message->getLoc()->first_column); + p.endLine = zeroBased(message->getLoc()->last_line); + p.endColumn = std::max(0, message->getLoc()->last_column); + p.code = std::to_string(static_cast(message->getError())); + p.message = message->getMessage(); + problems.push_back(std::move(p)); + } + } + + // Whether a statement standing as an if's true branch, with an else + // after it, would take that else for its own once printed: an if with + // no else, or a loop or an else that ends in one. + bool endsInBareIf(LSLASTNode* statement) + { + LSLASTNode* s = statement; + while (s && s->getNodeType() == NODE_STATEMENT) + { + switch (s->getNodeSubType()) + { + case NODE_IF_STATEMENT: + { + LSLASTNode* otherwise = s->getChild(2); + if (!otherwise || otherwise->getNodeType() == NODE_NULL) + { + return true; + } + s = otherwise; + break; + } + case NODE_WHILE_STATEMENT: + s = s->getChild(1); + break; + case NODE_FOR_STATEMENT: + s = s->getChild(3); + break; + default: + return false; + } + } + return false; + } + + // Braces round each true branch that would otherwise take the else + // after it. The passes move branches about -- `if (!c) A else B` swapped + // round, an empty branch turned into a negated condition -- and the + // printer, as the grammar reads it back, gives an else to the nearest + // if before it. + void braceDanglingElses(LSLASTNode* node, ScriptAllocator* allocator) + { + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + braceDanglingElses(child, allocator); + } + if (node->getNodeType() != NODE_STATEMENT || node->getNodeSubType() != NODE_IF_STATEMENT) + { + return; + } + LSLASTNode* otherwise = node->getChild(2); + if (!otherwise || otherwise->getNodeType() == NODE_NULL || !endsInBareIf(node->getChild(1))) + { + return; + } + LSLASTNode* yes = node->takeChild(1); + auto* block = allocator->newTracked(nullptr); + block->setLoc(yes->getLoc()); + block->pushChild(yes); + node->setChild(1, block); + } + // A user function a run left standing, with what putting it in place + // would take but for its bytes (ALLSLCosts), and where it is in the + // source. + struct Standing + { + std::string name; + ALLSLCosts::Inlining shape; + S32 line = 0; + S32 column = 0; + S32 endLine = 0; + S32 endColumn = 0; + }; + + // Each user function left in the script, as the inliner would put it + // in place: one whose body is `return e;` as its expression, with its + // arguments where its parameters were; any other as a block, each + // parameter a local and its value another, each return but a last one + // a jump. + void standingFunctions(LSLScript* script, const ALSourceMap& inlinedMap, std::vector& out) + { + if (!script->getGlobals()) + { + return; + } + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() != NODE_GLOBAL_FUNCTION) + { + continue; + } + auto* function = static_cast(global); + LSLSymbol* sym = function->getSymbol(); + LSLStatement* body = function->getStatements(); + if (!sym || !body) + { + continue; + } + Standing f; + f.name = sym->getName(); + f.shape.name = f.name.size(); + f.shape.calls = sym->getReferences() - 1; + if (LSLFunctionDec* dec = function->getArguments()) + { + f.shape.params = static_cast(dec->getNumChildren()); + } + const bool value = sym->getType() && sym->getType()->getIType() != LST_NULL; + LSLASTNode* last = nullptr; + for (LSLASTNode* statement = body->getChild(0); statement; statement = statement->getNext()) + { + last = statement; + } + const bool returnsLast = last && last->getNodeType() == NODE_STATEMENT && last->getNodeSubType() == NODE_RETURN_STATEMENT; + const bool expression = value && returnsLast && body->getNumChildren() == 1; + f.shape.locals = expression ? 0 : f.shape.params + (value ? 1 : 0); + boost::unordered_flat_set> strings; + std::vector stack{ body }; + while (!stack.empty()) + { + LSLASTNode* node = stack.back(); + stack.pop_back(); + if (node->getNodeType() == NODE_STATEMENT && node->getNodeSubType() == NODE_RETURN_STATEMENT && node != last) + { + ++f.shape.jumps; + } + else if (node->getNodeType() == NODE_CONSTANT && node->getNodeSubType() == NODE_STRING_CONSTANT) + { + strings.insert(static_cast(node)->getValue()); + } + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + f.shape.strings = static_cast(strings.size()); + for (const std::string& text : strings) + { + f.shape.chars += static_cast(text.size()); + } + if (const Tailslide::YYLTYPE* loc = function->getLoc()) + { + f.line = zeroBased(loc->first_line); + f.column = zeroBased(loc->first_column); + f.endLine = zeroBased(loc->last_line); + f.endColumn = zeroBased(loc->last_column); + if (!inlinedMap.empty()) + { + const ALSourceMap::Loc from = inlinedMap.toSource(f.line, f.column); + const ALSourceMap::Loc to = inlinedMap.toSource(f.endLine, f.endColumn); + if (from.found() && to.found()) + { + f.line = from.line; + f.column = from.column; + f.endLine = to.line; + f.endColumn = to.column; + } + } + } + out.push_back(std::move(f)); + } + } + // One run over a script: put in place what is to be, then the rounds, + // then the printing, and what was printed checked. The functions it + // leaves standing into `standing`, where that is asked for; a list's + // shapes made where `lists` says so. + ALLSLOptimizer::Result once(std::string_view source, const ALLSLOptimizer::Options& options, std::vector* standing, bool lists = true) + { + using Result = ALLSLOptimizer::Result; + Result result; + result.text = std::string(source); + result.sizeBefore = source.size(); + result.sizeAfter = source.size(); + result.map = ALSourceMap::identity(result.text); + if (!ALLSLService::builtinsLoaded()) + { + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Error; + p.source = ALScriptProblem::Source::Optimizer; + p.message = "the LSL definitions are not loaded, so nothing was optimized"; + p.key = "OptimizerNoDefinitions"; + result.problems.push_back(std::move(p)); + return result; + } + + // The functions called once put in place first, in the text, so that + // what is parsed below is an ordinary script; its map is under the + // printer's. Before the engine is taken for the rest, so that the + // inliner's hold is its only one and it can let go between rounds. + std::string inlined; + ALSourceMap inlinedMap; + size_t spent = 0; + if (options.inlining || !options.constFunctions.empty()) + { + ALLSLInliner::Asked asked; + asked.marked = options.inlineNames; + asked.constant = options.constFunctions; + asked.others = options.inlining; + ALLSLInliner::Result put = ALLSLInliner::run(source, asked, options.visitBudget); + // One budget for the two: what the inliner visited is spent. + spent = put.visited; + if (put.stoppedEarly) + { + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Note; + p.source = ALScriptProblem::Source::Optimizer; + p.key = "InlinerStoppedEarly"; + p.message = "stopped putting functions in place part way: there may be more to put in place"; + result.problems.push_back(std::move(p)); + } + if (put.inlined > 0) + { + inlined = std::move(put.text); + inlinedMap = std::move(put.map); + source = inlined; + for (ALScriptProblem& note : put.notes) + { + result.problems.push_back(std::move(note)); + } + } + } + AL_SCRIPT_ENGINE_HELD; + // What is said from here on is said of the inlined text, and brought + // back to the source on the way out. + const size_t saidOfInlined = result.problems.size(); + const auto bringBack = [&]() { + if (inlinedMap.empty()) + { + return; + } + for (size_t i = saidOfInlined; i < result.problems.size(); ++i) + { + ALScriptProblem& p = result.problems[i]; + const ALSourceMap::Loc from = inlinedMap.toSource(p.line, p.column); + const ALSourceMap::Loc to = inlinedMap.toSource(p.endLine, p.endColumn); + if (from.found()) + { + p.line = from.line; + p.column = from.column; + } + if (to.found()) + { + p.endLine = to.line; + p.endColumn = to.column; + } + } + }; + + ScopedScriptParser parser(nullptr); + LSLScript* script = parser.parseLSLBytes(source.data(), static_cast(source.size())); + if (!script || parser.logger.getErrors()) + { + collectMessages(parser.logger, result.problems); + bringBack(); + result.uncompiled = inlinedMap.empty(); + return result; + } + script->collectSymbols(); + script->determineTypes(); + script->recalculateReferenceData(); + // LSO writes every string where it is used, so that two joined are + // always the smaller there; elsewhere the joined may be one more + // string held beside the two, which the option says. + ALLSLArithmetic behavior(&parser.allocator, options.addstrings || options.target == ALLSLOptimizer::Target::LSO, options.target); + // Tailslide's values until what the script writes is known, and + // with it, where folding is asked for, what the locals hold as the + // code runs (FlowValues). + const ALLSLEffects* flowing = nullptr; + const auto propagate = [&]() { + if (flowing) + { + flowValues(script, &behavior, &parser.allocator, *flowing, options.target != ALLSLOptimizer::Target::Luau); + return; + } + ConstantDeterminingVisitor values(&behavior, &parser.allocator); + script->visit(&values); + }; + propagate(); + script->finalPass(); + if (parser.logger.getErrors()) + { + collectMessages(parser.logger, result.problems); + bringBack(); + result.uncompiled = inlinedMap.empty(); + return result; + } + + const ALLSLEffects effects(script); + if (options.constfold) + { + flowing = &effects; + propagate(); + } + Ctx ctx; + ctx.allocator = &parser.allocator; + ctx.context = &parser.context; + ctx.target = options.target; + ctx.foldtabs = options.foldtabs; + ctx.effects = &effects; + Report report(result.problems, options.notes); + // Each pass opens the way for the others; round and round until a + // round changes nothing -- or until the run has visited as much as + // its budget allows, since a large script whose passes keep finding + // work would otherwise hold whoever asked for as long as it liked. + // Every walk of the script counted as it is made -- a pass, and the + // references and values found again after a pass that changed + // something -- from what the inliner left of the budget. + size_t visited = spent; + const auto nodes = [&]() { + size_t count = 0; + Gather gather(count); + script->visit(&gather); + return count; + }; + const size_t perWalk = std::max(1, nodes()); + const auto walks = [&](size_t count) { visited += perWalk * count; }; + const auto refresh = [&]() { + script->recalculateReferenceData(); + propagate(); + walks(2); + }; + const auto settle = [&]() { + for (int round = 0; round < 64; ++round) + { + // No round begun that the budget cannot see through its passes. + if (visited + perWalk * 3 > options.visitBudget) + { + result.stoppedEarly = true; + report.note(nullptr, "OptimizerStoppedEarly", "stopped after [1] rounds: there may be more to do", { std::to_string(round) }); + break; + } + // The references and values found again after the folder and the + // simplifier together, not after each: what the folder makes are + // constants, whose values the simplifier reads off them, and what + // counts it leaves behind can only be too high -- which a pass + // takes as a reason to do less, never as leave to do wrong. Dead + // code is looked for with everything found again, since what + // runs is told from the values. + int changes = 0; + if (options.constfold) + { + const int folded = fold(ctx, report, options, script); + walks(1); + const int simplified = simplify(ctx, report, options, script); + walks(1); + if (folded + simplified) + { + changes += folded + simplified; + refresh(); + } + } + if (options.dcr) + { + const int removed = removeDead(ctx, report, options, script); + walks(1); + if (removed) + { + changes += removed; + refresh(); + } + } + // Branches and loops in fewer jumps, once what can never run + // is gone; the functions given and giving what their calls + // need, and globals that are only scratch made locals. + if (options.constfold) + { + int restructured = restructure(ctx, report, options, script); + walks(1); + restructured += trimSignatures(ctx, report, options, script); + walks(1); + restructured += localizeGlobals(ctx, report, options, script); + walks(1); + if (restructured) + { + changes += restructured; + refresh(); + } + } + if (!changes) + { + break; + } + } + }; + settle(); + // A call made again and again kept in a local, once nothing more + // will fold; and the rounds again where one was, for what the + // places it leaves open. + if (options.constfold && !result.stoppedEarly) + { + const int kept = keepRepeatedCalls(ctx, report, options, script, Keeping::Calls); + walks(1); + if (kept) + { + refresh(); + settle(); + } + } + // What is made for size alone, once nothing more will fold. + bool weighLists = false; + if (options.constfold) + { + shape(ctx, report, options, script, ShapeStage::Values); + restructure(ctx, report, options, script, true); + walks(2); + // A constant written at many places kept once: in a global where + // the target holds one smaller so, else in a local where the + // compiler says that is no larger. After the shapes, which take + // some constants away -- x + 1 as -~x -- and never folded again. + if (!result.stoppedEarly) + { + poolConstants(ctx, report, options, script); + keepRepeatedCalls(ctx, report, options, script, Keeping::Constants); + walks(2); + } + // A list's shapes on Mono, which references a list's helpers + // once for the whole script -- one to add a string, another to + // add an integer, one for a literal. At each place they are + // smaller (ALLSLCosts), which is all they could cost but for a + // helper nothing in the script called before: where they bring + // one in, and the places they save at are too few to pay for it + // at the most a helper costs, the script is weighed with them + // and without (below), and the smaller kept. + const bool mono = lists && options.target == ALLSLOptimizer::Target::Mono; + const ListHelpers had = mono ? listHelpers(script) : ListHelpers{}; + if (lists) + { + const int shaped = shape(ctx, report, options, script, ShapeStage::Lists, mono ? &had : nullptr); + walks(1); + if (mono && shaped) + { + const ListHelpers has = listHelpers(script); + const ALLSLCosts& costs = ALLSLCosts::of(options.target); + weighLists = shaped * costs.listShapeLeast <= had.freshIn(has) * costs.listHelperMost; + } + } + } + if (standing) + { + standingFunctions(script, inlinedMap, *standing); + } + if (options.shrinknames) + { + shrinkNames(script, parser.context, parser.allocator, report, result, options.target); + } + braceDanglingElses(script, &parser.allocator); + + Printed printed = print(script, options); + std::string written = std::move(printed.text); + // What was written must check as what it was made from did: a script + // the optimizer made unable to compile is the optimizer's fault, and + // the source goes as it was, said so, rather than that. + if (std::optional refused = ALLSLOptimizer::checkWritten(written)) + { + result.problems.clear(); + result.problems.push_back(std::move(*refused)); + return result; + } + result.text = std::move(written); + result.map = std::move(printed.map); + if (!inlinedMap.empty()) + { + result.map = result.map.composed(inlinedMap); + } + bringBack(); + result.sizeAfter = result.text.size(); + result.optimized = true; + if (weighLists) + { + // Made again without a list's shapes, both weighed, the smaller + // kept with its weight. + std::vector alone; + ALLSLOptimizer::Result plain = once(source, options, standing ? &alone : nullptr, false); + result.weight = weigh(options.target, result.text); + plain.weight = weigh(options.target, plain.text); + if (plain.optimized && plain.weight.compiled && result.weight.compiled && plain.weight.total <= result.weight.total) + { + if (standing) + { + *standing = std::move(alone); + } + return plain; + } + } + return result; + } +} // namespace + +ALLSLOptimizer::Result ALLSLOptimizer::run(std::string_view source, const Options& options) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + const bool byCost = options.inlining && options.inlineByCost; + std::vector standing; + Result kept = once(source, options, byCost ? &standing : nullptr); + // Of the functions left standing, those called from more than one + // place and not marked -- a marked one went wherever it could -- are + // what the cost may put in place. + std::erase_if(standing, [&options](const Standing& f) { + return f.shape.calls < 2 || std::find(options.inlineNames.begin(), options.inlineNames.end(), f.name) != options.inlineNames.end(); + }); + if (!byCost || !kept.optimized || standing.empty()) + { + return kept; + } + if (!kept.weight.compiled) + { + kept.weight = weigh(options.target, kept.text); + } + if (!kept.weight.compiled) + { + return kept; + } + // Each estimated from what it weighs as the run left it. + const ALLSLCosts& costs = ALLSLCosts::of(options.target); + std::vector> estimated; + for (const Standing& f : standing) + { + const auto renamed = kept.renamed.find(f.name); + const std::string shown = renamed == kept.renamed.end() ? f.name : renamed->second; + for (const ALScriptWeight::Part& part : kept.weight.parts) + { + if (part.kind != ALScriptWeight::Part::Kind::Function || part.name != shown) + { + continue; + } + ALLSLCosts::Inlining shape = f.shape; + shape.bytes = static_cast(part.bytes); + shape.name = shown.size(); + estimated.emplace_back(&f, costs.inlined(shape)); + break; + } + } + // Tried, and kept only where it is smaller as the compiler counts it: + // those estimated within what an estimate is good to -- a function near + // nothing either way is often worth it beside the others -- and those + // estimated to save alone, where they are fewer; the smallest kept. + constexpr S32 MARGIN = 16; + size_t last = 0; + std::optional best; + for (const S32 under : { MARGIN, 0 }) + { + Options more = options; + std::vector chosen; + for (const auto& [f, estimate] : estimated) + { + if (estimate < under) + { + more.inlineNames.push_back(f->name); + chosen.push_back(f->name); + } + } + if (chosen.empty() || chosen.size() == last) + { + continue; + } + last = chosen.size(); + std::vector left; + Result tried = once(source, more, &left); + // What the inliner could not take is the same text. + if (!tried.optimized || tried.text == kept.text) + { + continue; + } + if (!tried.weight.compiled) + { + tried.weight = weigh(options.target, tried.text); + } + if (!tried.weight.compiled || tried.weight.total >= (best ? best->weight.total : kept.weight.total)) + { + continue; + } + const size_t saved = kept.weight.total - tried.weight.total; + for (const auto& [f, estimate] : estimated) + { + if (std::find(chosen.begin(), chosen.end(), f->name) == chosen.end() || !options.notes || + std::any_of(left.begin(), left.end(), [f](const Standing& l) { return l.name == f->name; })) + { + continue; + } + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Note; + p.source = ALScriptProblem::Source::Optimizer; + p.key = "InlinerChoseFunction"; + p.line = f->line; + p.column = f->column; + p.endLine = f->endLine; + p.endColumn = f->endColumn; + p.args = { f->name, std::to_string(f->shape.calls), std::to_string(saved), ALScriptWeight::nameOf(weightTarget(options.target)) }; + p.message = ALScriptProblem::fill("put the function [1] in place at its [2] calls, of what put in place so made the code [3] bytes smaller on [4]", + p.args); + tried.problems.push_back(std::move(p)); + } + best = std::move(tried); + } + return best ? std::move(*best) : std::move(kept); +} + +// static +void ALLSLOptimizer::foldGlobals(LSLScript* script, ScriptAllocator* allocator, ScriptContext* context, Target target) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + if (!script || !script->getGlobals()) + { + return; + } + Options options; + options.target = target; + options.addstrings = true; + options.notes = false; + ALLSLArithmetic behavior(allocator, options.addstrings, target); + Ctx ctx; + ctx.allocator = allocator; + ctx.context = context; + ctx.target = target; + ALScriptProblems unsaid; + Report report(unsaid, false); + script->recalculateReferenceData(); + // A value folded may be what another is folded from: round and round, + // as a run does, until a round folds nothing. + for (int round = 0; round < 16; ++round) + { + ConstantDeterminingVisitor values(&behavior, allocator); + script->visit(&values); + if (!foldGlobalValues(ctx, report, options, script)) + { + break; + } + script->recalculateReferenceData(); + } +} + +// static +std::optional ALLSLOptimizer::checkWritten(std::string_view written) +{ + AL_SCRIPT_ENGINE_HELD; + ScopedScriptParser check(nullptr); + const std::string text(written); + LSLScript* again = check.parseLSLBytes(text.data(), static_cast(text.size())); + if (again && !check.logger.getErrors()) + { + again->collectSymbols(); + again->determineTypes(); + } + if (again && !check.logger.getErrors()) + { + return std::nullopt; + } + ALScriptProblems said; + collectMessages(check.logger, said); + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Warning; + p.source = ALScriptProblem::Source::Optimizer; + p.key = "OptimizerWroteUncompilable"; + p.args = { said.empty() ? std::string() : said.front().message }; + p.message = ALScriptProblem::fill("not optimized: what the optimizer made of this script did not compile ([1]), so it goes as it was; " + "please report it", + p.args); + return p; +} diff --git a/indra/alscript/allsloptimizer.h b/indra/alscript/lsl/optimizer/allsloptimizer.h similarity index 100% rename from indra/alscript/allsloptimizer.h rename to indra/alscript/lsl/optimizer/allsloptimizer.h diff --git a/indra/alscript/lsl/optimizer/allsloptimizerpass.cpp b/indra/alscript/lsl/optimizer/allsloptimizerpass.cpp new file mode 100644 index 00000000000..86efc04e0fc --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsloptimizerpass.cpp @@ -0,0 +1,358 @@ +/** + * @file allsloptimizerpass.cpp + * @brief What the LSL optimizer's passes share. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allsloptimizerpass.h" + +#include + +#include +#include +#include +#include +#include + +namespace ALLSLPasses +{ + std::string render(LSLASTNode* node) + { + PrettyPrintOpts opts{}; + PrettyPrintVisitor printer(opts); + node->visit(&printer); + std::string text = printer.mStream.str(); + while (!text.empty() && (text.back() == '\n' || text.back() == ' ')) + { + text.pop_back(); + } + // A statement's rendering starts with its indentation. + const size_t start = text.find_first_not_of(" \t"); + return start == std::string::npos ? text : text.substr(start); + } + bool wantsParens(LSLASTNode* parent, LSLASTNode* old) + { + if (!parent || parent->getNodeType() != NODE_EXPRESSION) + { + return false; + } + switch (parent->getNodeSubType()) + { + case NODE_BINARY_EXPRESSION: + if (operation_mutates(static_cast(parent)->getOperation()) && parent->getChild(1) == old) + { + return false; + } + return true; + // A cast's own printing puts its operand in parentheses where + // it needs them, and around parentheses too. + case NODE_TYPECAST_EXPRESSION: + return false; + case NODE_UNARY_EXPRESSION: + case NODE_PRINT_EXPRESSION: + case NODE_BOOL_CONVERSION_EXPRESSION: + return true; + default: + return false; + } + } + + void putInPlace(LSLASTNode* old, LSLExpression* replacement, ScriptAllocator* allocator) + { + LSLASTNode* parent = old->getParent(); + replacement->setLoc(old->getLoc()); + if (wantsParens(parent, old) && replacement->getNodeSubType() != NODE_CONSTANT_EXPRESSION && + replacement->getNodeSubType() != NODE_LVALUE_EXPRESSION && replacement->getNodeSubType() != NODE_PARENTHESIS_EXPRESSION && + replacement->getNodeSubType() != NODE_FUNCTION_EXPRESSION) + { + auto* parens = allocator->newTracked(replacement); + parens->setType(replacement->getType()); + parens->setLoc(old->getLoc()); + LSLASTNode::replaceNode(old, parens); + return; + } + LSLASTNode::replaceNode(old, replacement); + } + ALScriptWeight::Target weightTarget(ALLSLOptimizer::Target target) + { + return target == ALLSLOptimizer::Target::LSO ? ALScriptWeight::Target::LSO + : target == ALLSLOptimizer::Target::Luau ? ALScriptWeight::Target::LSLLuau + : ALScriptWeight::Target::Mono; + } + + ALScriptWeight weigh(ALLSLOptimizer::Target target, std::string_view text) + { + switch (weightTarget(target)) + { + case ALScriptWeight::Target::LSO: + return ALScriptWeigh::lso(text); + case ALScriptWeight::Target::LSLLuau: + return ALScriptWeigh::lslLuau(text); + default: + return ALScriptWeigh::mono(text); + } + } + +std::optional rangeOf(LSLExpression* e, int depth) + { + e = bare(e); + if (!e || depth > 8) + { + return std::nullopt; + } + if (LSLConstant* cv = e->getConstantValue()) + { + double v = 0.0; + if (cv->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + v = static_cast(cv)->getValue(); + } + else if (cv->getNodeSubType() == NODE_FLOAT_CONSTANT && std::isfinite(static_cast(cv)->getValue())) + { + v = static_cast(cv)->getValue(); + } + else + { + return std::nullopt; + } + return Range{ v, v }; + } + const auto number = [](LSLExpression* x) { return x && (x->getIType() == LST_INTEGER || x->getIType() == LST_FLOATINGPOINT); }; + switch (e->getNodeSubType()) + { + case NODE_LVALUE_EXPRESSION: + { + auto* read = static_cast(e); + LSLSymbol* sym = read->getSymbol(); + LSLASTNode* decl = sym && !read->getMember() && read->getIsFoldable() && sym->getSubType() == SYM_LOCAL && sym->getAssignments() == 0 + ? sym->getVarDecl() + : nullptr; + LSLASTNode* init = decl ? decl->getChild(1) : nullptr; + return init && init->getNodeType() == NODE_EXPRESSION ? rangeOf(static_cast(init), depth + 1) : std::nullopt; + } + case NODE_FUNCTION_EXPRESSION: + { + LSLSymbol* sym = e->getSymbol(); + if (!sym || sym->getSubType() != SYM_BUILTIN) + { + return std::nullopt; + } + if (!strcmp(sym->getName(), "llFrand")) + { + // From nought towards what it is given, whose sign it + // has; the bound itself let in, which rounding can give. + LSLASTNode* arg = static_cast(e)->getArguments()->getChild(0); + const std::optional mag = arg && arg->getNodeType() == NODE_EXPRESSION ? rangeOf(static_cast(arg), depth + 1) + : std::nullopt; + if (!mag) + { + return std::nullopt; + } + return Range{ std::min(0.0, mag->least), std::max(0.0, mag->most) }; + } + S32 least = 0, most = 0; + if (e->getIType() == LST_INTEGER && ALLSLTraits::bounds(sym->getName(), least, most)) + { + return Range{ static_cast(least), static_cast(most) }; + } + return std::nullopt; + } + case NODE_UNARY_EXPRESSION: + return e->getOperation() == OP_BOOLEAN_NOT ? std::optional(Range{ 0.0, 1.0 }) : std::nullopt; + case NODE_BINARY_EXPRESSION: + { + auto* b = static_cast(e); + switch (b->getOperation()) + { + case OP_LESS: + case OP_GREATER: + case OP_LEQ: + case OP_GEQ: + case OP_EQ: + case OP_BOOLEAN_AND: + case OP_BOOLEAN_OR: + return Range{ 0.0, 1.0 }; + case OP_NEQ: + // A list's != is how much longer it is; LSO's of + // a string not only 1 or 0. + if (b->getLHS()->getIType() == LST_LIST && isEmptyList(b->getRHS())) + { + return Range{ 0.0, static_cast(INT32_MAX) }; + } + return number(b->getLHS()) && number(b->getRHS()) ? std::optional(Range{ 0.0, 1.0 }) : std::nullopt; + case OP_BIT_AND: + for (LSLExpression* side : { b->getLHS(), b->getRHS() }) + { + const std::optional r = side && side->getIType() == LST_INTEGER ? rangeOf(side, depth + 1) : std::nullopt; + if (r && r->least >= 0.0) + { + return Range{ 0.0, r->most }; + } + } + return std::nullopt; + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } + } + + // How many labels of a name the function or event a node is in has. + // A jump goes to the last of them; the compiler finds it among + // those in scope. + int labelsNamed(LSLASTNode* node, const char* name) + { + LSLASTNode* callable = node; + while (callable && callable->getNodeType() != NODE_GLOBAL_FUNCTION && callable->getNodeType() != NODE_EVENT_HANDLER) + { + callable = callable->getParent(); + } + int named = 0; + const std::function count = [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL && !strcmp(static_cast(n)->getIdentifier()->getName(), name)) + { + ++named; + } + for (LSLASTNode* child = n->getChild(0); child; child = child->getNext()) + { + count(child); + } + }; + if (callable) + { + count(callable); + } + return named; + } + + // Whether what follows a statement never runs by running on from it: + // it returns, changes state or jumps; resets the script; is an if + // every way of which does so, or a loop that never ends. A label + // jumped to after it is the dead code's own business (above). + bool stops(LSLASTNode* stmt) + { + if (!stmt || stmt->getNodeType() != NODE_STATEMENT) + { + return false; + } + const auto forever = [](LSLExpression* check) { + LSLConstant* cv = check ? check->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && static_cast(cv)->getValue() != 0; + }; + switch (stmt->getNodeSubType()) + { + case NODE_RETURN_STATEMENT: + case NODE_STATE_STATEMENT: + case NODE_JUMP_STATEMENT: + return true; + case NODE_EXPRESSION_STATEMENT: + { + LSLExpression* expr = bare(static_cast(stmt)->getExpr()); + LSLSymbol* sym = expr && expr->getNodeSubType() == NODE_FUNCTION_EXPRESSION ? expr->getSymbol() : nullptr; + return sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llResetScript"); + } + case NODE_COMPOUND_STATEMENT: + { + LSLASTNode* last = nullptr; + for (LSLASTNode* child = stmt->getChild(0); child; child = child->getNext()) + { + last = child; + } + return stops(last); + } + case NODE_IF_STATEMENT: + { + auto* branch = static_cast(stmt); + return branch->getFalseBranch() && stops(branch->getTrueBranch()) && stops(branch->getFalseBranch()); + } + case NODE_WHILE_STATEMENT: + return forever(static_cast(stmt)->getCheckExpr()); + case NODE_DO_STATEMENT: + return forever(static_cast(stmt)->getCheckExpr()); + case NODE_FOR_STATEMENT: + return forever(static_cast(stmt)->getCheckExpr()); + default: + return false; + } + } + + bool holdsLabel(LSLASTNode* root) + { + bool found = false; + eachNode(root, [&found](LSLASTNode* n) { found = found || (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL); }); + return found; + } + + void setStatements(LSLASTNode* block, const std::vector& statements, ScriptContext& context) + { + // The statements are the ones they were, every reference in them + // still in the script: counted neither out nor in again. + const Uncounted uncounted(context); + std::vector had; + for (LSLASTNode* stmt = block->getChild(0); stmt; stmt = stmt->getNext()) + { + had.push_back(stmt); + } + for (LSLASTNode* stmt : had) + { + block->removeChild(stmt); + } + for (LSLASTNode* stmt : statements) + { + block->pushChild(stmt); + } + } + + std::optional heldFor(ALLSLOptimizer::Target target, LSLConstant* cv) + { + const ALLSLCosts& costs = ALLSLCosts::of(target); + const auto whole = [](std::initializer_list parts) { + return std::all_of(parts.begin(), parts.end(), [](double v) { return integral(v); }); + }; + switch (cv->getIType()) + { + case LST_INTEGER: + return costs.integer; + case LST_FLOATINGPOINT: + return whole({ static_cast(cv)->getValue() }) ? costs.wholeFloating : costs.floating; + case LST_VECTOR: + { + const Vector3* v = static_cast(cv)->getValue(); + return whole({ v->x, v->y, v->z }) ? costs.wholeVector : costs.vector; + } + case LST_QUATERNION: + { + const Quaternion* q = static_cast(cv)->getValue(); + return whole({ q->x, q->y, q->z, q->s }) ? costs.wholeRotation : costs.rotation; + } + case LST_STRING: + return costs.stringOf(static_cast(strlen(static_cast(cv)->getValue()))); + case LST_KEY: + return costs.stringOf(static_cast(strlen(static_cast(cv)->getValue()))); + default: + return std::nullopt; + } + } +} diff --git a/indra/alscript/lsl/optimizer/allsloptimizerpass.h b/indra/alscript/lsl/optimizer/allsloptimizerpass.h new file mode 100644 index 00000000000..bc6fde6de74 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allsloptimizerpass.h @@ -0,0 +1,451 @@ +/** + * @file allsloptimizerpass.h + * @brief What the LSL optimizer's passes share. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizer.h" + +#include "allslcosts.h" +#include "allsltraits.h" +#include "allslvalues.h" +#include "llstl.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +class ALLSLEffects; + +// What the LSL optimizer's passes share: each pass is a file of its own +// (allslfolder, allslsimplifier, allsldeadcode, ...), run by +// ALLSLOptimizer::run (allsloptimizer.cpp) over one parsed script, and +// none is known outside the optimizer. Here: the notes a pass makes, the +// run's context, a pass's base, and what each needs of Tailslide's tree. +namespace ALLSLPasses +{ + using namespace Tailslide; + + // ---- what the definitions say of the library ------------------------------------- + + inline bool isPure(const char* name) { return ALLSLTraits::pure(name); } + + inline S32 zeroBased(int one) { return std::max(0, one - 1); } + + // ---- numbers as the printer writes them ---------------------------------------------- + + using ALLSLValues::floatText; + using ALLSLValues::integral; + + inline bool ascii(const char* s) + { + for (; *s; ++s) + { + if (static_cast(*s) >= 0x80 || (*s < 0x20 && *s != '\n' && *s != '\t')) + { + return false; + } + } + return true; + } + + // What a number can be, the least and the most, where something + // says: a constant; a library function's answer (ALLSLTraits::bounds), + // llFrand's of a magnitude whose sign is known; a truth; an & with a + // number not below nought; a list's length as l != []; a local set + // to one of these where it is declared and never after. + struct Range + { + double least = 0.0; + double most = 0.0; + }; + std::optional rangeOf(LSLExpression* e, int depth = 0); + + // ---- notes --------------------------------------------------------------------------- + + class Report + { + public: + Report(ALScriptProblems& problems, bool wanted) : mProblems(problems), mWanted(wanted) {} + + // Whether anybody reads the notes: a pass that would have to + // print a subtree to say what it did asks first. + bool wanted() const { return mWanted; } + + // A note with a key a translation may be found under, and the + // words -- [1], [2] ... in the text -- it is built from. + void note(const Tailslide::YYLTYPE* loc, const char* key, std::string_view text, std::vector args = {}) + { + if (!mWanted) + { + return; + } + ALScriptProblem p; + p.severity = ALScriptProblem::Severity::Note; + p.source = ALScriptProblem::Source::Optimizer; + if (loc) + { + p.line = zeroBased(loc->first_line); + p.column = zeroBased(loc->first_column); + p.endLine = zeroBased(loc->last_line); + // Tailslide's last column is one past the end, counted from + // one: the end as the studio counts it, as the LSL service + // reads it. + p.endColumn = zeroBased(loc->last_column); + } + p.message = ALScriptProblem::fill(text, args); + p.key = key; + p.args = std::move(args); + mProblems.push_back(std::move(p)); + } + + private: + ALScriptProblems& mProblems; + bool mWanted = true; + }; + + // A node as Tailslide prints it, from its first character to its + // last: what a note says was, and is. + std::string render(LSLASTNode* node); + + inline bool sideEffectFree(LSLASTNode* node) { return ALLSLTraits::sideEffectFree(node); } + // What may be dropped, though not moved: a read of the world or the + // clock changes nothing. + inline bool changesNothing(LSLASTNode* node) { return ALLSLTraits::changesNothing(node); } + + // Skips the parentheses around an expression. + inline LSLExpression* bare(LSLExpression* expr) + { + while (expr && expr->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION) + { + expr = static_cast(expr)->getChildExpr(); + } + return expr; + } + + inline bool isInteger(LSLASTNode* node, int value) + { + LSLConstant* cv = node ? node->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && static_cast(cv)->getValue() == value; + } + + inline bool isFloat(LSLASTNode* node, double value) + { + LSLConstant* cv = node ? node->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_FLOAT_CONSTANT && static_cast(cv)->getValue() == value; + } + + inline bool isEmptyString(LSLASTNode* node) + { + LSLConstant* cv = node ? node->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_STRING_CONSTANT && !*static_cast(cv)->getValue(); + } + + inline bool isEmptyList(LSLASTNode* node) + { + LSLConstant* cv = node ? node->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_LIST_CONSTANT && static_cast(cv)->getLength() == 0; + } + + // Whether the parent of an expression is another expression that could + // bind a replacement differently: parentheses go around it then. The + // right side of an assignment binds nothing. + bool wantsParens(LSLASTNode* parent, LSLASTNode* old); + + // Puts `replacement` where `old` stands, in parentheses if the place + // could bind it differently. + void putInPlace(LSLASTNode* old, LSLExpression* replacement, ScriptAllocator* allocator); + + // Nodes put together off the script -- a sum built a term at a time + // -- with Tailslide's counting of the references under each held off + // while they are: every node made counts its children's whole + // subtrees again, so a sum of n terms, each the left side of the + // next, is n squared. The whole is counted once as it is put in + // place. Tailslide holds it off the same way while it parses; a node + // made meanwhile is marked made by us, as it would have been. + class Uncounted + { + public: + explicit Uncounted(ScriptContext& context) : mContext(context), mWas(context.parsing) { mContext.parsing = true; } + ~Uncounted() { mContext.parsing = mWas; } + Uncounted(const Uncounted&) = delete; + Uncounted& operator=(const Uncounted&) = delete; + + template T* made(T* node) const + { + node->setSynthesized(true); + return node; + } + + private: + ScriptContext& mContext; + const bool mWas; + }; + + // Every node of a subtree, the root first. + template void eachNode(LSLASTNode* root, const F& f) + { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* node = stack.back(); + stack.pop_back(); + if (!node) + { + continue; + } + f(node); + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + } + + // How many labels of a name the function or event a node is in has. + // A jump goes to the last of them; the compiler finds it among those + // in scope. + int labelsNamed(LSLASTNode* node, const char* name); + + // Whether what follows a statement never runs by running on from it: + // it returns, changes state or jumps; resets the script; is an if + // every way of which does so, or a loop that never ends. + bool stops(LSLASTNode* stmt); + + // Whether a label stands anywhere in a subtree. + bool holdsLabel(LSLASTNode* root); + + // A block's statements made `statements`, in that order: those it had + // and those moved in from elsewhere, none counted again. + void setStatements(LSLASTNode* block, const std::vector& statements, ScriptContext& context); + + // What a constant costs held in a global against written where it is + // read, on a target (ALLSLCosts::Held): a float's, a vector's and a + // rotation's as whole or not, a string's and a key's by their length; + // nothing for a list. + std::optional heldFor(ALLSLOptimizer::Target target, LSLConstant* cv); + + // What a text weighs on the target's compiler, and the weigher's name + // for the target. + ALScriptWeight::Target weightTarget(ALLSLOptimizer::Target target); + ALScriptWeight weigh(ALLSLOptimizer::Target target, std::string_view text); + + struct Ctx + { + ScriptAllocator* allocator = nullptr; + ScriptContext* context = nullptr; + ALLSLOptimizer::Target target = ALLSLOptimizer::Target::Mono; + bool foldtabs = false; + // Whether a call is larger than the string that answers it, by the + // call as written and whether the code holds the answer already: + // weighed once for the run (Folder::smallerAnswered). + boost::unordered_flat_map> answers; + // What each function may write, found once for the run: the passes + // only take code away, so what it says stays true, if more than + // is left. + const ALLSLEffects* effects = nullptr; + + // A builtin constant as an answer: the JSON_* names, whose values + // are characters no literal may carry, are folded to the name. + // The value handed back is the builtin's own, which the call + // recognises and puts the name in for. + std::map namedValues; + LSLConstant* builtin(const char* name) + { + if (!context || !context->builtins) + { + return nullptr; + } + LSLSymbol* sym = context->builtins->lookup(name, SYM_VARIABLE); + if (!sym || !sym->getConstantValue()) + { + return nullptr; + } + namedValues.emplace(sym->getConstantValue(), name); + return sym->getConstantValue(); + } + const std::string* nameOf(LSLConstant* value) const + { + const auto found = namedValues.find(value); + return found == namedValues.end() ? nullptr : &found->second; + } + + LSLConstant* integer(int v) { return allocator->newTracked(v); } + LSLConstant* number(double v) + { + const bool single = target != ALLSLOptimizer::Target::Luau; + if (single) + { + const float f = static_cast(v); + if (!std::isfinite(f)) + { + return nullptr; + } + return allocator->newTracked(static_cast(f)); + } + return std::isfinite(v) ? allocator->newTracked(v) : nullptr; + } + // An ll function's float answer, which is a single on every target: + // LSO's and Mono's F32, and SLua's luaSL_pushfloat. + LSLConstant* single(float f) { return std::isfinite(f) ? allocator->newTracked(static_cast(f)) : nullptr; } + LSLConstant* string(const std::string& s) + { + if (!ascii(s.c_str()) || (!foldtabs && s.find('\t') != std::string::npos)) + { + return nullptr; + } + return allocator->newTracked(allocator->copyStr(s.c_str())); + } + LSLConstant* key(const std::string& s) + { + if (!ascii(s.c_str())) + { + return nullptr; + } + return allocator->newTracked(allocator->copyStr(s.c_str())); + } + LSLConstant* vector(double x, double y, double z) + { + const float fx = static_cast(x), fy = static_cast(y), fz = static_cast(z); + if (!std::isfinite(fx) || !std::isfinite(fy) || !std::isfinite(fz)) + { + return nullptr; + } + return allocator->newTracked(fx, fy, fz); + } + LSLConstant* rotation(float x, float y, float z, float s) + { + if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || !std::isfinite(s)) + { + return nullptr; + } + return allocator->newTracked(x, y, z, s); + } + }; + using Counted = boost::unordered_flat_map>; + + // The string literals in what it visits, keys' too. + struct Strings : public ASTVisitor + { + Counted found; + + bool visit(LSLStringConstant* c) override + { + ++found[c->getValue()]; + return false; + } + bool visit(LSLKeyConstant* c) override + { + ++found[c->getValue()]; + return false; + } + }; + + struct Pass + { + Ctx& ctx; + Report& report; + const ALLSLOptimizer::Options& options; + int changes = 0; + + Pass(Ctx& c, Report& r, const ALLSLOptimizer::Options& o) : ctx(c), report(r), options(o) {} + + LSLConstantExpression* constant(LSLConstant* cv, LSLASTNode* at) + { + auto* expr = ctx.allocator->newTracked(cv); + expr->setLoc(at->getLoc()); + return expr; + } + + // Whether a value may be written into the script as a literal. A + // list literal is no smaller than the list expression it came + // from, but it is smaller than a call. + bool inlineable(LSLConstant* cv, bool listsToo = false) + { + if (!cv || cv->containsNaN()) + { + return false; + } + switch (cv->getIType()) + { + case LST_LIST: + if (!listsToo) + { + return false; + } + for (LSLASTNode* item : *cv) + { + if (!inlineable(static_cast(item)) && item->getIType() != LST_KEY) + { + return false; + } + } + return true; + case LST_KEY: + // A key literal is a string. + return false; + case LST_STRING: + { + const char* s = static_cast(cv)->getValue(); + return options.foldtabs || !strchr(s, '\t'); + } + case LST_FLOATINGPOINT: + return std::isfinite(static_cast(cv)->getValue()); + default: + return true; + } + } + + void fold(LSLASTNode* node, LSLConstant* cv, const char* key, const char* what) + { + // What it was and what it became, printed only where the + // notes are read: printing is a visit of the whole subtree, + // and a fold is what a pass does most. + LSLConstantExpression* expr = nullptr; + if (report.wanted()) + { + const std::string was = render(node); + expr = constant(cv, node); + const std::string now = render(expr); + if (was != now) + { + report.note(node->getLoc(), key, std::string(what) + " [1] to [2]", { was, now }); + } + } + else + { + expr = constant(cv, node); + } + LSLASTNode::replaceNode(node, expr); + ++changes; + } + }; +} diff --git a/indra/alscript/lsl/optimizer/allslprinter.cpp b/indra/alscript/lsl/optimizer/allslprinter.cpp new file mode 100644 index 00000000000..b2291f44145 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslprinter.cpp @@ -0,0 +1,342 @@ +/** + * @file allslprinter.cpp + * @brief The LSL optimizer's printer. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslprinter.h" + +#include "allslcosts.h" + +#include + +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // Tailslide's printer with floats that read back exactly, integers + // where a float may be one, and a note of where every name, number + // and statement came from. + class Printer : public PrettyPrintVisitor + { + public: + Printer(const PrettyPrintOpts& opts, const ALLSLOptimizer::Options& options) : PrettyPrintVisitor(opts), mOptions(options) {} + + bool visit(LSLIdentifier* id) override { return marked(id, [&] { return PrettyPrintVisitor::visit(id); }); } + bool visit(LSLIntegerConstant* c) override + { + return marked(c, [&] { + if (negativeCast(c, c->getValue())) + { + mStream << "((integer)" << c->getValue() << ")"; + return false; + } + return PrettyPrintVisitor::visit(c); + }); + } + bool visit(LSLStringConstant* c) override { return marked(c, [&] { return PrettyPrintVisitor::visit(c); }); } + bool visit(LSLKeyConstant* c) override { return marked(c, [&] { return PrettyPrintVisitor::visit(c); }); } + bool visit(LSLFloatConstant* c) override + { + return marked(c, [&] { + const double v = c->getValue(); + const bool integer = integerAllowed(c); + // A whole float where nothing converts an integer for it, + // as the cast of one where that is smaller; not in a + // global's value, which takes no cast, nor a negative + // zero, which the cast would lose the sign of. + if (!integer && mOptions.optfloats && ALLSLCosts::of(mOptions.target).castForWholeFloat && std::isfinite(v) && std::floor(v) == v && + std::fabs(v) < 2147483648.0 && !(v == 0.0 && std::signbit(v)) && !inGlobal(c)) + { + mStream << "((float)" << static_cast(v) << ")"; + return false; + } + mStream << signedNumber(c, v, integer); + return false; + }); + } + bool visit(LSLVectorConstant* c) override + { + return marked(c, [&] { + const Vector3* v = c->getValue(); + mStream << '<' << signedNumber(c, v->x, true) << ", " << signedNumber(c, v->y, true) << ", " << signedNumber(c, v->z, true) << '>'; + return false; + }); + } + bool visit(LSLQuaternionConstant* c) override + { + return marked(c, [&] { + const Quaternion* q = c->getValue(); + mStream << '<' << signedNumber(c, q->x, true) << ", " << signedNumber(c, q->y, true) << ", " << signedNumber(c, q->z, true) << ", " + << signedNumber(c, q->s, true) << '>'; + return false; + }); + } + // Tailslide calls the type quaternion, which LSL takes, but the + // word everyone writes is rotation. + bool visit(LSLType* type) override + { + if (type->getIType() == LST_QUATERNION) + { + mStream << "rotation"; + return false; + } + return PrettyPrintVisitor::visit(type); + } + bool visit(LSLGlobalVariable* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLGlobalFunction* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLState* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLEventHandler* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLExpressionStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLDeclaration* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLIfStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLWhileStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLDoStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLForStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLReturnStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLJumpStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLLabel* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + bool visit(LSLStateStatement* n) override { return at(n, [&] { return PrettyPrintVisitor::visit(n); }); } + + // The map, once everything is printed. + ALSourceMap map(const std::string& name) + { + ALSourceMap out; + out.addFile(name, std::string()); + const std::string text = mStream.str(); + // Marks that start together -- a statement and the name it + // begins with -- kept in the order they were printed, so that + // the map, which answers with the last at a place, answers the + // same for the same script every time. + std::stable_sort(mMarks.begin(), mMarks.end(), [](const Mark& a, const Mark& b) { return a.offset < b.offset; }); + S32 line = 0, column = 0; + size_t pos = 0; + for (const Mark& mark : mMarks) + { + for (; pos < mark.offset && pos < text.size(); ++pos) + { + if (text[pos] == '\n') + { + ++line; + column = 0; + } + else + { + ++column; + } + } + ALSourceMap::Segment s; + s.outLine = line; + s.outColumn = column; + s.length = static_cast(mark.length); + s.line = zeroBased(mark.loc.first_line); + s.column = zeroBased(mark.loc.first_column); + s.verbatim = false; + out.add(s); + } + out.finish(); + return out; + } + + private: + struct Mark + { + size_t offset = 0; + size_t length = 0; + Tailslide::YYLTYPE loc{}; + }; + + template bool marked(LSLASTNode* node, F print) + { + const size_t start = static_cast(mStream.tellp()); + const bool r = print(); + const size_t end = static_cast(mStream.tellp()); + if (node->getLoc()->first_line > 0) + { + mMarks.push_back(Mark{ start, end - start, *node->getLoc() }); + } + return r; + } + + template bool at(LSLASTNode* node, F print) + { + if (node->getLoc()->first_line > 0) + { + mMarks.push_back(Mark{ static_cast(mStream.tellp()), 0, *node->getLoc() }); + } + return print(); + } + + std::string number(double v, bool asInteger) + { + if (mOptions.optfloats && asInteger && integral(v) && ALLSLCosts::of(mOptions.target).integerForFloat) + { + return std::to_string(static_cast(v)); + } + return floatText(v, mOptions.target == ALLSLOptimizer::Target::Luau); + } + + // A negative number as the cast of one, where that is smaller + // (ALLSLCosts::castForNegative): one constant, where -5 is 5 negated. + // Not in a global's value, which takes no cast and reads -5 as one + // constant already. + bool negativeCast(LSLASTNode* c, double v) const + { + return mOptions.constfold && v < 0.0 && std::isfinite(v) && ALLSLCosts::of(mOptions.target).castForNegative && !inGlobal(c); + } + + // A float as number() writes it, the cast of it where it is negative: + // an integer's where it is written as one. + std::string signedNumber(LSLASTNode* c, double v, bool asInteger) + { + const std::string text = number(v, asInteger); + if (!negativeCast(c, v)) + { + return text; + } + const bool whole = text.find_first_not_of("-0123456789") == std::string::npos; + return std::string(whole ? "((integer)" : "((float)") + text + ")"; + } + + // Whether an integer literal may stand where this float does: where + // LSL converts one on its own, and the operation does not change + // with it. + static bool inGlobal(LSLASTNode* c) + { + for (LSLASTNode* up = c->getParent(); up; up = up->getParent()) + { + if (up->getNodeType() == NODE_GLOBAL_VARIABLE) + { + return true; + } + } + return false; + } + + static bool integerAllowed(LSLASTNode* c) + { + LSLASTNode* node = c->getParent(); + LSLASTNode* parent = node ? node->getParent() : nullptr; + // Up through parentheses and a leading minus. + while (parent && parent->getNodeType() == NODE_EXPRESSION && + (parent->getNodeSubType() == NODE_PARENTHESIS_EXPRESSION || + (parent->getNodeSubType() == NODE_UNARY_EXPRESSION && static_cast(parent)->getOperation() == OP_MINUS))) + { + node = parent; + parent = parent->getParent(); + } + if (!parent || !node) + { + return false; + } + const int slot = node->getParentSlot(); + switch (parent->getNodeType()) + { + case NODE_GLOBAL_VARIABLE: + return slot == 1 && parent->getChild(0)->getIType() == LST_FLOATINGPOINT; + case NODE_STATEMENT: + if (parent->getNodeSubType() == NODE_DECLARATION) + { + return slot == 1 && parent->getChild(0)->getIType() == LST_FLOATINGPOINT; + } + if (parent->getNodeSubType() == NODE_RETURN_STATEMENT) + { + for (LSLASTNode* up = parent; up; up = up->getParent()) + { + if (up->getNodeType() == NODE_GLOBAL_FUNCTION) + { + return up->getChild(0)->getIType() == LST_FLOATINGPOINT; + } + } + } + return false; + case NODE_AST_NODE_LIST: + { + // An argument: the parameter's type decides. + LSLASTNode* call = parent->getParent(); + if (!call || call->getNodeSubType() != NODE_FUNCTION_EXPRESSION) + { + return false; + } + LSLSymbol* sym = call->getSymbol(); + if (!sym || !sym->getFunctionDecl()) + { + return false; + } + LSLASTNode* param = sym->getFunctionDecl()->getChild(slot); + return param && param->getIType() == LST_FLOATINGPOINT; + } + case NODE_EXPRESSION: + break; + default: + return false; + } + auto* expr = static_cast(parent); + switch (expr->getNodeSubType()) + { + case NODE_VECTOR_EXPRESSION: + case NODE_QUATERNION_EXPRESSION: + return true; + case NODE_BINARY_EXPRESSION: + { + const LSLOperator op = expr->getOperation(); + LSLASTNode* other = expr->getChild(slot == 0 ? 1 : 0); + if (op == OP_ASSIGN || op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN || op == OP_MUL_ASSIGN || op == OP_DIV_ASSIGN) + { + return slot == 1 && expr->getChild(0)->getIType() == LST_FLOATINGPOINT; + } + if (op == OP_MOD || op == OP_MOD_ASSIGN) + { + return false; + } + // The other operand keeps the operation in floats. + return other && (other->getIType() == LST_FLOATINGPOINT || other->getIType() == LST_VECTOR || other->getIType() == LST_QUATERNION); + } + default: + return false; + } + } + + const ALLSLOptimizer::Options& mOptions; + std::vector mMarks; + }; +} + + Printed print(LSLScript* script, const ALLSLOptimizer::Options& options) + { + PrettyPrintOpts opts{}; + opts.mangle_local_names = options.shrinknames; + opts.mangle_func_names = options.shrinknames; + opts.mangle_global_names = options.shrinknames; + opts.show_unmangled = false; + Printer printer(opts, options); + script->visit(&printer); + Printed printed; + printed.text = printer.mStream.str(); + printed.map = printer.map(std::string()); + return printed; + } +} diff --git a/indra/alscript/lsl/optimizer/allslprinter.h b/indra/alscript/lsl/optimizer/allslprinter.h new file mode 100644 index 00000000000..6c7dd7b000f --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslprinter.h @@ -0,0 +1,41 @@ +/** + * @file allslprinter.h + * @brief The LSL optimizer's printer. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // The script as written out: Tailslide's printer with floats that read + // back exactly, integers where a float may be one, names mangled where + // they were shrunk, and the map from each place in what was written to + // where it came from. + struct Printed + { + std::string text; + ALSourceMap map; + }; + Printed print(LSLScript* script, const ALLSLOptimizer::Options& options); +} diff --git a/indra/alscript/lsl/optimizer/allslrepeatedcalls.cpp b/indra/alscript/lsl/optimizer/allslrepeatedcalls.cpp new file mode 100644 index 00000000000..3be47fd5c4a --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslrepeatedcalls.cpp @@ -0,0 +1,584 @@ +/** + * @file allslrepeatedcalls.cpp + * @brief The LSL optimizer's repeated calls kept in a local. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslrepeatedcalls.h" + +#include "allslcosts.h" +#include "allsleffects.h" +#include "alscriptlexicon.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // A pure library call made more than once in a straight run of a + // block's statements, given only what nothing in the run changes: made + // once, into a local declared before the run, which each place then + // reads -- where the target's compiler says that is no larger, the call + // at each place weighed against the local. The run holds no label, which + // a jump could bring the code in by past the local; and its first place + // is one its statement always comes to, so that the call runs no more + // often than it did. It goes ahead of that statement, which changes + // nothing it is given. + // A global's read so too, where the run sets nothing of it; and, once + // nothing more will fold, a constant written again and again: each read + // or written at any time alike, its first place anywhere in the run. + class RepeatedCalls : public ASTVisitor, public Pass + { + public: + RepeatedCalls(Ctx& c, Report& r, const ALLSLOptimizer::Options& o, LSLScript* script, Keeping keeping) + : Pass(c, r, o), mCosts(ALLSLCosts::of(o.target)), mKeeping(keeping) + { + // Every name the script writes, which a local's may be none of: + // LSL has no shadowing. + each(script, [this](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getName()) + { + mTaken.insert(static_cast(n)->getName()); + } + }); + } + + // The block's own runs first: a local before one of its statements + // holds the places in the blocks that statement holds too. + bool visit(LSLCompoundStatement* block) override + { + while (keepOne(block)) + { + } + visitChildren(block); + return false; + } + + private: + // Each call that may be kept, by what it is: its text and the + // variables it reads, in the order it reads them. + struct Group + { + std::vector reads; + std::vector> places; + size_t length = 0; + }; + + const ALLSLCosts& mCosts; + const Keeping mKeeping; + + // A read of a global the script sets, whole, that is no write of it. + static LSLSymbol* globalRead(LSLASTNode* n) + { + if (n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(n)->getMember()) + { + return nullptr; + } + LSLSymbol* sym = n->getSymbol(); + LSLASTNode* parent = n->getParent(); + const bool write = parent && parent->getNodeType() == NODE_EXPRESSION && operation_mutates(static_cast(parent)->getOperation()) && + parent->getChild(0) == n; + // One never set is the folder's, which writes its value out or not. + return sym && sym->getSymbolType() == SYM_VARIABLE && sym->getSubType() == SYM_GLOBAL && sym->getAssignments() > 0 && !write && + typeName(sym->getIType()) + ? sym + : nullptr; + } + + // A constant of a type a local may hold, but a list. + static bool keptConstant(LSLASTNode* n) + { + if (n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_CONSTANT_EXPRESSION || !n->getConstantValue()) + { + return false; + } + const LSLIType type = n->getIType(); + return type != LST_LIST && typeName(type) && !n->getConstantValue()->containsNaN(); + } + + // A number as the printer writes it for the target: whole as an + // integer where that is smaller, negative as its cast where that is. + std::string printedNumber(double v, bool asInteger, bool alone) const + { + const bool wide = ctx.target == ALLSLOptimizer::Target::Luau; + std::string text = asInteger && integral(v) && mCosts.integerForFloat ? std::to_string(static_cast(v)) : floatText(v, wide); + if (alone && !asInteger && mCosts.castForWholeFloat && integral(v) && std::fabs(v) < 2147483648.0 && !(v == 0.0 && std::signbit(v))) + { + return "((float)" + std::to_string(static_cast(v)) + ")"; + } + if (v < 0.0 && mCosts.castForNegative) + { + const bool whole = text.find_first_not_of("-0123456789") == std::string::npos; + return std::string(whole ? "((integer)" : "((float)") + text + ")"; + } + return text; + } + + // A constant as the printer writes it, for a weighing. + std::optional printed(LSLConstant* cv) const + { + switch (cv->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + { + const S32 v = static_cast(cv)->getValue(); + return v < 0 && mCosts.castForNegative ? "((integer)" + std::to_string(v) + ")" : std::to_string(v); + } + case NODE_FLOAT_CONSTANT: + return printedNumber(static_cast(cv)->getValue(), false, true); + case NODE_VECTOR_CONSTANT: + { + const Vector3* v = static_cast(cv)->getValue(); + return "<" + printedNumber(v->x, true, false) + ", " + printedNumber(v->y, true, false) + ", " + printedNumber(v->z, true, false) + ">"; + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* q = static_cast(cv)->getValue(); + return "<" + printedNumber(q->x, true, false) + ", " + printedNumber(q->y, true, false) + ", " + printedNumber(q->z, true, false) + ", " + + printedNumber(q->s, true, false) + ">"; + } + default: + return ALLSLValues::literal(cv, ctx.target == ALLSLOptimizer::Target::Luau); + } + } + + template static void each(LSLASTNode* root, const F& f) + { + std::vector stack{ root }; + while (!stack.empty()) + { + LSLASTNode* node = stack.back(); + stack.pop_back(); + if (!node) + { + continue; + } + f(node); + for (LSLASTNode* child = node->getChild(0); child; child = child->getNext()) + { + stack.push_back(child); + } + } + } + + static bool keepable(LSLFunctionExpression* call) + { + LSLSymbol* sym = call->getSymbol(); + if (!sym || sym->getSubType() != SYM_BUILTIN || !isPure(sym->getName()) || !call->getArguments() || !call->getArguments()->hasChildren()) + { + return false; + } + switch (call->getIType()) + { + case LST_INTEGER: + case LST_FLOATINGPOINT: + case LST_STRING: + case LST_KEY: + case LST_VECTOR: + case LST_QUATERNION: + case LST_LIST: + return sideEffectFree(call); + default: + return false; + } + } + + static const char* typeName(LSLIType type) + { + switch (type) + { + case LST_INTEGER: return "integer"; + case LST_FLOATINGPOINT: return "float"; + case LST_STRING: return "string"; + case LST_KEY: return "key"; + case LST_VECTOR: return "vector"; + case LST_QUATERNION: return "rotation"; + case LST_LIST: return "list"; + default: return nullptr; + } + } + + // Whether a statement's own evaluation always comes to a node in + // it: through expressions alone, from an expression statement, a + // declaration's value, a return, or what an if, a while or a for + // looks at first. + static bool always(LSLASTNode* node, LSLASTNode* statement) + { + LSLASTNode* child = node; + for (LSLASTNode* up = node->getParent(); up; child = up, up = up->getParent()) + { + if (up == statement) + { + switch (statement->getNodeSubType()) + { + case NODE_EXPRESSION_STATEMENT: + case NODE_DECLARATION: + case NODE_RETURN_STATEMENT: + return true; + case NODE_IF_STATEMENT: + case NODE_WHILE_STATEMENT: + return child == statement->getChild(0); + case NODE_FOR_STATEMENT: + // Its first part, and its check, which runs once + // at the least. + return child == statement->getChild(0) || child == statement->getChild(1); + default: + return false; + } + } + if (up->getNodeType() != NODE_EXPRESSION && up->getNodeType() != NODE_AST_NODE_LIST) + { + return false; + } + } + return false; + } + + static bool holdsLabel(LSLASTNode* root) + { + bool found = false; + each(root, [&found](LSLASTNode* n) { found = found || (n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == NODE_LABEL); }); + return found; + } + + // What the local is named for: a call's function without its ll, a + // length's `length`; a global's `local` and its name; a constant's + // type, by its first letter. + static std::string nameFor(LSLExpression* kept) + { + if (kept->getNodeSubType() == NODE_FUNCTION_EXPRESSION) + { + const std::string_view called = kept->getSymbol()->getName(); + std::string base = called == "llGetListLength" || called == "llStringLength" ? std::string("length") : std::string(called.substr(2)); + base[0] = static_cast(std::tolower(static_cast(base[0]))); + return base; + } + if (kept->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + std::string name = kept->getSymbol()->getName(); + name[0] = static_cast(std::toupper(static_cast(name[0]))); + return "local" + name; + } + switch (kept->getIType()) + { + case LST_INTEGER: return "n"; + case LST_FLOATINGPOINT: return "f"; + case LST_STRING: return "s"; + case LST_KEY: return "k"; + case LST_VECTOR: return "v"; + default: return "r"; + } + } + + // A name of the script's own for the local, numbered where it is taken. + std::string fresh(const std::string& base) + { + const auto usable = [&](const std::string& name) { + return !mTaken.contains(name) && ALScriptLexicon::lslWord(name) == ALScriptLexicon::LSL_NAME && + !(ctx.context->builtins && ctx.context->builtins->lookup(name.c_str(), SYM_ANY)); + }; + std::string name = base; + for (int n = 2; !usable(name); ++n) + { + name = base + std::to_string(n); + } + mTaken.insert(name); + return name; + } + + // Whether the local is no larger than what it keeps at `places` + // places, each compiled in a function of its own: a call given what + // it reads, a global's read with the global declared, a constant as + // the printer writes it. + bool smaller(LSLExpression* first, const std::vector& reads, size_t places) + { + const char* type = typeName(first->getIType()); + std::string globals; + std::string given; + std::string text; + if (first->getNodeSubType() == NODE_CONSTANT_EXPRESSION) + { + const std::optional literal = printed(first->getConstantValue()); + if (!literal) + { + return false; + } + text = *literal; + } + else if (first->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + LSLSymbol* sym = first->getSymbol(); + globals = std::string(typeName(sym->getIType())) + " " + sym->getName() + ";\n"; + text = sym->getName(); + } + else + { + std::vector seen; + for (LSLSymbol* sym : reads) + { + if (sym->getSubType() == SYM_BUILTIN || std::find(seen.begin(), seen.end(), sym) != seen.end()) + { + continue; + } + const char* its = typeName(sym->getIType()); + if (!its) + { + return false; + } + seen.push_back(sym); + given += std::string(given.empty() ? "" : ", ") + its + " " + sym->getName(); + } + text = render(first); + } + const std::string head = globals + "keptCall(" + given + ")\n{\n"; + const std::string tail = "}\ndefault\n{\n state_entry()\n {\n }\n}\n"; + std::string asCalls = head; + std::string asLocal = head + " " + type + " kept = " + text + ";\n"; + for (size_t i = 0; i < places; ++i) + { + asCalls += " llOwnerSay((string)(" + text + "));\n"; + asLocal += " llOwnerSay((string)kept);\n"; + } + asCalls += tail; + asLocal += tail; + const std::string key = "kept\n" + asCalls; + if (const auto found = ctx.answers.find(key); found != ctx.answers.end()) + { + return found->second; + } + const ALScriptWeight byCalls = weigh(ctx.target, asCalls); + const ALScriptWeight byLocal = weigh(ctx.target, asLocal); + // The local's frame too, on a target that keeps one for it. + const bool no = byCalls.compiled && byLocal.compiled && byLocal.total + mCosts.localFrame <= byCalls.total; + ctx.answers.emplace(key, no); + return no; + } + + // The first call worth keeping in the block kept; whether there was one. + bool keepOne(LSLCompoundStatement* block) + { + std::vector statements; + for (LSLASTNode* stmt : *block) + { + statements.push_back(stmt); + } + std::vector groups; + boost::unordered_flat_map> index; + for (size_t i = 0; i < statements.size(); ++i) + { + each(statements[i], [&](LSLASTNode* n) { + const bool call = mKeeping == Keeping::Calls && n->getNodeType() == NODE_EXPRESSION && + n->getNodeSubType() == NODE_FUNCTION_EXPRESSION && keepable(static_cast(n)); + const bool global = mKeeping == Keeping::Calls && globalRead(n); + const bool constant = mKeeping == Keeping::Constants && keptConstant(n); + if (!call && !global && !constant) + { + return; + } + auto* kept = static_cast(n); + std::vector reads; + each(kept, [&reads](LSLASTNode* m) { + if (m->getNodeType() == NODE_EXPRESSION && m->getNodeSubType() == NODE_LVALUE_EXPRESSION) + { + reads.push_back(m->getSymbol()); + } + }); + // A string's and a key's literals are written alike. + const std::string text = render(kept); + std::string key = std::string(typeName(kept->getIType())) + "\n" + text; + for (LSLSymbol* sym : reads) + { + key += "\n" + std::to_string(reinterpret_cast(sym)); + } + auto [at, fresh] = index.try_emplace(key, groups.size()); + if (fresh) + { + groups.push_back(Group{ std::move(reads), {}, text.size() }); + } + groups[at->second].places.emplace_back(i, kept); + }); + } + // The outermost first: a call kept takes with it the calls it is + // given, which would otherwise be kept apart. + std::vector order; + for (Group& g : groups) + { + if (g.places.size() > 1) + { + order.push_back(&g); + } + } + if (order.empty()) + { + return false; + } + std::stable_sort(order.begin(), order.end(), [](const Group* a, const Group* b) { return a->length > b->length; }); + std::vector writes(statements.size()); + std::vector labelled(statements.size()); + for (size_t i = 0; i < statements.size(); ++i) + { + writes[i] = ctx.effects->of(statements[i]); + labelled[i] = holdsLabel(statements[i]); + } + for (Group* g : order) + { + // Runs of statements that change nothing the call reads, each + // begun where a place is always come to. + size_t open = std::string::npos; + LSLExpression* first = nullptr; + std::vector held; + const auto close = [&]() { + const bool kept = held.size() > 1 && smaller(first, g->reads, held.size()); + if (kept) + { + keep(block, statements[open], first, held); + } + open = std::string::npos; + held.clear(); + return kept; + }; + size_t next = 0; + for (size_t i = 0; i < statements.size(); ++i) + { + const bool barred = labelled[i] || std::any_of(g->reads.begin(), g->reads.end(), [&](LSLSymbol* s) { return writes[i].writes(s); }); + std::vector here; + while (next < g->places.size() && g->places[next].first == i) + { + here.push_back(g->places[next++].second); + } + if (barred) + { + if (close()) + { + return true; + } + continue; + } + if (here.empty()) + { + continue; + } + if (open == std::string::npos) + { + // A call only where its statement always comes to it; what + // reads the same at any time anywhere. + const auto starts = std::find_if(here.begin(), here.end(), [&](LSLExpression* c) { + return c->getNodeSubType() != NODE_FUNCTION_EXPRESSION || always(c, statements[i]); + }); + if (starts == here.end()) + { + continue; + } + open = i; + first = *starts; + } + held.insert(held.end(), here.begin(), here.end()); + } + if (close()) + { + return true; + } + } + return false; + } + + // `first`'s call made the value of a local declared before `before`, + // and each place held made a read of it. + void keep(LSLCompoundStatement* block, LSLASTNode* before, LSLExpression* first, const std::vector& held) + { + LSLType* type = first->getType(); + const std::string local = fresh(nameFor(first)); + const std::string said = report.wanted() ? render(first) : std::string(); + const char* name = ctx.allocator->copyStr(local.c_str()); + auto* id = ctx.allocator->newTracked(type, name); + id->setLoc(first->getLoc()); + auto* decl = ctx.allocator->newTracked(id, static_cast(nullptr)); + decl->setLoc(first->getLoc()); + auto* sym = ctx.allocator->newTracked(name, type, SYM_VARIABLE, SYM_LOCAL, first->getLoc(), nullptr, decl); + id->setSymbol(sym); + for (LSLExpression* place : held) + { + auto* read = ctx.allocator->newTracked(type, name); + read->setSymbol(sym); + read->setLoc(place->getLoc()); + auto* lvalue = ctx.allocator->newTracked(read, static_cast(nullptr)); + lvalue->setType(type); + lvalue->setLoc(place->getLoc()); + lvalue->setIsFoldable(true); + LSLASTNode::replaceNode(place, lvalue); + } + decl->setChild(1, first); + { + // The statements are the ones they were: counted neither out + // nor in again. The references are found again after the pass. + const Uncounted uncounted(*ctx.context); + std::vector statements; + for (LSLASTNode* stmt : *block) + { + statements.push_back(stmt); + } + for (LSLASTNode* stmt : statements) + { + block->removeChild(stmt); + } + for (LSLASTNode* stmt : statements) + { + if (stmt == before) + { + block->pushChild(decl); + } + block->pushChild(stmt); + } + } + if (LSLSymbolTable* table = block->getSymbolTable()) + { + table->define(sym); + } + else + { + decl->defineSymbol(sym); + } + report.note(first->getLoc(), "OptimizerKeptCall", "kept [1] in the local [2], which its [3] places read", + { said, local, std::to_string(held.size()) }); + ++changes; + } + + boost::unordered_flat_set> mTaken; + }; +} + + int keepRepeatedCalls(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, Keeping keeping) + { + RepeatedCalls repeated(ctx, report, options, script, keeping); + script->visit(&repeated); + return repeated.changes; + } +} diff --git a/indra/alscript/lsl/optimizer/allslrepeatedcalls.h b/indra/alscript/lsl/optimizer/allslrepeatedcalls.h new file mode 100644 index 00000000000..a491f19624a --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslrepeatedcalls.h @@ -0,0 +1,44 @@ +/** + * @file allslrepeatedcalls.h + * @brief The LSL optimizer's repeated calls kept in a local. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // What a run keeps in a local: pure library calls and globals' reads, + // or -- once nothing more will fold, which would write them back -- + // constants. + enum class Keeping : U8 + { + Calls, + Constants + }; + + // A pure library call, a global's read or a constant made more than once + // in a straight run of a block's statements kept in a local, where the + // target's compiler says that is no larger. How many it kept. + int keepRepeatedCalls(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, Keeping keeping); +} diff --git a/indra/alscript/lsl/optimizer/allslshapes.cpp b/indra/alscript/lsl/optimizer/allslshapes.cpp new file mode 100644 index 00000000000..28fb05d79d5 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslshapes.cpp @@ -0,0 +1,1866 @@ +/** + * @file allslshapes.cpp + * @brief The LSL optimizer's shapes made for size. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslshapes.h" + +#include "allslcosts.h" +#include "allsleffects.h" + +#include +#include +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + class ListHelpersFinder : public ASTVisitor + { + public: + ListHelpers found; + + bool visit(LSLFunctionExpression* expr) override + { + LSLSymbol* sym = expr->getSymbol(); + LSLASTNodeList* args = expr->getArguments(); + if (sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llDumpList2String") && args && args->getNumChildren() == 2) + { + LSLConstant* cv = args->getChild(1)->getConstantValue(); + found.dumps += cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue() ? 1 : 0; + } + return true; + } + + bool visit(LSLGlobalVariable* global) override + { + mGlobal = true; + visitChildren(global); + mGlobal = false; + return false; + } + bool visit(LSLListExpression* expr) override + { + found.used.insert(mGlobal ? ListHelpers::GLOBAL_LITERAL : expr->hasChildren() ? ListHelpers::LITERAL : ListHelpers::EMPTY); + return true; + } + bool visit(LSLTypecastExpression* expr) override + { + if (expr->getIType() == LST_LIST) + { + found.used.insert(ListHelpers::CAST); + } + else if (expr->getIType() == LST_STRING && expr->getChildExpr() && expr->getChildExpr()->getIType() == LST_LIST) + { + found.used.insert(ListHelpers::TO_STRING); + } + return true; + } + bool visit(LSLBinaryExpression* expr) override + { + const LSLOperator op = expr->getOperation(); + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + if ((op == OP_PLUS || op == OP_ADD_ASSIGN) && expr->getIType() == LST_LIST && left && right) + { + found.used.insert(left->getIType() == LST_LIST ? ListHelpers::APPEND | static_cast(right->getIType()) : ListHelpers::PREPEND); + } + return true; + } + + private: + bool mGlobal = false; + }; + + // Rewrites made for size alone, once the rounds are done: each hides a + // value the folder could have used -- a list's literal made a sum is + // no longer a constant -- so they wait until nothing more will fold. + class Shapes : public ASTVisitor, public Pass + { + public: + // Integers' and conditions' shapes, and increments', which the + // code makes of plain instructions; and lists', which Mono makes + // of helpers its assembly references once each, so that what they + // save at each place a helper new to the script can cost more than + // -- weighed, where it can (once()). + enum class Stage : U8 + { + Values, + Lists + }; + Shapes(Ctx& c, Report& r, const ALLSLOptimizer::Options& o, Stage stage) : Pass(c, r, o), mCosts(ALLSLCosts::of(o.target)), mStage(stage) {} + + // An increment whose value nothing reads, before rather than after. + bool visit(LSLExpressionStatement* stmt) override + { + visitChildren(stmt); + if (mStage == Stage::Values) + { + pre(stmt->getExpr()); + } + return false; + } + + // Where only whether it is true counts; and an if whose condition + // is a == with an else, turned around over a ^. + bool visit(LSLIfStatement* stmt) override + { + visitChildren(stmt); + if (mStage != Stage::Values) + { + return false; + } + nestedIf(stmt); + if (keyTest(stmt, 0) < 0) + { + // k == NULL_KEY as k, its ways the other way round. + LSLASTNode* yes = stmt->takeChild(1); + LSLASTNode* no = stmt->getFalseBranch(); + if (no && no->getNodeType() != NODE_NULL) + { + stmt->setChild(1, stmt->takeChild(2)); + } + else + { + auto* nop = ctx.allocator->newTracked(); + nop->setLoc(stmt->getLoc()); + stmt->setChild(1, nop); + } + stmt->setChild(2, yes); + } + truth(stmt, 0); + LSLExpression* cond = bare(stmt->getCheckExpr()); + LSLStatement* yes = stmt->getTrueBranch(); + LSLStatement* no = stmt->getFalseBranch(); + if (mCosts.xorForNotEqual && no && yes && cond && cond->getNodeSubType() == NODE_BINARY_EXPRESSION && cond->getOperation() == OP_EQ && + integerOperands(static_cast(cond))) + { + const std::string before = report.wanted() ? render(cond) : std::string(); + cond->setOperation(OP_BIT_XOR); + stmt->setTrueBranch(nullptr); + stmt->setFalseBranch(nullptr); + stmt->setTrueBranch(no); + stmt->setFalseBranch(yes); + if (report.wanted()) + { + report.note(stmt->getLoc(), "OptimizerSwappedOnXor", "wrote if ([1]) as if ([2]), its branches swapped", { before, render(cond) }); + } + ++changes; + } + return false; + } + + // The ! of a comparison as the comparison the other way. + bool visit(LSLUnaryExpression* expr) override + { + visitChildren(expr); + if (mStage == Stage::Values && !inGlobal(expr)) + { + notComparison(expr); + } + return false; + } + bool visit(LSLWhileStatement* stmt) override + { + visitChildren(stmt); + if (mStage == Stage::Values) + { + keyTest(stmt, 0, false); + truth(stmt, 0); + } + return false; + } + bool visit(LSLDoStatement* stmt) override + { + visitChildren(stmt); + if (mStage == Stage::Values) + { + keyTest(stmt, 1, false); + truth(stmt, 1); + } + return false; + } + bool visit(LSLForStatement* stmt) override + { + visitChildren(stmt); + if (mStage != Stage::Values) + { + return false; + } + keyTest(stmt, 1, false); + truth(stmt, 1); + if (LSLASTNode* steps = stmt->getIncrExprs()) + { + for (LSLASTNode* step = steps->getChild(0); step; step = step->getNext()) + { + pre(step); + } + } + return false; + } + + bool visit(LSLFunctionExpression* expr) override + { + visitChildren(expr); + if (!inGlobal(expr)) + { + if (mStage == Stage::Values) + { + nullKeys(expr); + } + else + { + // A list cast to a string, a list's helper on Mono. + libraryCast(expr); + } + } + return false; + } + + bool visit(LSLBinaryExpression* expr) override + { + if (inGlobal(expr)) + { + return false; + } + // x = x + 1 as ++x, before its x + 1 is seen and made -~x. + if (mStage == Stage::Values && increment(expr)) + { + return false; + } + // A list's elements bare, seen as the author wrote them, before + // what is left of a literal is made a sum below. + if (mStage == Stage::Lists) + { + if (LSLExpression* made = elements(expr)) + { + made->visit(this); + return false; + } + visitChildren(expr); + return false; + } + visitChildren(expr); + if (bitTests(expr)) + { + return false; + } + const int before = changes; + integers(expr); + if (changes == before) + { + operators(expr); + } + return false; + } + + bool visit(LSLDeclaration* decl) override + { + visitChildren(decl); + if (mStage == Stage::Values) + { + dropDefault(decl, decl->getSymbol(), false); + } + return false; + } + + bool visit(LSLGlobalVariable* global) override + { + visitChildren(global); + if (mStage == Stage::Values) + { + dropDefault(global, global->getSymbol(), true); + } + return false; + } + + bool visit(LSLListExpression* expr) override + { + visitChildren(expr); + if (mStage != Stage::Lists || !options.listadd || !mCosts.listAsSum || expr->getNumChildren() == 0) + { + return false; + } + // Not in a global's initializer, which must stay simple. + if (inGlobal(expr)) + { + return false; + } + // A sum takes its right side first, so its elements are taken + // last to first, where a literal's are taken first to last: + // where one of them changes something, and another is anything + // but a constant, that one could see the change the other way + // round. + size_t changing = 0; + size_t varying = 0; + for (LSLASTNode* child : *expr) + { + if (child->getIType() == LST_LIST || child->getIType() == LST_ERROR) + { + return false; + } + // Read at another time than it was: a read of the clock + // counts as a change here. + changing += sideEffectFree(child) ? 0 : 1; + varying += child->getConstantValue() ? 0 : 1; + } + if (changing > 0 && varying > 1) + { + return false; + } + // [a, b, c] as (list)a + b + c. + const std::string was = render(expr); + std::vector terms; + terms.reserve(expr->getNumChildren()); + while (expr->hasChildren()) + { + // takeChild leaves a null in the slot, dropped each time. + terms.push_back(static_cast(expr->takeChild(0))); + expr->removeChild(expr->getChild(0)); + } + LSLExpression* sum = nullptr; + { + const Uncounted uncounted(*ctx.context); + sum = uncounted.made(ctx.allocator->newTracked(TYPE(LST_LIST), terms.front())); + sum->setLoc(terms.front()->getLoc()); + for (size_t i = 1; i < terms.size(); ++i) + { + sum = uncounted.made(ctx.allocator->newTracked(sum, OP_PLUS, uncounted.made(bracketed(terms[i])))); + sum->setType(TYPE(LST_LIST)); + sum->setLoc(terms[i]->getLoc()); + } + } + report.note(expr->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { was, render(sum) }); + putInPlace(expr, sum, ctx.allocator); + ++changes; + return false; + } + + public: + // What list helpers the script called before a list's shapes, where + // they are held once for the script (Mono): a list dumped with + // nothing between is cast to a string only where the script casts + // one already, or the dumps are enough to pay for the helper. + void setHelpers(const ListHelpers* had) { mHad = had; } + + private: + const ALLSLCosts& mCosts; + const Stage mStage; + const ListHelpers* mHad = nullptr; + + static bool inGlobal(LSLASTNode* node) + { + for (LSLASTNode* up = node->getParent(); up; up = up->getParent()) + { + if (up->getNodeType() == NODE_GLOBAL_VARIABLE) + { + return true; + } + } + return false; + } + + void wrote(LSLASTNode* was, LSLExpression* now, const std::string& before) + { + if (report.wanted()) + { + report.note(was->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { before, render(now) }); + } + ++changes; + } + + // `op` over `x`, which is bracketed where what it is could read + // otherwise after the operator: a sum, or -- after a minus -- a + // minus of its own, which would read as a decrement. + LSLExpression* unary(const Uncounted& uncounted, LSLOperator op, LSLExpression* x) + { + bool tight = false; + switch (x->getNodeSubType()) + { + case NODE_LVALUE_EXPRESSION: + case NODE_FUNCTION_EXPRESSION: + case NODE_PARENTHESIS_EXPRESSION: + case NODE_TYPECAST_EXPRESSION: + tight = true; + break; + case NODE_UNARY_EXPRESSION: + tight = op != OP_MINUS || (x->getOperation() != OP_MINUS && x->getOperation() != OP_PRE_DECR); + break; + default: + break; + } + if (!tight) + { + auto* parens = uncounted.made(ctx.allocator->newTracked(x)); + parens->setType(x->getType()); + parens->setLoc(x->getLoc()); + x = parens; + } + auto* made = uncounted.made(ctx.allocator->newTracked(x, op)); + made->setType(op == OP_MINUS ? x->getType() : TYPE(LST_INTEGER)); + made->setLoc(x->getLoc()); + return made; + } + + // `expr`, a binary expression, made `ops` over its operand in + // `slot`, the last of them outermost: `-~x` is { OP_BIT_NOT, + // OP_MINUS }. + void over(LSLBinaryExpression* expr, int slot, std::initializer_list ops) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + auto* x = static_cast(expr->takeChild(slot)); + LSLExpression* made = x; + { + const Uncounted uncounted(*ctx.context); + for (LSLOperator op : ops) + { + made = unary(uncounted, op, made); + } + } + made->setLoc(expr->getLoc()); + // Unary binds tighter than anything around it. + LSLASTNode::replaceNode(expr, made); + wrote(expr, made, before); + } + + // Whether a value is never below -1: a find's, a count's, or a + // local's that is set to one where it is declared and never after. + static bool atLeastMinusOne(LSLExpression* x) + { + x = bare(x); + if (x && x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember()) + { + LSLSymbol* sym = x->getSymbol(); + LSLASTNode* decl = sym && sym->getSubType() == SYM_LOCAL && sym->getAssignments() == 0 ? sym->getVarDecl() : nullptr; + LSLASTNode* init = decl ? decl->getChild(1) : nullptr; + return init && init->getNodeType() == NODE_EXPRESSION && atLeastMinusOne(static_cast(init)); + } + if (!x || x->getNodeSubType() != NODE_FUNCTION_EXPRESSION) + { + return false; + } + LSLSymbol* sym = static_cast(x)->getSymbol(); + S32 least = 0, most = 0; + return sym && sym->getSubType() == SYM_BUILTIN && ALLSLTraits::bounds(sym->getName(), least, most) && least >= -1; + } + + static bool integerOperands(LSLBinaryExpression* expr) + { + return expr->getLHS() && expr->getRHS() && expr->getLHS()->getIType() == LST_INTEGER && expr->getRHS()->getIType() == LST_INTEGER; + } + + // !(a < b) as a >= b, and each comparison's so: of integers, where + // the order of any two is known; and of anything but lists for == + // and !=, which are each other's opposite whatever the values -- + // a list's != is how much longer one is. + void notComparison(LSLUnaryExpression* expr) + { + LSLExpression* inner = bare(expr->getChildExpr()); + if (!mCosts.comparisonForNot || expr->getOperation() != OP_BOOLEAN_NOT || !inner || inner->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return; + } + auto* cmp = static_cast(inner); + LSLOperator to = OP_NONE; + const LSLOperator op = cmp->getOperation(); + switch (op) + { + case OP_LESS: to = OP_GEQ; break; + case OP_GEQ: to = OP_LESS; break; + case OP_GREATER: to = OP_LEQ; break; + case OP_LEQ: to = OP_GREATER; break; + case OP_EQ: to = OP_NEQ; break; + case OP_NEQ: to = OP_EQ; break; + default: return; + } + LSLExpression* left = cmp->getLHS(); + LSLExpression* right = cmp->getRHS(); + if (!left || !right) + { + return; + } + const bool equality = op == OP_EQ || op == OP_NEQ; + const bool fits = integerOperands(cmp) || (equality && left->getIType() != LST_LIST && right->getIType() != LST_LIST && + left->getIType() != LST_ERROR && right->getIType() != LST_ERROR); + if (!fits) + { + return; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + cmp->getParent()->takeChild(cmp->getParentSlot()); + cmp->setOperation(to); + putInPlace(expr, cmp, ctx.allocator); + wrote(expr, cmp, before); + } + + // a >= 5 as a > 4, and a <= 5 as a < 6, either way round, where the + // constant one either side of it is an integer too. + void strict(LSLBinaryExpression* expr) + { + const LSLOperator op = expr->getOperation(); + if (!mCosts.strictForInclusive || (op != OP_GEQ && op != OP_LEQ) || !integerOperands(expr)) + { + return; + } + for (int slot = 0; slot < 2; ++slot) + { + LSLASTNode* side = expr->getChild(slot); + LSLConstant* cv = side->getConstantValue(); + if (!cv || cv->getNodeSubType() != NODE_INTEGER_CONSTANT || expr->getChild(1 - slot)->getConstantValue()) + { + continue; + } + // Whether the constant is on the greater side: a >= C, C <= a. + const bool below = (op == OP_GEQ) == (slot == 1); + const int32_t c = static_cast(cv)->getValue(); + if ((below && c == INT32_MIN) || (!below && c == INT32_MAX)) + { + return; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLASTNode::replaceNode(side, constant(ctx.integer(below ? c - 1 : c + 1), side)); + expr->setOperation(op == OP_GEQ ? OP_GREATER : OP_LESS); + wrote(expr, expr, before); + return; + } + } + + // What an integer comparison or sum comes to in fewer bytes, as a + // value: x == -1 as !~x, x < 0 as !~x of a find, x + 1 as -~x, + // x - 1 as ~-x, and two either way. + // Bit tests of one value merged into one (L18), each form smaller on + // every target (ALLSLCosts::bitTestsMerged): a chain of `x & c` one + // bit each, all asked, as `!~(x | ~mask)`; of `!(x & c)` as + // `!(x & mask)`; and an | of `x & c` as `x & mask` -- and, where only + // whether it is true counts, an || of them (anyBit). Each form is + // one this makes, so that a chain merges a link at a time. x is read + // once where it was read at each test: only what is the same at any + // time, an integer. + enum class Test : U8 + { + Set, // x & c, or a !~(x | ~mask) this made: the bits all set + Clear, // !(x & c): the bits all clear + Masked, // x & c, for an |: the bits as they are + }; + std::optional> bitTest(LSLExpression* e, Test kind) const + { + e = bare(e); + if (!e) + { + return std::nullopt; + } + if (kind == Test::Clear || kind == Test::Set) + { + // A ! over the rest: !(x & c), or !~(x | k). + if (e->getNodeSubType() != NODE_UNARY_EXPRESSION || e->getOperation() != OP_BOOLEAN_NOT) + { + if (kind == Test::Clear) + { + return std::nullopt; + } + } + else + { + LSLExpression* inner = bare(static_cast(e)->getChildExpr()); + if (kind == Test::Clear) + { + return bitTest(inner, Test::Masked); + } + if (!inner || inner->getNodeSubType() != NODE_UNARY_EXPRESSION || inner->getOperation() != OP_BIT_NOT) + { + return std::nullopt; + } + const auto set = operand(bare(static_cast(inner)->getChildExpr()), OP_BIT_OR); + return set ? std::optional>({ set->first, ~set->second }) : std::nullopt; + } + } + const auto masked = operand(e, OP_BIT_AND); + // One bit each, for a test that asks it is set. + if (masked && kind == Test::Set && (masked->second == 0 || (masked->second & (masked->second - 1)) != 0)) + { + return std::nullopt; + } + return masked; + } + + // `x op c` or `c op x`: x, an integer that is the same at any time, + // and the constant. + static std::optional> operand(LSLExpression* e, LSLOperator op) + { + if (!e || e->getNodeSubType() != NODE_BINARY_EXPRESSION || e->getOperation() != op || e->getIType() != LST_INTEGER) + { + return std::nullopt; + } + for (int slot = 0; slot < 2; ++slot) + { + LSLConstant* cv = e->getChild(slot)->getConstantValue(); + LSLExpression* x = static_cast(e->getChild(1 - slot)); + if (cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && !x->getConstantValue() && x->getIType() == LST_INTEGER && sideEffectFree(x)) + { + return std::make_pair(x, static_cast(static_cast(cv)->getValue())); + } + } + return std::nullopt; + } + + // The tests a chain of `op` is made of, all of one x; their x and + // their bits together. + std::optional> chain(LSLBinaryExpression* expr, LSLOperator op, Test kind) const + { + std::vector leaves; + const std::function gather = [&](LSLExpression* e) { + LSLExpression* inner = bare(e); + if (inner && inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getOperation() == op) + { + gather(static_cast(inner)->getLHS()); + gather(static_cast(inner)->getRHS()); + return; + } + leaves.push_back(e); + }; + gather(expr->getLHS()); + gather(expr->getRHS()); + LSLExpression* x = nullptr; + std::string said; + U32 bits = 0; + for (LSLExpression* leaf : leaves) + { + const auto test = bitTest(leaf, kind); + if (!test) + { + return std::nullopt; + } + const std::string text = render(test->first); + if (x && text != said) + { + return std::nullopt; + } + x = test->first; + said = text; + bits |= test->second; + } + return x ? std::optional>({ x, bits }) : std::nullopt; + } + + // `x op mask`, x taken from where it stands. + LSLExpression* withMask(const Uncounted& uncounted, LSLExpression* x, LSLOperator op, U32 mask, LSLASTNode* at) + { + LSLASTNode* parent = x->getParent(); + parent->takeChild(x->getParentSlot()); + auto* made = uncounted.made(ctx.allocator->newTracked(x, op, constant(ctx.integer(static_cast(mask)), at))); + made->setType(TYPE(LST_INTEGER)); + made->setLoc(at->getLoc()); + return made; + } + + bool bitTests(LSLBinaryExpression* expr) + { + if (!mCosts.bitTestsMerged || inGlobal(expr)) + { + return false; + } + const LSLOperator op = expr->getOperation(); + std::optional> set, clear, masked; + if (op == OP_BOOLEAN_AND) + { + set = chain(expr, op, Test::Set); + clear = set ? std::nullopt : chain(expr, op, Test::Clear); + } + else if (op == OP_BIT_OR) + { + masked = chain(expr, op, Test::Masked); + } + if (!set && !clear && !masked) + { + return false; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLExpression* made = nullptr; + { + const Uncounted uncounted(*ctx.context); + if (set) + { + made = unary(uncounted, OP_BOOLEAN_NOT, unary(uncounted, OP_BIT_NOT, withMask(uncounted, set->first, OP_BIT_OR, ~set->second, expr))); + } + else if (clear) + { + made = unary(uncounted, OP_BOOLEAN_NOT, withMask(uncounted, clear->first, OP_BIT_AND, clear->second, expr)); + } + else + { + made = withMask(uncounted, masked->first, OP_BIT_AND, masked->second, expr); + } + } + made->setLoc(expr->getLoc()); + putInPlace(expr, made, ctx.allocator); + wrote(expr, made, before); + return true; + } + + // An || of `x & c` where only whether it is true counts: `x & mask`. + bool anyBit(LSLBinaryExpression* expr) + { + const auto any = mCosts.bitTestsMerged ? chain(expr, OP_BOOLEAN_OR, Test::Masked) : std::nullopt; + if (!any) + { + return false; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLExpression* made = nullptr; + { + const Uncounted uncounted(*ctx.context); + made = withMask(uncounted, any->first, OP_BIT_AND, any->second, expr); + } + putInPlace(expr, made, ctx.allocator); + wrote(expr, made, before); + return true; + } + + void integers(LSLBinaryExpression* expr) + { + strict(expr); + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + if (!left || !right || expr->getIType() != LST_INTEGER || left->getIType() != LST_INTEGER || right->getIType() != LST_INTEGER) + { + return; + } + // Two numbers are the folder's, whose one number is smaller than + // any shape of them: 1 + 2 is 3, never -~-~1 -- though a run that + // stops before it folds leaves them as they are. + if (left->getNodeSubType() == NODE_CONSTANT_EXPRESSION && right->getNodeSubType() == NODE_CONSTANT_EXPRESSION) + { + return; + } + switch (expr->getOperation()) + { + case OP_EQ: + if (mCosts.notComplementForMinusOne && (isInteger(right, -1) || isInteger(left, -1))) + { + over(expr, isInteger(right, -1) ? 0 : 1, { OP_BIT_NOT, OP_BOOLEAN_NOT }); + } + return; + case OP_LESS: + if (mCosts.notComplementForMinusOne && isInteger(right, 0) && atLeastMinusOne(left)) + { + over(expr, 0, { OP_BIT_NOT, OP_BOOLEAN_NOT }); + } + return; + case OP_GREATER: + if (mCosts.notComplementForMinusOne && isInteger(left, 0) && atLeastMinusOne(right)) + { + over(expr, 1, { OP_BIT_NOT, OP_BOOLEAN_NOT }); + } + return; + case OP_PLUS: + for (int slot = 0; slot < 2; ++slot) + { + LSLExpression* other = slot == 0 ? right : left; + if (mCosts.negateComplementForIncrement && isInteger(other, 1)) + { + over(expr, slot, { OP_BIT_NOT, OP_MINUS }); + return; + } + if (mCosts.negateComplementForIncrement && isInteger(other, 2)) + { + over(expr, slot, { OP_BIT_NOT, OP_MINUS, OP_BIT_NOT, OP_MINUS }); + return; + } + // x + -1 and x + -2, which a target with a + -b the + // smaller (plusForMinus) leaves as they are. + if (mCosts.complementNegateForDecrement && isInteger(other, -1)) + { + over(expr, slot, { OP_MINUS, OP_BIT_NOT }); + return; + } + if (mCosts.complementNegateForDecrement && isInteger(other, -2)) + { + over(expr, slot, { OP_MINUS, OP_BIT_NOT, OP_MINUS, OP_BIT_NOT }); + return; + } + } + return; + case OP_MINUS: + if (mCosts.complementNegateForDecrement && isInteger(right, 1)) + { + over(expr, 0, { OP_MINUS, OP_BIT_NOT }); + } + else if (mCosts.complementNegateForDecrement && isInteger(right, 2)) + { + over(expr, 0, { OP_MINUS, OP_BIT_NOT, OP_MINUS, OP_BIT_NOT }); + } + return; + default: + return; + } + } + + // Whether a value is only ever 1 or 0: a comparison, a !, an && or an + // ||, as rangeOf() reads them. + static bool truthValue(LSLExpression* x) + { + const std::optional r = x && x->getIType() == LST_INTEGER ? rangeOf(x) : std::nullopt; + return r && r->least >= 0.0 && r->most <= 1.0; + } + + // A number written where `expr`'s operand in `slot` was. + void renumber(LSLBinaryExpression* expr, int slot, LSLConstant* cv) + { + LSLASTNode* side = expr->getChild(slot); + LSLASTNode::replaceNode(side, constant(cv, side)); + } + + // An integer's constant value, where it has one. + static std::optional integerValue(LSLASTNode* n) + { + LSLConstant* cv = n ? n->getConstantValue() : nullptr; + return cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT ? std::optional(static_cast(cv)->getValue()) + : std::nullopt; + } + + // The power of two an integer is, 1 to 30. + static std::optional powerOfTwo(S32 v) + { + for (S32 k = 1; k <= 30; ++k) + { + if (v == (S32(1) << k)) + { + return k; + } + } + return std::nullopt; + } + + // `a - b` made `a + -b`, and `a -= b` `a += -b`, the right side a + // constant negated where it is one: the same value, and the right + // run first as before. + void plus(LSLBinaryExpression* expr) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLExpression* right = expr->getRHS(); + LSLConstant* cv = right->getConstantValue(); + if (cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + renumber(expr, 1, ctx.integer(-static_cast(cv)->getValue())); + } + else if (cv && cv->getNodeSubType() == NODE_FLOAT_CONSTANT) + { + renumber(expr, 1, ctx.allocator->newTracked(-static_cast(cv)->getValue())); + } + else + { + auto* x = static_cast(expr->takeChild(1)); + LSLExpression* made = nullptr; + { + const Uncounted uncounted(*ctx.context); + made = unary(uncounted, OP_MINUS, x); + } + expr->setChild(1, made); + } + expr->setOperation(expr->getOperation() == OP_SUB_ASSIGN ? OP_ADD_ASSIGN : OP_PLUS); + wrote(expr, expr, before); + } + + // `a - b` where only whether a and b differ counts, and as the ! of + // that for a == b: a negative constant on the right added instead. + LSLExpression* difference(LSLBinaryExpression* expr) + { + const std::optional c = integerValue(expr->getRHS()); + if (c && *c < 0 && *c != INT32_MIN) + { + renumber(expr, 1, ctx.integer(-*c)); + expr->setOperation(OP_PLUS); + } + else + { + expr->setOperation(OP_MINUS); + } + return expr; + } + + // Operators the target has smaller another way (ALLSLCosts): an && + // of truths as &, -1 - x as ~x, a - b as a + -b, a shift left as a product, a + // doubling as a sum, a float divided by a power of two as a product, + // a == as the ! of a difference; and of what is never below nought, + // a shift right as a quotient and a mask as a remainder. + void operators(LSLBinaryExpression* expr) + { + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + if (!left || !right) + { + return; + } + const LSLIType type = expr->getIType(); + const bool numeric = (type == LST_INTEGER || type == LST_FLOATINGPOINT) && left->getIType() == type && right->getIType() == type; + const auto read = [](LSLExpression* x) { + LSLSymbol* sym = x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember() ? x->getSymbol() : nullptr; + return sym && (sym->getSubType() == SYM_LOCAL || sym->getSubType() == SYM_FUNCTION_PARAMETER || sym->getSubType() == SYM_EVENT_PARAMETER); + }; + switch (expr->getOperation()) + { + case OP_BOOLEAN_AND: + if (mCosts.bitAndForAnd && integerOperands(expr) && truthValue(left) && truthValue(right)) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + expr->setOperation(OP_BIT_AND); + wrote(expr, expr, before); + } + return; + case OP_MINUS: + case OP_SUB_ASSIGN: + { + // -1 - x as ~x. + if (mCosts.complementForMinusOneLess && expr->getOperation() == OP_MINUS && integerOperands(expr) && isInteger(left, -1) && + !right->getConstantValue()) + { + over(expr, 1, { OP_BIT_NOT }); + return; + } + // Not of a constant the folder would have, nor of the + // one number that negated is itself. + const std::optional c = integerValue(right); + if (mCosts.plusForMinus && numeric && !(left->getConstantValue() && right->getConstantValue()) && !(c && *c == INT32_MIN)) + { + plus(expr); + } + return; + } + case OP_SHIFT_LEFT: + { + const std::optional c = integerValue(right); + if (mCosts.productForShift && integerOperands(expr) && c && *c >= 1 && *c <= 30 && !left->getConstantValue()) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + renumber(expr, 1, ctx.integer(S32(1) << *c)); + expr->setOperation(OP_MUL); + wrote(expr, expr, before); + } + return; + } + case OP_SHIFT_RIGHT: + { + const std::optional c = integerValue(right); + const std::optional r = integerOperands(expr) ? rangeOf(left) : std::nullopt; + if (mCosts.quotientForShift && c && *c >= 1 && *c <= 30 && r && r->least >= 0.0 && !left->getConstantValue()) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + renumber(expr, 1, ctx.integer(S32(1) << *c)); + expr->setOperation(OP_DIV); + wrote(expr, expr, before); + } + return; + } + case OP_BIT_AND: + { + if (!mCosts.quotientForShift || !integerOperands(expr)) + { + return; + } + for (int slot = 0; slot < 2; ++slot) + { + auto* x = static_cast(expr->getChild(1 - slot)); + const std::optional mask = integerValue(expr->getChild(slot)); + const std::optional k = mask && *mask > 0 && *mask < INT32_MAX ? powerOfTwo(*mask + 1) : std::nullopt; + const std::optional r = k && !x->getConstantValue() ? rangeOf(x) : std::nullopt; + if (r && r->least >= 0.0) + { + // x % 2^k, x on the left. + const std::string before = report.wanted() ? render(expr) : std::string(); + if (slot == 0) + { + LSLASTNode* m = expr->takeChild(0); + LSLASTNode* v = expr->takeChild(1); + expr->setChild(0, v); + expr->setChild(1, m); + } + renumber(expr, 1, ctx.integer(*mask + 1)); + expr->setOperation(OP_MOD); + wrote(expr, expr, before); + return; + } + } + return; + } + case OP_MUL: + { + if (!mCosts.sumForDouble || !numeric) + { + return; + } + for (int slot = 0; slot < 2; ++slot) + { + auto* x = static_cast(expr->getChild(1 - slot)); + auto* two = expr->getChild(slot); + if (read(x) && (isInteger(two, 2) || (type == LST_FLOATINGPOINT && isFloat(two, 2.0)))) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLExpression* again = static_cast(x)->clone(); + again->setType(x->getType()); + again->setLoc(x->getLoc()); + LSLASTNode::replaceNode(two, again); + expr->setOperation(OP_PLUS); + wrote(expr, expr, before); + return; + } + } + return; + } + case OP_DIV: + { + LSLConstant* cv = right->getConstantValue(); + if (!mCosts.productForQuotient || type != LST_FLOATINGPOINT || !cv || left->getConstantValue()) + { + return; + } + double v = 0.0; + if (cv->getNodeSubType() == NODE_FLOAT_CONSTANT) + { + v = static_cast(cv)->getValue(); + } + else if (cv->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + v = static_cast(cv)->getValue(); + } + int k = 0; + const bool power = std::isfinite(v) && v != 0.0 && std::frexp(std::fabs(v), &k) == 0.5; + // A power of two from 2^-126 to 2^126, but for one, whose + // reciprocal is as exact. + if (!power || k - 1 < -126 || k - 1 > 126 || k == 1) + { + return; + } + LSLConstant* reciprocal = ctx.number(1.0 / v); + if (!reciprocal) + { + return; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + renumber(expr, 1, reciprocal); + expr->setOperation(OP_MUL); + wrote(expr, expr, before); + return; + } + case OP_EQ: + if (mCosts.differenceForNotEqual && integerOperands(expr) && !left->getConstantValue()) + { + // !(a - b), made of its operands. + const std::string before = report.wanted() ? render(expr) : std::string(); + auto* a = static_cast(expr->takeChild(0)); + auto* b = static_cast(expr->takeChild(1)); + LSLExpression* made = nullptr; + { + const Uncounted uncounted(*ctx.context); + auto* apart = uncounted.made(ctx.allocator->newTracked(a, OP_MINUS, b)); + apart->setType(TYPE(LST_INTEGER)); + apart->setLoc(expr->getLoc()); + made = unary(uncounted, OP_BOOLEAN_NOT, difference(apart)); + } + made->setLoc(expr->getLoc()); + LSLASTNode::replaceNode(expr, made); + wrote(expr, made, before); + } + return; + default: + return; + } + } + + // Whether a key is never anything but a key that is, or NULL_KEY: + // what a library function gives for one it found or did not, an + // event's key that names who or what was there, or a local only + // ever set to one of those. `if (k)` is then `k != NULL_KEY`, where + // of any other string the two differ. + bool validKey(LSLExpression* e, int depth = 0) const + { + static const char* const FOUND[] = { "llGetOwner", "llGetKey", "llGetCreator", "llDetectedKey", "llDetectedOwner", + "llAvatarOnSitTarget", "llAvatarOnLinkSitTarget", "llGetLinkKey", "llGetPermissionsKey", + "llGetInventoryKey", "llGetInventoryCreator", "llGetLandOwnerAt", "llGenerateKey", + "llHTTPRequest", "llRequestAgentData", "llRequestInventoryData", "llRequestSimulatorData", + "llRequestDisplayName", "llRequestUsername", "llRequestUserKey", "llName2Key", + "llGetNotecardLine", "llGetNumberOfNotecardLines", "llTransferLindenDollars", + "llReadKeyValue", "llCreateKeyValue", "llUpdateKeyValue", "llDeleteKeyValue", + "llDataSizeKeyValue", "llKeysKeyValue", "llRezObjectWithParams" }; + static const char* const NAMED[] = { "listen", "money", "object_rez", "attach", "control", "dataserver", "http_request", + "http_response", "transaction_result", "experience_permissions", + "experience_permissions_denied", "game_control" }; + e = bare(e); + if (!e || e->getIType() != LST_KEY || depth > 4) + { + return false; + } + if (e->getNodeSubType() == NODE_FUNCTION_EXPRESSION) + { + LSLSymbol* sym = e->getSymbol(); + return sym && sym->getSubType() == SYM_BUILTIN && + std::any_of(std::begin(FOUND), std::end(FOUND), [sym](const char* f) { return !strcmp(f, sym->getName()); }); + } + if (e->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(e)->getMember()) + { + return false; + } + LSLSymbol* sym = e->getSymbol(); + if (!sym) + { + return false; + } + if (sym->getSubType() == SYM_EVENT_PARAMETER) + { + // The handler the read is in, whose parameter it is. + LSLASTNode* handler = e; + while (handler && handler->getNodeType() != NODE_EVENT_HANDLER) + { + handler = handler->getParent(); + } + LSLSymbol* event = handler ? handler->getSymbol() : nullptr; + return event && sym->getAssignments() == 0 && + std::any_of(std::begin(NAMED), std::end(NAMED), [event](const char* n) { return !strcmp(n, event->getName()); }); + } + if (sym->getSubType() != SYM_LOCAL) + { + return false; + } + // Declared as one, and set to nothing else after. + LSLASTNode* decl = sym->getVarDecl(); + LSLASTNode* init = decl ? decl->getChild(1) : nullptr; + if (!init || init->getNodeType() != NODE_EXPRESSION || !validKey(static_cast(init), depth + 1)) + { + return false; + } + LSLASTNode* owner = decl; + while (owner && owner->getNodeType() != NODE_GLOBAL_FUNCTION && owner->getNodeType() != NODE_EVENT_HANDLER) + { + owner = owner->getParent(); + } + bool valid = owner != nullptr; + eachNode(owner, [&](LSLASTNode* n) { + if (!valid || n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_LVALUE_EXPRESSION || n->getSymbol() != sym) + { + return; + } + LSLASTNode* parent = n->getParent(); + if (parent && parent->getNodeType() == NODE_EXPRESSION && operation_mutates(static_cast(parent)->getOperation()) && + parent->getChild(0) == n) + { + valid = static_cast(parent)->getOperation() == OP_ASSIGN && parent->getChild(1)->getNodeType() == NODE_EXPRESSION && + validKey(static_cast(parent->getChild(1)), depth + 1); + } + }); + return valid; + } + + // A condition `k != NULL_KEY` of such a key made `k`, and, where + // `turned` lets it, `k == NULL_KEY` too, for its if's ways to be + // turned around: 1 where it was a !=, -1 a ==, 0 where nothing. + int keyTest(LSLASTNode* parent, int slot, bool turned = true) + { + LSLASTNode* child = parent->getChild(slot); + if (!child || child->getNodeType() != NODE_EXPRESSION) + { + return 0; + } + LSLExpression* cond = bare(static_cast(child)); + if (!cond || cond->getNodeSubType() != NODE_BINARY_EXPRESSION || (cond->getOperation() != OP_NEQ && (cond->getOperation() != OP_EQ || !turned))) + { + return 0; + } + static constexpr std::string_view NULL_TEXT = "00000000-0000-0000-0000-000000000000"; + const auto isNull = [](LSLASTNode* n) { + LSLConstant* cv = n ? n->getConstantValue() : nullptr; + if (!cv || (cv->getIType() != LST_KEY && cv->getIType() != LST_STRING)) + { + return false; + } + const char* text = cv->getIType() == LST_KEY ? static_cast(cv)->getValue() : static_cast(cv)->getValue(); + return text && NULL_TEXT == text; + }; + for (int side = 0; side < 2; ++side) + { + auto* k = static_cast(cond->getChild(side)); + if (!isNull(cond->getChild(1 - side)) || !validKey(k)) + { + continue; + } + const bool equal = cond->getOperation() == OP_EQ; + const std::string before = report.wanted() ? render(cond) : std::string(); + cond->takeChild(side); + LSLASTNode::replaceNode(child, k); + if (report.wanted()) + { + report.note(k->getLoc(), "OptimizerKeyAsTruth", "wrote [1] as the key itself, which is a key found or NULL_KEY", { before }); + } + ++changes; + return equal ? -1 : 1; + } + return 0; + } + + // Whether running an expression could stop the script: a division + // or a remainder by anything but a constant other than nought. + static bool mayFault(LSLASTNode* root) + { + bool fault = false; + eachNode(root, [&](LSLASTNode* n) { + if (fault || n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return; + } + const LSLOperator op = static_cast(n)->getOperation(); + if (op != OP_DIV && op != OP_MOD && op != OP_DIV_ASSIGN && op != OP_MOD_ASSIGN) + { + return; + } + LSLConstant* cv = n->getChild(1)->getConstantValue(); + fault = !cv || (cv->getNodeSubType() == NODE_INTEGER_CONSTANT && !static_cast(cv)->getValue()) || + (cv->getNodeSubType() == NODE_FLOAT_CONSTANT && static_cast(cv)->getValue() == 0.0) || + (cv->getNodeSubType() != NODE_INTEGER_CONSTANT && cv->getNodeSubType() != NODE_FLOAT_CONSTANT); + }); + return fault; + } + + // An operand of a new && or &, bracketed where it is an operation. + LSLExpression* operand(LSLExpression* x) + { + if (x->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return x; + } + auto* parens = ctx.allocator->newTracked(x); + parens->setType(x->getType()); + parens->setLoc(x->getLoc()); + return parens; + } + + // `if (a) if (b) S`, neither with an else, as `if (a && b) S`, or + // `if (a & b) S` where both are only ever 1 or 0: where b may run + // whatever a is -- it changes nothing, reads nothing of the world, + // nothing a writes, and cannot stop the script -- since LSL runs + // both sides, b first. + void nestedIf(LSLIfStatement* stmt) + { + LSLStatement* yes = stmt->getTrueBranch(); + LSLASTNode* no = stmt->getFalseBranch(); + LSLASTNode* inner = yes && yes->getNodeSubType() == NODE_COMPOUND_STATEMENT && yes->getNumChildren() == 1 ? yes->getChild(0) : yes; + if ((no && no->getNodeType() != NODE_NULL) || !inner || inner->getNodeType() != NODE_STATEMENT || inner->getNodeSubType() != NODE_IF_STATEMENT) + { + return; + } + auto* then = static_cast(inner); + LSLASTNode* other = then->getFalseBranch(); + LSLExpression* a = stmt->getCheckExpr(); + LSLExpression* b = then->getCheckExpr(); + if ((other && other->getNodeType() != NODE_NULL) || !a || !b || a->getIType() != LST_INTEGER || b->getIType() != LST_INTEGER) + { + return; + } + const bool truths = truthValue(a) && truthValue(b); + LSLOperator op = truths && mCosts.bitAndForNestedTruths ? OP_BIT_AND : mCosts.andForNestedIf ? OP_BOOLEAN_AND : OP_NONE; + if (op == OP_NONE || !sideEffectFree(b) || mayFault(b)) + { + return; + } + const ALLSLEffects::Writes writes = ctx.effects->of(a); + bool read = false; + eachNode(b, [&](LSLASTNode* n) { + read = read || (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_LVALUE_EXPRESSION && writes.writes(n->getSymbol())); + }); + if (read) + { + return; + } + const std::string before = report.wanted() ? render(a) + ") if (" + render(b) : std::string(); + const Uncounted uncounted(*ctx.context); + auto* left = static_cast(stmt->takeChild(0)); + auto* right = static_cast(then->takeChild(0)); + auto* body = then->takeChild(1); + auto* both = uncounted.made(ctx.allocator->newTracked(operand(left), op, operand(right))); + both->setType(TYPE(LST_INTEGER)); + both->setLoc(left->getLoc()); + stmt->setChild(0, both); + stmt->setChild(1, body); + if (report.wanted()) + { + report.note(stmt->getLoc(), "OptimizerMergedIfs", "wrote if ([1]) as one if", { before }); + } + ++changes; + } + + // What only counts as true or false -- a condition, an operand of + // !, && or || in one -- in fewer bytes: x != -1 as ~x, and x > -1 + // and x >= 0 as ~x of a find. + void truth(LSLASTNode* parent, int slot) + { + LSLASTNode* child = parent->getChild(slot); + if (!child || child->getNodeType() != NODE_EXPRESSION) + { + return; + } + LSLExpression* inner = bare(static_cast(child)); + if (!inner || inGlobal(inner)) + { + return; + } + if (inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) + { + truth(inner, 0); + return; + } + // A string's length is nothing where the string is. + if (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && mCosts.emptyForLength) + { + auto* call = static_cast(inner); + LSLSymbol* sym = call->getSymbol(); + LSLASTNodeList* args = call->getArguments(); + if (sym && sym->getSubType() == SYM_BUILTIN && !strcmp(sym->getName(), "llStringLength") && args && args->getNumChildren() == 1) + { + const std::string before = report.wanted() ? render(call) : std::string(); + auto* text = static_cast(args->takeChild(0)); + auto* made = ctx.allocator->newTracked(text, OP_NEQ, constant(ctx.string(std::string()), call)); + made->setType(TYPE(LST_INTEGER)); + made->setLoc(call->getLoc()); + putInPlace(call, made, ctx.allocator); + wrote(call, made, before); + } + return; + } + if (inner->getNodeSubType() != NODE_BINARY_EXPRESSION) + { + return; + } + auto* expr = static_cast(inner); + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + if (expr->getOperation() == OP_BOOLEAN_OR && anyBit(expr)) + { + return; + } + if (expr->getOperation() == OP_BOOLEAN_AND || expr->getOperation() == OP_BOOLEAN_OR) + { + truth(expr, 0); + truth(expr, 1); + // Either true is the two bits or'd true; LSL runs both + // sides of || either way. + if (expr->getOperation() == OP_BOOLEAN_OR && mCosts.bitOrForOr && integerOperands(expr)) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + expr->setOperation(OP_BIT_OR); + wrote(expr, expr, before); + } + return; + } + if (!left || !right || left->getIType() != LST_INTEGER || right->getIType() != LST_INTEGER) + { + return; + } + if (expr->getOperation() == OP_NEQ && !isInteger(right, -1) && !isInteger(left, -1)) + { + // Different is the bits differing, or the difference. + if (mCosts.xorForNotEqual) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + expr->setOperation(OP_BIT_XOR); + wrote(expr, expr, before); + } + else if (mCosts.differenceForNotEqual && !left->getConstantValue()) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + wrote(expr, difference(expr), before); + } + return; + } + // Odd is what is left over of a half. + if (expr->getOperation() == OP_BIT_AND && mCosts.remainderForOddTest) + { + for (int slot = 0; slot < 2; ++slot) + { + if (isInteger(expr->getChild(slot), 1) && !expr->getChild(1 - slot)->getConstantValue()) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + if (slot == 0) + { + LSLASTNode* one = expr->takeChild(0); + LSLASTNode* x = expr->takeChild(1); + expr->setChild(0, x); + expr->setChild(1, one); + } + renumber(expr, 1, ctx.integer(2)); + expr->setOperation(OP_MOD); + wrote(expr, expr, before); + return; + } + } + } + if (!mCosts.complementForNotMinusOne) + { + return; + } + switch (expr->getOperation()) + { + case OP_NEQ: + if (isInteger(right, -1) || isInteger(left, -1)) + { + over(expr, isInteger(right, -1) ? 0 : 1, { OP_BIT_NOT }); + } + return; + case OP_GREATER: + if (isInteger(right, -1) && atLeastMinusOne(left)) + { + over(expr, 0, { OP_BIT_NOT }); + } + return; + case OP_GEQ: + if (isInteger(right, 0) && atLeastMinusOne(left)) + { + over(expr, 0, { OP_BIT_NOT }); + } + return; + case OP_LESS: + if (isInteger(left, -1) && atLeastMinusOne(right)) + { + over(expr, 1, { OP_BIT_NOT }); + } + return; + case OP_LEQ: + if (isInteger(left, 0) && atLeastMinusOne(right)) + { + over(expr, 1, { OP_BIT_NOT }); + } + return; + default: + return; + } + } + + // A library call that a cast to a string says the same as: + // llDumpList2String(l, "") as (string)l, and the one detail + // llGetObjectDetails gives as a string -- or "", as both say where + // there is no such object. + void libraryCast(LSLFunctionExpression* expr) + { + LSLSymbol* sym = expr->getSymbol(); + LSLASTNodeList* args = expr->getArguments(); + if (!sym || sym->getSubType() != SYM_BUILTIN || !args || args->getNumChildren() != 2) + { + return; + } + auto* first = static_cast(args->getChild(0)); + LSLASTNode* second = args->getChild(1); + bool cast = false; + if (mCosts.castForDump && !strcmp(sym->getName(), "llDumpList2String")) + { + LSLConstant* cv = second->getConstantValue(); + const bool pays = !mHad || mHad->castsToString() || mHad->dumps * mCosts.listShapeLeast > mCosts.listHelperMost; + cast = pays && cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue(); + } + else if (mCosts.castForDetail && !strcmp(sym->getName(), "llList2String") && isInteger(second, 0)) + { + LSLExpression* details = bare(first); + LSLSymbol* inner = details && details->getNodeSubType() == NODE_FUNCTION_EXPRESSION ? details->getSymbol() : nullptr; + LSLASTNodeList* asked = inner ? static_cast(details)->getArguments() : nullptr; + LSLASTNode* which = asked && asked->getNumChildren() == 2 ? asked->getChild(1) : nullptr; + LSLExpression* named = which && which->getNodeType() == NODE_EXPRESSION ? bare(static_cast(which)) : nullptr; + // One detail asked for: a literal of one, or that literal as + // the cast listadd makes of it. + const bool one = named && ((named->getNodeSubType() == NODE_LIST_EXPRESSION && named->getNumChildren() == 1) || + (named->getNodeSubType() == NODE_TYPECAST_EXPRESSION && named->getIType() == LST_LIST && + static_cast(named)->getChildExpr() && + static_cast(named)->getChildExpr()->getIType() != LST_LIST)); + cast = inner && inner->getSubType() == SYM_BUILTIN && !strcmp(inner->getName(), "llGetObjectDetails") && one; + } + if (!cast) + { + return; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + args->takeChild(0); + auto* made = ctx.allocator->newTracked(TYPE(LST_STRING), first); + made->setType(TYPE(LST_STRING)); + made->setLoc(expr->getLoc()); + putInPlace(expr, made, ctx.allocator); + wrote(expr, made, before); + } + + // Whether a constant is no key a library function would find + // anything by: NULL_KEY, or anything that is not a key's form. + static bool nullKey(LSLConstant* cv) + { + if (!cv || (cv->getIType() != LST_STRING && cv->getIType() != LST_KEY)) + { + return false; + } + const std::string_view text = cv->getIType() == LST_STRING ? static_cast(cv)->getValue() + : static_cast(cv)->getValue(); + if (text.size() != 36) + { + return true; + } + bool zero = true; + for (size_t i = 0; i < text.size(); ++i) + { + const bool dash = i == 8 || i == 13 || i == 18 || i == 23; + if (dash ? text[i] != '-' : !isxdigit(static_cast(text[i]))) + { + return true; + } + zero = zero && (dash || text[i] == '0'); + } + return zero; + } + + // A library function given NULL_KEY, or no key at all, for a key + // it looks something up by, given "" instead, which it takes the + // same way; not the two that pass a key on as it came. + void nullKeys(LSLFunctionExpression* expr) + { + LSLSymbol* sym = expr->getSymbol(); + if (!mCosts.emptyForNullKey || !sym || sym->getSubType() != SYM_BUILTIN || !sym->getFunctionDecl() || !expr->getArguments() || + !strcmp(sym->getName(), "llMessageLinked") || !strcmp(sym->getName(), "llRemoteDataReply")) + { + return; + } + std::vector nulls; + LSLASTNode* param = sym->getFunctionDecl()->getChild(0); + for (LSLASTNode* arg = expr->getArguments()->getChild(0); arg && param; arg = arg->getNext(), param = param->getNext()) + { + LSLConstant* cv = arg->getConstantValue(); + const bool empty = cv && cv->getIType() == LST_STRING && !*static_cast(cv)->getValue(); + if (param->getIType() == LST_KEY && !empty && nullKey(cv)) + { + nulls.push_back(arg); + } + } + for (LSLASTNode* arg : nulls) + { + const std::string before = report.wanted() ? render(arg) : std::string(); + auto* made = constant(ctx.string(std::string()), arg); + LSLASTNode::replaceNode(arg, made); + wrote(arg, made, before); + } + } + + // x = x + 1, x = 1 + x and x += 1 as ++x, and x = x - 1 and x -= 1 + // as --x, of an integer variable (ALLSLCosts::incrementForAssign). + // ++x gives what the assignment gave, x after it, so a value read + // of it reads the same. True where it was written. + bool increment(LSLBinaryExpression* expr) + { + if (!mCosts.incrementForAssign || expr->getIType() != LST_INTEGER) + { + return false; + } + const LSLOperator op = expr->getOperation(); + LSLExpression* target = expr->getLHS(); + LSLExpression* value = expr->getRHS(); + if (!target || !value || target->getNodeSubType() != NODE_LVALUE_EXPRESSION || static_cast(target)->getMember() || + target->getIType() != LST_INTEGER || !target->getSymbol()) + { + return false; + } + LSLSymbol* sym = target->getSymbol(); + const auto reads = [sym](LSLExpression* x) { + x = bare(x); + return x && x->getNodeSubType() == NODE_LVALUE_EXPRESSION && !static_cast(x)->getMember() && x->getSymbol() == sym; + }; + int step = 0; + if ((op == OP_ADD_ASSIGN || op == OP_SUB_ASSIGN) && isInteger(value, 1)) + { + step = op == OP_ADD_ASSIGN ? 1 : -1; + } + else if (op == OP_ASSIGN && bare(value)->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* sum = static_cast(bare(value)); + if (sum->getOperation() == OP_PLUS && ((reads(sum->getLHS()) && isInteger(sum->getRHS(), 1)) || (isInteger(sum->getLHS(), 1) && reads(sum->getRHS())))) + { + step = 1; + } + else if (sum->getOperation() == OP_MINUS && reads(sum->getLHS()) && isInteger(sum->getRHS(), 1)) + { + step = -1; + } + } + if (step == 0) + { + return false; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + auto* x = static_cast(expr->takeChild(0)); + LSLExpression* made = nullptr; + { + const Uncounted uncounted(*ctx.context); + made = uncounted.made(ctx.allocator->newTracked(x, step > 0 ? OP_PRE_INCR : OP_PRE_DECR)); + } + made->setType(TYPE(LST_INTEGER)); + made->setLoc(expr->getLoc()); + LSLASTNode::replaceNode(expr, made); + wrote(expr, made, before); + return true; + } + + // A declaration's initializer that is its type's default, left out, + // where that loads smaller: `integer x = 0;` as `integer x;`. Its + // value exactly -- a float's +0, not -0; a key's "", not NULL_KEY; a + // rotation's <0, 0, 0, 1> -- since what is left out is what LSL + // gives it. + void dropDefault(LSLASTNode* decl, LSLSymbol* sym, bool global) + { + LSLASTNode* init = decl->getChild(1); + if (!sym || !init || init->getNodeType() != NODE_EXPRESSION) + { + return; + } + const LSLIType type = sym->getIType(); + const bool drop = global ? type == LST_INTEGER && mCosts.dropIntegerGlobalDefault + : (type == LST_INTEGER && mCosts.dropIntegerDefault) || (type == LST_FLOATINGPOINT && mCosts.dropFloatDefault) || + (type == LST_KEY && mCosts.dropKeyDefault) || (type == LST_VECTOR && mCosts.dropVectorDefault) || + (type == LST_QUATERNION && mCosts.dropRotationDefault); + LSLConstant* cv = drop ? init->getConstantValue() : nullptr; + if (!cv) + { + return; + } + const auto zero = [](double v) { return v == 0.0 && !std::signbit(v); }; + bool isDefault = false; + switch (cv->getIType()) + { + case LST_INTEGER: isDefault = type == LST_INTEGER && static_cast(cv)->getValue() == 0; break; + case LST_FLOATINGPOINT: isDefault = type == LST_FLOATINGPOINT && zero(static_cast(cv)->getValue()); break; + case LST_STRING: isDefault = type == LST_KEY && !*static_cast(cv)->getValue(); break; + case LST_KEY: isDefault = type == LST_KEY && !*static_cast(cv)->getValue(); break; + case LST_VECTOR: + { + const Vector3* v = static_cast(cv)->getValue(); + isDefault = type == LST_VECTOR && v && zero(v->x) && zero(v->y) && zero(v->z); + break; + } + case LST_QUATERNION: + { + const Quaternion* q = static_cast(cv)->getValue(); + isDefault = type == LST_QUATERNION && q && zero(q->x) && zero(q->y) && zero(q->z) && q->s == 1.0f; + break; + } + default: break; + } + // An integer 0 a float is set to is its default too. + if (cv->getIType() == LST_INTEGER && type == LST_FLOATINGPOINT) + { + isDefault = static_cast(cv)->getValue() == 0; + } + if (!isDefault) + { + return; + } + decl->setChild(1, nullptr); + report.note(decl->getLoc(), "OptimizerDroppedDefault", "left out [1]'s initializer, which is its default", { sym->getName() }); + ++changes; + } + + // x++ and x-- whose value nothing reads, as ++x and --x. + void pre(LSLASTNode* node) + { + if (!mCosts.preForPost || !node || node->getNodeType() != NODE_EXPRESSION || node->getNodeSubType() != NODE_UNARY_EXPRESSION) + { + return; + } + auto* expr = static_cast(node); + const LSLOperator op = expr->getOperation(); + if (op != OP_POST_INCR && op != OP_POST_DECR) + { + return; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + expr->setOperation(op == OP_POST_INCR ? OP_PRE_INCR : OP_PRE_DECR); + wrote(expr, expr, before); + } + + // A list's element added bare: l + [a, b] as l + a + b, [a] + l as + // a + l, l + (list)x as l + x, and l += [a] as l += a. What was + // made, where anything was. + LSLExpression* elements(LSLBinaryExpression* expr) + { + if (!mCosts.elementForList || expr->getIType() != LST_LIST) + { + return nullptr; + } + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + const LSLOperator op = expr->getOperation(); + if (!left || !right || (op != OP_PLUS && op != OP_ADD_ASSIGN) || left->getIType() != LST_LIST) + { + return nullptr; + } + if (LSLExpression* made = appended(expr, left, right, op)) + { + return made; + } + // [a] + l as a + l. + LSLExpression* literal = bare(left); + if (op == OP_PLUS && right->getIType() == LST_LIST && literal->getNodeSubType() == NODE_LIST_EXPRESSION && + literal->getNumChildren() == 1 && literal->getChild(0)->getIType() != LST_LIST) + { + const std::string before = report.wanted() ? render(expr) : std::string(); + LSLASTNode* a = literal->takeChild(0); + literal->removeChild(literal->getChild(0)); + LSLASTNode::replaceNode(left, a); + wrote(expr, expr, before); + return expr; + } + return nullptr; + } + + // A list with elements added to it: those elements bare. + LSLExpression* appended(LSLBinaryExpression* expr, LSLExpression* left, LSLExpression* right, LSLOperator op) + { + LSLExpression* added = bare(right); + // l + (list)x as l + x. + if (op == OP_PLUS && added->getNodeSubType() == NODE_TYPECAST_EXPRESSION && added->getIType() == LST_LIST) + { + LSLExpression* x = static_cast(added)->getChildExpr(); + if (!x || x->getIType() == LST_LIST) + { + return nullptr; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + added->takeChild(0); + LSLASTNode::replaceNode(right, x); + wrote(expr, expr, before); + return expr; + } + if (added->getNodeSubType() != NODE_LIST_EXPRESSION || added->getNumChildren() == 0) + { + return nullptr; + } + const size_t count = added->getNumChildren(); + if ((op == OP_ADD_ASSIGN && count != 1) || (mCosts.elementsForListMost > 0 && count > size_t(mCosts.elementsForListMost))) + { + return nullptr; + } + // Each element's order as a sum's -- last first -- against a + // literal's, which is known to nobody: as for listadd. + size_t changing = 0; + size_t varying = 0; + for (LSLASTNode* child : *added) + { + if (child->getIType() == LST_LIST || child->getIType() == LST_ERROR) + { + return nullptr; + } + changing += sideEffectFree(child) ? 0 : 1; + varying += child->getConstantValue() ? 0 : 1; + } + if (changing > 0 && varying > 1) + { + return nullptr; + } + const std::string before = report.wanted() ? render(expr) : std::string(); + std::vector terms; + while (added->hasChildren()) + { + terms.push_back(static_cast(added->takeChild(0))); + added->removeChild(added->getChild(0)); + } + if (op == OP_ADD_ASSIGN) + { + LSLASTNode::replaceNode(right, terms.front()); + wrote(expr, expr, before); + return expr; + } + // l + a + b, each on the right of its own +. + auto* l = static_cast(expr->takeChild(0)); + LSLExpression* sum = l; + { + const Uncounted uncounted(*ctx.context); + for (LSLExpression* term : terms) + { + sum = uncounted.made(ctx.allocator->newTracked(sum, OP_PLUS, uncounted.made(bracketed(term)))); + sum->setType(TYPE(LST_LIST)); + sum->setLoc(term->getLoc()); + } + } + sum->setLoc(expr->getLoc()); + putInPlace(expr, sum, ctx.allocator); + wrote(expr, sum, before); + return sum; + } + + // An element of a list as a sum has it, after the first: on the + // right of a +, which binds tighter than every operator but a + // product and what is unary, and so bracketed where it is any + // other. (The first goes under the cast, which the printer + // brackets for itself.) + LSLExpression* bracketed(LSLExpression* element) + { + bool bare = false; + switch (element->getNodeSubType()) + { + case NODE_CONSTANT_EXPRESSION: + case NODE_LVALUE_EXPRESSION: + case NODE_PARENTHESIS_EXPRESSION: + case NODE_FUNCTION_EXPRESSION: + case NODE_PRINT_EXPRESSION: + case NODE_VECTOR_EXPRESSION: + case NODE_QUATERNION_EXPRESSION: + case NODE_LIST_EXPRESSION: + bare = true; + break; + case NODE_UNARY_EXPRESSION: + case NODE_TYPECAST_EXPRESSION: + bare = true; + break; + case NODE_BINARY_EXPRESSION: + bare = element->getOperation() == OP_MUL || element->getOperation() == OP_DIV || element->getOperation() == OP_MOD; + break; + default: + break; + } + if (bare) + { + return element; + } + auto* parens = ctx.allocator->newTracked(element); + parens->setType(element->getType()); + parens->setLoc(element->getLoc()); + return parens; + } + }; +} + + ListHelpers listHelpers(LSLScript* script) + { + ListHelpersFinder finder; + script->visit(&finder); + return std::move(finder.found); + } + + int shape(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, ShapeStage stage, const ListHelpers* had) + { + Shapes shapes(ctx, report, options, stage == ShapeStage::Values ? Shapes::Stage::Values : Shapes::Stage::Lists); + shapes.setHelpers(had); + script->visit(&shapes); + return shapes.changes; + } +} diff --git a/indra/alscript/lsl/optimizer/allslshapes.h b/indra/alscript/lsl/optimizer/allslshapes.h new file mode 100644 index 00000000000..1e98ed8db6a --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslshapes.h @@ -0,0 +1,81 @@ +/** + * @file allslshapes.h + * @brief The LSL optimizer's shapes made for size. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +#include + +#include + +namespace ALLSLPasses +{ + // Which of Mono's list helpers a script calls, each by what Tailslide's + // Mono compiler calls one for: a literal, an empty list, a cast to a + // list and one of a list to a string, a list's literal among the + // globals, something added to a list -- a helper for each type added + // -- and something added before one. + // The assembly references each once, which is what a list's shapes can + // cost that they do not save at each place: a helper nothing in the + // script called before. + struct ListHelpers + { + enum : U32 + { + LITERAL = 1u << 8, + EMPTY = 2u << 8, + GLOBAL_LITERAL = 3u << 8, + CAST = 4u << 8, + PREPEND = 5u << 8, + APPEND = 6u << 8, + TO_STRING = 7u << 8 + }; + + boost::unordered_flat_set used; + // Lists dumped with nothing between their elements, each a cast to + // a string it may be. + S32 dumps = 0; + + bool castsToString() const { return used.contains(TO_STRING); } + + // How many of `after`'s helpers are not among these. + S32 freshIn(const ListHelpers& after) const + { + return static_cast(std::count_if(after.used.begin(), after.used.end(), [this](U32 helper) { return !used.contains(helper); })); + } + }; + ListHelpers listHelpers(LSLScript* script); + + // Rewrites made for size alone, once the rounds are done: integers', + // conditions' and increments' shapes (Values), and lists' (Lists), + // which on Mono may bring in helpers the script did not call before + // -- `had`, where that is weighed. How many changes they made. + enum class ShapeStage : U8 + { + Values, + Lists + }; + int shape(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script, ShapeStage stage, const ListHelpers* had = nullptr); +} diff --git a/indra/alscript/lsl/optimizer/allslsignatures.cpp b/indra/alscript/lsl/optimizer/allslsignatures.cpp new file mode 100644 index 00000000000..d7e6306136c --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslsignatures.cpp @@ -0,0 +1,363 @@ +/** + * @file allslsignatures.cpp + * @brief The LSL optimizer's functions given and giving no more than they need. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslsignatures.h" + +#include + +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // Each change is one function's, its calls found again for the next: + // a parameter taken away moves every argument after it. + class Signatures : public Pass + { + public: + using Pass::Pass; + + int run(LSLScript* script) + { + for (LSLASTNode* global : *script->getGlobals()) + { + if (global->getNodeType() == NODE_GLOBAL_FUNCTION && global->getSymbol()) + { + auto* function = static_cast(global); + while (unusedParameter(script, function) || constantParameter(script, function)) + { + } + unreadResult(script, function); + } + } + return changes; + } + + private: + static bool isStatement(LSLASTNode* n, LSLNodeSubType type) + { + return n && n->getNodeType() == NODE_STATEMENT && n->getNodeSubType() == type; + } + + // Every call of a function in the script. + static std::vector callsOf(LSLScript* script, LSLSymbol* function) + { + std::vector calls; + eachNode(script, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_EXPRESSION && n->getNodeSubType() == NODE_FUNCTION_EXPRESSION && n->getSymbol() == function) + { + calls.push_back(static_cast(n)); + } + }); + return calls; + } + + static std::vector parametersOf(LSLGlobalFunction* function) + { + std::vector params; + if (LSLFunctionDec* dec = function->getArguments()) + { + for (LSLASTNode* p = dec->getChild(0); p; p = p->getNext()) + { + params.push_back(static_cast(p)); + } + } + return params; + } + + static LSLASTNode* argumentAt(LSLFunctionExpression* call, size_t at) + { + LSLASTNode* arg = call->getArguments() ? call->getArguments()->getChild(0) : nullptr; + for (size_t k = 0; arg && k < at; ++k) + { + arg = arg->getNext(); + } + return arg; + } + + // How a body names a symbol: every time, and the times it writes it. + static std::pair namings(LSLASTNode* body, LSLSymbol* sym) + { + int named = 0, written = 0; + eachNode(body, [&](LSLASTNode* n) { + if (n->getNodeType() != NODE_EXPRESSION || n->getNodeSubType() != NODE_LVALUE_EXPRESSION || n->getSymbol() != sym) + { + return; + } + ++named; + LSLASTNode* parent = n->getParent(); + written += parent && parent->getNodeType() == NODE_EXPRESSION && operation_mutates(static_cast(parent)->getOperation()) && + parent->getChild(0) == n + ? 1 + : 0; + }); + return { named, written }; + } + + // A parameter and each call's argument for it taken away. + void takeAway(LSLGlobalFunction* function, const std::vector& calls, size_t at, LSLIdentifier* param) + { + const Uncounted uncounted(*ctx.context); + for (LSLFunctionExpression* call : calls) + { + if (LSLASTNode* arg = argumentAt(call, at)) + { + call->getArguments()->removeChild(arg); + } + } + LSLSymbol* sym = param->getSymbol(); + for (LSLASTNode* up = function; up && sym; up = up->getParent()) + { + if (up->getSymbolTable() && up->getSymbolTable()->remove(sym)) + { + break; + } + } + function->getArguments()->removeChild(param); + } + + // A parameter the body never names, where what each call gives it + // does nothing. + bool unusedParameter(LSLScript* script, LSLGlobalFunction* function) + { + const std::vector params = parametersOf(function); + const std::vector calls = callsOf(script, function->getSymbol()); + for (size_t at = 0; at < params.size(); ++at) + { + LSLSymbol* sym = params[at]->getSymbol(); + if (!sym || namings(function->getStatements(), sym).first != 0) + { + continue; + } + bool quiet = true; + for (LSLFunctionExpression* call : calls) + { + LSLASTNode* arg = argumentAt(call, at); + quiet = quiet && arg && changesNothing(arg); + } + if (!quiet) + { + continue; + } + report.note(params[at]->getLoc(), "OptimizerRemovedParameter", "removed the parameter [1] of [2], which it never reads, and what its calls gave it", + { sym->getName(), function->getSymbol()->getName() }); + takeAway(function, calls, at, params[at]); + ++changes; + return true; + } + return false; + } + + // Whether two constants are the one value, bit for bit. + static bool same(LSLConstant* a, LSLConstant* b) + { + if (!a || !b || a->getNodeSubType() != b->getNodeSubType()) + { + return false; + } + const auto bits = [](double x, double y) { return x == y && std::signbit(x) == std::signbit(y); }; + switch (a->getNodeSubType()) + { + case NODE_INTEGER_CONSTANT: + return static_cast(a)->getValue() == static_cast(b)->getValue(); + case NODE_FLOAT_CONSTANT: + return bits(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_STRING_CONSTANT: + return !strcmp(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_KEY_CONSTANT: + return !strcmp(static_cast(a)->getValue(), static_cast(b)->getValue()); + case NODE_VECTOR_CONSTANT: + { + const Vector3* u = static_cast(a)->getValue(); + const Vector3* v = static_cast(b)->getValue(); + return bits(u->x, v->x) && bits(u->y, v->y) && bits(u->z, v->z); + } + case NODE_QUATERNION_CONSTANT: + { + const Quaternion* p = static_cast(a)->getValue(); + const Quaternion* q = static_cast(b)->getValue(); + return bits(p->x, q->x) && bits(p->y, q->y) && bits(p->z, q->z) && bits(p->s, q->s); + } + default: + return false; + } + } + + // A parameter every call gives the same constant, never set in the + // body: a local of that value instead, `T p = C;` ahead of the rest, + // which the folder then writes where it is read. + bool constantParameter(LSLScript* script, LSLGlobalFunction* function) + { + const std::vector params = parametersOf(function); + const std::vector calls = callsOf(script, function->getSymbol()); + LSLStatement* body = function->getStatements(); + if (calls.empty() || !isStatement(body, NODE_COMPOUND_STATEMENT)) + { + return false; + } + for (size_t at = 0; at < params.size(); ++at) + { + LSLSymbol* sym = params[at]->getSymbol(); + if (!sym || sym->getIType() == LST_LIST || namings(body, sym).second != 0) + { + continue; + } + LSLASTNode* first = argumentAt(calls.front(), at); + LSLConstant* value = first ? first->getConstantValue() : nullptr; + bool one = value && inlineable(value) && first->getIType() == sym->getIType(); + for (LSLFunctionExpression* call : calls) + { + LSLASTNode* arg = argumentAt(call, at); + one = one && arg && same(arg->getConstantValue(), value); + } + if (!one) + { + continue; + } + report.note(params[at]->getLoc(), "OptimizerConstantParameter", "made the parameter [1] of [2] a local, every call giving it the same value", + { sym->getName(), function->getSymbol()->getName() }); + // The local, a symbol of its own, named where the parameter was. + LSLType* type = sym->getType(); + const char* name = sym->getName(); + auto* id = ctx.allocator->newTracked(type, name); + id->setLoc(params[at]->getLoc()); + auto* decl = ctx.allocator->newTracked(id, static_cast(constant(value->copy(ctx.allocator), params[at]))); + decl->setLoc(params[at]->getLoc()); + auto* local = ctx.allocator->newTracked(name, type, SYM_VARIABLE, SYM_LOCAL, params[at]->getLoc(), nullptr, decl); + id->setSymbol(local); + eachNode(body, [&](LSLASTNode* n) { + if (n->getNodeType() == NODE_IDENTIFIER && static_cast(n)->getSymbol() == sym) + { + static_cast(n)->setSymbol(local); + } + }); + takeAway(function, calls, at, params[at]); + std::vector stmts{ decl }; + for (LSLASTNode* stmt = body->getChild(0); stmt; stmt = stmt->getNext()) + { + stmts.push_back(stmt); + } + setStatements(body, stmts, *ctx.context); + if (LSLSymbolTable* table = body->getSymbolTable()) + { + table->define(local); + } + else + { + decl->defineSymbol(local); + } + ++changes; + return true; + } + return false; + } + + // A function whose value no call reads, made one that returns + // nothing: each `return e;` as `e; return;`, or `return;` where e + // does nothing. + void unreadResult(LSLScript* script, LSLGlobalFunction* function) + { + LSLIdentifier* id = function->getIdentifier(); + if (!id || id->getIType() == LST_NULL) + { + return; + } + for (LSLFunctionExpression* call : callsOf(script, function->getSymbol())) + { + LSLASTNode* up = call->getParent(); + LSLASTNode* loop = up ? up->getParent() : nullptr; + const bool statement = isStatement(up, NODE_EXPRESSION_STATEMENT); + const bool step = up && up->getNodeType() == NODE_AST_NODE_LIST && isStatement(loop, NODE_FOR_STATEMENT) && + (up == static_cast(loop)->getInitExprs() || up == static_cast(loop)->getIncrExprs()); + if (!statement && !step) + { + return; + } + } + std::vector returns; + eachNode(function->getStatements(), [&](LSLASTNode* n) { + if (isStatement(n, NODE_RETURN_STATEMENT)) + { + returns.push_back(n); + } + }); + report.note(id->getLoc(), "OptimizerUnreadResult", "made [1] return nothing, as no call reads what it returns", { function->getSymbol()->getName() }); + const Uncounted uncounted(*ctx.context); + id->setType(TYPE(LST_NULL)); + for (LSLASTNode* ret : returns) + { + LSLASTNode* value = ret->getChild(0); + if (!value || value->getNodeType() != NODE_EXPRESSION) + { + continue; + } + if (changesNothing(value)) + { + ret->setChild(0, ret->newNullNode()); + continue; + } + // What it worked out, still worked out, then the return. + auto* run = ctx.allocator->newTracked(static_cast(ret->takeChild(0))); + run->setLoc(ret->getLoc()); + LSLASTNode* parent = ret->getParent(); + if (isStatement(parent, NODE_COMPOUND_STATEMENT)) + { + std::vector stmts; + for (LSLASTNode* stmt = parent->getChild(0); stmt; stmt = stmt->getNext()) + { + if (stmt == ret) + { + stmts.push_back(run); + } + stmts.push_back(stmt); + } + setStatements(parent, stmts, *ctx.context); + } + else + { + const int slot = ret->getParentSlot(); + auto* block = ctx.allocator->newTracked(nullptr); + block->setLoc(ret->getLoc()); + parent->takeChild(slot); + block->pushChild(run); + block->pushChild(ret); + parent->setChild(slot, block); + } + } + ++changes; + } + }; +} + + int trimSignatures(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + Signatures signatures(ctx, report, options); + return signatures.run(script); + } +} diff --git a/indra/alscript/lsl/optimizer/allslsignatures.h b/indra/alscript/lsl/optimizer/allslsignatures.h new file mode 100644 index 00000000000..ef796770347 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslsignatures.h @@ -0,0 +1,37 @@ +/** + * @file allslsignatures.h + * @brief The LSL optimizer's functions given and giving no more than they need. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // The script's own functions given and giving no more than their calls + // need: a parameter the function never names taken away with what each + // call gives it, where that does nothing; a parameter every call gives + // the same constant made a local of that value, for the folder; and a + // value no call reads no longer returned. How many changes it made. + int trimSignatures(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/lsl/optimizer/allslsimplifier.cpp b/indra/alscript/lsl/optimizer/allslsimplifier.cpp new file mode 100644 index 00000000000..4ba27545d47 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslsimplifier.cpp @@ -0,0 +1,566 @@ +/** + * @file allslsimplifier.cpp + * @brief The LSL optimizer's simplifier. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "allslsimplifier.h" + +#include "allslcosts.h" + +#include +#include +#include +#include + +namespace ALLSLPasses +{ +namespace +{ + // The identities, the shorter spellings, and the reshaped conditions. + class Simplifier : public ASTVisitor, public Pass + { + public: + using Pass::Pass; + + bool visit(LSLBinaryExpression* expr) override + { + visitChildren(expr); + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + if (!left || !right || expr->getIType() == LST_ERROR) + { + return false; + } + if (settled(expr)) + { + return false; + } + const LSLOperator op = expr->getOperation(); + const LSLIType type = expr->getIType(); + const bool keepLeft = left->getIType() == type; + const bool keepRight = right->getIType() == type; + const bool numeric = type == LST_INTEGER || type == LST_FLOATINGPOINT; + const auto zero = [](LSLASTNode* n) { return isInteger(n, 0) || isFloat(n, 0.0); }; + const auto one = [](LSLASTNode* n) { return isInteger(n, 1) || isFloat(n, 1.0); }; + const auto minusOne = [](LSLASTNode* n) { return isInteger(n, -1) || isFloat(n, -1.0); }; + switch (op) + { + case OP_PLUS: + // What adds nothing, by what the sum is: an empty + // string to a string, an empty list to a list, and a + // zero to an integer. A list plus anything else is that + // list with one more element -- `l + 0` appends a zero, + // and so does `(list)a + 0`, which is what a list's + // literal is written as -- and a float plus zero is not + // the float where the float is -0.0. + if (keepLeft && ((type == LST_INTEGER && zero(right)) || (type == LST_STRING && isEmptyString(right)) || + (type == LST_LIST && isEmptyList(right)))) + { + return keep(expr, 0); + } + if (keepRight && ((type == LST_INTEGER && zero(left)) || (type == LST_STRING && isEmptyString(left)) || + (type == LST_LIST && isEmptyList(left)))) + { + return keep(expr, 1); + } + // Not where the target has a + -b the smaller (Shapes). + if (numeric && keepLeft && negative(right) && !ALLSLCosts::of(options.target).plusForMinus) return resign(expr, OP_MINUS); + break; + case OP_MINUS: + // Taking zero away leaves a number as it was, -0.0 too. + if (numeric && keepLeft && zero(right)) return keep(expr, 0); + if (numeric && keepLeft && negative(right)) return resign(expr, OP_PLUS); + break; + case OP_MUL: + if (keepLeft && one(right)) return keep(expr, 0); + if (keepRight && one(left)) return keep(expr, 1); + // x * -1 is -x, of an integer INT_MIN too, of a float its + // zero's sign. + if (numeric && keepLeft && minusOne(right)) return negated(expr, 0); + if (numeric && keepRight && minusOne(left)) return negated(expr, 1); + if (type == LST_INTEGER && isInteger(right, 0) && changesNothing(left)) return become(expr, ctx.integer(0)); + if (type == LST_INTEGER && isInteger(left, 0) && changesNothing(right)) return become(expr, ctx.integer(0)); + break; + case OP_DIV: + if (keepLeft && one(right)) return keep(expr, 0); + break; + case OP_BIT_OR: + case OP_BIT_XOR: + case OP_SHIFT_LEFT: + case OP_SHIFT_RIGHT: + if (isInteger(right, 0)) return keep(expr, 0); + if ((op == OP_BIT_OR || op == OP_BIT_XOR) && isInteger(left, 0)) return keep(expr, 1); + break; + case OP_BIT_AND: + if (isInteger(right, -1)) return keep(expr, 0); + if (isInteger(left, -1)) return keep(expr, 1); + if (isInteger(right, 0) && changesNothing(left)) return become(expr, ctx.integer(0)); + if (isInteger(left, 0) && changesNothing(right)) return become(expr, ctx.integer(0)); + break; + case OP_EQ: + // `x == 0` is `!x`, and one token shorter. + if (left->getIType() == LST_INTEGER && isInteger(right, 0)) return negate(expr, 0); + if (right->getIType() == LST_INTEGER && isInteger(left, 0)) return negate(expr, 1); + break; + case OP_BOOLEAN_AND: + case OP_BOOLEAN_OR: + { + // a || 1 is 1, and a && 0 is 0, where a does nothing; a + // || 0 and a && 1 are a, where it is 1 or 0 already. + const bool any = op == OP_BOOLEAN_OR; + for (int slot = 0; slot < 2; ++slot) + { + LSLASTNode* side = expr->getChild(slot); + auto* other = static_cast(expr->getChild(1 - slot)); + LSLConstant* cv = side->getConstantValue(); + if (!cv || cv->getNodeSubType() != NODE_INTEGER_CONSTANT || other->getIType() != LST_INTEGER) + { + continue; + } + const bool set = static_cast(cv)->getValue() != 0; + const std::optional r = rangeOf(other); + if (set == any && changesNothing(other)) + { + return become(expr, ctx.integer(any ? 1 : 0)); + } + if (set != any && r && r->least >= 0.0 && r->most <= 1.0) + { + return keep(expr, 1 - slot); + } + } + condition(expr, 0); + condition(expr, 1); + break; + } + default: + break; + } + return false; + } + + bool visit(LSLUnaryExpression* expr) override + { + visitChildren(expr); + LSLExpression* child = expr->getChildExpr(); + if (!child || expr->getIType() == LST_ERROR) + { + return false; + } + LSLExpression* inner = bare(child); + if (expr->getOperation() == OP_MINUS && inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_MINUS) + { + // -(-x) + LSLExpression* x = static_cast(inner)->getChildExpr(); + if (x && x->getIType() == expr->getIType()) + { + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { render(expr), render(x) }); + inner->setChild(0, nullptr); + putInPlace(expr, x, ctx.allocator); + ++changes; + } + return false; + } + if (expr->getOperation() == OP_BOOLEAN_NOT) + { + condition(expr, 0); + child = expr->getChildExpr(); + inner = bare(child); + if (inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getIType() == LST_INTEGER) + { + // !(a != b) is a == b, whatever they are: both are + // true or false. !(a == b) is a != b only for numbers -- + // a list's != is the difference of the lengths, and + // LSO's for a string is not only one or zero -- and the + // orderings only for integers, which have no NaN. + auto* bin = static_cast(inner); + LSLOperator opposite = OP_NONE; + const auto number = [](LSLExpression* e) { return e->getIType() == LST_INTEGER || e->getIType() == LST_FLOATINGPOINT; }; + const bool ints = bin->getLHS()->getIType() == LST_INTEGER && bin->getRHS()->getIType() == LST_INTEGER; + const bool numbers = number(bin->getLHS()) && number(bin->getRHS()); + switch (bin->getOperation()) + { + case OP_EQ: opposite = numbers ? OP_NEQ : OP_NONE; break; + case OP_NEQ: opposite = OP_EQ; break; + case OP_LESS: opposite = ints ? OP_GEQ : OP_NONE; break; + case OP_GREATER: opposite = ints ? OP_LEQ : OP_NONE; break; + case OP_LEQ: opposite = ints ? OP_GREATER : OP_NONE; break; + case OP_GEQ: opposite = ints ? OP_LESS : OP_NONE; break; + default: break; + } + if (opposite != OP_NONE) + { + const std::string was = render(expr); + bin->setOperation(opposite); + inner->getParent()->takeChild(inner->getParentSlot()); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(bin) }); + putInPlace(expr, bin, ctx.allocator); + ++changes; + } + } + } + return false; + } + + bool visit(LSLTypecastExpression* expr) override + { + visitChildren(expr); + LSLExpression* child = expr->getChildExpr(); + if (child && child->getIType() == expr->getIType() && expr->getIType() != LST_ERROR) + { + report.note(expr->getLoc(), "OptimizerDroppedCast", "dropped the cast in [1]", { render(expr) }); + expr->setChild(0, nullptr); + putInPlace(expr, child, ctx.allocator); + ++changes; + return false; + } + return false; + } + + bool visit(LSLFunctionExpression* expr) override + { + visitChildren(expr); + LSLSymbol* sym = expr->getSymbol(); + if (options.listlength && ALLSLCosts::of(options.target).lengthAsNotEqual && sym && sym->getSubType() == SYM_BUILTIN && + !strcmp(sym->getName(), "llGetListLength") && + expr->getArguments()->getNumChildren() == 1) + { + // llGetListLength(x) is x != [], which is the length. + auto* arg = static_cast(expr->getArguments()->takeChild(0)); + auto* empty = ctx.allocator->newTracked(ctx.allocator->newTracked(nullptr)); + auto* test = ctx.allocator->newTracked(arg, OP_NEQ, static_cast(empty)); + test->setType(TYPE(LST_INTEGER)); + report.note(expr->getLoc(), "OptimizerWroteAs", "wrote [1] as [2]", { render(expr), render(test) }); + putInPlace(expr, test, ctx.allocator); + ++changes; + } + return false; + } + + bool visit(LSLIfStatement* stmt) override + { + visitChildren(stmt); + condition(stmt, 0); + if (!options.ifelseswap) + { + return false; + } + LSLExpression* cond = stmt->getCheckExpr(); + LSLStatement* yes = stmt->getTrueBranch(); + LSLStatement* no = stmt->getFalseBranch(); + LSLExpression* inner = bare(cond); + if (no && inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) + { + // if (!c) A else B is if (c) B else A. + LSLExpression* c = static_cast(inner)->getChildExpr(); + if (c) + { + inner->setChild(0, nullptr); + stmt->setCheckExpr(c); + swapBranches(stmt); + report.note(stmt->getLoc(), "OptimizerSwappedBranches", "swapped the branches of if ([1]) and dropped the !", { render(c) }); + ++changes; + } + return false; + } + if (no && empty(yes) && cond->getIType() == LST_INTEGER) + { + // if (c) ; else B is if (!c) B: !(c) where c is an + // operation, which the ! would take only the first operand + // of otherwise. + stmt->setCheckExpr(nullptr); + LSLExpression* operand = cond; + if (cond->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + auto* parens = ctx.allocator->newTracked(cond); + parens->setType(cond->getType()); + parens->setLoc(cond->getLoc()); + operand = parens; + } + auto* negated = ctx.allocator->newTracked(operand, OP_BOOLEAN_NOT); + negated->setType(TYPE(LST_INTEGER)); + negated->setLoc(cond->getLoc()); + stmt->setCheckExpr(negated); + stmt->setTrueBranch(nullptr); + stmt->setFalseBranch(nullptr); + stmt->setTrueBranch(no); + report.note(stmt->getLoc(), "OptimizerTurnedEmptyBranch", "turned an empty if branch around"); + ++changes; + } + return false; + } + + bool visit(LSLWhileStatement* stmt) override + { + visitChildren(stmt); + condition(stmt, 0); + return false; + } + + bool visit(LSLDoStatement* stmt) override + { + visitChildren(stmt); + condition(stmt, 1); + return false; + } + + bool visit(LSLForStatement* stmt) override + { + visitChildren(stmt); + condition(stmt, 1); + return false; + } + + private: + // A comparison of numbers settled by what each side can be: never + // below nought is never below -1, and a random float of a positive + // magnitude never below nought. Only where neither side changes + // anything, since neither is run any more. + bool settled(LSLBinaryExpression* expr) + { + const LSLOperator op = expr->getOperation(); + if (op != OP_LESS && op != OP_GREATER && op != OP_LEQ && op != OP_GEQ && op != OP_EQ && op != OP_NEQ) + { + return false; + } + LSLExpression* left = expr->getLHS(); + LSLExpression* right = expr->getRHS(); + const auto number = [](LSLExpression* x) { return x->getIType() == LST_INTEGER || x->getIType() == LST_FLOATINGPOINT; }; + if (!number(left) || !number(right) || expr->getConstantValue() || !changesNothing(left) || !changesNothing(right)) + { + return false; + } + const std::optional a = rangeOf(left); + const std::optional b = rangeOf(right); + if (!a || !b) + { + return false; + } + std::optional is; + const bool apart = a->most < b->least || b->most < a->least; + switch (op) + { + case OP_LESS: is = a->most < b->least ? std::optional(true) : a->least >= b->most ? std::optional(false) : std::nullopt; break; + case OP_LEQ: is = a->most <= b->least ? std::optional(true) : a->least > b->most ? std::optional(false) : std::nullopt; break; + case OP_GREATER: is = a->least > b->most ? std::optional(true) : a->most <= b->least ? std::optional(false) : std::nullopt; break; + case OP_GEQ: is = a->least >= b->most ? std::optional(true) : a->most < b->least ? std::optional(false) : std::nullopt; break; + case OP_EQ: is = apart ? std::optional(false) : std::nullopt; break; + default: is = apart ? std::optional(true) : std::nullopt; break; + } + if (!is) + { + return false; + } + fold(expr, ctx.integer(*is ? 1 : 0), "OptimizerSettled", "settled"); + return true; + } + + static bool empty(LSLStatement* s) + { + if (!s) + { + return true; + } + if (s->getNodeSubType() == NODE_NOP_STATEMENT) + { + return true; + } + return s->getNodeSubType() == NODE_COMPOUND_STATEMENT && !s->hasChildren(); + } + + static void swapBranches(LSLIfStatement* stmt) + { + LSLStatement* yes = stmt->getTrueBranch(); + LSLStatement* no = stmt->getFalseBranch(); + stmt->setTrueBranch(nullptr); + stmt->setFalseBranch(nullptr); + stmt->setTrueBranch(no); + stmt->setFalseBranch(yes); + } + + // A constant below zero, whose sign can move onto the operator. + static bool negative(LSLASTNode* n) + { + LSLConstant* cv = n->getConstantValue(); + if (!cv) + { + return false; + } + if (cv->getNodeSubType() == NODE_INTEGER_CONSTANT) + { + const int v = static_cast(cv)->getValue(); + return v < 0 && v != INT32_MIN; + } + if (cv->getNodeSubType() == NODE_FLOAT_CONSTANT) + { + return static_cast(cv)->getValue() < 0.0; + } + return false; + } + + // The expression becomes one of its operands. + bool keep(LSLBinaryExpression* expr, int slot) + { + const std::string was = render(expr); + auto* kept = static_cast(expr->takeChild(slot)); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(kept) }); + putInPlace(expr, kept, ctx.allocator); + ++changes; + return false; + } + + bool become(LSLBinaryExpression* expr, LSLConstant* cv) + { + fold(expr, cv, "OptimizerSimplified", "simplified"); + return false; + } + + // x + -c as x - c, x - -c as x + c. + bool resign(LSLBinaryExpression* expr, LSLOperator op) + { + const std::string was = render(expr); + LSLConstant* cv = expr->getRHS()->getConstantValue(); + LSLConstant* pos = cv->getNodeSubType() == NODE_INTEGER_CONSTANT + ? ctx.integer(-static_cast(cv)->getValue()) + : ctx.allocator->newTracked(-static_cast(cv)->getValue()); + expr->setOperation(op); + expr->setRHS(constant(pos, expr->getRHS())); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(expr) }); + ++changes; + return false; + } + + // The expression becomes its operand in `slot` negated: x * -1 as + // -x, x bracketed but where it is one token or bracketed already. + bool negated(LSLBinaryExpression* expr, int slot) + { + const std::string was = render(expr); + auto* x = static_cast(expr->takeChild(slot)); + LSLExpression* operand = x; + switch (x->getNodeSubType()) + { + case NODE_LVALUE_EXPRESSION: + case NODE_FUNCTION_EXPRESSION: + case NODE_PARENTHESIS_EXPRESSION: + case NODE_TYPECAST_EXPRESSION: + break; + default: + operand = ctx.allocator->newTracked(x); + operand->setType(x->getType()); + break; + } + auto* minus = ctx.allocator->newTracked(operand, OP_MINUS); + minus->setType(x->getType()); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(minus) }); + putInPlace(expr, minus, ctx.allocator); + ++changes; + return false; + } + + // x == 0 as !x. + bool negate(LSLBinaryExpression* expr, int slot) + { + const std::string was = render(expr); + auto* x = static_cast(expr->takeChild(slot)); + LSLExpression* operand = x; + if (x->getNodeSubType() == NODE_BINARY_EXPRESSION) + { + operand = ctx.allocator->newTracked(x); + operand->setType(x->getType()); + } + auto* negated = ctx.allocator->newTracked(operand, OP_BOOLEAN_NOT); + negated->setType(TYPE(LST_INTEGER)); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(negated) }); + putInPlace(expr, negated, ctx.allocator); + ++changes; + return false; + } + + // A child that is only ever true or false: `x != 0` is `x`, and + // `!!x` is `x`. + void condition(LSLASTNode* parent, int slot) + { + LSLASTNode* child = parent->getChild(slot); + if (!child || child->getNodeType() != NODE_EXPRESSION) + { + return; + } + auto* expr = static_cast(child); + LSLExpression* inner = bare(expr); + // a | c, c not nought, is true, where a does nothing. + if (inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getOperation() == OP_BIT_OR && inner->getIType() == LST_INTEGER) + { + for (int slot = 0; slot < 2; ++slot) + { + LSLConstant* cv = inner->getChild(slot)->getConstantValue(); + if (cv && cv->getNodeSubType() == NODE_INTEGER_CONSTANT && static_cast(cv)->getValue() != 0 && + changesNothing(inner->getChild(1 - slot))) + { + fold(expr, ctx.integer(1), "OptimizerSimplified", "simplified"); + return; + } + } + } + if (inner->getNodeSubType() == NODE_BINARY_EXPRESSION && inner->getOperation() == OP_NEQ) + { + auto* bin = static_cast(inner); + int keepSlot = -1; + if (bin->getLHS()->getIType() == LST_INTEGER && isInteger(bin->getRHS(), 0)) keepSlot = 0; + else if (bin->getRHS()->getIType() == LST_INTEGER && isInteger(bin->getLHS(), 0)) keepSlot = 1; + if (keepSlot >= 0) + { + const std::string was = render(expr); + auto* kept = static_cast(bin->takeChild(keepSlot)); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(kept) }); + LSLASTNode::replaceNode(expr, kept); + ++changes; + return; + } + } + if (inner->getNodeSubType() == NODE_UNARY_EXPRESSION && inner->getOperation() == OP_BOOLEAN_NOT) + { + LSLExpression* once = bare(static_cast(inner)->getChildExpr()); + if (once && once->getNodeSubType() == NODE_UNARY_EXPRESSION && once->getOperation() == OP_BOOLEAN_NOT) + { + LSLExpression* x = static_cast(once)->getChildExpr(); + if (x && x->getIType() == LST_INTEGER) + { + const std::string was = render(expr); + once->setChild(0, nullptr); + report.note(expr->getLoc(), "OptimizerSimplified", "simplified [1] to [2]", { was, render(x) }); + LSLASTNode::replaceNode(expr, x); + ++changes; + } + } + } + } + }; +} + + int simplify(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script) + { + Simplifier simplifier(ctx, report, options); + script->visit(&simplifier); + return simplifier.changes; + } +} diff --git a/indra/alscript/lsl/optimizer/allslsimplifier.h b/indra/alscript/lsl/optimizer/allslsimplifier.h new file mode 100644 index 00000000000..87f794393a8 --- /dev/null +++ b/indra/alscript/lsl/optimizer/allslsimplifier.h @@ -0,0 +1,34 @@ +/** + * @file allslsimplifier.h + * @brief The LSL optimizer's simplifier. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "allsloptimizerpass.h" + +namespace ALLSLPasses +{ + // The identities, the shorter spellings, and the reshaped conditions. + // How many changes it made. + int simplify(Ctx& ctx, Report& report, const ALLSLOptimizer::Options& options, LSLScript* script); +} diff --git a/indra/alscript/alluauconfig.cpp b/indra/alscript/luau/alluauconfig.cpp similarity index 100% rename from indra/alscript/alluauconfig.cpp rename to indra/alscript/luau/alluauconfig.cpp diff --git a/indra/alscript/alluauconfig.h b/indra/alscript/luau/alluauconfig.h similarity index 100% rename from indra/alscript/alluauconfig.h rename to indra/alscript/luau/alluauconfig.h diff --git a/indra/alscript/alluauexports.cpp b/indra/alscript/luau/alluauexports.cpp similarity index 100% rename from indra/alscript/alluauexports.cpp rename to indra/alscript/luau/alluauexports.cpp diff --git a/indra/alscript/alluauexports.h b/indra/alscript/luau/alluauexports.h similarity index 100% rename from indra/alscript/alluauexports.h rename to indra/alscript/luau/alluauexports.h diff --git a/indra/alscript/alluauservice.cpp b/indra/alscript/luau/alluauservice.cpp similarity index 100% rename from indra/alscript/alluauservice.cpp rename to indra/alscript/luau/alluauservice.cpp diff --git a/indra/alscript/alluauservice.h b/indra/alscript/luau/alluauservice.h similarity index 100% rename from indra/alscript/alluauservice.h rename to indra/alscript/luau/alluauservice.h diff --git a/indra/alscript/aldiskcache.cpp b/indra/alscript/preprocessor/aldiskcache.cpp similarity index 100% rename from indra/alscript/aldiskcache.cpp rename to indra/alscript/preprocessor/aldiskcache.cpp diff --git a/indra/alscript/aldiskcache.h b/indra/alscript/preprocessor/aldiskcache.h similarity index 100% rename from indra/alscript/aldiskcache.h rename to indra/alscript/preprocessor/aldiskcache.h diff --git a/indra/alscript/aldiskincludes.cpp b/indra/alscript/preprocessor/aldiskincludes.cpp similarity index 100% rename from indra/alscript/aldiskincludes.cpp rename to indra/alscript/preprocessor/aldiskincludes.cpp diff --git a/indra/alscript/aldiskincludes.h b/indra/alscript/preprocessor/aldiskincludes.h similarity index 100% rename from indra/alscript/aldiskincludes.h rename to indra/alscript/preprocessor/aldiskincludes.h diff --git a/indra/alscript/alincludeidentity.cpp b/indra/alscript/preprocessor/alincludeidentity.cpp similarity index 100% rename from indra/alscript/alincludeidentity.cpp rename to indra/alscript/preprocessor/alincludeidentity.cpp diff --git a/indra/alscript/alincludeidentity.h b/indra/alscript/preprocessor/alincludeidentity.h similarity index 100% rename from indra/alscript/alincludeidentity.h rename to indra/alscript/preprocessor/alincludeidentity.h diff --git a/indra/alscript/alincludesearch.cpp b/indra/alscript/preprocessor/alincludesearch.cpp similarity index 100% rename from indra/alscript/alincludesearch.cpp rename to indra/alscript/preprocessor/alincludesearch.cpp diff --git a/indra/alscript/alincludesearch.h b/indra/alscript/preprocessor/alincludesearch.h similarity index 100% rename from indra/alscript/alincludesearch.h rename to indra/alscript/preprocessor/alincludesearch.h diff --git a/indra/alscript/almodulelook.cpp b/indra/alscript/preprocessor/almodulelook.cpp similarity index 100% rename from indra/alscript/almodulelook.cpp rename to indra/alscript/preprocessor/almodulelook.cpp diff --git a/indra/alscript/almodulelook.h b/indra/alscript/preprocessor/almodulelook.h similarity index 100% rename from indra/alscript/almodulelook.h rename to indra/alscript/preprocessor/almodulelook.h diff --git a/indra/alscript/alpreprocessor.cpp b/indra/alscript/preprocessor/alpreprocessor.cpp similarity index 100% rename from indra/alscript/alpreprocessor.cpp rename to indra/alscript/preprocessor/alpreprocessor.cpp diff --git a/indra/alscript/alpreprocessor.h b/indra/alscript/preprocessor/alpreprocessor.h similarity index 100% rename from indra/alscript/alpreprocessor.h rename to indra/alscript/preprocessor/alpreprocessor.h diff --git a/indra/alscript/alscriptenvelope.cpp b/indra/alscript/preprocessor/alscriptenvelope.cpp similarity index 100% rename from indra/alscript/alscriptenvelope.cpp rename to indra/alscript/preprocessor/alscriptenvelope.cpp diff --git a/indra/alscript/alscriptenvelope.h b/indra/alscript/preprocessor/alscriptenvelope.h similarity index 100% rename from indra/alscript/alscriptenvelope.h rename to indra/alscript/preprocessor/alscriptenvelope.h diff --git a/indra/alscript/alscriptjoblane.h b/indra/alscript/preprocessor/alscriptjoblane.h similarity index 100% rename from indra/alscript/alscriptjoblane.h rename to indra/alscript/preprocessor/alscriptjoblane.h diff --git a/indra/alscript/alscriptsnapshot.cpp b/indra/alscript/preprocessor/alscriptsnapshot.cpp similarity index 100% rename from indra/alscript/alscriptsnapshot.cpp rename to indra/alscript/preprocessor/alscriptsnapshot.cpp diff --git a/indra/alscript/alscriptsnapshot.h b/indra/alscript/preprocessor/alscriptsnapshot.h similarity index 100% rename from indra/alscript/alscriptsnapshot.h rename to indra/alscript/preprocessor/alscriptsnapshot.h diff --git a/indra/alscript/alscripttextcache.cpp b/indra/alscript/preprocessor/alscripttextcache.cpp similarity index 100% rename from indra/alscript/alscripttextcache.cpp rename to indra/alscript/preprocessor/alscripttextcache.cpp diff --git a/indra/alscript/alscripttextcache.h b/indra/alscript/preprocessor/alscripttextcache.h similarity index 100% rename from indra/alscript/alscripttextcache.h rename to indra/alscript/preprocessor/alscripttextcache.h diff --git a/indra/alscript/alsourcemap.cpp b/indra/alscript/preprocessor/alsourcemap.cpp similarity index 100% rename from indra/alscript/alsourcemap.cpp rename to indra/alscript/preprocessor/alsourcemap.cpp diff --git a/indra/alscript/alsourcemap.h b/indra/alscript/preprocessor/alsourcemap.h similarity index 100% rename from indra/alscript/alsourcemap.h rename to indra/alscript/preprocessor/alsourcemap.h diff --git a/indra/alscript/tests/aldiskcache_test.cpp b/indra/alscript/tests/aldiskcache_test.cpp index 687a6ec7be9..c23183e32fa 100644 --- a/indra/alscript/tests/aldiskcache_test.cpp +++ b/indra/alscript/tests/aldiskcache_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../aldiskcache.h" +#include "../preprocessor/aldiskcache.h" #include "fsyspath.h" #include "llfile.h" diff --git a/indra/alscript/tests/aldiskincludes_test.cpp b/indra/alscript/tests/aldiskincludes_test.cpp index b207b2399bc..c1ecf3fff39 100644 --- a/indra/alscript/tests/aldiskincludes_test.cpp +++ b/indra/alscript/tests/aldiskincludes_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../aldiskincludes.h" +#include "../preprocessor/aldiskincludes.h" #include "fsyspath.h" diff --git a/indra/alscript/tests/alincludeidentity_test.cpp b/indra/alscript/tests/alincludeidentity_test.cpp index 0e67e5e9ecc..72edc93253b 100644 --- a/indra/alscript/tests/alincludeidentity_test.cpp +++ b/indra/alscript/tests/alincludeidentity_test.cpp @@ -23,7 +23,7 @@ #include "linden_common.h" -#include "../alincludeidentity.h" +#include "../preprocessor/alincludeidentity.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alincludesearch_test.cpp b/indra/alscript/tests/alincludesearch_test.cpp index 930a633e316..64a4ba9466b 100644 --- a/indra/alscript/tests/alincludesearch_test.cpp +++ b/indra/alscript/tests/alincludesearch_test.cpp @@ -23,9 +23,9 @@ #include "linden_common.h" -#include "../alincludesearch.h" +#include "../preprocessor/alincludesearch.h" -#include "../alincludeidentity.h" +#include "../preprocessor/alincludeidentity.h" #include "fsyspath.h" #include "llfile.h" diff --git a/indra/alscript/tests/allslconsts_test.cpp b/indra/alscript/tests/allslconsts_test.cpp index 6e3edd37ad8..4c5c93ae297 100644 --- a/indra/alscript/tests/allslconsts_test.cpp +++ b/indra/alscript/tests/allslconsts_test.cpp @@ -24,9 +24,9 @@ #include "linden_common.h" -#include "../allslconsts.h" -#include "../allslservice.h" -#include "../alpreprocessor.h" +#include "../lsl/allslconsts.h" +#include "../lsl/allslservice.h" +#include "../preprocessor/alpreprocessor.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/allslcosts_test.cpp b/indra/alscript/tests/allslcosts_test.cpp index 1ebd41ee35c..eea81dc9efa 100644 --- a/indra/alscript/tests/allslcosts_test.cpp +++ b/indra/alscript/tests/allslcosts_test.cpp @@ -33,10 +33,10 @@ #include "linden_common.h" -#include "../allslcosts.h" +#include "../lsl/optimizer/allslcosts.h" -#include "../allslservice.h" -#include "../alscriptweight.h" +#include "../lsl/allslservice.h" +#include "../core/alscriptweight.h" #include "../test/lltut.h" @@ -200,15 +200,15 @@ namespace tut &ALLSLCosts::complementForNotMinusOne, true }, { "a find > -1 as ~find", { "", "if (llSubStringIndex(s, \"a\") > -1) i = 2;" }, { "", "if (~llSubStringIndex(s, \"a\")) i = 2;" }, "<<>", &ALLSLCosts::complementForNotMinusOne, true }, - { "x == -1 as !~x", { "", "i = j == -1;" }, { "", "i = !~j;" }, "<=>", &ALLSLCosts::notComplementForMinusOne }, - { "x == -1 as !~x, as a condition", { "", "if (j == -1) i = 2;" }, { "", "if (!~j) i = 2;" }, "<=>", &ALLSLCosts::notComplementForMinusOne }, + { "x == -1 as !~x", { "", "i = j == -1;" }, { "", "i = !~j;" }, "<<>", &ALLSLCosts::notComplementForMinusOne }, + { "x == -1 as !~x, as a condition", { "", "if (j == -1) i = 2;" }, { "", "if (!~j) i = 2;" }, "<<>", &ALLSLCosts::notComplementForMinusOne }, // Luau's answer turns on the operand -- a call here, a local above // -- and the flag keeps to the local's. - { "a find < 0 as !~find", { "", "if (llSubStringIndex(s, \"a\") < 0) i = 2;" }, { "", "if (!~llSubStringIndex(s, \"a\")) i = 2;" }, "<><", + { "a find < 0 as !~find", { "", "if (llSubStringIndex(s, \"a\") < 0) i = 2;" }, { "", "if (!~llSubStringIndex(s, \"a\")) i = 2;" }, "<<<", nullptr }, // One and two either way. - { "x + 1 as -~x", { "", "i = j + 1;" }, { "", "i = -~j;" }, "<=>", &ALLSLCosts::negateComplementForIncrement }, - { "x + 2 as -~-~x", { "", "i = j + 2;" }, { "", "i = -~-~j;" }, "<>>", &ALLSLCosts::negateComplementForIncrement }, + { "x + 1 as -~x", { "", "i = j + 1;" }, { "", "i = -~j;" }, "<<>", &ALLSLCosts::negateComplementForIncrement }, + { "x + 2 as -~-~x", { "", "i = j + 2;" }, { "", "i = -~-~j;" }, "<<>", &ALLSLCosts::negateComplementForIncrement }, { "x - 1 as ~-x", { "", "i = j - 1;" }, { "", "i = ~-j;" }, "<<>", &ALLSLCosts::complementNegateForDecrement }, { "x - 2 as ~-~-x", { "", "i = j - 2;" }, { "", "i = ~-~-j;" }, "<<>", &ALLSLCosts::complementNegateForDecrement }, // An increment whose value goes unused. @@ -236,7 +236,7 @@ namespace tut { "a >= 5 as a > 4", { "", "if (j >= 5) i = 2;" }, { "", "if (j > 4) i = 2;" }, "=<=", &ALLSLCosts::strictForInclusive }, { "a <= 5 as a < 6", { "", "if (j <= 5) i = 2;" }, { "", "if (j < 6) i = 2;" }, "=<=", &ALLSLCosts::strictForInclusive }, { "a || b as a | b", { "", "while (i || j) i--;" }, { "", "while (i | j) i--;" }, "=<>", &ALLSLCosts::bitOrForOr }, - { "a <= b as a < b + 1, of a variable", { "", "i = j <= i;" }, { "", "i = j < i + 1;" }, "><=", nullptr }, + { "a <= b as a < b + 1, of a variable", { "", "i = j <= i;" }, { "", "i = j < i + 1;" }, ">>=", nullptr }, // The library as casts. { "llDumpList2String(l, \"\") as (string)l", { "", "s = llDumpList2String(l, \"\");" }, { "", "s = (string)l;" }, "<<<", &ALLSLCosts::castForDump }, @@ -244,20 +244,140 @@ namespace tut { "", "s = (string)llGetObjectDetails(k, [OBJECT_NAME]);" }, "<<<", &ALLSLCosts::castForDetail }, // Not taken: a key's is NULL_KEY from an empty list, an integer's no 0 of a key. { "llList2Key(llGetObjectDetails(k, [X]), 0) as (key)(string)", { "", "k = llList2Key(llGetObjectDetails(k, [OBJECT_OWNER]), 0);" }, - { "", "k = (key)((string)llGetObjectDetails(k, [OBJECT_OWNER]));" }, "<>>", nullptr }, + { "", "k = (key)((string)llGetObjectDetails(k, [OBJECT_OWNER]));" }, "<<>", nullptr }, { "a whole float in a list as ((float)2)", { "", "l = [2.0];" }, { "", "l = [(float)2];" }, "><=", &ALLSLCosts::castForWholeFloat }, { "a whole float cast as ((float)2)", { "", "s = (string)2.0;" }, { "", "s = (string)((float)2);" }, "><=", &ALLSLCosts::castForWholeFloat }, // To come. - { "i = i + 1 as ++i", { "", "i = i + 1;" }, { "", "++i;" }, "===", nullptr }, - { "-5 as ((integer)-5)", { "", "i = -5;" }, { "", "i = ((integer)-5);" }, "===", nullptr }, - { "-5.5 as ((float)-5.5)", { "", "f = -5.5;" }, { "", "f = ((float)-5.5);" }, "=<>", nullptr }, - { "integer x = 0 as integer x", { "", "integer z = 0; i = z;" }, { "", "integer z; i = z;" }, "===", nullptr }, + // Bit tests of one value as one. + { "(a & 4) && (a & 8) as !~(a | -13)", { "", "if ((j & 4) && (j & 8)) i = 2;" }, { "", "if (!~(j | -13)) i = 2;" }, "<<<", + &ALLSLCosts::bitTestsMerged }, + { "the same, a value", { "", "i = (j & 4) && (j & 8);" }, { "", "i = !~(j | -13);" }, "<<<", &ALLSLCosts::bitTestsMerged }, + { "three bits", { "", "if ((j & 4) && (j & 8) && (j & 1)) i = 2;" }, { "", "if (!~(j | -14)) i = 2;" }, "<<<", &ALLSLCosts::bitTestsMerged }, + { "!(a & 4) && !(a & 8) as !(a & 12)", { "", "if (!(j & 4) && !(j & 8)) i = 2;" }, { "", "if (!(j & 12)) i = 2;" }, "<<<", + &ALLSLCosts::bitTestsMerged }, + { "(a & 4) || (a & 8) as a & 12, only truth asked", { "", "if ((j & 4) || (j & 8)) i = 2;" }, { "", "if (j & 12) i = 2;" }, "<<<", + &ALLSLCosts::bitTestsMerged }, + { "(a & 4) | (a & 8) as a & 12", { "", "i = (j & 4) | (j & 8);" }, { "", "i = j & 12;" }, "<<<", &ALLSLCosts::bitTestsMerged }, + { "i = i + 1 as ++i", { "", "i = i + 1;" }, { "", "++i;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "i = 1 + i as ++i", { "", "i = 1 + i;" }, { "", "++i;" }, "=<<", &ALLSLCosts::incrementForAssign }, + { "i = i - 1 as --i", { "", "i = i - 1;" }, { "", "--i;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "i += 1 as ++i", { "", "i += 1;" }, { "", "++i;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "i -= 1 as --i", { "", "i -= 1;" }, { "", "--i;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "j = i = i + 1 as j = ++i", { "", "j = i = i + 1;" }, { "", "j = ++i;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "a global's gi = gi + 1 as ++gi", { "", "gi = gi + 1;" }, { "", "++gi;" }, "=<=", &ALLSLCosts::incrementForAssign }, + { "a for's i = i + 1 as ++i", { "", "for (i = 0; i < 3; i = i + 1) j = i;" }, { "", "for (i = 0; i < 3; ++i) j = i;" }, "=<=", + &ALLSLCosts::incrementForAssign }, + // Not taken: a float's, and against -~x, which Mono writes x + 1 as. + { "f = f + 1 as ++f", { "", "f = f + 1;" }, { "", "++f;" }, "=>=", nullptr }, + { "i = -~i as ++i", { "", "i = -~i;" }, { "", "++i;" }, ">=<", nullptr }, + { "-5 as ((integer)-5)", { "", "i = -5;" }, { "", "i = ((integer)-5);" }, "=<=", &ALLSLCosts::castForNegative }, + { "-5 as an argument", { "", "i = llAbs(-5);" }, { "", "i = llAbs(((integer)-5));" }, "=<=", &ALLSLCosts::castForNegative }, + { "x * -2", { "", "i = j * -2;" }, { "", "i = j * ((integer)-2);" }, "=<>", &ALLSLCosts::castForNegative }, + { "-3 in a comparison", { "", "if (j == -3) i = 2;" }, { "", "if (j == ((integer)-3)) i = 2;" }, "=<=", &ALLSLCosts::castForNegative }, + { "-5 in a list", { "", "l = [-5];" }, { "", "l = [((integer)-5)];" }, "=<=", &ALLSLCosts::castForNegative }, + { "-2147483648", { "", "i = -2147483648;" }, { "", "i = ((integer)-2147483648);" }, "=<=", &ALLSLCosts::castForNegative }, + { "-0x10", { "", "i = -0x10;" }, { "", "i = ((integer)-0x10);" }, "=<=", &ALLSLCosts::castForNegative }, + { "-5.5 as ((float)-5.5)", { "", "f = -5.5;" }, { "", "f = ((float)-5.5);" }, "=<>", &ALLSLCosts::castForNegative }, + { "-5.5 as an argument", { "", "f = llFabs(-5.5);" }, { "", "f = llFabs(((float)-5.5));" }, "=<>", &ALLSLCosts::castForNegative }, + { "a vector's negative part", { "", "v = <-1.5, 0, 0>;" }, { "", "v = <((float)-1.5), 0, 0>;" }, "=<>", &ALLSLCosts::castForNegative }, + { "integer x = 0 as integer x", { "", "integer z = 0; i = z;" }, { "", "integer z; i = z;" }, "=<=", &ALLSLCosts::dropIntegerDefault }, + { "float x = 0 as float x", { "", "float z = 0; f = z;" }, { "", "float z; f = z;" }, "<>=", &ALLSLCosts::dropFloatDefault }, + { "key x = \"\" as key x", { "", "key z = \"\"; k = z;" }, { "", "key z; k = z;" }, "<=<", &ALLSLCosts::dropKeyDefault }, + { "key x = (key)\"\" as key x", { "", "key z = (key)\"\"; k = z;" }, { "", "key z; k = z;" }, "<=<", &ALLSLCosts::dropKeyDefault }, + { "vector x = ZERO_VECTOR as vector x", { "", "vector z = ZERO_VECTOR; v = z;" }, { "", "vector z; v = z;" }, "<==", + &ALLSLCosts::dropVectorDefault }, + { "vector x = <0, 0, 0> as vector x", { "", "vector z = <0, 0, 0>; v = z;" }, { "", "vector z; v = z;" }, "<>=", &ALLSLCosts::dropVectorDefault }, + { "rotation x = ZERO_ROTATION as rotation x", { "", "rotation z = ZERO_ROTATION; v = llRot2Euler(z);" }, + { "", "rotation z; v = llRot2Euler(z);" }, "<==", &ALLSLCosts::dropRotationDefault }, + { "rotation x = <0, 0, 0, 1> as rotation x", { "", "rotation z = <0, 0, 0, 1>; v = llRot2Euler(z);" }, + { "", "rotation z; v = llRot2Euler(z);" }, "<>=", &ALLSLCosts::dropRotationDefault }, + { "integer global = 0 as integer global", { "integer z0 = 0;", "i = z0;" }, { "integer z0;", "i = z0;" }, "=<=", + &ALLSLCosts::dropIntegerGlobalDefault }, + // Never smaller: a float written 0.0, a string's, a list's; and of + // globals but an integer, none. + { "float x = 0.0 as float x", { "", "float z = 0.0; f = z;" }, { "", "float z; f = z;" }, "===", nullptr }, + { "string x = \"\" as string x", { "", "string z = \"\"; s = z;" }, { "", "string z; s = z;" }, "===", nullptr }, + { "list x = [] as list x", { "", "list z = []; l = z;" }, { "", "list z; l = z;" }, "===", nullptr }, + { "float global = 0 as float global", { "float z0 = 0;", "f = z0;" }, { "float z0;", "f = z0;" }, "=>=", nullptr }, + { "string global = \"\" as string global", { "string z0 = \"\";", "s = z0;" }, { "string z0;", "s = z0;" }, "===", nullptr }, + { "list global = [] as list global", { "list z0 = [];", "l = z0;" }, { "list z0;", "l = z0;" }, "===", nullptr }, { "a && b as !(!a | !b)", { "", "if (i && j) i = 2;" }, { "", "if (!(!i | !j)) i = 2;" }, ">=>", nullptr }, { "NULL_KEY as \"\"", { "", "k = llGetOwnerKey(NULL_KEY);" }, { "", "k = llGetOwnerKey(\"\");" }, "<<<", &ALLSLCosts::emptyForNullKey }, { "llStringLength(s) as s != \"\"", { "", "if (llStringLength(s)) i = 2;" }, { "", "if (s != \"\") i = 2;" }, "<>>", &ALLSLCosts::emptyForLength }, { "llDumpList2String(l, \"\") as (string)l", { "", "s = llDumpList2String(l, \"\");" }, { "", "s = (string)l;" }, "<<<", nullptr }, { "if (!a) A else B as if (a) B else A", { "", "if (!j) i = 2; else i = 3;" }, { "", "if (j) i = 3; else i = 2;" }, "<<<", nullptr }, { "a trailing return;", { "integer i0; f1() { i0 = 1; return; }", "f1();" }, { "integer i0; f1() { i0 = 1; }", "f1();" }, "===", nullptr }, + // Operators another way. + { "a && b of truths as a & b", { "", "if (j < i && i > 2) i = 2;" }, { "", "if (j < i & i > 2) i = 2;" }, "=<>", &ALLSLCosts::bitAndForAnd }, + { "the same, a value", { "", "i = j < i && i > 2;" }, { "", "i = j < i & i > 2;" }, "=<>", &ALLSLCosts::bitAndForAnd }, + { "a - b as a + -b", { "", "i = j - i;" }, { "", "i = j + -i;" }, "><>", &ALLSLCosts::plusForMinus }, + { "a - 5 as a + -5", { "", "i = j - 5;" }, { "", "i = j + -5;" }, "><=", &ALLSLCosts::plusForMinus }, + { "f - g as f + -g", { "", "f = f - gf;" }, { "", "f = f + -gf;" }, "><>", &ALLSLCosts::plusForMinus }, + { "x -= y as x += -y", { "", "i -= j;" }, { "", "i += -j;" }, "><>", &ALLSLCosts::plusForMinus }, + { "x << 3 as x * 8", { "", "i = j << 3;" }, { "", "i = j * 8;" }, "><<", &ALLSLCosts::productForShift }, + { "-1 - x as ~x", { "", "i = -1 - j;" }, { "", "i = ~j;" }, "<<>", &ALLSLCosts::complementForMinusOneLess }, + { "x * 2 as x + x", { "", "i = j * 2;" }, { "", "i = j + j;" }, "=<<", &ALLSLCosts::sumForDouble }, + { "f * 2 as f + f", { "", "f = f * 2;" }, { "", "f = f + f;" }, "=<<", &ALLSLCosts::sumForDouble }, + { "f / 4 as f * 0.25", { "", "f = f / 4;" }, { "", "f = f * 0.25;" }, "=<=", &ALLSLCosts::productForQuotient }, + { "f / 2.0 as f * 0.5", { "", "f = f / 2.0;" }, { "", "f = f * 0.5;" }, "=<=", &ALLSLCosts::productForQuotient }, + { "x & 1 as x % 2, only truth asked", { "", "if (j & 1) i = 2;" }, { "", "if (j % 2) i = 2;" }, ">><", &ALLSLCosts::remainderForOddTest }, + { "a != b as a - b, only truth asked", { "", "if (j != i) i = 2;" }, { "", "if (j - i) i = 2;" }, "=><", &ALLSLCosts::differenceForNotEqual }, + { "a == b as !(a - b)", { "", "i = j == i;" }, { "", "i = !(j - i);" }, ">><", &ALLSLCosts::differenceForNotEqual }, + // Mono's first is the helper to shift, not each place. + { "x >> 2 as x / 4, x never below nought", { "", "i = j >> 2;" }, { "", "i = j / 4;" }, "><<", &ALLSLCosts::quotientForShift }, + { "x & 7 as x % 8, x never below nought", { "", "i = j & 7;" }, { "", "i = j % 8;" }, ">><", &ALLSLCosts::quotientForShift }, + { "x * -1 as -x", { "", "i = j * -1;" }, { "", "i = -j;" }, "<<<", nullptr }, + // Branches. + { "if (a) if (b) as if (a && b)", { "", "if (j) if (i) i = 2;" }, { "", "if (j && i) i = 2;" }, "<>=", &ALLSLCosts::andForNestedIf }, + { "if (a > 2) if (b < 5) as if (a > 2 & b < 5)", { "", "if (j > 2) if (i < 5) i = 2;" }, { "", "if (j > 2 & i < 5) i = 2;" }, "<<>", + &ALLSLCosts::bitAndForNestedTruths }, + { "if (c) x = 5; else x = 7; as x = (c) * -2 + 7, of a truth", { "", "if (j > 2) i = 5; else i = 7;" }, { "", "i = (j > 2) * -2 + 7;" }, "<<<", + nullptr }, + { "the same of anything else, as !!c", { "", "if (j) i = 5; else i = 7;" }, { "", "i = !!j * -2 + 7;" }, "<<=", &ALLSLCosts::arithmeticForSelect }, + { "if (c) x = a; else x = B; as x = B; if (c) x = a;", { "", "if (j > 2) s = gs; else s = \"no\";" }, { "", "s = \"no\"; if (j > 2) s = gs;" }, + "<<<", nullptr }, + { "if (c) return 1; return 0; as return c;", { "integer f1(integer a) { if (a > 3) return 1; return 0; }", "i = f1(i);" }, + { "integer f1(integer a) { return a > 3; }", "i = f1(i);" }, "<<<", nullptr }, + { "if (c) x = 1; else x = 0; as x = c;", { "", "if (j > 2) i = 1; else i = 0;" }, { "", "i = j > 2;" }, "<<<", nullptr }, + { "an else after a return", { "integer f1(integer a) { if (a) return 5; else return 6; }", "i = f1(i);" }, + { "integer f1(integer a) { if (a) return 5; return 6; }", "i = f1(i);" }, "<<<", nullptr }, + { "a jump over statements as an else", { "", "if (j) { i = 1; jump L; } i = 2; @L;" }, { "", "if (j) i = 1; else i = 2;" }, "==<", nullptr, true }, + { "an if with nothing to do", { "", "if (j > 2) {} i = 3;" }, { "", "i = 3;" }, "<<<", nullptr }, + // Loops. + { "a for whose first check passes as a do", { "", "for (i = 0; i < 3; ++i) j = i;" }, { "", "i = 0; do j = i; while (++i < 3);" }, "<<>", + &ALLSLCosts::doForKnownFirst }, + { "a while so", { "", "i = 0; while (i < 3) { j += i; ++i; }" }, { "", "i = 0; do { j += i; ++i; } while (i < 3);" }, "<<=", + &ALLSLCosts::doForKnownFirst }, + { "a counter read nowhere else, counted down", { "", "for (i = 0; i < 3; ++i) j += 2;" }, { "", "i = 3; do j += 2; while (--i);" }, "<<<", + nullptr }, + { "the same to a bound not known", { "", "for (i = 0; i < j; ++i) f += 2;" }, { "", "i = j; while (i--) f += 2;" }, "<<<", nullptr }, + // A key found or NULL_KEY. + { "k != NULL_KEY as k", { "", "k = llGetOwner(); if (k != NULL_KEY) i = 2;" }, { "", "k = llGetOwner(); if (k) i = 2;" }, "<<<", nullptr }, + { "if (k == NULL_KEY) A else B as if (k) B else A", { "", "k = llGetOwner(); if (k == NULL_KEY) i = 2; else i = 3;" }, + { "", "k = llGetOwner(); if (k) i = 3; else i = 2;" }, "<<<", nullptr }, + { "if (k == NULL_KEY) A as if (k) ; else A", { "", "k = llGetOwner(); if (k == NULL_KEY) i = 2;" }, { "", "k = llGetOwner(); if (k) ; else i = 2;" }, + "<<<", nullptr }, + // Functions and variables. + { "a parameter never read taken away", { "f1(integer a, integer b) { llOwnerSay((string)a); }", "f1(i, j); f1(j, i);" }, + { "f1(integer a) { llOwnerSay((string)a); }", "f1(i); f1(j);" }, "<<<", nullptr }, + { "a parameter every call gives 2 made a local", { "f1(integer a, integer b) { llOwnerSay((string)(a + b)); }", "f1(i, 2); f1(j, 2);" }, + { "f1(integer a) { llOwnerSay((string)(a + 2)); }", "f1(i); f1(j);" }, "<<<", nullptr }, + { "a string no call reads not returned", { "string f1(integer a) { llOwnerSay((string)a); return \"x\"; }", "f1(i); f1(j);" }, + { "f1(integer a) { llOwnerSay((string)a); }", "f1(i); f1(j);" }, "<<<", nullptr }, + { "a global set before it is read made a local", { "integer z0;", "z0 = llGetUnixTime(); i = z0;" }, { "", "integer z0 = llGetUnixTime(); i = z0;" }, + "<<<", nullptr }, + { "a value set again before it is read not set", { "", "integer z = i; z = j; i = z;" }, { "", "integer z = j; i = z;" }, "<<<", nullptr }, + { "a || 1 settled", { "", "if ((integer)s || 1) i = 2;" }, { "", "i = 2;" }, "<<<", nullptr }, + // What is kept in a local, as the weigher has it (Mono's frame not in it). + { "a global read three times read once", { "", "i = gi + j; j = gi + i; i = gi + j;" }, { "", "integer z = gi; i = z + j; j = z + i; i = z + j;" }, + "><<", nullptr }, + { "a vector written twice kept in a local", { "", "v = v + <1.5, 2.5, 3.5>; v = v - <1.5, 2.5, 3.5>;" }, + { "", "vector z = <1.5, 2.5, 3.5>; v = v + z; v = v - z;" }, "><<", nullptr }, + { "a loop that never ends by a jump back", { "", "do { i = i + j; if (i) jump L; } while (TRUE); @L;" }, + { "", "@M; { i = i + j; if (i) jump L; } jump M; @L;" }, "<<>", &ALLSLCosts::jumpForForever, true }, + // Smaller everywhere where the two are held nowhere else; LSO, + // which holds none, joins them whatever the option. + { "two string literals joined", { "", "s = \"abc\" + \"def\";" }, { "", "s = \"abcdef\";" }, "<<<", nullptr }, }; for (const Fact& fact : facts) { @@ -299,9 +419,7 @@ namespace tut const Delta jump = delta(t, { "", "jump L; @L;" }, { "", "" }); said("a jump", t, jump, -c.jump); - // What Luau's compiler makes of a jump is a byte more in some - // runs than in others, the same build and the same text. - ensure("a jump" + where + ": " + std::to_string(-jump.first), t == Target::Luau ? std::abs(-jump.first - c.jump) <= 1 : -jump.first == c.jump); + ensure("a jump" + where + ": " + std::to_string(-jump.first), -jump.first == c.jump); const Delta f2 = delta(t, none, two); const Delta f18 = delta(t, none, eighteen); diff --git a/indra/alscript/tests/allsleffects_test.cpp b/indra/alscript/tests/allsleffects_test.cpp index 478d56f03a0..d4a7b1de7f4 100644 --- a/indra/alscript/tests/allsleffects_test.cpp +++ b/indra/alscript/tests/allsleffects_test.cpp @@ -25,8 +25,8 @@ #include "linden_common.h" -#include "../allsleffects.h" -#include "../allslservice.h" +#include "../lsl/allsleffects.h" +#include "../lsl/allslservice.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/allsloptimizer_test.cpp b/indra/alscript/tests/allsloptimizer_test.cpp index 9bea9165081..78063bec983 100644 --- a/indra/alscript/tests/allsloptimizer_test.cpp +++ b/indra/alscript/tests/allsloptimizer_test.cpp @@ -25,11 +25,11 @@ #include "linden_common.h" -#include "../alscriptengine.h" -#include "../allslinliner.h" -#include "../allsloptimizer.h" -#include "../allslservice.h" -#include "../allsltraits.h" +#include "../core/alscriptengine.h" +#include "../lsl/optimizer/allslinliner.h" +#include "../lsl/optimizer/allsloptimizer.h" +#include "../lsl/allslservice.h" +#include "../lsl/allsltraits.h" #include "../test/lltut.h" @@ -90,6 +90,13 @@ namespace tut const size_t at = text.find(declaration + " = "); if (at == std::string::npos) { + // Declared with no value, where its default is the smaller + // (ALLSLCosts::drop*Default): the default. + if (text.find(declaration + ";") != std::string::npos) + { + const std::string type = declaration.substr(0, declaration.find(' ')); + return type == "vector" ? std::vector{ 0.f, 0.f, 0.f } : type == "rotation" ? std::vector{ 0.f, 0.f, 0.f, 1.f } : std::vector{ 0.f }; + } return out; } const char* p = text.c_str() + at + declaration.size() + 3; @@ -99,6 +106,20 @@ namespace tut } while (true) { + // A negative part may be written as its cast, on Mono. + while (*p == ' ') + { + ++p; + } + bool cast = false; + for (const char* spelt : { "((float)", "((integer)" }) + { + if (std::strncmp(p, spelt, std::strlen(spelt)) == 0) + { + p += std::strlen(spelt); + cast = true; + } + } char* end = nullptr; const F32 v = std::strtof(p, &end); if (end == p) @@ -107,6 +128,10 @@ namespace tut } out.push_back(v); p = end; + if (cast && *p == ')') + { + ++p; + } if (*p != ',') { break; @@ -145,6 +170,38 @@ namespace tut { return globals + "default\n{\n state_entry()\n {\n" + body + " }\n}\n"; } + + // A script optimized for a target with the defaults, what it became + // checked to compile there. + static std::string optimized(const std::string& source, ALLSLOptimizer::Target target) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + ALLSLService service; + for (const ALScriptProblem& p : service.check(r.text, target != ALLSLOptimizer::Target::LSO)) + { + ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + } + return r.text; + } + + static bool in(const std::string& text, const std::string& what) { return text.find(what) != std::string::npos; } + + // A script whose default state also says each of `globals` when + // touched: read before anything there sets them, they hold what was + // set before, and stay globals (localizeGlobals). + static std::string kept(const std::string& source, std::initializer_list globals) + { + std::string said; + for (const char* g : globals) + { + said += std::string(" llOwnerSay((string)") + g + ");\n"; + } + const size_t end = source.rfind("}\n"); + return source.substr(0, end) + " touch_start(integer kept)\n {\n" + said + " }\n}\n"; + } }; // Past TUT's fifty tests a group runs only what it is told it has. @@ -171,9 +228,9 @@ namespace tut void allsloptimizer_object::test<2>() { set_test_name("floats fold in single precision and print so that they read back exactly"); - // Each variable is assigned again later, so that it stays a - // variable and the fold shows in its declaration. - const std::string source = wrap("", " float a = 0.1 + 0.2;\n float b = PI * 2;\n float c = 1.0 / 3;\n llSay(0, (string)a + (string)b + (string)c);\n a = b = c = 0;\n"); + // Each variable may be assigned again before it is read, so that + // it stays a variable and the fold shows in its declaration. + const std::string source = wrap("", " float a = 0.1 + 0.2;\n float b = PI * 2;\n float c = 1.0 / 3;\n if (llFrand(1) < 0.5)\n {\n a = b = c = 0;\n }\n llSay(0, (string)a + (string)b + (string)c);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); ensure("optimized", r.optimized); // 0.1f + 0.2f is 0.3 exactly as a single, PI * 2 is 6.2831855f, @@ -184,7 +241,7 @@ namespace tut // A float that is a whole number prints as an integer where LSL // would convert one, and stays a float where it would not. - r = ALLSLOptimizer::run(wrap("", " float f = 2.0;\n list l = [2.0];\n string s = (string)2.0;\n vector v = <1.0, 2.0, 3.5>;\n llSay(0, (string)f + llList2CSV(l) + s + (string)v);\n f = 0; l = []; s = \"\"; v = ZERO_VECTOR;\n"), options()); + r = ALLSLOptimizer::run(wrap("", " float f = 2.0;\n list l = [2.0];\n string s = (string)2.0;\n vector v = <1.0, 2.0, 3.5>;\n if (llFrand(1) < 0.5)\n {\n f = 0; l = []; s = \"\"; v = ZERO_VECTOR;\n }\n llSay(0, (string)f + llList2CSV(l) + s + (string)v);\n"), options()); ensure("whole float as integer: " + r.text, r.text.find("float f = 2;") != std::string::npos); // As the cast of an integer on Mono, where that is smaller (ALLSLCosts::castForWholeFloat). ensure("list element stays a float: " + r.text, r.text.find("list l = (list)((float)2);") != std::string::npos); @@ -201,7 +258,8 @@ namespace tut ensure("optimized", r.optimized); ensure("wrapped: " + r.text, r.text.find("llSay(0, \"-2147483648\");") != std::string::npos); ensure("zero stays", r.text.find("(string)(1 / 0)") != std::string::npos); - ensure("the overflow stays", r.text.find("(string)(-2147483648 / -1)") != std::string::npos); + // On Mono, each negative number as its cast (ALLSLCosts::castForNegative). + ensure("the overflow stays: " + r.text, r.text.find("(string)(((integer)-2147483648) / ((integer)-1))") != std::string::npos); ensure("integer division", r.text.find("llSay(0, \"6\");") != std::string::npos); ensure("a wide shift stays", r.text.find("(string)(1 << 40)") != std::string::npos); ensure("modulo keeps the sign", r.text.find("llSay(0, \"-1\");") != std::string::npos); @@ -211,7 +269,7 @@ namespace tut void allsloptimizer_object::test<4>() { set_test_name("pure library calls with constant arguments are evaluated, others left"); - const std::string source = wrap("", " integer a = llAbs(-3);\n float b = llSqrt(16);\n string c = llToUpper(\"abc\");\n string d = llGetSubString(\"abcdef\", 4, 1);\n string e = llDeleteSubString(\"abcdef\", 1, 3);\n integer f = llStringLength(\"hello\");\n string g = llEscapeURL(\"a b\");\n string h = llBase64ToString(llStringToBase64(\"hi\"));\n integer i = llList2Integer([1, 2, 3], -1);\n string j = llList2String([1, 2.5, <1,2,3>], 1);\n list k = llList2List([1, 2, 3, 4], 1, 2);\n integer l = llFloor(2.7) + llCeil(2.2);\n string m = llGetSubString(\"h\xc3\xa9llo\", 0, 1);\n integer n = llGetUnixTime();\n string o = llList2CSV([1, \"a\", 2.0]);\n llSay(0, (string)a + (string)b + c + d + e + (string)f + g + h + (string)i + j + llList2CSV(k) + (string)l + m + (string)n + o);\n a = f = i = l = n = 0; b = 0; c = d = e = g = h = j = m = o = \"\"; k = [];\n"); + const std::string source = wrap("", " integer a = llAbs(-3);\n float b = llSqrt(16);\n string c = llToUpper(\"abc\");\n string d = llGetSubString(\"abcdef\", 4, 1);\n string e = llDeleteSubString(\"abcdef\", 1, 3);\n integer f = llStringLength(\"hello\");\n string g = llEscapeURL(\"a b\");\n string h = llBase64ToString(llStringToBase64(\"hi\"));\n integer i = llList2Integer([1, 2, 3], -1);\n string j = llList2String([1, 2.5, <1,2,3>], 1);\n list k = llList2List([1, 2, 3, 4], 1, 2);\n integer l = llFloor(2.7) + llCeil(2.2);\n string m = llGetSubString(\"h\xc3\xa9llo\", 0, 1);\n integer n = llGetUnixTime();\n string o = llList2CSV([1, \"a\", 2.0]);\n if (llFrand(1) < 0.5)\n {\n a = f = i = l = n = 0; b = 0; c = d = e = g = h = j = m = o = \"\"; k = [];\n }\n llSay(0, (string)a + (string)b + c + d + e + (string)f + g + h + (string)i + j + llList2CSV(k) + (string)l + m + (string)n + o);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); ensure("optimized", r.optimized); ensure("abs: " + r.text, r.text.find("integer a = 3;") != std::string::npos); @@ -241,17 +299,21 @@ namespace tut ensure("optimized", r.optimized); ensure("plus zero: " + r.text, r.text.find("integer a = g;") != std::string::npos); ensure("times one", r.text.find("integer b = g;") != std::string::npos); - ensure("minus minus", r.text.find("integer c = g + 1;") != std::string::npos); + // One more as -~g on Mono, whose ldc.i4 1 is five bytes + // (ALLSLCosts::negateComplementForIncrement). + ensure("minus minus: " + r.text, r.text.find("integer c = -~g;") != std::string::npos); // And on Mono, two less as ~-~-g (ALLSLCosts::complementNegateForDecrement). ensure("plus minus: " + r.text, r.text.find("integer d = ~-~-g;") != std::string::npos); ensure("cast", r.text.find("integer e = g;") != std::string::npos); - ensure("times zero", r.text.find("integer f = 0;") != std::string::npos); + // Left at its default on Mono (ALLSLCosts::dropIntegerDefault). + ensure("times zero: " + r.text, r.text.find("integer f;") != std::string::npos); ensure("equals zero", r.text.find("integer i = (!g);") != std::string::npos); ensure("not equal zero as condition", r.text.find("if (g)\n h = 1;") != std::string::npos); ensure("double not", r.text.find("if (g)\n h = 2;") != std::string::npos); // As a ^ on Mono, where only truth counts (ALLSLCosts::xorForNotEqual). ensure("not equals: " + r.text, r.text.find("if (g ^ h)\n h = 3;") != std::string::npos); - ensure("swapped", r.text.find("if (g)\n h = 5;\n else\n h = 4;") != std::string::npos); + // Turned round off its !, then a choice of two numbers as arithmetic. + ensure("swapped, then chosen by arithmetic: " + r.text, r.text.find("h = !!g + 4;") != std::string::npos); } template<> template<> @@ -285,10 +347,11 @@ namespace tut ensure("list add: " + r.text, r.text.find("l = (list)1 + \"a\";") != std::string::npos); ensure("an empty list stays", r.text.find("l = [];") != std::string::npos); - // On Mono the comparison is not; nor is the sum here, one sum not - // paying for the helper it would be the script's only use of. + // On Mono the comparison is not, though one more is -~x there too; + // nor is the sum here, one sum not paying for the helper it would + // be the script's only use of. r = ALLSLOptimizer::run(source, options()); - ensure("no comparison on Mono: " + r.text, r.text.find("llGetListLength(l) + 1") != std::string::npos && r.text.find("l != []") == std::string::npos); + ensure("no comparison on Mono: " + r.text, r.text.find("integer n = -~llGetListLength(l);") != std::string::npos && r.text.find("l != []") == std::string::npos); ensure("one sum not worth its helper on Mono: " + r.text, r.text.find("l = [1, \"a\"];") != std::string::npos); // On Luau neither. @@ -383,7 +446,7 @@ namespace tut void allsloptimizer_object::test<11>() { set_test_name("the rotation functions fold over unit rotations by the viewer's own quaternion, and leave the rest"); - const std::string source = wrap("", " rotation a = llEuler2Rot(<0, 0, PI_BY_TWO>);\n vector b = llRot2Euler(<0, 0, 0.7071068, 0.7071068>);\n rotation c = llAxisAngle2Rot(<0, 0, 2>, PI);\n vector d = llRot2Axis(<0, 0, 0.7071068, 0.7071068>);\n float e = llRot2Angle(<0, 0, 0.7071068, 0.7071068>);\n vector f = llRot2Fwd(<0, 0, 0.7071068, 0.7071068>);\n vector h = llRot2Up(<0.7071068, 0, 0, 0.7071068>);\n vector i = llRot2Axis(<0, 0, 0, 1>);\n vector j = llRot2Fwd(<0, 0, 2, 2>);\n llSay(0, (string)a + (string)b + (string)c + (string)d + (string)e + (string)f + (string)h + (string)i + (string)j);\n a = c = ZERO_ROTATION; b = d = f = h = i = j = ZERO_VECTOR; e = 0;\n"); + const std::string source = wrap("", " rotation a = llEuler2Rot(<0, 0, PI_BY_TWO>);\n vector b = llRot2Euler(<0, 0, 0.7071068, 0.7071068>);\n rotation c = llAxisAngle2Rot(<0, 0, 2>, PI);\n vector d = llRot2Axis(<0, 0, 0.7071068, 0.7071068>);\n float e = llRot2Angle(<0, 0, 0.7071068, 0.7071068>);\n vector f = llRot2Fwd(<0, 0, 0.7071068, 0.7071068>);\n vector h = llRot2Up(<0.7071068, 0, 0, 0.7071068>);\n vector i = llRot2Axis(<0, 0, 0, 1>);\n vector j = llRot2Fwd(<0, 0, 2, 2>);\n if (llFrand(1) < 0.5)\n {\n a = c = ZERO_ROTATION; b = d = f = h = i = j = ZERO_VECTOR; e = 0;\n }\n llSay(0, (string)a + (string)b + (string)c + (string)d + (string)e + (string)f + (string)h + (string)i + (string)j);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); ensure("optimized", r.optimized); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; @@ -394,7 +457,7 @@ namespace tut ensure("the axis back: " + r.text, about(numbers(r.text, "vector d"), { 0.f, 0.f, 1.f })); ensure("the angle back: " + r.text, about(numbers(r.text, "float e"), { 1.5707964f })); ensure("fwd of a quarter turn is left: " + r.text, !has("llRot2Fwd(<0, 0, 0.7071068, 0.7071068>)") && has("vector f = <")); - ensure("up of a roll: " + r.text, !has("llRot2Up(") && has("vector h = <0, -")); + ensure("up of a roll: " + r.text, !has("llRot2Up(") && has("vector h = <0, ((float)-")); ensure("no rotation has no axis: left", has("llRot2Axis(<0, 0, 0, 1>)")); ensure("not a unit rotation: left", has("llRot2Fwd(<0, 0, 2, 2>)")); } @@ -402,7 +465,7 @@ namespace tut void allsloptimizer_object::test<12>() { set_test_name("llJsonGetValue folds a string or a plain number out of strict JSON, and leaves the rest to the simulator"); - const std::string source = wrap("", " string a = llJsonGetValue(\"{\\\"name\\\": \\\"Ann\\\", \\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"name\"]);\n string b = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", -7]}\", [\"tags\", 1]);\n string c = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"tags\", 2, \"k\"]);\n string d = llJsonGetValue(\"[1.5]\", [0]);\n string e = llJsonGetValue(\"[true]\", [0]);\n string f = llJsonGetValue(\"[1]\", [3]);\n string g = llJsonGetValue(\"{\\\"a\\\": 1, \\\"a\\\": 2}\", [\"a\"]);\n string h = llJsonGetValue(\"[1,]\", [0]);\n string i = llJsonGetValue(\"[\\\"a\\\\\\\"b\\\"]\", [0]);\n llSay(0, a + b + c + d + e + f + g + h + i);\n a = b = c = d = e = f = g = h = i = \"\";\n"); + const std::string source = wrap("", " string a = llJsonGetValue(\"{\\\"name\\\": \\\"Ann\\\", \\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"name\"]);\n string b = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", -7]}\", [\"tags\", 1]);\n string c = llJsonGetValue(\"{\\\"tags\\\": [\\\"x\\\", 7, {\\\"k\\\": \\\"v\\\"}]}\", [\"tags\", 2, \"k\"]);\n string d = llJsonGetValue(\"[1.5]\", [0]);\n string e = llJsonGetValue(\"[true]\", [0]);\n string f = llJsonGetValue(\"[1]\", [3]);\n string g = llJsonGetValue(\"{\\\"a\\\": 1, \\\"a\\\": 2}\", [\"a\"]);\n string h = llJsonGetValue(\"[1,]\", [0]);\n string i = llJsonGetValue(\"[\\\"a\\\\\\\"b\\\"]\", [0]);\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = i = \"\";\n }\n llSay(0, a + b + c + d + e + f + g + h + i);\n"); ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, options()); ensure("optimized", r.optimized); auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; @@ -416,7 +479,7 @@ namespace tut ensure("a trailing comma is left", has("string h = llJsonGetValue")); ensure("an escape unescaped: " + r.text, has("string i = \"a\\\"b\";")); // The answers that are constants are the constants' names. - const std::string more = wrap("", " string a = llJsonGetValue(\"[true, null]\", [0]);\n string b = llJsonGetValue(\"[1]\", [3]);\n string c = llJsonValueType(\"{\\\"a\\\": [1, {}]}\", [\"a\", 1]);\n string d = llJsonValueType(\"{\\\"a\\\": 1}\", [\"b\"]);\n string e = llJsonValueType(\"[1,]\", []);\n string f = llList2Json(JSON_OBJECT, [\"name\", \"Ann Lee\", \"count\", 3]);\n string g = llList2Json(JSON_ARRAY, [1, \"true\"]);\n string h = llList2Json(JSON_ARRAY, [1.5]);\n list i = llJson2List(\"[1, \\\"two\\\", -3]\");\n list j = llJson2List(\"{\\\"k\\\": \\\"v\\\"}\");\n list k = llJson2List(\"[true]\");\n llSay(0, a + b + c + d + e + f + g + h + llList2CSV(i) + llList2CSV(j) + llList2CSV(k));\n a = b = c = d = e = f = g = h = \"\"; i = j = k = [];\n"); + const std::string more = wrap("", " string a = llJsonGetValue(\"[true, null]\", [0]);\n string b = llJsonGetValue(\"[1]\", [3]);\n string c = llJsonValueType(\"{\\\"a\\\": [1, {}]}\", [\"a\", 1]);\n string d = llJsonValueType(\"{\\\"a\\\": 1}\", [\"b\"]);\n string e = llJsonValueType(\"[1,]\", []);\n string f = llList2Json(JSON_OBJECT, [\"name\", \"Ann Lee\", \"count\", 3]);\n string g = llList2Json(JSON_ARRAY, [1, \"true\"]);\n string h = llList2Json(JSON_ARRAY, [1.5]);\n list i = llJson2List(\"[1, \\\"two\\\", -3]\");\n list j = llJson2List(\"{\\\"k\\\": \\\"v\\\"}\");\n list k = llJson2List(\"[true]\");\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = \"\"; i = j = k = [];\n }\n llSay(0, a + b + c + d + e + f + g + h + llList2CSV(i) + llList2CSV(j) + llList2CSV(k));\n"); ALLSLOptimizer::Result m = ALLSLOptimizer::run(more, options()); ensure("optimized", m.optimized); auto hasm = [&m](const char* text) { return m.text.find(text) != std::string::npos; }; @@ -428,12 +491,12 @@ namespace tut ensure("an object from plain strings and integers: " + m.text, hasm("string f = \"{\\\"name\\\":\\\"Ann Lee\\\",\\\"count\\\":3}\";")); ensure("a string that reads as a JSON word is left: " + m.text, hasm("string g = llList2Json")); ensure("a float is left: " + m.text, hasm("string h = llList2Json")); - ensure("an array to a list: " + m.text, hasm("list i = [1, \"two\", -3];")); + ensure("an array to a list: " + m.text, hasm("list i = [1, \"two\", ((integer)-3)];")); ensure("an object to a list: " + m.text, hasm("list j = [\"k\", \"v\"];")); ensure("a true in it is left: " + m.text, hasm("list k = llJson2List")); // A nested value comes out written the compact way; a set puts a // value in, at a key or an index, where the document is plain. - const std::string sets = wrap("", " string a = llJsonGetValue(\"{\\\"a\\\": [1, {\\\"b\\\": true}, \\\"c\\\"]}\", [\"a\"]);\n string b = llJsonSetValue(\"{\\\"a\\\": 1}\", [\"b\"], \"two\");\n string c = llJsonSetValue(\"[1, 2]\", [1], \"9\");\n string d = llJsonSetValue(\"[1, 2]\", [JSON_APPEND], JSON_TRUE);\n string e = llJsonSetValue(\"[1, 2]\", [5], \"9\");\n string f = llJsonSetValue(\"{}\", [\"a\", \"b\"], \"x\");\n string g = llJsonSetValue(\"[1]\", [0], \"1.5\");\n string h = llJsonGetValue(\"[1.5]\", []);\n llSay(0, a + b + c + d + e + f + g + h);\n a = b = c = d = e = f = g = h = \"\";\n"); + const std::string sets = wrap("", " string a = llJsonGetValue(\"{\\\"a\\\": [1, {\\\"b\\\": true}, \\\"c\\\"]}\", [\"a\"]);\n string b = llJsonSetValue(\"{\\\"a\\\": 1}\", [\"b\"], \"two\");\n string c = llJsonSetValue(\"[1, 2]\", [1], \"9\");\n string d = llJsonSetValue(\"[1, 2]\", [JSON_APPEND], JSON_TRUE);\n string e = llJsonSetValue(\"[1, 2]\", [5], \"9\");\n string f = llJsonSetValue(\"{}\", [\"a\", \"b\"], \"x\");\n string g = llJsonSetValue(\"[1]\", [0], \"1.5\");\n string h = llJsonGetValue(\"[1.5]\", []);\n if (llFrand(1) < 0.5)\n {\n a = b = c = d = e = f = g = h = \"\";\n }\n llSay(0, a + b + c + d + e + f + g + h);\n"); ALLSLOptimizer::Result n = ALLSLOptimizer::run(sets, options()); ensure("optimized", n.optimized); auto hasn = [&n](const char* text) { return n.text.find(text) != std::string::npos; }; @@ -445,7 +508,7 @@ namespace tut ensure("a path to create is left: " + n.text, hasn("string f = llJsonSetValue")); ensure("a value that is a fraction is left: " + n.text, hasn("string g = llJsonSetValue")); ensure("a document with a fraction is left: " + n.text, hasn("string h = llJsonGetValue")); - const std::string dels = wrap("", " string a = llJsonSetValue(\"{\\\"a\\\": 1, \\\"b\\\": 2}\", [\"a\"], JSON_DELETE);\n string b = llJsonSetValue(\"[1, 2, 3]\", [1], JSON_DELETE);\n string c = llJsonSetValue(\"[1, 2, 3]\", [7], JSON_DELETE);\n llSay(0, a + b + c);\n a = b = c = \"\";\n"); + const std::string dels = wrap("", " string a = llJsonSetValue(\"{\\\"a\\\": 1, \\\"b\\\": 2}\", [\"a\"], JSON_DELETE);\n string b = llJsonSetValue(\"[1, 2, 3]\", [1], JSON_DELETE);\n string c = llJsonSetValue(\"[1, 2, 3]\", [7], JSON_DELETE);\n if (llFrand(1) < 0.5)\n {\n a = b = c = \"\";\n }\n llSay(0, a + b + c);\n"); ALLSLOptimizer::Result q = ALLSLOptimizer::run(dels, options()); auto hasq = [&q](const char* text) { return q.text.find(text) != std::string::npos; }; ensure("a key deleted: " + q.text, hasq("string a = \"{\\\"b\\\":2}\";")); @@ -1321,7 +1384,7 @@ namespace tut ensure("optimized: " + notes(r), r.optimized); auto has = [&r](const std::string& text) { return r.text.find(text) != std::string::npos; }; ensure("b goes where it is read: " + r.text, !has("integer b") && has("llOwnerSay((string)(g * 2));")); - ensure("c's g is written between: it stays: " + r.text, has("integer c = g + 1;")); + ensure("c's g is written between: it stays: " + r.text, has("integer c = -~g;")); ensure("d is read twice: " + r.text, has("integer d = g * 3;")); ensure("e is read round a loop: " + r.text, has("integer e = g * 4;")); ensure("f is read in a branch, a statement of its own: " + r.text, has("integer f = g * 5;")); @@ -1493,7 +1556,7 @@ namespace tut void allsloptimizer_object::test<40>() { set_test_name("-1 tests, one and two either way, increments and list elements are written as each target has them smaller, and compile"); - const std::string source = wrap("list l;\ninteger g;\n", + const std::string source = kept(wrap("list l;\ninteger g;\n", " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n string s = llGetObjectName();\n" " integer found = llListFindList(l, [a]);\n" " if (a != -1) g = 1;\n" @@ -1514,7 +1577,7 @@ namespace tut " l = l + (list)s;\n" " l += [b];\n" " l = l + [llFrand(1), llFrand(2)];\n" - " llOwnerSay((string)g + (string)l + (string)a + (string)b);\n"); + " llOwnerSay((string)g + (string)l + (string)a + (string)b);\n"), { "g" }); const auto run = [&](ALLSLOptimizer::Target target) { ALLSLOptimizer::Options o = allsloptimizer_data::options(); o.target = target; @@ -1542,7 +1605,7 @@ namespace tut ensure("x - 2 as ~-~-x, a minus bracketed after a minus", has(lso, "g = ~-~-(-a);")); ensure("x != -1 as a value stays", has(lso, "g = a != -1;")); ensure("an increment before", has(lso, "++a;")); - ensure("and a for's step", has(lso, "--b)")); + ensure("and a for's step, the loop a do on LSO, its first check known", has(lso, "--b")); ensure("l + [a] as l + a", has(lso, "l = l + a;")); ensure("l + [a, s] as l + a + s", has(lso, "l = l + a + s;")); ensure("[s] + l as s + l", has(lso, "l = s + l;")); @@ -1552,8 +1615,9 @@ namespace tut const std::string mono = run(ALLSLOptimizer::Target::Mono); ensure("Mono: x != -1 as ~x: " + mono, has(mono, "if (~a)")); - ensure("Mono: x == -1 stays", has(mono, "g = a == -1;")); - ensure("Mono: x + 1 stays", has(mono, "g = a + 1;")); + // Mono's ldc.i4 is five bytes, -1 and 1 too. + ensure("Mono: x == -1 as !~x: " + mono, has(mono, "g = !~a;")); + ensure("Mono: x + 1 as -~x", has(mono, "g = -~a;")); ensure("Mono: x - 1 as ~-x", has(mono, "g = ~-b;")); ensure("Mono: ++a", has(mono, "++a;")); ensure("Mono: what it wrote weighs what it says", mono.find("l = l") != std::string::npos); @@ -1665,7 +1729,8 @@ namespace tut ensure("a loop's counter never read: " + r.text, !has("i = 0") && !has("i++") && !has("integer i") && has("for (; ") && has(" > 5; )")); ensure("a global read stays", has("shown = llGetUnixTime();")); ensure("a comparison left empty turned around the other way: " + r.text, has("if (llGetObjectName() != \"on\")")); - ensure("anything else, what it negates bracketed", has("if (!(llGetUnixTime() & 1))")); + ensure("anything else, what it negates bracketed; Luau's odd test a remainder", + has(target == ALLSLOptimizer::Target::Luau ? "if (!(llGetUnixTime() % 2))" : "if (!(llGetUnixTime() & 1))")); ensure("said, as set and never read", std::any_of(r.problems.begin(), r.problems.end(), [](const ALScriptProblem& p) { return p.key == "OptimizerRemovedWriteOnly" && p.args.size() == 1 && p.args[0] == "count"; })); @@ -1710,9 +1775,9 @@ namespace tut ensure("of strings, ==: " + r.text, has("s != \"x\"") && !has("!(s == \"x\")")); ensure("not of lists, whose != is a length", has("!(l == m)")); ensure("not of floats, which may be no number", has("!(f < g)")); - ensure("!= where only truth counts", has(lso || mono ? "if (a ^ b)" : "if (a != b)")); + ensure("!= where only truth counts, a difference on Luau", has(lso || mono ? "if (a ^ b)" : "if (a - b)")); ensure("an if on ==, its branches swapped", - lso || mono ? has("if (a ^ b)\n llOwnerSay(\"ne\");\n else\n llOwnerSay(\"eq\");") : has("if (a == b)")); + lso || mono ? has("if (a ^ b)\n llOwnerSay(\"ne\");\n else\n llOwnerSay(\"eq\");") : has("if (!(a - b))")); ensure("inclusive against a constant", has(mono ? "if (a > 4)" : "if (a >= 5)") && has(mono ? "if (b < 6)" : "if (b <= 5)")); ensure("not past the largest integer", has("if (b <= 2147483647)")); ensure("|| where only truth counts", has(mono ? "while (a | b)" : "while (a || b)")); @@ -1903,7 +1968,7 @@ namespace tut "default\n{\n touch_start(integer n)\n {\n" " integer busy = n;\n busy = busy * busy + busy;\n busy = busy - busy / 3;\n" " counted = busy;\n llOwnerSay(\"Q\" + (string)counted);\n state waiting;\n }\n}\n" - "state waiting\n{\n touch_start(integer n)\n {\n state default;\n }\n}\n"; + "state waiting\n{\n touch_start(integer n)\n {\n llOwnerSay((string)counted);\n state default;\n }\n}\n"; const auto run = [&source](ALLSLOptimizer::Target target) { ALLSLOptimizer::Options o = allsloptimizer_data::options(); o.target = target; @@ -1933,4 +1998,731 @@ namespace tut const ALScriptWeight asLso = ALScriptWeigh::mono(ALLSLOptimizer::run(source, plain).text); ensure("smaller on Mono than named for LSO", named.compiled && asLso.compiled && named.total < asLso.total); } + + template<> template<> + void allsloptimizer_object::test<51>() + { + set_test_name("a pure call made again, given what nothing between changes, is kept in a local where the target has that no larger (L12); " + "not across a write or a label, nor begun where its statement may not come to it"); + ALLSLOptimizer::Options lso = options(); + lso.target = ALLSLOptimizer::Target::LSO; + const std::string head = "default\n{\n touch_start(integer n)\n {\n string s = llDetectedName(0);\n"; + const std::string tail = " }\n}\n"; + const auto body = [&](const ALLSLOptimizer::Result& r) { + const size_t at = r.text.find(" string s = llDetectedName(0);\n"); + return at == std::string::npos ? r.text : r.text.substr(at + 38); + }; + + const std::string three = head + " llOwnerSay(llToUpper(s));\n llSay(0, llToUpper(s));\n llWhisper(0, llToUpper(s));\n" + tail; + ALLSLOptimizer::Result r = ALLSLOptimizer::run(three, lso); + ensure("optimized: " + notes(r), r.optimized); + ensure_equals("kept", body(r), " string toUpper = llToUpper(s);\n llOwnerSay(toUpper);\n llSay(0, toUpper);\n llWhisper(0, toUpper);\n" + tail); + ensure("noted: " + notes(r), has(r, "kept llToUpper(s) in the local toUpper, which its 3 places read")); + // On Mono a call given one variable is smaller than a local. + r = ALLSLOptimizer::run(three, options()); + ensure("not on Mono: " + r.text, r.text.find("toUpper") == std::string::npos); + + // A place in a loop after one its statement always comes to. + r = ALLSLOptimizer::run(head + " llSay(0, llToUpper(s));\n integer i;\n for (i = 0; i < n; ++i)\n llSay(i, llToUpper(s));\n" + tail, lso); + ensure("into the loop: " + r.text, r.text.find("string toUpper = llToUpper(s);\n llSay(0, toUpper);") != std::string::npos && + r.text.find("llSay(i, toUpper);") != std::string::npos); + + // The outermost call, and the call it is given with it. + r = ALLSLOptimizer::run(head + " llSay(0, llGetSubString(llToUpper(s), 0, 2));\n llSay(1, llGetSubString(llToUpper(s), 0, 2));\n" + tail, lso); + ensure_equals("outermost", body(r), + " string getSubString = llGetSubString(llToUpper(s), 0, 2);\n llSay(0, getSubString);\n llSay(1, getSubString);\n" + tail); + + // A name the script has is not taken again. + r = ALLSLOptimizer::run(head + " string toUpper = llDetectedName(1);\n llSay(0, toUpper);\n llSay(0, llToUpper(s));\n llSay(1, llToUpper(s));\n" + tail, lso); + ensure("numbered: " + r.text, r.text.find("string toUpper2 = llToUpper(s);") != std::string::npos); + + // What it is given written between: two places, each alone. + const std::string written = head + " llSay(0, llToUpper(s));\n s = llDetectedName(1);\n llSay(0, llToUpper(s));\n llSay(0, s);\n" + tail; + r = ALLSLOptimizer::run(written, lso); + ensure("not across a write: " + r.text, r.text.find("toUpper") == std::string::npos); + // A label between, which a jump could bring the code in by. + r = ALLSLOptimizer::run(head + " llSay(0, llToUpper(s));\n @again;\n llSay(0, llToUpper(s));\n if (llFrand(1) < 0.5)\n jump again;\n" + tail, lso); + ensure("not across a label: " + r.text, r.text.find("toUpper") == std::string::npos); + // The first place in a branch: kept, it would run where it did not. + r = ALLSLOptimizer::run(head + " if (n)\n llSay(0, llToUpper(s));\n llSay(0, llToUpper(s));\n" + tail, lso); + ensure("not begun in a branch: " + r.text, r.text.find("toUpper") == std::string::npos); + } + + template<> template<> + void allsloptimizer_object::test<52>() + { + set_test_name("two vectors' dot and cross products fold to what the VM gives: each part by the same part, and a vector by itself crossed to nothing"); + // Each may be set again before it is read, so that its fold shows + // in its declaration. + const std::string source = wrap("", " float d = <1, 3, -5> * <4, -2, -1>;\n vector c = <1.0, 2.0, 3.0> % <4.0, 5.0, 6.0>;\n" + " vector z = <44608876.0, 125129528.0, 357885664.0> % <44608876.0, 125129528.0, 357885664.0>;\n" + " if (llFrand(1) < 0.5)\n {\n d = 0;\n c = z = ZERO_VECTOR;\n }\n" + " llSay(0, (string)d + (string)c + (string)z);\n"); + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::LSO }) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + ensure("dot: " + r.text, about(numbers(r.text, "float d"), { 3.f })); + ensure("cross: " + r.text, about(numbers(r.text, "vector c"), { -3.f, 6.f, -3.f })); + ensure("crossed by itself: " + r.text, about(numbers(r.text, "vector z"), { 0.f, 0.f, 0.f })); + } + } + + template<> template<> + void allsloptimizer_object::test<53>() + { + set_test_name("a local set to a constant is that constant where it is read before it is set again, on every way there (L15); " + "not where the ways disagree, in a loop that sets it, past a label, nor after a write in the same statement"); + const std::string head = "default\n{\n touch_start(integer n)\n {\n"; + const std::string tail = " }\n}\n"; + ALLSLOptimizer::Result r = + ALLSLOptimizer::run(head + " integer x = 5;\n llSay(0, (string)x);\n x = 7;\n llSay(0, (string)x);\n n = 3;\n llSay(n, \"n\");\n" + tail, options()); + ensure("optimized: " + notes(r), r.optimized); + ensure_equals("each read its value, and the variable gone", r.text, + head + " llSay(0, \"5\");\n llSay(0, \"7\");\n llSay(3, \"n\");\n" + tail); + ensure("noted: " + notes(r), has(r, "folded (string)x to \"7\"") && has(r, "inlined n to 3") && has(r, "removed x, which is set and never read")); + + // The ways of an if: what both say goes on, what they do not stops. + r = ALLSLOptimizer::run(head + " integer x = 1;\n integer y = 1;\n if (n)\n {\n x = 2;\n llSay(0, \"a\");\n }\n" + " else\n {\n llSay(0, \"b\");\n }\n llSay(x, (string)y);\n x = y = 0;\n" + tail, + options()); + ensure("agreed: " + r.text, r.text.find("llSay(x, \"1\");") != std::string::npos); + // A way that returns says nothing of what comes after. + r = ALLSLOptimizer::run(head + " integer x = 1;\n if (n)\n {\n x = 2;\n return;\n }\n llSay(x, \"c\");\n x = 0;\n" + tail, + options()); + ensure("a way that ends: " + r.text, r.text.find("llSay(1, \"c\");") != std::string::npos); + + // A loop that sets it: not inside, nor after. + r = ALLSLOptimizer::run(head + " integer i = 0;\n while (i < n)\n {\n llSay(i, \"d\");\n ++i;\n }\n llSay(i, \"e\");\n" + tail, + options()); + ensure("in a loop: " + r.text, r.text.find("llSay(i, \"d\");") != std::string::npos && r.text.find("llSay(i, \"e\");") != std::string::npos); + // Past a label, which a jump may come to with anything. + r = ALLSLOptimizer::run(head + " integer x = 1;\n @top;\n llSay(x, \"f\");\n x = 2;\n if (llFrand(1) < 0.5)\n jump top;\n" + tail, + options()); + ensure("past a label: " + r.text, r.text.find("llSay(x, \"f\");") != std::string::npos); + // A loop holding a label forgets everything as it begins. + r = ALLSLOptimizer::run(head + " integer x = 1;\n while (llFrand(1) < 0.5)\n {\n @in;\n llSay(x, \"g\");\n }\n" + " x = 2;\n if (llFrand(1) < 0.5)\n jump in;\n" + tail, + options()); + ensure("a loop with a label: " + r.text, r.text.find("llSay(x, \"g\");") != std::string::npos); + // LSL runs a binary operator's right side first: the read on the + // left comes after the write. + r = ALLSLOptimizer::run(head + " integer x = 1;\n llSay(0, (string)(x + (x = 3)));\n llSay(x, \"h\");\n" + tail, options()); + ensure("after a write in the statement: " + r.text, r.text.find("(string)(x + (x = 3))") != std::string::npos); + // A read left where another read of the variable is not known: a + // literal in place of a read is no smaller, and the variable stays. + r = ALLSLOptimizer::run(head + " string s = \"a long string of text\";\n llSay(0, s);\n if (n)\n s = llDetectedName(0);\n llSay(1, s);\n" + tail, + options()); + ensure("not one read alone: " + r.text, r.text.find("llSay(0, s);") != std::string::npos); + } + + template<> template<> + void allsloptimizer_object::test<54>() + { + set_test_name("a comparison settled by what each side can be (L16): a count never below nought, a find never below -1, llFrand of a positive " + "magnitude, a truth 1 or 0, a type within its constants; not where the ranges meet, nor where a side changes something"); + const std::string source = "integer said() { llOwnerSay(\"said\"); return 1; }\n" + "default\n{\n touch_start(integer n)\n {\n" + " list l = llParseString2List(llDetectedName(0), [\" \"], []);\n" + " if (llGetListLength(l) < 0) llSay(0, \"a\");\n" + " integer count = llGetListLength(l);\n" + " if (count >= 0) llSay(0, \"b\");\n" + " if (llFrand(10.0) < 0) llSay(0, \"c\");\n" + " if (llListFindList(l, [\"x\"]) > -2) llSay(0, \"d\");\n" + " if (llGetListEntryType(l, 0) == 9) llSay(0, \"e\");\n" + " if (llSameGroup(llDetectedKey(0)) == 2) llSay(0, \"f\");\n" + " if (llFrand(10.0) < 5) llSay(0, \"g\");\n" + " if (llGetListLength(l) > 0) llSay(0, \"h\");\n" + " if (llStringLength(llList2String(l, said())) < 0) llSay(0, \"i\");\n" + " }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::LSO }) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + const auto said = [&r](const char* what) { return r.text.find(std::string("llSay(0, \"") + what + "\")") != std::string::npos; }; + ensure("a count below nought never: " + r.text, !said("a")); + ensure("a count's local nought or more always: " + r.text, said("b") && r.text.find("count") == std::string::npos); + ensure("llFrand of ten never below nought: " + r.text, !said("c")); + ensure("a find above -2 always: " + r.text, said("d") && r.text.find("llListFindList") == std::string::npos); + ensure("a type past TYPE_ROTATION never: " + r.text, !said("e")); + ensure("a truth never 2: " + r.text, !said("f")); + ensure("where the ranges meet, kept: " + r.text, said("g") && said("h")); + ensure("where a side changes something, kept: " + r.text, said("i")); + ensure("noted: " + notes(r), has(r, "settled llGetListLength(l) < 0 to 0") || has(r, "settled (l != []) < 0 to 0") || has(r, "settled l != [] < 0 to 0")); + } + } + template<> template<> + void allsloptimizer_object::test<55>() + { + set_test_name("the folding arithmetic is IEEE's whatever the build's float flags: llVecNorm folds where dividing by the length and " + "multiplying by its reciprocal agree, as LLVector3 does, and not where they differ; a negative zero keeps its sign"); + // Each may be set again before it is read, so that its fold shows + // in its declaration. + const std::string source = wrap("", " vector a = llVecNorm(<0.75, 3, 1>);\n vector b = llVecNorm(<0, 3, 4>);\n" + " float m = llVecMag(<0, 3, 4>);\n list l = [-0.0];\n" + " if (llFrand(1) < 0.5)\n {\n a = b = ZERO_VECTOR;\n m = 0;\n l = [];\n }\n" + " llSay(0, (string)a + (string)b + (string)m + llList2CSV(l));\n"); + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::LSO }) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + // 0.75 / 3.25 is 0.230769232 as a single, 0.75 * (1 / 3.25) + // 0.230769247: which the VM gives is not known, so it is left. + // Built with the viewer's float flags, the compiler made the + // division a product with the reciprocal, and this folded. + ensure("left where the two differ: " + r.text, r.text.find("vector a = llVecNorm(<0.75, ") != std::string::npos); + const std::vector b = numbers(r.text, "vector b"); + ensure("folded where they agree: " + r.text, b.size() == 3 && b[0] == 0.0f && b[1] == 3.0f / 5.0f && b[2] == 4.0f / 5.0f); + ensure("a length both ways the same: " + r.text, numbers(r.text, "float m") == std::vector{ 5.0f }); + const size_t at = r.text.find("list l = "); + ensure("a negative zero keeps its sign: " + r.text, + at != std::string::npos && r.text.substr(at, r.text.find('\n', at) - at).find("-0.0") != std::string::npos); + } + } + template<> template<> + void allsloptimizer_object::test<56>() + { + set_test_name("bit tests of one value merged (L18): one-bit tests all asked as !~(x | ~mask), all clear as !(x & mask), an | of them as " + "x & mask, an || of them so where only truth counts; not of two values, of a test of more than one bit, nor where x changes"); + const std::string source = "integer said() { llOwnerSay(\"said\"); return 4; }\n" + "default\n{\n touch_start(integer n)\n {\n" + " integer m = llGetLinkNumber();\n" + " if ((n & 4) && (n & 8)) llSay(0, \"a\");\n" + " integer v = (n & 4) && (n & 8) && (n & 1);\n" + " if (!(n & 4) && !(n & 8)) llSay(0, \"b\");\n" + " if ((n & 4) || (n & 16)) llSay(0, \"c\");\n" + " integer w = (n & 3) | (n & 12);\n" + " if ((n & 4) && (m & 8)) llSay(0, \"d\");\n" + " if ((n & 6) && (n & 8)) llSay(0, \"e\");\n" + " if ((said() & 4) && (said() & 8)) llSay(0, \"f\");\n" + " llSay(v, (string)w);\n" + " }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Luau }) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + const auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; + // -13 as its cast on Mono (ALLSLCosts::castForNegative). + ensure("all set: " + r.text, has("if (!~(n | -13))") || has("if (!~(n | ((integer)-13)))")); + // v and w are read once, and their values go where they are read. + ensure("three, a value: " + r.text, has("llSay(!~(n | -14), ") || has("llSay(!~(n | ((integer)-14)), ")); + ensure("all clear: " + r.text, has("if (!(n & 12))")); + ensure("any, where only truth counts: " + r.text, has("if (n & 20)")); + ensure("an | of them: " + r.text, has("(string)(n & 15)")); + ensure("two values left: " + r.text, has("(n & 4)") && has("(m & 8)")); + ensure("more than one bit left: " + r.text, has("(n & 6)") && has("(n & 8)")); + ensure("what changes left: " + r.text, has("(said() & 4)") && has("(said() & 8)")); + ensure("noted: " + notes(r), allsloptimizer_data::has(r, "wrote (n & 4) && (n & 8) as !~(n | -13)")); + } + } + template<> template<> + void allsloptimizer_object::test<57>() + { + set_test_name("more of the library folded where the two VMs cannot part (L17): parsing, CSV and sorting where no edge is in play; a " + "rotation between vectors, the angle between rotations and a rotation from axes where every way of working it out agrees"); + // Each may be set again before it is read, so that its fold shows + // in its declaration. + const std::string source = wrap("", " list p = llParseString2List(\"a,b;;c\", [\",\"], [\";\"]);\n" + " list k = llParseStringKeepNulls(\"a,,b,\", [\",\"], []);\n" + " list x = llParseString2List(\"ab\", [\"a\", \"ab\"], []);\n" + " list v = llCSV2List(\"a,b,c\");\n" + " list w = llCSV2List(\"a, b\");\n" + " list s = llListSort([3, \"c\", 1, \"a\", 2, \"b\"], 2, TRUE);\n" + " list t = llListSort([\"pear\", \"apple\", \"fig\"], 1, FALSE);\n" + " list u = llListSort([2, 1, 2], 1, TRUE);\n" + " list m = llListSort([\"b\", \"A\"], 1, TRUE);\n" + " rotation r = llRotBetween(<1, 0, 0>, <0, 1, 0>);\n" + " rotation q = llRotBetween(<1, 0, 0>, <1, 0, 0>);\n" + " rotation n = llRotBetween(<1, 0, 0>, <0, 0, 2>);\n" + " float h = llAngleBetween(<0, 0, 0, 1>, <0, 0, 0, 1>);\n" + " rotation z = llAxes2Rot(<1, 0, 0>, <0, 1, 0>, <0, 0, 1>);\n" + " rotation e = llAxes2Rot(<0, 1, 0>, <-1, 0, 0>, <0, 0, 1>);\n" + " if (llFrand(1) < 0.5)\n {\n p = k = x = v = w = s = t = u = m = [];\n" + " r = q = n = z = e = ZERO_ROTATION;\n h = 1;\n }\n" + " llOwnerSay(llList2CSV(p + k + x + v + w + s + t + u + m) + (string)[r, q, n, h, z, e]);\n"); + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::LSO }) + { + ALLSLOptimizer::Options o = options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + notes(r), r.optimized); + const auto has = [&r](const char* text) { return r.text.find(text) != std::string::npos; }; + ensure("split, spacers kept: " + r.text, has("list p = [\"a\", \"b\", \";\", \";\", \"c\"];")); + ensure("split, empties kept: " + r.text, has("list k = [\"a\", \"\", \"b\", \"\"];")); + ensure("a separator the start of another: left: " + r.text, has("list x = llParseString2List(")); + ensure("CSV: " + r.text, has("list v = [\"a\", \"b\", \"c\"];")); + ensure("a value starting with a space: left: " + r.text, has("list w = llCSV2List(")); + ensure("sorted by stride: " + r.text, has("list s = [1, \"a\", 2, \"b\", 3, \"c\"];")); + ensure("sorted down: " + r.text, has("list t = [\"pear\", \"fig\", \"apple\"];")); + ensure("two keys the same: left: " + r.text, has("list u = llListSort(")); + ensure("keys of two cases: left: " + r.text, has("list m = llListSort(")); + const float half = static_cast(std::sqrt(0.5)); + ensure("a quarter turn between: " + r.text, numbers(r.text, "rotation r") == std::vector{ 0.f, 0.f, half, half }); + ensure("parallel: left: " + r.text, has("rotation q = llRotBetween(")); + ensure("a quarter turn the other way: " + r.text, numbers(r.text, "rotation n") == std::vector{ 0.f, -half, 0.f, half }); + ensure("no angle between the same: " + r.text, numbers(r.text, "float h") == std::vector{ 0.f }); + ensure("the axes themselves: " + r.text, numbers(r.text, "rotation z") == std::vector{ 0.f, 0.f, 0.f, 1.f }); + // Made unit, as LLQuaternion does today, a quarter turn is a bit + // larger in its last place than left as it was, as in 2007. + ensure("ways that part by a bit: left: " + r.text, has("rotation e = llAxes2Rot(")); + } + } + + template<> template<> + void allsloptimizer_object::test<58>() + { + set_test_name("Mono's ++x for x = x + 1, a default left out, and a negative number as its cast; each where its target has it smaller, and what " + "must stay stays"); + const std::string source = "integer gz = 0;\n" + "integer gn = -5;\n" + "default\n" + "{\n" + " state_entry()\n" + " {\n" + " integer i = (integer)llFrand(9);\n" + " integer j = (integer)llFrand(9);\n" + " float f = llFrand(9);\n" + " vector v = llGetPos();\n" + " i = i + 1;\n" + " j -= 1;\n" + " j = i = 1 + i;\n" + " f = f + 1;\n" + " v.x = v.x + 1;\n" + " integer z = 0;\n" + " float y = 0.0;\n" + " float n = -0.0;\n" + " key k = \"\";\n" + " key nk = NULL_KEY;\n" + " string s = \"\";\n" + " vector w = ZERO_VECTOR;\n" + " rotation r = ZERO_ROTATION;\n" + " if (llFrand(1) < 0.5)\n" + " {\n" + " z = 3; y = 1.5; n = 2.5; k = llGetOwner(); nk = llGetOwner(); s = \"a\"; w = llGetPos(); r = llGetRot();\n" + " }\n" + " llSay(-5, \"x\");\n" + " llSetText(\"\", <-1.5, 0, 0>, 1);\n" + " llOwnerSay((string)[i, j, f, v, z, y, n, k, nk, s, w, r, gz, gn]);\n" + " }\n" + " touch_start(integer t)\n" + " {\n" + " gz = t;\n" + " gn = t;\n" + " }\n" + "}\n"; + const auto run = [&](ALLSLOptimizer::Target target) { + ALLSLOptimizer::Options o = allsloptimizer_data::options(); + o.target = target; + const ALLSLOptimizer::Result r = ALLSLOptimizer::run(source, o); + ensure("optimized: " + allsloptimizer_data::notes(r), r.optimized); + ALLSLService service; + const ALScriptProblems said = service.check(r.text, target != ALLSLOptimizer::Target::LSO); + for (const ALScriptProblem& p : said) + { + ensure("no error in what was written: " + p.message + "\n" + r.text, p.severity != ALScriptProblem::Severity::Error); + } + return r.text; + }; + const auto has = [](const std::string& text, const std::string& what) { return text.find(what) != std::string::npos; }; + + const std::string mono = run(ALLSLOptimizer::Target::Mono); + ensure("Mono: x = x + 1 as ++x: " + mono, has(mono, "++i;")); + ensure("Mono: x -= 1 as --x", has(mono, "--j;")); + ensure("Mono: as a value, 1 + x", has(mono, "j = ++i;")); + ensure("Mono: a float's stays", !has(mono, "++f") && !has(mono, "f;")); + ensure("Mono: a vector's part stays", !has(mono, "++v.x")); + ensure("Mono: an integer's default left out", has(mono, "integer z;")); + ensure("Mono: an integer global's too", has(mono, "integer gz;")); + ensure("Mono: a float's is not, there", !has(mono, "float y;")); + ensure("Mono: a negative number as its cast", has(mono, "llSay(((integer)-5), ")); + ensure("Mono: a vector's negative part", has(mono, "<((float)-1.5), 0, 0>")); + ensure("Mono: never in a global's value", has(mono, "integer gn = -5;")); + + const std::string lso = run(ALLSLOptimizer::Target::LSO); + ensure("LSO: x = x + 1 stays as LSO has it smaller: " + lso, !has(lso, "++i;") && !has(lso, "--j;")); + ensure("LSO: a float's default left out", has(lso, "float y;")); + ensure("LSO: -0.0 is not the default", !has(lso, "float n;")); + ensure("LSO: a key's default, \"\"", has(lso, "key k;")); + ensure("LSO: NULL_KEY is not a key's default", !has(lso, "key nk;")); + ensure("LSO: a string's never", !has(lso, "string s;")); + ensure("LSO: a vector's and a rotation's", has(lso, "vector w;") && has(lso, "rotation r;")); + ensure("LSO: an integer's is not, there", !has(lso, "integer z;")); + ensure("LSO: no casts of negative numbers", !has(lso, "((integer)-") && !has(lso, "((float)-")); + + const std::string luau = run(ALLSLOptimizer::Target::Luau); + ensure("Luau: a key's default left out: " + luau, has(luau, "key k;")); + ensure("Luau: nothing else", !has(luau, "integer z;") && !has(luau, "++i;") && !has(luau, "((integer)-")); + } + + + template<> template<> + void allsloptimizer_object::test<59>() + { + set_test_name("operators as each target has them smaller: && of truths as &, a - b as a + -b, -1 - x as ~x, a shift left as a product, a " + "doubling as a sum, a float halved as a product, x * -1 as -x; Luau's bitwise tests as arithmetic"); + const std::string source = kept(wrap("integer g;\nfloat h;\n", + " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n" + " integer c = llStringLength(llGetObjectName());\n float f = llFrand(9);\n float e = llFrand(9);\n" + " g = a - b;\n" + " g -= b;\n" + " h = f - e;\n" + " g = a << 3;\n" + " g = -1 - b;\n" + " g = a * 2;\n" + " h = f / 4;\n" + " g = b * -1;\n" + " if (a < b && b > 2)\n llOwnerSay(\"both\");\n" + " if (a & 1)\n llOwnerSay(\"odd\");\n" + " g = a == b;\n" + " g = c >> 2;\n" + " g = c & 7;\n" + " g = a >> 2;\n" + " llOwnerSay((string)[a, b, c, f, e, g, h]);\n"), { "g", "h" }); + const std::string mono = optimized(source, ALLSLOptimizer::Target::Mono); + ensure("Mono: a - b as a + -b: " + mono, in(mono, "g = a + -b;")); + ensure("Mono: -= as += -", in(mono, "g += -b;")); + ensure("Mono: of floats", in(mono, "h = f + -e;")); + ensure("Mono: a shift as a product", in(mono, "g = a * 8;")); + ensure("Mono: -1 - x as ~x", in(mono, "g = ~b;")); + ensure("Mono: a doubling as a sum", in(mono, "g = a + a;")); + ensure("Mono: a float by a power of two as a product", in(mono, "h = f * 0.25;")); + ensure("Mono: x * -1 as -x", in(mono, "g = -b;")); + ensure("Mono: && of truths as &", in(mono, "if (a < b & b > 2)")); + ensure("Mono: the bitwise stay", in(mono, "if (a & 1)") && in(mono, "g = c >> 2;") && in(mono, "g = c & 7;")); + + const std::string lso = optimized(source, ALLSLOptimizer::Target::LSO); + ensure("LSO: a - b stays: " + lso, in(lso, "g = a - b;") && in(lso, "g -= b;") && in(lso, "h = f - e;")); + ensure("LSO: -1 - x as ~x", in(lso, "g = ~b;")); + ensure("LSO: x * -1 as -x", in(lso, "g = -b;")); + ensure("LSO: the rest stays", in(lso, "g = a << 3;") && in(lso, "g = a * 2;") && in(lso, "h = f / 4;") && in(lso, "if (a < b && b > 2)")); + + const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); + ensure("Luau: a shift as a product: " + luau, in(luau, "g = a * 8;")); + ensure("Luau: a doubling as a sum", in(luau, "g = a + a;")); + ensure("Luau: -1 - x stays, ~ a call there", in(luau, "g = -1 - b;")); + ensure("Luau: an odd test as a remainder", in(luau, "if (a % 2)")); + ensure("Luau: == as the ! of a difference", in(luau, "g = !(a - b);")); + ensure("Luau: a shift right and a mask of a count as a quotient and a remainder", in(luau, "g = c / 4;") && in(luau, "g = c % 8;")); + ensure("Luau: not of what may be below nought", in(luau, "g = a >> 2;")); + ensure("Luau: a - b stays", in(luau, "g = a - b;")); + } + + template<> template<> + void allsloptimizer_object::test<60>() + { + set_test_name("branches in fewer jumps: a truth returned or set, a select as arithmetic, a value set before an if, ways the same once, an " + "else after a return dropped, a jump over statements as an else, an empty if gone, nested ifs merged"); + const std::string source = "integer g;\nstring t;\n" + "integer big(integer a)\n{\n if (a > 3)\n return TRUE;\n return FALSE;\n}\n" + "integer none(integer a)\n{\n if (a)\n return FALSE;\n else\n return TRUE;\n}\n" + "integer pick(integer a)\n{\n if (a)\n {\n llOwnerSay(\"one\");\n return 1;\n }\n else\n {\n" + " llOwnerSay(\"other\");\n }\n return 2;\n}\n" + "default\n{\n state_entry()\n {\n" + " integer a = (integer)llFrand(9);\n integer b = (integer)llFrand(9);\n" + " if (a > 2)\n g = 1;\n else\n g = 0;\n" + " if (b > 2)\n g = 5;\n else\n g = 7;\n" + " if (b)\n g = 5;\n else\n g = 7;\n" + " string u;\n if (a > 4)\n u = llGetObjectName();\n else\n u = \"none\";\n" + " if (a > 5)\n t = llGetObjectName();\n else\n t = \"global\";\n" + " if (llFrand(1) > 0.5)\n llOwnerSay(\"same\");\n else\n llOwnerSay(\"same\");\n" + " if (a == b)\n {\n llOwnerSay(\"eq\");\n llOwnerSay(\"end\");\n }\n" + " else\n {\n llOwnerSay(\"ne\");\n llOwnerSay(\"end\");\n }\n" + " if (b)\n {\n llOwnerSay(\"x\");\n jump past;\n }\n" + " llOwnerSay(\"y\");\n @past;\n" + " if (a < 3)\n {\n }\n" + " if (a > 1)\n if (b < 4)\n llOwnerSay(\"inner\");\n" + " if (a)\n if (b)\n llOwnerSay(\"both\");\n" + " if (a)\n if (100 / b)\n llOwnerSay(\"faults\");\n" + " llOwnerSay((string)[big(a), none(b), pick(a), g, t, u]);\n }\n" + " touch_start(integer n)\n {\n llOwnerSay((string)g + t);\n }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + const bool lso = target == ALLSLOptimizer::Target::LSO; + const bool mono = target == ALLSLOptimizer::Target::Mono; + ensure("a truth returned: " + text, in(text, "return a > 3;")); + ensure("its ! the other way", in(text, "return !a;")); + ensure("a truth set", in(text, "g = a > 2;")); + ensure("a select of a truth as arithmetic, on every target: " + text, in(text, "g = (b > 2) * ") && in(text, " + 7;")); + ensure("of anything else, on LSO and Mono", lso || mono ? in(text, "g = !!b * ") : !in(text, "g = !!b")); + ensure("a local's value set before the if: " + text, in(text, "u = \"none\";\n if (a > 4)")); + ensure("not a global's, which a call in the check could read", in(text, "t = \"global\";\n") && !in(text, "t = \"global\";\n if")); + ensure("ways the same written once", !in(text, "else\n llOwnerSay(\"same\")") && in(text, "llOwnerSay(\"same\");")); + ensure("what both ways end with once, after", in(text, "llOwnerSay(\"end\");") && text.find("llOwnerSay(\"end\")") == text.rfind("llOwnerSay(\"end\")")); + ensure("an else after a return dropped", !in(text, "else\n {\n llOwnerSay(\"other\")")); + ensure("a jump over statements as an else", !in(text, "jump past") && !in(text, "@past")); + ensure("an empty if gone", !in(text, "a < 3")); + ensure("nested ifs of truths merged on LSO and Mono: " + text, lso || mono ? in(text, "if ((a > 1) & (b < 4))") : in(text, "if (a > 1)\n if (b < 4)")); + ensure("nested ifs merged with && on LSO", lso ? in(text, "if (a && b)") : !in(text, "if (a && b)")); + ensure("not where the inner may stop the script", in(text, "100 / b")); + } + } + + template<> template<> + void allsloptimizer_object::test<61>() + { + set_test_name("loops: a for or a while whose first check passes run as a do on LSO and Mono, the step in its check; a counter read nowhere else " + "counted down; not where the check is not known, nor the counter read"); + const std::string source = "default\n{\n state_entry()\n {\n" + " list l = llParseString2List(llGetObjectName(), [\" \"], []);\n" + " integer i;\n integer j;\n integer k;\n integer m;\n integer n = (integer)llFrand(9);\n" + " for (i = 0; i < 10; ++i)\n llOwnerSay((string)i);\n" + " for (j = 0; j < 3; ++j)\n llOwnerSay(\"three\");\n" + " for (k = 0; k < llGetListLength(l); ++k)\n llOwnerSay(\"each\");\n" + " for (m = n; m < 10; ++m)\n llOwnerSay((string)m);\n" + " integer w = 0;\n while (w < 5)\n {\n llOwnerSay((string)w);\n w += 2;\n }\n" + " integer z;\n for (z = 5; z < 3; ++z)\n llOwnerSay(\"never\");\n" + " llOwnerSay((string)z);\n" + " }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + const bool luau = target == ALLSLOptimizer::Target::Luau; + ensure("a for known to run as a do, its step in the check: " + text, luau ? in(text, "for (i = 0; i < 10; ++i)") : in(text, "while(++i < 10);")); + ensure("a counter read nowhere else counted down", in(text, "j = 3;") && in(text, "while(--j);")); + ensure("to a count never below nought: " + text, (in(text, "integer k = llGetListLength(l);") || in(text, "integer k = (l != []);")) && in(text, "while (k--)")); + ensure("not where the first check is not known", in(text, "for (m = n; m < 10; ++m)")); + ensure("a while known to run as a do", luau ? in(text, "while (w < 5)") : in(text, "while(w < 5);")); + ensure("a loop known never to run gone, its first part kept: " + text, !in(text, "never") && in(text, "llOwnerSay(\"5\");")); + } + } + + template<> template<> + void allsloptimizer_object::test<62>() + { + set_test_name("a key that is a key found or NULL_KEY tested as itself: k != NULL_KEY as k, k == NULL_KEY as k with its ways turned round; " + "not of a key that may be any string"); + const std::string source = "default\n{\n" + " touch_start(integer d)\n {\n" + " key owner = llGetOwner();\n" + " if (owner != NULL_KEY)\n llOwnerSay(\"owned\");\n" + " if (llDetectedKey(0) == NULL_KEY)\n llOwnerSay(\"nobody\");\n else\n llOwnerSay(\"somebody\");\n" + " key sitter = llAvatarOnSitTarget();\n" + " if (sitter == NULL_KEY)\n llOwnerSay(\"empty\");\n" + " key given = (key)llGetObjectDesc();\n" + " if (given != NULL_KEY)\n llOwnerSay(\"given\");\n" + " }\n" + " listen(integer c, string n, key id, string m)\n {\n" + " if (id != NULL_KEY)\n llOwnerSay(m);\n }\n" + " link_message(integer s, integer num, string str, key id)\n {\n" + " if (id != NULL_KEY)\n llOwnerSay(str);\n }\n" + "}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + ensure("a local set to the owner: " + text, in(text, "if (owner)")); + ensure("a detected key, its ways turned round", in(text, "if (llDetectedKey(0))\n llOwnerSay(\"somebody\");\n else\n llOwnerSay(\"nobody\");")); + ensure("a lone == turned round", in(text, "if (sitter)\n ;\n else\n llOwnerSay(\"empty\");")); + ensure("a listen's speaker: " + text, in(text, "if (id)\n llOwnerSay(m);")); + ensure("not a key cast from a string", in(text, "if (given != ")); + ensure("not a link message's key, which is any string", in(text, "if (id != ") && in(text, "llOwnerSay(str);")); + } + } + + template<> template<> + void allsloptimizer_object::test<63>() + { + set_test_name("functions given and giving what their calls need: a parameter never read taken away with what each call gives it, one every " + "call gives the same constant made a local, a value no call reads no longer returned; not where a call's argument does " + "something, the calls differ, the parameter is set, or a call reads the value"); + const std::string source = "integer got;\n" + "say(integer channel, string message, integer unused)\n{\n llSay(channel, message);\n}\n" + "tell(integer quiet, string message)\n{\n llOwnerSay(message);\n}\n" + "integer noisy(integer n)\n{\n llOwnerSay((string)n);\n return n * 2;\n}\n" + "integer read(integer n)\n{\n return n + 1;\n}\n" + "set(integer n)\n{\n n = n + 1;\n llOwnerSay((string)n);\n}\n" + "default\n{\n state_entry()\n {\n" + " say(0, \"a\", 1);\n say(0, \"b\", 2);\n" + " tell(llListen(5, \"\", \"\", \"\"), \"c\");\n tell(3, \"d\");\n" + " noisy(llGetUnixTime());\n noisy(4);\n" + " got = read(llGetUnixTime());\n" + " set(7);\n set(7);\n" + " }\n" + " touch_start(integer t)\n {\n llOwnerSay((string)got);\n }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + ensure("a parameter never read gone, and its arguments: " + text, in(text, "say(string message)") && in(text, "say(\"a\");")); + ensure("one every call gives the same constant made its value", in(text, "llSay(0, message);")); + ensure("not where a call's argument does something", in(text, "tell(integer quiet, string message)") && in(text, "llListen(5")); + ensure("a value no call reads no longer returned", in(text, "\nnoisy(integer n)") && !in(text, "integer noisy")); + ensure("what it returned still worked out where it does something, or gone", !in(text, "return n * 2") || in(text, "n * 2;")); + ensure("not where a call reads it", in(text, "integer read(integer n)")); + ensure("not a parameter that is set", in(text, "set(integer n)") && in(text, "set(7);")); + } + } + + template<> template<> + void allsloptimizer_object::test<64>() + { + set_test_name("a value set again before it is read is not set; a global every handler sets before it reads made a local of each -- on " + "Mono, whose locals each cost a frame, only where that is less than the global; not one read before it is set, nor one a " + "function also sets; a || 1 and a && 0 settled"); + const std::string source = "integer scratch;\n" + "integer twice;\n" + "integer kept = 3;\n" + "integer shared;\n" + "poke(integer n)\n{\n shared = n;\n}\n" + "default\n{\n" + " state_entry()\n {\n" + " integer i = llGetUnixTime();\n i = (integer)llFrand(9);\n llOwnerSay((string)i);\n" + " integer j = 4;\n for (j = 0; j < (integer)llFrand(9); ++j)\n llOwnerSay((string)j);\n" + " scratch = llGetUnixTime();\n llOwnerSay((string)scratch);\n" + " twice = llGetUnixTime();\n llOwnerSay((string)twice);\n" + " shared = 1;\n poke(2);\n llOwnerSay((string)shared);\n" + " if ((integer)llFrand(2) || 1)\n llOwnerSay(\"always\");\n" + " if ((integer)llFrand(2) && 0)\n llOwnerSay(\"never\");\n" + " }\n" + " touch_start(integer t)\n {\n twice = t;\n llOwnerSay((string)twice + (string)kept);\n" + " kept = t;\n }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + // A read of the clock changes nothing, and goes with it. + ensure("a value set again before it is read not set: " + text, in(text, "integer i = (integer)llFrand(9);")); + ensure("a for's first part sets again", !in(text, "integer j = 4;")); + ensure("a global one handler sets before it reads made a local", in(text, "integer scratch = llGetUnixTime();") && !in(text, "integer scratch;\n")); + ensure("one two handlers set before they read, a local of each but on Mono", + target == ALLSLOptimizer::Target::Mono ? in(text, "integer twice;\n") : in(text, "integer twice = llGetUnixTime();") && in(text, "llOwnerSay((string)t + ")); + ensure("not one read before it is set", in(text, "integer kept")); + ensure("not one a function sets too", in(text, "integer shared")); + ensure("a || 1 settled true", in(text, "llOwnerSay(\"always\")") && !in(text, "|| 1")); + ensure("a && 0 settled false", !in(text, "never")); + } + } + + template<> template<> + void allsloptimizer_object::test<65>() + { + set_test_name("a global the script sets read again and again read once into a local, where nothing between sets it; a constant written at " + "many places kept once, in a global where the target holds one smaller so, else in a local; each on its target, Mono's " + "locals paying for their frames"); + const std::string source = "integer channel;\n" + "integer never = 7;\n" + "default\n{\n" + " state_entry()\n {\n channel = (integer)llFrand(1000);\n }\n" + " touch_start(integer n)\n {\n" + " llSay(channel, \"one\");\n llSay(channel, \"two\");\n llSay(channel, \"three\");\n" + " channel = n;\n llSay(channel, (string)never);\n llSay(never, (string)never);\n" + " llOwnerSay(\"a sentence long enough to be worth holding\");\n" + " llOwnerSay(\"a sentence long enough to be worth holding\");\n" + " llOwnerSay(\"a sentence long enough to be worth holding\");\n" + " llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n" + " llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n llSetPos(llGetPos() + <1.5, 2.5, 3.5>);\n" + " }\n}\n"; + const std::string lso = optimized(source, ALLSLOptimizer::Target::LSO); + ensure("LSO: a long string at three places kept in a global: " + lso, in(lso, "string s = \"a sentence long enough to be worth holding\";") && + in(lso, "llOwnerSay(s);")); + ensure("LSO: a vector at four places, in a global", in(lso, "vector v = <1.5, 2.5, 3.5>;")); + ensure("LSO: a global's reads stay, as large as a local's there", in(lso, "llSay(channel, \"one\");")); + + const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); + ensure("Luau: a global read three times read once: " + luau, in(luau, "integer localChannel = channel;") && in(luau, "llSay(localChannel, \"one\");")); + ensure("Luau: not across where it is set", in(luau, "channel = n;")); + ensure("Luau: never one the script never sets, which is the folder's", !in(luau, "localNever")); + ensure("Luau: constants held once in a function already", !in(luau, "string s = ")); + + const std::string mono = optimized(source, ALLSLOptimizer::Target::Mono); + ensure("Mono: a vector at four places kept in a local, its frame paid for: " + mono, in(mono, "vector v = <1.5, 2.5, 3.5>;")); + ensure("Mono: three reads of a global not worth a local's frame", !in(mono, "localChannel")); + ensure("Mono: a string held once already", !in(mono, "string s = ")); + } + + template<> template<> + void allsloptimizer_object::test<66>() + { + set_test_name("two string literals joined on LSO, which writes each where it is used, and where the option says elsewhere; a loop that " + "never ends run by a jump back on LSO and Mono"); + const std::string source = "default\n{\n touch_start(integer n)\n {\n" + " llOwnerSay(\"Hello, \" + \"world\");\n" + " while (TRUE)\n {\n llOwnerSay((string)llFrand(1));\n" + " if (llFrand(1) > 0.5)\n jump out;\n }\n" + " @out;\n llOwnerSay(\"done\");\n }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + const bool lso = target == ALLSLOptimizer::Target::LSO; + const bool luau = target == ALLSLOptimizer::Target::Luau; + ensure("two literals joined on LSO alone, by default: " + text, lso ? in(text, "\"Hello, world\"") : in(text, "\"Hello, \" + \"world\"")); + ensure("a loop that never ends by a jump back, not on Luau", luau ? in(text, "while (TRUE)") : in(text, "@loop;") && in(text, "jump loop;")); + // Its jump out and back made one jump back where the check fails. + ensure("what follows it still reached: " + text, in(text, "llOwnerSay(\"done\");") && (luau || in(text, "jump loop;\n llOwnerSay(\"done\");"))); + } + ALLSLOptimizer::Options o = options(); + o.addstrings = true; + ensure("joined on Mono where the option asks", in(ALLSLOptimizer::run(source, o).text, "\"Hello, world\"")); + } + + template<> template<> + void allsloptimizer_object::test<67>() + { + set_test_name("a string cast to an integer folds as the grid's 32-bit hosts cast one, past 0xFFFFFFFF -1; a JSON number past 32 bits " + "is left to the simulator"); + const std::string source = "default\n{\n touch_start(integer n)\n {\n" + " llOwnerSay((string)((integer)\"4294967296\"));\n" + " llOwnerSay((string)((integer)\"-4294967296\"));\n" + " llOwnerSay((string)((integer)\"0x1FFFFFFFF\"));\n" + " llOwnerSay((string)((integer)\"99999999999999999999\"));\n" + " llOwnerSay((string)((integer)\"3000000000\"));\n" + " llOwnerSay((string)((integer)\" 12abc\"));\n" + " llOwnerSay((string)((integer)\"0x10\"));\n" + " llOwnerSay((string)((integer)\"-0x10\"));\n" + " llOwnerSay(llList2CSV(llJson2List(\"[9999999999]\")) + llList2CSV(llJson2List(\"[2147483647, -2147483648]\")));\n" + " }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono, ALLSLOptimizer::Target::Luau }) + { + const std::string text = optimized(source, target); + size_t minus_ones = 0; + for (size_t at = text.find("llOwnerSay(\"-1\");"); at != std::string::npos; at = text.find("llOwnerSay(\"-1\");", at + 1)) + { + ++minus_ones; + } + ensure("past 0xFFFFFFFF, -1, its minus or not: " + text, minus_ones == 4); + ensure("within it, wrapped below nought: " + text, in(text, "llOwnerSay(\"-1294967296\");")); + ensure("spaces and what follows the number, hexadecimal only from a leading 0x: " + text, + in(text, "llOwnerSay(\"12\");") && in(text, "llOwnerSay(\"16\");") && in(text, "llOwnerSay(\"0\");")); + ensure("a JSON number past 32 bits left: " + text, in(text, "llJson2List(\"[9999999999]\")")); + ensure("one within them folded: " + text, in(text, "2147483647") && !in(text, "llJson2List(\"[2147483647")); + } + } + + template<> template<> + void allsloptimizer_object::test<68>() + { + set_test_name("a float function of the library folds where in double rounded once and in single precision it agrees, to a single on " + "every target, as each VM answers; a pow or an atan2 of -0 is 0; on Luau, sin of no single and a length no single " + "is are left"); + const std::string source = "default\n{\n touch_start(integer n)\n {\n" + " llOwnerSay((string)llPow(2.0, 10.0));\n" + " llOwnerSay((string)llSqrt(2.0));\n" + " llOwnerSay((string)llAtan2(-0.0, 1.0));\n" + " llOwnerSay((string)llPow(-0.0, 3.0));\n" + " llOwnerSay((string)llSin(1.0));\n" + " llOwnerSay((string)llSin(0.1 + 0.2));\n" + " llOwnerSay((string)llVecMag(<1, 1, 0>));\n" + " llOwnerSay((string)llVecMag(<0, 3, 4>));\n" + " }\n}\n"; + for (const ALLSLOptimizer::Target target : { ALLSLOptimizer::Target::LSO, ALLSLOptimizer::Target::Mono }) + { + const std::string text = optimized(source, target); + ensure("each folded as the VM's single: " + text, + in(text, "llOwnerSay(\"1024.000000\");") && in(text, "llOwnerSay(\"1.414214\");") && in(text, "llOwnerSay(\"0.841471\");") && + in(text, "llOwnerSay(\"0.295520\");") && in(text, "llOwnerSay(\"5.000000\");") && !in(text, "llSin") && !in(text, "llVecMag")); + ensure("-0 made 0, not -0: " + text, !in(text, "-0.000000")); + } + const std::string luau = optimized(source, ALLSLOptimizer::Target::Luau); + ensure("a single, not the double: " + luau, in(luau, "(string)0.8414709568023682") && in(luau, "(string)1.4142135381698608")); + ensure("exact, folded: " + luau, in(luau, "(string)1024.0") && in(luau, "(string)5.0")); + ensure("-0 made 0: " + luau, !in(luau, "-0.0")); + ensure("sin of what is no single, which SLua takes as it is, left: " + luau, in(luau, "llSin(0.30000000447034836)")); + ensure("a length no single is, which the simulator gives by a way not known, left: " + luau, in(luau, "llVecMag(")); + } } // namespace tut diff --git a/indra/alscript/tests/allslservice_test.cpp b/indra/alscript/tests/allslservice_test.cpp index 9daefb19fc0..72a8e22789e 100644 --- a/indra/alscript/tests/allslservice_test.cpp +++ b/indra/alscript/tests/allslservice_test.cpp @@ -24,8 +24,8 @@ #include "linden_common.h" -#include "../allslservice.h" -#include "../alscriptfixes.h" +#include "../lsl/allslservice.h" +#include "../lint/alscriptfixes.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 68543ba1ef9..7a44fbe9621 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -24,10 +24,10 @@ #include "linden_common.h" -#include "../allsltoslua.h" -#include "../alscriptfixes.h" -#include "../allslservice.h" -#include "../alluauservice.h" +#include "../lsl/allsltoslua.h" +#include "../lint/alscriptfixes.h" +#include "../lsl/allslservice.h" +#include "../luau/alluauservice.h" #include "../test/lltut.h" @@ -190,13 +190,16 @@ namespace tut ensure("and: " + r.text, has(r, "if a > 1 and #l > 1 then")); ensure("integer division: " + r.text, has(r, "local q = a // b") && noted(r, "SluaIntegerDivision")); ensure("remainder: " + r.text, has(r, "local m = a % b") && noted(r, "SluaModulo")); - ensure("a list no other holds grown in place: " + r.text, has(r, "table.insert(l, 3)") && has(r, "table.insert(l, 4)") && - has(r, "table.insert(l, \"five\")") && !has(r, "joinLists")); + ensure("a list no other holds grown in place: " + r.text, has(r, "table.insert(l, 3)") && has(r, "table.append(l, 4, \"five\")") && + !has(r, "joinLists")); ensure("TRUE and FALSE: " + r.text, has(r, "local t = 1") && has(r, "if false then")); ensure("a float as LSL writes it: " + r.text, has(r, "string.format(\"%.6f\", f)")); - ensure("a string as an integer: " + r.text, has(r, "lslInteger(\"12abc\")") && has(r, "local function lslInteger")); + ensure("a string as an integer, as a list's item converts it: " + r.text, + has(r, "lslInteger(\"12abc\")") && has(r, "return llcompat.List2Integer({ s }, 0)")); ensure("lists compared by length: " + r.text, has(r, "#l == #{1}") && noted(r, "SluaListCompare")); - ensure("bits: " + r.text, has(r, "bit32.bor(bit32.band(a, b), 4)") && noted(r, "SluaBit32")); + // bit32 answers 0 to 4294967295: made LSL's signed integer, but where + // bit32 takes it again. + ensure("bits: " + r.text, has(r, "bit32.s32(bit32.bor(bit32.band(a, b), 4))") && !has(r, "local function int32")); ensure("a vector's part set: " + r.text, has(r, "v = vector(4, v.y, v.z)") && has(r, "v = vector(v.x, v.y, v.z + 1)")); ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({v}, \"\")")); checksClean(r); @@ -224,7 +227,7 @@ namespace tut "} }\n"); ensure("a numeric for: " + r.text, has(r, "for i = 0, 9 do")); ensure("its counter declared by it alone: " + r.text, !has(r, "local i = 0")); - ensure("continue, the step Luau's own: " + r.text, has(r, "then\n continue")); + ensure("continue, the step Luau's own: " + r.text, has(r, "then continue end")); ensure("break: " + r.text, has(r, "break")); ensure("repeat: " + r.text, has(r, "repeat") && has(r, "until not (n > 5)")); ensure("a step: " + r.text, has(r, "n -= 1")); @@ -268,7 +271,8 @@ namespace tut " llOwnerSay((string)first(j));\n" "} }\n"); ensure("renamed: " + r.text, has(r, "local table_ = 1") && has(r, "(end_)")); - ensure("declared first: " + r.text, has(r, "local first, second\n") && has(r, "function first(end_)")); + ensure("only the one called before it is written declared first: " + r.text, + has(r, "local second\n") && has(r, "local function first(end_)") && has(r, "\nfunction second(x)")); ensure("an assignment in place: " + r.text, has(r, "(function() i = 4 return i end)()") && noted(r, "SluaAssignInExpression")); ensure("a step after, what it was: " + r.text, has(r, "(function() local was = i; i += 1 return was end)()")); checksClean(r); @@ -354,7 +358,10 @@ namespace tut const ALLSLToSLua::Result many = ALLSLToSLua::convert("default { touch_start(integer n) { state other; } }\n" "state other { touch_start(integer n) { state default; } }\n", options); - ensure("setState sets the fields: " + many.text, has(many, "(LLEvents :: any)[event] = handler") && has(many, "(LLEvents :: any)[event] = nil")); + // SLua stops the script at a field assigned nil: taken off by + // LLEvents:off, as LLEvents:on's are. + ensure("setState sets the fields: " + many.text, has(many, "(LLEvents :: any)[event] = handler") && + has(many, "LLEvents:off(event :: any, handler)") && !has(many, "[event] = nil")); checksClean(many); } @@ -515,7 +522,7 @@ namespace tut has(r, " table.insert(gAll, item)")); ensure("prepended: " + r.text, has(r, "table.insert(gAll, 1, \"first\")")); ensure("a list by its name: " + r.text, has(r, "table.move(more, 1, #more, #gAll + 1, gAll)")); - ensure("several that do not run apart: " + r.text, has(r, "table.move({ll.GetTime(), ll.Frand(1.0)}, 1, 2, #gAll + 1, gAll)")); + ensure("several that do not run apart, each had before any is added: " + r.text, has(r, "table.append(gAll, ll.GetTime(), ll.Frand(1.0))")); ensure("passed to a function that only reads it: " + r.text, has(r, "table.insert(gSeen, n)")); ensure("passed to one that keeps it: " + r.text, has(r, "gPassed = joinLists(gPassed, {n})") && @@ -650,7 +657,7 @@ namespace tut ensure("a key against a key: " + r.text, has(r, "if k ~= NULL_KEY then")); ensure("a string where LSL's string was: " + r.text, has(r, "tostring(NULL_KEY) ~= \"\"") && has(r, "ll.GetSubString(tostring(NULL_KEY), 1, 8)")); ensure("in a list, as LSL had it: " + r.text, has(r, "{tostring(NULL_KEY), tostring(TEXTURE_BLANK)}")); - ensure("a string's truth: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then\n print(")); + ensure("a string's truth: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then print(")); ensure("given to a string and a key: " + r.text, has(r, "local gName = tostring(NULL_KEY)") && has(r, "local gKey = NULL_KEY")); ensure("returned as a string function's: " + r.text, has(r, "return tostring(NULL_KEY)")); checksClean(r); @@ -762,7 +769,7 @@ namespace tut template<> template<> void allsltoslua_object::test<24>() { - set_test_name("a note only where it says something: bit32's range not for an & with a flag nor a truth, && not over a read, a deprecated call's reason rather than its indexes"); + set_test_name("a note only where it says something: && not over a read, a deprecated call's reason rather than its indexes; bit32's range made LSL's, not for an & with a flag nor a truth"); const ALLSLToSLua::Result r = convert("integer sayIt() { llOwnerSay(\"x\"); return 1; }\n" "default { changed(integer what) {\n" " if (what & CHANGED_LINK && llGetLinkNumber() == 0) llDie();\n" @@ -771,10 +778,10 @@ namespace tut " llOwnerSay(llList2String(l, what) + (string)m);\n" " if ((what & 3) && sayIt()) llDie();\n" "} }\n"); - ensure("bit32's range where it shows: " + r.text, - count(r, "-- LSL: bit32 answers") == 1 && has(r, "-- LSL: bit32 answers 0 to 4294967295; LSL's integers were signed, from -2147483648.\n local m")); + ensure("bit32's range made LSL's where it shows: " + r.text, has(r, "local m = bit32.s32(bit32.bor(what, ") && count(r, "-- LSL: bit32") == 1); + ensure("not for a flag's truth: " + r.text, has(r, "if bit32.btest(what, CHANGED_LINK)")); ensure("&& said once, over a call that does something: " + r.text, - count(r, "leaves its right side unrun") == 1 && has(r, "LSL ran both sides, the right one first.\n if bit32.band(what, 3) ~= 0 and sayIt()")); + count(r, "leaves its right side unrun") == 1 && has(r, "LSL ran both sides, the right one first.\n if bit32.btest(what, 3) and sayIt()")); ensure("the deprecated call's reason only: " + r.text, has(r, "-- LSL: SLua deprecates ll.List2String") && !has(r, "takes indexes from 0")); checksClean(r); } @@ -837,7 +844,7 @@ namespace tut " gOn = !gOn; if (gOn) llOwnerSay(\"on\");\n" "} }\n"); ensure("not a number: the other way round: " + nots.text, - has(nots, "if n == 0 then") && has(nots, "if bit32.band(n, 4) == 0 then") && has(nots, "if not (n > 1) then") && + has(nots, "if n == 0 then") && has(nots, "if not bit32.btest(n, 4) then") && has(nots, "if not (n > 1) then") && has(nots, "gOn = not gOn")); checksClean(nots); ensure("the check itself: " + r.text, has(r, " gOn = n > 1\n") && has(r, " gOn = not (n > 5)\n")); @@ -1111,4 +1118,351 @@ namespace tut ensure("the other built from it: " + r.text, has(r, "table.insert(allParts, line)") && has(r, "local all = table.concat(allParts)")); checksClean(r); } + + template<> template<> + void allsltoslua_object::test<36>() + { + set_test_name("bit32's answers made LSL's signed integers where they can pass 2147483647 -- a message number with its top bit set, the mask " + "that finds it, ~, a shift -- and not where bit32 takes them again, only truth is asked, or an & is with a constant not below nought"); + // A protocol of link messages whose numbers have their top bit set, + // as a set of scripts may share: before, the mask's answer was never + // the message's number, and no message was heard. + const ALLSLToSLua::Result r = convert("integer MASK = 0xFFFF0000;\n" + "integer MSG = 0xB3470000;\n" + "integer FN_MASK = 0x0000FF00;\n" + "integer FN_RESET = 0x0500;\n" + "default {\n" + " state_entry() {\n" + " llMessageLinked(LINK_SET, MSG | FN_RESET, \"\", \"\");\n" + " }\n" + " link_message(integer sender, integer num, string text, key id) {\n" + " if ((num & MASK) == MSG) {\n" + " integer fn = num & FN_MASK;\n" + " if (fn == FN_RESET) llOwnerSay(\"reset\");\n" + " }\n" + " integer inverse = ~num;\n" + " integer high = num << 4;\n" + " integer low = num >> 2;\n" + " integer part = FN_MASK >> 8;\n" + " if (~llGetPermissions() & PERMISSION_TRIGGER_ANIMATION) llOwnerSay(\"ask\");\n" + " llOwnerSay((string)(inverse + high + low + part));\n" + " }\n" + "}\n"); + ensure("the mask's answer signed: " + r.text, has(r, "if bit32.s32(bit32.band(num, MASK)) == MSG then")); + ensure("the number sent signed: " + r.text, has(r, "ll.MessageLinked(LINK_SET, bit32.s32(bit32.bor(MSG, FN_RESET)), \"\", \"\")")); + ensure("an & with a constant not below nought as it is: " + r.text, has(r, "local fn = bit32.band(num, FN_MASK)\n")); + ensure("~ signed: " + r.text, has(r, "local inverse = bit32.s32(bit32.bnot(num))")); + ensure("<< signed: " + r.text, has(r, "local high = bit32.s32(bit32.lshift(num, 4))")); + ensure(">> of what may be below nought signed: " + r.text, has(r, "local low = bit32.s32(bit32.arshift(num, 2))")); + ensure(">> of a constant not below nought as it is: " + r.text, has(r, "local part = bit32.arshift(FN_MASK, 8)\n")); + ensure("a truth, and what bit32 takes again, as they are: " + r.text, + has(r, "if bit32.btest(bit32.bnot(ll.GetPermissions()), PERMISSION_TRIGGER_ANIMATION) then")); + ensure("SLua's own bit32.s32, no helper, said once: " + r.text, !has(r, "local function int32") && count(r, "-- LSL: bit32 answers") == 1); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<37>() + { + set_test_name("the script's comments carried over, each over what it stood over: the script's own at the top, a global's, those inside a list, " + "a trailing one, a brace's, between branches, at a block's end; // as --, /* */ as --[[ ]] at a level nothing in it closes"); + const std::string lsl = "// Door script by Someone.\n" + "// Do as you like with it.\n" + "\n" + "// How far it swings, in degrees\n" + "float SWING = 90.0; // a right angle\n" + "list NAMES = [\n" + " \"open\", // when it is open\n" + " \"closed\" // when it is shut\n" + "];\n" + "/* old code:\n" + "integer unused() { return 1; }\n" + "*/\n" + "\n" + "// Turns the door.\n" + "swing(float by) { // by degrees\n" + " // the rotation it turns by\n" + " rotation r = llEuler2Rot(<0, 0, by * DEG_TO_RAD>);\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_ROT_LOCAL, r * llGetLocalRot()]); // turn\n" + " if (by > 0) {\n" + " llOwnerSay(\"opening\");\n" + " }\n" + " // when it shuts\n" + " else if (by < 0) {\n" + " llOwnerSay(\"closing\");\n" + " }\n" + " if (FALSE) {\n" + " /* left off */\n" + " } else if (by == 0) {\n" + " llOwnerSay(\"still\");\n" + " }\n" + " // done\n" + "}\n" + "\n" + "default {\n" + " // on a touch\n" + " touch_start(integer n) {\n" + " swing(SWING); //[[ not a block of Luau's ]]\n" + " /* it ends ]] here */\n" + " }\n" + "}\n"; + const ALLSLToSLua::Result r = convert(lsl); + const auto at = [&r](const std::string& text) { return r.text.find(text); }; + ensure("the script's own at the top: " + r.text, has(r, "-- Door script by Someone.\n-- Do as you like with it.\n\n")); + ensure("over everything, once: " + r.text, at("-- Door script by Someone.") < at("local SWING") && count(r, "Door script") == 1); + ensure("a global's, what trailed it still after it: " + r.text, has(r, "-- How far it swings, in degrees\nlocal SWING = 90.0 -- a right angle\n")); + ensure("those inside a list, over it: " + r.text, has(r, "-- when it is open\n-- when it is shut\nlocal NAMES = {\"open\", \"closed\"}\n")); + ensure("a block comment as Luau's: " + r.text, has(r, "--[[ old code:\ninteger unused() { return 1; }\n]]\n")); + ensure("over the function it stood over: " + r.text, at("--[[ old code:") > at("local NAMES") && at("--[[ old code:") < at("-- Turns the door.")); + ensure("a brace's, over what it opens: " + r.text, has(r, "-- Turns the door.\n-- by degrees\nlocal function swing(by)\n")); + ensure("over a statement: " + r.text, has(r, " -- the rotation it turns by\n local r = ")); + ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_ROT_LOCAL, r * ll.GetLocalRot()}) -- turn\n")); + ensure("between branches, over the elseif: " + r.text, has(r, " -- when it shuts\n elseif by < 0 then\n")); + ensure("an empty branch's, in it: " + r.text, has(r, " if false then\n --[[ left off ]]\n elseif by == 0 then\n")); + ensure("at a block's end: " + r.text, has(r, " -- done\nend\n")); + ensure("a state's, over its handler: " + r.text, has(r, "-- on a touch\nLLEvents:on(\"touch_start\"")); + ensure("not a block of Luau's: " + r.text, has(r, " swing(SWING) -- [[ not a block of Luau's ]]\n")); + ensure("a level nothing in it closes: " + r.text, has(r, " --[=[ it ends ]] here ]=]\nend)")); + checksClean(r); + + ALLSLToSLua::Options none; + none.keepComments = false; + const ALLSLToSLua::Result bare = ALLSLToSLua::convert(lsl, none); + ensure("none where asked: " + bare.text, bare.converted && bare.text.find("Door script") == std::string::npos && + bare.text.find("degrees") == std::string::npos && bare.text.find("--[[") == std::string::npos); + } + + template<> template<> + void allsltoslua_object::test<38>() + { + set_test_name("anchors: each global, function, handler and statement's LSL line beside the line of SLua made of it, under the head, its notes and comments"); + const std::string lsl = "integer count = 0;\n" // 0 + "add(integer n)\n" // 1 + "{\n" // 2 + " count += n;\n" // 3 + "}\n" // 4 + "default\n" // 5 + "{\n" // 6 + " touch_start(integer d)\n" // 7 + " {\n" // 8 + " // one more\n" // 9 + " add(1);\n" // 10 + " llSay(0, (string)count);\n" // 11 + " }\n" // 12 + "}\n"; // 13 + const ALLSLToSLua::Result r = convert(lsl); + std::vector lines; + for (size_t from = 0; from <= r.text.size();) + { + const size_t cut = std::min(r.text.find('\n', from), r.text.size()); + lines.push_back(r.text.substr(from, cut - from)); + from = cut + 1; + } + for (const auto& [l, s] : r.anchors) + { + ensure("within both: " + std::to_string(l) + " " + std::to_string(s), l >= 0 && l < 14 && s >= 0 && s < static_cast(lines.size())); + } + const auto at = [&](S32 lsl_line) -> std::string { + for (const auto& [l, s] : r.anchors) + { + if (l == lsl_line) + { + return lines[static_cast(s)]; + } + } + return "(none)"; + }; + ensure("the global: " + at(0) + "\n" + r.text, at(0).find("local count") != std::string::npos); + ensure("the function: " + at(1), at(1).find("function add(") != std::string::npos); + ensure("its statement: " + at(3), at(3).find("count") != std::string::npos && at(3).find("n") != std::string::npos); + ensure("the handler: " + at(7), at(7).find("touch_start") != std::string::npos); + ensure("a call under its comment, not the comment: " + at(10), at(10).find("add(1)") != std::string::npos); + ensure("the next: " + at(11), at(11).find("ll.Say(0") != std::string::npos); + } + + template<> template<> + void allsltoslua_object::test<39>() + { + set_test_name("in fewer calls: an & asked whether it is nought by bit32.btest, values added together by one table.append, a key's default " + "the null key; and hexadecimal as written where SLua reads the same number"); + const ALLSLToSLua::Result r = convert("integer MASK = 0x0000FF00;\n" + "integer HIGH = 0xF0000000;\n" + "integer S_MASK = 0xF0000000;\n" + "integer BIG = 4294967296;\n" + "integer BIG_HEX = 0x100000000;\n" + "integer WRAPPED = 2147483648;\n" + "key gSitter;\n" + "default { touch_start(integer n) {\n" + " integer flags = llGetParcelFlags(llGetPos());\n" + " if (flags & PARCEL_FLAG_ALLOW_CREATE_OBJECTS) llOwnerSay(\"build\");\n" + " if (!(flags & MASK & 0x300)) llOwnerSay(\"none\");\n" + " if ((flags & 4) == 0) llOwnerSay(\"no 4\");\n" + " integer masked = flags & MASK;\n" + " integer top = flags & 0x80000000;\n" + " integer sign = flags & S_MASK; if (flags & ~S_MASK) sign = 0;\n" + " integer low = 0xFFFF0000; sign = sign | low;\n" + " list params = [PRIM_NAME, \"a\"];\n" + " params += [PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, llGetPos()];\n" + " params += n;\n" + " llSetLinkPrimitiveParamsFast(LINK_THIS, params);\n" + " float f = (float)\"1.5\";\n" + " llOwnerSay((string)(masked + HIGH + top + BIG + BIG_HEX + WRAPPED + sign) + (string)f + (string)gSitter);\n" + "} }\n"); + ensure("an & as a condition: " + r.text, has(r, "if bit32.btest(flags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then")); + ensure("not of one, an & of &s one call: " + r.text, has(r, "if not bit32.btest(flags, MASK, 0x300) then")); + ensure("against nought: " + r.text, has(r, "if not bit32.btest(flags, 4) then")); + ensure("an & as a number is still bit32.band: " + r.text, has(r, "local masked = bit32.band(flags, MASK)")); + ensure("one bit32 takes, as written, which it reads the same: " + r.text, has(r, "local top = bit32.s32(bit32.band(flags, 0x80000000))")); + ensure("values added together: " + r.text, + has(r, "table.append(params, PRIM_LINK_TARGET, LINK_ROOT, PRIM_POSITION, ll.GetPos())") && has(r, "table.insert(params, n)")); + ensure("the null key: " + r.text, has(r, "local gSitter = NULL_KEY") && !has(r, "uuid(\"\")")); + ensure("a float from a string: " + r.text, has(r, "lslFloat(\"1.5\")") && has(r, "return llcompat.List2Float({ s }, 0)")); + ensure("hexadecimal kept: " + r.text, has(r, "local MASK = 0x0000FF00")); + ensure("past 0x7FFFFFFF, which SLua reads as another number, as LSL had it: " + r.text, has(r, "local HIGH = -268435456")); + // As the grid's 32-bit hosts read them: strtoul stops at 0xFFFFFFFF. + ensure("past 32 bits, -1: " + r.text, has(r, "local BIG = -1\n") && has(r, "local BIG_HEX = -1\n")); + ensure("and said: " + r.text, noted(r, "SluaIntegerPast32Bits", "4294967296") && noted(r, "SluaIntegerPast32Bits", "0x100000000")); + ensure("a global only bit32 reads keeps its hexadecimal: " + r.text, has(r, "local S_MASK = 0xF0000000\n") && + has(r, "bit32.band(flags, S_MASK)")); + ensure("one read as a number does not: " + r.text, has(r, "local HIGH = -268435456")); + ensure("nor a local, never set, only bit32 reads: " + r.text, has(r, "local low = 0xFFFF0000\n")); + // Each said once, over the first place: what SLua reads such a + // number as, and what bit32.s32 is for. + const auto times = [&r](const char* key) { + return std::count_if(r.notes.begin(), r.notes.end(), [key](const ALScriptProblem& p) { return p.key == key; }); + }; + ensure("the hexadecimal's number said once, over the first: " + r.text, + times("SluaHexSign") == 1 && noted(r, "SluaHexSign", "0xF0000000") && + has(r, "-- LSL: SLua reads 0xF0000000 as 4026531840, where LSL wrapped it to -268435456: bit32 takes either the same, so it " + "stays as written where only bit32 reads it, and is -268435456 where it is read as a number.\nlocal HIGH = -268435456")); + ensure("bit32.s32 said once: " + r.text, times("SluaBit32Signed") == 1 && count(r, "-- LSL: bit32 answers 0 to 4294967295") == 1); + ensure("within them, wrapped: " + r.text, has(r, "local WRAPPED = -2147483648\n")); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<40>() + { + set_test_name("numeric for by a constant's name, and a counter its fors share; each function local but one called before it is written"); + const ALLSLToSLua::Result r = convert("integer ACTIVE_PROPS_STRIDE = 3;\n" + "list activeProps = [\"a\", 1, 2, \"b\", 3, 4];\n" + "setProps(list unattachedProps) {\n" + " integer i;\n" + " for (i = llGetListLength(activeProps) - ACTIVE_PROPS_STRIDE; i >= 0; i -= ACTIVE_PROPS_STRIDE) {\n" + " llOwnerSay((string)i);\n" + " }\n" + " for (i = 0; i < llGetListLength(unattachedProps); i++) {\n" + " llOwnerSay((string)i + llList2String(unattachedProps, i));\n" + " }\n" + " report();\n" + "}\n" + "report() { llOwnerSay(\"done\"); }\n" + "integer twice(integer n) { return n * 2; }\n" + "default { state_entry() { setProps([\"x\"]); llOwnerSay((string)twice(2)); } }\n"); + ensure("down by the constant's name: " + r.text, has(r, "for i = #activeProps - ACTIVE_PROPS_STRIDE, 0, -ACTIVE_PROPS_STRIDE do")); + ensure("the same counter's other for: " + r.text, has(r, "for i = 0, #unattachedProps - 1 do")); + ensure("the counter's declaration gone: " + r.text, !has(r, "local i = 0") && !has(r, "while i")); + ensure("only the one called first declared first: " + r.text, has(r, "local report\n") && has(r, "local function setProps(") && + has(r, "\nfunction report()") && has(r, "local function twice(")); + checksClean(r); + + const ALLSLToSLua::Result set = convert("integer gStep = 2;\n" + "default { state_entry() { integer i; for (i = 0; i < 10; i += gStep) llOwnerSay((string)i); gStep = 3; } }\n"); + ensure("a step by a global something sets keeps the while: " + set.text, has(set, "while i < 10 do")); + checksClean(set); + } + + template<> template<> + void allsltoslua_object::test<41>() + { + set_test_name("the script's comments as it laid them out: one after a statement still after it, rules of stars and slashes as dashes, " + "the //* toggle as ---[[, and an if on one line on one line"); + ALLSLToSLua::Options options; + options.types = true; + const ALLSLToSLua::Result r = ALLSLToSLua::convert("/***************************************/\n" + "/************** CONSTANTS **************/\n" + "/***************************************/\n" + "integer S_MASK = 0xF0000000; // -268435456\n" + "///////////////////////// DEBUGGING ///////////////////////////\n" + "integer gDebug = TRUE;\n" + "//*\n" + "string get(integer keey) {\n" + " return llLinksetDataRead(\"ARS#\" + (string)keey);\n" + "}\n" + "//*/\n" + "/*\n" + "string old() { return \"\"; }\n" + "//*/\n" + "integer allowed(integer parcelFlags) {\n" + " if (parcelFlags & PARCEL_FLAG_ALLOW_CREATE_OBJECTS) return TRUE; // may build\n" + " if (parcelFlags & 4)\n" + " return 2;\n" + " integer n = 3; // three\n" + " n += 1; /* and one */\n" + " return FALSE;\n" + "}\n" + "default { state_entry() { llOwnerSay(get(allowed(S_MASK)) + (string)gDebug); } }\n", + options); + ensure("converted", r.converted); + ensure("a rule of stars: " + r.text, has(r, "\n-----------------------------------------\n--------------- CONSTANTS ---------------\n")); + ensure("after its statement: " + r.text, has(r, "local S_MASK: number = -268435456 -- -268435456\n")); + ensure("a rule of slashes: " + r.text, has(r, "\n---------------------------------------------------------------\n") || + has(r, "------------------------- DEBUGGING ---------------------------\n")); + ensure("the toggle: " + r.text, has(r, "---[[\nlocal function get(") && has(r, "end\n\n--]]\n")); + ensure("the toggle the other way: " + r.text, has(r, "--[[\nstring old() { return \"\"; }\n--]]")); + ensure("an if on one line on one: " + r.text, + has(r, " if bit32.btest(parcelFlags, PARCEL_FLAG_ALLOW_CREATE_OBJECTS) then return 1 end -- may build\n")); + ensure("one on two lines on three: " + r.text, has(r, " if bit32.btest(parcelFlags, 4) then\n return 2\n end\n")); + ensure("after a declaration and a block comment after a step: " + r.text, + has(r, "local n: number = 3 -- three\n") && has(r, "n += 1 --[[ and one ]]\n")); + // The lines beside the LSL's past an if put on one line. + std::vector lines; + for (size_t from = 0; from <= r.text.size();) + { + const size_t cut = std::min(r.text.find('\n', from), r.text.size()); + lines.push_back(r.text.substr(from, cut - from)); + from = cut + 1; + } + const auto at = [&](S32 lsl_line) -> std::string { + for (const auto& [l, s] : r.anchors) + { + if (l == lsl_line && s >= 0 && s < static_cast(lines.size())) + { + return lines[static_cast(s)]; + } + } + return "(none)"; + }; + ensure("the if on one line beside its line: " + at(15), at(15).find("then return 1 end") != std::string::npos); + ensure("the next beside its own: " + at(16), at(16).find("if bit32.btest(parcelFlags, 4) then") != std::string::npos); + ensure("and those after: " + at(18) + " / " + at(19), at(18).find("local n: number = 3") != std::string::npos && at(19).find("n += 1") != std::string::npos); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<42>() + { + set_test_name("declarations the LSL lined up lined up again, their = and the comments after them; those it did not, as written"); + ALLSLToSLua::Options options; + options.types = true; + const ALLSLToSLua::Result r = ALLSLToSLua::convert("integer S_MASK = 0xF0000000; // the sign\n" + "integer ACTIVE_STRIDE = 3; // per prop\n" + "string PREFIX = \"ARS#\"; // linkset data\n" + "\n" + "integer a = 1;\n" + "string bee = \"b\";\n" + "default { state_entry() {\n" + " integer x = 1;\n" + " float yy = 2.5;\n" + " llOwnerSay(PREFIX + (string)(S_MASK + ACTIVE_STRIDE + a + x + yy) + bee);\n" + "} }\n", + options); + ensure("converted", r.converted); + ensure("globals lined up: " + r.text, has(r, "local S_MASK: number = -268435456 -- the sign\n" + "local ACTIVE_STRIDE: number = 3 -- per prop\n" + "local PREFIX: string = \"ARS#\" -- linkset data\n")); + ensure("not lined up, not lined up: " + r.text, has(r, "local a: number = 1\nlocal bee: string = \"b\"\n")); + ensure("locals lined up: " + r.text, has(r, "\nlocal x: number = 1\nlocal yy: number = 2.5\n")); + checksClean(r); + } } diff --git a/indra/alscript/tests/alluauconfig_test.cpp b/indra/alscript/tests/alluauconfig_test.cpp index d456340b61e..c21d89bc05d 100644 --- a/indra/alscript/tests/alluauconfig_test.cpp +++ b/indra/alscript/tests/alluauconfig_test.cpp @@ -24,8 +24,8 @@ #include "linden_common.h" -#include "../alluauconfig.h" -#include "../alscriptlintpass.h" +#include "../luau/alluauconfig.h" +#include "../lint/alscriptlintpass.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alluauservice_test.cpp b/indra/alscript/tests/alluauservice_test.cpp index fb6d6436326..2a671947286 100644 --- a/indra/alscript/tests/alluauservice_test.cpp +++ b/indra/alscript/tests/alluauservice_test.cpp @@ -24,11 +24,11 @@ #include "linden_common.h" -#include "../alluauservice.h" -#include "../alluauconfig.h" -#include "../alscriptfixes.h" -#include "../alscriptlintpass.h" -#include "../alselenefilters.h" +#include "../luau/alluauservice.h" +#include "../luau/alluauconfig.h" +#include "../lint/alscriptfixes.h" +#include "../lint/alscriptlintpass.h" +#include "../lint/alselenefilters.h" #include "../test/lltut.h" #include "llsdserialize.h" diff --git a/indra/alscript/tests/almessagemap_test.cpp b/indra/alscript/tests/almessagemap_test.cpp index 4c7889aa145..1ead8fe7031 100644 --- a/indra/alscript/tests/almessagemap_test.cpp +++ b/indra/alscript/tests/almessagemap_test.cpp @@ -24,8 +24,8 @@ #include "linden_common.h" -#include "../almessagemap.h" -#include "../alscriptproblem.h" +#include "../core/almessagemap.h" +#include "../core/alscriptproblem.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/almodulelook_test.cpp b/indra/alscript/tests/almodulelook_test.cpp index ef9cf3048ae..726888f1023 100644 --- a/indra/alscript/tests/almodulelook_test.cpp +++ b/indra/alscript/tests/almodulelook_test.cpp @@ -23,9 +23,9 @@ #include "linden_common.h" -#include "../almodulelook.h" +#include "../preprocessor/almodulelook.h" -#include "../alincludeidentity.h" +#include "../preprocessor/alincludeidentity.h" #include "fsyspath.h" #include "llfile.h" diff --git a/indra/alscript/tests/alnotecardformat_test.cpp b/indra/alscript/tests/alnotecardformat_test.cpp index af9d12f141e..c2fbb920a72 100644 --- a/indra/alscript/tests/alnotecardformat_test.cpp +++ b/indra/alscript/tests/alnotecardformat_test.cpp @@ -25,7 +25,7 @@ #include "linden_common.h" -#include "../alnotecardformat.h" +#include "../core/alnotecardformat.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alpreprocessor_test.cpp b/indra/alscript/tests/alpreprocessor_test.cpp index c62acac964c..ae913b1051c 100644 --- a/indra/alscript/tests/alpreprocessor_test.cpp +++ b/indra/alscript/tests/alpreprocessor_test.cpp @@ -25,8 +25,8 @@ #include "linden_common.h" -#include "../allslservice.h" -#include "../alpreprocessor.h" +#include "../lsl/allslservice.h" +#include "../preprocessor/alpreprocessor.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alpreprocessorc11_test.cpp b/indra/alscript/tests/alpreprocessorc11_test.cpp index 5815a7e1ed8..8fd4b1c1002 100644 --- a/indra/alscript/tests/alpreprocessorc11_test.cpp +++ b/indra/alscript/tests/alpreprocessorc11_test.cpp @@ -25,7 +25,7 @@ #include "linden_common.h" -#include "../alpreprocessor.h" +#include "../preprocessor/alpreprocessor.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscript_bench.cpp b/indra/alscript/tests/alscript_bench.cpp index 5b07dda9341..27d0a1e43f6 100644 --- a/indra/alscript/tests/alscript_bench.cpp +++ b/indra/alscript/tests/alscript_bench.cpp @@ -40,16 +40,16 @@ #include "linden_common.h" -#include "../aldiskcache.h" -#include "../aldiskincludes.h" -#include "../allslservice.h" -#include "../allsloptimizer.h" -#include "../alluauservice.h" -#include "../almessagemap.h" -#include "../alpreprocessor.h" -#include "../alscriptformatter.h" -#include "../alscriptstack.h" -#include "../alscriptweight.h" +#include "../preprocessor/aldiskcache.h" +#include "../preprocessor/aldiskincludes.h" +#include "../lsl/allslservice.h" +#include "../lsl/optimizer/allsloptimizer.h" +#include "../luau/alluauservice.h" +#include "../core/almessagemap.h" +#include "../preprocessor/alpreprocessor.h" +#include "../core/alscriptformatter.h" +#include "../core/alscriptstack.h" +#include "../core/alscriptweight.h" #include "albigscript.h" #include "llfile.h" diff --git a/indra/alscript/tests/alscriptenvelope_test.cpp b/indra/alscript/tests/alscriptenvelope_test.cpp index 376cdbd6f39..4ca4f8deb55 100644 --- a/indra/alscript/tests/alscriptenvelope_test.cpp +++ b/indra/alscript/tests/alscriptenvelope_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscriptenvelope.h" +#include "../preprocessor/alscriptenvelope.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptfixes_test.cpp b/indra/alscript/tests/alscriptfixes_test.cpp index 616292fe569..fff505d24d3 100644 --- a/indra/alscript/tests/alscriptfixes_test.cpp +++ b/indra/alscript/tests/alscriptfixes_test.cpp @@ -24,16 +24,16 @@ #include "linden_common.h" -#include "../alscriptfixes.h" -#include "../alscriptweight.h" +#include "../lint/alscriptfixes.h" +#include "../core/alscriptweight.h" -#include "../allslexports.h" -#include "../alluauexports.h" +#include "../lsl/allslexports.h" +#include "../luau/alluauexports.h" -#include "../allslservice.h" -#include "../alluauservice.h" -#include "../alpreprocessor.h" -#include "../allsloptimizer.h" +#include "../lsl/allslservice.h" +#include "../luau/alluauservice.h" +#include "../preprocessor/alpreprocessor.h" +#include "../lsl/optimizer/allsloptimizer.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index 56ffad8ded8..eabdf464302 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscriptformatter.h" +#include "../core/alscriptformatter.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptjoblane_test.cpp b/indra/alscript/tests/alscriptjoblane_test.cpp index 4ce098c5c56..cc19f76b356 100644 --- a/indra/alscript/tests/alscriptjoblane_test.cpp +++ b/indra/alscript/tests/alscriptjoblane_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscriptjoblane.h" +#include "../preprocessor/alscriptjoblane.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptlexicon_test.cpp b/indra/alscript/tests/alscriptlexicon_test.cpp index 256c08306f6..4e422abd7f5 100644 --- a/indra/alscript/tests/alscriptlexicon_test.cpp +++ b/indra/alscript/tests/alscriptlexicon_test.cpp @@ -25,7 +25,7 @@ #include "linden_common.h" -#include "../alscriptlexicon.h" +#include "../core/alscriptlexicon.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptlintpass_test.cpp b/indra/alscript/tests/alscriptlintpass_test.cpp index 871771cc5f1..c07baf0fdd4 100644 --- a/indra/alscript/tests/alscriptlintpass_test.cpp +++ b/indra/alscript/tests/alscriptlintpass_test.cpp @@ -24,11 +24,11 @@ #include "linden_common.h" -#include "../allslservice.h" -#include "../allsltoslua.h" -#include "../alluauservice.h" -#include "../alscriptfixes.h" -#include "../alscriptlintpass.h" +#include "../lsl/allslservice.h" +#include "../lsl/allsltoslua.h" +#include "../luau/alluauservice.h" +#include "../lint/alscriptfixes.h" +#include "../lint/alscriptlintpass.h" #include "../test/lltut.h" @@ -910,4 +910,32 @@ namespace tut ALScriptProblem::Severity::Note) .find("LuauLintSlGlobalInFunction") == std::string::npos); } + + template<> template<> + void object::test<24>() + { + set_test_name("SlIntegerPast32Bits: a whole number past 0xFFFFFFFF, decimal or hexadecimal, read as -1 and as 1 with a minus before it; " + "not one within 32 bits, a float, or one in a string or a comment"); + ensure("builtins: " + error, lslLoaded); + ensure_equals("LSL", found("integer a = 4294967296;\n" + "integer b = 0x100000000;\n" + "integer c = -4294967296;\n" + "integer d = 99999999999999999999999;\n" + "integer e = 4294967295; integer f = 0xFFFFFFFF; integer g = 2147483648;\n" + "float h = 4294967296.0; float i = 1e10; string j = \"4294967296\"; // 4294967296\n" + "default { state_entry() {\n" + " integer k = a - 4294967296;\n" + " vector v = <4294967296, 0, 0>;\n" + " llOwnerSay((string)(k + b + c + d + e + f + g) + (string)h + (string)i + j + (string)v);\n" + "} }\n", + "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, false), + std::string("0 LSLSlIntegerPast32Bits|4294967296|-1\n" + "1 LSLSlIntegerPast32Bits|0x100000000|-1\n" + "2 LSLSlIntegerPast32Bits|-4294967296|1\n" + "3 LSLSlIntegerPast32Bits|99999999999999999999999|-1\n" + "7 LSLSlIntegerPast32Bits|4294967296|-1\n" + "8 LSLSlIntegerPast32Bits|4294967296|-1\n")); + ensure_equals("not SLua's, whose numbers are doubles", found("local a = 4294967296\n", "SlIntegerPast32Bits", ALScriptProblem::Severity::Warning, true), + std::string()); + } } diff --git a/indra/alscript/tests/alscriptmessages_test.cpp b/indra/alscript/tests/alscriptmessages_test.cpp index b73c4b06a32..ccdd70a2ce8 100644 --- a/indra/alscript/tests/alscriptmessages_test.cpp +++ b/indra/alscript/tests/alscriptmessages_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscriptmessages.h" +#include "../core/alscriptmessages.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptnameindex_test.cpp b/indra/alscript/tests/alscriptnameindex_test.cpp index 641dae63a8e..0b719612790 100644 --- a/indra/alscript/tests/alscriptnameindex_test.cpp +++ b/indra/alscript/tests/alscriptnameindex_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscriptnameindex.h" +#include "../core/alscriptnameindex.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptsnapshot_test.cpp b/indra/alscript/tests/alscriptsnapshot_test.cpp index 23adf4a4cee..445e7fd98a7 100644 --- a/indra/alscript/tests/alscriptsnapshot_test.cpp +++ b/indra/alscript/tests/alscriptsnapshot_test.cpp @@ -23,7 +23,7 @@ #include "linden_common.h" -#include "../alscriptsnapshot.h" +#include "../preprocessor/alscriptsnapshot.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptstack_test.cpp b/indra/alscript/tests/alscriptstack_test.cpp index 9d9f85571ba..74771fb7f59 100644 --- a/indra/alscript/tests/alscriptstack_test.cpp +++ b/indra/alscript/tests/alscriptstack_test.cpp @@ -24,9 +24,9 @@ #include "linden_common.h" -#include "../alscriptstack.h" +#include "../core/alscriptstack.h" -#include "../alpreprocessor.h" +#include "../preprocessor/alpreprocessor.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscripttempfiles_test.cpp b/indra/alscript/tests/alscripttempfiles_test.cpp index 7500d041da4..eda560df85a 100644 --- a/indra/alscript/tests/alscripttempfiles_test.cpp +++ b/indra/alscript/tests/alscripttempfiles_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscripttempfiles.h" +#include "../core/alscripttempfiles.h" #include "fsyspath.h" #include "llfile.h" diff --git a/indra/alscript/tests/alscripttextcache_test.cpp b/indra/alscript/tests/alscripttextcache_test.cpp index 80b80320d60..cbea4350a1f 100644 --- a/indra/alscript/tests/alscripttextcache_test.cpp +++ b/indra/alscript/tests/alscripttextcache_test.cpp @@ -24,7 +24,7 @@ #include "linden_common.h" -#include "../alscripttextcache.h" +#include "../preprocessor/alscripttextcache.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alscriptweight_test.cpp b/indra/alscript/tests/alscriptweight_test.cpp index 76a939c17d9..c3ba519175e 100644 --- a/indra/alscript/tests/alscriptweight_test.cpp +++ b/indra/alscript/tests/alscriptweight_test.cpp @@ -24,10 +24,10 @@ #include "linden_common.h" -#include "../alscriptweight.h" +#include "../core/alscriptweight.h" -#include "../allslservice.h" -#include "../alsourcemap.h" +#include "../lsl/allslservice.h" +#include "../preprocessor/alsourcemap.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/alsourcemap_test.cpp b/indra/alscript/tests/alsourcemap_test.cpp index 0c7ff6fb538..186641fc0fe 100644 --- a/indra/alscript/tests/alsourcemap_test.cpp +++ b/indra/alscript/tests/alsourcemap_test.cpp @@ -23,7 +23,7 @@ #include "linden_common.h" -#include "../alsourcemap.h" +#include "../preprocessor/alsourcemap.h" #include "../test/lltut.h" diff --git a/indra/alscript/tests/optimizer/test_lso_expected.lsl b/indra/alscript/tests/optimizer/test_lso_expected.lsl index 05d7d7da3bd..6d72b06c335 100644 --- a/indra/alscript/tests/optimizer/test_lso_expected.lsl +++ b/indra/alscript/tests/optimizer/test_lso_expected.lsl @@ -2,6 +2,6 @@ default { state_entry() { - llSay(0, "5" + "0.300000"); + llSay(0, "50.300000"); } } diff --git a/indra/alscript/tests/optimizer/test_shrink_expected.lsl b/indra/alscript/tests/optimizer/test_shrink_expected.lsl index 5e97c3c865d..b0417d08cc2 100644 --- a/indra/alscript/tests/optimizer/test_shrink_expected.lsl +++ b/indra/alscript/tests/optimizer/test_shrink_expected.lsl @@ -14,16 +14,14 @@ default touch_start(integer f) { integer d; - for (d = 0; d < f; d = d + 1) + for (d = 0; d < f; ++d) { - b = b + 1; + ++b; c((string)b); } @g; - if (b > 100) - jump h; - jump g; - @h; + if (b < 101) + jump g; state a; } } diff --git a/indra/alscript/tests/optimizer/test_vendor_expected.lsl b/indra/alscript/tests/optimizer/test_vendor_expected.lsl index 019c2b89ad9..7a7db92e947 100644 --- a/indra/alscript/tests/optimizer/test_vendor_expected.lsl +++ b/indra/alscript/tests/optimizer/test_vendor_expected.lsl @@ -9,18 +9,18 @@ default { state_entry() { - integer i = 0; + integer i; llSetText(describe(), <1, 1, 1>, 1); while (i < 3) { - i = i + 1; + ++i; } } touch_start(integer total) { - listener = llListen(-12345, "", llDetectedKey(0), ""); - llDialog(llDetectedKey(0), describe(), BUTTONS, -12345); + listener = llListen(((integer)-12345), "", llDetectedKey(0), ""); + llDialog(llDetectedKey(0), describe(), BUTTONS, ((integer)-12345)); if (total) llSetTimerEvent(30); } @@ -32,7 +32,7 @@ default llSay(0, "Thanks!"); return; } - else if (message == "Info") + if (message == "Info") llSay(0, describe()); llListenRemove(listener); llSetTimerEvent(0); diff --git a/indra/cmake/ConfigurationReport.cmake b/indra/cmake/ConfigurationReport.cmake index 9311bd01592..188f0ed0026 100644 --- a/indra/cmake/ConfigurationReport.cmake +++ b/indra/cmake/ConfigurationReport.cmake @@ -351,6 +351,11 @@ function(al_configuration_report) _al_report_row("Python" "not found; the tests that spawn a Python peer are disabled") endif() + if(AL_BUILD_TESTS) + get_property(lscript_status GLOBAL PROPERTY AL_LSCRIPT_STATUS) + _al_report_row("LL's compiler" "${lscript_status}") + endif() + if(LL_GENERATOR_IS_MULTI_CONFIG) _al_report_row("Staging" "${INDRA_BINARY_DIR}/newview/") elseif(DARWIN) diff --git a/indra/llui/alchoicelist.cpp b/indra/llui/alchoicelist.cpp index 079f7bcf4df..d5a387280a2 100644 --- a/indra/llui/alchoicelist.cpp +++ b/indra/llui/alchoicelist.cpp @@ -26,6 +26,7 @@ #include "alchoicelist.h" +#include "lllocalcliprect.h" #include "llrender.h" #include "lluicolortable.h" #include "lluictrlfactory.h" @@ -218,6 +219,9 @@ void ALChoiceList::drawBeforeRows(const LLRect& text) const LLRect local = getLocalRect(); const S32 top = screenTopOf(text, mChosen, 0); const S32 h = layout().rowHeightOf(mChosen, 0); + // Drawn before the text's clip, and the list scrolls the chosen row + // partly or wholly out of sight: only what of it is in the text. + LLLocalClipRect clip(LLRect(local.mLeft + 1, text.mTop, local.mRight - 1, text.mBottom)); gl_rect_2d(local.mLeft + 1, top, local.mRight - 1, top - h, selectionColor() % alpha); } diff --git a/indra/llui/alcodeeditor.cpp b/indra/llui/alcodeeditor.cpp index f2e35cab2f4..26a919a70db 100644 --- a/indra/llui/alcodeeditor.cpp +++ b/indra/llui/alcodeeditor.cpp @@ -236,9 +236,10 @@ void ALCodeEditor::onEdit(const ALTextDocument::Edit& edit) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; hideCard(); - // Numbers and tints were for the text they were given with. + // Numbers, tints and spacers were for the text they were given with. mLineNumbers.clear(); setLineTints({}); + setSpacerLines({}); // Each run of lines an edit replaced -- one, or a batch's several. const std::vector& spans = edit.lineSpans(); const S32 lines = document().lineCount(); @@ -1426,6 +1427,9 @@ void ALCodeEditor::drawBeforeRows(const LLRect& text) S32 row; layout().xOf(caret().line, caret().column, &row); const S32 top = screenTopOf(text, caret().line, row); + // Drawn before the text's clip, and the caret's line may be + // scrolled partly or wholly out of sight. + LLLocalClipRect clip(text); gl_rect_2d(text.mLeft, top, text.mRight, top - layout().rowHeight(), currentLineColor() % alpha); } drawGutter(text, alpha); diff --git a/indra/llui/aldiffview.cpp b/indra/llui/aldiffview.cpp index 4bcad450562..845c6cfec90 100644 --- a/indra/llui/aldiffview.cpp +++ b/indra/llui/aldiffview.cpp @@ -43,15 +43,18 @@ namespace LLColor4 colorOf(const char* name, const LLColor4& otherwise) { return LLUIColorTable::instance().getColor(name, otherwise).get(); } // One side's text as it is shown: its lines, each's number (0 for a - // line put in to line the sides up), its tint, and the words marked. + // line put in to line the sides up), its tint, the words marked, and + // which lines are only there to line the sides up, which a copy leaves + // out. struct Shown { std::string text; std::vector numbers; std::vector tints; std::vector words; + std::vector spacers; - S32 add(const std::string& line, S32 number, const LLColor4& tint) + S32 add(const std::string& line, S32 number, const LLColor4& tint, bool spacer = false) { if (!numbers.empty()) { @@ -60,6 +63,7 @@ namespace text += line; numbers.push_back(number); tints.push_back(tint); + spacers.push_back(spacer); return static_cast(numbers.size()) - 1; } @@ -81,6 +85,7 @@ namespace editor.setText(text); editor.setLineNumbers(std::move(numbers)); editor.setLineTints(std::move(tints)); + editor.setSpacerLines(std::move(spacers)); editor.setDecorations(std::move(words)); } }; @@ -141,10 +146,11 @@ ALCodeEditor* ALDiffView::makeSide(const ALCodeEditor::Params& side, const std:: return made; } -void ALDiffView::setTexts(std::string_view left, std::string_view right) +void ALDiffView::setTexts(std::string_view left, std::string_view right, const ALTextDiff::anchors_t& anchors) { mLeftText = std::string(left); mRightText = std::string(right); + mAnchors = anchors; rebuild(); } @@ -199,7 +205,7 @@ void ALDiffView::rebuild() { const std::vector left = ALTextDiff::split(mLeftText); const std::vector right = ALTextDiff::split(mRightText); - const std::vector runs = ALTextDiff::lines(left, right); + const std::vector runs = mAnchors.empty() ? ALTextDiff::lines(left, right) : ALTextDiff::lines(left, right, mAnchors); const LLColor4 none(0.f, 0.f, 0.f, 0.f); const LLColor4 out = colorOf("CodeDiffRemovedColor", LLColor4(0.85f, 0.25f, 0.25f, 0.18f)); const LLColor4 in = colorOf("CodeDiffAddedColor", LLColor4(0.25f, 0.75f, 0.35f, 0.18f)); @@ -243,8 +249,8 @@ void ALDiffView::rebuild() { const bool has_out = n < gone.size(); const bool has_in = n < made.size(); - const S32 lrow = has_out ? ls.add(left[static_cast(gone[n])], gone[n] + 1, out) : ls.add(std::string(), 0, padding); - const S32 rrow = has_in ? rs.add(right[static_cast(made[n])], made[n] + 1, in) : rs.add(std::string(), 0, padding); + const S32 lrow = has_out ? ls.add(left[static_cast(gone[n])], gone[n] + 1, out) : ls.add(std::string(), 0, padding, true); + const S32 rrow = has_in ? rs.add(right[static_cast(made[n])], made[n] + 1, in) : rs.add(std::string(), 0, padding, true); if (has_out && has_in) { ALTextDiff::spans_t lspans; @@ -274,6 +280,8 @@ void ALDiffView::rebuild() is.mark(first_in + static_cast(n), rspans, in_words); } } + mRightRows = rs.numbers; + mInlineRows = is.numbers; ls.into(*mLeft); rs.into(*mRight); is.into(*mInlined); @@ -360,8 +368,67 @@ bool ALDiffView::goToChange(bool forward) return true; } +std::pair ALDiffView::rightAtCaret() const +{ + const ALCodeEditor* side = shown(); + const std::vector& rows = side == mInlined ? mInlineRows : mRightRows; + const S32 row = side->caret().line; + if (rows.empty()) + { + return { 0, 0 }; + } + const S32 at = llclamp(row, 0, static_cast(rows.size()) - 1); + if (rows[static_cast(at)] > 0) + { + // The column where the caret stands in the right's own text: the + // right side's, or a line inline that the right has. + return { rows[static_cast(at)] - 1, side == mLeft ? 0 : side->caret().column }; + } + for (S32 n = at + 1; n < static_cast(rows.size()); ++n) + { + if (rows[static_cast(n)] > 0) + { + return { rows[static_cast(n)] - 1, 0 }; + } + } + for (S32 n = at - 1; n >= 0; --n) + { + if (rows[static_cast(n)] > 0) + { + return { rows[static_cast(n)] - 1, 0 }; + } + } + return { 0, 0 }; +} + +bool ALDiffView::handleUnicodeCharHere(llwchar uni_char) +{ + // A character the side in front would not take, being read only. + if (mEdit && uni_char >= 0x20 && uni_char != 0x7F) + { + const auto [line, column] = rightAtCaret(); + if (LLView* to = mEdit(line, column)) + { + return to->handleUnicodeChar(uni_char, true); + } + } + return LLUICtrl::handleUnicodeCharHere(uni_char); +} + bool ALDiffView::handleKeyHere(KEY key, MASK mask) { + // A key the side in front would not take, being read only, that + // changes the text: a line broken or joined, a tab, a paste or a cut. + const bool edits = ((key == KEY_RETURN || key == KEY_BACKSPACE || key == KEY_DELETE || key == KEY_TAB) && (mask == MASK_NONE || mask == MASK_SHIFT)) || + ((key == 'V' || key == 'X') && mask == MASK_CONTROL); + if (mEdit && edits) + { + const auto [line, column] = rightAtCaret(); + if (LLView* to = mEdit(line, column)) + { + return to->handleKey(key, mask, true); + } + } if (key == KEY_F7 && (mask == MASK_NONE || mask == MASK_SHIFT)) { goToChange(mask == MASK_NONE); diff --git a/indra/llui/aldiffview.h b/indra/llui/aldiffview.h index f56ae61bc6d..2468820c3d3 100644 --- a/indra/llui/aldiffview.h +++ b/indra/llui/aldiffview.h @@ -26,6 +26,7 @@ #define AL_ALDIFFVIEW_H #include "alcodeeditor.h" +#include "altextdiff.h" #include "alviewtype.h" #include "lluictrl.h" @@ -46,7 +47,8 @@ class LLTextBox; // words that changed are marked. Each side has a title over it. // // F7 goes to the next change and Shift-F7 to the one before; Escape tells -// whoever shows it, to put back what was there. +// whoever shows it, to put back what was there. What is typed in it, which +// it cannot take, goes to whoever shows it, where it says. class ALDiffView : public LLUICtrl { public: @@ -66,8 +68,9 @@ class ALDiffView : public LLUICtrl ~ALDiffView() override; // The texts, the left the one taken from, the right the one made, and - // what each is. - void setTexts(std::string_view left, std::string_view right); + // what each is; lined up where lines are known to stand for each other + // (ALTextDiff's anchors), however they differ. + void setTexts(std::string_view left, std::string_view right, const ALTextDiff::anchors_t& anchors = {}); void setTitles(const std::string& left, const std::string& right); void setSyntax(const std::string& syntax); // The grammar both sides are coloured by, as another view has it. @@ -92,9 +95,20 @@ class ALDiffView : public LLUICtrl // Told when Escape is pressed in it. void setOnEscape(std::function escape) { mEscape = std::move(escape); } + // Told when what is typed in it would change the right's text -- a + // character, a line broken or joined, a paste -- with where, as a line + // and column of the right's text; answered with the view to type in + // instead, or nothing where the typing goes nowhere. + typedef std::function edit_t; + void setOnEdit(edit_t edit) { mEdit = std::move(edit); } + // The line of the right's text the caret of the side in front is on, + // and its column there: the row's, or where the row has none -- a + // line taken out, a gap -- the nearest line after it, else before. + std::pair rightAtCaret() const; void reshape(S32 width, S32 height, bool called_from_parent = true) override; bool handleKeyHere(KEY key, MASK mask) override; + bool handleUnicodeCharHere(llwchar uni_char) override; void draw() override; protected: @@ -108,6 +122,11 @@ class ALDiffView : public LLUICtrl std::string mLeftText; std::string mRightText; + ALTextDiff::anchors_t mAnchors; + // Each row's line of the right, side by side and inline, counted from + // one; nought where the row has none. + std::vector mRightRows; + std::vector mInlineRows; std::string mLeftTitle; std::string mRightTitle; bool mInline = false; @@ -123,6 +142,7 @@ class ALDiffView : public LLUICtrl S32 mScrolledY = 0; F32 mScrolledX = 0.f; std::function mEscape; + edit_t mEdit; }; #endif // AL_ALDIFFVIEW_H diff --git a/indra/llui/altextdiff.cpp b/indra/llui/altextdiff.cpp index 0a6e3e8a4b5..8c1f1f4cccb 100644 --- a/indra/llui/altextdiff.cpp +++ b/indra/llui/altextdiff.cpp @@ -32,6 +32,7 @@ #include #include +#include namespace { @@ -244,6 +245,109 @@ std::vector ALTextDiff::lines(const std::vector& l return runs(a, b); } +// static +std::vector ALTextDiff::lines(const std::vector& left, const std::vector& right, const anchors_t& anchors) +{ + // The pairs kept: within both texts, by the right then the left the + // other way, so that of two on one line of the right the longest + // rising run takes one at most; then that run, rising on the left. + anchors_t given; + for (const std::pair& pair : anchors) + { + if (pair.first >= 0 && pair.second >= 0 && pair.first < static_cast(left.size()) && pair.second < static_cast(right.size())) + { + given.push_back(pair); + } + } + std::sort(given.begin(), given.end(), [](const std::pair& a, const std::pair& b) { + return a.second != b.second ? a.second < b.second : a.first > b.first; + }); + std::vector tails; // the pair ending the best run of each length + std::vector before(given.size(), std::numeric_limits::max()); + for (size_t i = 0; i < given.size(); ++i) + { + const auto at = std::lower_bound(tails.begin(), tails.end(), given[i].first, + [&given](size_t tail, S32 first) { return given[tail].first < first; }); + if (at != tails.begin()) + { + before[i] = *(at - 1); + } + if (at == tails.end()) + { + tails.push_back(i); + } + else + { + *at = i; + } + } + anchors_t kept; + for (size_t i = tails.empty() ? std::numeric_limits::max() : tails.back(); i != std::numeric_limits::max(); i = before[i]) + { + kept.push_back(given[i]); + } + std::reverse(kept.begin(), kept.end()); + + std::vector out; + const auto push = [&out](const Run& run) { + if (run.count <= 0 && run.kind != Kind::Same) + { + return; + } + if (run.count > 0 && !out.empty() && out.back().kind == run.kind && out.back().count > 0) + { + Run& last = out.back(); + const bool next = run.kind == Kind::Same ? last.left + last.count == run.left && last.right + last.count == run.right + : run.kind == Kind::Removed ? last.left + last.count == run.left && last.right == run.right + : last.right + last.count == run.right && last.left == run.left; + if (next) + { + last.count += run.count; + return; + } + } + out.push_back(run); + }; + S32 l = 0; + S32 r = 0; + for (size_t i = 0; i <= kept.size(); ++i) + { + const S32 to_left = i < kept.size() ? kept[i].first : static_cast(left.size()); + const S32 to_right = i < kept.size() ? kept[i].second : static_cast(right.size()); + // The stretch before the pair, on its own. + const std::vector some_left(left.begin() + l, left.begin() + to_left); + const std::vector some_right(right.begin() + r, right.begin() + to_right); + for (Run run : lines(some_left, some_right)) + { + run.left += l; + run.right += r; + push(run); + } + if (i == kept.size()) + { + break; + } + if (left[static_cast(to_left)] == right[static_cast(to_right)]) + { + push(Run{ Kind::Same, to_left, to_right, 1 }); + } + else + { + // Parted from a change just before, so that the pair stands + // first in its own. + if (!out.empty() && out.back().kind != Kind::Same) + { + out.push_back(Run{ Kind::Same, to_left, to_right, 0 }); + } + push(Run{ Kind::Removed, to_left, to_right, 1 }); + push(Run{ Kind::Added, to_left + 1, to_right, 1 }); + } + l = to_left + 1; + r = to_right + 1; + } + return out; +} + // static std::vector ALTextDiff::split(std::string_view text) { diff --git a/indra/llui/altextdiff.h b/indra/llui/altextdiff.h index cae5a891363..2f3dc963561 100644 --- a/indra/llui/altextdiff.h +++ b/indra/llui/altextdiff.h @@ -72,6 +72,17 @@ class ALTextDiff static constexpr S32 MOST_CHANGES = 1000; static std::vector lines(const std::vector& left, const std::vector& right); + // Lines known to stand for each other -- an LSL statement and the SLua + // it was written as -- each a line of the left and one of the right, + // counted from nought. Lined up whatever they say: each pair kept beside + // each other, and the stretches between them compared on their own. + // Only pairs in order on both sides can be kept: of those given, the + // most that are, and none outside either text. + typedef std::vector> anchors_t; + // As lines(), lined up at the anchors. A pair that differs is a change + // of its own, the first lines of it the pair's; a Same run of no lines + // goes before it, to part it from a change just before. + static std::vector lines(const std::vector& left, const std::vector& right, const anchors_t& anchors); // A text's lines, for lines(). static std::vector split(std::string_view text); diff --git a/indra/llui/altextview.cpp b/indra/llui/altextview.cpp index 0c61961e8c2..1e03b6daf2c 100644 --- a/indra/llui/altextview.cpp +++ b/indra/llui/altextview.cpp @@ -3682,7 +3682,11 @@ void ALTextView::copy() { text += '\n'; } - text += one.empty() ? mDocument.line(one.end.line) : mDocument.text(one); + if (one.empty() && spacerLine(one.end.line)) + { + continue; + } + text += one.empty() ? mDocument.line(one.end.line) : copiedText(one); any = true; } if (any) @@ -3694,7 +3698,7 @@ void ALTextView::copy() } if (!hasSelection()) { - if (!mClipsLines) + if (!mClipsLines || spacerLine(mCaret.line)) { return; } @@ -3708,6 +3712,40 @@ void ALTextView::copy() LLClipboard::instance().copyToClipboard(text, 0, static_cast(text.size())); } +std::string ALTextView::copiedText(const ALTextRange& range) const +{ + if (mSpacerLines.empty()) + { + return mDocument.text(range); + } + // Each line's part that is in the range, and its break where the range + // goes on past it and the text has a line after it: a spacer gives + // neither, and the text's last line has no break of its own to give, + // whatever spacers are shown after it. + const ALTextRange ordered = range.normalised(); + S32 last = mDocument.lineCount() - 1; + while (last > 0 && spacerLine(last)) + { + --last; + } + std::string text; + for (S32 line = ordered.begin.line; line <= ordered.end.line; ++line) + { + if (spacerLine(line)) + { + continue; + } + const S32 from = line == ordered.begin.line ? ordered.begin.column : 0; + const S32 to = line == ordered.end.line ? ordered.end.column : static_cast(mDocument.line(line).size()); + text += mDocument.text(ALTextRange(ALTextPos(line, from), ALTextPos(line, to))); + if (line < ordered.end.line && line < last) + { + text += '\n'; + } + } + return text; +} + bool ALTextView::canPaste() const { return !mReadOnly && LLClipboard::instance().isTextAvailable(); @@ -5194,6 +5232,8 @@ void ALTextView::draw() // A tint behind each line that has one, under everything else. if (!mLineTints.empty()) { + // The first and last rows in sight may be partly out of it. + LLLocalClipRect clip(text); const S32 row_h = layout().rowHeight(); forEachVisibleRow(text, [&](S32 line, S32, S32 screen_top) { if (line < static_cast(mLineTints.size()) && mLineTints[static_cast(line)].mV[VALPHA] > 0.f) diff --git a/indra/llui/altextview.h b/indra/llui/altextview.h index d876de90faa..822a3c8daad 100644 --- a/indra/llui/altextview.h +++ b/indra/llui/altextview.h @@ -342,7 +342,10 @@ class ALTextView : public LLUICtrl, public LLEditMenuHandler, public LLSpellChec ALTextRange selection() const { return ALTextRange(mAnchor, mCaret); } bool hasSelection() const { return mAnchor != mCaret; } void setSelection(const ALTextRange& range); - std::string selectedText() const { return mDocument.text(selection()); } + // The selection's text, as a copy takes it: but for spacer lines. + std::string selectedText() const { return copiedText(selection()); } + // A range's text but for the spacer lines in it, each with its break. + std::string copiedText(const ALTextRange& range) const; // The identifier the caret is at the end of: letters, digits and // underscores back from the caret. Empty at anything else. std::string wordBeforeCaret() const; @@ -506,6 +509,14 @@ class ALTextView : public LLUICtrl, public LLEditMenuHandler, public LLSpellChec // For a text shown rather than edited -- a side of a diff. void setLineTints(std::vector tints) { mLineTints = std::move(tints); } const std::vector& lineTints() const { return mLineTints; } + // Lines shown that are no part of the text -- the empty rows a diff + // lines its sides up with -- which a copy leaves out: each one a line, + // from the first, true for a spacer. + void setSpacerLines(std::vector spacers) { mSpacerLines = std::move(spacers); } + bool spacerLine(S32 line) const + { + return line >= 0 && line < static_cast(mSpacerLines.size()) && mSpacerLines[static_cast(line)]; + } // --- atoms --------------------------------------------------------------- @@ -1368,6 +1379,7 @@ class ALTextView : public LLUICtrl, public LLEditMenuHandler, public LLSpellChec ALAnchoredRanges mAtoms; ALAnchoredRanges